A high-throughput and memory-efficient inference and serving engine for LLMs
Mirrored from https://github.com/tduncan-lum/Nvidia-Optimization-Stack---VLLM.git
Synced Nov 4, 08:45 UTC
[Bugfix] Fix persistent_topk inter-CTA init race on RadixRowState (#41444)
Signed-off-by: Yongye Zhu <zyy1102000@gmail.com>
Yongye Zhu committed
May 1, 2026 at 17:42 UTC
edd60ac93a3247c7ef1bf1e2a3e9c0e95bc83bf6
2 files changed
+29
-19
csrc/persistent_topk.cuh
+6
-19
@@ -887,27 +887,14 @@ __global__ void __launch_bounds__(kThreadsPerBlock, 2)
887
uint32_t* shared_ordered =
888
reinterpret_cast<uint32_t*>(smem_raw + kFixedSmemLarge);
889
890
- // RadixRowState for multi-CTA cooperative radix
890
+ // RadixRowState for multi-CTA cooperative radix.
891
+ // Zero-initialization is done host-side via cudaMemsetAsync in topk.cu
892
+ // before launch — that gives a stream-ordered happens-before edge for all
893
+ // CTAs, which the previous in-kernel init (CTA-0 only + intra-CTA
894
+ // __syncthreads) did not provide and which manifested as a race against
895
+ // CTA-1+'s first red_release on arrival_counter.
896
RadixRowState* state = ¶ms.row_states[group_id];
897
893
- // -- Initialize RadixRowState (only needed if large rows exist) --
894
- if (params.max_seq_len > RADIX_THRESHOLD) {
895
- if (cta_in_group == 0) {
896
- for (uint32_t buf = 0; buf < 3; buf++) {
897
- for (uint32_t i = tx; i < RADIX; i += kThreadsPerBlock) {
898
- state->histogram[buf][i] = 0;
899
- }
900
- }
901
- if (tx == 0) {
902
- state->remaining_k = 0;
903
- state->prefix = 0;
904
- state->arrival_counter = 0;
905
- state->output_counter = 0;
906
- }
907
- }
908
- __syncthreads();
909
- }
910
-
898
int barrier_phase = 0;
899
const uint32_t total_iters = (params.num_rows + num_groups - 1) / num_groups;
900
csrc/topk.cu
+23
@@ -153,6 +153,29 @@ void launch_persistent_topk(const torch::Tensor& logits,
153
TORCH_CHECK(workspace.size(0) >= static_cast<int64_t>(state_bytes),
154
"workspace too small, need ", state_bytes, " bytes");
155
156
+ // Zero the per-group RadixRowState region before launch — only when the
157
+ // radix path will actually run (max_seq_len > RADIX_THRESHOLD). The
158
+ // RadixRowState fields (arrival_counter, histograms) are only touched by
159
+ // radix_topk; the decode/medium paths inside the persistent kernel
160
+ // operate purely in shared memory and never read these globals, so a
161
+ // stale workspace is harmless for them.
162
+ //
163
+ // Why we need the memset (when needs_cooperative is true):
164
+ // 1. arrival_counter accumulates within a launch and is never reset,
165
+ // so a prior call leaves it at a large positive value. Without this
166
+ // reset, the very first wait_ge in the next call sees counter >>
167
+ // target and returns instantly, breaking the barrier.
168
+ // 2. The previous in-kernel init only ran in CTA-0 with intra-CTA
169
+ // __syncthreads(), so it had no happens-before edge to CTA-1+'s
170
+ // first red_release. cudaMemsetAsync is stream-ordered: the zero
171
+ // is globally visible before any CTA runs.
172
+ if (needs_cooperative) {
173
+ cudaError_t mz_err = cudaMemsetAsync(workspace.data_ptr<uint8_t>(), 0,
174
+ state_bytes, stream);
175
+ TORCH_CHECK(mz_err == cudaSuccess,
176
+ "row_states memset failed: ", cudaGetErrorString(mz_err));
177
+ }
178
+
179
P::PersistentTopKParams params;
180
params.input = logits.data_ptr<float>();
181
params.output = output.data_ptr<int32_t>();