Repository navigation
Conversation
A memory model can price a top-k heap before building one: the struct, the heap and digest arrays, the digest index and the owned key bytes for HHHeap; the cells, candidate slots, identity tuples and encoded keys for WeightedFrequency. Checked against size_of and a counting allocator. A retained candidate's identity tuple is now allocated at its exact length; it held four slots for a one-value tuple.
Contributor
Author
|
Closing: |
GordonYuanyc
added a commit
that referenced
this pull request
Oct 20, 2026
…223, #228, #229, #233, #238) (#242) * feat(univmon): check_shape refuses a pyramid shape without building it `UnivMon::check_shape(heap_size, rows, cols, layers)` accepts exactly the shapes `UnivMon::new` builds: positive dimensions, at most UNIVMON_MAX_LAYERS layers, at most MATRIX_MAX_ROWS rows, rows within the 128-bit hash budget and a counter count that fits usize. `new` panics exactly when it fails, so it no longer builds a pyramid the ASAPv1 wire refuses. Ports #229. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(univmon): count every cardinality candidate, direct L2, documented error bounds `estimate_cardinality` counts every heap key, including zero-weight keys and keys whose Count Sketch estimate is not positive. `estimate_l2` reads layer 0's Count Sketch F2 estimate in standard mode and sums the strata's F2 estimates in terminal mode; `estimate_g_sum` keeps the paper's recurrence, and its flag is now `count_every_key` (`true` counts every heap key). New `top_layer_complete()`. The L2 bound and the distinct-count bound, stated jointly with `top_layer_complete()`, are derived in docs/api/api_univmon.md, with Monte Carlo tests. Ports #221. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(univmon_two_tier): count every cardinality candidate and direct L2 `UnivMonTwoTier` gets the estimators `UnivMon` has: `estimate_cardinality` counts every heap key whatever its estimate, zero-weight keys included; `estimate_l2` reads layer 0's Count Sketch F2 estimate in standard mode and sums the strata's in terminal mode; new `top_layer_complete()`. The API doc states the bounds in the two-tier layout's terms, with Monte Carlo tests. Ports #222. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(hydra): group-total query from per-cell mass `Hydra::query_group_total(key)` returns a subpopulation's estimated total mass: the minimum over rows of the subkey's cell `HydraCounter::total_mass()`, a Count-Min cell's inner row sum or a KLL cell's retained weight. Count Sketch, HyperLogLog and UnivMon counters return an error. The total is read from existing cell state, so the wire format is unchanged. `query_key` and the new query share one subkey hash helper. Ports #223. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * perf(kll): allocate the item buffer as items arrive Kll's item buffer starts empty, then holds 16 items and doubles up to max_capacity; a full buffer compacts only at max_capacity, so retained items, coin tosses, quantiles, merges and ASAPv1 bytes match the eagerly allocated buffer for the same input and seed. The merge scratch buffer starts empty; merge, from_portable_state and the ASAPv1 decoder size the buffer to what they hold. lazy_allocation_matches_eager compares every level and the coin with a test-only eager copy (k = 8 to 1024, up to 50,000 items, with a merge). Ports #238. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(kll): Kll::memory_bytes and KllDynamic::memory_bytes Both report the bytes the sketch holds now: Kll its struct, item buffer, level array, merge scratch space and memoized CDF; KllDynamic its struct, item buffer and level bounds. Ports #238. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(kmv): estimate and merge by shared reference, memory_bytes, early reject past the k-th hash Kmv::estimate takes &self and Kmv::merge takes &Kmv. Kmv::memory_bytes is the struct plus the hash slots the heap has allocated (CommonHeap::allocated_slots, crate-private). A full sketch turns away a hash at or above its largest retained one before scanning the retained hashes for a duplicate; the retained set and every estimate are unchanged. Ports #228. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(heap)!: TopKHeap::estimated_memory_bytes; UnivMonQ::memory_bytes TopKHeap::estimated_memory_bytes(heap_size, key) is the bytes a heap of that capacity holds in steady state once keys shaped like key churn through it: the struct, entry and digest arrays, the digest index (std HashMap layout, grown once more when tombstones force a resize of an index over half full) and the owned key bytes. A heap holds at most this. UnivMonQ::estimated_memory_bytes(&self) becomes memory_bytes(&self) and counts what the instance holds: its counters, candidate tables (bucket count from the largest capacity() seen), candidate heaps and ordered-sample heap. tests/heap_memory_bytes.rs checks both against a counting global allocator: the heap estimate as an upper bound after a fill and exactly after churn, UnivMon-Q exactly. Ports #233 (the heap half). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(cms,cs)!: check_shape, clear and memory accounting for the matrix sketches CountMinSketch, CountSketch and their WithHeap and WithSpaceSaving forms gain check_shape(rows, cols), which accepts the ASAPv1 geometry, 1 <= rows <= MATRIX_MAX_ROWS and 1 <= cols <= u32::MAX, on every hashing path. with_dimensions, new and from_storage panic on any other shape. Past 20 rows the regular path and the fast path's per-row (Rows) layout hash row r + 20 with row r's seed; the packed layouts reuse no seed. Tests that built such a shape to watch the encoder refuse it check check_shape instead, Hydra's included. clear() zeroes the counters (and empties the heap for the WithHeap types) and returns the Nitro sampler to its start (Nitro::reset, through the new MatrixStorage::reset_sampler), keeping shape, allocation and admission rate. TopKHeap::memory_bytes reports the bytes a heap holds, taking the digest index's bucket count from the largest capacity() it has reported, which evictions' tombstones cannot lower. The WithHeap types add memory_bytes and estimated_memory_bytes(rows, cols, heap_size, key). tests/heap_memory_bytes.rs checks all of them against a counting allocator, after a fill and after 50 times heap_size evicting keys. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(hash)!: a seed for every seed index; matrix sketches take any row count Seed index i hashes with seed_for_index(i) = SEED_LIST[i % 20] * (i / 20 + 1) + i / 20 in wrapping u64 arithmetic; indexes below 20 keep their seed. The matrix hash passes seed indexes through unreduced, so rows past 20 on the regular path and the fast path's per-row layout hash independently, and UnivMon-Q's occurrence priorities hash at their own seed. check_shape on Count-Min, Count Sketch and their WithHeap and WithSpaceSaving forms refuses only a zero dimension and a counter count past usize; Elastic, Hydra and UnivMon build any row count. to_asapv1 and from_asapv1 keep the MATRIX_MAX_ROWS bound. The UnivMon-Q residual-CDF accuracy test hashes each of its 32 sketches at its own seed index. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(cms,cs)!: memory_bytes, CountMinF64 at the root, check_shape without type parameters CountMinSketch and CountSketch gain memory_bytes(&self): the struct plus its rows * cols counters, exact for Vector2D storage, checked against a counting allocator in tests/heap_memory_bytes.rs. CountMinF64 is re-exported from sketches and the crate root. check_shape on CountMinSketch, CountSketch, their WithHeap and WithSpaceSaving forms is defined on the type with its default parameters, so CountMinSketch::check_shape(3, 64) resolves without naming storage, path or hasher. A call naming non-default type parameters no longer compiles. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(asapv1)!: seed_derivation is "seed_list_index_scaled" The ASAPv1 seed_derivation string names seed_for_index. Bytes carrying "seed_list_index_wrap" no longer decode. The golden submodule moves to the regenerated fixtures (sketchlib-golden-bytes#3). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: zz_y <zeyingz@umd.edu> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
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
A planner prices a top-k heap before it builds one, and today it has to guess the heap's bytes. ASAPPlanner charges 24 bytes per heap entry for Count-Min/Count Sketch with a heap and for UnivMon, while the library holds far more: a
WeightedFrequencycandidate is a 56-byte struct plus its identity tuple and its encoded key, and anHHHeapresident is a 48-byteHHItem, an 8-byte digest and a 40-byte index entry. Neither type says what it holds, so every caller re-derives (and drifts from) the layout.While measuring this, a retained
WeightedFrequencycandidate turned out to hold its identity tuple in a four-slotVec(fromcollect::<Result<Vec<_>, _>>(), whose size hint starts at 0): 72 spare bytes per candidate for a one-value identity.What
HHHeap::estimated_memory_bytes(k, key): bytes a heap of capacitykholds once full of keys shaped likekey.WeightedFrequency::estimated_memory_bytes(width, depth, capacity, identity): bytes a state of that shape holds once it retainscapacitycandidates shaped likeidentity.Stacked on #227 (the
WeightedFrequencystack: #160 → #225 → #226 → #227), sinceWeightedFrequencyis not onmainyet.How
Both functions add up the parts from the types' own
size_ofand the containers' growth:HHHeap: the struct, theHHItemarray and theu64digest array (both reservemin(k, 1024)and double from there), the digest index (stdHashMapbuckets for that reservation, grown as a fresh fill grows them, with its control bytes), and each resident's owned key bytes (a string's or byte slice's length, 0 for a number).WeightedFrequency: the struct,width × depth8-byte cells, the candidate heap's slots (it is rebuilt each update by pushing up tocapacity + 1candidates, somax(4, next power of two ≥ capacity + 1)slots), and per candidate its identity tuple (size_of::<FrequencyIdentity>()per value plusUtf8bytes) and its key (bincode::serialized_sizeof the identity).Left out, and said so in the docs: index tombstones from evictions can make the
HHHeapindex rehash into twice its buckets, and aWeightedFrequencyupdate briefly holds the previous heap's slots too.Key code interfaces added / modified
src/common/heap.rssrc/sketches/weighted_frequency.rsUsage, end to end: a planner pricing a top-k state per group before it builds any (numbers from running these calls at the head commit).
Before this PR
A caller had no way to ask what a heap holds, and guessed: ASAPPlanner priced 24 bytes per entry. A
WeightedFrequencystate of capacity 1,000 with Int64 identities held 174,960 bytes, 72 of them per candidate in unused identity slots.After this PR
The same state holds 102,960 bytes, and
estimated_memory_bytesreturns exactly that before the state exists. ASAPPlanner's plan selection can read these functions instead of hardcoding a per-entry size once its pin includes this PR.Verification
tests/heap_memory_bytes.rs(new; failed to compile before the functions existed): checks both functions againstsize_ofand against the bytes actually allocated, counted by a global allocator:HHHeapfilled withk∈ {1, 3, 7, 8, 100, 1024, 2000, 5000} u64 keys and 9-byte keys, andWeightedFrequencyafter4 × capacityupdates for capacity ∈ {1, 7, 64, 100, 1000} with Int64 and Utf8+Bool identities. Passes.cargo test --all-features --locked --lib -- weighted_frequency heap: 120 passed. Doc tests forweighted_frequency: pass.cargo fmt --all -- --check,cargo clippy --workspace --all-targets --all-features --locked -- -D warnings: clean.HashMap's table layout on the build target (16-byte control groups with SSE2 on x86, 8 elsewhere); a std change to that layout would show up as a failure of this test.