andygrove opened a new pull request, #6943:
URL: https://github.com/apache/datafusion-comet/pull/6943
## Which issue does this PR close?
Part of #6941.
## Rationale for this change
Arrow's offsets buffers are running sums, so they barely compress: seven
short string columns carry 28 bytes per row of offsets, and LZ4 keeps all of
them while zstd keeps about 16 (#5678 has the investigation). Lengths compress
far better. Stored as lengths in byte planes, the offsets of a block mostly
turn into a few long runs.
With zstd that makes shuffle files with string, binary, array and map
columns 9% to 40% smaller, and zstd gets no slower, because it has less to do.
LZ4 is left out for now: lz4_flex decodes the long runs that lengths compress
into two bytes at a time, which makes LZ4 decoding much slower
(PSeitz/lz4_flex#227 would fix that). Snappy also spends more time than it
saves.
`shuffle_bench`, one map task, 16 partitions, 8192-row batches. CPU is per
block on real shuffle blocks, the minimum of 21 interleaved runs on an Apple M3
Max.
| Data | zstd bytes per row, before → after | Encode | Decode |
| --- | --- | --- | --- |
| Short IDs, fixed length | 31.2 → 25.3 (−19%) | −2% | +1% |
| Short IDs, variable length | 40.5 → 26.4 (−35%) | | |
| Vocabulary strings | 48.1 → 31.0 (−36%) | −13% | −15% |
| Random strings | 68.8 → 49.8 (−28%) | | |
| TPC-H SF1 `lineitem` | 39.9 → 31.0 (−22%) | −4% | −3% |
| Structs nested 6 deep | 31.5 → 20.8 (−34%) | −3% | −10% |
| One `Int64` column | 4.6 → 4.6 (0%) | 0% | 0% |
From 16 to 4,000 partitions the savings range from 9% to 40%. Spark 4.1 with
zstd writes 28.1, 26.4, 39.9, 69.1, 47.5 and 37.6 bytes per row for the first
six, so at 16 partitions Comet's zstd shuffle is now at or below Spark's on all
of them. At 4,000 partitions the two short-ID datasets stay 6% to 9% above
Spark.
## What changes are included in this PR?
- `native/shuffle/src/offsets_as_lengths.rs` (new): which IPC body buffer of
each type holds offsets, mirroring arrow-ipc's `write_array_data`, and the
in-place rewrite between offsets and lengths in byte planes. The first entry is
kept, so any offsets round-trip, including ones that don't start at zero.
- `ShuffleBlockWriter::with_offsets_as_lengths` (off by default, zstd only)
rewrites the encoded body on the dictionary-free local path and sets the high
bit of the codec tag's first byte. The existing tags are ASCII, and the JVM
never reads the tag.
- `read_ipc_compressed_impl`, which every read path goes through, rebuilds
the offsets of flagged blocks under any codec, so turning lengths on for LZ4
later only changes the writer. It bounds-checks every buffer it touches, and
rejects a flagged block whose schema has no mapped layout.
- `spark.comet.shuffle.compression.offsetsAsLengths.enabled` (default true,
effective only with zstd), passed through the `ShuffleWriter` protobuf to
native shuffle and through `writeSortedFileNative` to the JVM columnar shuffle.
- Unchanged byte for byte: Celeborn writers, dictionary schemas, schemas
without offsets, and types whose layout isn't mapped (views, unions, run-end
encoding).
- `shuffle_bench --offsets-as-lengths`, and a paragraph in the shuffle
tuning guide.
## How are these changes tested?
- Native unit tests:
- Buffer roles match arrow-ipc's layout for every mapped type.
- The rewrite round-trips any bit pattern, leaves other buffers and
trailing bytes alone, and rejects out-of-range buffers.
- Blocks of every mapped type round-trip under all four codecs, on both
the trusted and the validating decode. The data includes nulls, empty values,
and sliced inputs whose offsets don't start at zero.
- Lengths are used only when asked for, and only with zstd.
- Blocks without offsets, dictionary blocks and Celeborn blocks keep the
plain tag.
- A flagged block with an unmapped layout is an error.
- New tests in `CometNativeShuffleSuite` and `CometColumnarShuffleSuite`,
the latter run through `CometShuffleSuite` and `DisableAQECometShuffleSuite`.
They check answers against Spark, check that zstd writes fewer shuffle bytes
with the setting on, and check that LZ4 writes identical bytes either way. The
columnar test covers both the bypass-merge and the sort-based writer.
- Run locally, 210 tests in total, all passing: `CometNativeShuffleSuite`,
`CometShuffleSuite`, `DisableAQECometShuffleSuite`,
`CometShuffleEncryptionSuite`, `CometShuffleInputConversionSuite`,
`SpillSorterSuite`, `CometDiskBlockWriterSuite`, plus the shuffle crate's tests
and core's shuffle tests.
- Before narrowing this to zstd, I checked the same rewrite with every codec
on 7 datasets. For 112 combinations of dataset, codec and partition count,
files written with and without it decoded to identical batches, 496M rows in
all. With the rewrite enabled for all codecs, a reader that ignored the flag
failed 26 of 73 `CometNativeShuffleSuite` tests, so the suites do exercise the
new blocks.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]