Repository navigation
feat(planner): certify UnivMon L2 and distinct count on the certifiable sketchlib estimators - #702
Merged
Merged
Conversation
The workspace pins ProjectASAP/asap_sketchlib 831a504, which combines the unmerged upstream PRs #221 (unbiased calc_card, layer-0 calc_l2, top_layer_complete, get_l2_sqr) and #220 (rows * (ceil(log2 cols) + 1) <= 128 and at most 19 layers, enforced at construction). The executor pinned 5f03ccb for the Float64 weighted frequency kernels (upstream #160, also unmerged), which 831a504 lacks. It now pins e6b0314, the merge of 5f03ccb into 831a504, so its UnivMon readouts use the same estimators the planner certifies. Both pins move to upstream main once those PRs merge.
asap_sketchlib now panics on a UnivMon whose hashes would not be independent: more than 19 layers, or rows * (ceil(log2 cols) + 1) > 128 (each row reads its column and sign bits from one 128-bit hash). check_dimensions mirrors both, so such a shape is an error rather than a panic, and the capability check uses the same function instead of its own copy with the old 64-layer limit.
The CountSketch guarantee bounded the median's failure with Hoeffding's exp(-depth/18), which assumes each row fails with probability 1/3. A row fails with probability q/eps^2 = q*w/3 at eps = sqrt(3/w), where q is the chance two keys share a column. asap_sketchlib takes a column as a ceil(log2 w)-bit hash slice modulo w, so q = 1/w only for a power-of-two width and q <= 9/(8w) otherwise: p = 3/8 there, and the old slack understated delta for every non-power-of-two width. The failure probability is now the exact tail P(Bin(depth, p) >= (depth+1)/2), and count_sketch_depth(width, delta) picks the smallest odd depth meeting delta at that width. binomial_upper_tail and column_collision live in the estimators module for UnivMon's L2 to share. Example 1's CountSketch (non-power-of-two width) needs depth 149 instead of 125 for its target; the committed viewer fixture is regenerated.
With asap_sketchlib's certifiable UnivMon estimators (#220, #221), Stage 1 states guarantees for two more readouts and sizes UnivMon per readout. L2 (RelativeValue) reads layer 0's Count Sketch. Per row, Chebyshev with Var <= 2q F2^2 fails with probability 2q/eps2^2; the guarantee fixes that at 0.1, so the columns give eps2 = sqrt(20q) and the rows give delta = P(Bin(r, 0.1) >= ceil(r/2)). The norm's error is 1 - sqrt(1 - eps2). Sizing picks the smallest rows meeting delta and the smallest width (power of two or not) meeting eps, on one layer with a one-key heap, clamped to rows * (ceil(log2 cols) + 1) <= 128. Distinct count (Cardinality) is unbiased with Var <= 1.35 F0^2/k for k >= 8 once the top layer is complete. Sizing picks the smallest heap with 1.35/(k eps^2) + (e/4)^(k/2) <= delta and records delta in the new SketchParams::UnivMon distinct_failure_probability (ignored by equality, like KLL's). Stage 1 keeps 19 layers, the most the library builds, since the layers needed depend on the data. For a distinct count over a source's own rows the failure probability is the union of the Chebyshev share and univmon_incomplete_top_layer_probability[k, layers](max_distinct[source]); Stage 3 binds it from the data workload's max_distinct_per_evaluation: 0 when F0_max <= k, (e/4)^(k/2) when layers >= ceil(log2(F0_max/k)) + 2, else 1; unbound stays unmet. Entropy stays uncertified, and the total stays exact. Terminal-mode states (OctoSketch aggregators fed promoted deltas) are not covered; the executor rejects them. Tests changed by this sizing: - frontend-promql univmon_candidates: the four readouts no longer share one state (total and entropy keep the baseline shape and still do); L2 and the distinct count now carry guarantees, and the uncalibrated test covers entropy only. - replacement (legacy search) cardinality tests: at eps ~ 0.008 the distinct-sized UnivMon (a 2M-key heap on 19 layers, ~2.5 GB) exceeds the search's 512 MiB cap, so they use eps = 0.05.
… a merge With the pin past asap_sketchlib #221, L2 reads layer 0's Count Sketch, whose counters add, so a merged UnivMon's L2 has the guarantee of one build over the union. The distinct count's bound needs each layer's heap to keep min(|S_j|, k) of that layer's keys, chosen without reading the next layer's selection bit. UnivMon::merge re-ranks the union of the parts' heap keys by the merged sketch, which keeps both, and its top-layer completeness is the union's. Both readouts are now SameAsUnionBuild in the family-capability table. Entropy and top-k need heavy items that a part may have dropped, and stay Approximate. heap_readouts now covers only entropy and top-k. A distinct count reads the heaps only for how many keys they hold.
zzylol
force-pushed
the
feat/univmon-certification
branch
from
October 15, 2026 03:57
c544d62 to
667088b
Compare
This was referenced Oct 15, 2026
zzylol
added a commit
that referenced
this pull request
Oct 15, 2026
Composition sizes each consumer's summary build alone, for its own
target. The summary-capability rule groups the builds of one candidate
that chose the same family over the same input sub-DAG (window, WHERE
and time range included), item and weight, grouping and filter, whose
every reader is an estimate the family answers (the family-capability
table's readouts), and that differ only in size. For every choice, per
group, of no sharing or one subset of two or more members whose sizes
differ (partial sharing included), the subset becomes one build sized
with each parameter's largest value among the members (a KLL's failure
probability and DDSketch's alpha: the smallest), and every guarantee
above it is composed again, so plan selection checks each consumer
against the shared state. Builds of equal size stay the
identical-expression rule's; unshared builds are untouched and no
consumer changes family.
Stage 1 lists each composed candidate, its identical-expression
candidates, then each summary-capability candidate followed by its own
identical-expression candidates, every distinct DAG once. The Pass 2
record (Pass2Candidate) names the shared summaries ("UnivMon for Q1,
Q3") and the shared identical groups.
Example 2 (PromQL stand-in): with #702's sizing the three UnivMons
differ (Q1 a distinct-count heap on 19 layers, Q2 the baseline shape for
entropy, Q3 a one-layer Count Sketch for L2), so the rule offers one
UnivMon for each pair and one for all three, each sized with the
members' largest heap, rows, columns and layers and the smallest
recorded distinct-count failure probability; the third query keeps its
own options. A shared UnivMon whose rows and columns together exceed
asap_sketchlib's hash budget (rows * (ceil(log2 cols) + 1) <= 128) is
not offered, since the library would not build it. The shared UnivMon
stays one build in Stage 2.
zzylol
added a commit
that referenced
this pull request
Oct 15, 2026
Composition sizes each consumer's summary build alone, for its own
target. The summary-capability rule groups the builds of one candidate
that chose the same family over the same input sub-DAG (window, WHERE
and time range included), item and weight, grouping and filter, whose
every reader is an estimate the family answers (the family-capability
table's readouts), and that differ only in size. For every choice, per
group, of no sharing or one subset of two or more members whose sizes
differ (partial sharing included), the subset becomes one build sized
with each parameter's largest value among the members (a KLL's failure
probability and DDSketch's alpha: the smallest), and every guarantee
above it is composed again, so plan selection checks each consumer
against the shared state. Builds of equal size stay the
identical-expression rule's; unshared builds are untouched and no
consumer changes family.
Stage 1 lists each composed candidate, its identical-expression
candidates, then each summary-capability candidate followed by its own
identical-expression candidates, every distinct DAG once. The Pass 2
record (Pass2Candidate) names the shared summaries ("UnivMon for Q1,
Q3") and the shared identical groups.
Example 2 (PromQL stand-in): with #702's sizing the three UnivMons
differ (Q1 a distinct-count heap on 19 layers, Q2 the baseline shape for
entropy, Q3 a one-layer Count Sketch for L2), so the rule offers one
UnivMon for each pair and one for all three, each sized with the
members' largest heap, rows, columns and layers and the smallest
recorded distinct-count failure probability; the third query keeps its
own options. A shared UnivMon whose rows and columns together exceed
asap_sketchlib's hash budget (rows * (ceil(log2 cols) + 1) <= 128) is
not offered, since the library would not build it. The shared UnivMon
stays one build in Stage 2.
zzylol
added a commit
that referenced
this pull request
Oct 15, 2026
…and UnivMon frequency statistics (#707) feat(types): summary-capability facts in the family-capability table Each family lists the readouts one of its states answers (Readout: total, item frequency, any quantile rank, distinct count, L2, entropy, top-k), so a planner rule can check that one state serves every consumer's statistic: UnivMon answers its total and every frequency moment, KLL every rank, CountSketch item frequencies and L2, the heap sketches top-k and item frequencies. Exact accumulators are read as finalized values and answer no readout. feat(logical-asap-optimization): Pass 2 summary-capability rule Composition sizes each consumer's summary build alone, for its own target. The summary-capability rule groups the builds of one candidate that chose the same family over the same input sub-DAG (window, WHERE and time range included), item and weight, grouping and filter, whose every reader is an estimate the family answers (the family-capability table's readouts), and that differ only in size. For every choice, per group, of no sharing or one subset of two or more members whose sizes differ (partial sharing included), the subset becomes one build sized with each parameter's largest value among the members (a KLL's failure probability and DDSketch's alpha: the smallest), and every guarantee above it is composed again, so plan selection checks each consumer against the shared state. Builds of equal size stay the identical-expression rule's; unshared builds are untouched and no consumer changes family. Stage 1 lists each composed candidate, its identical-expression candidates, then each summary-capability candidate followed by its own identical-expression candidates, every distinct DAG once. The Pass 2 record (Pass2Candidate) names the shared summaries ("UnivMon for Q1, Q3") and the shared identical groups. Example 2 (PromQL stand-in): with #702's sizing the three UnivMons differ (Q1 a distinct-count heap on 19 layers, Q2 the baseline shape for entropy, Q3 a one-layer Count Sketch for L2), so the rule offers one UnivMon for each pair and one for all three, each sized with the members' largest heap, rows, columns and layers and the smallest recorded distinct-count failure probability; the third query keeps its own options. A shared UnivMon whose rows and columns together exceed asap_sketchlib's hash budget (rows * (ceil(log2 cols) + 1) <= 128) is not offered, since the library would not build it. The shared UnivMon stays one build in Stage 2. feat(frontend-sql): recognize the SQL frequency L2 and entropy idioms #509 Example 2 writes L2(src_ip) and Entropy(src_ip) as SQL idioms over grouped unit counts. The SQL lowering (single query and batch) now names them at the query root as FrequencyL2 / FrequencyEntropy aggregates carrying the query's accuracy target, so Pass 1 offers exact and summary candidates for each: - L2: SQRT(SUM(c * c)) over COUNT(*) per key, only when the product is already Float64 (CAST(c AS DOUBLE) factors or CAST(COUNT(*) AS DOUBLE)). The Int64 product stays relational: SQL integer overflow is observable. - Entropy: -SUM(p * LN(p)) with p = COUNT(*)[*1.0] / SUM(COUNT(*)) OVER () over the complete unordered frame; the IR's bits are converted to nats and SQL's -0.0 is kept. Both require a single non-nullable Bool/Int64/Utf8 key, no HAVING and no measure filter, follow projections only, keep the original output schema, and decide SQL's empty-input NULL by an exact population count joined beside the statistic, never by an estimate. Scaled probabilities, LOG2, partitioned or ordered windows and nullable keys stay relational. Ports the frontend part of #598 (originally #562 and #564). feat(physical-operators): exact distinct, L2 and entropy reducers Exact Cardinality, FrequencyL2 and FrequencyEntropy aggregates had no native implementation, so the exact option of #509 Example 2's three statistics could not execute. - Reduction::Cardinality(cols) counts distinct tuples; a tuple with any NULL is skipped, and grouped key encoding keeps tuple boundaries and folds signed zero and NaN payloads. - Reduction::FrequencyL2(col) and FrequencyEntropy(col) count the unit frequencies of one Bool/Int64/Float64/Utf8 identity column: NULL is skipped, a non-finite float is an error, an empty population gives 0.0, and entropy is in bits, as AggIntent::FrequencyEntropy and UnivMon's readout define it. - The dictionaries are memory-accounted and cooperative. - Logical aggregates bind them: grouped ones (SQL COUNT(DISTINCT)) and per-series range reductions (distinct_over_time, l2_over_time, entropy_over_time), which reduce each series' window samples, restore the run's timestamp and drop the metric name as range functions do. - The analytical cost lowering prices them as hash aggregates. Ports the exact-reducer part of #597 (from #559 and #563). feat(physical-operators): native UnivMon state over typed identities UnivMon had a kernel but no native DAG state: a build was rejected ("summary family has no native DAG state implementation"), a summary build accepted only Float64 samples, and Pass 1's UnivMon update (each value of the item column one unit occurrence) did not bind, so no UnivMon candidate of #509 Example 2 could execute. - UnivMon is a native family; its states validate against their parameters, and one state answers its total, distinct count, L2 norm and entropy. - A UnivMon build takes Bool, Int64 and Utf8 identities as type-tagged key bytes (Int64 above 2^53 stays distinct) besides Float64 samples; NULL is skipped. Retained heap key bytes count toward state memory. - A unit-frequency update over one item column binds, for UnivMon and HLL, as a value-frequency build over that column: grouped (SQL src_ip) and per series (PromQL sample values). Ports the UnivMon part of #597 (from #557). test(integration-tests): Example 2 in SQL; the recognized idioms execute Example 2's SQL Q3 casts its counts to DOUBLE (the design's Int64 SUM(c * c) can overflow and stays relational), and the SQL Q2 and Q3 now lower to the entropy and L2 intents, so the SQL workload has the PromQL stand-in's Pass 1 options. The recognized entropy executes in nats with SQL's -0.0, L2 executes exactly, and both give NULL on empty input, decided by the exact population count. feat(devtools): stage_pipeline example planner-layering-2 stage_pipeline plans #509 Example 2: three SQL dashboard panels over the last minute of flows (distinct sources at eps 0.02, entropy at 0.05, L2 at 0.01, delta 0.01), lowered over its flows(ts, src_ip) table. SQL workloads lower every entry, batch and repeating, each with its own accuracy target (lower_sql_batch read only batch entries). The stage-viewer tab 2 row is regenerated from it: 82 plans, 16 from Pass 1, the rest from the identical-expression and summary-capability rules (one UnivMon for each pair of queries and for all three); 6 valid, P1 (all exact) selected at 6.08 per second. The row previously said 8 plans were valid. feat(logical-asap-optimization): guarantees through a join beside exact inputs and an exactly decided CASE SQL Example 2's L2 decides the empty-input NULL from an exact population count cross-joined beside the estimate and a CASE on that count. No guarantee rule covered the join or the CASE, so every SQL plan with UnivMon for Q3 was rejected ("the output has no guarantee"), although the L2 estimate is certified. - A join (inner, outer or cross) whose inputs are exact but one keeps that input's guarantee when its predicate reads only exact columns: which rows match, and how often, then does not depend on an estimate. A key on the approximate input's columns, a semi- or anti-join, or two approximate inputs have none. - A CASE whose operand and conditions are exact keeps its weakest branch's guarantee; a literal or NULL branch, or a missing ELSE, is exact. A condition over an approximate value has none. - A projection over such a join composes each output column this way. Guarantees are per node, so every column of the approximate input counts as approximate; column-level tracking (#689) stays the general solution. A root's default metric (#690) looks through the same shape: a projection whose CASE value branches read columns (or literals, cast or not) takes those columns' metric, following them through joins and row-preserving operators to an aggregate's measure, so SQL's L2 root resolves to a relative target as the PromQL one does. Any other expression stays absolute. docs(stage-viewer): regenerate the Example 2 row with SQL L2 through UnivMon With the join and CASE guarantee rules, the SQL Q3 UnivMon plans are valid: 12 of the 82 plans are valid (Q3 exact or UnivMon), 70 invalid (Q1 HLL or UnivMon without a distinct bound, Q2 UnivMon without an entropy bound). P1 (all exact) is still selected at 6.08 per second; UnivMon on Q3 costs 8.08. fix(physical-operators): charge UnivMon key memory without walking its heaps A build charges a state's memory after every input row, and UnivMon's charge summed the bytes of every key on every layer's heap, so building was quadratic in the heap: a 20,000-key heap on 19 layers over 40,000 keys took 3.3 s in release. The charge now bounds the retained keys by every heap slot holding the longest key seen (tracked on insert, merge and adoption of a decoded sketch), in constant time. A test checks that the charge does not grow with heap occupancy; a timing test (ignored, release) checks the large build. fix(logical-asap-optimization): summary-capability review fixes - A capability group of more than 63 builds is an error: one 64-bit mask enumerates the subsets of at most 63, and the shift was clamped, so a larger group was enumerated wrongly (and took 2^63 steps) instead of refused. - A summary-capability variant whose guarantee composition fails carries its own error, so Stage 1 lists it as a candidate that cannot be composed and keeps the remaining variants; one failure used to drop them all. - shared_size_params no longer claims every sizing bound is monotone (a Count Sketch's collision bound is tighter at a power-of-two width than one column wider); the shared state is checked by composing every guarantee above it again. fix(logical-asap-optimization): read each column's guarantee in a projection over a join A projection's guarantee assumed the error lies only in its child's sample-value column (the column named "value", or the only numeric one). Over a join that column can be an exact input's, so the approximate input's columns were read as exact: projecting sum(m) * 1000 beside the exact side's value composed to the approximate side's absolute error 1, while the truth is 1000. A projection over a join (directly, or through projections, filters, sorts, limits or deduplication) now reads each column's own guarantee (column_guarantees), and a projection over a computed projection over a join, whose columns' guarantees are unknown, has none. Plan selection's operand-domain lookup reads columns the same way. column_guarantees and join_guarantee document their assumption that every approximate node of Stage 1 has an exact row set. Per-column tracking in general is #689. fix(logical-asap-optimization): refuse summary-capability groups of more than 20 builds Every subset of two or more builds of a group is a candidate, 2^n of them; past 20 builds (about a million) the rule returns an error naming the limit instead of enumerating. fix(physical-operators): bound UnivMon key memory by the key bytes inserted Charging every heap slot at the longest key seen overcounts badly: one 1 KB key in a sketch of 19 layers of 20,000 slots was charged 380 MB. Each layer's heap keeps at most one copy of a key, so the retained keys are also bounded by the layers times the bytes of every key inserted (or merged in); the charge takes the smaller bound, still in constant time. asap_sketchlib exposes no eviction hook for an exact running total.
This was referenced Oct 15, 2026
18 of 53 tasks
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.
Why
Stage 3 could not use a UnivMon for anything but the exact total. Its L2 and distinct-count readouts had no guarantee, and its shape was a fixed 256 / 5×1024 / 16 layers whatever the target. Upstream asap_sketchlib now has estimators with provable bounds: #221 (unbiased
calc_card,calc_l2read directly from layer 0,top_layer_complete(),get_l2_sqr()) and #220 (a shape whose hashes are not independent is rejected at construction; at most 19 layers). This PR moves the pin to them and certifies the two readouts in the planner. This is G5 step 2, done ahead of G5.What
831a504(branchplanner-pin/univmon-certifiable), which merges QueryExpr has duplicate relational/scalar-language shapes for the same semantics (Scalar/Literal, BinaryOp/Compare/Arith) #220 into feat: rewrite uniqExact to COUNT(DISTINCT ...) #221.831a504lacks them. The executor therefore pinse6b0314(branchplanner-pin/univmon-certifiable-weighted-frequency), a clean merge of controller-OMP #160's5f03ccbinto831a504. Its UnivMon readouts now use the same estimators the planner certifies.RelativeValue) is certified from layer 0's direct estimate.Cardinality) is certified with the rigorous Chebyshev bound, given the source's distinct bound.exp(−depth/18).How
L2. Each row of layer 0 estimates F2 with
Var ≤ 2q·F2², whereqis the chance two keys share a column.q = 1/wfor a power-of-two width. Otherwiseq ≤ 9/(8w), because columns are hash slices taken modulow.2q/ε₂²at 0.1, so a shape states one (ε, δ):ε₂ = √(20q);δ = P(Bin(r, 0.1) ≥ ⌈r/2⌉), summed exactly;1 − √(1 − ε₂).rmeeting δ, then the smallest width meeting ε. That width may or may not be a power of two, whichever is smaller.r·(⌈log2 w⌉ + 1) ≤ 128andr ≤ 19. A target past that gets the largest shape that can be built, and its guarantee then misses the target.Distinct count. The estimator is unbiased with
Var ≤ 1.35·F0²/kfork ≥ 8once the top layer is complete.(e/4)^(k/2)whenlayers ≥ ⌈log2(F0_max/k)⌉ + 2.kwith1.35/(kε²) + (e/4)^(k/2) ≤ δand records δ in a new optionalSketchParams::UnivMon.distinct_failure_probability. Like KLL'sfailure_probability, it is ignored by equality and omitted from JSON whenNone.ε = √(1.35/(k·δ_c)), whereδ_c = δ − (e/4)^(k/2). Its failure probability isUnionBound[δ_c, univmon_incomplete_top_layer_probability[k=…,layers=…](max_distinct[source])].max_distinct_per_evaluation:F0_max ≤ k;(e/4)^(k/2)whenF0_max ≤ k·2^(layers−2);CountSketch. A row fails with probability
q·w/3: 1/3 for a power-of-two width and 3/8 otherwise.exp(−depth/18)assumed 1/3, so it understated δ for every non-power-of-two width. The guarantee now usesP(Bin(d, p) ≥ (d+1)/2), andcount_sketch_depth(width, δ)takes the smallest odd depth meeting it.Not certified:
Key code interfaces added / modified
crates/types/src/post_asap/sketch.rs,family_capability.rscrates/asap-aware-mapping/src/accuracy/estimators/univmon.rscrates/asap-aware-mapping/src/accuracy/estimators/count_sketch.rs,mod.rscrates/asap-aware-mapping/src/per_computation_candidates.rs,empirical_cost.rscrates/asap-physical-operators/src/summary_kernels/univmon.rs,capability.rsCargo.toml,crates/asap-physical-operators/Cargo.tomlUsage, end to end (numbers from running the stages at this PR's merge commit
b788fc52, Example 2's Q1 alone):Before this PR
g = x².After this PR
Example 2 (PromQL stand-in; Q1 distinct (0.02, 0.01), Q2 entropy (0.05, 0.01), Q3 L2 (0.01, 0.01)):
Example 1: its CountSketch has a non-power-of-two width and now needs depth 149 instead of 125 for δ. The committed viewer fixture is regenerated.
Verification
estimators/univmon.rs,count_sketch.rs):9/(8w)collision bound off powers of two;rejects_shapes_past_the_hash_budget. It failed (a library panic) before the fix.planner_layering_example2.rs):stage3_plan_selection_accepts_univmon_distinct_given_a_distinct_bound,stage3_plan_selection_rejects_univmon_distinct_past_its_layersandstage3_plan_selection_accepts_univmon_l2;stage1_logical_asap_optimization_univmon_is_sized_per_accuracy_target;stage3_plan_selection_judges_univmon_with_an_accuracy_modelstays ignored, since entropy has no model.frontend-promql/tests/univmon_candidates.rs: the four readouts no longer share one state, because each is sized for itself. The total and entropy keep the baseline shape and still share it. Sharing across readouts is G5's summary-capability rule. L2 and the distinct count now carry guarantees, so the "uncalibrated" test covers entropy only.replacement.rs(legacy search) cardinality tests: at ε ≈ 0.008 the distinct-sized UnivMon (a 2M-key heap on 19 layers, about 2.5 GB) exceeds the search's 512 MiB cap. These tests now use ε = 0.05.tools/dag-viewer/examples/planner-layering-example1.json: CountSketch depth 125 → 149.cargo fmt --all -- --check,cargo clippy --workspace --all-targets --all-features --locked -- -D warningsandcargo nextest run --workspaceon head c544d62: all pass, 1666 tests.Limitations and follow-ups
F0_maxis bound, but Stage 3 does not resize today.distinct_failure_probabilityis ignored by equality, following KLL's precedent. G5's per-parameter merge must also take the minimum δ of a shared build. Because the guarantee is a point on a δ–ε curve, a shared UnivMon must be re-checked against each consumer.⌈3/ε²⌉. For targets with a small δ, a power-of-two width (p = 1/3) would need fewer rows.