Skip to content

CUDA: improve top-k algorithm selection - #28713

Merged
ORippler merged 15 commits into
ggml-org:masterfrom
praneshgo:pgonegandla/topk-radix-only
Oct 8, 2026
Merged

ORippler merged 15 commits into
ggml-org:masterfrom
praneshgo:pgonegandla/topk-radix-only

Conversation

@praneshgo

@praneshgo praneshgo commented Sep 10, 2026 •

Copy link
Copy Markdown
Contributor

Overview

This PR supersedes #29278.
It starts from #28547's boundary, as in #29278, with the same three build-time thresholds, and makes a few refinements on top of them.

Refinement 1 (CUDA only): bitonic for a padded width up to 1024 while the rows fit in one wave of SMs, set by a new fourth threshold, GGML_CUDA_TOP_K_NCOLS_THRESHOLD_BITONIC_FEW_ROWS (1024; 0 on HIP/MUSA). This fixes #29278's own CUDA 12.8 losses at 1024 columns × 2–128 rows (55–78%).
Refinement 2: DeviceTopK for up to 2 rows when it's available. GGML_CUDA_TOP_K_NROWS_THRESHOLD now defaults to 2 with DeviceTopK and stays 1 without it. This fixes the 2-row losses in #29278's 13.2 chart (52–80%).
Refinement 3: in CUDA builds with DeviceTopK (CCCL ≥ 3.4.3), a single row always uses DeviceTopK, the same as master. Rows ≥2 and builds without DeviceTopK keep the rules above. This fixes the single-row losses at ≤512 columns (down to 45% on the RTX 5090, 62% on GB10 and 81% on GB300).

Testing

Correctness was checked with test-backend-ops -o TOP_K on the patched build:

GPU Default CCCL CCCL 3.4.3
RTX 5090 (sm_120, Windows, CUDA 12.9) 527/527 527/527
DGX Spark GB10 (sm_121, Linux, CUDA 13.0) 527/527 527/527
DGX Station GB300 (sm_103, Linux, CUDA 13.4, native CCCL is 3.4.3) n/a 527/527

Performance

Method: the same harness as #29278. 1027 shapes, graphs on, runs capped at 1024, master vs patched, two passes each.

DGX Spark

Default CCCL

topk_grid_gb10_defaultcccl

CCCL v3.4.3

topk_grid_gb10_cccl343

RTX 5090

Default CCCL

topk_grid_5090_cuda129_defaultcccl

CCCL v3.4.3

topk_grid_5090_cccl343

GB300

Native CCCL 3.4.3

topk_grid_gb300_bundledcccl343

Patched vs master per cell (master time / patched time); geomean, with the lowest cell in brackets.

Rows RTX 5090, default CCCL RTX 5090, CCCL 3.4.3 GB10, default CCCL GB10, CCCL 3.4.3 GB300, native CCCL 3.4.3
1 192% (min 99%) 100% (min 98%) 148% (min 100%) 100% (min 98%) 100% (min 94%)
2 294% (min 98%) 107% (min 93%) 304% (min 100%) 114% (min 97%) 117% (min 95%)
4-256 286% (min 97%) 2573% (min 120%) 330% (min 99%) 1260% (min 110%) 1641% (min 123%)
512-4096 348% (min 98%) 23744% (min 1090%) 284% (min 99%) 3051% (min 131%) 12574% (min 1188%)
All cells 295% (min 97%) 3109% (min 93%) 294% (min 98.6%) 1131% (min 97%) 2021% (min 94%)

Adding some E2E perf numbers below

E2E perf numbers

GB300

DGX Station GB300 — Qwen3.8-Flash-Next UD-IQ4_XS, CUDA 13.4.2 (bundled CCCL 3.4.3)

Test master t/s PR t/s PR/master
pp2048 @ d0 1755.8 2852.8 1.625
pp2048 @ d8192 1650.9 2719.9 1.648
pp2048 @ d32768 1605.4 2626.9 1.636
tg128 @ d0 100.08 99.89 0.998
tg128 @ d4096 98.27 98.16 0.999
tg128 @ d32768 93.87 93.92 1.001
batched decode, 3 seq 215.99 219.73 1.017
batched decode, 4 seq 254.38 261.25 1.027
batched prefill, 3 seq 1500.0 2016.1 1.344
batched prefill, 4 seq 1518.7 2081.2 1.370

llama-bench -fa 1 -ngl 99 -r 3 (pp: -b 2048 -ub 2048); batched: llama-batched-bench -c 65792 -b 2048 -ub 512 -kvu -npp 1024 -ntg 64 -npl 3,4

GB10

DGX Spark GB10 — Qwen3.8-Flash-Next UD-IQ4_XS, CUDA 13.0, CCCL 3.4.3

Test master t/s PR t/s PR/master
pp2048 @ d0 852.66 1062.37 1.246
pp2048 @ d8192 779.18 1006.55 1.292
pp2048 @ d32768 739.09 936.35 1.267
tg128 @ d0 27.77 27.77 1.000
tg128 @ d4096 27.30 27.28 0.999
tg128 @ d32768 26.17 26.18 1.000
batched decode, 3 seq 58.93 59.11 1.003
batched decode, 4 seq 70.23 70.57 1.005
batched prefill, 3 seq 724.00 882.95 1.220
batched prefill, 4 seq 752.43 924.53 1.229

llama-bench -fa 1 -ngl 99 -b 2048 -ub 2048 -r 3; batched: llama-batched-bench -c 65792 -b 2048 -ub 512 -kvu -npp 1024 -ntg 64 -npl 3,4

Requirements

  • I have read and agree with the contributing guidelines
  • AI usage disclosure: AI was used to partially assist while making the code edits and to understand the contexts of the code base.

Replaces CUB's per-row DeviceTopKKernel with a grid-over-rows radix select,
gated on GGML_CUDA_TOPK_RADIX_MIN_ROWS. On qwen4exp at 34,816 tokens this cuts
top-k from 1,671,253 launches / 5,761.8 ms to 2,329 / 941.8 ms.
@github-actions github-actions Bot added ggml changes relating to the ggml tensor library for machine learning CUDA Related to the CUDA backend labels Sep 10, 2026
@ORippler

Copy link
Copy Markdown
Collaborator

Please see discussion over at #28547 for a principled analysis that can be used to determine a threshold. Might make sense to add a data-point for DGX Spark here

@praneshgo

Copy link
Copy Markdown
Contributor Author

Please see discussion over at #28547 for a principled analysis that can be used to determine a threshold. Might make sense to add a data-point for DGX Spark here

Acknowledged. I will check the PR #28547.
Also, we already have a data point from DGX Spark in the description. Re-adding it here in this comment.

Performance with various -lzm options on Qwen 3.8 Flash Next over DGX Spark

Six runs per arm over two rounds. Cold cache before every arm. Prompts from a 2.09M-word prose corpus so no two requests share n-grams — filler from a small vocabulary saturates the reachable n-gram set after one request and inflates the demand-paged arm. On/off order flipped between rounds.

The on arm is I/O-bound, demand-paging a 26.8 GiB embedding table one fault at a time, so removing launches buys nothing there — hence its runs interleaving. An independent 2×2 at pp8192 agrees: 675.2 → 800.8 resident, 182.4 → 193.6 faulting.

Decode is unchanged in every arm (~17.9–22.3 tok/s): ggml_top_k is not on this model's decode path at batch 1.

--lazy-mode auto (resolves to OFF on this device, so the table is read resident)

run prompt tok radix on radix off
1 34,865 798.60 639.90
2 34,661 804.00 644.60
3 34,798 800.90 643.60
4 34,812 805.40 658.80
5 34,742 803.40 662.60
6 34,733 804.00 662.40
mean 802.72 ± 2.50 651.98 ± 10.38
median 803.70 651.70

--lazy-mode on-direct (from #28136 )

run prompt tok radix on radix off
1 34,865 728.90 619.30
2 34,661 737.00 622.10
3 34,798 741.80 624.10
4 34,812 733.90 616.40
5 34,742 742.70 621.60
6 34,733 748.10 625.70
mean 738.73 ± 6.87 621.53 ± 3.34
median 739.40 621.85

--lazy-mode on (demand-paged mmap)

run prompt tok radix on radix off
1 34,865 214.40 196.80
2 34,661 216.30 216.70
3 34,798 247.40 233.50
4 34,812 199.60 196.30
5 34,742 227.60 218.00
6 34,733 225.60 217.90
mean 221.82 ± 16.01 213.20 ± 14.31
median 220.95 217.30

Summary:

--lazy-mode radix on radix off effect of radix
auto (resident) 802.72 ± 2.50 651.98 ± 10.38 +23.1%
on-direct 738.73 ± 6.87 621.53 ± 3.34 +18.9%
on (demand-paged) 221.82 ± 16.01 213.20 ± 14.31 +4.0%, inside noise

@praneshgo
praneshgo marked this pull request as ready for review September 22, 2026 08:58
@praneshgo
praneshgo requested a review from a team as a code owner September 22, 2026 08:58
Comment thread ggml/src/ggml-cuda/top-k.cu Outdated
for (int i = 0; i < nrows; i++) {
top_k_cub(pool, src0_d + i * ncols, dst_d + i * k, ncols, k, stream);
if (nrows >= top_k_radix_min_rows() && ncols > 1024) {
top_k_radix_cuda(pool, src0_d, dst_d, ncols, nrows, k, stream);

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.

Looks like this allocates the memory for all rows at once.

ggml_cuda_pool_alloc<int> histograms_alloc(pool, (size_t) nrows * blocks_per_row * NBINS);

@albertnsoliz

Copy link
Copy Markdown

Seems like these are targeting the same file, can you verify:
#29326

Does the compile flags resolve it or is there a need for the other additions here?

@ORippler

Copy link
Copy Markdown
Collaborator

Imo, this should be superseeded by #29278, which implements the heuristic determined in #28547

@ORippler

Copy link
Copy Markdown
Collaborator

Feel free to reopen if the PR would change the heuristic of #28547

@anskumar01

Copy link
Copy Markdown

#29278 is closed as [fairydreaming] will not continue working on that. @praneshgo , can you reopen this PR and take over.

@praneshgo

praneshgo commented Oct 5, 2026 •

Copy link
Copy Markdown
Contributor Author

#29278 is closed as [fairydreaming] will not continue working on that. @praneshgo , can you reopen this PR and take over.

Yes, I plan to work on it this week.

@praneshgo

Copy link
Copy Markdown
Contributor Author

Reopening this PR and marking it as draft as #29278 is closed.

@ynankani ynankani reopened this Oct 5, 2026
@praneshgo
praneshgo marked this pull request as draft October 5, 2026 04:36
Pranesh Gonegandla added 3 commits October 6, 2026 08:18
Replace the nrows/ncols special case with the decision boundary from ggml-org#28547
(as implemented in ggml-org#29278): bitonic for short rows, radix select for several
long rows, and DeviceTopK or CUB argsort for a single long row. The
thresholds stay overridable at build time.

Two refinements on top of that boundary:
- bitonic stays in use for rows up to a padded 1024 while the rows fit in one
  wave of blocks (nrows <= number of SMs); radix select pays a fixed cost of
  about a dozen launches that only amortizes over more rows
- with DeviceTopK available, it handles up to two rows

Radix select now processes rows in chunks so its scratch memory stays bounded,
and the bitonic path keeps its chunking. HIP and MUSA keep their previous
thresholds.

Add perf cases around the bitonic/radix crossover to test-backend-ops.
@praneshgo praneshgo changed the title CUDA: radix top-k for large row counts CUDA: improve top-k algorithm selection Oct 6, 2026
@github-actions github-actions Bot added the testing Everything test related label Oct 6, 2026
@praneshgo
praneshgo marked this pull request as ready for review October 7, 2026 07:40
@praneshgo
praneshgo requested a review from ggerganov as a code owner October 7, 2026 07:40
@praneshgo
praneshgo requested a review from ynankani October 7, 2026 07:40
@praneshgo

Copy link
Copy Markdown
Contributor Author

@gaugarg-nv @ORippler can you please review this PR?

@gaugarg-nv

Copy link
Copy Markdown
Contributor
image

What causes perf regression for row=1, CCCL 3.4.3?

@gaugarg-nv gaugarg-nv 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.

Looks like this check is no longer needed-

#else
return op->src[0]->ne[0] <= 1024;
#endif // defined(GGML_USE_HIP) || defined(GGML_CUDA_USE_CUB)

@praneshgo

Copy link
Copy Markdown
Contributor Author

I'd also recommend to share the chunking helpers across the paths that do chunking (argsort, radix, bitonic)

The chunking helpers are now shared across paths in the latest commit.

@praneshgo
praneshgo requested a review from ORippler October 8, 2026 06:21

@ORippler ORippler left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Thanks for taking this up! Please add a test to test-backend-ops to exercise the chunked paths for both radix-sort & bitonic sort in addition to the comments made below.

Comment thread ggml/src/ggml-cuda/top-k.cu Outdated
Comment thread ggml/src/ggml-cuda/top-k.cu Outdated
Comment thread ggml/src/ggml-cuda/top-k.cu Outdated
Comment thread ggml/src/ggml-cuda/top-k.cu Outdated
Comment thread ggml/src/ggml-cuda/top-k.cu Outdated
Comment thread ggml/src/ggml-cuda/top-k.cu Outdated
Comment thread ggml/src/ggml-cuda/top-k.cu Outdated
Comment thread ggml/src/ggml-cuda/top-k.cu Outdated
@praneshgo

Copy link
Copy Markdown
Contributor Author

Thanks for taking this up! Please add a test to test-backend-ops to exercise the chunked paths for both radix-sort & bitonic sort in addition to the comments made below.

The tests are added now in the latest code.

Comment thread ggml/src/ggml-cuda/top-k.cu Outdated
Comment thread ggml/src/ggml-cuda/top-k.cu Outdated
Comment thread ggml/src/ggml-cuda/top-k.cu Outdated
Comment thread ggml/src/ggml-cuda/ggml-cuda.cu Outdated
return ggml_is_contiguous_rows(op->src[0]);
case GGML_OP_TOP_K:
#if defined(GGML_USE_HIP) || defined(GGML_CUDA_USE_CUB)
return true;

@ORippler ORippler Oct 8, 2026 •

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I think we should double check the limits here in a follow-up PR (for CCCL 2.8, sorting based algos tend to limit to n_cols <= INT_MAX, radix-sort implemented in the CUDA backend also supports only n_cols <= INT_MAX for now)

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.

The latest code now limits supports_op to ne[0] <= INT_MAX for TOP_K in all builds, and for argsort in CUB builds. argsort in builds without CUB was already capped by its shared-memory check.

@praneshgo
praneshgo requested a review from ORippler October 8, 2026 14:48
@ORippler
ORippler merged commit 71ad059 into ggml-org:master Oct 8, 2026
23 of 26 checks passed
feal87 added a commit to feal87/myllama.cpp that referenced this pull request Oct 8, 2026
Upstream brings CUDA top-k/argsort/mmq fixes (PR ggml-org#28713, ggml-org#29953, ggml-org#30147,
ggml-org#29453), a DFlash output-head fix (ggml-org#30111) and a UI fix (ggml-org#29668).

Conflicts and resolution:
- top-k.cu / argsort.cu: upstream reworked the top-k selection and the same
  radix path the fork added. Take upstream's shape-based dispatch and int64_t
  fixes, keep the fork workspace cap in the shared ggml_cuda_chunk_nrows
  (GGML_CUDA_ARGSORT_CHUNK_MB) and the fork benchmark-only GGML_CUDA_TOPK_IMPL
  selector used by bench-topk.py.
- mmq.cu: upstream PR ggml-org#29953 fixes the same mul_mat_id padding over-read the
  fork patched in 1386ec2, but sizes the padding from the chosen J tile in
  both branches. Take upstream's version.
- test-backend-ops.cpp: keep the fork GGML_TOPK_BENCH shapes and add upstream's
  chunk-spanning cases.

Assisted-by: pi (deepseek-flash)
mclrcha added a commit to mclrcha/llama.cpp that referenced this pull request Oct 10, 2026
…-10-10)

Conflicts:
- ggml-backend.cpp: upstream moved input copies to ggml_backend_sched_copy_input with host weights last (ggml-org#29943);
  the GGML_SCHED_BATCH_INPUTS fast path now runs in the first loop before falling back to it.
- ggml-cuda/fattn-common.cuh: launch_fattn takes upstream async_kv_preload, then our parallel_blocks_fixed
  (GQA-6 tile launcher updated).
- ggml-cuda/fattn-mma-f16.cuh: upstream swizzle refactor (ggml-org#29612); its helpers take ncols2 like the rest of
  the fork's config getters.
- ggml-cuda/gated_delta_net.cu: keep the fork's kernel and launch geometry (its columns_per_block template
  argument is not upstream's cols_per_warp, ggml-org#30087).
- ggml-cuda/mmq.cu: upstream per-tile src1 padding (ggml-org#29953); ggml_cuda_mul_mat_q_src1_nbytes and the MMQ input
  cache use the worst-case padding so producers and projections with other tiles can share the buffer.
- ggml-cuda/mmq-vec-dot.cuh: fork vec_dot kernels pass GGML_PREC_Q8 to the config helpers (ggml-org#30168).
- ggml-cuda/mmvf.cu/.cuh: keep both includes and the HIP declarations, upstream warp_size argument.
- ggml-cuda/rope.cu: upstream grid-stride rms_norm+mul+rope (ggml-org#28175) with the fork's M-RoPE theta (nchannels
  instead of gridDim.y) and rope_store_cast.
- ggml-cuda/top-k.cu: upstream shape-based selection (ggml-org#28713), RDNA4 tournament kept for few long rows.
- src/llama-batch.*: mixed token/embd batches (ggml-org#29622) copy embd rows per entry; other batches keep pointing at
  the ext rows.
- src/llama-context.cpp: expert copy callback also on the second MTP scheduler.
- src/llama-graph.cpp: build_rs keeps the GET_ROWS of the ubatch states (GDN fusions, deferred state) and gathers
  the extra states from the tail like upstream's custom getter path (ggml-org#29856).
@BrewTestBot BrewTestBot mentioned this pull request Oct 15, 2026
1 task done
gaetan-puleo pushed a commit to rcfa/strix-llama.cpp that referenced this pull request Oct 16, 2026
Make the existing radix selector available in hipCUB builds and dispatch it
only above measured width/batch cutoffs. Gather boundary ties in input order
so the selected set matches stable hipCUB sorting. Preserve the existing
single-row and other-device paths and the original non-CUB radix behavior.

All 569 operator cases pass, including extreme scores and cutoff boundaries.
The five target-model workloads retain all 40972800 logits and token IDs;
WikiText2 perplexity remains 2.0259. Native PP2048 at depth40000 measures
358.986 +/-17.688 ->392.294 +/-0.692 tok/s in one five-sample pair; zero-depth
prefill is unchanged within variation. The deep control is noisy, although
every candidate sample exceeds every control sample.

Assisted-by: Codex
(cherry picked from commit ec05a66)

[rebase: top-k.cu change dropped - upstream ggml-org#28713 already dispatches radix top-k on HIP for nrows > 1, ncols > 1024; only the tests are kept]
edwardyoon added a commit to edwardyoon/focus-llama that referenced this pull request Oct 24, 2026
amangupta-tether added a commit to tetherto/qvac-fabric-llm.cpp that referenced this pull request Oct 27, 2026
cuda : top-k selection by shape for temp-11259 (port of upstream ggml-org#28713)
auRose94 pushed a commit to auRose94/llama.cpp that referenced this pull request Oct 28, 2026
* CUDA: radix top-k for large row counts

Replaces CUB's per-row DeviceTopKKernel with a grid-over-rows radix select,
gated on GGML_CUDA_TOPK_RADIX_MIN_ROWS. On qwen4exp at 34,816 tokens this cuts
top-k from 1,671,253 launches / 5,761.8 ms to 2,329 / 941.8 ms.

* CUDA: select the TOP_K implementation by shape

Replace the nrows/ncols special case with the decision boundary from ggml-org#28547
(as implemented in ggml-org#29278): bitonic for short rows, radix select for several
long rows, and DeviceTopK or CUB argsort for a single long row. The
thresholds stay overridable at build time.

Two refinements on top of that boundary:
- bitonic stays in use for rows up to a padded 1024 while the rows fit in one
  wave of blocks (nrows <= number of SMs); radix select pays a fixed cost of
  about a dozen launches that only amortizes over more rows
- with DeviceTopK available, it handles up to two rows

Radix select now processes rows in chunks so its scratch memory stays bounded,
and the bitonic path keeps its chunking. HIP and MUSA keep their previous
thresholds.

Add perf cases around the bitonic/radix crossover to test-backend-ops.

* CUDA: make top-k comments less verbose

* CUDA: remove the TOP_K width limit from supports_op

* CUDA: use DeviceTopK for single-row TOP_K if available

* CUDA: avoid ncols overflow in the TOP_K bitonic check

* CUDA: share the row chunking helper between argsort and top-k

* CUDA: do the TOP_K radix blocks_per_row math in int64_t

* CUDA: rename GGML_CUDA_TOP_K_NROWS_THRESHOLD_DEVICETOPK to GGML_CUDA_TOP_K_NROWS_THRESHOLD

* CUDA: share one sort helper between the bitonic and CUB TOP_K paths

* CUDA: update the TOP_K TODO, threshold and chunking comments

* tests: add TOP_K cases that span several row chunks

* CUDA: use int64_t col in the TOP_K radix loops, fix threshold comment

* CUDA: limit TOP_K and ARGSORT support to ne[0] <= INT_MAX

---------

Co-authored-by: praneshgo <227579474+praneshgo@users.noreply.github.com>
Co-authored-by: Pranesh Gonegandla <pgonegandla@nvidia.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CUDA Related to the CUDA backend ggml changes relating to the ggml tensor library for machine learning testing Everything test related

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants