CUDA: radix top-k for large row counts - #28713
Draft
praneshgo wants to merge 1 commit into
Draft
Conversation
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Overview
Replaces CUB's per-row DeviceTopKKernel with a grid-over-rows radix select, gated on GGML_CUDA_TOPK_RADIX_MIN_ROWS.
Current implementation of top_k finding is sequential over rows (internal to a row, it is parallel). There is a for loop that goes over all the rows. The upstream top-k uses CPU processing over rows to loop over, but GPU processing internal to the row. This PR introduces radix select that is a single call at the op level that processes on the GPU completely and has a positive impact on the prefill perf.
Also, the optimization applies to any model that calls ggml_top_k, with some conditionals on row and column count. Some examples that call ggml_top_k include deepseek32, deepseek4, minimax-m3.
top_k calls are used after MOE router, with backend sampling, DFlash and Sparse-attention indexers.
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)--lazy-mode on-direct(from #28136 )--lazy-mode on(demand-paged mmap)Summary:
--lazy-modeauto(resident)on-directon(demand-paged)Requirements