Skip to content

perf: preserve native DV bitmap compression - #6945

Open
ErikBPF wants to merge 2 commits into
apache:mainfrom
ErikBPF:perf/native-dv-bitmap-compression-20261013
Open

ErikBPF wants to merge 2 commits into
apache:mainfrom
ErikBPF:perf/native-dv-bitmap-compression-20261013

Conversation

@ErikBPF

@ErikBPF ErikBPF commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

Which issue does this PR close?

Closes #5656.

Reintroduces the accepted candidate from dwsmith1983#3 on Apache main after #5365 landed; deleting that fork's base branch closed the earlier PR.

Rationale for this change

The native-format deletion-vector decoder (magic 1681511376) deserializes compressed RoaringBitmap containers, then enumerates every deleted row and reinserts it into a RoaringTreemap. Decoding therefore scales with deletion cardinality and expands compact run containers. On base 474b5ece841a7fc6dff37ae04bd6787e654d7be7 with Roaring 0.11.5, decoding one contiguous one-million-row sub-bitmap yields 131,208 serialized bytes instead of 230.

What changes are included in this PR?

  • Move non-empty decoded sub-bitmaps directly into RoaringTreemap::from_bitmaps.
  • Keep empty implicit partitions omitted without changing subsequent partition keys.
  • Add the dense-container compression regression, which now also drives the decoded treemap through build_access_plan, and cover an empty intermediate partition in native-format decoding.

Only native/core/src/execution/delta_dv.rs changes, and only the decoder's per-row reinsertion pass is removed: access-plan construction still iterates deleted rows to build row-group selections, including partially deleted row groups. #5655 is not included.

Which tables use the native format?

Every Delta OSS writer emits the portable format (magic 1681511377) instead: Spark passes RoaringBitmapArrayFormat.Portable to DeletionVectorUtils.serialize at every deletion-vector write site, and has since DV support was open-sourced (0a0ea97bc6, 2023-10); the spec switched in c7934d3b30 (2022-12); PROTOCOL.md describes only that payload. NativeRoaringBitmapArraySerializationFormat survives on the read path alone, so this decoder is compatibility coverage for deletion-vector files written before the switch, i.e. Databricks Runtime writes predating portable. The Delta suites in the linked comment therefore never execute the changed code, and the two native unit tests here are its only coverage. If a native-emitting writer is known, naming it would make the affected table class explicit.

How are these changes tested?

  • RED: the new compression regression fails on unchanged production code with serialized size 131,208 versus the expected 230.
  • GREEN: all 44 native DV tests pass after the fix, including malformed-input, corruption, split/Parquet and memory-accounting coverage.
  • Local validation setup, the Delta/Spark integration results, and the current-head CI request are in a separate comment.

Decoder measurements

Release-mode harness over the base and patched decoder bodies, Roaring 0.11.5, identical rows and cardinalities verified before timing. One native sub-bitmap of one million positions per pattern; the contiguous fixture uses insert_range.

Pattern Deleted rows Base / patched microseconds per decode Base / patched allocated bytes
Sparse (0.1%) 1,000 40.780 / 2.715 11,888 / 3,216
Contiguous compressed run 1,000,000 18,319.494 / 0.917 657,506 / 1,282
Alternating 500,000 10,161.456 / 22.292 788,512 / 132,288

The contiguous fixture decodes to 230 serialized bytes on this patch and 131,208 on the base. Medians of seven 20-decode sample means on an unpinned shared Linux host; allocated bytes are cumulative requested bytes, not peak memory or RSS. Decoder-only, with no access-plan or I/O measurement.

@ErikBPF

ErikBPF commented Oct 13, 2026

Copy link
Copy Markdown
Contributor Author

Current-head hosted checks at 764896677 report action_required and await maintainer approval:

Please approve these fork runs and apply run-delta-tests for a hosted integration verdict. Orion results and their scope limits are in the PR body; local success does not replace hosted checks. This PR remains draft pending review and those verdicts.

@dwsmith1983 dwsmith1983 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could the description be shortened a bit? The most useful parts are the rationale, the three bullets under "What changes are included", the before/after result of the new compression test, and the decoder timing table. The details about the validation setup and the request to approve CI would work well as a separate comment on the PR instead.

One question about scope. The integration results table lists the Delta suites on Spark 3.5, 4.0 and 4.1, but as far as I can tell delta-spark writes deletion vectors in the portable format, and this PR changes only the native-format decoding path. If that's right, those suites never run the changed code, and the two unit tests here are the only coverage for it. Do you know which writers produce native-format deletion vectors in practice? Saying so in the description would make it clear which tables benefit from the change.

Comment on lines +1130 to +1144
fn native_decode_preserves_dense_bitmap_compression() {
let mut bitmap = RoaringBitmap::new();
bitmap.insert_range(0..1_000_000);
let compressed_size = bitmap.serialized_size();
let mut bitmap_bytes = Vec::new();
bitmap.serialize_into(&mut bitmap_bytes).unwrap();

let mut data = NATIVE_MAGIC.to_le_bytes().to_vec();
data.extend(1i32.to_le_bytes());
data.extend((bitmap_bytes.len() as i32).to_le_bytes());
data.extend(bitmap_bytes);

let decoded = deserialize_dv_bitmap(&data).unwrap();
let (_, decoded_bitmap) = decoded.bitmaps().next().unwrap();
assert_eq!(decoded_bitmap.serialized_size(), compressed_size);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This pins the container type, which is the point of the PR. Could it also push the decoded treemap through build_access_plan? Something like full_file_access_plan(&[600_000, 600_000], &decoded) with group 0 expected as Skip and group 1 as skip 400_000 then select 200_000, plus assert_eq!(decoded.len(), 1_000_000). Nothing in this file currently drives a run container through the plan builder, and after this change dense native DVs will.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Extended the test with the plan-builder push: full_file_access_plan(&[600_000, 600_000], &decoded) asserts group 0 is Skip, group 1 is skip 400_000 then select 200_000, and decoded.len() == 1_000_000. That is the first case in the file that drives a run container through build_access_plan. Pushed as 263ad46c8.

#[test]
fn native_format_decodes() {
let values = [0u64, 2, 3, 100, (1u64 << 32) + 7];
let values = [0u64, 2, 3, 100, (2u64 << 32) + 7];

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Worth making the intent visible here, since this value is what guards the empty-partition skip. Either a short comment ("key 1 is an empty partition, key 2 must keep its index") or an assertion on decoded.bitmaps() yielding keys [0, 2].

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added the assertion as suggested: native_format_decodes now checks decoded.bitmaps() keys are [0, 2] and names the empty partition in a comment, so the skip is asserted where it is guarded. Pushed as 263ad46c8.

ErikBPF added a commit to ErikBPF/datafusion-comet that referenced this pull request Oct 14, 2026
Review on apache#6945 asked for the empty intermediate partition to carry its
intent where the value guards it, and for the dense native DV to be driven
through build_access_plan, which nothing in this file did before.
@ErikBPF

ErikBPF commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor Author

Validation setup and the CI request, moved out of the description per review.

Current-head CI request. The draft checks at 764896677 report action_required and await maintainer approval:

Please approve these fork runs and apply run-delta-tests for a hosted integration verdict. Label-only runs are advisory and do not replace required checks.

Local validation setup. Upstream base 474b5ece841a7fc6dff37ae04bd6787e654d7be7, Rust 1.98.1, Roaring 0.11.5, Linux. Initial checks used Java 21; an independent hash-verified checkout repeated the native validation on JDK 17 and ran the JVM suites.

  • cargo fmt --all -- --check: passed.
  • cargo clippy --locked --all-targets --workspace -- -D warnings: passed.
  • cargo build --locked -p comet-test-udfs, then cargo test --locked -p datafusion-comet --lib: 874 passed, 5 ignored, zero failures. The first run's 11 missing-fixture failures were resolved by building that documented shared-library fixture; no source or assertion workaround was applied.
  • Full-reactor ./mvnw -Pspark-4.1 compile -DskipTests: passed.
  • The independent checkout passed the regression, DV and full core suites, fmt, clippy, native build and Spark 4.1 JVM compilation. Its isolated native target and Maven repository prevented cross-worktree artifact reuse; source hashes matched the reviewed candidate and the packaged Spark 4.1 JNI library matched its native build.
  • Temporary staging uses O(nonempty partitions) metadata; bitmap payloads are moved, not duplicated. No universal peak-memory claim is made.

It then ran root-reactor clean install -DskipTests, followed by test -Dtest=none -DwildcardSuites=<explicit Delta suites> for each -Pspark-<version>,delta, without -pl. XML reports and explicit command exit statuses were checked:

Local integration scope Passed Canceled Failed
Spark 3.5: Delta scan, native scan, DML, plan injector 277 1 0
Spark 4.0: same four suites 278 0 0
Spark 4.1: same four suites 278 0 0
Spark 3.5: required MinIO/S3 suite 3 0 0

Scope: these suites write and read portable-format deletion vectors, so they exercise the shared decoding path and confirm no regression there; the only cases that execute the changed native decoder are the two native unit tests. The canceled Spark 3.5 timestamp case explicitly requires Spark 4+ and passes on both 4.x profiles. COMET_DELTA_S3_REQUIRED=1 prevented a false green from unavailable containers; all three S3 tests executed through rootless Podman. These are local integration results, not hosted CI approval or complete Spark SQL-suite coverage.

Rebased onto main. The branch is now 29a1ef963 + ae0df6e9b on 8a4029ccf; main had moved six commits, none touching delta_dv.rs, and both patch-ids match the commits you reviewed. cargo fmt --all -- --check, the 44 delta_dv tests and cargo test -p datafusion-comet --lib (874 passed, 5 ignored) are clean at that head, as is workspace clippy with -D warnings. The test commit only adds assertions, so the JVM/Delta integration results above still measure the same native code.

Transfer decoded sub-bitmaps instead of reinserting each deleted row
to retain compressed runs and implicit high-partition keys.

Closes apache#5656
Review on apache#6945 asked for the empty intermediate partition to carry its
intent where the value guards it, and for the dense native DV to be driven
through build_access_plan, which nothing in this file did before.
@ErikBPF
ErikBPF force-pushed the perf/native-dv-bitmap-compression-20261013 branch from 263ad46 to ae0df6e Compare October 14, 2026 03:15
@ErikBPF
ErikBPF marked this pull request as ready for review October 14, 2026 03:15

@sunchao sunchao left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed the full two-commit diff from 8a4029ccf9a079385224bef8aca95d561993b221 to ae0df6e9b22689e4d040276b24ab598656188c96. The PR is not a draft. Read existing reviews, conversation comments and inline threads. Routed skills: review-comet-pr and review-comet-memory-pr.

No introduced P1/P2 issues found within this review. No substantiated existing P1/P2 concerns remain unresolved.

Summary

  • Prior state and problem: Native-format deletion-vector decoding deserialized compressed bitmaps, then enumerated and reinserted every deleted position. This added cardinality-dependent work and expanded compact run containers.
  • Design approach: Retain each decoded nonempty bitmap and transfer it into RoaringTreemap::from_bitmaps, preserving its compression and partition key.
  • Correctness: Compared Delta’s Scala serialization and row-position composition with both decoder implementations. Independent tests preserved row positions, cardinality and iterator behavior across dense runs, sparse and alternating patterns, unsigned 32-bit boundaries and empty partitions. Delta-generated native and portable fixtures each matched all 1,000,082 JVM row positions.
  • Compatibility analysis: Checked RoaringBitmapArray sources for Delta 3.3.2, 4.0.1 and 4.3.1, paired with Spark 3.5, 4.0 and 4.1. Their relevant implementations are identical. The native format retains implicit array-index keys. Spark 3.4 and 4.2 Delta contrib support remains outside the existing supported pairing. No protocol or dependency changes are introduced.
  • Key design decisions: Omitting empty bitmaps without renumbering subsequent keys preserves native-format semantics. Converting the validated nonnegative i32 count to u32 is safe. Existing framing and decoding validation remains intact.
  • Implementation sketch: The decoder accumulates owned (key, bitmap) pairs instead of calling treemap.insert per row. Tests assert keys [0, 2], preserved compression and the expected fully deleted and partially deleted row-group selections.
  • Performance: Independently reproduced 230 serialized bytes at the head versus 131,208 on the base for one million contiguous deletions. The removed reinsertion pass explains the improvement. Access-plan construction still visits deleted rows. Decoder timings, end-to-end speed and peak RSS were not independently measured.
  • Design: This is a small, localized use of the existing Roaring API. Parsing, error handling and downstream access-plan responsibilities remain easy to follow. No P1/P2 design issue was identified.
  • Abstraction & complexity: No new abstraction is introduced. Bitmap payloads move without cloning, while temporary metadata scales with nonempty partitions. Existing selector reservations and Spark memory-pool behavior are unchanged.
  • Behavioral changes worth calling out: Only native-format payloads benefit; portable decoding is unchanged. Latest release branch branch-1.1 lacks delta_dv.rs and has a build-gate Delta planner stub. Native DV support already exists in this PR’s base, so this PR optimizes that unreleased path without introducing another routing change.
  • Suggested improvements: No additional issue meets the P1/P2 reporting bar. Both earlier requests, explicit empty-partition keys and run-container access-plan coverage, are implemented at this head.

Exact-head CI: Comet CI and CodeQL report action_required. The label workflow passed. There is no hosted build/test verdict for this head.

Validation: Seven isolated tests passed using the exact extracted base/head decoder functions and pinned Roaring 0.11.5, including fixtures generated by compiling Delta’s Scala serializer. git diff --check passed. The full native test attempt could not resolve cached DataFusion 55.2.0 in offline mode. Full Comet, access-plan and Spark/Delta integration suites were not independently rerun; author-reported results are not treated as independent validation. Project files remain unchanged.

@dwsmith1983 dwsmith1983 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for applying both suggestions. I ran a few more checks on ae0df6e merged with current main, and everything holds up.

  • I compared the new native-format decoder against the one on main, using randomly generated inputs (sparse sets, dense runs, empty partitions between keys, all-empty arrays, a zero count) and malformed ones (every truncation, negative and oversized counts and sizes, random byte flips). The two return the same values on every valid input and fail with the same error on every malformed one.
  • I encoded bitmaps with Delta's own RoaringBitmapArray in the native format (delta-spark 4.3.1), including gaps between keys and values above 2^32, and the new decoder reads each one exactly. Delta writes one entry per key index and uses empty bitmaps for the gaps, which is the layout the decoder assumes.
  • Under mutation testing of deserialize_dv_bitmap, the PR's tests catch every mutant on the lines this PR changes, including dropping the empty-bitmap filter, shifting the key and going back to inserting value by value. The as u32 loop bound behaves the same as the old as u64, because count is a non-negative i32.
  • fmt, clippy and the delta_dv tests pass, and CometDeltaNativeScanSuite passes on Spark 4.1 with -Pdelta, deletion vector tests included.

One optional addition: nothing in delta_dv::tests feeds malformed native bytes, so the length checks earlier in this function are not exercised. A small table test would cover them, with a native magic followed by a short header, a count of -1, a count larger than the entries present, and a negative or oversized size. That code predates this PR, so a follow-up is fine too.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request performance

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Preserve compressed bitmap containers when decoding native-format deletion vectors

3 participants