Repository navigation
perf(encoding): shrink flat mini-block chunk index metadata - #7947
Conversation
Follow-up to lance-format#7651. The sparse structural scheduler cached a `Vec<ChunkMeta>` (num_values, byte size, byte offset per chunk) plus a separate `Arc<[u64]>` of cumulative value offsets -- ~32 bytes/chunk, all derivable from two cumulative arrays. Sparse value chunks are themselves mini-blocks, so this reuses the `MiniBlockChunkIndex` from lance-format#7651: - byte ranges come from the existing `byte_starts` prefix sums (u32 when the value buffer fits), and - value->chunk lookups use `RowMapping`, collapsing to arithmetic (`UniformFlat`) when non-last chunks share a value count, else an explicit value-starts array. This drops the redundant per-chunk byte offsets and value offsets, taking the cached state from ~32 bytes/chunk to 4-8 bytes/chunk while preserving all metadata validation. `ChunkMeta`, now unused, is removed. Co-authored-by: Cursor <cursoragent@cursor.com>
📝 WalkthroughWalkthroughSparse primitive decoding replaces cached chunk metadata and cumulative offsets with ChangesCompact chunk index migration
Estimated code review effort: 4 (Complex) | ~45 minutes Possibly related PRs
Suggested labels: Suggested reviewers: Sequence Diagram(s)sequenceDiagram
participant PageInitialization
participant build_value_chunk_index
participant SparseStructuralCacheableState
participant schedule_ranges
participant MiniBlockChunkIndex
participant append_value_range
PageInitialization->>build_value_chunk_index: metadata bytes
build_value_chunk_index->>SparseStructuralCacheableState: constructed value_index
PageInitialization->>schedule_ranges: cached value_index
schedule_ranges->>MiniBlockChunkIndex: find_chunk for visible range
schedule_ranges->>append_value_range: selected chunk range
append_value_range->>MiniBlockChunkIndex: first_row and items_in_chunk
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
🧹 Nitpick comments (1)
rust/lance-encoding/src/encodings/logical/primitive/sparse.rs (1)
5654-5670: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick winConsider covering the enlarged-last-chunk case. The test exercises
uniform_flatandflat, but not aUniformFlatpage whose last chunk holds more values than the non-last chunks (e.g.[(_, 2), (_, 4)]), which is where the arithmetic row→chunk mapping is most fragile. Adding a case that both initializes and resolves a value inside that last chunk would guard the concern raised on thebuild_value_chunk_indexuniform selection.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@rust/lance-encoding/src/encodings/logical/primitive/sparse.rs` around lines 5654 - 5670, Extend value_chunk_index_stays_compact_across_row_mappings to cover a UniformFlat layout with a larger final chunk, such as value counts 2 then 4. Initialize this case through init_value_index and resolve an index targeting a value in the final chunk, verifying the row-to-chunk mapping succeeds and remains correct.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@rust/lance-encoding/src/encodings/logical/primitive/sparse.rs`:
- Around line 5654-5670: Extend
value_chunk_index_stays_compact_across_row_mappings to cover a UniformFlat
layout with a larger final chunk, such as value counts 2 then 4. Initialize this
case through init_value_index and resolve an index targeting a value in the
final chunk, verifying the row-to-chunk mapping succeeds and remains correct.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: QUIET
Plan: Pro Plus
Run ID: 12680847-b85e-4db3-b13c-561a3cd669cd
📒 Files selected for processing (2)
rust/lance-encoding/src/encodings/logical/primitive.rsrust/lance-encoding/src/encodings/logical/primitive/sparse.rs
💤 Files with no reviewable changes (1)
- rust/lance-encoding/src/encodings/logical/primitive.rs
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
Review the sparse chunk-index migration against total cache accounting, not just heap children. Box nested-only row metadata so flat indices stay small, retain trailer bits without Arrow buffer header overhead, and remove redundant page and chunk counts. Build final prefix sums by rescanning validated metadata instead of holding two temporary per-chunk vectors. Derive complete DeepSizeOf accounting for the sparse structural plan and extend tests across non-uniform reads, enlarged final chunks, empty/single/multi-chunk memory, and all row-mapping variants. Co-authored-by: Cursor <cursoragent@cursor.com>
Adds the memory and timing coverage needed to evaluate changes to the sparse structural chunk index, which had no benchmarks of its own. `sparse_footprint` reports exact resident scheduler state, allocation volume and allocation count, so a memory claim can be checked rather than argued. `sparse_decode` times cold initialize, full scan and scattered take separately, since a change to the cached chunk index moves them independently. Layout selection is a writer-side heuristic, so `sparse_bench_layouts` asserts every input still lands on the layout its case declares. Without it a change to the rep/def budget could quietly move cases onto the dense mini-block path, leaving the benches green while measuring nothing. `run_ab.sh` and `compare.py` make the comparison reproducible: identical bench code on both arms, separate target directories per arm, and interleaved rotated rounds with a paired permutation test. Co-authored-by: Cursor <cursoragent@cursor.com>
|
Ran an evaluation on this. Summary: the memory win is large where the chunk index dominates, timings show four significant improvements and no significant regression, and there is one caveat worth knowing. I've pushed the benchmarks I used as CPUS=<physical core ids> rust/lance-encoding/benches/sparse/run_ab.sh d8030a364 539a59dac 8
Memory: resident scheduler state after
|
| case | before | after | delta |
|---|---|---|---|
uniform/many_chunks (3.2M values) |
97.91 KiB | 12.92 KiB | −86.8% |
nested/list_of_list |
49.09 KiB | 6.82 KiB | −86.1% |
wide/32_columns (32 pages) |
398.99 KiB | 71.74 KiB | −82.0% |
sweep/256k_values |
8.25 KiB | 1.71 KiB | −79.2% |
sweep/16k_values |
767 B | 795 B | +3.7% |
degenerate/single_value |
287 B | 735 B | +156.1% |
non_uniform/many_chunks |
6.21 MiB | 6.12 MiB | −1.5% |
automatic/skewed_lists |
63.23 KiB | 62.00 KiB | −1.9% |
Allocation count during initialize drops from 93 to 91 per page, which confirms the two-fewer-temporary-vectors claim exactly rather than by inference.
Timing: 8 interleaved rounds, arms rotated, pinned to physical cores
Paired per-round deltas with an exact two-sided sign-flip permutation test. Only significant results shown; everything else came back as noise or not significant, and nothing regressed significantly.
| benchmark | delta | p |
|---|---|---|
sparse_init/uniform_many_chunks |
−20.1% | 0.016 |
sparse_init/nested_list_of_list |
−18.3% | 0.016 |
sparse_init/wide_32_columns |
−14.1% | 0.031 |
sparse_take/wide_32_columns |
−11.1% | 0.016 |
Full scans are flat, as expected since the chunk index affects scheduling rather than the value-decode inner loop.
Caveat: a constant +448 bytes per sparse page
A single-chunk page goes from 287 B to 735 B. I checked whether that was fixed or scaling by measuring 64 single-value pages: the cost was exactly 64 × 448 B, so it is a flat per-page addition. The sweep puts break-even at roughly 16k leaf values per page, above which the saving climbs to ~87%. Good trade for normal pages; mildly negative only for tables of very many tiny pages.
There was a problem hiding this comment.
The narrowed change reduces flat chunk-index metadata without changing the durable format or row/chunk semantics. The earlier sparse-path concerns no longer apply.
The remaining non-blocking risk is nested-page performance: nested metadata gains one allocation and indirection, with a small increase in modeled cache weight. Nested-heavy initialization and scan latency have not been measured; a targeted comparison would quantify that cost. No correctness change is requested.
Xuanwo
left a comment
There was a problem hiding this comment.
Current head keeps the existing Planar sparse path and limits the change to private cached index metadata. Applicable CI is green, and the remaining nested-page latency uncertainty is bounded and acceptable for this scope.
Summary
MiniBlockChunkIndexis cached per page and column. ItsRowMappingkept thenested-only prefix sums, trailer bitset, and item counts inline, so the largest
variant increased the resident footprint of every flat mapping.
This moves nested-only metadata behind a
Boxand stores trailer bits as abit-packed
Box<[u8]>without an Arrow buffer header. It also derives the chunkcount from
byte_startsinstead of duplicating it inUniformFlat. The currentPlanar sparse path and shared
MiniBlockSchedulerare unchanged, and there is nofile format change.
Evidence
On the same three-chunk fixtures used by the chunk-index tests, the inline
MiniBlockChunkIndexrepresentation drops from 144 B to 72 B on the 64-bit testbuild. The cache's modeled
DeepSizeOfalso drops by 72 B for the uniform andflat fixtures. Nested pages pay one additional
Boxallocation and indirection,and their modeled weight rises by 8 B. This is representation evidence, not an
RSS or latency benchmark.