Skip to content

Optimize partition - #25154

Draft
res-life wants to merge 7 commits into
NVIDIA:mainfrom
res-life:perf/partition-combined-cpu-plans
Draft

res-life wants to merge 7 commits into
NVIDIA:mainfrom
res-life:perf/partition-combined-cpu-plans

Conversation

@res-life

@res-life res-life commented Nov 9, 2026

Copy link
Copy Markdown
Contributor

Description

Optimize cudf::partition by selecting partition-map construction and result materialization from cached CPU metadata. The implementation reduces atomic contention, intermediate-map traffic, temporary memory, kernel-launch overhead and host/stream synchronization, while materializing eligible columns together.

The heuristics use row count, partition count, column count, fixed-width storage size, cached null counts, STRING offset type, and the character-buffer pointer. Selection is O(number of columns), with no GPU sampling, length/skew measurement, or device readback to select a performance plan. Mandatory STRING allocation-bound readbacks remain separate from heuristic selection. A validity mask with null_count() == 0 qualifies for non-null paths.

All implemented optimizations

  1. Remove per-row partition-offset atomicAdd from optimized rank/scan paths. Warp-local histograms use matching-lane ballots, population counts, and one elected writer per bin. A partition-major prefix scan supplies partition bases and preceding-block counts together, replacing contended per-row global cursor increments.

  2. Specialize two-partition work. CUB DevicePartition::If uses a counting input iterator and an output proxy to emit directly to the selected sink, avoiding a separate materialized row-index input and unnecessary general-bin machinery.

  3. Save compact local ranks when recomputation is more expensive. Tile-local ranks fit in uint16_t (2 bytes per row), rather than a full-width rank. The selected saved-rank path reuses them during emission.

  4. Recompute ranks to eliminate the N-entry saved-rank buffer. Histogram-only counting followed by rank recomputation trades extra map reads for less temporary memory and global rank-buffer traffic.

  5. Use two-tile superblocks where selected. The super2 gather-map path handles two adjacent 1,024-row microtiles per block, carries partition bases between them, and reduces block-count/prefix storage without storing per-row ranks globally.

  6. Amortize global reservation atomics. The reserve plan computes local ranks and makes one global cursor reservation per nonempty block/bin, rather than one per row. This plan still uses atomicAdd; nullable bitmap updates and the generic fallback also retain atomics.

  7. Write eligible fixed-width result columns directly. Fuse destination calculation with payload and validity emission, removing the N-entry gather map and the subsequent generic gather pass. A shared table sink materializes all selected fixed-width columns in the same kernel invocation instead of separate per-column gather launches; 1/2/4/8/16-byte storage has specialized aligned copies.

  8. Reorder wide output within shared-memory tiles. Group source rows/destinations by partition before emission to improve destination-store locality. This preserves direct output and avoids a global row permutation.

  9. Exploit cached null counts. Skip invalid payload reads/writes, omit output masks for zero-null columns, and reuse the input null counts because a complete permutation preserves them. Direct materialization bypasses generic validity-count calculation/readback, saving that host synchronization and associated stream.sync waiting time.

  10. Bypass permutation for identity cases. One partition or one row uses direct copies or an implicit identity iterator rather than an N-entry row map. Fixed-width data uses device copies; non-null STRING identity copies reuse/rebase offsets and copy the selected character range directly. Nullable STRING/nested values retain generic identity gather.

  11. Fold uniform all-NULL and CPU-known empty-STRING columns. Exclude invariant columns from partition work, partition only the reduced schema, then restore them after internal row-map/rank temporaries are released. Entirely uniform tables need boundaries only, not a row permutation; zero-column tables with rows follow the same boundary-only handling.

  12. Calculate STRING allocation sizes across result columns together. One sizing kernel reads every eligible column's sliced boundary offsets, followed by one batched pinned-host size readback. A permutation preserves each column's total character count, so output allocation does not require a separate post-gather size readback for every column. Identity STRING bounds are also read in one batch.

  13. Save synchronization time by batching STRING preparation. Allocate character buffers from the batched input-size readback, enqueue output-offset scans using reusable CUB scratch storage, and construct result columns without sequential per-column allocation-size readbacks. GPU operations remain ordered on the supplied stream; scans are still per column, not concurrent-stream scans. Required allocation/type-limit checks, descriptor uploads, and host partition-boundary readbacks can still synchronize. No isolated millisecond saving for synchronization is claimed; existing measurements are end-to-end.

  14. Fuse gathering STRING columns into one character-copy kernel launch. A two-dimensional grid assigns columns through blockIdx.y, allowing blocks for multiple result columns to execute concurrently, subject to GPU scheduling. The general fused path shares the gather/inverse maps; other selected STRING paths scatter all column sizes together. This avoids one character-copy launch per result column. Offset scans and allocation preparation remain separate stages.

  15. Reduce row maps for two-partition, small-schema STRING inputs. Below 4M rows, retain one scatter map and emit output lengths during partitioning. At/above 4M rows, a single STRING column recomputes destinations into a 1,024-entry shared tile with no N-entry global row map; two/three columns store the binary bin plus tile-local rank in one uint16_t per row, avoiding full-width maps and repeated partition-map reads during character copy.

  16. Reduce STRING copy register lifetime and dependent loads. The mapless path stores destinations in shared memory and keeps its copy loop rolled so rank/key registers can die before variable-length copying. The packed-rank path loads both bin bases independently of the packed rank to allow prefix/rank load overlap.

  17. Balance per-string copy work inside the selected kernel. Four-lane groups handle short strings; eligible rebalanced kernels use two 16-lane groups for medium strings; full warps handle long strings with vector loads and aligned 16-byte stores. Boundary bytes use scalar copies, with loads bounded to the individual string. This avoids a separate length-sampling or host dispatch pass.

  18. Avoid empty and duplicate preparation work. Zero-character columns use zeroed offsets and skip character copy; fused STRING scans reuse one scratch allocation. Generic gather-map construction uses histogram-only/two-tile kernels, and supported rank/gather-map helpers read host boundaries through pinned staging.

  19. Remove redundant block barriers and shared histogram code. Histogram-only tiles retain the barrier before shared-memory reuse; reordered emission relies on its caller's synchronization. Saved local ranks reuse the existing warp histogram helper. Identity-copy headers declare their dependencies, and trivial partition boundaries use pinned D2H staging.

All heuristic scenarios

The following tables describe the current dispatch order and thresholds. Let N be rows, P partitions, C columns, W total fixed-width bytes per row, F mean all columns are fixed-width, and A mean at least one column has an actual null (null_count() > 0). After removing invariant columns, C/W/A refer to the reduced schema. “Large” means N >= 1,000,000. “Dense-null” means F, A, large, and every column is at least 75% null. direct emits result payloads immediately; otherwise the sink builds a gather map for generic gather. Rows within each table are considered in order unless marked as an override.

1. Empty, identity, and invariant inputs (before algorithm selection)

Scenario Selected behavior
N = 0 or P = 0 Existing empty-output handling.
P = 1 Boundaries {0, N}; copy/materialize identity output without a partition row map.
N = 1, P > 1 Read the single partition-map value only to construct required host boundaries; identity materialization.
Every column is flat all-NULL, or non-null STRING with a null character-buffer pointer (CPU-known empty strings); also zero columns with rows Histogram/scan for boundaries only, then construct uniform columns; no payload permutation.
Some, but not all, columns are invariant Remove them, recursively select a plan for remaining columns, then restore their original positions after reduced-call temporaries are released.
Identity materialization of non-null STRING Batch byte bounds. Copy offsets directly if output/input offset types match and the first byte offset is zero; otherwise rebase/convert offsets. Preserve large-STRING limits.
Identity materialization of nullable STRING or nested values Generic gather with an implicit counting/identity iterator, without an outer N-entry map.

2. Specialized STRING plans (before fixed-width/general selection)

Admission requires N >= 1,024, 2 <= P <= 256, 1 <= C <= 65,535, all columns STRING, zero actual nulls, and INT32 input offsets. P > 2 additionally requires C >= 2. Each output character-buffer size must be below the configured INT64-offset threshold; otherwise this specialization declines and dispatch continues to generic handling.

Scenario Partition/map and materialization plan
P = 2, C >= 4 Binary partition to a gather map; fused multi-column STRING materialization with an inverse/scatter map.
3 <= P <= 256, C >= 2 super2 gather-map construction; fused multi-column STRING materialization with an inverse/scatter map.
P = 2, 1 <= C <= 3, 1,024 <= N < 4,000,000 Binary partition emits output sizes and one scatter map; per-column offset scans; one character-copy launch. C = 1 uses ordinary four-lane copy groups, while C = 2/3 enables medium-string rebalancing.
P = 2, C = 1, N >= 4,000,000 Histogram/prefix counts, recomputed output sizes, and mapless character copy using shared destination tiles; no N-entry global rank/gather/scatter map.
P = 2, C = 2 or 3, N >= 4,000,000 Save packed binary-bin/tile-rank uint16_t values, scatter sizes from them, and copy all character columns in one rebalanced launch; no full-width global gather/scatter map.

3. Fixed-width/general partition-plan selection

Admission requires N >= 1,024 and 2 <= P <= 256. STRING admission above has already been attempted.

Scenario Selected plan
Dense-null, C > 2 (takes priority even when P = 2) recompute, direct, reordered.
P = 2: default binary; direct only if F, C <= 2, and (C = 1 or no A or not large or W >= 16); otherwise build a gather map.
P = 2, F, no A, large, C = 1/2, W <= 4 (override) Keep binary, direct.
P = 2, F, no A, large, C = 1/2, W > 4 (override) recompute, direct; reorder only for C = 1 and W = 16.
P = 2, F, no A, large, C > 2, C <= 4 and W >= 32 (override) recompute, direct, reordered.
P = 2, F, no A, large, C > 2 outside that width/schema case (override) saved, direct, not reordered.
3 <= P <= 256, F, large, C = 2, A, W >= 16, not dense-null saved, direct, not reordered.
3 <= P <= 256, F, large, either C = 1 or (C = 2 and (no A or W >= 16)), excluding preceding saved case reserve for P >= 16; otherwise recompute; direct, not reordered.
3 <= P <= 256, F, large, no A, remaining schemas (C > 2) recompute, direct, reordered.
Any remaining admitted case Use the block-rank/gather-map fallback below.

4. Block-rank and generic fallbacks

Scenario Selected behavior
N >= 1,024, 2 <= P <= 256, one fixed-width column with zero actual nulls, not selected earlier Save 16-bit tile ranks and scatter payload directly; no gather map.
N >= 1,024, 2 <= P <= 256, all other remaining schemas Two-tile histogram/scan, recompute local ranks, emit an output-to-input gather map, then generic gather. Covers remaining nullable, mixed, and nested schemas and STRING cases declined above.
Outside the optimized row/partition range, after trivial handling Existing histogram/exclusive-scan, per-row atomic scatter-map construction, and generic scatter.

5. Device-side STRING copy work sharing (not CPU plan selection)

Copy-kernel specialization and individual string length L Work sharing
Medium rebalancing enabled, L <= 8 bytes Four lanes per string.
Medium rebalancing enabled, 8 < L <= 128 bytes Queue medium strings within a warp; two 16-lane groups handle up to two strings at a time.
Medium rebalancing disabled, L <= 128 bytes Four lanes per string. Used by single-column scatter/mapless copy.
Either specialization, L > 128 bytes A full warp copies one queued string using vectorized interior copies and scalar boundary copies.
Zero-character columns in general fused gather Zero offsets; omit the column from the character-copy launch.

These are measured heuristic tradeoffs for CPU-metadata groups, not a guarantee of the fastest algorithm for every map skew or string-length distribution. The API, sliced offsets, offset-width limits, supplied stream, output resource, and generic fallback semantics remain unchanged.

Validation

Current tested head on NVIDIA L4 (sm89), CUDA 12.9.1 / GCC 14.2.1, Release, per-thread default stream:

  • PARTITIONING_TEST: 2,140 passed, one existing disabled test; STREAM_PARTITIONING_TEST: 7 passed.
  • CTest stream-interposition run passed (including its resource fixture).
  • Compute Sanitizer: 15 memcheck cases and 5 synccheck cases, with zero errors; 5 targeted racecheck cases, with zero errors or warnings. Racecheck covers the modified histogram, rank and reordered-emission kernels.
  • Coverage includes 48 exact host-oracle cases with 1,000,003 rows each (partial final tile, data/map slices, 1/2/4/8 INT64 columns, P2/P100, null density and skew), single/two-column large sparse STRING copies, map types, offset limits, mixed/nested schemas, non-default streams and resource handling.
  • The identity header compiles independently with the production CUDA flags. Formatting, spelling and whitespace checks passed. Redundant block barriers were checked in generated sm89 SASS: one/two-tile histogram 3->2 / 6->4 sites; reordered output 10->9 sites.

Performance and memory

Fresh baseline-versus-PR measurement, 2026-11-06.

  • Baseline: PR base commit 65b9b3c222ee7d6ba8f0f442688bd64268709e93.
  • PR: tested head c285951eeddfae0a34379d82f0c40dbbd264e695.
  • Hardware/build: NVIDIA L4 (sm89, 23,034 MiB reported VRAM), CUDA 12.9.1, GCC 14.2.1, Release (-O3, C++20), per-thread default stream, RMM CUDA async allocator.
  • The same frozen native cudf::partition benchmark and 348 identical fixtures were run freshly against both exact libraries. Each case has a minimum 0.15-second warmup and 15 timed samples. Input construction and independent per-partition row-hash multiset checks are outside timing.
  • CUDA-event elapsed time spans the API call, including stream gaps caused by required host work. Wall time includes allocations/readbacks, output destruction and final event synchronization. These are native cuDF measurements, not Spark/JNI end-to-end timings.
  • Every initial >5% slowdown candidate in either timing metric, plus key cases, was rerun: 15 fixtures, three paired alternating-order rounds, 30 samples per case per library. All accepted runs passed correctness, library/executable/fixture hash and competing-compute-process checks.

Speedups are equal-fixture-weight geometric means of per-case median baseline/PR time. They are descriptive results, not workload-weighted predictions or confidence intervals. GPU peak is tracked output + temporary allocations, excluding input, context/driver/code caches and host pinned staging. Peak reduction is the arithmetic mean of each case's reduction, not the reduction of a pooled memory total.

Metric cuDF baseline This PR
CUDA-event speedup, 348-case geometric mean 1.000x 3.122x
Wall-time speedup, 348-case geometric mean 1.000x 3.053x
Mean per-case output + temporary GPU peak reduction 0.00% 28.06%

Breakdown versus the same baseline. Large means N >= 1M with 2 <= P <= 256; each group includes its selected/fallback behavior. NULL percentages are fixture-generation targets; dispatch uses each column's actual cached null count.

Fixture group Cases CUDA-event speedup Wall speedup Mean GPU peak reduction
Large fixed-width, no NULLs 94 4.369x 4.267x 19.81%
Large fixed-width, NULL target >=75% 37 5.668x 5.477x 32.05%
Large non-null STRING 116 2.424x 2.411x 32.23%
Empty / identity inputs 2 16.971x 13.001x 75.17%
Other boundaries / fallbacks 46 2.313x 2.206x 37.15%
Other large nullable fixed-width 25 3.671x 3.567x 26.45%
Uniform NULL / empty STRING 2 9.460x 8.501x 52.86%
Large nullable STRING 13 1.383x 1.383x 7.57%
Large mixed schemas 13 1.495x 1.493x 19.47%

Selected absolute timings and memory. Times are medians of the three 30-sample round medians; the displayed speedup is the ratio of those displayed timing medians. MiB values are output + temporary GPU allocation peaks.

Scenario Baseline event ms PR event ms Speedup Baseline peak MiB PR peak MiB
10,000,000 rows; 1 x INT64; P=256; NULL target=0%; uniform 3.272192 1.104976 2.961x 114.61 76.47
10,000,000 rows; 2 x INT64; P=256; NULL target=0%; uniform 4.840384 2.124752 2.278x 190.91 152.76
10,000,000 rows; 2 x INT64; P=256; NULL target=90%; uniform 5.961408 1.417424 4.206x 231.44 155.14
10,000,003 rows; 4 x INT64; P=2; NULL target=0%; 90% hot; slice=7 12.269568 3.289120 3.730x 343.32 305.25
10,000,000 rows; 2 x STRING (0-128 B); P=2; NULL target=0%; 90% hot 30.236160 16.141776 1.873x 1717.84 1315.92
4,000,003 rows; 2 x STRING (0-1 B); P=2; NULL target=0%; uniform; slice=7 5.990400 1.103200 5.430x 202.78 42.01
10,000,000 rows; 10 x STRING (0-64 B); P=200; NULL target=0%; uniform 111.340031 69.078514 1.612x 3855.85 3510.94
10,000,000 rows; INT64 + STRING (0-64 B) + INT64; P=256; NULL target=10%; uniform 16.502304 16.513553 0.999x 811.71 656.56
1,000,000 rows; 10 x STRING (0-64 B); P=257; NULL target=0%; all hot 8.047104 8.006656 1.005x 385.83 385.83
1,024 rows; 2 x INT8; P=257; NULL target=0%; uniform 0.053024 0.054272 0.977x 0.01 0.01

Regression check versus baseline. A confirmed regression requires >5% slowdown in every paired repeat round for the same metric. This does not claim every individual case is faster.

None of the initial >5% slowdown candidates remained >5% slower in all three rounds (15 fixtures repeated). Smaller or inconsistent slowdowns may still exist; see the representative fallback case above.

CPU-metadata heuristics make speed/memory tradeoffs across inputs sharing the same schema metadata; they do not use observed string lengths or map skew to dispatch. No physical DRAM transaction measurement is claimed.

Checklist

  • I am familiar with the Contributing Guidelines.
  • New or existing tests cover these changes.
  • The documentation is up to date with these changes.

Integrate the partition rank, memory, and STRING work from PR1 and PR2.
Select plans from cached host metadata, write fixed-width outputs directly,
fold uniform columns, and reduce STRING row maps.

Signed-off-by: Chong Gao <chongg@nvidia.com>
Signed-off-by: Chong Gao <chongg@nvidia.com>
Recompute and reorder two-partition output for a few wide fixed-width
columns, removing saved per-row ranks. For large two/three-column STRING
tables, pack the binary bin and tile-local rank into uint16_t and decode
sizes/copy destinations without rereading the partition map. Rebalance
medium-string copies while retaining the original single-column paths.

Plan selection continues to use cached CPU metadata only. Large STRING
copies trade 2*N temporary bytes for speed; extremely short strings can
remain slower and use more memory than the mapless PR5 path.

Validation on L4: full partition and stream tests, stream interposition,
and targeted memcheck. The same frozen benchmark covered 348 fixtures;
selected cases had three rotating-order rounds of 30 samples per library.
Overall event-time geometric speedup over original PR5 was about 1.012x;
this is a measured speed/memory tradeoff, not a per-case speed guarantee.
Remove redundant histogram-only and reordered-output block barriers while
retaining synchronization before shared storage is reused. Reuse the warp
histogram helper for local ranks, declare identity-copy dependencies, and
stage trivial partition boundaries through pinned host memory.

Dense-null and wide-column exact-result coverage was added in eac9bee.
Validation: 2140 partition tests, 7 stream tests, stream interposition,
15 memcheck cases, and 5 synccheck/racecheck cases passed. Fresh 348-case
before/after matrices and three paired repeat rounds found no consistent
slowdown above 5%; tracked GPU allocation peaks were unchanged.
@copy-pr-bot

copy-pr-bot Bot commented Nov 9, 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.

@github-actions github-actions Bot added the libcudf Affects libcudf (C++/CUDA) code. label Nov 9, 2026
@res-life

res-life commented Nov 9, 2026

Copy link
Copy Markdown
Contributor Author

I discovered #23507 when I was nearly done with the main implementation of this PR. I used AI to analyze the similarities and differences between the two PRs.
From AI:

The main distinction is that the two PRs optimize different APIs. They share several optimization ideas, but #25154 is not a superset of #23507.

This comparison is based on #23507 at 7253ecfa and #25154 at c285951e.

Scope

The optimizations apply to separate API paths, so neither PR automatically provides its improvements to the other API.

Shared optimization ideas

  • Local counts followed by a global prefix scan. Both count rows per block and partition, then scan the counts to obtain output positions.
  • Direct fixed-width materialization. Both avoid gathering the entire fixed-width result through an intermediate row map.
  • Fused column copies. Both process multiple fixed-width columns while reusing routing information.
  • Compact per-row metadata. Replace per-column hash partition copies with one shared memory kernel #23507 packs the partition ID and local offset into 32 bits when possible. Optimize partition #25154 uses 16-bit local ranks and packs binary partition IDs and ranks into 16 bits for selected STRING paths.
  • Improved output locality. Both use shared memory to organize work by partition before writing output.
  • Copies specialized by storage width. Both support 1/2/4/8/16-byte values.

Two implementation differences are worth highlighting:

Additional optimizations in #25154

Multiple partition-map algorithms

#25154 provides binary, saved, recompute, super2, and reserve paths, selected using CPU metadata such as row count, partition count, column count, storage width, and cached null counts.

These paths include:

  • Recomputing ranks to eliminate the N-entry rank buffer.
  • Processing two tiles per block to reduce block-count and prefix storage.
  • Reserving output space with one global atomic operation per nonempty block/partition pair.
  • Using CUB binary partitioning with an output proxy that invokes the sink directly.

Specialized STRING materialization

#23507 continues to use generic gather for STRING and nested columns. #25154 adds:

  • Batched character-size readback for output allocation.
  • Fused destination calculation and output-length generation.
  • Reusable scan scratch storage across columns.
  • One character-copy kernel launch covering multiple columns.
  • Four-, sixteen-, or thirty-two-lane copying based on individual string length.
  • Vectorized 16-byte copying for long strings.
  • A mapless recompute path for large single-column inputs.
  • Packed 16-bit binary partition/rank metadata for large inputs with two or three STRING columns.

Fused fixed-width validity handling

#23507 copies fixed-width payloads directly but still builds a gather map and calls gather_bitmask() when actual nulls are present.

The direct-output paths in #25154 check input validity, skip invalid payload copies, and update output validity during emission. They also preserve the cached null count because a complete permutation does not change it.

Identity and uniform-column optimizations

#25154 adds special handling for:

  • A single partition or a single input row.
  • All-null fixed-width and STRING columns.
  • All-empty STRING columns identifiable from CPU metadata.
  • Removing uniform columns before partitioning the remaining columns, then reconstructing them.
  • Computing only partition boundaries when every column is uniform.

Optimizations in #23507 that #25154 does not add

  • A specialized fixed-width row hasher.
  • Hybrid materialization for mixed schemas: direct copying of fixed-width payloads while sharing one gather map for STRING, nested columns, and validity handling.
  • CTA size and rows-per-thread selection based on available shared memory.
  • Column-copy batching based on L2 capacity.
  • Streaming input loads using cub::LOAD_CS.

Performance comparison

The reported performance results use different baselines and do not establish which PR is faster. Comparing their overlapping fixed-width materialization strategies would require equivalent workloads on the same hardware.

@res-life

res-life commented Nov 9, 2026

Copy link
Copy Markdown
Contributor Author

@davidwendt @ttnghia Could you please take a look at this PR. cudf-spark heavily depends on cudf::partition, optimization on partition would help on TPC-DS performance.

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

Labels

libcudf Affects libcudf (C++/CUDA) code.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant