In DeepSeek Sparse Attention (DSA), TopK is used to select the most relevant positions from a large number of context tokens. During sampling, TopK is also used to select candidate tokens from a large vocabulary. To improve end-to-end inference performance, we designed a new TopK algorithm.
The pseudocode below captures the core flow of the algorithm. The actual implementation requires hardware-specific optimization.
The algorithm keeps a top-
-
Scan: Process the input one block of size
$B$ at a time, following a random block order. -
Filter: Filter elements using the current top-
$k$ threshold$T$ , and append only those that may still enter the top-$k$ totopk_candidate(i.e.$> T$ ). -
Compact: When the candidate buffer becomes large or at the end of the algorithm, run radix-select-based TopK in shared memory (or another TopK algorithm) to reduce it back to
$k$ elements, and set$T$ to the smallest element among them.
In the pseudocode below, topk_candidate is compacted. A reasonable configuration for
DeepSelectTopk(x[0:N), k, B, B2):
require 1 <= k <= N and B >= 1 and B2 >= 1
topk_candidate = [] # shared memory
topk_threshold = -inf # current top-k threshold
p = random_permutation(ceil_div(N, B)) # independent of x
for block in p:
lo, hi = block * B, min((block + 1) * B, N)
# Keep only elements that may still enter the top-k.
for j in range(lo, hi):
if x[j] > topk_threshold:
topk_candidate.append((x[j], j))
# Periodically compact the candidate buffer.
if len(topk_candidate) >= k + B2:
topk_candidate = RadixSelectTopK(topk_candidate, k)
topk_threshold = min(v for v, _ in topk_candidate)
return RadixSelectTopK(topk_candidate, k)
Initially, the threshold is -inf, so all elements are accepted before the first compaction. Afterwards, topk_threshold is the current
To avoid performance degradation on unfavorable inputs, blocks are processed in a random order. Below, we establish an expected performance bound under a uniform random block permutation.
Notes:
- At any time,
len(topk_candidate) <= k + B2 + Bholds - Every element of
$x$ is read exactly once, with accesses performed in contiguous blocks of size$B$ . - Excluding the random block permutation, the algorithm requires only
$O(k+B+B_2)$ additional space, which can reside in a smaller and faster memory space such as shared memory.
With a fixed input, the only randomness in the following analysis comes from a uniform random block permutation independent of the input.
For simplicity, assume that all elements are distinct. Equal-valued elements do not worsen the performance bound and are omitted from the discussion.
Let topk_candidate at the start of processing TopK calls, including the final one. Define
Since
Now consider the set of the first
Conditioned on
Let
Let TopK calls. Each such call removes at least
Across all TopK calls, each appended element contributes once to an input, while each in-loop call retains
Therefore,
Each input element is read from global memory exactly once, with accesses performed in contiguous blocks. The remaining computation is performed mainly on a candidate set of size
When torch.topk in DSA workloads.
We implement this DeepSelect kernel in CUDA C++ and make extensive use of low-level PTX optimization techniques and bit manipulation techniques to reduce constant factors, lower runtime, and improve GPU bandwidth utilization.
Normally, in the Filter step, each CUDA thread needs to scan the input twice: the first scan counts how many of the numbers read by each thread are greater than the threshold, so that each thread knows where to write its results in the topk_candidate array; the second scan goes through each element again and places elements greater than the threshold in the correct positions in topk_candidate. Even if shared memory is used here to avoid repeatedly reading global memory, the overhead of CUDA Core operations still limits the performance of the entire kernel. To address this, while counting the number of elements greater than the threshold in the first scan, we generate a uint32_t mask whose i-th bit records whether the i-th element is greater than the threshold. When placing elements later, we can use __ffs(mask) to get the index of the lowest set bit in mask, and then clear the lowest set bit with mask &= mask - 1u, thereby quickly finding all elements greater than the threshold. In addition, we use low-level PTX instructions such as set.s32.bf16x2, prmt.b32, and dp4a.s32.s32 to accelerate mask generation.
In addition, there are a very large number of comparison and bitwise instructions in this kernel. Based on our observations, such instructions contend with integer addition instructions for some hardware resources, reducing kernel performance. Even if integer additions are replaced with integer multiply-add (mad) instructions that do not conflict with comparison and bitwise instructions, their throughput is only half that of floating-point multiply-add, making it a performance bottleneck. We observed that when x and y satisfy 0 <= x, y <= 2^22, x+y = __float_as_uint(__uint_as_float(x) + __uint_as_float(y)) holds; that is, the result of adding two non-negative integers no greater than float32 (in which case both should be denormal numbers) and then performing floating-point addition. Therefore, we can use floating-point addition to replace some integer additions to improve throughput.
In addition to the above two points, we also use low-level PTX instructions and bitwise operations to accelerate other operations, including warp-level prefix sum, byte extraction, histogram construction, etc. See the code for details.
For scenarios with a small batch size (or scenarios where the number of active SMs in the last wave is small due to wave quantization), we use Threadblock Cluster to split the computation of a sequence across multiple Streaming Multiprocessors (SMs). Specifically, each sequence is divided into several parts. For each part, a CTA (CUDA Threadblock) first computes the top-k largest elements in that part. Then, each CTA sends these top-k elements to CTA0 in its Threadblock Cluster, and CTA0 selects the top k largest elements among them.