feat(fastlanes): store a bit width per FastLanes chunk in BitPacked - #9750
feat(fastlanes): store a bit width per FastLanes chunk in BitPacked#9750mhk197 wants to merge 2 commits into
Conversation
Merging this PR will degrade performance by 3.24%
|
| Mode | Benchmark | BASE |
HEAD |
Efficiency | |
|---|---|---|---|---|---|
| ❌ | Simulation | bitpacked_compress_u32 |
36.8 µs | 53.6 µs | -31.35% |
| ❌ | Simulation | random_i8[0.5] |
66.8 µs | 90.7 µs | -26.32% |
| ❌ | Simulation | compress_rd[f64, (2000, 0.01)] |
169.3 µs | 194.1 µs | -12.8% |
| ❌ | Simulation | compress_rd[f64, (2000, 0.1)] |
169.2 µs | 194.1 µs | -12.8% |
| ❌ | Simulation | compress_rd[f64, (2000, 0.0)] |
169.2 µs | 194 µs | -12.77% |
| ❌ | Simulation | compress_rd[f32, (2000, 0.01)] |
138 µs | 157.4 µs | -12.33% |
| ❌ | Simulation | compress_rd[f32, (2000, 0.0)] |
138 µs | 157.4 µs | -12.29% |
| ❌ | Simulation | compress_rd[f32, (2000, 0.1)] |
138 µs | 157.3 µs | -12.24% |
| ⚡ | Simulation | random_i8[0.8] |
98.2 µs | 68.8 µs | +42.6% |
| ⚡ | Simulation | decompress[u64, (4000, 1024)] |
85.6 µs | 70.4 µs | +21.71% |
| ⚡ | WallTime | arrow_checked_add_u32_avx512[16384] |
21.3 µs | 17.6 µs | +21.05% |
| ⚡ | WallTime | arrow_checked_add_u32_avx2[16384] |
21.3 µs | 17.6 µs | +20.76% |
| ⚡ | Simulation | allocate_drop_arrow[0] |
456.9 ns | 402.7 ns | +13.45% |
| 🆕 | Simulation | compress_v1[u16, drift] |
N/A | 288.8 µs | N/A |
| 🆕 | Simulation | compress_v1[u16, drift+exc1%] |
N/A | 691.2 µs | N/A |
| 🆕 | Simulation | compress_v1[u16, drift+null10%] |
N/A | 516.2 µs | N/A |
| 🆕 | Simulation | compress_v1[u16, random] |
N/A | 293.5 µs | N/A |
| 🆕 | Simulation | compress_v1[u16, spiky] |
N/A | 748.5 µs | N/A |
| 🆕 | Simulation | compress_v1[u16, spiky+exc1%] |
N/A | 754.7 µs | N/A |
| 🆕 | Simulation | compress_v1[u16, uniform] |
N/A | 265.5 µs | N/A |
| ... | ... | ... | ... | ... | ... |
ℹ️ Only the first 20 benchmarks are displayed. Go to the app to view all benchmarks.
Tip
Investigate this regression by commenting @codspeedbot fix this regression on this PR, or directly use the CodSpeed MCP with your agent.
Comparing mk/bitpacked-v2 (dcd7e85) with develop (98a2c86)
Footnotes
-
164 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports. ↩
-
1 benchmark was run, but is now archived. If it was deleted in another branch, consider rebasing to remove it from the report. Instead if it was added back, click here to restore it. ↩
7de5950 to
b5b9d6d
Compare
e9b4072 to
013afc1
Compare
Each 1024-element chunk of a BitPacked array is packed at its own width, chosen from that chunk's histogram by charging the exact packed block plus its exceptions. Uniform widths still serialize as fastlanes.bitpacked, byte for byte. Differing widths serialize as fastlanes.bitpacked_v2, carrying the widths in a u8 child so metadata stays bounded and a compressor can re-encode the table; that format's metadata is offset and patches only. Following the editions model, BitPackedPlugin owns both wire formats. The encoding's own serializer is labelled with the original ID, so it returns None for differing widths rather than emitting bytes that ID's readers would misread. The compressor still picks one global width, so its output is unchanged. CUDA decodes uniform arrays and rejects differing widths until it has a per-chunk kernel. Signed-off-by: Matt Katz <mhkatz97@gmail.com>
…tic patterns Signed-off-by: Matt Katz <mhkatz97@gmail.com>
013afc1 to
dcd7e85
Compare
Summary
BitPackednow stores a bit width per 1024-element FastLanes chunk, and serializes under one of two wire formats following the model indocs/specs/editions.md(one in-memory array, one wire ID per revision):fastlanes.bitpacked(unchanged, frozen): emitted whenever every chunk shares one width. Metadata and packed bytes are identical to today, so old readers keep reading new files and new readers keep reading old ones.fastlanes.bitpacked_v2: emitted only when chunk widths differ. The width table travels as au8child array with one entry per chunk, so metadata stays bounded and a compressor can re-encode the table; the metadata is the original struct withbit_widthset to the widest chunk. Old readers reject it as an unknown encoding. It is not in any edition yet, so the default writer cannot produce it; a follow-up adds the edition entry and compressor scheme.Following
docs/specs/editions.md, oneArrayPluginowns both formats; the coreVTabletrait is unchanged.Nothing about the default writer's output changes:
BitPackingSchemestill picks one global width, so every array it produces serializes asfastlanes.bitpacked. The btrblocks goldens are untouched.Changes
vortex-fastlanes.BitPackedPluginregisters both wire IDs. It serializes uniform arrays through the encoding's own serializer, so that path is byte-identical to today, and differing widths asfastlanes.bitpacked_v2withBitPackedV2Metadata { offset, patches }and the width table as the last child. It deserializes both IDs and enforces each one's contract.BitPackedPatchedPluginforwards to it.Nonefor differing widths. The blanketArrayPluginimpl labels its output with the vtable's own ID,fastlanes.bitpacked, so registering the vtable directly can only ever write valid v1 or fail, never v2 bytes under the v1 ID.ChunkWidths: one width per chunk plus cached byte offsets.BitPacked::try_newtakes it;bit_width()now returns the widest chunk.bitpack_to_best_chunk_widthschooses each chunk's width from its own histogram, charging the exact packed block plus exceptions, and packs, gathers exceptions and picks widths in one pass over each chunk while it is in cache. The existingbitpack_to_best_bit_width/bitpack_encodeAPI keeps producing uniform widths.is_constant,scalar_at, the FoR fused decode) indexes chunks through the width table; each differs from before by a handful of lines. Sequential decode walks the table with a running offset.BitPacked::with_width_tableswaps in a re-encoded width table. CUDA decode accepts uniform arrays and rejects differing widths, since there is no per-chunk kernel yet.benches/bitpack_chunk_widths.rssweeps u8..u64 over uniform, drifting, random, zero-heavy and spiky width patterns, with and without exceptions and nulls, comparing the two width policies on size, compress time and decompress time.Benchmark (32 chunks per case, divan fastest, per-chunk relative to global width)
Bytes: uniform data unchanged (0 bytes of overhead, since it stays in the original format); drift 43–50% smaller; random widths 40–44%; zero-heavy 50%; spiky 16–58%.
Compress: -15% to +7% on cases without exceptions, 14–35% faster wherever exceptions exist, 35–49% faster on spiky data where per-chunk widths avoid the patches a global width needs; 19% slower on u8 spiky data with exceptions, where it takes patches for a 58% smaller array.
Decompress: within 2% on uniform data, 6–21% faster on drift, random and zero-heavy data, 25–75% faster on spiky data.
Tests
Per-chunk behaviour (roundtrips, slicing across chunk boundaries, take, filter, compare, between, cast,
is_constant, conformance harnesses), fused-versus-multipass equivalence, a short array whose single packed block exceeds its raw size, and serde: uniform and empty arrays serialize byte-identically under the original ID with no width table, differing widths under the v2 ID with the table as a child, a re-encoded table survives a round trip, the table is validated for dtype, length and presence, each ID rejects the other's children, and the bare vtable serializer refuses differing widths.Validation
cargo nextest run -p vortex-fastlanes -p vortex-array -p vortex-btrblocks -p vortex-file -p vortexcargo test --doc -p vortex-fastlanes -p vortex-arraycargo clippy --all-targets --all-featureson vortex-fastlanes, vortex-btrblocks, vortex-file, vortex-cuda;cargo +nightly fmt --all