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 6, 23:00 UTC
[BUGFIX][Mamba][Qwen3.5] Zero freed SSM cache blocks on GPU (#35219)
Signed-off-by: Vadim Gimpelson <vadim.gimpelson@gmail.com>
Vadim Gimpelson committed
Mar 10, 2026 at 14:32 UTC
4ff8c3c8f9ece010a1d0e376f5cc1b468b95f366
10 files changed
+287
-8
vllm/utils/math_utils.py
+5
@@ -30,3 +30,8 @@ def round_up(x: int, y: int) -> int:
30
def round_down(x: int, y: int) -> int:
31
"""Round down x to the nearest multiple of y."""
32
return (x // y) * y
33
+
34
+
35
+def largest_power_of_2_divisor(n: int) -> int:
36
+ """Return the largest power-of-2 that divides *n* (isolate lowest set bit)."""
37
+ return n & (-n)
vllm/v1/attention/backend.py
+20
@@ -86,6 +86,26 @@ class AttentionBackend(ABC):
86
) -> tuple[int, ...]:
87
raise NotImplementedError
88
89
+ @classmethod
90
+ def get_kv_cache_block_dim(
91
+ cls,
92
+ block_size: int,
93
+ num_kv_heads: int,
94
+ head_size: int,
95
+ cache_dtype_str: str = "auto",
96
+ ) -> int:
97
+ """Discover which tensor dim is the block index, since different
98
+ backends lay out dims differently."""
99
+ _S = 1234567
100
+ shape = cls.get_kv_cache_shape(
101
+ _S,
102
+ block_size,
103
+ num_kv_heads,
104
+ head_size,
105
+ cache_dtype_str=cache_dtype_str,
106
+ )
107
+ return shape.index(_S)
108
+
109
@staticmethod
110
def get_kv_cache_stride_order(
111
include_num_layers_dimension: bool = False,
vllm/v1/core/kv_cache_manager.py
+7
@@ -501,6 +501,13 @@ class KVCacheManager:
501
# Only create new KVCacheBlocks for non-empty blocks
502
return KVCacheBlocks(blocks) if any(blocks) else self.empty_kv_cache_blocks
503
504
+ def take_new_block_ids(self) -> list[int]:
505
+ """Drain and return new attention block IDs for zeroing."""
506
+ ids: list[int] = []
507
+ for mgr in self.coordinator.single_type_managers:
508
+ ids.extend(mgr.take_new_block_ids())
509
+ return ids
510
+
511
def new_step_starts(self) -> None:
512
"""Called when a new step is started."""
513
self.coordinator.new_step_starts()
vllm/v1/core/sched/output.py
+5
@@ -233,6 +233,11 @@ class SchedulerOutput:
233
# EC Cache Connector metadata
234
ec_connector_metadata: ECConnectorMetadata | None = None
235
236
+ # Block IDs freshly allocated from the pool during this scheduling step.
237
+ # The worker zeros the corresponding GPU memory before the blocks are used,
238
+ # preventing stale NaN/data from corrupting attention or SSM computation.
239
+ new_block_ids_to_zero: list[int] | None = None
240
+
241
@classmethod
242
def make_empty(cls) -> "SchedulerOutput":
243
return cls(
vllm/v1/core/sched/scheduler.py
+10
-8
@@ -48,7 +48,7 @@ from vllm.v1.core.sched.output import (
48
from vllm.v1.core.sched.request_queue import SchedulingPolicy, create_request_queue
49
from vllm.v1.core.sched.utils import check_stop, remove_all
50
from vllm.v1.engine import EngineCoreEventType, EngineCoreOutput, EngineCoreOutputs
51
-from vllm.v1.kv_cache_interface import KVCacheConfig, MambaSpec
51
+from vllm.v1.kv_cache_interface import KVCacheConfig
52
from vllm.v1.metrics.perf import ModelMetrics, PerfStats
53
from vllm.v1.metrics.stats import PrefixCacheStats, SchedulerStats
54
from vllm.v1.outputs import DraftTokenIds, KVConnectorOutput, ModelRunnerOutput
@@ -233,13 +233,8 @@ class Scheduler(SchedulerInterface):
233
self.use_pp = self.parallel_config.pipeline_parallel_size > 1
234
self.use_v2_model_runner = envs.VLLM_USE_V2_MODEL_RUNNER
235
236
- def has_mamba_layers(kv_cache_config: KVCacheConfig) -> bool:
237
- return any(
238
- isinstance(group_spec.kv_cache_spec, MambaSpec)
239
- for group_spec in kv_cache_config.kv_cache_groups
240
- )
241
-
242
- self.has_mamba_layers = has_mamba_layers(kv_cache_config)
236
+ self.has_mamba_layers = kv_cache_config.has_mamba_layers
237
+ self.needs_kv_cache_zeroing = kv_cache_config.needs_kv_cache_zeroing
238
self.need_mamba_block_aligned_split = (
239
self.has_mamba_layers and self.cache_config.mamba_cache_mode == "align"
240
)
@@ -890,6 +885,12 @@ class Scheduler(SchedulerInterface):
885
self.prev_step_scheduled_req_ids.clear()
886
self.prev_step_scheduled_req_ids.update(num_scheduled_tokens.keys())
887
888
+ new_block_ids_to_zero = (
889
+ (self.kv_cache_manager.take_new_block_ids() or None)
890
+ if self.needs_kv_cache_zeroing
891
+ else None
892
+ )
893
+
894
scheduler_output = SchedulerOutput(
895
scheduled_new_reqs=new_reqs_data,
896
scheduled_cached_reqs=cached_reqs_data,
@@ -905,6 +906,7 @@ class Scheduler(SchedulerInterface):
906
# the previous and the current steps.
907
finished_req_ids=self.finished_req_ids,
908
free_encoder_mm_hashes=self.encoder_cache_manager.get_freed_mm_hashes(),
909
+ new_block_ids_to_zero=new_block_ids_to_zero,
910
)
911
912
# NOTE(Kuntai): this function is designed for multiple purposes:
vllm/v1/core/single_type_kv_cache_manager.py
+11
@@ -55,6 +55,7 @@ class SingleTypeKVCacheManager(ABC):
55
self.kv_cache_spec = kv_cache_spec
56
self.block_pool = block_pool
57
self.enable_caching = enable_caching
58
+ self.new_block_ids: list[int] = []
59
60
# Mapping from request ID to blocks to track the blocks allocated
61
# for each request, so that we can free the blocks when the request
@@ -208,6 +209,8 @@ class SingleTypeKVCacheManager(ABC):
209
cdiv(num_total_computed_tokens, self.block_size) - len(req_blocks)
210
)
211
req_blocks.extend(allocated_blocks)
212
+ if type(self.kv_cache_spec) is FullAttentionSpec:
213
+ self.new_block_ids.extend(b.block_id for b in allocated_blocks)
214
215
def allocate_new_blocks(
216
self, request_id: str, num_tokens: int, num_tokens_main_model: int
@@ -234,8 +237,16 @@ class SingleTypeKVCacheManager(ABC):
237
else:
238
new_blocks = self.block_pool.get_new_blocks(num_new_blocks)
239
req_blocks.extend(new_blocks)
240
+ if type(self.kv_cache_spec) is FullAttentionSpec:
241
+ self.new_block_ids.extend(b.block_id for b in new_blocks)
242
return new_blocks
243
244
+ def take_new_block_ids(self) -> list[int]:
245
+ """Drain and return block IDs allocated since the last call."""
246
+ ids = self.new_block_ids
247
+ self.new_block_ids = []
248
+ return ids
249
+
250
def cache_blocks(self, request: Request, num_tokens: int) -> None:
251
"""
252
Cache the blocks for the request.
vllm/v1/kv_cache_interface.py
+8
@@ -489,3 +489,11 @@ class KVCacheConfig:
489
For models with multiple types of attention, there will be multiple groups,
490
see `_get_kv_cache_config_uniform_page_size` for more details.
491
"""
492
+
493
+ @property
494
+ def has_mamba_layers(self) -> bool:
495
+ return any(isinstance(g.kv_cache_spec, MambaSpec) for g in self.kv_cache_groups)
496
+
497
+ @property
498
+ def needs_kv_cache_zeroing(self) -> bool:
499
+ return self.has_mamba_layers
vllm/v1/worker/gpu_model_runner.py
+27
@@ -197,6 +197,7 @@ from vllm.v1.worker.workspace import lock_workspace
197
198
from .utils import (
199
AttentionGroup,
200
+ KVBlockZeroer,
201
add_kv_sharing_layers_to_kv_cache_groups,
202
bind_kv_cache,
203
prepare_kernel_block_sizes,
@@ -982,6 +983,26 @@ class GPUModelRunner(
983
decode_threshold=self.reorder_batch_threshold,
984
)
985
986
+ def _init_kv_zero_meta(self) -> None:
987
+ """One-time precomputation for _zero_block_ids.
988
+
989
+ Delegates to KVBlockZeroer.init_meta with the runner's state.
990
+ Called from gpu_worker.py outside the CuMem pool context.
991
+ """
992
+ self._kv_block_zeroer = KVBlockZeroer(self.device, self.pin_memory)
993
+ self._kv_block_zeroer.init_meta(
994
+ attn_groups_iter=self._kv_cache_spec_attn_group_iterator(),
995
+ kernel_block_sizes=self._kernel_block_sizes,
996
+ cache_dtype=self.cache_config.cache_dtype,
997
+ runner_only_attn_layers=self.runner_only_attn_layers,
998
+ static_forward_context=(self.compilation_config.static_forward_context),
999
+ )
1000
+
1001
+ def _zero_block_ids(self, block_ids: list[int]) -> None:
1002
+ """Zero the KV cache memory for the given block IDs."""
1003
+ if hasattr(self, "_kv_block_zeroer"):
1004
+ self._kv_block_zeroer.zero_block_ids(block_ids)
1005
+
1006
# Note: used for model runner override.
1007
def _init_device_properties(self) -> None:
1008
"""Initialize attributes from torch.cuda.get_device_properties"""
@@ -1018,6 +1039,11 @@ class GPUModelRunner(
1039
for req_id in scheduler_output.finished_req_ids:
1040
self.input_batch.remove_request(req_id)
1041
1042
+ # Zero GPU memory for freshly allocated cache blocks to prevent
1043
+ # stale NaN/data from corrupting attention or SSM computation.
1044
+ if scheduler_output.new_block_ids_to_zero:
1045
+ self._zero_block_ids(scheduler_output.new_block_ids_to_zero)
1046
+
1047
# Free the cached encoder outputs.
1048
for mm_hash in scheduler_output.free_encoder_mm_hashes:
1049
self.encoder_cache.pop(mm_hash, None)
@@ -6476,6 +6502,7 @@ class GPUModelRunner(
6502
kernel_block_sizes = prepare_kernel_block_sizes(
6503
kv_cache_config, self.attn_groups
6504
)
6505
+ self._kernel_block_sizes = kernel_block_sizes
6506
6507
# create metadata builders
6508
self.initialize_metadata_builders(kv_cache_config, kernel_block_sizes)
vllm/v1/worker/gpu_worker.py
+8
@@ -556,6 +556,14 @@ class Worker(WorkerBase):
556
else:
557
self.model_runner.initialize_kv_cache(kv_cache_config)
558
559
+ # Build KV-zero metadata outside the CuMem pool so the bookkeeping
560
+ # GPU tensors (seg_addrs, block-id buffers) use the standard PyTorch
561
+ # allocator and are not discarded during sleep/wake cycles.
562
+ if kv_cache_config.needs_kv_cache_zeroing and hasattr(
563
+ self.model_runner, "_init_kv_zero_meta"
564
+ ):
565
+ self.model_runner._init_kv_zero_meta()
566
+
567
@instrument(span_name="Warmup (GPU)")
568
def compile_or_warm_up_model(self) -> float:
569
warmup_sizes: list[int] = []
vllm/v1/worker/utils.py
+186
@@ -2,7 +2,10 @@
2
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
import math
4
from collections import defaultdict
5
+from collections.abc import Iterable
6
from dataclasses import dataclass, field
7
+from itertools import product as iprod
8
+from typing import Any
9
10
import torch
11
@@ -12,6 +15,8 @@ from vllm.model_executor.layers.attention import Attention
15
from vllm.model_executor.models.interfaces import MultiModalEmbeddings
16
from vllm.model_executor.models.utils import extract_layer_index
17
from vllm.platforms import current_platform
18
+from vllm.triton_utils import tl, triton
19
+from vllm.utils.math_utils import largest_power_of_2_divisor
20
from vllm.utils.mem_utils import MemorySnapshot, format_gib
21
from vllm.v1.attention.backend import (
22
AttentionBackend,
@@ -21,6 +26,7 @@ from vllm.v1.attention.backend import (
26
from vllm.v1.kv_cache_interface import (
27
AttentionSpec,
28
EncoderOnlyAttentionSpec,
29
+ FullAttentionSpec,
30
KVCacheConfig,
31
KVCacheGroupSpec,
32
KVCacheSpec,
@@ -31,6 +37,186 @@ from vllm.v1.kv_cache_interface import (
37
logger = init_logger(__name__)
38
39
40
+@triton.jit
41
+def _zero_kv_blocks_kernel(
42
+ seg_addrs_ptr,
43
+ block_ids_ptr,
44
+ n_blocks,
45
+ N_SEGS: tl.constexpr,
46
+ PAGE_SIZE_EL: tl.constexpr,
47
+ BLOCK_SIZE: tl.constexpr,
48
+):
49
+ """Zero KV cache blocks across all segments in a single launch.
50
+
51
+ Each segment is a contiguous region of one block's data. For backends
52
+ where blocks are outermost (block_dim=0) there is one segment per
53
+ buffer. For backends where K/V is outermost (block_dim=1) there are
54
+ two segments per buffer (one for K, one for V).
55
+
56
+ seg_addrs_ptr holds absolute byte addresses (int64) for each segment,
57
+ allowing segments to live in different CUDA allocations.
58
+
59
+ Programs are mapped as (block_index, seg_index, chunk_index).
60
+ """
61
+ pid = tl.program_id(0)
62
+ chunks = PAGE_SIZE_EL // BLOCK_SIZE
63
+ work_per_block = N_SEGS * chunks
64
+ block_index = pid // work_per_block
65
+ if block_index >= n_blocks:
66
+ return
67
+ remainder = pid % work_per_block
68
+ seg_index = remainder // chunks
69
+ chunk_index = remainder % chunks
70
+ block_id = tl.load(block_ids_ptr + block_index)
71
+ seg_addr = tl.load(seg_addrs_ptr + seg_index)
72
+ ptr = tl.cast(seg_addr, tl.pointer_type(tl.int32))
73
+ offset = (
74
+ block_id.to(tl.int64) * PAGE_SIZE_EL + chunk_index.to(tl.int64) * BLOCK_SIZE
75
+ )
76
+ cols = tl.arange(0, BLOCK_SIZE).to(tl.int64)
77
+ tl.store(ptr + offset + cols, tl.zeros([BLOCK_SIZE], dtype=tl.int32))
78
+
79
+
80
+class KVBlockZeroer:
81
+ """Manages efficient zeroing of KV cache blocks via a Triton kernel.
82
+
83
+ Call :meth:`init_meta` once after KV caches are allocated to precompute
84
+ segment addresses, then call :meth:`zero_block_ids` each step to zero
85
+ newly-allocated blocks.
86
+ """
87
+
88
+ def __init__(self, device: torch.device, pin_memory: bool):
89
+ self.device = device
90
+ self.pin_memory = pin_memory
91
+ self._meta: tuple[torch.Tensor, int, int, int] | None = None
92
+ self._id_cap: int = 0
93
+ self._ids_pinned: torch.Tensor | None = None
94
+ self._ids_gpu: torch.Tensor | None = None
95
+
96
+ def init_meta(
97
+ self,
98
+ attn_groups_iter: Iterable["AttentionGroup"],
99
+ kernel_block_sizes: list[int],
100
+ cache_dtype: str,
101
+ runner_only_attn_layers: set[str],
102
+ static_forward_context: dict[str, Any],
103
+ ) -> None:
104
+ """One-time precomputation for zero_block_ids.
105
+
106
+ Builds absolute-address table for the Triton zeroing kernel.
107
+ Each entry is the absolute byte address of a segment start on the
108
+ GPU, so segments in different CUDA allocations work correctly.
109
+
110
+ Block IDs from the scheduler reference logical blocks whose size
111
+ may differ from the kernel block size (virtual block splitting).
112
+ PAGE_SIZE_EL accounts for this ratio so that
113
+ ``block_id * PAGE_SIZE_EL`` lands at the correct offset.
114
+
115
+ Only AttentionSpec layers are processed; Mamba layers are skipped.
116
+ """
117
+ seen_ptrs: set[int] = set()
118
+ seg_addrs: list[int] = []
119
+ page_size_el: int | None = None
120
+
121
+ for group in attn_groups_iter:
122
+ spec = group.kv_cache_spec
123
+ if type(spec) is not FullAttentionSpec:
124
+ continue
125
+ if group.kv_cache_group_id >= len(kernel_block_sizes):
126
+ continue
127
+ kernel_bs = kernel_block_sizes[group.kv_cache_group_id]
128
+ ratio = spec.block_size // kernel_bs
129
+ block_dim = group.backend.get_kv_cache_block_dim(
130
+ kernel_bs,
131
+ spec.num_kv_heads,
132
+ spec.head_size,
133
+ cache_dtype_str=cache_dtype,
134
+ )
135
+
136
+ for layer_name in group.layer_names:
137
+ if layer_name in runner_only_attn_layers:
138
+ continue
139
+ kv = static_forward_context[layer_name].kv_cache[0]
140
+ if isinstance(kv, list):
141
+ continue
142
+ dp = kv.data_ptr()
143
+ if dp in seen_ptrs:
144
+ continue
145
+ seen_ptrs.add(dp)
146
+
147
+ el = kv.element_size()
148
+ cur_bytes = kv.stride(block_dim) * el
149
+ assert cur_bytes % 4 == 0
150
+ kernel_block_el = cur_bytes // 4
151
+ cur_page_el = kernel_block_el * ratio
152
+ if page_size_el is None:
153
+ page_size_el = cur_page_el
154
+ else:
155
+ assert page_size_el == cur_page_el, (
156
+ f"Non-uniform page sizes: {page_size_el} vs {cur_page_el}"
157
+ )
158
+
159
+ block_stride_bytes = cur_bytes
160
+ outer_dims = [
161
+ d
162
+ for d in range(block_dim)
163
+ if kv.stride(d) * el > block_stride_bytes
164
+ ]
165
+ outer_strides = [kv.stride(d) * el for d in outer_dims]
166
+ for outer in iprod(*(range(kv.shape[d]) for d in outer_dims)):
167
+ off_bytes = sum(i * s for i, s in zip(outer, outer_strides))
168
+ seg_addrs.append(dp + off_bytes)
169
+
170
+ if not seg_addrs or page_size_el is None:
171
+ self._meta = None
172
+ return
173
+
174
+ blk_size = min(largest_power_of_2_divisor(page_size_el), 1024)
175
+ self._id_cap = 8192
176
+ self._ids_pinned = torch.empty(
177
+ self._id_cap,
178
+ dtype=torch.int64,
179
+ pin_memory=self.pin_memory,
180
+ )
181
+ self._ids_gpu = torch.empty(self._id_cap, dtype=torch.int64, device=self.device)
182
+ self._meta = (
183
+ torch.tensor(seg_addrs, dtype=torch.int64, device=self.device),
184
+ page_size_el,
185
+ blk_size,
186
+ len(seg_addrs),
187
+ )
188
+
189
+ def zero_block_ids(self, block_ids: list[int]) -> None:
190
+ """Zero the KV cache memory for the given block IDs."""
191
+ if not block_ids or self._meta is None:
192
+ return
193
+ seg_addrs, page_size_el, blk_size, n_segs = self._meta
194
+ n_blocks = len(block_ids)
195
+ if n_blocks > self._id_cap:
196
+ self._id_cap = n_blocks * 2
197
+ self._ids_pinned = torch.empty(
198
+ self._id_cap,
199
+ dtype=torch.int64,
200
+ pin_memory=self.pin_memory,
201
+ )
202
+ self._ids_gpu = torch.empty(
203
+ self._id_cap, dtype=torch.int64, device=self.device
204
+ )
205
+ assert self._ids_pinned is not None and self._ids_gpu is not None
206
+ self._ids_pinned[:n_blocks].numpy()[:] = block_ids
207
+ idx = self._ids_gpu[:n_blocks]
208
+ idx.copy_(self._ids_pinned[:n_blocks], non_blocking=True)
209
+ grid = (n_blocks * n_segs * (page_size_el // blk_size),)
210
+ _zero_kv_blocks_kernel[grid](
211
+ seg_addrs,
212
+ idx,
213
+ n_blocks,
214
+ N_SEGS=n_segs,
215
+ PAGE_SIZE_EL=page_size_el,
216
+ BLOCK_SIZE=blk_size,
217
+ )
218
+
219
+
220
@dataclass
221
class AttentionGroup:
222
backend: type[AttentionBackend]