[Bugfix][Model Runner V2][Spec Decode] Fix int32 offset overflow in sampler kernels (#46560)
Signed-off-by: xiaojun.wei <jessiewei747@gmail.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: Robert Shaw <114415538+robertgshaw2-redhat@users.noreply.github.com>
JessieWei committed
Jun 25, 2026 at 02:00 UTC
3c43237233a8f753a0ed24d95c0978b09f009d29
10 files changed
+90
-16
tests/v1/sample/test_logprobs.py
+34
@@ -1263,3 +1263,37 @@ def test_prompt_logprobs_with_chunking_and_preemption():
1263
assert preemptions > 0, "Test did not trigger any preemptions"
1264
1265
print(f"Test passed with {preemptions} preemptions")
1266
+
1267
+
1268
+@large_gpu_mark(min_gb=24)
1269
+def test_token_logprobs_large_batch_int64_row_offset():
1270
+ """Regression: logprob kernel row offset (row * vocab_size) must use int64.
1271
+
1272
+ The rejection-sampler logprobs path runs the logprob kernels over the
1273
+ spec-expanded logits batch, so batch_size * vocab_size can exceed 2**31
1274
+ (e.g. DFlash drafts K tokens per request). With int32 offset arithmetic the
1275
+ per-row pointer wraps to a negative address and the kernel hits a CUDA
1276
+ illegal memory access. Run over a batch where batch_size * vocab_size > 2**31
1277
+ and check the highest-offset row matches a reference log-softmax.
1278
+ """
1279
+ if not current_platform.is_cuda():
1280
+ pytest.skip("int32 row-offset overflow is a CUDA kernel issue")
1281
+ from vllm.v1.worker.gpu.sample.logprob import compute_token_logprobs
1282
+
1283
+ device = torch.device("cuda")
1284
+ vocab_size = 131072
1285
+ batch_size = 2**31 // vocab_size + 64 # batch_size * vocab_size > 2**31
1286
+ # logits (the large input) plus small logprob/rank outputs; ~1 GB headroom.
1287
+ required_bytes = batch_size * vocab_size * 4 + (1 << 30)
1288
+ if torch.cuda.mem_get_info()[0] < required_bytes:
1289
+ pytest.skip(f"needs ~{required_bytes / 1e9:.0f} GB of free GPU memory")
1290
+
1291
+ logits = torch.randn(batch_size, vocab_size, device=device, dtype=torch.float32)
1292
+ token_ids = torch.full((batch_size, 1), 7, device=device, dtype=torch.int64)
1293
+ logprobs = compute_token_logprobs(logits, token_ids)
1294
+ torch.accelerator.synchronize() # surface any async illegal memory access
1295
+ last = batch_size - 1
1296
+ ref = torch.log_softmax(logits[last].float(), dim=-1)[7]
1297
+ assert torch.allclose(logprobs[last, 0], ref, atol=1e-2), (
1298
+ f"logprob {logprobs[last, 0].item()} != ref {ref.item()}"
1299
+ )
tests/v1/sample/test_topk_topp_sampler.py
+40
@@ -4,6 +4,7 @@ import pytest
4
import torch
5
from torch import Generator
6
7
+from tests.utils import large_gpu_mark
8
from vllm.platforms import current_platform
9
from vllm.triton_utils import HAS_TRITON
10
from vllm.utils.torch_utils import set_random_seed
@@ -404,6 +405,45 @@ class TestTritonTopkTopp:
405
406
self._compare_results(logits, k, p)
407
408
+ @large_gpu_mark(min_gb=24)
409
+ def test_large_batch_int64_row_offset(self):
410
+ """Regression: per-row offset (row * vocab_size) must not overflow int32.
411
+
412
+ Speculative decoding expands the logits batch (e.g. DFlash drafts K
413
+ tokens per request), so batch_size * vocab_size can exceed 2**31. With
414
+ int32 offset arithmetic the per-row pointer wraps to a negative address
415
+ and the kernel hits a CUDA illegal memory access. Use a batch where
416
+ batch_size * vocab_size > 2**31 and give the highest-offset row the same
417
+ logits as row 0: an overflow there would read a different row and change
418
+ the kept set.
419
+ """
420
+ from vllm.v1.sample.ops.topk_topp_triton import apply_top_k_top_p_triton
421
+
422
+ if not current_platform.is_cuda():
423
+ pytest.skip("int32 row-offset overflow is a CUDA kernel issue")
424
+ vocab_size = 131072
425
+ batch_size = 2**31 // vocab_size + 64 # batch_size * vocab_size > 2**31
426
+ # logits is modified in place; the only extra device memory is the
427
+ # per-SM scratch buffer (~num_sm * vocab), so allow ~1 GB of headroom.
428
+ required_bytes = batch_size * vocab_size * 4 + (1 << 30)
429
+ if torch.cuda.mem_get_info()[0] < required_bytes:
430
+ pytest.skip(f"needs ~{required_bytes / 1e9:.0f} GB of free GPU memory")
431
+
432
+ logits = torch.randn(
433
+ batch_size, vocab_size, generator=self.generator, dtype=torch.float32
434
+ )
435
+ logits[batch_size - 1] = logits[0]
436
+ k = torch.full((batch_size,), 5, dtype=torch.int32)
437
+ result = apply_top_k_top_p_triton(logits, k, None)
438
+ torch.accelerator.synchronize() # surface any async illegal memory access
439
+ kept_first = (result[0] > float("-inf")).nonzero(as_tuple=True)[0]
440
+ kept_last = (result[batch_size - 1] > float("-inf")).nonzero(as_tuple=True)[0]
441
+ assert kept_first.numel() == 5, f"row 0 kept {kept_first.numel()}, expected 5"
442
+ assert torch.equal(kept_first, kept_last), (
443
+ "highest-offset row produced a different top-k mask than the "
444
+ "identical row 0 (int32 row-offset overflow)"
445
+ )
446
+
447
@pytest.mark.parametrize(
448
"mode",
449
["topk_only", "topp_only", "topk_and_topp"],
vllm/v1/sample/ops/topk_topp_triton.py
+1
-1
@@ -111,7 +111,7 @@ def _topk_topp_kernel(
111
pid = tl.program_id(0)
112
num_programs = tl.num_programs(0)
113
for row_id in tl.range(pid, BATCH_SIZE, num_programs):
114
- LOGITS_ROW = LOGITS + row_id * LOGITS_STRIDE_0
114
+ LOGITS_ROW = LOGITS + row_id.to(tl.int64) * LOGITS_STRIDE_0
115
BUFFER_ROW = BUFFER + pid * VOCAB_SIZE
116
117
final_pivot = -float("inf")
vllm/v1/worker/gpu/sample/bad_words.py
+1
-1
@@ -114,7 +114,7 @@ def _bad_words_kernel(
114
input_ids_ptr,
115
expanded_local_pos_ptr,
116
):
117
- token_idx = tl.program_id(0)
117
+ token_idx = tl.program_id(0).to(tl.int64)
118
bw_idx = tl.program_id(1)
119
120
req_state_idx = tl.load(expanded_idx_mapping_ptr + token_idx)
vllm/v1/worker/gpu/sample/gumbel.py
+3
-3
@@ -23,7 +23,7 @@ def _temperature_kernel(
23
vocab_size,
24
BLOCK_SIZE: tl.constexpr,
25
):
26
- token_idx = tl.program_id(0)
26
+ token_idx = tl.program_id(0).to(tl.int64)
27
req_state_idx = tl.load(expanded_idx_mapping_ptr + token_idx)
28
temperature = tl.load(temperature_ptr + req_state_idx).to(tl.float32)
29
if temperature == 0.0 or temperature == 1.0:
@@ -91,7 +91,7 @@ def gumbel_block_argmax(
91
USE_FP64: tl.constexpr,
92
PER_TOKEN_COL: tl.constexpr = False,
93
):
94
- req_state_idx = tl.load(expanded_idx_mapping_ptr + token_idx)
94
+ req_state_idx = tl.load(expanded_idx_mapping_ptr + token_idx).to(tl.int64)
95
temp = tl.load(temp_ptr + req_state_idx).to(tl.float32)
96
if temp != 0.0 and APPLY_TEMPERATURE:
97
# Apply temperature.
@@ -169,7 +169,7 @@ def _gumbel_sample_kernel(
169
USE_FP64: tl.constexpr,
170
PER_TOKEN_COL: tl.constexpr,
171
):
172
- token_idx = tl.program_id(0)
172
+ token_idx = tl.program_id(0).to(tl.int64)
173
block_idx = tl.program_id(1)
174
block = block_idx * BLOCK_SIZE + tl.arange(0, BLOCK_SIZE)
175
mask = block < vocab_size
vllm/v1/worker/gpu/sample/logit_bias.py
+1
-1
@@ -169,7 +169,7 @@ def _bias_kernel(
169
BLOCK_SIZE: tl.constexpr,
170
LOGITS_BLOCK_SIZE: tl.constexpr,
171
):
172
- token_idx = tl.program_id(0)
172
+ token_idx = tl.program_id(0).to(tl.int64)
173
req_state_idx = tl.load(expanded_idx_mapping_ptr + token_idx)
174
175
block = tl.arange(0, BLOCK_SIZE)
vllm/v1/worker/gpu/sample/logprob.py
+2
-2
@@ -21,7 +21,7 @@ def _topk_log_softmax_kernel(
21
BLOCK_SIZE: tl.constexpr,
22
PADDED_TOPK: tl.constexpr,
23
):
24
- req_idx = tl.program_id(0)
24
+ req_idx = tl.program_id(0).to(tl.int64)
25
row_ptr = logits_ptr + req_idx * logits_stride
26
27
max_val = float("-inf")
@@ -61,7 +61,7 @@ def _ranks_kernel(
61
vocab_size,
62
BLOCK_SIZE: tl.constexpr,
63
):
64
- req_idx = tl.program_id(0)
64
+ req_idx = tl.program_id(0).to(tl.int64)
65
row_ptr = logits_ptr + req_idx * logits_stride
66
67
token_id = tl.load(token_ids_ptr + req_idx)
vllm/v1/worker/gpu/sample/min_p.py
+1
-1
@@ -14,7 +14,7 @@ def _min_p_kernel(
14
vocab_size,
15
BLOCK_SIZE: tl.constexpr,
16
):
17
- token_idx = tl.program_id(0)
17
+ token_idx = tl.program_id(0).to(tl.int64)
18
req_state_idx = tl.load(expanded_idx_mapping_ptr + token_idx)
19
min_p = tl.load(min_p_ptr + req_state_idx).to(tl.float32)
20
if min_p == 0.0:
vllm/v1/worker/gpu/sample/penalties.py
+1
-1
@@ -120,7 +120,7 @@ def _penalties_kernel(
120
vocab_size,
121
BLOCK_SIZE: tl.constexpr,
122
):
123
- token_idx = tl.program_id(0)
123
+ token_idx = tl.program_id(0).to(tl.int64)
124
req_state_idx = tl.load(expanded_idx_mapping_ptr + token_idx)
125
rep_penalty = tl.load(repetition_penalty_ptr + req_state_idx)
126
freq_penalty = tl.load(frequency_penalty_ptr + req_state_idx)
vllm/v1/worker/gpu/spec_decode/rejection_sampler_utils.py
+6
-6
@@ -79,14 +79,14 @@ def _compute_block_stats_kernel(
79
BLOCK_SIZE: tl.constexpr,
80
HAS_DRAFT_LOGITS: tl.constexpr,
81
):
82
- logit_idx = tl.program_id(0)
82
+ logit_idx = tl.program_id(0).to(tl.int64)
83
draft_step_idx = tl.load(expanded_local_pos_ptr + logit_idx)
84
85
if draft_step_idx >= num_speculative_steps:
86
# Bonus token. Max/argmax and summed exponentials are not needed.
87
return
88
89
- req_state_idx = tl.load(expanded_idx_mapping_ptr + logit_idx)
89
+ req_state_idx = tl.load(expanded_idx_mapping_ptr + logit_idx).to(tl.int64)
90
temp = tl.load(temp_ptr + req_state_idx).to(tl.float32)
91
92
block_idx = tl.program_id(1)
@@ -206,8 +206,8 @@ def _rejection_kernel(
206
SYNTHETIC_MODE: tl.constexpr,
207
):
208
req_idx = tl.program_id(0)
209
- req_state_idx = tl.load(idx_mapping_ptr + req_idx)
210
- start_idx = tl.load(cu_num_logits_ptr + req_idx)
209
+ req_state_idx = tl.load(idx_mapping_ptr + req_idx).to(tl.int64)
210
+ start_idx = tl.load(cu_num_logits_ptr + req_idx).to(tl.int64)
211
end_idx = tl.load(cu_num_logits_ptr + req_idx + 1)
212
num_tokens = end_idx - start_idx
213
seed = tl.load(seed_ptr + req_state_idx)
@@ -349,10 +349,10 @@ def _resample_kernel(
349
):
350
req_idx = tl.program_id(0)
351
resample_idx = tl.load(rejected_step_ptr + req_idx)
352
- start_idx = tl.load(cu_num_logits_ptr + req_idx)
352
+ start_idx = tl.load(cu_num_logits_ptr + req_idx).to(tl.int64)
353
end_idx = tl.load(cu_num_logits_ptr + req_idx + 1)
354
resample_token_idx = start_idx + resample_idx
355
- req_state_idx = tl.load(expanded_idx_mapping_ptr + resample_token_idx)
355
+ req_state_idx = tl.load(expanded_idx_mapping_ptr + resample_token_idx).to(tl.int64)
356
357
temp = tl.load(temp_ptr + req_state_idx).to(tl.float32)
358
is_bonus = resample_token_idx == end_idx - 1