[Mamba] Add FlashInfer ReplaySSM backend (#52506)
Signed-off-by: Andrii Skliar <askliar@nvidia.com> Signed-off-by: Andrii Skliar <andreyws96@gmail.com> Co-authored-by: Andrii Skliar <askliar@nvidia.com>
Andrii Skliar committed
Sep 1, 2026 at 16:26 UTC
adebc41b7e9f1085d3f73434e23beb76883b9eb4
19 files changed
+1029
-105
tests/kernels/mamba/test_ssu_dispatch.py
+70
-2
@@ -7,12 +7,15 @@ import pytest
7
import torch
8
9
from vllm.config.mamba import MambaBackendEnum, MambaConfig, MambaSSUAlgorithm
10
+from vllm.model_executor.layers.mamba.mamba_utils import MambaStateShapeCalculator
11
from vllm.model_executor.layers.mamba.ops.ssu_dispatch import (
12
FlashInferSSUBackend,
13
TritonSSUBackend,
14
get_mamba_ssu_backend,
15
initialize_mamba_ssu_backend,
16
+ reset_replayssm_ring_trackers,
17
selective_state_update,
18
+ update_replayssm_ring_trackers,
19
)
20
from vllm.utils.torch_utils import set_random_seed
21
from vllm.v1.attention.backends.registry import MambaAttentionBackendEnum
@@ -30,6 +33,47 @@ except ImportError:
33
HAS_FLASHINFER = False
34
35
36
+@pytest.fixture(autouse=True)
37
+def restore_backend_state():
38
+ import vllm.model_executor.layers.mamba.ops.ssu_dispatch as mod
39
+
40
+ old_backend = mod._mamba_ssu_backend
41
+ old_replayssm_kernel = mod._flashinfer_replayssm_kernel
42
+ yield
43
+ mod._mamba_ssu_backend = old_backend
44
+ mod._flashinfer_replayssm_kernel = old_replayssm_kernel
45
+
46
+
47
+def test_flashinfer_replayssm_ring_tracker_lifecycle():
48
+ ring_start = torch.zeros(2, dtype=torch.int32, device="cuda")
49
+ prev_num_accepted = torch.zeros(2, dtype=torch.int32, device="cuda")
50
+ state_batch_indices = torch.tensor([1], dtype=torch.int32, device="cuda")
51
+
52
+ observed = []
53
+ for _ in range(33):
54
+ update_replayssm_ring_trackers(
55
+ ring_start,
56
+ prev_num_accepted,
57
+ state_batch_indices,
58
+ logical_window=16,
59
+ ring_buffer_len=17,
60
+ )
61
+ observed.append((int(ring_start[1]), int(prev_num_accepted[1])))
62
+
63
+ assert observed[4] == (0, 5)
64
+ assert observed[15] == (0, 16)
65
+ assert observed[16] == (16, 1)
66
+ assert observed[31] == (16, 16)
67
+ assert observed[32] == (15, 1)
68
+
69
+ reset_replayssm_ring_trackers(
70
+ ring_start,
71
+ prev_num_accepted,
72
+ state_batch_indices,
73
+ )
74
+ assert (ring_start[1].item(), prev_num_accepted[1].item()) == (0, 0)
75
+
76
+
77
def _kv_cache_config_with_ssu(
78
mamba_type: MambaAttentionBackendEnum = MambaAttentionBackendEnum.MAMBA2,
79
) -> KVCacheConfig:
@@ -116,11 +160,10 @@ def test_flashinfer_forwards_ssu_algorithm(
160
def test_uninitialized_backend_raises():
161
import vllm.model_executor.layers.mamba.ops.ssu_dispatch as mod
162
119
- old = mod._mamba_ssu_backend
163
+ # restore_backend_state (autouse) puts the global back afterwards.
164
mod._mamba_ssu_backend = None
165
with pytest.raises(RuntimeError, match="not been initialized"):
166
get_mamba_ssu_backend()
123
- mod._mamba_ssu_backend = old
167
168
169
@pytest.mark.parametrize(
@@ -186,3 +229,28 @@ def test_triton_basic_call():
229
out=out,
230
)
231
assert not torch.isnan(out).any()
232
+
233
+
234
+@pytest.mark.parametrize(
235
+ ("backend", "expected_ring_len"),
236
+ [
237
+ (MambaBackendEnum.TRITON, 16),
238
+ (MambaBackendEnum.FLASHINFER, 17),
239
+ ],
240
+)
241
+def test_replayssm_physical_ring_shape(backend, expected_ring_len):
242
+ base_shapes = ((64, 3), (8, 4, 16))
243
+
244
+ shapes = MambaStateShapeCalculator.append_replayssm_ring(
245
+ base_shapes,
246
+ n_groups=4,
247
+ tp_world_size=2,
248
+ logical_window=16,
249
+ backend=backend,
250
+ )
251
+
252
+ assert shapes[2:] == (
253
+ (8, expected_ring_len, 4),
254
+ (8, expected_ring_len),
255
+ (2, expected_ring_len, 16),
256
+ )
tests/model_executor/test_replayssm_warmup.py
new
+150
@@ -0,0 +1,150 @@
1
+# SPDX-License-Identifier: Apache-2.0
2
+# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
+
4
+from types import SimpleNamespace
5
+from unittest.mock import Mock, patch
6
+
7
+import numpy as np
8
+import pytest
9
+import torch
10
+
11
+from vllm.config.mamba import MambaBackendEnum
12
+from vllm.model_executor.layers.mamba.mamba_mixer2 import MambaMixer2
13
+from vllm.model_executor.warmup import replayssm_warmup as warmup
14
+from vllm.platforms import current_platform
15
+from vllm.utils.flashinfer import has_flashinfer
16
+
17
+pytestmark = pytest.mark.skipif(
18
+ not current_platform.is_cuda() or not has_flashinfer(),
19
+ reason="FlashInfer ReplaySSM warmup tests require CUDA and FlashInfer",
20
+)
21
+
22
+PREFILL_KWARGS = {
23
+ "num_tokens": 128,
24
+ "skip_eplb": True,
25
+ "is_profile": True,
26
+ "randomize_inputs": True,
27
+}
28
+
29
+
30
+def _autotune_runner(
31
+ *,
32
+ use_v2_model_runner: bool = False,
33
+ query_len: int = 6,
34
+ max_num_seqs: int = 32,
35
+ num_blocks: int = 17,
36
+ max_num_tokens: int = 100,
37
+ use_replayssm: bool = True,
38
+ backend: MambaBackendEnum = MambaBackendEnum.FLASHINFER,
39
+) -> SimpleNamespace:
40
+ return SimpleNamespace(
41
+ vllm_config=SimpleNamespace(
42
+ cache_config=SimpleNamespace(use_replayssm=use_replayssm),
43
+ mamba_config=SimpleNamespace(backend=backend),
44
+ use_v2_model_runner=use_v2_model_runner,
45
+ ),
46
+ uniform_decode_query_len=query_len,
47
+ decode_query_len=query_len,
48
+ max_num_tokens=max_num_tokens,
49
+ scheduler_config=SimpleNamespace(max_num_seqs=max_num_seqs),
50
+ kv_cache_config=SimpleNamespace(num_blocks=num_blocks),
51
+ )
52
+
53
+
54
+@pytest.mark.parametrize(
55
+ ("runner_kwargs", "expected_num_reqs"),
56
+ [
57
+ # max_num_seqs (32) vs max_num_tokens // query_len (16) vs blocks-1 (16).
58
+ (dict(query_len=6, use_v2_model_runner=False), 16),
59
+ (dict(query_len=6, use_v2_model_runner=True), 16),
60
+ # num_blocks - 1 is the binding constraint.
61
+ (dict(query_len=1, max_num_tokens=128, max_num_seqs=64, num_blocks=5), 4),
62
+ ],
63
+ ids=["v1", "v2", "clamped_to_state_capacity"],
64
+)
65
+def test_replayssm_autotune_decode_kwargs(runner_kwargs, expected_num_reqs):
66
+ query_len = runner_kwargs["query_len"]
67
+ with patch.object(
68
+ warmup, "flashinfer_replayssm_autotune_supported", return_value=True
69
+ ):
70
+ result = warmup._replayssm_autotune_kwargs(_autotune_runner(**runner_kwargs))
71
+
72
+ expected_kwargs = {
73
+ **PREFILL_KWARGS,
74
+ "num_tokens": expected_num_reqs * query_len,
75
+ "uniform_decode": True,
76
+ }
77
+ if runner_kwargs.get("use_v2_model_runner"):
78
+ expected_kwargs["valid_dummy_state_slots"] = True
79
+ else:
80
+ expected_kwargs.update(
81
+ allow_microbatching=False,
82
+ force_attention=True,
83
+ profile_seq_lens=query_len + 1,
84
+ )
85
+ assert result == (expected_num_reqs, expected_kwargs)
86
+
87
+
88
+@pytest.mark.parametrize(
89
+ ("runner_kwargs", "flashinfer_supported"),
90
+ [
91
+ (dict(use_replayssm=False), True),
92
+ (dict(backend=MambaBackendEnum.TRITON), True),
93
+ ({}, False),
94
+ ],
95
+ ids=["replayssm_disabled", "non_flashinfer_backend", "kernel_unavailable"],
96
+)
97
+def test_replayssm_autotune_kwargs_skipped(runner_kwargs, flashinfer_supported):
98
+ with patch.object(
99
+ warmup,
100
+ "flashinfer_replayssm_autotune_supported",
101
+ return_value=flashinfer_supported,
102
+ ):
103
+ result = warmup._replayssm_autotune_kwargs(_autotune_runner(**runner_kwargs))
104
+ assert result is None
105
+
106
+
107
+def test_replayssm_autotune_slots_restore_state_and_trackers():
108
+ mixer = MambaMixer2.__new__(MambaMixer2)
109
+ torch.nn.Module.__init__(mixer)
110
+ mixer.use_replayssm = True
111
+ mixer.replayssm_buffer_len = 16
112
+ mixer.kv_cache = (
113
+ torch.full((4, 2), 3.0),
114
+ torch.full((4, 2), 3.0),
115
+ *(torch.full((4, 2, 17), 3.0) for _ in range(3)),
116
+ )
117
+ mixer._replayssm_ring_start = torch.full((4,), 3, dtype=torch.int32)
118
+ mixer._replayssm_prev_num_accepted = torch.full((4,), 3, dtype=torch.int32)
119
+ tracked = (
120
+ *mixer.kv_cache,
121
+ mixer._replayssm_ring_start,
122
+ mixer._replayssm_prev_num_accepted,
123
+ )
124
+
125
+ block_ids = np.arange(10, 14, dtype=np.int32).reshape(4, 1)
126
+ original_block_ids = block_ids.copy()
127
+ block_table = SimpleNamespace(block_table=SimpleNamespace(np=block_ids))
128
+ multi_group_block_table = SimpleNamespace(
129
+ block_tables=[block_table], commit_block_table=Mock()
130
+ )
131
+ runner = SimpleNamespace(
132
+ vllm_config=SimpleNamespace(use_v2_model_runner=False),
133
+ input_batch=SimpleNamespace(block_table=multi_group_block_table),
134
+ get_model=lambda: SimpleNamespace(modules=lambda: (mixer,)),
135
+ )
136
+
137
+ with warmup._temporary_replayssm_autotune_state(runner, 2):
138
+ assert block_ids[:2, 0].tolist() == [1, 2]
139
+ for tensor in tracked:
140
+ tensor[1:3].fill_(9)
141
+
142
+ assert np.array_equal(block_ids, original_block_ids)
143
+ assert multi_group_block_table.commit_block_table.call_args_list == [
144
+ ((2,), {}),
145
+ ((2,), {}),
146
+ ]
147
+ for tensor in tracked:
148
+ assert torch.count_nonzero(tensor[1:3]) == 0
149
+ assert torch.all(tensor[0] == 3)
150
+ assert torch.all(tensor[3] == 3)
tests/v1/attention/test_replayssm_metadata_builder.py
+46
-12
@@ -16,6 +16,7 @@ from tests.v1.attention.utils import (
16
create_common_attn_metadata,
17
create_vllm_config,
18
)
19
+from vllm.config.mamba import MambaBackendEnum
20
from vllm.v1.kv_cache_interface import MambaSpec
21
22
BLOCK_SIZE = 16
@@ -187,34 +188,47 @@ REPLAYSSM_BUILD_CASES = {
188
}
189
190
190
-def _make_mamba_spec(buffer_len: int) -> MambaSpec:
191
- # Five-tensor ReplaySSM page; the builder only reads shapes[4][0] (bc groups).
191
+def _make_mamba_spec(
192
+ buffer_len: int,
193
+ mamba_backend: MambaBackendEnum,
194
+) -> MambaSpec:
195
+ ring_buffer_len = buffer_len + (
196
+ 1 if mamba_backend == MambaBackendEnum.FLASHINFER else 0
197
+ )
198
+ shapes = (
199
+ (1, 1),
200
+ (1, 1, 1),
201
+ (1, ring_buffer_len, 1),
202
+ (1, ring_buffer_len),
203
+ (1, ring_buffer_len, 1),
204
+ )
205
return MambaSpec(
206
block_size=BLOCK_SIZE,
194
- shapes=(
195
- (1, 1),
196
- (1, 1, 1),
197
- (1, buffer_len, 1),
198
- (1, buffer_len),
199
- (1, buffer_len, 1),
200
- ),
207
+ shapes=shapes,
208
dtypes=(torch.float32,),
209
)
210
211
212
def _create_replayssm_builder(
206
- buffer_len: int, mamba_cache_mode: str = "none"
213
+ buffer_len: int,
214
+ mamba_cache_mode: str = "none",
215
+ *,
216
+ mamba_backend: MambaBackendEnum = MambaBackendEnum.TRITON,
217
) -> MockMambaBuilder:
218
vllm_config = create_vllm_config(
219
model_name="Qwen/Qwen3.5-0.8B", block_size=BLOCK_SIZE
220
)
221
# Set the flags after construction to skip validate_mamba_cached_kernel
212
- # (it requires a Triton backend) on the mock model.
222
+ # (it requires a real SupportsReplaySSM model) on the mock model.
223
vllm_config.cache_config.use_replayssm = True
224
vllm_config.cache_config.replayssm_buffer_len = buffer_len
225
vllm_config.cache_config.mamba_cache_mode = mamba_cache_mode
226
+ vllm_config.mamba_config.backend = mamba_backend
227
return MockMambaBuilder(
217
- _make_mamba_spec(buffer_len), ["layer0"], vllm_config, DEVICE
228
+ _make_mamba_spec(buffer_len, mamba_backend),
229
+ ["layer0"],
230
+ vllm_config,
231
+ DEVICE,
232
)
233
234
@@ -254,3 +268,23 @@ def test_resumed_request_differs_from_fresh():
268
269
assert meta.write_pos_d.tolist()[:2] == [5, 0]
270
assert meta.is_flush_d.tolist()[:2] == [0, 0]
271
+
272
+
273
+def test_flashinfer_replayssm_scratch_metadata_fresh_decode():
274
+ checkpointing_ssu = pytest.importorskip("flashinfer.mamba.checkpointing_ssu")
275
+ if not hasattr(checkpointing_ssu, "allocate_checkpointing_ssu_scratch"):
276
+ pytest.skip("FlashInfer does not expose ReplaySSM scratch allocation")
277
+
278
+ builder = _create_replayssm_builder(16, mamba_backend=MambaBackendEnum.FLASHINFER)
279
+ case = REPLAYSSM_BUILD_CASES["fresh_decode"]
280
+ meta = _build(builder, case)
281
+
282
+ assert meta.write_pos_d is None
283
+ assert meta.is_flush_d is None
284
+ assert meta.bc_pre_scratch is None
285
+ assert meta.replayssm_scratch is not None
286
+ assert [tensor.shape for tensor in meta.replayssm_scratch] == [
287
+ (1, 1, 32, 8),
288
+ (1, 1, 16),
289
+ (1, 1, 32, 8),
290
+ ]
tests/v1/e2e/test_replayssm_decode.py
+37
-2
@@ -4,6 +4,7 @@
4
5
import pytest
6
7
+import vllm.envs as envs
8
from vllm.v1.metrics.reader import Counter
9
10
from ...models.utils import check_logprobs_close
@@ -21,29 +22,48 @@ PROMPTS = [
22
]
23
24
24
-def _check_replayssm_parity(vllm_runner, model_name, *, tensor_parallel_size=1):
25
+def _check_replayssm_parity(
26
+ vllm_runner,
27
+ model_name,
28
+ *,
29
+ tensor_parallel_size=1,
30
+ mamba_backend: str = "triton",
31
+ name_1: str = "replayssm",
32
+ require_v2: bool = False,
33
+ monkeypatch: pytest.MonkeyPatch | None = None,
34
+):
35
# Compare logprobs, not greedy ids: ReplaySSM's fp arithmetic can flip a
36
# near-tie. Baseline and ReplaySSM run at the same TP, so TP numerics are
37
# common-mode and only ReplaySSM varies.
38
+ if require_v2:
39
+ assert monkeypatch is not None
40
+ monkeypatch.setenv("VLLM_USE_V2_MODEL_RUNNER", "1")
41
+ envs.disable_envs_cache()
42
+
43
common = dict(
44
max_model_len=1024,
45
trust_remote_code=True,
46
enable_prefix_caching=False,
47
mamba_cache_mode="none",
48
tensor_parallel_size=tensor_parallel_size,
49
+ mamba_backend=mamba_backend,
50
)
51
with vllm_runner(model_name, **common) as llm:
52
+ if require_v2:
53
+ assert llm.llm.llm_engine.vllm_config.use_v2_model_runner
54
baseline = llm.generate_greedy_logprobs(PROMPTS, max_tokens=32, num_logprobs=5)
55
with vllm_runner(
56
model_name, use_replayssm=True, replayssm_buffer_len=16, **common
57
) as llm:
58
+ if require_v2:
59
+ assert llm.llm.llm_engine.vllm_config.use_v2_model_runner
60
replay = llm.generate_greedy_logprobs(PROMPTS, max_tokens=32, num_logprobs=5)
61
62
check_logprobs_close(
63
outputs_0_lst=baseline,
64
outputs_1_lst=replay,
65
name_0="baseline",
46
- name_1="replayssm",
66
+ name_1=name_1,
67
)
68
69
@@ -60,6 +80,21 @@ def test_replayssm_decode_matches_baseline_tp2(vllm_runner, model_name):
80
_check_replayssm_parity(vllm_runner, model_name, tensor_parallel_size=2)
81
82
83
+@pytest.mark.parametrize("model_name", MODELS)
84
+def test_replayssm_flashinfer_decode_matches_baseline_v2(
85
+ vllm_runner, model_name, monkeypatch
86
+):
87
+ pytest.importorskip("flashinfer.mamba.checkpointing_ssu")
88
+ _check_replayssm_parity(
89
+ vllm_runner,
90
+ model_name,
91
+ mamba_backend="flashinfer",
92
+ name_1="replayssm_flashinfer_v2",
93
+ require_v2=True,
94
+ monkeypatch=monkeypatch,
95
+ )
96
+
97
+
98
# Prefix spans several mamba blocks; prefix caching only reuses full blocks.
99
_PC_SENTENCE = (
100
"In a detailed survey of state space models, the authors compared many "
tests/v1/worker/test_kv_cache_allocation_scope.py
+2
-2
@@ -48,7 +48,7 @@ def test_mrv2_kv_pool_only_wraps_backing_allocation(monkeypatch) -> None:
48
result = attn_utils.init_kv_cache(
49
[],
50
{},
51
- object(),
51
+ SimpleNamespace(kv_cache_groups=[]),
52
torch.device("cpu"),
53
[],
54
config,
@@ -83,7 +83,7 @@ def test_mrv1_kv_pool_only_wraps_backing_allocation(monkeypatch) -> None:
83
)
84
result = gpu_model_runner.GPUModelRunner.initialize_kv_cache_tensors(
85
runner,
86
- object(),
86
+ SimpleNamespace(kv_cache_groups=[]),
87
[],
88
kv_cache_allocation_context=scope,
89
)
tests/v1/worker/test_utils.py
+57
@@ -1,11 +1,68 @@
1
# SPDX-License-Identifier: Apache-2.0
2
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4
+from types import SimpleNamespace
5
+
6
import torch
7
8
+from vllm.config.mamba import MambaBackendEnum, MambaConfig
9
+from vllm.model_executor.layers.mamba.mamba_mixer2 import MambaMixer2
10
from vllm.v1.worker.utils import bind_kv_cache
11
12
13
+class _TestReplaySSMMixer(MambaMixer2):
14
+ def __init__(self):
15
+ torch.nn.Module.__init__(self)
16
+ self.use_replayssm = True
17
+ self.mamba_config = MambaConfig(backend=MambaBackendEnum.FLASHINFER)
18
+ self._replayssm_ring_start = torch.empty(0, dtype=torch.int32)
19
+ self._replayssm_prev_num_accepted = torch.empty(0, dtype=torch.int32)
20
+ self._updates_replayssm_trackers = True
21
+
22
+ def get_state_shape(self) -> tuple[tuple[int, ...], ...]:
23
+ return ((2,), (3,), (4,), (5,), (6,))
24
+
25
+ def get_state_dtype(self) -> tuple[torch.dtype, ...]:
26
+ return (torch.float32,) * 5
27
+
28
+
29
+def _packed_replayssm_cache(num_blocks: int) -> torch.Tensor:
30
+ return torch.full((num_blocks, 1, 1, 80), 0, dtype=torch.int8)
31
+
32
+
33
+def test_bind_kv_cache_shares_replayssm_trackers_by_cache_group():
34
+ mixers = [_TestReplaySSMMixer() for _ in range(3)]
35
+ layer_names = [f"layers.{i}.mixer" for i in range(3)]
36
+ ctx = dict(zip(layer_names, mixers))
37
+ # Reverse insertion order: updater must follow layer index, not dict order.
38
+ kv_cache = {
39
+ layer_names[2]: _packed_replayssm_cache(4),
40
+ layer_names[1]: _packed_replayssm_cache(4),
41
+ layer_names[0]: _packed_replayssm_cache(4),
42
+ }
43
+ kv_cache_groups = [
44
+ SimpleNamespace(layer_names=[layer_names[0], layer_names[2]]),
45
+ SimpleNamespace(layer_names=[layer_names[1]]),
46
+ ]
47
+
48
+ bind_kv_cache(kv_cache, ctx, [], kv_cache_groups=kv_cache_groups)
49
+
50
+ assert (
51
+ mixers[0]._replayssm_ring_start.data_ptr()
52
+ == mixers[2]._replayssm_ring_start.data_ptr()
53
+ )
54
+ assert (
55
+ mixers[0]._replayssm_prev_num_accepted.data_ptr()
56
+ == mixers[2]._replayssm_prev_num_accepted.data_ptr()
57
+ )
58
+ assert (
59
+ mixers[1]._replayssm_ring_start.data_ptr()
60
+ != mixers[0]._replayssm_ring_start.data_ptr()
61
+ )
62
+ # Group {0, 2} shares trackers; layer 2 (not 0) updates after both run.
63
+ assert [m._updates_replayssm_trackers for m in mixers] == [False, True, True]
64
+
65
+
66
def test_bind_kv_cache(default_vllm_config):
67
from vllm.model_executor.layers.attention import Attention
68
vllm/config/cache.py
+6
-5
@@ -196,15 +196,16 @@ class CacheConfig:
196
caching is enabled.
197
"""
198
replayssm_buffer_len: int = Field(default=16, gt=0)
199
- """ReplaySSM history buffer length B for standard Mamba2 decode. Kimi-K3
200
- speculative decoding does not use B. Default 16."""
199
+ """ReplaySSM logical history length B for Mamba2. Triton uses B physical
200
+ rows and FlashInfer uses B+1. Kimi-K3 speculative decode does not use B.
201
+ Default 16."""
202
use_replayssm: bool = False
203
"""Use the ReplaySSM Mamba2 decode kernel: cache recent SSM inputs and skip
204
the per-step full-state store, writing the checkpoint back only on flush.
205
Requires mamba_cache_mode 'none' or 'align' (prefix caching) and the Triton
205
- mamba backend; standard (non-speculative) decode only. In align mode flushes
206
- are most efficient when mamba_block_size is a multiple of replayssm_buffer_len,
207
- but this is not required."""
206
+ or FlashInfer mamba backend; standard (non-speculative) decode only. In align
207
+ mode flushes are most efficient when mamba_block_size is a multiple of
208
+ replayssm_buffer_len, but this is not required."""
209
use_kda_recoverssm: bool = field(default=False, init=False)
210
"""Whether Kimi-K3 KDA uses RecoverSSM speculative decode."""
211
vllm/config/vllm.py
+17
-2
@@ -2889,13 +2889,28 @@ class VllmConfig:
2889
raise ValueError(
2890
"RecoverSSM currently requires pipeline_parallel_size=1"
2891
)
2892
+ if self.mamba_config.backend != MambaBackendEnum.TRITON:
2893
+ raise ValueError("RecoverSSM requires --mamba-backend triton")
2894
elif self.cache_config.mamba_cache_mode == "all":
2895
raise ValueError(
2896
"--use-replayssm supports prefix caching only in align mode; "
2897
"pass --mamba-cache-mode align"
2898
)
2897
- if self.mamba_config.backend != MambaBackendEnum.TRITON:
2898
- raise ValueError("--use-replayssm requires --mamba-backend triton")
2899
+ elif self.mamba_config.backend == MambaBackendEnum.FLASHINFER:
2900
+ if self.cache_config.mamba_cache_mode == "align":
2901
+ raise ValueError(
2902
+ "FlashInfer ReplaySSM does not support "
2903
+ "--mamba-cache-mode align yet; use none"
2904
+ )
2905
+ elif self.mamba_config.backend != MambaBackendEnum.TRITON:
2906
+ raise ValueError(
2907
+ "--use-replayssm requires --mamba-backend triton or flashinfer"
2908
+ )
2909
+ elif self.use_v2_model_runner:
2910
+ raise ValueError(
2911
+ "Triton ReplaySSM requires Model Runner V1; use "
2912
+ "--mamba-backend flashinfer or Model Runner V1"
2913
+ )
2914
if (
2915
self.kv_transfer_config is not None
2916
and self.kv_transfer_config.is_kv_transfer_instance
vllm/model_executor/layers/mamba/mamba_mixer2.py
+148
-37
@@ -2,10 +2,13 @@
2
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4
5
+from collections.abc import Sequence
6
+
7
import torch
8
from torch import nn
9
10
from vllm.config import CacheConfig, ModelConfig, get_current_vllm_config
11
+from vllm.config.mamba import MambaBackendEnum
12
from vllm.distributed import (
13
divide,
14
get_tensor_model_parallel_rank,
@@ -16,6 +19,7 @@ from vllm.distributed import (
19
from vllm.forward_context import ForwardContext, get_forward_context
20
from vllm.logger import init_logger
21
from vllm.model_executor.custom_op import CustomOp, PluggableLayer
22
+from vllm.model_executor.layers.attention import Attention
23
from vllm.model_executor.layers.linear import (
24
ColumnParallelLinear,
25
MergedColumnParallelLinear,
@@ -38,7 +42,11 @@ from vllm.model_executor.layers.mamba.ops.selective_state_update_replayssm_outpu
42
from vllm.model_executor.layers.mamba.ops.ssd_combined import (
43
mamba_chunk_scan_combined_varlen,
44
)
41
-from vllm.model_executor.layers.mamba.ops.ssu_dispatch import selective_state_update
45
+from vllm.model_executor.layers.mamba.ops.ssu_dispatch import (
46
+ reset_replayssm_ring_trackers,
47
+ selective_state_update,
48
+ selective_state_update_replayssm_flashinfer,
49
+)
50
from vllm.model_executor.layers.quantization import QuantizationConfig
51
from vllm.model_executor.model_loader.weight_utils import (
52
LoaderFunction,
@@ -58,6 +66,7 @@ from vllm.utils.torch_utils import (
66
from vllm.v1.attention.backend import AttentionMetadata
67
from vllm.v1.attention.backends.mamba2_attn import Mamba2AttentionMetadata
68
from vllm.v1.attention.backends.registry import MambaAttentionBackendEnum
69
+from vllm.v1.kv_cache_interface import KVCacheGroupSpec
70
71
logger = init_logger(__name__)
72
@@ -515,10 +524,12 @@ class MambaMixer2(MambaBase, PluggableLayer):
524
raise ValueError(
525
"--use-replayssm requires tensor-parallel heads to divide evenly"
526
)
518
- # The tuple is (conv_state, ssm_state); with the cached (ReplaySSM) decode
519
- # kernel enabled it is (conv_state, ssm_state, x_cache, dt_cache, B_cache).
527
+ # ReplaySSM appends x/dt/B rings to (conv_state, ssm_state).
528
_n_state = 5 if self.use_replayssm else 2
529
self.kv_cache = tuple(torch.tensor([]) for _ in range(_n_state))
530
+ self._replayssm_ring_start = torch.empty(0, dtype=torch.int32)
531
+ self._replayssm_prev_num_accepted = torch.empty(0, dtype=torch.int32)
532
+ self._updates_replayssm_trackers = True
533
534
self.num_spec = vllm_config.num_speculative_tokens
535
if self.num_spec > 0:
@@ -706,6 +717,7 @@ class MambaMixer2(MambaBase, PluggableLayer):
717
assert self.cache_config is not None
718
mamba_block_size = self.cache_config.mamba_block_size
719
is_mamba_cache_all = self.cache_config.mamba_cache_mode == "all"
720
+ ring_start = prev_num_accepted = None
721
722
attn_metadata: AttentionMetadata | None = None
723
if attn_metadata_raw is not None:
@@ -723,7 +735,10 @@ class MambaMixer2(MambaBase, PluggableLayer):
735
)
736
ssm_state = self.kv_cache[1]
737
if self.use_replayssm:
726
- x_cache, dt_cache, B_cache = self.kv_cache[2:]
738
+ x_cache, dt_cache, B_cache = self.kv_cache[2:5]
739
+ if self.mamba_config.backend == MambaBackendEnum.FLASHINFER:
740
+ ring_start = self._replayssm_ring_start
741
+ prev_num_accepted = self._replayssm_prev_num_accepted
742
else:
743
x_cache = dt_cache = B_cache = None
744
has_initial_states_p = attn_metadata.has_initial_states_p
@@ -982,6 +997,13 @@ class MambaMixer2(MambaBase, PluggableLayer):
997
# tensor
998
assert state_indices_tensor_p is not None
999
ssm_state[state_indices_tensor_p] = varlen_states
1000
+ if ring_start is not None and self._updates_replayssm_trackers:
1001
+ assert prev_num_accepted is not None
1002
+ reset_replayssm_ring_trackers(
1003
+ ring_start,
1004
+ prev_num_accepted,
1005
+ state_indices_tensor_p,
1006
+ )
1007
1008
# Process decode requests
1009
if has_decode:
@@ -1060,35 +1082,64 @@ class MambaMixer2(MambaBase, PluggableLayer):
1082
)
1083
if self.use_replayssm:
1084
assert self.replayssm_buffer_len is not None
1063
- selective_state_update_replayssm_output_only(
1064
- ssm_state,
1065
- hidden_states_d,
1066
- dt_d,
1067
- A_d,
1068
- B_d,
1069
- C_d,
1070
- D_d,
1071
- dt_bias,
1072
- dt_softplus=True,
1073
- x_cache=x_cache,
1074
- dt_cache=dt_cache,
1075
- B_cache=B_cache,
1076
- bc_pre=attn_metadata.bc_pre_scratch,
1077
- write_pos=attn_metadata.write_pos_d,
1078
- is_flush=attn_metadata.is_flush_d,
1079
- max_cache_len=self.replayssm_buffer_len,
1080
- state_batch_indices=state_indices_tensor_d_input,
1081
- out=preallocated_ssm_out_d,
1082
- # Stochastic Rounding for the vanilla decode path is read
1083
- # from mamba_config inside ssu_dispatch; the replay kernel
1084
- # isn't a dispatch backend, so pass it here.
1085
- enable_stochastic_rounding=(
1086
- self.mamba_config.enable_stochastic_rounding
1087
- ),
1088
- cache_philox_rounds=(
1089
- self.mamba_config.stochastic_rounding_philox_rounds
1090
- ),
1091
- )
1085
+ if self.mamba_config.backend == MambaBackendEnum.FLASHINFER:
1086
+ assert ring_start is not None
1087
+ assert prev_num_accepted is not None
1088
+ assert attn_metadata.replayssm_scratch is not None
1089
+ selective_state_update_replayssm_flashinfer(
1090
+ ssm_state,
1091
+ hidden_states_d,
1092
+ dt_d,
1093
+ A_d,
1094
+ B_d,
1095
+ C_d,
1096
+ preallocated_ssm_out_d,
1097
+ x_cache,
1098
+ B_cache,
1099
+ dt_cache,
1100
+ ring_start,
1101
+ prev_num_accepted,
1102
+ logical_window=self.replayssm_buffer_len,
1103
+ D=D_d,
1104
+ dt_bias=dt_bias,
1105
+ dt_softplus=True,
1106
+ state_batch_indices=state_indices_tensor_d_input,
1107
+ scratch=attn_metadata.replayssm_scratch,
1108
+ update_trackers=self._updates_replayssm_trackers,
1109
+ enable_stochastic_rounding=(
1110
+ self.mamba_config.enable_stochastic_rounding
1111
+ ),
1112
+ stochastic_rounding_philox_rounds=(
1113
+ self.mamba_config.stochastic_rounding_philox_rounds
1114
+ ),
1115
+ )
1116
+ else:
1117
+ selective_state_update_replayssm_output_only(
1118
+ ssm_state,
1119
+ hidden_states_d,
1120
+ dt_d,
1121
+ A_d,
1122
+ B_d,
1123
+ C_d,
1124
+ D_d,
1125
+ dt_bias,
1126
+ dt_softplus=True,
1127
+ x_cache=x_cache,
1128
+ dt_cache=dt_cache,
1129
+ B_cache=B_cache,
1130
+ bc_pre=attn_metadata.bc_pre_scratch,
1131
+ write_pos=attn_metadata.write_pos_d,
1132
+ is_flush=attn_metadata.is_flush_d,
1133
+ max_cache_len=self.replayssm_buffer_len,
1134
+ state_batch_indices=state_indices_tensor_d_input,
1135
+ out=preallocated_ssm_out_d,
1136
+ enable_stochastic_rounding=(
1137
+ self.mamba_config.enable_stochastic_rounding
1138
+ ),
1139
+ cache_philox_rounds=(
1140
+ self.mamba_config.stochastic_rounding_philox_rounds
1141
+ ),
1142
+ )
1143
else:
1144
selective_state_update(
1145
ssm_state,
@@ -1137,10 +1188,11 @@ class MambaMixer2(MambaBase, PluggableLayer):
1188
if self.use_replayssm:
1189
assert self.replayssm_buffer_len is not None
1190
return MambaStateShapeCalculator.append_replayssm_ring(
1140
- base_shape,
1141
- self.n_groups,
1142
- tp_world_size,
1143
- self.replayssm_buffer_len,
1191
+ base_shapes=base_shape,
1192
+ n_groups=self.n_groups,
1193
+ tp_world_size=tp_world_size,
1194
+ logical_window=self.replayssm_buffer_len,
1195
+ backend=self.mamba_config.backend,
1196
)
1197
return base_shape
1198
@@ -1149,6 +1201,65 @@ class MambaMixer2(MambaBase, PluggableLayer):
1201
return MambaAttentionBackendEnum.MAMBA2
1202
1203
1204
+def share_replayssm_ring_trackers(
1205
+ ordered_layer_names: list[str],
1206
+ forward_context: dict[str, Attention],
1207
+ kv_cache_groups: Sequence[KVCacheGroupSpec] | None = None,
1208
+) -> None:
1209
+ """Share ring cursors within each cache-slot index namespace.
1210
+
1211
+ Layers backed by one KV-cache group use the same physical block indices and
1212
+ can therefore share cursors. Different KV-cache groups may assign different
1213
+ block indices to the same request and must keep separate cursor tensors.
1214
+ The final local layer in each group advances its cursors after every layer
1215
+ in that group has consumed the previous values.
1216
+ """
1217
+
1218
+ replayssm_mixers: dict[str, MambaMixer2] = {}
1219
+ for layer_name in ordered_layer_names:
1220
+ layer = forward_context[layer_name]
1221
+ if (
1222
+ isinstance(layer, MambaMixer2)
1223
+ and layer.use_replayssm
1224
+ and layer.mamba_config.backend == MambaBackendEnum.FLASHINFER
1225
+ ):
1226
+ replayssm_mixers[layer_name] = layer
1227
+
1228
+ layer_to_group: dict[str, int] = {}
1229
+ if kv_cache_groups:
1230
+ for group_idx, group in enumerate(kv_cache_groups):
1231
+ for layer_name in group.layer_names:
1232
+ layer_to_group[layer_name] = group_idx
1233
+
1234
+ groups_by_namespace: dict[int | str, list[str]] = {}
1235
+ for layer_name in ordered_layer_names:
1236
+ if layer_name not in replayssm_mixers:
1237
+ continue
1238
+ namespace = layer_to_group.get(layer_name, layer_name)
1239
+ groups_by_namespace.setdefault(namespace, []).append(layer_name)
1240
+
1241
+ for group_layer_names in groups_by_namespace.values():
1242
+ last_layer_name = group_layer_names[-1]
1243
+
1244
+ first_mixer = replayssm_mixers[group_layer_names[0]]
1245
+ first_state = first_mixer.kv_cache[1]
1246
+ num_blocks, device = first_state.shape[0], first_state.device
1247
+ for layer_name in group_layer_names:
1248
+ state = replayssm_mixers[layer_name].kv_cache[1]
1249
+ if (state.shape[0], state.device) != (num_blocks, device):
1250
+ raise ValueError(
1251
+ "ReplaySSM layers in one cache group must share cache capacity"
1252
+ )
1253
+
1254
+ ring_start = torch.zeros(num_blocks, dtype=torch.int32, device=device)
1255
+ prev_num_accepted = torch.zeros_like(ring_start)
1256
+ for layer_name in group_layer_names:
1257
+ mixer = replayssm_mixers[layer_name]
1258
+ mixer._replayssm_ring_start = ring_start
1259
+ mixer._replayssm_prev_num_accepted = prev_num_accepted
1260
+ mixer._updates_replayssm_trackers = layer_name == last_layer_name
1261
+
1262
+
1263
def mamba_mixer2(
1264
projected_states: torch.Tensor,
1265
output: torch.Tensor,
vllm/model_executor/layers/mamba/mamba_utils.py
+14
-8
@@ -10,6 +10,7 @@ import torch
10
11
import vllm.envs as envs
12
from vllm.config.cache import MambaDType
13
+from vllm.config.mamba import MambaBackendEnum
14
from vllm.config.model import ModelDType
15
from vllm.distributed import divide
16
from vllm.logger import init_logger
@@ -214,20 +215,25 @@ class MambaStateShapeCalculator:
215
base_shapes: tuple[tuple[int, ...], ...],
216
n_groups: int,
217
tp_world_size: int,
217
- replayssm_buffer_len: int,
218
+ logical_window: int,
219
+ backend: MambaBackendEnum,
220
) -> tuple[tuple[int, ...], ...]:
219
- """Append the ReplaySSM ring shapes (x_cache, dt_cache, B_cache) to a
220
- base ``(conv, ssm)`` tuple. ``base_shapes[1]`` is the ssm shape
221
- ``(nheads // tp, head_dim, state_size)``; B_cache uses the un-extended
222
- ``n_groups``.
221
+ """Append the physical ReplaySSM ring shapes.
222
+
223
+ ``base_shapes[1]`` is ``(nheads // tp, head_dim, state_size)``;
224
+ B_cache uses the un-extended ``n_groups``.
225
"""
226
+ ring_buffer_len = logical_window
227
+ if backend == MambaBackendEnum.FLASHINFER:
228
+ # FlashInfer keeps the live window and appended token together.
229
+ ring_buffer_len += 1
230
local_nheads, head_dim, state_size = base_shapes[1]
231
local_ngroups = divide(n_groups, tp_world_size)
232
return (
233
*base_shapes,
228
- (local_nheads, replayssm_buffer_len, head_dim),
229
- (local_nheads, replayssm_buffer_len),
230
- (local_ngroups, replayssm_buffer_len, state_size),
234
+ (local_nheads, ring_buffer_len, head_dim),
235
+ (local_nheads, ring_buffer_len),
236
+ (local_ngroups, ring_buffer_len, state_size),
237
)
238
239
@classmethod
vllm/model_executor/layers/mamba/ops/ssu_dispatch.py
+221
-14
@@ -10,11 +10,14 @@ the backend defaults to 'cpu'.
10
"""
11
12
from abc import ABC, abstractmethod
13
+from collections.abc import Callable
14
+from functools import cache
15
16
import torch
17
18
from vllm.config.mamba import MambaBackendEnum, MambaConfig, MambaSSUAlgorithm
19
from vllm.logger import init_logger
20
+from vllm.triton_utils import tl, triton
21
from vllm.v1.attention.backends.registry import MambaAttentionBackendEnum
22
from vllm.v1.attention.backends.utils import NULL_BLOCK_ID
23
from vllm.v1.kv_cache_interface import KVCacheConfig, MambaSpec
@@ -22,6 +25,99 @@ from vllm.v1.kv_cache_interface import KVCacheConfig, MambaSpec
25
logger = init_logger(__name__)
26
27
28
+@triton.jit(
29
+ do_not_specialize=["n_slots", "state_batch_indices_stride"],
30
+ do_not_specialize_on_alignment=["state_batch_indices"],
31
+)
32
+def _update_replayssm_ring_trackers_kernel(
33
+ ring_start,
34
+ prev_num_accepted,
35
+ state_batch_indices,
36
+ state_batch_indices_stride,
37
+ n_slots,
38
+ num_states,
39
+ logical_window: tl.constexpr,
40
+ ring_buffer_len: tl.constexpr,
41
+ pad_slot_id: tl.constexpr,
42
+ RESET: tl.constexpr,
43
+ BLOCK: tl.constexpr,
44
+) -> None:
45
+ offsets = tl.program_id(0) * BLOCK + tl.arange(0, BLOCK)
46
+ mask = offsets < n_slots
47
+ slots = tl.load(
48
+ state_batch_indices + offsets * state_batch_indices_stride,
49
+ mask=mask,
50
+ other=pad_slot_id,
51
+ )
52
+ valid = mask & (slots != pad_slot_id) & (slots >= 0) & (slots < num_states)
53
+ if RESET:
54
+ tl.store(ring_start + slots, 0, mask=valid)
55
+ tl.store(prev_num_accepted + slots, 0, mask=valid)
56
+ else:
57
+ prev = tl.load(prev_num_accepted + slots, mask=valid, other=0)
58
+ start = tl.load(ring_start + slots, mask=valid, other=0)
59
+ must_checkpoint = prev + 1 > logical_window
60
+ next_start = tl.where(
61
+ must_checkpoint,
62
+ (start + prev) % ring_buffer_len,
63
+ start,
64
+ )
65
+ next_prev = tl.where(must_checkpoint, 1, prev + 1)
66
+ tl.store(ring_start + slots, next_start, mask=valid)
67
+ tl.store(prev_num_accepted + slots, next_prev, mask=valid)
68
+
69
+
70
+def update_replayssm_ring_trackers(
71
+ ring_start: torch.Tensor,
72
+ prev_num_accepted: torch.Tensor,
73
+ state_batch_indices: torch.Tensor,
74
+ logical_window: int | None = None,
75
+ ring_buffer_len: int | None = None,
76
+ pad_slot_id: int = NULL_BLOCK_ID,
77
+) -> None:
78
+ """Reset selected trackers, or advance them when a window is provided."""
79
+ if state_batch_indices.dim() > 1:
80
+ state_batch_indices = state_batch_indices[:, 0]
81
+ n_slots = state_batch_indices.numel()
82
+ if n_slots == 0:
83
+ return
84
+ reset = logical_window is None
85
+ if reset:
86
+ logical_window = 0
87
+ ring_buffer_len = 1
88
+ else:
89
+ assert ring_buffer_len is not None
90
+ block = 128
91
+ _update_replayssm_ring_trackers_kernel[(triton.cdiv(n_slots, block),)](
92
+ ring_start,
93
+ prev_num_accepted,
94
+ state_batch_indices,
95
+ state_batch_indices.stride(0),
96
+ n_slots,
97
+ min(ring_start.numel(), prev_num_accepted.numel()),
98
+ logical_window,
99
+ ring_buffer_len,
100
+ pad_slot_id,
101
+ RESET=reset,
102
+ BLOCK=block,
103
+ )
104
+
105
+
106
+def reset_replayssm_ring_trackers(
107
+ ring_start: torch.Tensor,
108
+ prev_num_accepted: torch.Tensor,
109
+ state_batch_indices: torch.Tensor,
110
+ pad_slot_id: int = NULL_BLOCK_ID,
111
+) -> None:
112
+ """Reset selected ReplaySSM ring trackers."""
113
+ update_replayssm_ring_trackers(
114
+ ring_start,
115
+ prev_num_accepted,
116
+ state_batch_indices,
117
+ pad_slot_id=pad_slot_id,
118
+ )
119
+
120
+
121
class MambaSSUBackend(ABC):
122
"""Abstract base class for Mamba SSU backends."""
123
@@ -262,15 +358,115 @@ _BACKEND_REGISTRY: dict[MambaBackendEnum, type[MambaSSUBackend]] = {
358
_mamba_ssu_backend: MambaSSUBackend | None = None
359
360
361
+_flashinfer_replayssm_kernel: Callable[..., torch.Tensor] | None = None
362
+
363
+
364
+@cache
365
+def flashinfer_replayssm_autotune_supported() -> bool:
366
+ """Return True when FlashInfer exposes ReplaySSM autotuning."""
367
+ try:
368
+ from flashinfer.mamba.checkpointing_ssu import ( # noqa: F401
369
+ CheckpointingSSURunner,
370
+ )
371
+ except ImportError:
372
+ return False
373
+ return True
374
+
375
+
376
+def selective_state_update_replayssm_flashinfer(
377
+ state: torch.Tensor,
378
+ x: torch.Tensor,
379
+ dt: torch.Tensor,
380
+ A: torch.Tensor,
381
+ B: torch.Tensor,
382
+ C: torch.Tensor,
383
+ out: torch.Tensor,
384
+ x_cache: torch.Tensor,
385
+ B_cache: torch.Tensor,
386
+ dt_cache: torch.Tensor,
387
+ ring_start: torch.Tensor,
388
+ prev_num_accepted_tokens: torch.Tensor,
389
+ logical_window: int,
390
+ D: torch.Tensor | None = None,
391
+ dt_bias: torch.Tensor | None = None,
392
+ dt_softplus: bool = False,
393
+ state_batch_indices: torch.Tensor | None = None,
394
+ null_block_id: int = NULL_BLOCK_ID,
395
+ scratch: tuple[torch.Tensor, torch.Tensor, torch.Tensor] | None = None,
396
+ update_trackers: bool = True,
397
+ enable_stochastic_rounding: bool = False,
398
+ stochastic_rounding_philox_rounds: int = 0,
399
+) -> torch.Tensor:
400
+ """Run FlashInfer checkpointing SSU and optionally advance shared trackers."""
401
+ if _flashinfer_replayssm_kernel is None:
402
+ raise RuntimeError(
403
+ "FlashInfer ReplaySSM has not been initialized. "
404
+ "Call initialize_mamba_ssu_backend() with use_replayssm=True."
405
+ )
406
+
407
+ if x.dim() == 3:
408
+ x = x.unsqueeze(1)
409
+ dt = dt.unsqueeze(1)
410
+ B = B.unsqueeze(1)
411
+ C = C.unsqueeze(1)
412
+ out = out.unsqueeze(1)
413
+
414
+ indices = state_batch_indices
415
+ if indices is not None and indices.dim() > 1:
416
+ indices = indices[:, 0]
417
+
418
+ cb_scaled = cumAdt_vec = cb_old = None
419
+ if scratch is not None:
420
+ cb_scaled, cumAdt_vec, cb_old = scratch
421
+
422
+ rand_seed = (
423
+ torch.randint(0, 2**32, (1,), device=state.device, dtype=torch.int64)
424
+ if enable_stochastic_rounding
425
+ else None
426
+ )
427
+ result = _flashinfer_replayssm_kernel(
428
+ state,
429
+ x_cache,
430
+ B_cache,
431
+ dt_cache,
432
+ ring_start,
433
+ prev_num_accepted_tokens,
434
+ x,
435
+ dt,
436
+ A,
437
+ B,
438
+ C,
439
+ out,
440
+ D=D,
441
+ dt_bias=dt_bias,
442
+ dt_softplus=dt_softplus,
443
+ state_batch_indices=indices,
444
+ pad_slot_id=null_block_id,
445
+ rand_seed=rand_seed,
446
+ philox_rounds=stochastic_rounding_philox_rounds or 10,
447
+ cb_scaled=cb_scaled,
448
+ cumAdt_vec=cumAdt_vec,
449
+ cb_old=cb_old,
450
+ )
451
+ if update_trackers and indices is not None:
452
+ update_replayssm_ring_trackers(
453
+ ring_start,
454
+ prev_num_accepted_tokens,
455
+ indices,
456
+ logical_window=logical_window,
457
+ ring_buffer_len=x_cache.size(2),
458
+ pad_slot_id=null_block_id,
459
+ )
460
+ return result
461
+
462
+
463
def initialize_mamba_ssu_backend(
464
mamba_config: MambaConfig,
465
kv_cache_config: KVCacheConfig,
466
+ *,
467
+ use_replayssm: bool = False,
468
) -> None:
269
- """Initialize the global Mamba SSU backend.
270
-
271
- No-op if `kv_cache_config` contains no specs that call
272
- selective_state_update.
273
- """
469
+ """Initialize the Mamba SSU backend and optional FlashInfer ReplaySSM."""
470
if not any(
471
isinstance(g.kv_cache_spec, MambaSpec)
472
and g.kv_cache_spec.mamba_type
@@ -279,13 +475,9 @@ def initialize_mamba_ssu_backend(
475
):
476
return
477
282
- global _mamba_ssu_backend
283
-
478
+ global _flashinfer_replayssm_kernel, _mamba_ssu_backend
479
backend = mamba_config.backend
480
286
- # On CPU-only platforms (PowerPC, x86 without CUDA) Triton JIT is
287
- # unstable or unavailable. Silently fall back to the CPU
288
- # backend unless the user explicitly chose something other than "triton".
481
if backend == MambaBackendEnum.TRITON:
482
from vllm.platforms import current_platform
483
@@ -301,13 +493,28 @@ def initialize_mamba_ssu_backend(
493
f"Unknown Mamba SSU backend: {backend}. "
494
f"Valid options: {list(_BACKEND_REGISTRY.keys())}"
495
)
496
+ if use_replayssm and backend not in (
497
+ MambaBackendEnum.TRITON,
498
+ MambaBackendEnum.FLASHINFER,
499
+ ):
500
+ raise ValueError(f"ReplaySSM does not support mamba backend {backend.value!r}")
501
502
backend_cls = _BACKEND_REGISTRY[backend]
306
- if isinstance(_mamba_ssu_backend, backend_cls):
307
- return
503
+ if not isinstance(_mamba_ssu_backend, backend_cls):
504
+ _mamba_ssu_backend = backend_cls(mamba_config)
505
+ logger.info("Using %s Mamba SSU backend.", _mamba_ssu_backend.name)
506
309
- _mamba_ssu_backend = backend_cls(mamba_config)
310
- logger.info("Using %s Mamba SSU backend.", _mamba_ssu_backend.name)
507
+ _flashinfer_replayssm_kernel = None
508
+ if use_replayssm and backend == MambaBackendEnum.FLASHINFER:
509
+ try:
510
+ from flashinfer.mamba.checkpointing_ssu import checkpointing_ssu
511
+ except ImportError as e:
512
+ raise ImportError(
513
+ "FlashInfer ReplaySSM requires a compatible flashinfer-python package"
514
+ ) from e
515
+ _flashinfer_replayssm_kernel = checkpointing_ssu
516
+ if use_replayssm:
517
+ logger.info("Using %s ReplaySSM backend.", backend.value)
518
519
520
def get_mamba_ssu_backend() -> MambaSSUBackend:
vllm/model_executor/models/nemotron_h.py
+7
-5
@@ -747,7 +747,8 @@ class NemotronHForCausalLM(
747
)
748
if cache_config.use_replayssm:
749
return MambaStateDtypeCalculator.append_replayssm_ring(
750
- base_dtype, vllm_config.model_config.dtype
750
+ base_dtype,
751
+ vllm_config.model_config.dtype,
752
)
753
return base_dtype
754
@@ -784,10 +785,11 @@ class NemotronHForCausalLM(
785
)
786
if cache_config.use_replayssm:
787
return MambaStateShapeCalculator.append_replayssm_ring(
787
- base_shape,
788
- hf_config.n_groups,
789
- parallel_config.tensor_parallel_size,
790
- cache_config.replayssm_buffer_len,
788
+ base_shapes=base_shape,
789
+ n_groups=hf_config.n_groups,
790
+ tp_world_size=parallel_config.tensor_parallel_size,
791
+ logical_window=cache_config.replayssm_buffer_len,
792
+ backend=vllm_config.mamba_config.backend,
793
)
794
return base_shape
795
vllm/model_executor/warmup/kernel_warmup.py
+4
@@ -38,6 +38,9 @@ from vllm.model_executor.warmup.kimi_k3_triton_warmup import (
38
kimi_k3_triton_warmup,
39
)
40
from vllm.model_executor.warmup.qwen_triton_warmup import qwen_triton_warmup
41
+from vllm.model_executor.warmup.replayssm_warmup import (
42
+ replayssm_autotune_warmup,
43
+)
44
from vllm.platforms import current_platform
45
from vllm.utils.deep_gemm import is_deep_gemm_supported
46
from vllm.utils.flashinfer import has_flashinfer
@@ -335,6 +338,7 @@ def flashinfer_autotune(runner: "GPUModelRunner") -> None:
338
fi_utils.autotune(tune_mode=True, **autotune_kwargs),
339
):
340
_run_flashinfer_autotune_dummy_runs(runner)
341
+ replayssm_autotune_warmup(runner)
342
finally:
343
set_autotune_process_group(None)
344
vllm/model_executor/warmup/replayssm_warmup.py
new
+159
@@ -0,0 +1,159 @@
1
+# SPDX-License-Identifier: Apache-2.0
2
+# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
+
4
+from collections.abc import Iterator
5
+from contextlib import contextmanager
6
+from typing import TYPE_CHECKING, Any
7
+
8
+import torch
9
+
10
+from vllm.config.mamba import MambaBackendEnum
11
+from vllm.logger import init_logger
12
+from vllm.model_executor.layers.mamba.ops.ssu_dispatch import (
13
+ flashinfer_replayssm_autotune_supported,
14
+)
15
+
16
+if TYPE_CHECKING:
17
+ from vllm.v1.worker.gpu_model_runner import GPUModelRunner
18
+
19
+logger = init_logger(__name__)
20
+
21
+
22
+def _replayssm_autotune_kwargs(
23
+ runner: "GPUModelRunner",
24
+) -> tuple[int, dict[str, Any]] | None:
25
+ config = runner.vllm_config
26
+ if not (
27
+ config.cache_config.use_replayssm
28
+ and config.mamba_config.backend == MambaBackendEnum.FLASHINFER
29
+ ):
30
+ return None
31
+ if not flashinfer_replayssm_autotune_supported():
32
+ logger.info_once(
33
+ "Skipping FlashInfer ReplaySSM autotuning because "
34
+ "flashinfer.mamba.checkpointing_ssu.CheckpointingSSURunner "
35
+ "is unavailable."
36
+ )
37
+ return None
38
+ v2_runner: Any = runner
39
+ query_len = (
40
+ v2_runner.decode_query_len
41
+ if config.use_v2_model_runner
42
+ else runner.uniform_decode_query_len
43
+ )
44
+ max_num_reqs = min(
45
+ runner.scheduler_config.max_num_seqs,
46
+ runner.max_num_tokens // query_len,
47
+ runner.kv_cache_config.num_blocks - 1,
48
+ )
49
+ decode_kwargs = {
50
+ "num_tokens": max_num_reqs * query_len,
51
+ "skip_eplb": True,
52
+ "is_profile": True,
53
+ "randomize_inputs": True,
54
+ "uniform_decode": True,
55
+ }
56
+ if config.use_v2_model_runner:
57
+ decode_kwargs["valid_dummy_state_slots"] = True
58
+ else:
59
+ decode_kwargs.update(
60
+ allow_microbatching=False,
61
+ force_attention=True,
62
+ profile_seq_lens=query_len + 1,
63
+ )
64
+ return max_num_reqs, decode_kwargs
65
+
66
+
67
+@contextmanager
68
+def _temporary_replayssm_autotune_state(
69
+ runner: "GPUModelRunner", max_num_reqs: int
70
+) -> Iterator[None]:
71
+ from vllm.model_executor.layers.mamba.mamba_mixer2 import MambaMixer2
72
+ from vllm.model_executor.layers.mamba.ops.ssu_dispatch import (
73
+ reset_replayssm_ring_trackers,
74
+ update_replayssm_ring_trackers,
75
+ )
76
+
77
+ reset_tensors: dict[int, torch.Tensor] = {}
78
+ tracker_specs: dict[int, tuple[torch.Tensor, torch.Tensor, int, int]] = {}
79
+ for module in runner.get_model().modules():
80
+ if not isinstance(module, MambaMixer2) or not module.use_replayssm:
81
+ continue
82
+ assert module.replayssm_buffer_len is not None
83
+ ring_start = module._replayssm_ring_start
84
+ prev_num_accepted = module._replayssm_prev_num_accepted
85
+ tracker_specs.setdefault(
86
+ ring_start.data_ptr(),
87
+ (
88
+ ring_start,
89
+ prev_num_accepted,
90
+ module.replayssm_buffer_len,
91
+ module.kv_cache[2].size(2),
92
+ ),
93
+ )
94
+ tensors = (
95
+ *module.kv_cache,
96
+ ring_start,
97
+ prev_num_accepted,
98
+ )
99
+ for tensor in tensors:
100
+ if tensor.numel():
101
+ reset_tensors.setdefault(tensor.data_ptr(), tensor)
102
+
103
+ v2_runner: Any = runner
104
+ block_tables = saved_block_ids = None
105
+ if not runner.vllm_config.use_v2_model_runner:
106
+ block_tables = runner.input_batch.block_table.block_tables
107
+ saved_block_ids = tuple(
108
+ block_table.block_table.np[:max_num_reqs, 0].copy()
109
+ for block_table in block_tables
110
+ )
111
+ dummy_block_ids = range(1, max_num_reqs + 1)
112
+ for block_table in block_tables:
113
+ block_table.block_table.np[:max_num_reqs, 0] = dummy_block_ids
114
+ runner.input_batch.block_table.commit_block_table(max_num_reqs)
115
+
116
+ first_tracker = next(iter(tracker_specs.values()), None)
117
+ if first_tracker is not None and first_tracker[0].is_cuda:
118
+ state_slots = torch.arange(
119
+ 1, max_num_reqs + 1, dtype=torch.int32, device=first_tracker[0].device
120
+ )
121
+ for (
122
+ ring_start,
123
+ prev_num_accepted,
124
+ logical_window,
125
+ ring_buffer_len,
126
+ ) in tracker_specs.values():
127
+ # Compile reset (prefill) and advance (decode) before inference.
128
+ # The final reset leaves the decode tuning run in a clean state.
129
+ reset_replayssm_ring_trackers(ring_start, prev_num_accepted, state_slots)
130
+ update_replayssm_ring_trackers(
131
+ ring_start,
132
+ prev_num_accepted,
133
+ state_slots,
134
+ logical_window,
135
+ ring_buffer_len,
136
+ )
137
+ reset_replayssm_ring_trackers(ring_start, prev_num_accepted, state_slots)
138
+
139
+ try:
140
+ yield
141
+ finally:
142
+ if runner.vllm_config.use_v2_model_runner:
143
+ v2_runner.block_tables.get_dummy_block_tables(max_num_reqs)
144
+ else:
145
+ assert block_tables is not None and saved_block_ids is not None
146
+ for block_table, block_ids in zip(block_tables, saved_block_ids):
147
+ block_table.block_table.np[:max_num_reqs, 0] = block_ids
148
+ runner.input_batch.block_table.commit_block_table(max_num_reqs)
149
+ for tensor in reset_tensors.values():
150
+ tensor[1 : max_num_reqs + 1].zero_()
151
+
152
+
153
+def replayssm_autotune_warmup(runner: "GPUModelRunner") -> None:
154
+ autotune = _replayssm_autotune_kwargs(runner)
155
+ if autotune is None:
156
+ return
157
+ max_num_reqs, decode_kwargs = autotune
158
+ with _temporary_replayssm_autotune_state(runner, max_num_reqs):
159
+ runner._dummy_run(**decode_kwargs)
vllm/v1/attention/backends/mamba_attn.py
+55
-11
@@ -8,6 +8,7 @@ from typing import Any, ClassVar, TypeVar
8
import torch
9
10
from vllm.config import VllmConfig
11
+from vllm.config.mamba import MambaBackendEnum
12
from vllm.utils.math_utils import cdiv
13
from vllm.utils.torch_utils import async_tensor_h2d
14
from vllm.v1.attention.backend import (
@@ -74,12 +75,15 @@ class BaseMambaAttentionMetadata:
75
nums_dict: dict | None = None
76
batch_ptr: torch.Tensor | None = None
77
token_chunk_offset_ptr: torch.Tensor | None = None
77
- # ReplaySSM standard decode: per-row ring cursor and flush flag, plus the
78
- # per-step (decode_rows, ngroups, replayssm_buffer_len) fp32 scratch for the
79
- # precomputed k^T q products. All None when use_replayssm is disabled.
78
+ # ReplaySSM standard decode — Triton: per-row ring cursor and flush flag,
79
+ # plus (decode_rows, ngroups, replayssm_buffer_len) fp32 scratch for
80
+ # precomputed B·C products. All None when use_replayssm is disabled or
81
+ # the FlashInfer ReplaySSM backend is selected.
82
write_pos_d: torch.Tensor | None = None
83
is_flush_d: torch.Tensor | None = None
84
bc_pre_scratch: torch.Tensor | None = None
85
+ # ReplaySSM — FlashInfer checkpointing_ssu two-kernel scratch.
86
+ replayssm_scratch: tuple[torch.Tensor, torch.Tensor, torch.Tensor] | None = None
87
88
89
class BaseMambaAttentionMetadataBuilder(AttentionMetadataBuilder[M], abc.ABC):
@@ -107,6 +111,10 @@ class BaseMambaAttentionMetadataBuilder(AttentionMetadataBuilder[M], abc.ABC):
111
self.use_spec_decode = self.num_spec_tokens > 0
112
self.use_replayssm = vllm_config.cache_config.use_replayssm
113
self.replayssm_buffer_len = vllm_config.cache_config.replayssm_buffer_len
114
+ self.use_flashinfer_replayssm = (
115
+ self.use_replayssm
116
+ and vllm_config.mamba_config.backend == MambaBackendEnum.FLASHINFER
117
+ )
118
119
scheduler_config = vllm_config.scheduler_config
120
self.decode_cudagraph_max_bs: int = scheduler_config.max_num_seqs
@@ -166,9 +174,12 @@ class BaseMambaAttentionMetadataBuilder(AttentionMetadataBuilder[M], abc.ABC):
174
dtype=torch.int32,
175
device=device,
176
)
169
- # ReplaySSM standard-decode CUDA-graph buffers: per-row ring cursor,
170
- # flush flag, and the k^T q precompute scratch.
171
- if self.use_replayssm:
177
+ self.decode_bc_pre_scratch: torch.Tensor | None = None
178
+ self.decode_replayssm_scratch: (
179
+ tuple[torch.Tensor, torch.Tensor, torch.Tensor] | None
180
+ ) = None
181
+ # ReplaySSM CUDA-graph buffers for the selected backend.
182
+ if self.use_replayssm and not self.use_flashinfer_replayssm:
183
self.decode_write_pos_d: torch.Tensor = torch.empty(
184
(self.decode_cudagraph_max_bs,),
185
dtype=torch.int32,
@@ -185,7 +196,7 @@ class BaseMambaAttentionMetadataBuilder(AttentionMetadataBuilder[M], abc.ABC):
196
bc_scratch_bs = max(
197
self.decode_cudagraph_max_bs, scheduler_config.max_num_seqs
198
)
188
- self.decode_bc_pre_scratch: torch.Tensor = torch.empty(
199
+ self.decode_bc_pre_scratch = torch.empty(
200
(
201
bc_scratch_bs,
202
bc_ngroups,
@@ -194,8 +205,20 @@ class BaseMambaAttentionMetadataBuilder(AttentionMetadataBuilder[M], abc.ABC):
205
dtype=torch.float32,
206
device=device,
207
)
197
- else:
198
- self.decode_bc_pre_scratch = None
208
+ elif self.use_flashinfer_replayssm:
209
+ from flashinfer.mamba.checkpointing_ssu import (
210
+ allocate_checkpointing_ssu_scratch,
211
+ )
212
+
213
+ nheads = kv_cache_spec.shapes[2][0]
214
+ self.decode_replayssm_scratch = allocate_checkpointing_ssu_scratch(
215
+ batch_size=scheduler_config.max_num_seqs,
216
+ num_heads=nheads,
217
+ num_predicted_tokens=1,
218
+ max_window=self.replayssm_buffer_len,
219
+ dtype=vllm_config.model_config.dtype,
220
+ device=device,
221
+ )
222
223
self._init_reorder_batch_threshold(1, self.use_spec_decode)
224
if self.use_spec_decode:
@@ -500,6 +523,7 @@ class BaseMambaAttentionMetadataBuilder(AttentionMetadataBuilder[M], abc.ABC):
523
nums_dict, batch_ptr, token_chunk_offset_ptr = None, None, None
524
write_pos_d = None
525
is_flush_d = None
526
+ replayssm_scratch = None
527
528
if self.vllm_config.cache_config.mamba_cache_mode == "all":
529
num_computed_tokens = common_attn_metadata.compute_num_computed_tokens()
@@ -586,7 +610,7 @@ class BaseMambaAttentionMetadataBuilder(AttentionMetadataBuilder[M], abc.ABC):
610
num_reqs - num_prefills : num_reqs
611
]
612
589
- if self.use_replayssm and num_decodes > 0:
613
+ if self.use_replayssm and not self.use_flashinfer_replayssm and num_decodes > 0:
614
decode_base_cpu = common_attn_metadata.replayssm_decode_base_cpu
615
num_computed_tokens_cpu = common_attn_metadata._num_computed_tokens_cpu
616
if decode_base_cpu is None or num_computed_tokens_cpu is None:
@@ -651,6 +675,15 @@ class BaseMambaAttentionMetadataBuilder(AttentionMetadataBuilder[M], abc.ABC):
675
device=common_attn_metadata.query_start_loc.device,
676
)
677
678
+ if self.use_flashinfer_replayssm and num_decodes > 0:
679
+ assert self.decode_replayssm_scratch is not None
680
+ cb_scaled, cumAdt_vec, cb_old = self.decode_replayssm_scratch
681
+ replayssm_scratch = (
682
+ cb_scaled[:num_decodes],
683
+ cumAdt_vec[:num_decodes],
684
+ cb_old[:num_decodes],
685
+ )
686
+
687
bc_pre_scratch = None
688
if (
689
self.use_replayssm
@@ -671,6 +704,7 @@ class BaseMambaAttentionMetadataBuilder(AttentionMetadataBuilder[M], abc.ABC):
704
write_pos_d=write_pos_d,
705
is_flush_d=is_flush_d,
706
bc_pre_scratch=bc_pre_scratch,
707
+ replayssm_scratch=replayssm_scratch,
708
num_accepted_tokens=num_accepted_tokens,
709
query_start_loc_d=query_start_loc_d,
710
block_idx_last_scheduled_token=block_idx_last_scheduled_token,
@@ -708,6 +742,7 @@ class BaseMambaAttentionMetadataBuilder(AttentionMetadataBuilder[M], abc.ABC):
742
write_pos_d = metadata.write_pos_d
743
is_flush_d = metadata.is_flush_d
744
bc_pre_scratch = metadata.bc_pre_scratch
745
+ replayssm_scratch = metadata.replayssm_scratch
746
if (
747
metadata.num_prefills == 0
748
and metadata.num_decodes <= self.decode_cudagraph_max_bs
@@ -769,7 +804,7 @@ class BaseMambaAttentionMetadataBuilder(AttentionMetadataBuilder[M], abc.ABC):
804
)
805
block_idx_last_scheduled_token_prev_step[metadata.num_decodes :] = 0
806
772
- if self.use_replayssm:
807
+ if self.use_replayssm and not self.use_flashinfer_replayssm:
808
assert write_pos_d is not None
809
assert is_flush_d is not None
810
self.decode_write_pos_d[: metadata.num_decodes].copy_(
@@ -788,6 +823,14 @@ class BaseMambaAttentionMetadataBuilder(AttentionMetadataBuilder[M], abc.ABC):
823
824
if self.decode_bc_pre_scratch is not None:
825
bc_pre_scratch = self.decode_bc_pre_scratch[:padded_bs]
826
+ elif self.use_flashinfer_replayssm:
827
+ assert self.decode_replayssm_scratch is not None
828
+ cb_scaled, cumAdt_vec, cb_old = self.decode_replayssm_scratch
829
+ replayssm_scratch = (
830
+ cb_scaled[:padded_bs],
831
+ cumAdt_vec[:padded_bs],
832
+ cb_old[:padded_bs],
833
+ )
834
835
return replace(
836
metadata,
@@ -797,6 +840,7 @@ class BaseMambaAttentionMetadataBuilder(AttentionMetadataBuilder[M], abc.ABC):
840
write_pos_d=write_pos_d,
841
is_flush_d=is_flush_d,
842
bc_pre_scratch=bc_pre_scratch,
843
+ replayssm_scratch=replayssm_scratch,
844
block_idx_last_scheduled_token=block_idx_last_scheduled_token,
845
block_idx_last_computed_token=block_idx_last_computed_token,
846
block_idx_last_scheduled_token_prev_step=(
vllm/v1/worker/gpu/attn_utils.py
+7
-1
@@ -229,7 +229,13 @@ def init_kv_cache(
229
in ("longcat_flash", "longcat_flash_ngram")
230
else 1
231
)
232
- bind_kv_cache(kv_caches, forward_context, runner_kv_caches, num_attn_module)
232
+ bind_kv_cache(
233
+ kv_caches,
234
+ forward_context,
235
+ runner_kv_caches,
236
+ num_attn_module,
237
+ kv_cache_groups=kv_cache_config.kv_cache_groups,
238
+ )
239
return kv_caches
240
241
vllm/v1/worker/gpu/model_runner.py
+19
-3
@@ -633,7 +633,9 @@ class GPUModelRunner(LoRAModelRunnerMixin):
633
cls=self.pcp_manager_cls,
634
)
635
initialize_mamba_ssu_backend(
636
- self.vllm_config.mamba_config, self.kv_cache_config
636
+ self.vllm_config.mamba_config,
637
+ self.kv_cache_config,
638
+ use_replayssm=self.vllm_config.cache_config.use_replayssm,
639
)
640
if self.adaptive_verification is not None:
641
self.compilation_config.cudagraph_mode = CUDAGraphMode.FULL_AND_PIECEWISE
@@ -707,6 +709,7 @@ class GPUModelRunner(LoRAModelRunnerMixin):
709
context_len: int = 0,
710
skip_eplb: bool = False,
711
is_profile: bool = False,
712
+ valid_dummy_state_slots: bool = False,
713
**kwargs,
714
) -> tuple[torch.Tensor | None, torch.Tensor | None]:
715
if skip_attn and not is_profile:
@@ -764,6 +767,7 @@ class GPUModelRunner(LoRAModelRunnerMixin):
767
skip_attn_for_dummy_run=skip_attn,
768
is_profile=is_profile,
769
context_len=context_len,
770
+ valid_dummy_state_slots=valid_dummy_state_slots,
771
)
772
self.kv_connector.set_disabled(False)
773
@@ -1390,9 +1394,18 @@ class GPUModelRunner(LoRAModelRunnerMixin):
1394
return block_tables, slot_mappings
1395
1396
def prepare_dummy_attn(
1393
- self, input_batch: InputBatch
1397
+ self, input_batch: InputBatch, valid_state_slots: bool = False
1398
) -> tuple[tuple[torch.Tensor, ...], torch.Tensor]:
1399
block_tables = self.block_tables.get_dummy_block_tables(input_batch.num_reqs)
1400
+ if valid_state_slots:
1401
+ state_slots = torch.arange(
1402
+ 1,
1403
+ input_batch.num_reqs + 1,
1404
+ dtype=torch.int32,
1405
+ device=self.device,
1406
+ )
1407
+ for block_table in block_tables:
1408
+ block_table[:, 0].copy_(state_slots)
1409
slot_mappings = pcp.maybe_get_pcp_dummy_slot_mappings(
1410
self.pcp_manager, self.block_tables, input_batch.num_tokens
1411
)
@@ -1525,6 +1538,7 @@ class GPUModelRunner(LoRAModelRunnerMixin):
1538
skip_attn_for_dummy_run: bool = False,
1539
is_profile: bool = False,
1540
context_len: int = 0,
1541
+ valid_dummy_state_slots: bool = False,
1542
) -> ModelRunnerOutput | IntermediateTensors | None:
1543
if not dummy_run:
1544
# Update the request states.
@@ -1624,7 +1638,9 @@ class GPUModelRunner(LoRAModelRunnerMixin):
1638
max_query_len=batch_desc.max_query_len,
1639
)
1640
if not skip_attn_for_dummy_run:
1627
- block_tables, slot_mappings = self.prepare_dummy_attn(input_batch)
1641
+ block_tables, slot_mappings = self.prepare_dummy_attn(
1642
+ input_batch, valid_dummy_state_slots
1643
+ )
1644
if context_len:
1645
set_dummy_context(
1646
input_batch,
vllm/v1/worker/gpu_model_runner.py
+4
-1
@@ -7434,6 +7434,7 @@ class GPUModelRunner(
7434
self.compilation_config.static_forward_context,
7435
self.kv_caches,
7436
num_attn_module,
7437
+ kv_cache_groups=kv_cache_config.kv_cache_groups,
7438
)
7439
return kv_caches
7440
@@ -7485,7 +7486,9 @@ class GPUModelRunner(
7486
self.maybe_add_kv_sharing_layers_to_kv_cache_groups(kv_cache_config)
7487
self.initialize_attn_backend(kv_cache_config, is_profiling=is_profiling)
7488
initialize_mamba_ssu_backend(
7488
- self.vllm_config.mamba_config, self.kv_cache_config
7489
+ self.vllm_config.mamba_config,
7490
+ self.kv_cache_config,
7491
+ use_replayssm=self.vllm_config.cache_config.use_replayssm,
7492
)
7493
# The kernel block size for all KV cache groups. For example, if
7494
# kv_cache_manager uses block_size 256 for a given group, but the attention
vllm/v1/worker/utils.py
+6
@@ -13,6 +13,7 @@ import torch
13
from vllm.config import CacheConfig, VllmConfig
14
from vllm.logger import init_logger
15
from vllm.model_executor.layers.attention import Attention
16
+from vllm.model_executor.layers.mamba.mamba_mixer2 import share_replayssm_ring_trackers
17
from vllm.model_executor.layers.utils import warmup_rocm_skinny_gemm_workspaces
18
from vllm.model_executor.models.interfaces import MultiModalEmbeddings
19
from vllm.model_executor.models.utils import extract_layer_index
@@ -575,6 +576,7 @@ def bind_kv_cache(
576
forward_context: dict[str, Attention],
577
runner_kv_caches: list[torch.Tensor],
578
num_attn_module: int = 1,
579
+ kv_cache_groups: Sequence[KVCacheGroupSpec] | None = None,
580
) -> None:
581
"""
582
Bind the allocated KV cache to both ModelRunner and forward context so
@@ -600,6 +602,7 @@ def bind_kv_cache(
602
for layer_name in kv_caches:
603
index2name[extract_layer_index(layer_name, num_attn_module)].append(layer_name)
604
605
+ ordered_layer_names: list[str] = []
606
for layer_index in sorted(index2name.keys()):
607
layer_names = index2name[layer_index]
608
if len(layer_names) > 1:
@@ -613,6 +616,7 @@ def bind_kv_cache(
616
current_platform.check_runner_kv_caches_multi_layer()
617
for layer_name in layer_names:
618
runner_kv_caches.append(kv_caches[layer_name])
619
+ ordered_layer_names.append(layer_name)
620
621
# Bind kv_caches to forward context. Each layer's bind_kv_cache unpacks
622
# its raw allocation into the per-layer view(s) it needs (e.g. Mamba
@@ -621,6 +625,8 @@ def bind_kv_cache(
625
for layer_name, kv_cache in kv_caches.items():
626
forward_context[layer_name].bind_kv_cache(kv_cache)
627
628
+ share_replayssm_ring_trackers(ordered_layer_names, forward_context, kv_cache_groups)
629
+
630
631
def clear_layer_kv_caches(layers: Iterable[Any]) -> None:
632
"""Detach the KV/state cache tensors installed by bind_kv_cache().