[Perf][DSv4/DSv3.2] Add cluster-cooperative topK kernel for low-latency scenarios (#43008)
Signed-off-by: LopezCastroRoberto <rocastro@redhat.com>
Roberto L. Castro committed
Jun 24, 2026 at 01:11 UTC
855cd4d787608b9bdecdc491d51fefadd3fc67dd
10 files changed
+1568
-45
.buildkite/test_areas/kernels.yaml
+2
@@ -47,8 +47,10 @@ steps:
47
- csrc/fused_deepseek_v4_qnorm_rope_kv_insert_kernel.cu
48
- vllm/models/deepseek_v4/common/ops/
49
- tests/kernels/test_fused_deepseek_v4_qnorm_rope_kv_insert.py
50
+ - tests/kernels/test_top_k_per_row.py # it runs on Blackwell too - some kernels have arch-specific optimizations
51
commands:
52
- pytest -v -s kernels/test_fused_deepseek_v4_*.py
53
+ - pytest -v -s kernels/test_top_k_per_row.py
54
55
- label: Deepseek V4 Kernel Test (B200)
56
key: deepseek-v4-kernel-test-b200
CMakeLists.txt
+30
@@ -382,6 +382,24 @@ if(VLLM_GPU_LANG STREQUAL "CUDA" OR VLLM_GPU_LANG STREQUAL "HIP")
382
"csrc/libtorch_stable/custom_all_reduce.cu"
383
"csrc/libtorch_stable/fused_deepseek_v4_qnorm_rope_kv_insert_kernel.cu")
384
385
+ if(VLLM_GPU_LANG STREQUAL "CUDA" AND
386
+ DEFINED CMAKE_CUDA_COMPILER_VERSION AND
387
+ CMAKE_CUDA_COMPILER_VERSION VERSION_GREATER_EQUAL 12.0)
388
+
389
+ if(${CMAKE_CUDA_COMPILER_VERSION} VERSION_GREATER_EQUAL 13.0)
390
+ cuda_archs_loose_intersection(COOPERATIVE_TOPK_ARCHS
391
+ "9.0a;10.0f;10.1f;10.3f;11.0f;12.0f;12.1f" "${CUDA_ARCHS}")
392
+ else()
393
+ cuda_archs_loose_intersection(COOPERATIVE_TOPK_ARCHS
394
+ "9.0a;10.0a;10.1a;10.3a;12.0a;12.1a" "${CUDA_ARCHS}")
395
+ endif()
396
+
397
+ if(COOPERATIVE_TOPK_ARCHS)
398
+ list(APPEND VLLM_GPU_FLAGS "-DVLLM_ENABLE_COOPERATIVE_TOPK=1")
399
+
400
+ endif()
401
+ endif()
402
+
403
if(VLLM_GPU_LANG STREQUAL "CUDA")
404
SET(CUTLASS_ENABLE_HEADERS_ONLY ON CACHE BOOL "Enable only the header library")
405
@@ -498,6 +516,14 @@ if(VLLM_GPU_LANG STREQUAL "CUDA" OR VLLM_GPU_LANG STREQUAL "HIP")
516
SRCS "${VLLM_STABLE_EXT_SRC}"
517
CUDA_ARCHS "${CUDA_ARCHS}")
518
519
+ if(COOPERATIVE_TOPK_ARCHS)
520
+ list(APPEND VLLM_STABLE_EXT_SRC
521
+ "csrc/libtorch_stable/cooperative_topk.cu")
522
+ set_gencode_flags_for_srcs(
523
+ SRCS "csrc/libtorch_stable/cooperative_topk.cu"
524
+ CUDA_ARCHS "${COOPERATIVE_TOPK_ARCHS}")
525
+ endif()
526
+
527
# Only build Marlin kernels if we are building for at least some compatible archs.
528
# Keep building Marlin for 9.0 as there are some group sizes and shapes that
529
# are not supported by Machete yet.
@@ -1049,6 +1075,10 @@ if(VLLM_GPU_LANG STREQUAL "CUDA" OR VLLM_GPU_LANG STREQUAL "HIP")
1075
target_compile_definitions(_C_stable_libtorch PRIVATE
1076
TORCH_TARGET_VERSION=0x020B000000000000ULL)
1077
target_compile_definitions(_C_stable_libtorch PRIVATE USE_CUDA)
1078
+ if(COOPERATIVE_TOPK_ARCHS)
1079
+ target_compile_definitions(_C_stable_libtorch PRIVATE
1080
+ VLLM_ENABLE_COOPERATIVE_TOPK=1)
1081
+ endif()
1082
# Needed by CUTLASS kernels
1083
target_compile_definitions(_C_stable_libtorch PRIVATE
1084
CUTLASS_ENABLE_DIRECT_CUDA_DRIVER_CALL=1)
csrc/libtorch_stable/cooperative_topk.cu
new
+146
@@ -0,0 +1,146 @@
1
+// Cooperative cluster TopK for DeepSeek V3 sparse attention indexer.
2
+// See cooperative_topk.cuh for kernel implementation.
3
+
4
+#include <cuda_runtime.h>
5
+
6
+#include "torch_utils.h"
7
+
8
+#ifndef USE_ROCM
9
+ #include "cooperative_topk.cuh"
10
+namespace ct = vllm::cooperative;
11
+namespace hist4096 = vllm::topk_histogram_4096;
12
+#endif
13
+
14
+#ifndef USE_ROCM
15
+template <uint32_t TopK, uint32_t CS>
16
+void launch_cooperative_cluster(ct::CooperativeTopKParams<TopK>& params,
17
+ size_t smem, cudaStream_t stream) {
18
+ auto kernel = []() {
19
+ if constexpr (CS == 16) {
20
+ return &ct::cooperative_topk_cs16<TopK>;
21
+ } else if constexpr (CS == 8) {
22
+ return &ct::cooperative_topk_cs8<TopK>;
23
+ } else {
24
+ static_assert(CS == 4, "unsupported cooperative_topk cluster size");
25
+ return &ct::cooperative_topk_cs4<TopK>;
26
+ }
27
+ }();
28
+ if constexpr (CS > 8) {
29
+ cudaFuncSetAttribute(kernel, cudaFuncAttributeNonPortableClusterSizeAllowed,
30
+ 1);
31
+ }
32
+ cudaFuncSetAttribute(kernel, cudaFuncAttributeMaxDynamicSharedMemorySize,
33
+ smem);
34
+
35
+ cudaLaunchConfig_t cfg = {};
36
+ cfg.gridDim = dim3(params.num_rows, CS);
37
+ cfg.blockDim = dim3(hist4096::kBlockSize);
38
+ cfg.dynamicSmemBytes = smem;
39
+ cfg.stream = stream;
40
+ cudaLaunchAttribute attrs[1];
41
+ attrs[0].id = cudaLaunchAttributeClusterDimension;
42
+ attrs[0].val.clusterDim = {1, CS, 1};
43
+ cfg.numAttrs = 1;
44
+ cfg.attrs = attrs;
45
+ cudaError_t err = cudaLaunchKernelEx(&cfg, kernel, params);
46
+ STD_TORCH_CHECK(err == cudaSuccess,
47
+ "cooperative_topk launch failed: ", cudaGetErrorString(err));
48
+}
49
+
50
+template <uint32_t TopK>
51
+void launch_cooperative_topk_impl(const torch::stable::Tensor& logits,
52
+ const torch::stable::Tensor& lengths,
53
+ torch::stable::Tensor& output,
54
+ torch::stable::Tensor& workspace,
55
+ int64_t max_seq_len) {
56
+ (void)max_seq_len; // Kept for signature parity with persistent_topk.
57
+ const int64_t num_rows = logits.size(0);
58
+ const cudaStream_t stream = get_current_cuda_stream();
59
+
60
+ const uint32_t stride = static_cast<uint32_t>(logits.stride(0));
61
+ // 32 = max clusters for CS=4 (32 x 4 = 128 CTAs = 66% of SMs, leaves
62
+ // headroom)
63
+ STD_TORCH_CHECK(
64
+ num_rows <= 32,
65
+ "cooperative_topk supports <=32 rows; use persistent_topk for "
66
+ "larger batches");
67
+
68
+ STD_TORCH_CHECK(stride % 4 == 0,
69
+ "cooperative_topk: stride must be multiple of 4 for TMA "
70
+ "alignment, got stride (max_model_len)=",
71
+ stride);
72
+
73
+ STD_TORCH_CHECK(workspace.is_cuda(), "workspace must be CUDA tensor");
74
+ STD_TORCH_CHECK(
75
+ workspace.scalar_type() == torch::headeronly::ScalarType::Byte,
76
+ "workspace must be uint8");
77
+
78
+ ct::CooperativeTopKParams<TopK> params;
79
+ params.input = logits.const_data_ptr<float>();
80
+ params.output = output.mutable_data_ptr<int32_t>();
81
+ params.lengths = lengths.const_data_ptr<int32_t>();
82
+ params.num_rows = static_cast<uint32_t>(num_rows);
83
+ params.stride = stride;
84
+ params.tie_ws =
85
+ reinterpret_cast<hist4096::Tie*>(workspace.mutable_data_ptr<uint8_t>());
86
+
87
+ constexpr uint32_t kTieWsPerRow =
88
+ TopK <= hist4096::kBlockSize ? hist4096::kMaxTies : TopK;
89
+ STD_TORCH_CHECK(
90
+ workspace.size(0) >=
91
+ static_cast<int64_t>(num_rows * kTieWsPerRow * sizeof(hist4096::Tie)),
92
+ "workspace too small");
93
+
94
+ const bool supports_cluster16 = get_device_prop()->major >= 10;
95
+ if (num_rows <= 4 && supports_cluster16) {
96
+ launch_cooperative_cluster<TopK, 16>(params, ct::kSmemSize8, stream);
97
+ } else if (num_rows <= 8) {
98
+ launch_cooperative_cluster<TopK, 8>(params, ct::kSmemSize8, stream);
99
+ } else {
100
+ launch_cooperative_cluster<TopK, 4>(params, ct::kSmemSize4, stream);
101
+ }
102
+}
103
+#endif // USE_ROCM
104
+
105
+void cooperative_topk(const torch::stable::Tensor& logits,
106
+ const torch::stable::Tensor& lengths,
107
+ torch::stable::Tensor& output,
108
+ torch::stable::Tensor& workspace, int64_t k,
109
+ int64_t max_seq_len) {
110
+#ifndef USE_ROCM
111
+ STD_TORCH_CHECK(logits.is_cuda(), "logits must be CUDA tensor");
112
+ STD_TORCH_CHECK(lengths.is_cuda(), "lengths must be CUDA tensor");
113
+ STD_TORCH_CHECK(output.is_cuda(), "output must be CUDA tensor");
114
+ STD_TORCH_CHECK(logits.scalar_type() == torch::headeronly::ScalarType::Float,
115
+ "Only float32 supported");
116
+ STD_TORCH_CHECK(lengths.scalar_type() == torch::headeronly::ScalarType::Int,
117
+ "lengths must be int32");
118
+ STD_TORCH_CHECK(output.scalar_type() == torch::headeronly::ScalarType::Int,
119
+ "output must be int32");
120
+ STD_TORCH_CHECK(logits.dim() == 2, "logits must be 2D");
121
+ STD_TORCH_CHECK(lengths.dim() == 1 || lengths.dim() == 2,
122
+ "lengths must be 1D or 2D");
123
+ STD_TORCH_CHECK(lengths.is_contiguous(), "lengths must be contiguous");
124
+ STD_TORCH_CHECK(output.dim() == 2, "output must be 2D");
125
+ const int64_t num_rows = logits.size(0);
126
+ STD_TORCH_CHECK(lengths.numel() == num_rows, "lengths size mismatch");
127
+ STD_TORCH_CHECK(output.size(0) == num_rows && output.size(1) == k,
128
+ "output size mismatch");
129
+ STD_TORCH_CHECK(
130
+ k == 512 || k == 1024 || k == 2048,
131
+ "cooperative_topk supports k=512, k=1024, or k=2048, got k=", k);
132
+
133
+ if (k == 512) {
134
+ launch_cooperative_topk_impl<512>(logits, lengths, output, workspace,
135
+ max_seq_len);
136
+ } else if (k == 1024) {
137
+ launch_cooperative_topk_impl<1024>(logits, lengths, output, workspace,
138
+ max_seq_len);
139
+ } else {
140
+ launch_cooperative_topk_impl<2048>(logits, lengths, output, workspace,
141
+ max_seq_len);
142
+ }
143
+#else
144
+ STD_TORCH_CHECK(false, "cooperative_topk is not supported on ROCm");
145
+#endif
146
+}
csrc/libtorch_stable/cooperative_topk.cuh
new
+593
@@ -0,0 +1,593 @@
1
+/*
2
+ * Cooperative TopK kernel for DSA Indexer
3
+ */
4
+
5
+#ifndef COOPERATIVE_TOPK_CUH_
6
+#define COOPERATIVE_TOPK_CUH_
7
+
8
+#include <cooperative_groups.h>
9
+#include <cuda.h>
10
+#include <cuda_fp16.h>
11
+#include <cuda_runtime.h>
12
+#include <cuda/ptx>
13
+#include <algorithm>
14
+#include <cstdint>
15
+
16
+#include "topk_histogram_4096.cuh"
17
+
18
+namespace vllm {
19
+namespace cooperative {
20
+
21
+namespace hist4096 = topk_histogram_4096;
22
+
23
+constexpr uint32_t kHistBits = 10;
24
+constexpr uint32_t kHistBins = 1 << kHistBits;
25
+constexpr uint32_t kMaxTopK = 2048;
26
+
27
+constexpr uint32_t kElemPerStage = 16;
28
+constexpr uint32_t kSizePerStage =
29
+ kElemPerStage * hist4096::kBlockSize; // 16384
30
+
31
+// CS=4 two-pass path uses two TMA stages as a double buffer.
32
+constexpr uint32_t kStreamingStagesCS4 = 2;
33
+// CS=8/16 fused paths keep all loaded TMA stages resident in smem.
34
+constexpr uint32_t kFusedStagesCS8 = 2;
35
+constexpr uint32_t kFusedStagesCS16 = 2;
36
+
37
+// CS=4 single-pass path
38
+constexpr uint32_t kMaxSinglePassStages = 3;
39
+constexpr uint32_t kMaxSinglePassPerBlock =
40
+ kMaxSinglePassStages * kSizePerStage; // 49152
41
+
42
+template <uint32_t TopK = 1024>
43
+struct CooperativeTopKParams {
44
+ const float* __restrict__ input;
45
+ int32_t* __restrict__ output;
46
+ const int32_t* __restrict__ lengths;
47
+ hist4096::Tie* __restrict__ tie_ws; // per-row tie workspace, see
48
+ // kTieWsPerRow
49
+ uint32_t num_rows, stride;
50
+};
51
+
52
+// ============================================================================
53
+// Cooperative helpers
54
+// ============================================================================
55
+
56
+// only CS adjacent lanes participate (sub-warp reduce), in opposite to
57
+// warp_reduce_sum_full
58
+template <uint32_t N>
59
+__device__ __forceinline__ uint32_t warp_reduce_sum_subN(uint32_t v) {
60
+#pragma unroll
61
+ for (uint32_t m = N >> 1; m > 0; m >>= 1)
62
+ v += __shfl_xor_sync(0xFFFFFFFF, v, m, 32);
63
+ return v;
64
+}
65
+
66
+// ============================================================================
67
+// Helpers
68
+// ============================================================================
69
+
70
+__device__ __forceinline__ uint32_t extract_coarse_bin(float x) {
71
+ return hist4096::extract_coarse_bin_N<kHistBits>(x);
72
+}
73
+
74
+__device__ __forceinline__ void mbarrier_init(uint64_t* a, uint32_t n) {
75
+ cuda::ptx::mbarrier_init(a, n);
76
+}
77
+__device__ __forceinline__ void mbarrier_wait(uint64_t* a, uint32_t p) {
78
+ while (!cuda::ptx::mbarrier_try_wait_parity(cuda::ptx::sem_relaxed,
79
+ cuda::ptx::scope_cta, a, p));
80
+}
81
+__device__ __forceinline__ void mbarrier_arrive_expect_tx(uint64_t* a,
82
+ uint32_t t) {
83
+ cuda::ptx::mbarrier_arrive_expect_tx(cuda::ptx::sem_relaxed,
84
+ cuda::ptx::scope_cta,
85
+ cuda::ptx::space_shared, a, t);
86
+}
87
+__device__ __forceinline__ void tma_load(void* d, const void* s, uint32_t n,
88
+ uint64_t* m) {
89
+ cuda::ptx::cp_async_bulk(cuda::ptx::space_shared, cuda::ptx::space_global, d,
90
+ s, n, m);
91
+}
92
+
93
+// ============================================================================
94
+// DSMEM histogram reduce
95
+// ============================================================================
96
+
97
+template <uint32_t CS>
98
+__device__ __forceinline__ void dsmem_hist_reduce(uint32_t* histogram) {
99
+ static_assert(kHistBins <= hist4096::kBlockSize);
100
+ auto cluster = cooperative_groups::this_cluster();
101
+ cluster.sync();
102
+ const auto tx = threadIdx.x;
103
+ const auto rank = blockIdx.y;
104
+ constexpr auto kLocal = kHistBins / CS;
105
+ const auto off = kLocal * rank;
106
+ if (tx < kHistBins) {
107
+ const auto addr = &histogram[off + tx / CS];
108
+ const auto src = cluster.map_shared_rank(addr, tx % CS);
109
+ *src = warp_reduce_sum_subN<CS>(*src);
110
+ }
111
+ cluster.sync();
112
+}
113
+
114
+// ============================================================================
115
+// Find threshold from reduced histogram
116
+// ============================================================================
117
+
118
+// NOTE: caller must ensure a cluster.sync() or __syncthreads() happened
119
+// before calling this, so warp_sum writes are visible across warps.
120
+// The first internal __syncthreads() is still needed for the warp_sum exchange.
121
+template <uint32_t TopK>
122
+__device__ __forceinline__ void find_threshold(uint32_t* histogram,
123
+ uint32_t* warp_sum,
124
+ uint32_t* counter_gt,
125
+ uint32_t* counter_eq,
126
+ hist4096::MatchBin* match) {
127
+ const auto tx = threadIdx.x;
128
+ const auto li = tx % hist4096::kWarpSize, wi = tx / hist4096::kWarpSize;
129
+ const auto value = tx < kHistBins ? histogram[tx] : 0;
130
+ const auto winc = hist4096::warp_inclusive_sum(li, value);
131
+ if (li == hist4096::kWarpSize - 1) warp_sum[wi] = winc;
132
+ __syncthreads();
133
+ const auto tmp = warp_sum[li];
134
+ const auto total = hist4096::warp_reduce_sum_full(tmp);
135
+ auto pfx = hist4096::warp_reduce_sum_full(li < wi ? tmp : 0) + winc;
136
+ const auto above = total - pfx;
137
+ if (tx < kHistBins && above < TopK && above + value >= TopK) {
138
+ *counter_gt = *counter_eq = 0;
139
+ *match = {.bin = tx, .above_count = above, .equal_count = value};
140
+ }
141
+ __syncthreads();
142
+}
143
+
144
+// Streams data through shared memory in chunks, processing each chunk before
145
+// loading the next overwrites each buffer after processing it (the epilogue
146
+// prefetch loads the next chunk into the same slot)
147
+template <typename SmemType, uint32_t kStages, uint32_t kBinBits,
148
+ bool kIsScatter>
149
+__device__ void tma_stream_pass(const float* scores, uint32_t length,
150
+ uint32_t thr_bin, int32_t* indices,
151
+ uint32_t* phases, SmemType* smem) {
152
+ const auto tx = threadIdx.x;
153
+ const auto lane = tx % hist4096::kWarpSize;
154
+ const auto ni =
155
+ (length + kSizePerStage - 1) / kSizePerStage; // total stages needed
156
+ const auto la =
157
+ (length + 3u) & ~3u; // length rounded up to float4 (TMA alignment)
158
+ const auto pass =
159
+ kIsScatter ? 1 : 0; // barrier dim: [0] for histogram, [1] for scatter
160
+
161
+ // Prologue: issue initial TMA loads - prefill the pipeline
162
+ if (tx == 0) {
163
+#pragma unroll
164
+ for (uint32_t i = 0; i < kStages; i++) {
165
+ if (i >= ni) {
166
+ break;
167
+ }
168
+ const auto o = i * kSizePerStage;
169
+ const auto sz = min(kSizePerStage, la - o) * sizeof(float);
170
+ tma_load(smem->score_buffer[i], scores + o, sz,
171
+ &smem->barrier[pass][i]); // cp.async.bulk is non-blocking
172
+ mbarrier_arrive_expect_tx(&smem->barrier[pass][i], sz);
173
+ }
174
+ }
175
+
176
+ // Main loop: process stages
177
+ for (uint32_t it = 0; it < ni; it++) {
178
+ const auto b = it % kStages; // which buffer slot (0 or 1)
179
+ const auto o = it * kSizePerStage;
180
+ const auto sz = min(kSizePerStage, length - o);
181
+
182
+ if (lane == 0) {
183
+ mbarrier_wait(&smem->barrier[pass][b],
184
+ phases[b] & 1); // wait for the data
185
+ }
186
+ phases[b]++; // advances the phase for next time this slot is reused
187
+ __syncwarp();
188
+
189
+#pragma unroll
190
+ for (uint32_t i = 0; i < kElemPerStage; i++) {
191
+ const auto li = tx + i * hist4096::kBlockSize;
192
+ if (li >= sz) {
193
+ break;
194
+ }
195
+ const auto sc = smem->score_buffer[b][li];
196
+ const auto bn = hist4096::extract_coarse_bin_N<kBinBits>(sc);
197
+ if constexpr (kIsScatter) { // compile-time branch
198
+ // Scatter pass: place above-threshold and collect ties
199
+ const auto gi = o + li;
200
+ if (bn > thr_bin) {
201
+ indices[atomicAdd(&smem->counter_gt, 1)] = gi;
202
+ } else if (bn == thr_bin) {
203
+ const auto p = atomicAdd(&smem->counter_eq, 1);
204
+ if (p < hist4096::kMaxTies) {
205
+ smem->tie_buffer[p] = {gi, sc};
206
+ }
207
+ }
208
+ } else {
209
+ // Histogram pass: just count
210
+ atomicAdd(&smem->histogram[bn], 1);
211
+ }
212
+ }
213
+ __syncthreads(); // ensures all threads finished processing their buffer
214
+ // before next TMA load
215
+
216
+ // Epilogue: issue next TMA load
217
+ if (tx == 0 && it + kStages < ni) {
218
+ const auto no = (it + kStages) * kSizePerStage;
219
+ const auto nsz = min(kSizePerStage, la - no) * sizeof(float);
220
+ tma_load(smem->score_buffer[b], scores + no, nsz,
221
+ &smem->barrier[pass][b]);
222
+ mbarrier_arrive_expect_tx(&smem->barrier[pass][b], nsz);
223
+ }
224
+ }
225
+}
226
+
227
+// ============================================================================
228
+// Fused path: single TMA pass, rescan smem for scatter
229
+// ============================================================================
230
+
231
+// Fused shared memory layout for cluster cooperative paths.
232
+// kPasses=1 for single-pass (CS=8, CS=4 singlepass), kPasses=2 for two-pass
233
+// (CS=4).
234
+template <uint32_t kStages, uint32_t kPasses = 1>
235
+struct SmemFused {
236
+ uint64_t barrier[kPasses][kStages];
237
+ alignas(128) uint32_t counter_gt;
238
+ alignas(128) uint32_t counter_eq;
239
+ alignas(128) hist4096::MatchBin match;
240
+ uint32_t warp_sum[hist4096::kNumWarps];
241
+ union {
242
+ uint32_t histogram[kHistBins];
243
+ hist4096::Tie tie_buffer[kMaxTopK];
244
+ };
245
+ alignas(128) float score_buffer[kStages][kSizePerStage];
246
+};
247
+
248
+using Smem8 = SmemFused<kFusedStagesCS8>;
249
+using Smem16 = SmemFused<kFusedStagesCS16>;
250
+using Smem4 = SmemFused<kStreamingStagesCS4, 2>;
251
+using SmemSinglePass = SmemFused<kMaxSinglePassStages>;
252
+
253
+// Cluster-cooperative large path.
254
+// kFused=true: all TMA stages resident, single-pass histogram + scatter (rescan
255
+// from smem). kFused=false: TMA double-buffer streaming, two passes (histogram
256
+// then scatter).
257
+template <uint32_t TopK, uint32_t CS, typename SmemType, bool kFused>
258
+__device__ void large_topk(const float* __restrict__ row_input,
259
+ int32_t* __restrict__ row_output, uint32_t seq_len,
260
+ uint32_t* phases, hist4096::Tie* tie_ws) {
261
+ const auto rank = blockIdx.y; // this block's position in cluster
262
+ const auto tx = threadIdx.x;
263
+ const auto lane = tx % hist4096::kWarpSize;
264
+
265
+ extern __shared__ uint8_t smem_raw[];
266
+ auto* smem = reinterpret_cast<SmemType*>(smem_raw);
267
+ int32_t* s_topk = reinterpret_cast<int32_t*>(smem_raw + sizeof(SmemType));
268
+
269
+ // Partition row across cluster ranks
270
+ constexpr uint32_t kAlign = 4;
271
+ const auto units =
272
+ (seq_len + kAlign - 1) / kAlign; // float4-aligned element count
273
+ const auto base = units / CS, extra = units % CS; // elements per block
274
+ const auto lu = base + (rank < extra ? 1u : 0u); // remainder blocks
275
+ const auto ou =
276
+ rank * base + min(rank, extra); // this block's count (load-balanced)
277
+ const auto my_start = ou * kAlign; // global start offset
278
+ const auto my_len = min(my_start + lu * kAlign, seq_len) -
279
+ my_start; // actual length of this block
280
+ const auto num_iters =
281
+ (my_len + kSizePerStage - 1) / kSizePerStage; // TMA stages needed
282
+ const auto len_aligned = (my_len + 3u) & ~3u;
283
+
284
+ if constexpr (kFused) {
285
+ // Fused init + TMA prologue
286
+ if (tx < kHistBins) {
287
+ smem->histogram[tx] = 0; // all threads zero histogram
288
+ }
289
+ if (tx == 0) { // thread 0 issues TMA - then all threads continue working
290
+ // until mbarrier sync
291
+ smem->counter_gt = 0;
292
+ smem->counter_eq = 0;
293
+ for (uint32_t i = 0; i < num_iters; i++) {
294
+ const auto off = i * kSizePerStage;
295
+ const auto sz = min(kSizePerStage, len_aligned - off) * sizeof(float);
296
+ tma_load(smem->score_buffer[i], row_input + my_start + off, sz,
297
+ &smem->barrier[0][i]); // cp.async.bulk of size kSizePerStage
298
+ // × sizeof(float)
299
+ mbarrier_arrive_expect_tx(&smem->barrier[0][i], sz);
300
+ }
301
+ }
302
+ __syncthreads();
303
+
304
+ // Histogram build. ILP unroll-by-2, no inter-stage sync
305
+ for (uint32_t iter = 0; iter < num_iters; iter++) {
306
+ const auto off = iter * kSizePerStage;
307
+ const auto sz = min(kSizePerStage, my_len - off);
308
+ if (lane == 0) {
309
+ mbarrier_wait(&smem->barrier[0][iter],
310
+ phases[iter] & 1); // wait for TMA
311
+ }
312
+ phases[iter]++;
313
+ __syncwarp();
314
+#pragma unroll
315
+ for (uint32_t i = 0; i < kElemPerStage; i += 2) {
316
+ const auto li0 = tx + i * hist4096::kBlockSize;
317
+ const auto li1 = tx + (i + 1) * hist4096::kBlockSize;
318
+ if (li0 >= sz) {
319
+ break;
320
+ }
321
+ const auto b0 = extract_coarse_bin(smem->score_buffer[iter][li0]);
322
+ if (li1 < sz) {
323
+ const auto b1 = extract_coarse_bin(smem->score_buffer[iter][li1]);
324
+ atomicAdd(&smem->histogram[b0], 1);
325
+ atomicAdd(&smem->histogram[b1], 1);
326
+ } else {
327
+ atomicAdd(&smem->histogram[b0], 1);
328
+ }
329
+ }
330
+ }
331
+ } else {
332
+ // Twopass: init then stream histogram pass
333
+ if (tx < kHistBins) {
334
+ smem->histogram[tx] = 0;
335
+ }
336
+ if (tx == 0) {
337
+ smem->counter_gt = 0;
338
+ smem->counter_eq = 0;
339
+ }
340
+ __syncthreads();
341
+ tma_stream_pass<SmemType, kStreamingStagesCS4, kHistBits, false>(
342
+ row_input + my_start, my_len, 0, nullptr, phases, smem);
343
+ }
344
+
345
+ // DSMEM all-reduce + find threshold
346
+ dsmem_hist_reduce<CS>(
347
+ smem->histogram); // each block histogram is summed across all CS blocks
348
+ find_threshold<TopK>(smem->histogram, smem->warp_sum, &smem->counter_gt,
349
+ &smem->counter_eq, &smem->match);
350
+
351
+ const auto thr = smem->match.bin;
352
+
353
+ if constexpr (kFused) {
354
+ // Fused scatter: rescan score_buffer (still in smem)
355
+ for (uint32_t iter = 0; iter < num_iters; iter++) {
356
+ const auto off = iter * kSizePerStage;
357
+ const auto sz = min(kSizePerStage, my_len - off);
358
+#pragma unroll
359
+ for (uint32_t i = 0; i < kElemPerStage; i++) {
360
+ const auto li = tx + i * hist4096::kBlockSize;
361
+ if (li >= sz) {
362
+ break;
363
+ }
364
+ const auto score = smem->score_buffer[iter][li]; // still in smem
365
+ const auto bin = extract_coarse_bin(score);
366
+ const auto gidx = off + li;
367
+ if (bin > thr) {
368
+ s_topk[atomicAdd(&smem->counter_gt, 1)] = gidx; // above -> s_topk
369
+ } else if (bin == thr) {
370
+ const auto p = atomicAdd(&smem->counter_eq,
371
+ 1); // equal -> ties (later refinement)
372
+ if (p < hist4096::kMaxTies) {
373
+ smem->tie_buffer[p] = {gidx, score};
374
+ }
375
+ }
376
+ }
377
+ }
378
+ __syncthreads();
379
+ } else {
380
+ // Twopass scatter: re-stream data via TMA
381
+ uint32_t scatter_phases[kStreamingStagesCS4] = {0, 0};
382
+ tma_stream_pass<SmemType, kStreamingStagesCS4, kHistBits, true>(
383
+ row_input + my_start, my_len, thr, s_topk, scatter_phases, smem);
384
+ }
385
+
386
+ // Output collection via DSMEM prefix sum
387
+ constexpr uint32_t kAboveBits = 16;
388
+ constexpr uint32_t kAboveMask = (1 << kAboveBits) - 1;
389
+ static_assert(kAboveMask >= TopK);
390
+ static_assert(kAboveMask >= kMaxSinglePassPerBlock,
391
+ "kAboveBits must cover max per-block element count");
392
+
393
+ const uint32_t la = smem->counter_gt;
394
+ const uint32_t le_full = smem->counter_eq;
395
+ const uint32_t le =
396
+ min(le_full, hist4096::kMaxTies); // written smem tie_buffer entries
397
+
398
+ __shared__ uint32_t s_local_counts[CS];
399
+ __shared__ uint32_t s_prefix_packed;
400
+ __shared__ uint32_t s_total_above, s_total_equal;
401
+
402
+ auto cluster = cooperative_groups::this_cluster();
403
+ if (tx < CS) {
404
+ // Pack written tie counts into 32-bit: (equal << 16) | above.
405
+ // `le_full` may exceed the per-block tie buffer cap; using it here creates
406
+ // holes in tie_ws and can make TopK=2048 refine unwritten workspace slots.
407
+ const uint32_t packed = (le << kAboveBits) | la;
408
+ const auto dst = cluster.map_shared_rank(s_local_counts, tx);
409
+ dst[rank] = packed; // write my count to every block's s_local_counts[rank]
410
+ }
411
+ cluster.sync();
412
+
413
+ // Thread 0 computes serial prefix sum
414
+ if (tx == 0) {
415
+ uint32_t prefix = 0, ta = 0, te = 0;
416
+ for (uint32_t i = 0; i < CS; i++) {
417
+ if (i == rank) {
418
+ s_prefix_packed = prefix; // my prefix
419
+ }
420
+ ta += s_local_counts[i] & kAboveMask; // total above
421
+ te += s_local_counts[i] >> kAboveBits; // total equal
422
+ prefix += s_local_counts[i];
423
+ }
424
+ s_total_above = ta;
425
+ s_total_equal = te;
426
+ }
427
+ __syncthreads();
428
+
429
+ const uint32_t prefix_above = s_prefix_packed & kAboveMask;
430
+ const uint32_t prefix_equal = s_prefix_packed >> kAboveBits;
431
+
432
+ // Write to global output
433
+ for (uint32_t i = tx; i < la; i += hist4096::kBlockSize) {
434
+ // indices are placed contiguously starting at prefix_above
435
+ row_output[prefix_above + i] =
436
+ s_topk[i] + my_start; // my_start: block-local -> row-global index
437
+ }
438
+ for (uint32_t i = tx; i < le; i += hist4096::kBlockSize) {
439
+ const auto t = smem->tie_buffer[i];
440
+ uint32_t p = s_total_above + prefix_equal + i;
441
+ if (p < TopK) {
442
+ row_output[p] = t.idx + my_start;
443
+ }
444
+ uint32_t tp = prefix_equal + i;
445
+ if (tp < (TopK <= hist4096::kBlockSize ? hist4096::kMaxTies : TopK)) {
446
+ tie_ws[tp] = hist4096::Tie{t.idx + my_start, t.score};
447
+ }
448
+ }
449
+
450
+ // Tie refinement
451
+ cooperative_groups::this_cluster().sync();
452
+ if (rank != 0) { // only rank 0 does tie refinement
453
+ return;
454
+ }
455
+ if (s_total_above + s_total_equal <= TopK) { // no ties to refine
456
+ return;
457
+ }
458
+
459
+ // Tie-breaking uses FP32 (4-round radix sort)
460
+ if constexpr (TopK <= hist4096::kBlockSize) {
461
+ // copy ties from tie_ws back to smem, then refine
462
+ const uint32_t num_ties = min(s_total_equal, hist4096::kMaxTies);
463
+ // TODO (roberto): could vectorize with uint2 (8 bytes = exactly one Tie)
464
+ for (uint32_t i = tx; i < num_ties; i += hist4096::kBlockSize) {
465
+ smem->tie_buffer[i] = hist4096::Tie{tie_ws[i].idx, tie_ws[i].score};
466
+ }
467
+ __syncthreads();
468
+ hist4096::tie_handle<TopK>(smem->tie_buffer, num_ties, s_total_above,
469
+ row_output, smem);
470
+ } else {
471
+ // TopK=2048: process directly from tie_ws (GMEM)
472
+ const uint32_t num_ties = min(s_total_equal, static_cast<uint32_t>(TopK));
473
+ hist4096::tie_handle_large<TopK>(tie_ws, num_ties, s_total_above,
474
+ row_output, smem);
475
+ }
476
+}
477
+
478
+// ============================================================================
479
+// Adapted from https://github.com/sgl-project/sglang/pull/23600
480
+// sgl-project/sglang
481
+// (python/sglang/jit_kernel/include/sgl_kernel/deepseek_v4/topk/)
482
+// ============================================================================
483
+
484
+template <uint32_t TopK, uint32_t CS>
485
+__device__ void cooperative_topk_body(CooperativeTopKParams<TopK> params) {
486
+ const auto rank = blockIdx.y, row = blockIdx.x, tx = threadIdx.x;
487
+ const auto sl = params.lengths[row];
488
+ int32_t* out = params.output + row * TopK;
489
+ const float* in = params.input + row * params.stride;
490
+
491
+ // Trivial: seq_len <= TopK
492
+ if (sl <= static_cast<int32_t>(TopK)) {
493
+ if (rank == 0) {
494
+ for (uint32_t i = tx; i < TopK; i += hist4096::kBlockSize) {
495
+ out[i] = (i < static_cast<uint32_t>(sl)) ? static_cast<int32_t>(i) : -1;
496
+ }
497
+ }
498
+ return;
499
+ }
500
+
501
+ // Short-Medium path: histogram_4096_topk on rank 0 only - all data fits in RF
502
+ if (sl <= static_cast<int32_t>(hist4096::kHist4096MaxLen)) {
503
+ if (rank == 0) {
504
+ extern __shared__ uint8_t sr[];
505
+ hist4096::histogram_4096_topk<TopK, 12>(
506
+ in, out, sl, sr); // 4096-bin (12-bit) histogram
507
+ }
508
+ return;
509
+ }
510
+
511
+ // Large path: init mbarriers + state, then dispatch fused or twopass
512
+ const uint32_t per_block =
513
+ (params.stride + CS - 1) / CS; // how many elements per block
514
+ constexpr uint32_t kFusedMax = ((CS == 16) ? kFusedStagesCS16
515
+ : (CS == 8) ? kFusedStagesCS8
516
+ : kMaxSinglePassStages) *
517
+ kSizePerStage;
518
+ const bool use_singlepass =
519
+ per_block <=
520
+ kFusedMax; // single pass or TMA streaming: histogram+scatter
521
+
522
+ // Select smem type and stage count at compile time based on CS
523
+ constexpr uint32_t kFusedStages = (CS == 16) ? kFusedStagesCS16
524
+ : (CS == 8) ? kFusedStagesCS8
525
+ : kMaxSinglePassStages;
526
+ using FusedSmem = SmemFused<kFusedStages>;
527
+
528
+ extern __shared__ uint8_t sr[];
529
+
530
+ constexpr uint32_t kTieWsPerRow =
531
+ TopK <= hist4096::kBlockSize ? hist4096::kMaxTies : TopK;
532
+ hist4096::Tie* row_tie_ws = params.tie_ws + row * kTieWsPerRow;
533
+
534
+ if (use_singlepass) {
535
+ auto* smem = reinterpret_cast<FusedSmem*>(sr);
536
+ const uint32_t sp_stages = (per_block + kSizePerStage - 1) / kSizePerStage;
537
+ if (tx < sp_stages) {
538
+ mbarrier_init(&smem->barrier[0][tx],
539
+ 1); // init 1 barrier per TMA stage -
540
+ // signal when async copies complete
541
+ }
542
+ __syncthreads();
543
+ uint32_t phases[kFusedStages] =
544
+ {}; // tracks the parity for mbarrier wait/arrive protocol
545
+ large_topk<TopK, CS, FusedSmem, true>(in, out, sl, phases, row_tie_ws);
546
+ } else {
547
+ // Two-pass: only CS=4 in practice (CS=8 always fits in singlepass)
548
+ auto* smem = reinterpret_cast<Smem4*>(sr);
549
+ if (tx < 2 * kStreamingStagesCS4) {
550
+ mbarrier_init(&smem->barrier[0][tx],
551
+ 1); // init 2×2=4 barriers (2 passes × 2 stages)
552
+ }
553
+ __syncthreads();
554
+ uint32_t hp[kStreamingStagesCS4] = {0,
555
+ 0}; // histogram+scatter pass counters
556
+ large_topk<TopK, CS, Smem4, false>(in, out, sl, hp, row_tie_ws);
557
+ }
558
+}
559
+
560
+template <uint32_t TopK>
561
+__global__ void __launch_bounds__(hist4096::kBlockSize, 1)
562
+ __cluster_dims__(1, 4, 1)
563
+ cooperative_topk_cs4(CooperativeTopKParams<TopK> params) {
564
+ cooperative_topk_body<TopK, 4>(params);
565
+}
566
+
567
+template <uint32_t TopK>
568
+__global__ void __launch_bounds__(hist4096::kBlockSize, 1)
569
+ __cluster_dims__(1, 8, 1)
570
+ cooperative_topk_cs8(CooperativeTopKParams<TopK> params) {
571
+ cooperative_topk_body<TopK, 8>(params);
572
+}
573
+
574
+template <uint32_t TopK>
575
+__global__ void __launch_bounds__(hist4096::kBlockSize, 1)
576
+ __cluster_dims__(1, 16, 1)
577
+ cooperative_topk_cs16(CooperativeTopKParams<TopK> params) {
578
+ cooperative_topk_body<TopK, 16>(params);
579
+}
580
+
581
+constexpr size_t kSmemSize4_base = sizeof(Smem4);
582
+constexpr size_t kSmemSize4_sp = sizeof(SmemSinglePass);
583
+constexpr size_t kSmemSize4 =
584
+ (kSmemSize4_base > kSmemSize4_sp ? kSmemSize4_base : kSmemSize4_sp) +
585
+ sizeof(int32_t) * 2048 + 128;
586
+constexpr size_t kSmemSize8 =
587
+ sizeof(SmemFused<kFusedStagesCS8>) + sizeof(int32_t) * 2048 + 128;
588
+
589
+} // namespace cooperative
590
+
591
+} // namespace vllm
592
+
593
+#endif // COOPERATIVE_TOPK_CUH_
csrc/libtorch_stable/ops.h
+8
@@ -343,6 +343,14 @@ void persistent_topk(const torch::stable::Tensor& logits,
343
torch::stable::Tensor& workspace, int64_t k,
344
int64_t max_seq_len);
345
346
+#ifdef VLLM_ENABLE_COOPERATIVE_TOPK
347
+void cooperative_topk(const torch::stable::Tensor& logits,
348
+ const torch::stable::Tensor& lengths,
349
+ torch::stable::Tensor& output,
350
+ torch::stable::Tensor& workspace, int64_t k,
351
+ int64_t max_seq_len);
352
+#endif
353
+
354
void selective_scan_fwd(
355
const torch::stable::Tensor& u, const torch::stable::Tensor& delta,
356
const torch::stable::Tensor& A, const torch::stable::Tensor& B,
csrc/libtorch_stable/persistent_topk.cuh
+49
-18
@@ -11,6 +11,8 @@
11
#include <cub/cub.cuh>
12
#include <cstdint>
13
14
+#include "topk_histogram_4096.cuh"
15
+
16
namespace vllm {
17
namespace persistent {
18
@@ -935,8 +937,16 @@ __global__ void __launch_bounds__(kThreadsPerBlock, 2)
937
} // namespace persistent
938
939
// ============================================================================
938
-// FlashInfer FilteredTopK (BS>32 dispatch) — float32 only.
939
-// Extracted from flashinfer_topk.cuh. Lives in namespace vllm (not persistent).
940
+// ============================================================================
941
+// Optimized FilteredTopK — single CTA per row for bs > 32.
942
+// Kept with persistent_topk so the portable fallback owns the non-cluster path.
943
+// ============================================================================
944
+namespace filtered_topk {
945
+
946
+namespace hist4096 = topk_histogram_4096;
947
+
948
+// ============================================================================
949
+// FilteredTopK — single CTA per row for bs > 32
950
// Adapted from https://github.com/flashinfer-ai/flashinfer/pull/2215
951
// ============================================================================
952
@@ -963,13 +973,6 @@ struct vec_t {
973
data[i] = ptr[i];
974
}
975
}
966
-
967
- FLASHINFER_INLINE void cast_store(T* ptr) const {
968
-#pragma unroll
969
- for (size_t i = 0; i < N; ++i) {
970
- ptr[i] = data[i];
971
- }
972
- }
976
};
977
#undef FLASHINFER_INLINE
978
@@ -1013,7 +1016,8 @@ constexpr size_t FILTERED_TOPK_SMEM_DYNAMIC =
1016
* \tparam IdType Index type (int32_t)
1017
* \tparam VEC_SIZE Vector size for input loads (1, 2, 4, or 8)
1018
*/
1016
-template <typename DType, typename IdType, int VEC_SIZE, uint32_t MAX_K = 2048>
1019
+template <typename DType, typename IdType, int VEC_SIZE, uint32_t MAX_K = 2048,
1020
+ bool UsePredicatedShortLoads = false>
1021
__global__ void __launch_bounds__(FILTERED_TOPK_BLOCK_THREADS)
1022
FilteredTopKUnifiedKernel(const DType* __restrict__ input,
1023
IdType* __restrict__ output,
@@ -1042,6 +1046,19 @@ __global__ void __launch_bounds__(FILTERED_TOPK_BLOCK_THREADS)
1046
return;
1047
}
1048
1049
+ // Short path
1050
+ if (length <= 32768) {
1051
+ extern __shared__ uint8_t _smem_reg[];
1052
+ if constexpr (UsePredicatedShortLoads) {
1053
+ hist4096::histogram_4096_topk_predicated<MAX_K, 12, 8>(score, dst, length,
1054
+ _smem_reg);
1055
+ } else {
1056
+ hist4096::histogram_4096_topk<MAX_K, 12, 8>(score, dst, length,
1057
+ _smem_reg);
1058
+ }
1059
+ return;
1060
+ }
1061
+
1062
// Static shared memory
1063
alignas(128) __shared__ int s_histogram_buf[2][RADIX + 128];
1064
alignas(128) __shared__ int s_counter;
@@ -1285,14 +1302,15 @@ cudaError_t FilteredTopKRaggedTransform(const DType* input,
1302
1303
const int vec_size = ComputeFilteredTopKVecSize<DType>(max_len);
1304
1288
-#define DISPATCH_VEC_SIZE(VS) \
1289
- if (vec_size == VS) { \
1290
- auto kernel = FilteredTopKUnifiedKernel<DType, IdType, VS, MAX_K>; \
1291
- FLASHINFER_CUDA_CALL(cudaFuncSetAttribute( \
1292
- kernel, cudaFuncAttributeMaxDynamicSharedMemorySize, smem_size)); \
1293
- FLASHINFER_CUDA_CALL(cudaLaunchKernel((void*)kernel, grid, block, args, \
1294
- smem_size, stream)); \
1295
- return cudaSuccess; \
1305
+#define DISPATCH_VEC_SIZE(VS) \
1306
+ if (vec_size == VS) { \
1307
+ auto kernel = \
1308
+ FilteredTopKUnifiedKernel<DType, IdType, VS, MAX_K, (VS != MAX_VEC)>; \
1309
+ FLASHINFER_CUDA_CALL(cudaFuncSetAttribute( \
1310
+ kernel, cudaFuncAttributeMaxDynamicSharedMemorySize, smem_size)); \
1311
+ FLASHINFER_CUDA_CALL(cudaLaunchKernel((void*)kernel, grid, block, args, \
1312
+ smem_size, stream)); \
1313
+ return cudaSuccess; \
1314
}
1315
1316
DISPATCH_VEC_SIZE(1)
@@ -1306,6 +1324,19 @@ cudaError_t FilteredTopKRaggedTransform(const DType* input,
1324
return cudaSuccess;
1325
}
1326
1327
+} // namespace filtered_topk
1328
+
1329
+template <typename DType, typename IdType, uint32_t MAX_K = 2048>
1330
+cudaError_t FilteredTopKRaggedTransform(const DType* input,
1331
+ IdType* output_indices,
1332
+ const IdType* lengths,
1333
+ uint32_t num_rows, uint32_t top_k_val,
1334
+ uint32_t max_len,
1335
+ cudaStream_t stream = 0) {
1336
+ return filtered_topk::FilteredTopKRaggedTransform<DType, IdType, MAX_K>(
1337
+ input, output_indices, lengths, num_rows, top_k_val, max_len, stream);
1338
+}
1339
+
1340
} // namespace vllm
1341
1342
#endif // PERSISTENT_TOPK_CUH_
csrc/libtorch_stable/topk_histogram_4096.cuh
new
+554
@@ -0,0 +1,554 @@
1
+/*
2
+ * Shared 4096-bin single-CTA TopK helpers.
3
+ */
4
+
5
+#ifndef TOPK_HISTOGRAM_4096_CUH_
6
+#define TOPK_HISTOGRAM_4096_CUH_
7
+
8
+#include <cuda_fp16.h>
9
+#include <cuda_runtime.h>
10
+#include <cstdint>
11
+
12
+namespace vllm {
13
+namespace topk_histogram_4096 {
14
+
15
+constexpr uint32_t kBlockSize = 1024;
16
+constexpr uint32_t RADIX = 256;
17
+constexpr uint32_t kMaxTies = 1024;
18
+static_assert(kMaxTies <= kBlockSize,
19
+ "tie_handle requires kMaxTies <= kBlockSize");
20
+constexpr uint32_t kWarpSize = 32;
21
+constexpr uint32_t kNumWarps = kBlockSize / kWarpSize;
22
+
23
+// Register path
24
+constexpr uint32_t kHist4096VecsPerThread = 4;
25
+constexpr uint32_t kHist4096MaxLen =
26
+ kHist4096VecsPerThread * 4 * kBlockSize; // 16384
27
+
28
+struct alignas(16) MatchBin {
29
+ uint32_t bin, above_count, equal_count;
30
+};
31
+struct alignas(8) Tie {
32
+ uint32_t idx;
33
+ float score;
34
+};
35
+
36
+__device__ __forceinline__ void load_float4_predicated(const float* ptr,
37
+ int base, int seq_len,
38
+ float& v0, float& v1,
39
+ float& v2, float& v3) {
40
+ uint32_t r0, r1, r2, r3;
41
+ const int p0 = (base < seq_len);
42
+ const int p1 = (base + 1 < seq_len);
43
+ const int p2 = (base + 2 < seq_len);
44
+ const int p3 = (base + 3 < seq_len);
45
+ asm volatile(
46
+ "{\n"
47
+ " .reg .pred pr0, pr1, pr2, pr3;\n"
48
+ " setp.ne.u32 pr0, %4, 0;\n"
49
+ " setp.ne.u32 pr1, %5, 0;\n"
50
+ " setp.ne.u32 pr2, %6, 0;\n"
51
+ " setp.ne.u32 pr3, %7, 0;\n"
52
+ " mov.u32 %0, 0xFF800000;\n"
53
+ " mov.u32 %1, 0xFF800000;\n"
54
+ " mov.u32 %2, 0xFF800000;\n"
55
+ " mov.u32 %3, 0xFF800000;\n"
56
+ " @pr0 ld.global.cg.u32 %0, [%8];\n"
57
+ " @pr1 ld.global.cg.u32 %1, [%8+4];\n"
58
+ " @pr2 ld.global.cg.u32 %2, [%8+8];\n"
59
+ " @pr3 ld.global.cg.u32 %3, [%8+12];\n"
60
+ "}\n"
61
+ : "=r"(r0), "=r"(r1), "=r"(r2), "=r"(r3)
62
+ : "r"(p0), "r"(p1), "r"(p2), "r"(p3), "l"(ptr));
63
+ v0 = __uint_as_float(r0);
64
+ v1 = __uint_as_float(r1);
65
+ v2 = __uint_as_float(r2);
66
+ v3 = __uint_as_float(r3);
67
+}
68
+
69
+// converts the float32 score to a 32-bit ordered unsigned integer — the full
70
+// precision key for radix sorting
71
+__device__ __forceinline__ auto convert_to_uint32_v2(float x) -> uint32_t {
72
+ uint32_t bits = __float_as_uint(x);
73
+ return (bits & 0x80000000u) ? ~bits : (bits | 0x80000000u);
74
+}
75
+
76
+// Converts each score to a 12-bit bin (FP16 sign-magnitude -> top 12 bits ->
77
+// bin 0-4095)
78
+template <uint32_t kBits>
79
+__device__ __forceinline__ uint32_t extract_coarse_bin_N(float x) {
80
+ __half h = __float2half_rn(x);
81
+ uint16_t bits = __half_as_ushort(h);
82
+ uint16_t key = (bits & 0x8000) ? static_cast<uint16_t>(~bits)
83
+ : static_cast<uint16_t>(bits | 0x8000);
84
+ return key >> (16 - kBits);
85
+}
86
+
87
+// running sum within each warp — thread 0 gets its own value, thread 1 gets
88
+// thread 0 + thread 1, thread 2 gets threads 0+1+2, etc.
89
+__device__ __forceinline__ uint32_t warp_inclusive_sum(uint32_t lane,
90
+ uint32_t v) {
91
+#pragma unroll
92
+ for (uint32_t o = 1; o < 32; o *= 2) {
93
+ uint32_t n = __shfl_up_sync(0xFFFFFFFF, v, o);
94
+ if (lane >= o) v += n;
95
+ }
96
+ return v;
97
+}
98
+
99
+// Returns the sum of a value across all 32 threads in the warp, and every
100
+// thread gets the same result SM90+ PTX instruction that does a hardware
101
+// warp-wide reduction in a single instruction w.r.t. warp::reduce_sum(), which
102
+// uses a __shfl_xor_sync butterfly tree (5 shuffles for 32 lanes)
103
+__device__ __forceinline__ uint32_t warp_reduce_sum_full(uint32_t v) {
104
+ uint32_t r;
105
+ asm("redux.sync.add.u32 %0, %1, 0xFFFFFFFF;" : "=r"(r) : "r"(v));
106
+ return r;
107
+}
108
+
109
+// ============================================================================
110
+// Tie refinement (single CTA): 4-round radix-256 topK on the full FP32 ordered
111
+// key Each round narrows by 8 bits until ties are fully resolved
112
+// ============================================================================
113
+
114
+template <uint32_t TopK>
115
+__device__ void tie_handle(const Tie* ties, uint32_t num_ties,
116
+ uint32_t num_above, int32_t* output, void* _smem) {
117
+ struct TS {
118
+ alignas(128) uint32_t counter;
119
+ alignas(128) MatchBin match;
120
+ uint32_t histogram[RADIX];
121
+ uint32_t warp_sum[kNumWarps];
122
+ };
123
+ auto* s = static_cast<TS*>(_smem);
124
+ const auto tx = threadIdx.x;
125
+ const auto li = tx % kWarpSize, wi = tx / kWarpSize;
126
+
127
+ // Each thread loads one tie element.
128
+ const bool has = tx < num_ties;
129
+ const auto tie = has ? ties[tx] : Tie{0, 0.0f};
130
+ const uint32_t key = convert_to_uint32_v2(tie.score);
131
+
132
+ bool active = has; // tracks whether this thread's tie is still a candidate.
133
+ uint32_t remain =
134
+ TopK - num_above; // decreases each round as ties are resolved.
135
+ uint32_t wpos = TopK; // wpos will hold the final output position.
136
+ s->counter = 0;
137
+ __syncthreads();
138
+
139
+ // The 4-round radix loop - each round narrows by 8 bits until ties are fully
140
+ // resolved
141
+#pragma unroll
142
+ for (int r = 0; r < 4; r++) {
143
+ uint32_t sh = 24 - r * 8; // round 0: bits 31-24, round 1: 23-16, etc.
144
+ uint32_t bin = (key >> sh) & 0xFF; // this tie's 8-bit bin for this round
145
+
146
+ // Step 1: Build 256-bin histogram.
147
+ if (tx < RADIX) s->histogram[tx] = 0;
148
+ __syncthreads();
149
+ if (active) atomicAdd(&s->histogram[bin], 1);
150
+ __syncthreads();
151
+
152
+ // Step 2: Prefix scan to find threshold
153
+ uint32_t hv = 0, wi2 = 0;
154
+ if (tx < RADIX) {
155
+ hv = s->histogram[tx];
156
+ wi2 = warp_inclusive_sum(li, hv);
157
+ if (li == kWarpSize - 1) s->warp_sum[wi] = wi2;
158
+ }
159
+ __syncthreads();
160
+
161
+ if (tx < RADIX) {
162
+ auto tmp = (li < RADIX / kWarpSize) ? s->warp_sum[li] : 0;
163
+ auto tot = warp_reduce_sum_full(tmp);
164
+ auto inter = warp_reduce_sum_full(li < wi ? tmp : 0);
165
+ auto above = tot - (inter + wi2);
166
+ if (above < remain && above + hv >= remain) {
167
+ s->match = {tx, above, remain - above};
168
+ }
169
+ }
170
+ __syncthreads();
171
+
172
+ // Step 3: Scatter
173
+ auto [thr, na, _] = s->match; // threshold bin, num above, unused
174
+ if (active) {
175
+ if (bin > thr) {
176
+ wpos = num_above +
177
+ atomicAdd(&s->counter, 1); // above -> place in output directly
178
+ active = false;
179
+ } else if (bin < thr)
180
+ active = false; // below -> discard
181
+ else if (r == 3)
182
+ wpos = TopK - atomicAdd(&s->match.equal_count,
183
+ -1u); // last round: place remaining
184
+ }
185
+ remain -= na;
186
+ if (!remain) break; // all ties resolved early
187
+ }
188
+ // Final write
189
+ if (wpos < TopK) output[wpos] = tie.idx;
190
+}
191
+
192
+// Extended tie_handle for TopK > kBlockSize (e.g. TopK=2048).
193
+// tie_handle assumes 1 tie per thread (max 1024).
194
+// This version handles 2 ties per thread via kPerThread=2
195
+template <uint32_t TopK>
196
+__device__ void tie_handle_large(const Tie* ties, uint32_t num_ties,
197
+ uint32_t num_above, int32_t* output,
198
+ void* _smem) {
199
+ static_assert(TopK > kBlockSize);
200
+ struct TS {
201
+ alignas(128) uint32_t counter;
202
+ alignas(128) MatchBin match;
203
+ uint32_t histogram[RADIX];
204
+ uint32_t warp_sum[kNumWarps];
205
+ };
206
+ auto* s = static_cast<TS*>(_smem);
207
+ const auto tx = threadIdx.x;
208
+ const auto li = tx % kWarpSize;
209
+ const auto wi = tx / kWarpSize;
210
+
211
+ constexpr uint32_t kPerThread = (TopK + kBlockSize - 1) / kBlockSize;
212
+ Tie my_ties[kPerThread];
213
+ uint32_t keys[kPerThread];
214
+ bool active[kPerThread];
215
+
216
+ for (uint32_t e = 0; e < kPerThread; e++) {
217
+ uint32_t idx = e * kBlockSize + tx;
218
+ if (idx < num_ties) {
219
+ my_ties[e] = ties[idx];
220
+ keys[e] = convert_to_uint32_v2(ties[idx].score);
221
+ active[e] = true;
222
+ } else {
223
+ my_ties[e] = {0, 0.0f};
224
+ keys[e] = 0;
225
+ active[e] = false;
226
+ }
227
+ }
228
+
229
+ uint32_t remain = TopK - num_above;
230
+ s->counter = 0;
231
+ __syncthreads();
232
+
233
+ for (int r = 0; r < 4; r++) {
234
+ uint32_t sh = 24 - r * 8;
235
+ if (tx < RADIX) {
236
+ s->histogram[tx] = 0;
237
+ }
238
+ __syncthreads();
239
+
240
+ for (uint32_t e = 0; e < kPerThread; e++) {
241
+ if (active[e]) {
242
+ atomicAdd(&s->histogram[(keys[e] >> sh) & 0xFF], 1);
243
+ }
244
+ }
245
+ __syncthreads();
246
+
247
+ uint32_t hv = 0;
248
+ if (tx < RADIX) {
249
+ hv = s->histogram[tx];
250
+ auto wi2 = warp_inclusive_sum(li, hv);
251
+ if (li == kWarpSize - 1) {
252
+ s->warp_sum[wi] = wi2;
253
+ }
254
+ }
255
+ __syncthreads();
256
+ if (tx < RADIX) {
257
+ auto tmp2 = (li < RADIX / kWarpSize) ? s->warp_sum[li] : 0;
258
+ auto total = warp_reduce_sum_full(tmp2);
259
+ auto inter = warp_reduce_sum_full(li < wi ? tmp2 : 0);
260
+ auto wi2 = warp_inclusive_sum(li, hv);
261
+ auto above = total - (inter + wi2);
262
+ if (above < remain && above + hv >= remain) {
263
+ s->match = {
264
+ .bin = tx, .above_count = above, .equal_count = remain - above};
265
+ }
266
+ }
267
+ __syncthreads();
268
+
269
+ auto thr = s->match.bin;
270
+ auto na = s->match.above_count;
271
+
272
+ for (uint32_t e = 0; e < kPerThread; e++) {
273
+ if (!active[e]) {
274
+ continue;
275
+ }
276
+ uint32_t bin = (keys[e] >> sh) & 0xFF;
277
+ if (bin > thr) {
278
+ uint32_t wpos = num_above + atomicAdd(&s->counter, 1);
279
+ if (wpos < TopK) {
280
+ output[wpos] = my_ties[e].idx;
281
+ }
282
+ active[e] = false;
283
+ } else if (bin < thr) {
284
+ active[e] = false;
285
+ } else if (r == 3) {
286
+ uint32_t wpos = TopK - atomicAdd(&s->match.equal_count, -1u);
287
+ if (wpos < TopK) {
288
+ output[wpos] = my_ties[e].idx;
289
+ }
290
+ }
291
+ }
292
+
293
+ num_above += na;
294
+ remain -= na;
295
+ __syncthreads();
296
+ s->counter = 0;
297
+ __syncthreads();
298
+ }
299
+}
300
+
301
+// ============================================================================
302
+// Register-based single-CTA fast path for seq_len <= 16384
303
+// 4 float4 per thread × 1024 threads = 16384 elements max
304
+// Uses 4096-bin (12-bit) histogram for better precision
305
+// ============================================================================
306
+
307
+template <uint32_t TopK, uint32_t HIST_BITS>
308
+struct Histogram4096Smem {
309
+ static constexpr uint32_t HIST_BINS = 1 << HIST_BITS;
310
+ static constexpr uint32_t TIE_CAPACITY = TopK > kMaxTies ? TopK : kMaxTies;
311
+ alignas(128) uint32_t counter_gt;
312
+ alignas(128) uint32_t counter_eq;
313
+ MatchBin match;
314
+ uint32_t warp_sum[kNumWarps];
315
+ union {
316
+ uint32_t histogram[HIST_BINS];
317
+ Tie tie_buffer[TIE_CAPACITY];
318
+ };
319
+};
320
+
321
+template <uint32_t TopK, uint32_t HIST_BITS,
322
+ uint32_t VECS_PER_THREAD = kHist4096VecsPerThread,
323
+ bool UsePredicatedLoads = false>
324
+__device__ void histogram_4096_topk(const float* __restrict__ scores,
325
+ int32_t* __restrict__ output,
326
+ uint32_t length, void* _smem) {
327
+ constexpr uint32_t HIST_BINS = 1 << HIST_BITS;
328
+ constexpr uint32_t ITEMS_PER_THREAD = HIST_BINS / kBlockSize;
329
+ static_assert(HIST_BINS >= kBlockSize,
330
+ "HIST_BITS must give >= kBlockSize bins");
331
+
332
+ using Smem = Histogram4096Smem<TopK, HIST_BITS>;
333
+ auto* smem = static_cast<Smem*>(_smem);
334
+ const auto tx = threadIdx.x;
335
+ const auto lane_id = tx % kWarpSize;
336
+ const auto warp_id = tx / kWarpSize;
337
+
338
+ // Phase 1: Load all data into RF + build histogram
339
+ float4
340
+ vecs[VECS_PER_THREAD]; // 4 vectors x 4 floats = 16 elements per thread
341
+ if constexpr (ITEMS_PER_THREAD >= 4) {
342
+ // Zero the histogram (SMEM writes)
343
+ for (uint32_t i = 0; i < ITEMS_PER_THREAD / 4; i++)
344
+ reinterpret_cast<uint4*>(
345
+ smem->histogram)[tx * (ITEMS_PER_THREAD / 4) + i] =
346
+ make_uint4(0, 0, 0, 0);
347
+ } else {
348
+ if (tx < HIST_BINS) smem->histogram[tx] = 0;
349
+ }
350
+ if (tx == 0) {
351
+ smem->counter_gt = 0;
352
+ smem->counter_eq = 0;
353
+ }
354
+ if constexpr (UsePredicatedLoads) {
355
+ const bool row_aligned = (reinterpret_cast<uintptr_t>(scores) & 0xFu) == 0;
356
+#pragma unroll
357
+ for (uint32_t v = 0; v < VECS_PER_THREAD; v++) {
358
+ const uint32_t base = (tx + v * kBlockSize) * 4;
359
+ if (base < length) {
360
+ if (row_aligned && base + 3 < length) {
361
+ vecs[v] = *reinterpret_cast<const float4*>(scores + base);
362
+ } else {
363
+ load_float4_predicated(scores + base, static_cast<int>(base),
364
+ static_cast<int>(length), vecs[v].x, vecs[v].y,
365
+ vecs[v].z, vecs[v].w);
366
+ }
367
+ }
368
+ }
369
+ } else {
370
+#pragma unroll
371
+ for (uint32_t v = 0; v < VECS_PER_THREAD; v++) {
372
+ const uint32_t base = (tx + v * kBlockSize) * 4;
373
+ if (base < length) {
374
+ vecs[v] = *reinterpret_cast<const float4*>(scores + base);
375
+ }
376
+ }
377
+ }
378
+ __syncthreads();
379
+
380
+ // Build histogram from RF via atomic adds into the shared histogram
381
+ bool done = false;
382
+#pragma unroll
383
+ for (uint32_t v = 0; v < VECS_PER_THREAD && !done; v++) {
384
+ const float* elems = reinterpret_cast<const float*>(&vecs[v]);
385
+#pragma unroll
386
+ for (uint32_t e = 0; e < 4 && !done; e++) {
387
+ const uint32_t idx = (tx + v * kBlockSize) * 4 + e;
388
+ if (idx >= length) {
389
+ done = true;
390
+ } else {
391
+ atomicAdd(&smem->histogram[extract_coarse_bin_N<HIST_BITS>(elems[e])],
392
+ 1);
393
+ }
394
+ }
395
+ }
396
+ __syncthreads();
397
+
398
+ // Phase 2: Prefix scan to find threshold bin
399
+ // Multi-element scan (4096 bins: 4 per thread)
400
+ uint32_t orig[ITEMS_PER_THREAD];
401
+ uint32_t local_sum = 0;
402
+
403
+ // Step 1: Each thread sums its 4 bins
404
+#pragma unroll
405
+ for (uint32_t i = 0; i < ITEMS_PER_THREAD; i++) {
406
+ orig[i] = smem->histogram[tx * ITEMS_PER_THREAD + i];
407
+ local_sum += orig[i];
408
+ }
409
+
410
+ // Step 2: Warp-level inclusive prefix sum on local_sum
411
+ const auto warp_inc = warp_inclusive_sum(lane_id, local_sum);
412
+ if (lane_id == kWarpSize - 1) smem->warp_sum[warp_id] = warp_inc;
413
+ __syncthreads();
414
+
415
+ // Step 3: Inter-warp prefix via redux.sync
416
+ const auto tmp = smem->warp_sum[lane_id];
417
+ uint32_t prefix = warp_reduce_sum_full(
418
+ lane_id < warp_id ? tmp : 0); // sum of all prior warps
419
+ prefix +=
420
+ warp_inc - local_sum; // exclusive prefix within this thread's position
421
+
422
+ // Step 4: Find threshold - scan 4 bins, accumulate prefix
423
+#pragma unroll
424
+ for (uint32_t i = 0; i < ITEMS_PER_THREAD; i++) {
425
+ prefix += orig[i];
426
+ const auto above = length - prefix; // elements in bins ABOVE this one
427
+ if (above < TopK && above + orig[i] >= TopK) {
428
+ smem->match = {.bin = tx * ITEMS_PER_THREAD + i,
429
+ .above_count = above,
430
+ .equal_count = orig[i]};
431
+ }
432
+ }
433
+
434
+ __syncthreads();
435
+
436
+ // Phase 3: Scatter from registers
437
+ const auto [thr_bin, num_above, num_equal] = smem->match;
438
+ const bool need_tie = (num_equal + num_above > TopK);
439
+
440
+ done = false;
441
+#pragma unroll
442
+ for (uint32_t v = 0; v < VECS_PER_THREAD && !done; v++) {
443
+ const float* elems = reinterpret_cast<const float*>(&vecs[v]);
444
+#pragma unroll
445
+ for (uint32_t e = 0; e < 4 && !done; e++) {
446
+ const uint32_t idx = (tx + v * kBlockSize) * 4 + e;
447
+ if (idx >= length) {
448
+ done = true;
449
+ } else {
450
+ const uint32_t bin = extract_coarse_bin_N<HIST_BITS>(elems[e]);
451
+ if (bin > thr_bin) {
452
+ output[atomicAdd(&smem->counter_gt, 1)] =
453
+ idx; // above -> output directly
454
+ } else if (bin == thr_bin) {
455
+ const auto pos = atomicAdd(&smem->counter_eq, 1);
456
+ if (!need_tie) {
457
+ if (pos + num_above < TopK) {
458
+ output[pos + num_above] = idx; // all fit
459
+ }
460
+ } else {
461
+ if (pos < TopK) {
462
+ smem->tie_buffer[pos] = {idx, elems[e]}; // store for refirement
463
+ }
464
+ }
465
+ }
466
+ // else: bin < thr_bin - discard (not in top-k)
467
+ }
468
+ }
469
+ }
470
+
471
+ // Phase 4: Tie-breaking
472
+ if (!need_tie) return;
473
+ __syncthreads();
474
+
475
+ // Fast warp-ballot tie-breaking for small tie counts
476
+ const uint32_t num_ties = min(num_equal, static_cast<uint32_t>(TopK));
477
+ const uint32_t topk_remain =
478
+ TopK - num_above; // pick exactly remaining elements to fill topK
479
+
480
+ auto is_greater = [](const Tie& a, const Tie& b) {
481
+ return (a.score > b.score) || (a.score == b.score && a.idx < b.idx);
482
+ };
483
+
484
+ if (num_ties <= kWarpSize) {
485
+ // <=32 ties - Use warp ballot
486
+ // All-to-all comparison in one __ballot_sync. 32 ties x 32 warps = 1024
487
+ // comparisons in one instruction per warp. O(1) work.
488
+ const auto lane_id = tx % kWarpSize;
489
+ const auto warp_id = tx / kWarpSize;
490
+ if (lane_id >= num_ties || warp_id >= num_ties) return;
491
+ const uint32_t mask = (1ull << num_ties) - 1u;
492
+ const auto tie = smem->tie_buffer[lane_id]; // each lane holds one tie
493
+ const auto target =
494
+ smem->tie_buffer[warp_id]; // each warp evaluates one candidate
495
+ const bool pred =
496
+ is_greater(tie, target); // compare all ties against target
497
+ const auto rank = static_cast<uint32_t>(
498
+ __popc(__ballot_sync(mask, pred))); // count how many are greater
499
+ if (lane_id == 0 && rank < topk_remain) {
500
+ output[num_above + rank] = target.idx; // place at correct position
501
+ }
502
+ } else if (num_ties <=
503
+ kWarpSize *
504
+ 2) { // TODO (roberto): try to refactor this with <=32 case
505
+ // Same idea but each thread handles 2 tie elements
506
+ const auto lane_id = tx % kWarpSize;
507
+ const auto warp_id = tx / kWarpSize;
508
+ const auto lane1 = lane_id + kWarpSize;
509
+ const auto warp1 = warp_id + kWarpSize;
510
+ const auto invalid = Tie{0xFFFFFFFF, -__FLT_MAX__};
511
+ const auto tie0 = smem->tie_buffer[lane_id];
512
+ const auto tie1 = lane1 < num_ties ? smem->tie_buffer[lane1] : invalid;
513
+ if (warp_id < num_ties) {
514
+ const auto target = smem->tie_buffer[warp_id];
515
+ const auto r0 =
516
+ __popc(__ballot_sync(0xFFFFFFFF, is_greater(tie0, target)));
517
+ const auto r1 =
518
+ __popc(__ballot_sync(0xFFFFFFFF, is_greater(tie1, target)));
519
+ if (lane_id == 0 && r0 + r1 < topk_remain)
520
+ output[num_above + r0 + r1] = target.idx;
521
+ }
522
+ if (warp1 < num_ties) {
523
+ const auto target = smem->tie_buffer[warp1];
524
+ const auto r0 =
525
+ __popc(__ballot_sync(0xFFFFFFFF, is_greater(tie0, target)));
526
+ const auto r1 =
527
+ __popc(__ballot_sync(0xFFFFFFFF, is_greater(tie1, target)));
528
+ if (lane_id == 0 && r0 + r1 < topk_remain)
529
+ output[num_above + r0 + r1] = target.idx;
530
+ }
531
+ } else {
532
+ // Large tie count: fall back to 4-round radix-256 sort
533
+ if constexpr (TopK <= kBlockSize) {
534
+ tie_handle<TopK>(smem->tie_buffer, num_ties, num_above, output, smem);
535
+ } else {
536
+ tie_handle_large<TopK>(smem->tie_buffer, num_ties, num_above, output,
537
+ smem);
538
+ }
539
+ }
540
+}
541
+
542
+template <uint32_t TopK, uint32_t HIST_BITS,
543
+ uint32_t VECS_PER_THREAD = kHist4096VecsPerThread>
544
+__device__ __noinline__ void histogram_4096_topk_predicated(
545
+ const float* __restrict__ scores, int32_t* __restrict__ output,
546
+ uint32_t length, void* _smem) {
547
+ histogram_4096_topk<TopK, HIST_BITS, VECS_PER_THREAD, true>(scores, output,
548
+ length, _smem);
549
+}
550
+
551
+} // namespace topk_histogram_4096
552
+} // namespace vllm
553
+
554
+#endif // TOPK_HISTOGRAM_4096_CUH_
csrc/libtorch_stable/torch_bindings.cpp
+9
@@ -493,6 +493,12 @@ STABLE_TORCH_LIBRARY_FRAGMENT(_C, ops) {
493
"persistent_topk(Tensor logits, Tensor lengths, Tensor! output, "
494
"Tensor workspace, int k, int max_seq_len) -> ()");
495
496
+#ifdef VLLM_ENABLE_COOPERATIVE_TOPK
497
+ ops.def(
498
+ "cooperative_topk(Tensor logits, Tensor lengths, Tensor! output, "
499
+ "Tensor workspace, int k, int max_seq_len) -> ()");
500
+#endif
501
+
502
// Activation ops
503
ops.def(
504
"persistent_masked_m_silu_mul_quant(Tensor input, Tensor counts, Tensor! "
@@ -711,6 +717,9 @@ STABLE_TORCH_LIBRARY_IMPL(_C, CUDA, ops) {
717
ops.impl("top_k_per_row_prefill", TORCH_BOX(&top_k_per_row_prefill));
718
ops.impl("top_k_per_row_decode", TORCH_BOX(&top_k_per_row_decode));
719
ops.impl("persistent_topk", TORCH_BOX(&persistent_topk));
720
+#ifdef VLLM_ENABLE_COOPERATIVE_TOPK
721
+ ops.impl("cooperative_topk", TORCH_BOX(&cooperative_topk));
722
+#endif
723
724
// Activation kernels (shared CUDA/ROCm)
725
ops.impl("persistent_masked_m_silu_mul_quant",
tests/kernels/test_top_k_per_row.py
+151
-26
@@ -14,6 +14,70 @@ TOP_K_VALUES = [2048, 3000]
14
BATCH_SIZE = [1, 2, 2048]
15
NEXT_N = [1, 8]
16
DATA_GENERATION = ["random", "10LSBits"]
17
+RADIX_TOPK_WORKSPACE_SIZE = 1024 * 1024
18
+
19
+
20
+def _has_device_capability(major: int) -> bool:
21
+ return current_platform.is_cuda() and current_platform.has_device_capability(major)
22
+
23
+
24
+COOPERATIVE_TOPK_BACKEND = pytest.param(
25
+ "cooperative_topk",
26
+ marks=pytest.mark.skipif(
27
+ not _has_device_capability(90),
28
+ reason="cooperative_topk requires SM90+",
29
+ ),
30
+)
31
+WORKSPACE_TOPK_BACKENDS = ["persistent_topk", COOPERATIVE_TOPK_BACKEND]
32
+TOPK_BACKENDS = ["top_k_per_row_decode", *WORKSPACE_TOPK_BACKENDS]
33
+
34
+
35
+def _run_topk_backend(
36
+ backend: str,
37
+ logits: torch.Tensor,
38
+ lengths: torch.Tensor,
39
+ indices: torch.Tensor,
40
+ top_k: int,
41
+ max_seq_len: int,
42
+ next_n: int = 1,
43
+) -> None:
44
+ if backend == "top_k_per_row_decode":
45
+ torch.ops._C.top_k_per_row_decode(
46
+ logits,
47
+ next_n,
48
+ lengths,
49
+ indices,
50
+ indices.shape[0],
51
+ logits.stride(0),
52
+ logits.stride(1),
53
+ top_k,
54
+ )
55
+ elif backend == "persistent_topk":
56
+ workspace = torch.empty(
57
+ RADIX_TOPK_WORKSPACE_SIZE, dtype=torch.uint8, device="cuda"
58
+ )
59
+ torch.ops._C.persistent_topk(
60
+ logits, lengths, indices, workspace, top_k, max_seq_len
61
+ )
62
+ elif backend == "cooperative_topk":
63
+ if indices.shape[0] > 32:
64
+ pytest.skip(
65
+ "cooperative_topk supports <=32 rows; "
66
+ "persistent_topk covers larger batches"
67
+ )
68
+ if logits.stride(0) % 4 != 0:
69
+ pytest.skip(
70
+ "cooperative_topk requires row stride divisible by 4; "
71
+ "persistent_topk covers unaligned strides"
72
+ )
73
+ workspace = torch.empty(
74
+ RADIX_TOPK_WORKSPACE_SIZE, dtype=torch.uint8, device="cuda"
75
+ )
76
+ torch.ops._C.cooperative_topk(
77
+ logits, lengths, indices, workspace, top_k, max_seq_len
78
+ )
79
+ else:
80
+ raise ValueError(f"Unknown top-k backend: {backend}")
81
82
83
def create_random_logits(
@@ -322,16 +386,19 @@ def test_top_k_per_row_decode_large_vocab_size(clean_logits: bool) -> None:
386
@pytest.mark.parametrize("clean_logits", [True, False])
387
@pytest.mark.parametrize("top_k", [2048])
388
@pytest.mark.parametrize("next_n", [1, 4])
389
+@pytest.mark.parametrize("backend", WORKSPACE_TOPK_BACKENDS)
390
@torch.inference_mode()
326
-def test_deepseek_persistent_topk(
391
+def test_deepseek_workspace_topk(
392
seq_len_range: tuple[int, int],
393
test_id: str,
394
clean_logits: bool,
395
top_k: int,
396
next_n: int,
397
+ backend: str,
398
) -> None:
399
"""
334
- Test persistent_topk with varying sequence lengths and speculative decoding.
400
+ Test workspace top-k backends with varying sequence lengths and speculative
401
+ decoding.
402
Supports speculative decoding with next_n > 1.
403
"""
404
set_random_seed(42 if test_id == "short_sequences" else 43)
@@ -347,6 +414,7 @@ def test_deepseek_persistent_topk(
414
dtype=torch.int32,
415
device="cuda",
416
)
417
+ seq_lens = (seq_lens + 3) & ~3 # align to 4 for TMA
418
419
# Compute row boundaries for speculative decoding
420
row_starts = torch.zeros(num_rows, dtype=torch.int32, device="cuda")
@@ -366,14 +434,11 @@ def test_deepseek_persistent_topk(
434
offsets = torch.arange(next_n, device=logits.device, dtype=torch.int32)
435
lengths = (seq_lens.unsqueeze(1) - next_n + 1 + offsets).flatten()
436
369
- workspace = torch.empty(1024 * 1024, dtype=torch.uint8, device="cuda")
437
max_seq_len = int(seq_lens.max().item())
371
- torch.ops._C.persistent_topk(
372
- logits, lengths, indices, workspace, top_k, max_seq_len
373
- )
438
+ _run_topk_backend(backend, logits, lengths, indices, top_k, max_seq_len, next_n)
439
440
validate_topk_against_reference(
376
- logits, indices, row_starts, row_ends, top_k, f"persistent_topk ({test_id})"
441
+ logits, indices, row_starts, row_ends, top_k, f"{backend} ({test_id})"
442
)
443
444
@@ -383,9 +448,10 @@ def run_large_context_topk_test(
448
top_k: int,
449
data_type: str = "random",
450
seed: int = 42,
451
+ backend: str = "cooperative_topk",
452
) -> None:
453
"""
388
- Helper to run persistent_topk kernel test with given parameters.
454
+ Helper to run a top-k backend test with given parameters.
455
456
Args:
457
batch_size: Number of rows/sequences
@@ -393,6 +459,7 @@ def run_large_context_topk_test(
459
top_k: Number of top elements to select
460
data_type: Type of test data to generate
461
seed: Random seed for reproducibility
462
+ backend: Top-k backend to test
463
"""
464
torch.set_default_device("cuda:0")
465
set_random_seed(seed)
@@ -449,11 +516,8 @@ def run_large_context_topk_test(
516
# Create output tensor
517
indices = torch.empty((num_rows, top_k), dtype=torch.int32, device="cuda")
518
452
- workspace = torch.empty(1024 * 1024, dtype=torch.uint8, device="cuda")
519
max_seq_len = max(seq_lens)
454
- torch.ops._C.persistent_topk(
455
- logits, lengths, indices, workspace, top_k, max_seq_len
456
- )
520
+ _run_topk_backend(backend, logits, lengths, indices, top_k, max_seq_len)
521
522
torch.accelerator.synchronize()
523
@@ -605,8 +669,9 @@ def run_large_context_topk_test(
669
),
670
],
671
)
672
+@pytest.mark.parametrize("backend", WORKSPACE_TOPK_BACKENDS)
673
@torch.inference_mode()
609
-def test_persistent_topk_correctness(test_config: dict) -> None:
674
+def test_workspace_topk_correctness(test_config: dict, backend: str) -> None:
675
"""
676
Comprehensive correctness tests covering:
677
- Sequence length edge cases (trivial, boundary, varied)
@@ -620,6 +685,7 @@ def test_persistent_topk_correctness(test_config: dict) -> None:
685
seq_lens=test_config["seq_lens"],
686
top_k=test_config["top_k"],
687
data_type=test_config.get("data_type", "random"),
688
+ backend=backend,
689
)
690
691
@@ -668,8 +734,9 @@ def test_persistent_topk_correctness(test_config: dict) -> None:
734
),
735
],
736
)
737
+@pytest.mark.parametrize("backend", WORKSPACE_TOPK_BACKENDS)
738
@torch.inference_mode()
672
-def test_persistent_topk_algorithm_paths(test_config: dict) -> None:
739
+def test_workspace_topk_algorithm_paths(test_config: dict, backend: str) -> None:
740
"""
741
Test different algorithm execution paths (capped at 163840 for DeepSeek V3.2):
742
- Batch size scalability (1, 4, 32, 256)
@@ -680,12 +747,14 @@ def test_persistent_topk_algorithm_paths(test_config: dict) -> None:
747
batch_size=test_config["batch_size"],
748
seq_lens=[test_config["seq_len"]] * test_config["batch_size"],
749
top_k=test_config["top_k"],
750
+ backend=backend,
751
)
752
753
754
@pytest.mark.skipif(not current_platform.is_cuda(), reason="This test requires CUDA")
755
+@pytest.mark.parametrize("backend", WORKSPACE_TOPK_BACKENDS)
756
@torch.inference_mode()
688
-def test_persistent_topk_stress() -> None:
757
+def test_workspace_topk_stress(backend: str) -> None:
758
"""
759
Stress test with random configurations to catch edge cases.
760
Capped at 163840 (DeepSeek V3.2 max context) for realistic testing.
@@ -700,16 +769,73 @@ def test_persistent_topk_stress() -> None:
769
batch_size = torch.randint(1, 32, (1,)).item()
770
771
# Random sequence lengths capped at DeepSeek V3.2 max context
703
- seq_lens = torch.randint(100, 163840, (batch_size,)).tolist()
772
+ seq_lens_tensor = torch.randint(100, 163840, (batch_size,))
773
+ if backend == "cooperative_topk":
774
+ seq_lens = ((seq_lens_tensor + 3) & ~3).tolist()
775
+ else:
776
+ seq_lens = seq_lens_tensor.tolist()
777
778
run_large_context_topk_test(
779
batch_size=batch_size,
780
seq_lens=seq_lens,
781
top_k=top_k,
782
seed=seed,
783
+ backend=backend,
784
)
785
786
787
+@pytest.mark.skipif(not current_platform.is_cuda(), reason="This test requires CUDA")
788
+@pytest.mark.parametrize("backend", TOPK_BACKENDS)
789
+@pytest.mark.parametrize("top_k", [512, 1024, 2048])
790
+@torch.inference_mode()
791
+def test_deepseek_topk_backends_no_error_and_reference(
792
+ backend: str,
793
+ top_k: int,
794
+) -> None:
795
+ """Exercise every production top-k backend on the same inputs."""
796
+ run_large_context_topk_test(
797
+ batch_size=4,
798
+ seq_lens=[2049, 4097, 8191, 12000],
799
+ top_k=top_k,
800
+ data_type="random",
801
+ seed=123,
802
+ backend=backend,
803
+ )
804
+
805
+
806
+@pytest.mark.skipif(not _has_device_capability(90), reason="This test requires SM90+")
807
+@torch.inference_mode()
808
+def test_cooperative_topk_512_tie_workspace_is_per_row() -> None:
809
+ """Regression test for TopK=512 tie workspace row overlap."""
810
+ torch.set_default_device("cuda:0")
811
+
812
+ top_k = 512
813
+ num_rows = 2
814
+ stride = 65536
815
+ lengths = torch.tensor([40960, 65536], dtype=torch.int32, device="cuda")
816
+ logits = torch.full(
817
+ (num_rows, stride), float("-inf"), dtype=torch.float32, device="cuda"
818
+ )
819
+
820
+ # Row 0 must never select these low indices: many better row-0 ties exist.
821
+ logits[0, :2048] = -10.0
822
+ logits[0, 2048 : lengths[0]] = 1.0
823
+ # Row 1 has higher exact tie scores. With the old row * TopK tie_ws stride,
824
+ # these row-1 ties could overwrite row 0's TopK=512 refinement workspace.
825
+ logits[1, : lengths[1]] = 2.0
826
+
827
+ indices = torch.empty((num_rows, top_k), dtype=torch.int32, device="cuda")
828
+ workspace = torch.empty(RADIX_TOPK_WORKSPACE_SIZE, dtype=torch.uint8, device="cuda")
829
+ torch.ops._C.cooperative_topk(logits, lengths, indices, workspace, top_k, stride)
830
+ torch.accelerator.synchronize()
831
+
832
+ row0 = indices[0].cpu()
833
+ assert torch.all(row0 >= 2048), (
834
+ "cooperative_topk TopK=512 selected row-0 low-score indices, likely "
835
+ "from overlapping tie_ws rows"
836
+ )
837
+
838
+
839
@pytest.mark.skipif(not current_platform.is_cuda(), reason="This test requires CUDA")
840
@pytest.mark.parametrize(
841
"test_config",
@@ -774,10 +900,11 @@ def test_persistent_topk_stress() -> None:
900
],
901
)
902
@pytest.mark.parametrize("top_k", [512, 2048])
903
+@pytest.mark.parametrize("backend", WORKSPACE_TOPK_BACKENDS)
904
@torch.inference_mode()
778
-def test_persistent_topk(test_config: dict, top_k: int) -> None:
905
+def test_workspace_topk(test_config: dict, top_k: int, backend: str) -> None:
906
"""
780
- Tests specific to the persistent_topk kernel:
907
+ Tests specific to workspace top-k backends:
908
- Mixed medium/large rows in the same batch (dynamic per-row dispatch)
909
- Boundary around LARGE_THRESHOLD (32K)
910
- Trivial + medium + large rows in a single batch
@@ -787,15 +914,17 @@ def test_persistent_topk(test_config: dict, top_k: int) -> None:
914
seq_lens=test_config["seq_lens"],
915
top_k=top_k,
916
data_type=test_config.get("data_type", "random"),
917
+ backend=backend,
918
)
919
920
921
@pytest.mark.skipif(not current_platform.is_cuda(), reason="This test requires CUDA")
922
@pytest.mark.parametrize("top_k", [512, 2048])
923
+@pytest.mark.parametrize("backend", WORKSPACE_TOPK_BACKENDS)
924
@torch.inference_mode()
796
-def test_persistent_topk_padded_stride(top_k: int) -> None:
925
+def test_workspace_topk_padded_stride(top_k: int, backend: str) -> None:
926
"""
798
- Test persistent_topk with padded logits (large stride, small seq_len)
927
+ Test workspace top-k backends with padded logits (large stride, small seq_len)
928
to simulate the e2e CUDAGraph scenario where fp8_paged_mqa_logits
929
returns [B, max_model_len] with max_model_len=163840.
930
"""
@@ -818,11 +947,7 @@ def test_persistent_topk_padded_stride(top_k: int) -> None:
947
948
lengths = torch.tensor(actual_seq_lens, dtype=torch.int32, device="cuda")
949
indices = torch.empty((batch_size, top_k), dtype=torch.int32, device="cuda")
821
- workspace = torch.empty(1024 * 1024, dtype=torch.uint8, device="cuda")
822
-
823
- torch.ops._C.persistent_topk(
824
- logits, lengths, indices, workspace, top_k, max(actual_seq_lens)
825
- )
950
+ _run_topk_backend(backend, logits, lengths, indices, top_k, max(actual_seq_lens))
951
torch.accelerator.synchronize()
952
953
# Validate against torch.topk
@@ -840,6 +965,6 @@ def test_persistent_topk_padded_stride(top_k: int) -> None:
965
expected_vals = logits[i, expected].cpu().sort(descending=True)[0]
966
actual_vals = logits[i, actual].cpu().sort(descending=True)[0]
967
assert torch.allclose(expected_vals, actual_vals, rtol=1e-4, atol=1e-4), (
843
- f"Row {i}: persistent_topk with padded stride doesn't match. "
968
+ f"Row {i}: {backend} with padded stride doesn't match. "
969
f"seq_len={sl}, stride={padded_stride}"
970
)
vllm/model_executor/layers/sparse_attn_indexer.py
+26
-1
@@ -334,7 +334,32 @@ def sparse_attn_indexer(
334
num_rows = logits.shape[0]
335
topk_indices = topk_indices_buffer[:num_padded_tokens, :topk_tokens]
336
337
- if current_platform.is_cuda() and topk_tokens in (512, 1024, 2048):
337
+ use_cooperative_topk = (
338
+ current_platform.is_cuda()
339
+ and topk_tokens in (512, 1024, 2048)
340
+ and num_rows <= 32
341
+ and logits.stride(0) % 4 == 0 # TMA 16-byte alignment
342
+ and current_platform.has_device_capability(90)
343
+ )
344
+ use_persistent_topk = current_platform.is_cuda() and topk_tokens in (
345
+ 512,
346
+ 1024,
347
+ 2048,
348
+ )
349
+ if use_cooperative_topk:
350
+ workspace_manager = current_workspace_manager()
351
+ (topk_workspace,) = workspace_manager.get_simultaneous(
352
+ ((RADIX_TOPK_WORKSPACE_SIZE,), torch.uint8),
353
+ )
354
+ torch.ops._C.cooperative_topk(
355
+ logits,
356
+ seq_lens,
357
+ topk_indices,
358
+ topk_workspace,
359
+ topk_tokens,
360
+ attn_metadata_narrowed.max_seq_len,
361
+ )
362
+ elif use_persistent_topk:
363
workspace_manager = current_workspace_manager()
364
(topk_workspace,) = workspace_manager.get_simultaneous(
365
((RADIX_TOPK_WORKSPACE_SIZE,), torch.uint8),