Summary
_compute_slot_mapping_kernel in vllm/v1/worker/block_table.py masks its block-table load by
token validity and CP ownership, but not by whether the computed column index is inside the
row. Any KV cache group whose block table does not span the sequence in raw token positions
therefore performs an unbounded out-of-bounds read, scaling with sequence length.
On 8× MI325X (gfx942, ROCm 7.2.3) this eventually crosses into an unmapped page and takes down
every rank with Memory access fault by GPU node-N ... Reason: Unknown.
The missing guard
vllm/v1/worker/block_table.py @ main 94d96e24:
block_indices = ( # :463-466
virtual_block_indices * BLOCKS_PER_KV_BLOCK
+ local_block_offsets // block_size
)
block_numbers = tl.load( # :467-471
block_table_ptr + row_offset + block_indices,
mask=mask & is_local, # :469 ← no range term
other=0,
).to(tl.int64)
slot_offsets = local_block_offsets % block_size
slot_ids = block_numbers * block_size + slot_offsets
slot_ids = tl.where(is_local, slot_ids, PAD_ID) # :474 ← no range term
The model-side QSA slot helpers in #53896 do bound this (valid &= logical_blocks < block_table.shape[1]
and a clamp before indexing); the generic kernel is the one path we found that does not.
Two independent instances, both from in-flight model PRs
Neither spec class is on main yet; both are added by open PRs, and each declares a
one-block-per-request side cache that the kernel then indexes with raw token positions:
| PR |
spec |
max_num_blocks_per_req() |
effective row width |
index computed |
| #53896 (Qwen3.8-Flash-Next) |
CircularBufferSpec |
1 |
128 tokens (after the 128-token alignment in get_block_table_width) |
pos // 8 |
| #53906 (GLM-5.3-Flash) |
KpoolTailSpec |
1 |
128 tokens |
pos // index_kpool |
Both reach the kernel because vllm/v1/worker/gpu_model_runner.py:7387-7390 assigns
SlotMappingMode.NONE only to KVCacheSpecKind.MAMBA and TOKEN_TO_KV_SLOT to everything else.
(That classification rule deserves its own discussion; we have raised it on both PRs rather than here.)
Observed
Instrumenting the kernel to print when max(block_indices) >= block_table_stride:
[slotmap!!OOR] kvbs=8 bs=8 bpk=1 stride=16 rows=16
A 200-token request triggers it 456 times. At ~95k tokens the read is ~11,875 int32 elements
(≈47 KB) past a ~1 KB tensor, and that is where it started faulting for us. The threshold is not
a clean constant — it depends on what the caching allocator has mapped after the tensor, which is
why it is latency-to-detect and layout-dependent. Enabling speculative decoding (which adds KV
cache groups and shifts the allocation layout) moved the threshold: with MTP
num_speculative_tokens=3 the fault appeared at ~95k tokens, with =1 at ~100k, and without
MTP the same model ran 250k without faulting — the read was still landing in mapped memory.
Fix
+ in_range = block_indices < block_table_stride
block_numbers = tl.load(
block_table_ptr + row_offset + block_indices,
- mask=mask & is_local,
+ mask=mask & is_local & in_range,
other=0,
).to(tl.int64)
slot_offsets = local_block_offsets % block_size
slot_ids = block_numbers * block_size + slot_offsets
- slot_ids = tl.where(is_local, slot_ids, PAD_ID)
+ slot_ids = tl.where(is_local & in_range, slot_ids, PAD_ID)
For a group whose block table does span the sequence, in_range is always true and behaviour is
unchanged. For a narrow group the entry becomes PAD_SLOT_ID instead of a wild read — and in both
instances above the generic output is discarded anyway, because each builder recomputes its own
circular mapping (see the KpoolTailMetadataBuilder docstring in #53906, which states the generic
per-group kernel output "cannot express a 1-block-per-request circular buffer").
Result
With the two-line guard, Qwen3.8-Flash-Next with MTP num_speculative_tokens=3 at
--max-model-len 262144 — the configuration that faulted at ~95k above — runs needle-in-haystack
100k / 150k / 250k all 3/3 with zero GPU faults. GLM-5.3-Flash without MTP (which had not faulted
before the guard; its read stayed inside mapped memory) is unchanged at 100k / 250k 3/3.
Independent of ROCm?
The missing mask is platform-independent. Whether it manifests on CUDA depends on the allocator
layout and how far past the row the read goes; we only have gfx942 data.
Reproduction
- Serve either model from the PRs above on 8 GPUs with a long context.
- Add a debug print in
_compute_slot_mapping_kernel when
max(block_indices) >= block_table_stride (one-shot; the condition is true on essentially
every step, so an unconditional device print will flood).
- Any request longer than the side cache's aligned row width (128 tokens for both specs) trips it.
What we verified / did not verify
- Verified: the missing mask on
main @ 94d96e24 (no in_range token anywhere in the file);
the OOR instrumentation counts; that the two-line guard eliminates the faults on our hardware.
- Not verified: that the out-of-bounds read is the only cause of every fault we saw. It was
for one configuration (GLM without MTP, and Qwen with MTP); a second GLM configuration
(with MTP) still faults after this fix, for an unrelated reason in aiter's MLA decode dispatch,
filed separately.
- Not verified: any CUDA-platform manifestation.
Summary
_compute_slot_mapping_kernelinvllm/v1/worker/block_table.pymasks its block-table load bytoken validity and CP ownership, but not by whether the computed column index is inside the
row. Any KV cache group whose block table does not span the sequence in raw token positions
therefore performs an unbounded out-of-bounds read, scaling with sequence length.
On 8× MI325X (gfx942, ROCm 7.2.3) this eventually crosses into an unmapped page and takes down
every rank with
Memory access fault by GPU node-N ... Reason: Unknown.The missing guard
vllm/v1/worker/block_table.py@main94d96e24:The model-side QSA slot helpers in #53896 do bound this (
valid &= logical_blocks < block_table.shape[1]and a clamp before indexing); the generic kernel is the one path we found that does not.
Two independent instances, both from in-flight model PRs
Neither spec class is on
mainyet; both are added by open PRs, and each declares aone-block-per-request side cache that the kernel then indexes with raw token positions:
max_num_blocks_per_req()CircularBufferSpec1get_block_table_width)pos // 8KpoolTailSpec1pos // index_kpoolBoth reach the kernel because
vllm/v1/worker/gpu_model_runner.py:7387-7390assignsSlotMappingMode.NONEonly toKVCacheSpecKind.MAMBAandTOKEN_TO_KV_SLOTto everything else.(That classification rule deserves its own discussion; we have raised it on both PRs rather than here.)
Observed
Instrumenting the kernel to print when
max(block_indices) >= block_table_stride:A 200-token request triggers it 456 times. At ~95k tokens the read is ~11,875 int32 elements
(≈47 KB) past a ~1 KB tensor, and that is where it started faulting for us. The threshold is not
a clean constant — it depends on what the caching allocator has mapped after the tensor, which is
why it is latency-to-detect and layout-dependent. Enabling speculative decoding (which adds KV
cache groups and shifts the allocation layout) moved the threshold: with MTP
num_speculative_tokens=3the fault appeared at ~95k tokens, with=1at ~100k, and withoutMTP the same model ran 250k without faulting — the read was still landing in mapped memory.
Fix
For a group whose block table does span the sequence,
in_rangeis always true and behaviour isunchanged. For a narrow group the entry becomes
PAD_SLOT_IDinstead of a wild read — and in bothinstances above the generic output is discarded anyway, because each builder recomputes its own
circular mapping (see the
KpoolTailMetadataBuilderdocstring in #53906, which states the genericper-group kernel output "cannot express a 1-block-per-request circular buffer").
Result
With the two-line guard, Qwen3.8-Flash-Next with MTP
num_speculative_tokens=3at--max-model-len 262144— the configuration that faulted at ~95k above — runs needle-in-haystack100k / 150k / 250k all 3/3 with zero GPU faults. GLM-5.3-Flash without MTP (which had not faulted
before the guard; its read stayed inside mapped memory) is unchanged at 100k / 250k 3/3.
Independent of ROCm?
The missing mask is platform-independent. Whether it manifests on CUDA depends on the allocator
layout and how far past the row the read goes; we only have gfx942 data.
Reproduction
_compute_slot_mapping_kernelwhenmax(block_indices) >= block_table_stride(one-shot; the condition is true on essentiallyevery step, so an unconditional device print will flood).
What we verified / did not verify
main@94d96e24(noin_rangetoken anywhere in the file);the OOR instrumentation counts; that the two-line guard eliminates the faults on our hardware.
for one configuration (GLM without MTP, and Qwen with MTP); a second GLM configuration
(with MTP) still faults after this fix, for an unrelated reason in aiter's MLA decode dispatch,
filed separately.