Repository navigation
hll: stop carrying HIP's kxq0/kxq1 on the wire (new ASAPv1 kind 0x01 0x04) - #257
Merged
Merged
Conversation
kxq0 and kxq1 are the sums of 2^-register over the registers below 32 and of 32 or more. Every term is a power of two, so recomputing them from the registers on decode gives the incrementally kept values bit for bit, and a decoded sketch keeps counting exactly as the encoded one would. The HIP payload is now the registers and hip_est under the new kind 01 04; the retired 01 03 still decodes, its two sums dropped, and is never encoded. The golden submodule gains the hll_hip_est_* fixtures. 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:37
zzylol
force-pushed
the
hll/hip-derive-kxq
branch
from
October 29, 2026 00:37
c988a01 to
2477e97
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#6 (merge first; the submodule pointer then moves to the merged commit).
Why
The HIP payload (
0x01 0x03) carried four fields:registers,hip_kxq0,hip_kxq1andhip_est.kxq0andkxq1are a cache of the registers.kxq0issum(2^-r)over the registersrbelow 32, andkxq1is the same sum over registers of 32 or more.update_with_hashkeeps both up to date so that each insert costs O(1). Decode took them as given and did not check them against the registers. A payload could therefore carry sums that no register state produces, and the decoded sketch would take every later estimate increment from those wrong sums. The golden fixtures do exactly this:hll_hip_p12carrieskxq0 = 1.5, kxq1 = 2.5with 4096 registers. Q-DERIVED says state a decoder can recompute exactly is not carried. This PR is the HLL-HIP row of the "cached values carried on the wire although derivable" table in the umbrella issue.hip_eststays. HIP adds2^p / (kxq0 + kxq1)to it each time an insert raises a register, so its value depends on the order the registers rose in, and no walk of the registers reproduces it.What
0x01 0x04, with payload[registers, hip_est]. The retired0x01 0x03([registers, hip_kxq0, hip_kxq1, hip_est]) still decodes. Its two sums are read as float64, as before, and dropped. It is never encoded.kxq0andkxq1are recomputed from the registers. The recomputed values equal the incrementally kept ones bit for bit (see Correctness), so a decoded sketch that keeps inserting gives the samehip_estas a sketch that never left memory. There is no last-bit difference. The tests check bit equality, not a tolerance.0x07 0x03) holdsHyperLogLog<Ertl>registers, andEhBucketSketch's HLL arm is Ertl-MLE.EhBucketSketchrejects0x01 0x04the same way it rejects0x01 0x03, and its sibling-id test now covers it.HashSketchEnsemble'sHllHipmember has no wire kind.hll-hip-0104(b55ca65), which addshll_hip_est_p12andhll_hip_est_p14.hll_hip_p12andhll_hip_p14stay as decode-only fixtures for0x01 0x03.hip_estis carried,kxq0/kxq1are not" note, a retired-kind note, decode rule 1), theEhBucketSketchsibling-id paragraph, the Section 4 float-field list, and Q-DERIVED (HIP'skxq0/kxq1added under "Recomputed"; the encoder-check sentence now applies to caches an API can set, since HIP's are private and exact by construction)api_hyperloglog.md,api_eh_bucket_sketch.md(paragraph rewrapped),message_pack_format.md,docs/tests.mdand CHANGELOGNo public field or method is removed.
kxq0andkxq1stay in memory as private fields so that an update still costs O(1). No non-test code reads anything this PR orphans.How
src/sketches/hll.rs: new privatehip_kxq(registers) -> (f64, f64). It sums2^-rwith the samer < 32split thatupdate_with_hashuses. Each term comes from the new privateinverse_power_of_two(r), which builds2^-rfrom its bits (f64::from_bits((1023 - r) << 52)), so it is exact by construction rather than relying onpowi.update_with_hashnow uses it too, in place of1.0 / ((1 << r) as f64), which fixes a pre-existing shift overflow atr = 64(precision 1). Its doc comment states the exactness bound. Thekxq0,kxq1andestfields now have doc comments saying which values are derived and which are state.src/sketches/hll/wire.rs:HllPayloadHipis{ registers, hip_est }. A newHllPayloadHipWithKxqreads the retired four-field payload.HyperLogLogHipImpl::from_asapv1reads the stored kind id and validates the envelope against that id. Each id fixes its payload length: a0x01 0x04payload with four fields, or a0x01 0x03payload with two, is rejected. It then rebuildskxq0/kxq1withhip_kxq.src/asapv1/kind.rs:HLL_HIPis0x01 0x04. The newHLL_HIP_WITH_KXQis0x01 0x03.Key code interfaces added / modified
src/asapv1/kind.rssrc/sketches/hll.rssrc/sketches/hll/wire.rsASAPv1 payload, §3.1:
Usage
A producer counts distinct keys and ships the sketch. A receiver decodes it and keeps counting the same stream, and its estimate stays equal to the producer's. This was compiled and run as an example binary at
c988a01. It printedpayload 16731 bytes, estimate 99458.Major algorithm changes
HIP decode:
kxq0/kxq1recomputed, not readkxq0andkxq1from the payload unchecked. Thehll_hip_p12fixture (kxq0 = 1.5, kxq1 = 2.5, 4096 registers that mostly read 0) decoded with those values. Its next estimate increment would have been4096 / 4.0 = 1024instead of about4096 / 4092.6 ≈ 1.0.from_asapv1read(registers, kxq0, kxq1, est)and stored them as given.from_asapv1reads(registers, est)(or the retired four fields, dropping the middle two), then sets(kxq0, kxq1) = hip_kxq(registers).2^-ris a power of two.2^53times that term.kxq0: terms run from2^0down to2^-31, and the sum is at mostm = 2^p. It is exact forp + 31 <= 53, so forp <= 22.kxq1: terms run from2^-32down to2^-(65 - p), and the sum is at most2^(p - 32). The ratio to the smallest term is2^33at everyp, sokxq1is exact at every precision.update_with_hashkeeps incrementally are the samef64. The crate builds P12, P14 and P16.hip_decode_then_continue_matches_an_unserialized_sketchchecks this at P12, P14 and P16. Every 7th hash is placed at a rank of 32 or more, sokxq1is non-zero. The test encodes after 0, 1,000 or 40,000 updates, decodes, feeds both sketches the same further hashes, and asserts bit equality of registers,kxq0,kxq1andest.2^22registers (precision 22), and onlykxq0sets that bound. Above it, a customimpl_hll_registers!storage'skxq0may differ from the incremental value in the last bits.Before this PR
A HIP sketch encoded as
0x01 0x03with four fields. Decode trustedkxq0/kxq1, so a payload with sums inconsistent with its registers made the decoded sketch's future estimate increments wrong.After this PR
A HIP sketch encodes as
0x01 0x04with[registers, hip_est]. Decode deriveskxq0/kxq1from the registers, so they always match the registers, and a decoded sketch continues exactly like the original. Bytes written as0x01 0x03still decode, and they re-encode as0x01 0x04.Verification
At
c988a01(golden submodule atb55ca65):hip_encodes_registers_and_estimate_onlyfailed with kind[1, 3]instead of[1, 4].retired_hip_kind_decodes_with_kxq_recomputedfailed because the decodedkxq0was the payload's1.5instead of the sum over the registers (about4092.63).hip_decode_then_continue_matches_an_unserialized_sketchpassed before the change too (the sums were carried then). It now guards that recomputing them changes nothing.cargo test --lib hll::: 29 passed, including the three tests above,hip_payload_must_match_its_kind_field_count(the two-field0x01 0x04payload under0x01 0x03, and the four-field one under0x01 0x04, are both rejected),known_state_matches_golden(encodes to the new fixtures; decodes the retired ones with recomputed sums),hll_hip_round_trip_preserves_state(bit equality) andhll_rejects_out_of_range_register_values(both kinds).cargo test --lib asapv1::: 12 passed (float_fields_reject_an_integer_and_the_other_float_widthcovership_eston0x01 0x04and the three floats on0x01 0x03; the decode-path table has a row for each kind).cargo test --lib eh_bucket_sketch: 20 passed.cargo test --test asapv1_golden: 71 passed (default features), 72 passed (--features experimental), includinghll_hip_retired_fixtures_decode_to_their_est_twins.cargo test --test hll_custom_precision: 5 passed, default andexperimental. The P13 HIP round trip now also inserts 2,000 more keys into both sketches and checks that the estimates still agree.cargo test --test pbt hll: 102 passed (registers_ofreads the two-field payload).cargo fmt --check;cargo clippy --all-targetsandcargo clippy --all-targets --features experimental: clean.Limitations and follow-ups
asapv1-only/hll). The mirror is a commit stacked on that branch (separate PR body). That branch's golden submodule predates theseed_list_index_scaledfixtures, so it does not yet meethll_hip_est_*. The Go tests derive the0x01 0x04bytes from the retired fixtures instead.SerializeToBytesfor HIP still carries the sums. The Go cleanup PR removes that format.🤖 Generated with Claude Code