fix: Summary quantiles collapsing for targeted quantiles with 2*epsilon >= 1-quantile - #2396
Open
olegkovalenko wants to merge 1 commit into
Open
Conversation
olegkovalenko
requested review from
dhoard,
fstab,
jaydeluca and
zeitlinger
as code owners
August 19, 2026 13:08
…on >= 1-quantile Fixes prometheus#2292. CKMSQuantiles returned values from far below the requested quantile for quantile configurations such as (0.9, 0.05) or (0.99, 0.005) - often the minimum of all observations, regardless of the input data. Interacting root causes, all stemming from the error function f() being of order n-r below a target quantile when 2*epsilon >= 1-quantile: 1. compress(): a single sample was allowed to span all ranks from r to n, so compress() merged away the samples that hold the information needed to answer the quantile query. With quantiles {(0.9, 0.05), (0.99, 0.005)} the sample list collapsed to 3 samples. 2. insertBefore(): freshly inserted samples get delta = f(r) - 1, so below a target their possible-rank intervals are centered near rank n regardless of the sample's actual position, making them indistinguishable from genuine samples near the target. 3. get(): the scan stopped at the first sample with r + g + delta > desiredRank + f(desiredRank)/2 and returned the value of the sample before it; a single wide sample (see 2., and get() flushes the buffer right before scanning, so such samples are always present) made the scan stop far before the target rank. The fix bounds sample widths by maxWidthNotCrossingTargets(r) in addition to f(r) at both places where widths are created - merging in compress() and delta assignment in insertBefore() - so that every target quantile keeps enough resolution around its accuracy window [quantile*n - epsilon*n, quantile*n + epsilon*n]. The bound is anchored at the window's start with a floor of 2*epsilon*n so that it does not degenerate for targets with quantile + epsilon >= 1 (window end == n), e.g. (0.99, 0.01) or (0.95, 0.05). get() returns the value of the sample whose possible rank interval is centered closest to the desired rank, which cannot be derailed by a single wide sample. Verified against exact percentiles on 3720 test cases (31 quantiles across 13 configurations x 6 distributions x 2 sizes x 10 seeds): worst rank error 1.75 * epsilon, no case above 2 * epsilon. Before the fix the worst rank error was 330 * epsilon. Also includes the deterministic regression case from the review of PR prometheus#2316 (values 1..10,000 shuffled with seed 2, single quantile (0.99, 0.005)), which this fix passes, plus regression tests for the quantile + epsilon >= 1 family and for descending input order. Signed-off-by: Oleg Kovalenko <okovalenko@evolution.com>
olegkovalenko
force-pushed
the
fix-targeted-quantile-collapse
branch
from
August 19, 2026 13:10
fbed814 to
2e86dac
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #2292. Alternative to #2316, addressing the issues raised in its review.
Problem
For targeted quantile configurations with
2*epsilon >= 1 - quantile— e.g.(0.9, 0.05)or(0.99, 0.005), both taken from real-world configurations —Summaryreported values from far below the requested quantile, often the minimum of all observations, regardless of the input data.The root causes all stem from the same property: below a target quantile, the CKMS error function
f(r) = 2*epsilon*(n-r)/(1-q)is of ordern-rwhen2*epsilon >= 1-q. Three things break:compress()destroys the sketch. A single sample may span all ranks fromrton, socompress()merges away the samples that hold the information needed to answer the quantile query. With{(0.9, 0.05), (0.99, 0.005)}the sample list collapsed to 3 samples no matter how many values were inserted.insertBefore()assigns misleading deltas. Freshly inserted samples getdelta = f(r) - 1, so below a target their possible-rank intervals are centered near ranknregardless of the sample's actual position — indistinguishable from genuine samples near the target.get()stops too early. The scan stopped at the first sample withr + g + delta > desiredRank + f(desiredRank)/2and returned the value of the sample before it; a single wide fresh sample (always present, sinceget()flushes the buffer right before scanning) tripped it far before the target rank.Fix
Sample widths are additionally bounded by
maxWidthNotCrossingTargets(r)at both places where widths are created — merging incompress()and delta assignment ininsertBefore(), via a sharedeffectiveMaxWidth(r)— so every target quantile keeps enough resolution around its accuracy window[q*n - eps*n, q*n + eps*n]: below a window a sample may extend at mostmax(windowStart - r, 2*eps*n)— it can intrude into the window but never reach the window's end — and any sample overlapping a window has width at most the window's size2*eps*n. So no single sample can span a whole window, and the center of a sample's possible-rank interval is withineps*nof any rank the sample covers inside the window. The bound is anchored at the window's start so it does not degenerate for targets withquantile + epsilon >= 1(e.g.(0.99, 0.01),(0.95, 0.05)), where the window's end is ranknand an end-anchored bound would be no constraint at all. For configurations with2*epsilon < 1-quantilethe bound is larger thanf()near the target, so behavior there is mostly unchanged.get()returns the value of the sample whose possible rank interval[r+g, r+g+delta]is centered closest to the desired rank, which cannot be derailed by a single wide sample.Relation to #2316 and its review
#2316 diagnoses the compress and get parts but bounds
compress()differently (minimum of the error function over the merged interval) and leaves insert-time deltas unbounded. The review found a deterministic counterexample: single quantile(0.99, 0.005), values 1..10,000 shuffled withRandom(2)— #2316 returns 9784, outside the allowed[9800, 10000].This fix passes that case (returns 9940), and it is included as a regression test (
testSingleTargetedQuantileSmallN).On the selection rule in
get(): the center of a sample's possible rank interval is its best point estimate of rank, and with the width bound applied at insert and merge time, samples near a target cannot be wide, so the estimate is tight exactly where it matters. This is backed by the verification below rather than a formal proof — happy to discuss if a proof-oriented selection is preferred.Verification
Verified against exact percentiles on 3,720 test cases (31 quantiles across 13 configurations × 6 distributions — uniform, descending, heavy-tail from a production latency CDF, exponential, lognormal, gaussian — × 2 sizes × 10 seeds): worst rank error
1.75 * epsilon, no case above2 * epsilon. Before the fix the worst rank error was330 * epsilon. The evaluation harness (including instrumented variants of the query rule and the width bound that were compared before settling on this fix) is available at https://gist.github.com/olegkovalenko/83c58835a1357d3450e6538f89e2cda7.Additional targeted sweeps with values
1..nshuffled (true rank = value), pass = rank error<= 2*epsilon, across n ∈ {1k, 10k, 100k} × 50 seeds:(0.99, 0.005), n=10k, seed 2(0.99, 0.005)× 150(0.9, 0.05)+(0.99, 0.005)× 300(0.5, 0.05)+(0.9, 0.01)+(0.99, 0.001)× 450(0.9, 0.06)(strictly above boundary) × 150(0.99, 0.01)(window end = n) × 150(0.95, 0.05)(window end = n) × 150Memory impact of the width bound is a handful of extra samples on the affected configurations (e.g.
(0.99, 0.005): 3–4 → 6–11 samples after 1M inserts); well-behaved configurations such as(0.5, 0.05)(0.9, 0.01)(0.99, 0.001)are unchanged (37–40 samples).Tests
testTargetedQuantilesDoNotCollapse— the original reproducer from Summary quantiles collapse to the minimum observation when 2·epsilon ≥ 1−quantile #2292testSingleTargetedQuantileDoesNotCollapse— single targeted quantile at the boundarytestTargetedQuantilesWithMedian— collapse still occurred with a well-behaved quantile addedtestSingleTargetedQuantileSmallN— the deterministic small-n case from the fix: prevent Summary quantiles from collapsing to the minimum observation #2316 reviewtestTargetedQuantileWindowReachingMaximum— the degeneratequantile + epsilon >= 1familytestTargetedQuantilesDescendingInput— descending input order, the worst case for these configurationsAll assert the
2*epsilonrank bound viavalidateResultson values1..ninserted in the respective order (value = true rank). All existingCKMSQuantilesTestcases pass unchanged (22 tests), as does the fullprometheus-metrics-coresuite (162 tests).