Repository navigation
countsketch_l2_norm: recompute l2 on decode, drop it from the wire (new ASAPv1 kinds 0x19 0x01, 0x10 0x01, 0x11 0x01, 0x07 0x05) - #259
Merged
Conversation
… 0x01, 0x10 0x01, 0x11 0x01, 0x07 0x05 CountSketchWithL2Norm's per-row L2 accumulator now always equals the row's sum of squared counters (saturated at i64::MAX): a write to a saturated row sums the row again. update_weighted_with_hash_without_l2 and as_storage_mut, the only ways to move counters without it, are removed. The accumulator stays in memory as a cache and leaves the wire: CountSketchWithL2Norm, UnivMon, two-tier UnivMon and Hydra-over-UnivMon payloads drop l2 under new kind ids, and the decoder recomputes it. The retired 0x19 0x00, 0x10 0x00, 0x11 0x00 and 0x07 0x04 still decode (l2 read and dropped), also when nested in an EhBucketSketch, and are never encoded. Golden fixtures: *_no_l2 twins. Part of #250 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
zzylol
marked this pull request as ready for review
October 29, 2026 00:29
zzylol
force-pushed
the
csl2/derive-l2-on-decode
branch
from
October 29, 2026 00:29
eae2400 to
20caf2e
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.
Part of #250. Golden fixtures: ProjectASAP/sketchlib-golden-bytes#8 (merge first; the submodule pointer then moves to the merged commit).
Why
CountSketchWithL2Normkeeps one L2 accumulator per row, and the UnivMon family's layers are this sketch.mergealready recomputes each accumulator as the row's sum of squared counters. Still, the ASAPv1 payloads carried it (§3.19, §3.15, §3.16 and the Hydra UnivMon cells). The wire spec said it could not be derived for two reasons:update_weighted_with_hash_without_l2moved counters without touching the accumulator. Only a property test called it.i64::MAXon a decrement, so after saturation it no longer matched the counters.With both gone,
l2is a cache of the counters. Per Q-DERIVED, a value the decoder can recompute does not ride the wire.What
i128sum of its counters' squares, with each square capped at2^63. Every write removes the old counter's term and adds the new one's, exactly and in O(1), including past saturation. The value is clamped toi64::MAXwherever it is read. A single capped term already reachesi64::MAX, so the clamped value ismin(sum(counts[row]^2), i64::MAX). Each row's accumulator grows from 8 to 16 bytes, andmemory_bytes/estimated_memory_bytescount that.l2, because the old accumulator stepped down fromi64::MAX. It now reports the true value. Only in that case dol2,l2_squared, UnivMon'sestimate_l2and its terminal-mode heavy-hitter threshold change.CountSketchWithL2Norm::update_weighted_with_hash_without_l2tests/pbt/topk_wrappers.rs), now onupdate_weighted_with_hashCountSketchWithL2Norm::as_storage_mutl2is read, dropped and recomputed. They are never encoded.0x19 0x00CountSketchWithL2Norm[counts, l2]0x19 0x01[counts]0x10 0x00UnivMon (8 fields,l2at position 1)0x10 0x01[counts, heap_lens, keys, heap_counts, candidate_complete, bucket_size, update_mode]0x11 0x00two-tier UnivMon0x11 0x010x07 0x04Hydra, UnivMon counter0x07 0x05[cells], each cell the 7-field pyramidHydra's variants
0x07 0x00-0x07 0x04are all taken, and the variant byte is Hydra's only version marker. So the new UnivMon-counter payload takes the next free variant,0x07 0x05, and the reserved range for further base sketches becomes0x07 0x06-0x07 0x7f.EhBucketSketch, and through itExponentialHistogrambuckets, accepts a nested0x19 0x00or0x10 0x00and decodes it into the same variant. On re-encode the bucket gets0x19 0x01or0x10 0x01. This mirrors what the DDSketch PR does for0x05 0x00. No existing EH or EhBucketSketch golden fixture nests these kinds, so no nested fixture changes.l2arrays. They are read element by element asi64and not kept (RetiredL2), so retired payloads allocate nothing forl2, yet anl2that is not an integer array is still rejected.csl2-derived-l2(baffc4b), which adds six*_no_l2fixtures. Each one is its retired twin decoded and re-encoded. The branch also scopes the README's CountL2HH integer-width note to the retired fixture.docs/asapv1_wire_format.md: the registry, the Hydra mapping note, the §3 "derivable" rule, §3.9, §3.15, §3.16, the §3.18 nested-id table and rule 2, §3.19, the matrix-row bound paragraph, the integer-array list, Q-DERIVED, Q-HYDRA-IDS and Q-UNIVMON-SCOPE. Also the API pages (Count Sketch, CountSketchWithHeap, EhBucketSketch, Hydra, UnivMon, two-tier UnivMon),docs/message_pack_format.md,docs/tests.md,tests/PBT_COVERAGE.mdand the CHANGELOG.estimate_l1, Octo UnivMon and UnivMon-Q. Their results change only through the saturated-then-decreased case above.How
src/sketches/countsketch_l2_norm.rs:row_l2_squaredis now aVector1D<i128>.capped_square(c) = min(c^2, 2^63),row_capped_square_sum(row), andclamp_to_i64, which reads an accumulator clamped to[0, i64::MAX].mergerecomputes withrow_capped_square_sum.write_counter, the single write path forupdate_weighted_with_hashandapply_delta, doesacc += capped_square(new) - capped_square(old).l2_squaredreadsclamp_to_i64.src/sketches/countsketch_l2_norm/wire.rs:layer_statereturnscountsonly and checks each row's accumulator againstrow_capped_square_sum.rebuild_layer(rows, cols, seed_index, counts)recomputes the accumulators.from_asapv1picks the payload struct by kind id.CountSketchWithL2NormPayloadCarryingL2reads the retired layout, withl2: RetiredL2.RetiredL2is a crate-private newtype. ItsDeserializevisits the array asi64s and keeps none of them. ItsSerializewrites held values; only tests use it, to craft retired bytes.src/sketch_framework/univmon/wire.rs:PyramidPayloadand the internalPyramidState/DecodedPyramidlosel2.PyramidPayloadCarryingL2<K>(withl2: RetiredL2), itsFromintoPyramidPayload<K>, anddecode_pyramid_payload(payload, carries_l2)read the retired layout.decode_pyramidgains acarries_l2argument.rows * layers == len(l2)is gone. Allocation stays bounded:check_layerscapslayersatUNIVMON_MAX_LAYERSbefore the geometry vector is built, andrebuild_layersmeasures the total cell count againstlen(counts)before any matrix is built. The spec's §3.15 rule 5 and §3.16 rule 4 say so.src/sketch_framework/univmon_two_tier/wire.rs: the same switch on kind id.accumulator_count, whose only caller was the removedl2length check, is deleted.src/sketch_framework/hydra/wire.rs:decode_univmon_carrying_l2reads0x07 0x04.unpack_univmon_cells(.., carries_l2)readsHydraUnivMonPayloadCarryingL2<K>and rewrites each cell in the current shape before handing it toUnivMon::from_asapv1.src/sketch_framework/eh_bucket_sketch/wire.rs:variant_nameandrebuild_variantaccept the two retired nested ids.src/asapv1/kind.rs:COUNT_SKETCH_WITH_L2_NORM,UNIVMON,UNIVMON_TWO_TIERandHYDRA_UNIVMONnow name the new ids.*_CARRYING_L2constants name the retired ones.retired_l2_fixtures_decode_to_their_no_l2_twinschecks every pair.Key code interfaces added / modified
src/sketches/countsketch_l2_norm.rssrc/sketches/countsketch_l2_norm/wire.rssrc/sketch_framework/univmon/wire.rs(crate-private payload types)UnivMon,UnivMonTwoTier,HydraandEhBucketSketchkeep their publicto_asapv1/from_asapv1signatures. Only the ids they write and accept change, as in the table above.src/asapv1/kind.rs(crate-private)Usage
A producer ships a CountSketchWithL2Norm and a UnivMon to a receiver, and the receiver reports the same L2. Compiled and run as an example at
eae2400:Bytes written before this PR (
0x19 0x00,0x10 0x00,0x11 0x00,0x07 0x04) still decode with the same calls.retired_l2_fixtures_decode_to_their_no_l2_twinschecks this on the golden fixtures.Before this PR
i64per row per layer forl2.update_weighted_with_hash_without_l2could leave the accumulator stale.After this PR
l2. The fixtures show the saving: the 3-layer 2x4 UnivMon fixture shrinks by 23 bytes, the two-tier fixture by 24, and the 2x4 CountL2HH fixture by 15.l2is now the true value instead of an understatement. Round-trip tests comparel2,estimate_l2, entropy, cardinality and L1.Verification
Only the tests covering the changed code, plus format and lint, at
eae2400(one commit on origin/main21b6357):cargo test --lib -- countsketch_l2_norm univmon hydra eh_bucket_sketch eh:: octo eh_univmon sketch_catalog: 267 passed.cargo test --lib --features experimental -- countsketch_l2 univmon hydra eh_bucket_sketch eh::: 263 passed.cargo test --test asapv1_golden: 71 passed (default), 72 passed (experimental).cargo test --test e2e -- wire:: hydra_univmon: 17 passed (default), 18 passed (experimental).cargo test --release --test e2e -- octo:: univmon: 104 passed.cargo test --test pbt -- topk_wrappers univmon hydra: 43 passed.conformance_kit -- univmon l2 hydra: 2 passed.heap_memory_bytes: 1 passed.cargo test --test capability: 3 passed (default andexperimental).cargo test --features octo-runtime --lib -- octo univmon countsketch_l2: 211 passed.cargo test --release --features octo-runtime --test e2e -- octo:: univmon: 127 passed.cargo fmt --check: clean.cargo clippy --all-targets,--features experimentaland--all-features: no warnings.count_sketch_with_l2_norm_payload_carries_no_l2andunivmon_payload_carries_no_l2check the payload field counts.*_retired_kind_decodes_and_recomputes_l2, for CountSketchWithL2Norm, UnivMon, two-tier UnivMon and Hydra: a retired envelope with a true, wrong-length, negative, stale, all-zero or emptyl2decodes and re-encodes to the new bytes.count_sketch_with_l2_norm_retired_kind_rejects_a_non_integer_l2: anl2of strings is rejected.count_sketch_with_l2_norm_rejects_a_stale_l2_cache: the encoder check, and its error names row 0.count_sketch_with_l2_norm_l2_stays_derivable_past_saturation: a row goes to2^32, then back to2^31, and the accumulator is2^62 + 1.the_l2_accumulator_tracks_rows_into_saturation_and_back. A first weight of2^33saturates every row. Up to 47 more weights in±2^33follow, then all of them negated in reverse, back to zero. After every step,l2_squaredequals the median ofmin(sum(c^2), i64::MAX)computed from the counters, and equals themergeand ASAPv1-decode recomputations.eh_bucket_sketch_decodes_retired_l2_carrying_kind_idschecks the nested ids.retired_l2_fixtures_decode_to_their_no_l2_twinschecks every golden pair.count_sketch_with_l2_norm_writes_the_cells_the_shared_map_namesnow inserts throughupdate_weighted_with_hash. It also asserts that the runningl2equals the onemergerecomputes and the one the decoder recomputes.known_state_matches_goldenbuilds the recomputed accumulators, which read as[5368610562, i64::MAX], and also decodes the retired fixture.tests/e2e/wire.rsexpect the new ids.pbt/e2esuites and benchmarks. CI is not run on this draft.Limitations and follow-ups
csl2-derived-l2(baffc4b) must merge first. The submodule pointer then moves to the merged commit.wire/asapv1/kind.go(KindCountL2HH,KindUnivMon,KindUnivMonOptimized,KindHydraUnivMon, all at the old ids). Adding0x19 0x01,0x10 0x01,0x11 0x01and0x07 0x05, marking the old ids retired, and updatingTestRegistry's counts is a small follow-up. It must land once Go's golden submodule is bumped to the new fixtures. No Go change is in this PR.l2is checked only for being an integer array. Its values are dropped, so a stale or craftedl2in old bytes is silently replaced by the true value.src/asapv1/kind.rs,eh_bucket_sketch/wire.rs,tests/asapv1_golden.rs, the wire spec and the CHANGELOG. Whichever merges second rebases; the edits touch different lines or table rows.update_weighted_with_hash_without_l2oras_storage_muton aCountSketchWithL2Norm.🤖 Generated with Claude Code