Skip to content

Deterministically compute parquet page fragments that first see dictionary keys - #22323

Closed
mhaseeb123 wants to merge 35 commits into
NVIDIA:mainfrom
mhaseeb123:fea/deterministic-dict-encode
Closed

Deterministically compute parquet page fragments that first see dictionary keys#22323
mhaseeb123 wants to merge 35 commits into
NVIDIA:mainfrom
mhaseeb123:fea/deterministic-dict-encode

Conversation

@mhaseeb123

@mhaseeb123 mhaseeb123 commented Apr 29, 2026

Copy link
Copy Markdown
Contributor

Description

Follow up #22279. Closes #13995

This PR enables parquet writer to deterministically compute the fragment index that first inserts a key into the dictionary improving the spatial locality of keys across written pages.

Checklist

  • Measure any performance impact from this PR
  • I am familiar with the Contributing Guidelines.
  • New or existing tests cover these changes.
  • The documentation is up to date with these changes.

mhaseeb123 and others added 28 commits April 22, 2026 20:38
Repurpose the unused mapped_type slot in populate_chunk_hash_maps_kernel
to carry the fragment index of the block that first inserts each value.
Rewrite collect_map_entries_kernel to bucket dict_ids by that fragment
index so pages that only reference earlier fragments' values see small
max dict_index.

No file-size delta in isolation; prerequisite for the per-page bit-width
change landing next.

Made-with: Cursor
…#13995)

Each data page now RLE-encodes dictionary indices using
ceil(log2(page_max_dict_index + 1)) bits instead of the chunk-wide
maximum. Combined with the first-appearance dict_id ordering from the
prior commit, this closes the ~30% file-size gap vs Spark on
moderate-cardinality INT64 and STRING workloads.

Page size estimation continues to use the chunk-wide bits as a
conservative upper bound; the dictionary page itself is unaffected.

Made-with: Cursor
@copy-pr-bot

copy-pr-bot Bot commented Apr 29, 2026

Copy link
Copy Markdown

This pull request requires additional validation before any workflows can run on NVIDIA's runners.

Pull request vetters can view their responsibilities here.

Contributors can view more details about this message here.

@mhaseeb123 mhaseeb123 changed the title 🚧 Deterministically compute the parquet page fragment that first writes a dictionary key 🚧 Deterministically compute parquet page fragments that first see dictionary keys Apr 29, 2026
@GregoryKimball GregoryKimball moved this to Burndown in libcudf May 4, 2026
Comment on lines +317 to +335
// Initialize all fragment offsets to 0
for (auto idx = t; idx < num_frags; idx += block_size) {
fragment_offsets[idx] = 0;
}
__syncthreads();

// Iterate over slots and count the number of dict values first seen page fragment
{
for (auto slot_idx = t; slot_idx < chunk.dict_map_size; slot_idx += block_size) {
auto const* slot = map_storage.data() + chunk.dict_map_offset + slot_idx;
if (slot->first != KEY_SENTINEL) {
auto const frag_loc = static_cast<size_type>(slot->second) - frag_start;
cudf_assert(frag_loc >= 0 && frag_loc < num_frags &&
"fragment index in the slot is out of range of the chunk");
atomicAdd(&fragment_offsets[frag_loc], 1);
}
}
__syncthreads();
}

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.

@pmattione-nvidia Part 1, phase 1 with this PR becomes this

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.

Is the time spent here on reading slot->second or on the add? If it's on reading slot->second you could keep counters while filling the map in the first place.

@mhaseeb123 mhaseeb123 added 2 - In Progress Currently a work in progress improvement Improvement / enhancement to an existing function non-breaking Non-breaking change labels May 8, 2026
@mhaseeb123 mhaseeb123 moved this from Burndown to Slip in libcudf May 14, 2026
rapids-bot Bot pushed a commit that referenced this pull request May 15, 2026
…22279)

Contributes to #13995

This PR enables the Parquet writer to assign lower dictionary indices to elements appearing earlier in each column chunk thereby writing variable (reduced) number of dictionary key bits for earlier pages.

New algorithm:
1. `map_insert_fn` now inserts this pair in the static map: `{row_idx, frag_idx}` instead of `{row_idx, row_idx}`. `frag_idx` is just `blockIdx.x` and indicates which page fragment actually inserted this entry in the static map (CAS race dependent but earlier thread blocks generally win this race)
2. `map_insert_fn` also writes the `PageFragment::num_dict_vals` field for each page fragment to keep a record of number of unique values inserted by it.
3. Then in `collect_map_entries_kernel` (where we assign keys to dict values), we optionally assign spatially-local keys to dictionary values inserted by the same page fragment. See algorithm details [here](#22279 (comment))
4. Finally, we launch `compute_page_dict_bits_kernel` to compute the max bit width required to encode dict keys for each parquet page. See algorithm [here](#22279 (comment))

See follow up PR #22323 that deterministically writes the index of the first page fragment that sees each dictionary key

Authors:
  - Muhammad Haseeb (https://github.com/mhaseeb123)

Approvers:
  - Vukasin Milovanovic (https://github.com/vuule)
  - Paul Mattione (https://github.com/pmattione-nvidia)
  - Nghia Truong (https://github.com/ttnghia)

URL: #22279
@mhaseeb123 mhaseeb123 changed the title 🚧 Deterministically compute parquet page fragments that first see dictionary keys Deterministically compute parquet page fragments that first see dictionary keys May 15, 2026
@mhaseeb123

mhaseeb123 commented Jun 1, 2026

Copy link
Copy Markdown
Contributor Author

Performance Impact

  • Benchmark name: PARQUET_WRITER_NVBENCH suite.
  • Hardware: NVIDIA B200, driver 580.82.07.

Summary

Parquet writer-speed regression reported across 58 configurations of the benchmark suite, typically +5–12% in parquet_write_options, parquet_write_io_compression, and integer/list parquet_write_encode.

Raw numbers (sorted by % diff)

Click to expand
Benchmark Axes main (ms) PR (ms) Δ noise(m/p)
parquet_write_options statistics=STATISTICS_NONE compression_type=LZ4 row_group_size_bytes=0 row_group_size_rows=0 241.070 271.127 +12.47% 0.4%/2.5%
parquet_write_dict_encoding reverse_order=1 num_rows=1000000 page_size_rows=100000 cardinality=100000 freq_set_ratio=0.01 7.477 8.323 +11.32% 4.7%/9.4%
parquet_write_options statistics=STATISTICS_NONE compression_type=ZSTD row_group_size_bytes=0 row_group_size_rows=0 230.911 256.844 +11.23% 0.3%/2.6%
parquet_write_options statistics=STATISTICS_COLUMN compression_type=SNAPPY file_path=unused_path.parquet row_group_size_bytes=0 row_group_size_rows=0 241.141 267.990 +11.13% 0.3%/3.2%
parquet_write_io_compression io_type=FILEPATH compression_type=NONE cardinality=0 run_length=1 row_group_size_bytes=0 row_group_size_rows=0 398.446 355.024 -10.90% 12.5%/9.5%
parquet_write_options statistics=STATISTICS_PAGE compression_type=ZSTD row_group_size_bytes=0 row_group_size_rows=0 237.486 262.206 +10.41% 2.4%/0.2%
parquet_write_options statistics=STATISTICS_PAGE compression_type=LZ4 row_group_size_bytes=0 row_group_size_rows=0 251.800 276.381 +9.76% 3.6%/0.4%
parquet_write_dict_encoding reverse_order=1 num_rows=1000000 page_size_rows=10000 cardinality=100000 freq_set_ratio=0.001 6.838 7.482 +9.42% 7.1%/8.0%
parquet_write_dict_encoding reverse_order=1 num_rows=1000000 page_size_rows=10000 cardinality=64000 freq_set_ratio=0.01 6.214 6.782 +9.14% 6.8%/8.1%
parquet_write_io_compression io_type=VOID compression_type=NONE cardinality=1000 run_length=1 row_group_size_bytes=0 row_group_size_rows=0 100.962 109.765 +8.72% 0.3%/0.3%
parquet_write_encode data_type=INTEGRAL cardinality=1000 run_length=1 row_group_size_bytes=0 row_group_size_rows=0 16.768 18.209 +8.59% 0.3%/0.2%
parquet_write_encode data_type=INTEGRAL cardinality=10000 run_length=1 row_group_size_bytes=0 row_group_size_rows=0 18.044 19.519 +8.17% 0.6%/0.2%
parquet_write_io_compression io_type=HOST_BUFFER compression_type=ZSTD cardinality=1000 run_length=1 row_group_size_bytes=0 row_group_size_rows=0 265.835 287.337 +8.09% 2.2%/3.0%
parquet_write_io_compression io_type=VOID compression_type=NONE cardinality=1000 run_length=32 row_group_size_bytes=0 row_group_size_rows=0 94.624 102.165 +7.97% 0.4%/0.3%
parquet_write_io_compression io_type=FILEPATH compression_type=SNAPPY cardinality=1000 run_length=32 row_group_size_bytes=0 row_group_size_rows=0 140.757 151.568 +7.68% 1.8%/1.7%
parquet_write_io_compression io_type=VOID compression_type=LZ4 cardinality=1000 run_length=32 row_group_size_bytes=0 row_group_size_rows=0 104.856 112.853 +7.63% 0.2%/0.2%
parquet_write_options statistics=STATISTICS_NONE compression_type=SNAPPY row_group_size_bytes=0 row_group_size_rows=0 250.628 268.926 +7.30% 4.2%/3.2%
parquet_write_io_compression io_type=VOID compression_type=SNAPPY cardinality=1000 run_length=32 row_group_size_bytes=0 row_group_size_rows=0 103.418 110.917 +7.25% 0.2%/0.2%
parquet_write_io_compression io_type=FILEPATH compression_type=ZSTD cardinality=1000 run_length=32 row_group_size_bytes=0 row_group_size_rows=0 142.670 153.012 +7.25% 2.2%/1.5%
parquet_write_options statistics=STATISTICS_NONE compression_type=SNAPPY file_path=unused_path.parquet row_group_size_bytes=0 row_group_size_rows=0 246.573 263.905 +7.03% 1.9%/3.1%
parquet_write_io_compression io_type=VOID compression_type=LZ4 cardinality=1000 run_length=1 row_group_size_bytes=0 row_group_size_rows=0 128.740 137.666 +6.93% 0.2%/0.5%
parquet_write_encode data_type=LIST cardinality=0 run_length=1 row_group_size_bytes=0 row_group_size_rows=0 122.232 130.549 +6.80% 0.1%/0.2%
parquet_write_io_compression io_type=VOID compression_type=SNAPPY cardinality=1000 run_length=1 row_group_size_bytes=0 row_group_size_rows=0 119.298 127.259 +6.67% 0.3%/0.5%
parquet_write_options statistics=STATISTICS_NONE compression_type=NONE file_path=unused_path.parquet row_group_size_bytes=0 row_group_size_rows=0 245.146 261.473 +6.66% 3.7%/3.0%
parquet_write_io_compression io_type=VOID compression_type=ZSTD cardinality=1000 run_length=32 row_group_size_bytes=0 row_group_size_rows=0 109.804 117.100 +6.64% 0.5%/0.3%
parquet_write_io_compression io_type=HOST_BUFFER compression_type=LZ4 cardinality=1000 run_length=32 row_group_size_bytes=0 row_group_size_rows=0 111.910 119.282 +6.59% 1.3%/0.2%
parquet_write_io_compression io_type=HOST_BUFFER compression_type=SNAPPY cardinality=1000 run_length=1 row_group_size_bytes=0 row_group_size_rows=0 245.891 261.983 +6.54% 1.5%/2.6%
parquet_write_io_compression io_type=HOST_BUFFER compression_type=SNAPPY cardinality=1000 run_length=32 row_group_size_bytes=0 row_group_size_rows=0 110.179 117.189 +6.36% 1.8%/0.3%
parquet_write_io_compression io_type=FILEPATH compression_type=NONE cardinality=1000 run_length=32 row_group_size_bytes=0 row_group_size_rows=0 141.179 149.999 +6.25% 2.3%/2.3%
parquet_write_dict_encoding reverse_order=0 num_rows=1000000 page_size_rows=10000 cardinality=100000 freq_set_ratio=0.001 5.971 6.332 +6.04% 4.2%/3.5%
parquet_write_options statistics=STATISTICS_ROWGROUP compression_type=ZSTD file_path=unused_path.parquet row_group_size_bytes=0 row_group_size_rows=0 244.595 259.346 +6.03% 3.2%/2.0%
parquet_write_io_compression io_type=HOST_BUFFER compression_type=NONE cardinality=1000 run_length=32 row_group_size_bytes=0 row_group_size_rows=0 121.716 129.018 +6.00% 1.1%/0.5%
parquet_write_io_compression io_type=HOST_BUFFER compression_type=ZSTD cardinality=1000 run_length=32 row_group_size_bytes=0 row_group_size_rows=0 115.353 122.259 +5.99% 0.5%/0.2%
parquet_write_io_compression io_type=VOID compression_type=ZSTD cardinality=1000 run_length=1 row_group_size_bytes=0 row_group_size_rows=0 136.807 144.939 +5.94% 0.3%/0.3%
parquet_write_options statistics=STATISTICS_NONE compression_type=ZSTD file_path=unused_path.parquet row_group_size_bytes=0 row_group_size_rows=0 243.503 257.785 +5.87% 2.0%/0.5%
parquet_write_options statistics=STATISTICS_ROWGROUP compression_type=SNAPPY file_path=unused_path.parquet row_group_size_bytes=0 row_group_size_rows=0 251.349 265.362 +5.58% 3.9%/2.7%
parquet_write_io_compression io_type=FILEPATH compression_type=LZ4 cardinality=1000 run_length=32 row_group_size_bytes=0 row_group_size_rows=0 144.384 152.348 +5.52% 2.2%/1.4%
parquet_write_options statistics=STATISTICS_NONE compression_type=LZ4 file_path=unused_path.parquet row_group_size_bytes=0 row_group_size_rows=0 256.967 271.053 +5.48% 2.5%/3.2%
parquet_write_options statistics=STATISTICS_PAGE compression_type=NONE file_path=unused_path.parquet row_group_size_bytes=0 row_group_size_rows=0 237.798 250.563 +5.37% 4.7%/4.0%
parquet_write_options statistics=STATISTICS_ROWGROUP compression_type=SNAPPY row_group_size_bytes=0 row_group_size_rows=0 251.380 264.659 +5.28% 2.9%/3.7%
parquet_write_options statistics=STATISTICS_NONE compression_type=NONE row_group_size_bytes=0 row_group_size_rows=0 240.259 252.613 +5.14% 5.1%/4.1%
parquet_write_io_compression io_type=FILEPATH compression_type=LZ4 cardinality=1000 run_length=1 row_group_size_bytes=0 row_group_size_rows=0 247.855 260.288 +5.02% 2.5%/3.3%
parquet_write_options statistics=STATISTICS_COLUMN compression_type=ZSTD row_group_size_bytes=0 row_group_size_rows=0 250.357 262.442 +4.83% 2.3%/2.0%
parquet_write_options statistics=STATISTICS_ROWGROUP compression_type=NONE row_group_size_bytes=0 row_group_size_rows=0 243.267 254.489 +4.61% 5.9%/5.9%
parquet_write_options statistics=STATISTICS_ROWGROUP compression_type=NONE file_path=unused_path.parquet row_group_size_bytes=0 row_group_size_rows=0 258.318 270.149 +4.58% 4.1%/3.8%
parquet_write_io_compression io_type=FILEPATH compression_type=SNAPPY cardinality=0 run_length=1 row_group_size_bytes=0 row_group_size_rows=0 520.141 542.951 +4.39% 11.9%/15.0%
parquet_write_options statistics=STATISTICS_COLUMN compression_type=ZSTD file_path=unused_path.parquet row_group_size_bytes=0 row_group_size_rows=0 252.761 263.836 +4.38% 2.9%/2.1%
parquet_write_options statistics=STATISTICS_PAGE compression_type=LZ4 file_path=unused_path.parquet row_group_size_bytes=0 row_group_size_rows=0 262.904 251.550 -4.32% 0.3%/0.4%
parquet_write_options statistics=STATISTICS_COLUMN compression_type=LZ4 row_group_size_bytes=0 row_group_size_rows=0 267.971 279.543 +4.32% 2.2%/3.2%
parquet_write_io_compression io_type=FILEPATH compression_type=ZSTD cardinality=1000 run_length=1 row_group_size_bytes=0 row_group_size_rows=0 251.898 262.256 +4.11% 3.0%/3.7%
parquet_write_options statistics=STATISTICS_COLUMN compression_type=SNAPPY row_group_size_bytes=0 row_group_size_rows=0 244.176 254.106 +4.07% 0.3%/1.0%
parquet_write_io_compression io_type=FILEPATH compression_type=ZSTD cardinality=0 run_length=1 row_group_size_bytes=0 row_group_size_rows=0 457.308 439.167 -3.97% 7.7%/9.9%
parquet_write_options statistics=STATISTICS_COLUMN compression_type=NONE row_group_size_bytes=0 row_group_size_rows=0 256.795 247.416 -3.65% 6.0%/0.5%
parquet_write_options statistics=STATISTICS_COLUMN compression_type=LZ4 file_path=unused_path.parquet row_group_size_bytes=0 row_group_size_rows=0 280.795 270.813 -3.55% 0.5%/0.3%
parquet_write_options statistics=STATISTICS_ROWGROUP compression_type=LZ4 file_path=unused_path.parquet row_group_size_bytes=0 row_group_size_rows=0 265.523 274.752 +3.48% 2.2%/3.5%
parquet_write_io_compression io_type=FILEPATH compression_type=LZ4 cardinality=0 run_length=1 row_group_size_bytes=0 row_group_size_rows=0 445.173 431.004 -3.18% 8.9%/7.5%
parquet_write_options statistics=STATISTICS_PAGE compression_type=SNAPPY file_path=unused_path.parquet row_group_size_bytes=0 row_group_size_rows=0 248.545 241.549 -2.81% 3.1%/0.4%
parquet_write_options statistics=STATISTICS_PAGE compression_type=ZSTD file_path=unused_path.parquet row_group_size_bytes=0 row_group_size_rows=0 237.897 242.577 +1.97% 2.5%/1.2%
parquet_write_options statistics=STATISTICS_COLUMN compression_type=NONE file_path=unused_path.parquet row_group_size_bytes=0 row_group_size_rows=0 263.678 268.496 +1.83% 3.7%/4.5%
parquet_write_dict_encoding reverse_order=0 num_rows=1000000 page_size_rows=10000 cardinality=64000 freq_set_ratio=0.01 5.725 5.635 -1.58% 5.1%/3.6%
parquet_write_io_compression io_type=FILEPATH compression_type=NONE cardinality=1000 run_length=1 row_group_size_bytes=0 row_group_size_rows=0 228.136 231.438 +1.45% 7.1%/0.2%
parquet_write_io_compression io_type=FILEPATH compression_type=SNAPPY cardinality=0 run_length=32 row_group_size_bytes=0 row_group_size_rows=0 139.373 141.367 +1.43% 4.6%/2.9%
parquet_write_options statistics=STATISTICS_ROWGROUP compression_type=LZ4 row_group_size_bytes=0 row_group_size_rows=0 261.618 264.939 +1.27% 4.0%/2.9%
parquet_write_io_compression io_type=FILEPATH compression_type=SNAPPY cardinality=1000 run_length=1 row_group_size_bytes=0 row_group_size_rows=0 243.634 240.745 -1.19% 2.9%/3.1%
parquet_write_dict_encoding reverse_order=0 num_rows=1000000 page_size_rows=100000 cardinality=100000 freq_set_ratio=0.01 6.920 6.985 +0.94% 7.1%/4.3%
parquet_write_options statistics=STATISTICS_PAGE compression_type=SNAPPY row_group_size_bytes=0 row_group_size_rows=0 245.960 247.763 +0.73% 4.5%/2.9%
parquet_write_options statistics=STATISTICS_ROWGROUP compression_type=ZSTD row_group_size_bytes=0 row_group_size_rows=0 249.716 249.051 -0.27% 0.4%/0.5%
parquet_write_options statistics=STATISTICS_PAGE compression_type=NONE row_group_size_bytes=0 row_group_size_rows=0 248.385 248.945 +0.23% 3.3%/3.4%
parquet_write_dict_encoding reverse_order=0 num_rows=1000000 page_size_rows=100000 cardinality=100000 freq_set_ratio=0.001 6.489 6.479 -0.16% 5.8%/5.3%

@pmattione-nvidia

Copy link
Copy Markdown
Contributor

Make sure to modify this to reduce the # of atomic operations, per 2nd paragraph of #2) at #13995 (comment)

@mhaseeb123

Copy link
Copy Markdown
Contributor Author

Make sure to modify this to reduce the # of atomic operations, per 2nd paragraph of #2) at #13995 (comment)

Good idea. Let me first verify if the slowdown is in the insert or collect kernel. If it's in collect from the new loops, then this PR may be doomed anyway

@pmattione-nvidia

pmattione-nvidia commented Jun 3, 2026

Copy link
Copy Markdown
Contributor

10% slowdown is not a big deal if the disk space savings are large. Do you have an idea what those are? Are these tests large enough to trigger a large disk savings? The perf numbers will probably be bad if you don't implement the block reordering as it will be too many atomic cas loops.

@mhaseeb123

mhaseeb123 commented Jun 3, 2026

Copy link
Copy Markdown
Contributor Author

10% slowdown is not a big deal if the disk space savings are large. Do you have an idea what those are? Are these tests large enough to trigger a large disk savings? The perf numbers will probably be bad if you don't implement the block reordering as it will be too many atomic cas loops.

I can certainly compare but I am not sure how much further disk saving by simply making this deterministic would we get as compared to main branch.

The perf numbers will probably be bad if you don't implement the block reordering as it will be too many atomic cas loops.

I analyzed both the populate and collect kernels using ncu and nsys and the main regression (roughly 20-30% -> e2e 5-10% in benchmarks at least) is inside the collect kernel from the extra loops atomically computing the number of unique elements per fragment (agnostic of insertion order).

@mhaseeb123

mhaseeb123 commented Jun 3, 2026

Copy link
Copy Markdown
Contributor Author

Performance analysis (part 2)

Experimental Setup

Write 50 columns of mixed types — cycling through INT32, INT64, STRING, LIST, and STRUCT<INT32, STRING>, all dictionary-encoded at the leaf level (dictionary_policy::ALWAYS) for a more realistic workload.
Sweep NONE, SNAPPY, and ZSTD compression.
Report write timings alongside file sizes for every (hot pages layout × codec).

Results

hot pages layout comp filesize MiB (main) filesize MiB (PR) Δ size main ms PR ms write Δ
front NONE 208.37 207.83 −0.26% 130.6 137.0 +4.9%
front SNAPPY 200.35 199.80 −0.27% 197.3 204.9 +3.9%
front ZSTD 192.05 191.94 −0.06% 201.7 216.1 +7.1%
back NONE 378.98 378.74 −0.06% 168.0 175.9 +4.7%
back SNAPPY 326.90 326.66 −0.07% 224.8 233.3 +3.8%
back ZSTD 222.04 221.83 −0.09% 221.9 233.3 +5.1%
spread NONE 354.50 369.00 +4.09% 147.9 175.3 +18.5%
spread SNAPPY 327.94 342.32 +4.39% 219.4 235.9 +7.5%
spread ZSTD 261.24 268.85 +2.91% 227.4 243.6 +7.1%

CC: @pmattione-nvidia @GregoryKimball @vuule

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

Labels

2 - In Progress Currently a work in progress CMake CMake build issue improvement Improvement / enhancement to an existing function libcudf Affects libcudf (C++/CUDA) code. non-breaking Non-breaking change

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[FEA] Add variable bit-width keys and improved key order for Parquet dict pages

3 participants