[Model] Decoder-side SWA bounded replay for DeepSeek-V4.1 (#58132)
Signed-off-by: Yifan Qiao <yifanqiao@inferact.ai> Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Yifan Qiao committed
Sep 29, 2026 at 01:25 UTC
4b2e1cfa7b8094f1a9547aee41c39e05d2ce5c97
9 files changed
+1001
-60
.buildkite/test_areas/models_basic.yaml
+3
-1
@@ -66,8 +66,10 @@ steps:
66
- tests/models/transformers/test_backend.py
67
- tests/models/test_registry.py
68
- tests/models/test_deepseek_v4_vl_rocm.py
69
+ - tests/models/test_deepseek_v41_decoder_replay_layers.py
70
+ - tests/models/test_deepseek_v41_replay_batch.py
71
commands:
70
- - pytest -v -s models/test_terratorch.py models/transformers/test_backend.py models/test_registry.py models/test_deepseek_v4_vl_rocm.py
72
+ - pytest -v -s models/test_terratorch.py models/transformers/test_backend.py models/test_registry.py models/test_deepseek_v4_vl_rocm.py models/test_deepseek_v41_decoder_replay_layers.py models/test_deepseek_v41_replay_batch.py
73
mirror:
74
amd:
75
label: ":amd: (MI355 DPX) Basic Models (Other)"
tests/distributed/test_engram_dp_shard.py
+17
-16
@@ -507,20 +507,22 @@ def _check_dummy_hash_model_forward(
507
assert torch.all(hashes == engram_ops.DEAD_ID)
508
output = engram.embed(hashes[:, 0])
509
# Trailing None is previous_aux; this stub captures no aux states.
510
- return output, None, None, None, None, None
511
-
512
- model = SimpleNamespace(
513
- use_mega_moe=False,
514
- use_sequence_parallel=False,
515
- fuse_mhc_all_reduce=False,
516
- engram_hash=state,
517
- engram_swa_prefix="swa",
518
- engram_dp_shared_memory=dp_shared_memory,
519
- layers=[Decoder(engram=engram)],
520
- start_layer=0,
521
- end_layer=1,
522
- aux_hidden_state_layers=(),
523
- )
510
+ return output, hidden, hidden, hidden, hidden, None
511
+
512
+ model = model_ops.DeepseekV4Model.__new__(model_ops.DeepseekV4Model)
513
+ torch.nn.Module.__init__(model)
514
+ model.use_mega_moe = False
515
+ model.use_sequence_parallel = False
516
+ model.fuse_mhc_all_reduce = False
517
+ model.engram_hash = state
518
+ model.engram_swa_prefix = "swa"
519
+ model.engram_dp_shared_memory = dp_shared_memory
520
+ model.layers = [Decoder(engram=engram)]
521
+ model.start_layer = 0
522
+ model.end_layer = 1
523
+ model.decoder_replay_start = model.end_layer
524
+ model.decoder_replay_layers = None
525
+ model.aux_hidden_state_layers = ()
526
metadata = (
527
None
528
if dp_rank == 1
@@ -550,8 +552,7 @@ def _check_dummy_hash_model_forward(
552
dtype=torch.int32,
553
),
554
):
553
- output = model_ops.DeepseekV4Model.forward(
554
- model,
555
+ output = model(
556
torch.arange(tokens, device="cuda"),
557
torch.arange(tokens, device="cuda"),
558
intermediate_tensors=None,
tests/models/test_deepseek_v41_decoder_replay_layers.py
new
+104
@@ -0,0 +1,104 @@
1
+# SPDX-License-Identifier: Apache-2.0
2
+# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
+"""DecoderReplayLayers runs the replay layers on the forward's replay batch."""
4
+
5
+import pytest
6
+import torch
7
+
8
+from vllm.forward_context import (
9
+ ForwardContext,
10
+ get_forward_context,
11
+ override_forward_context,
12
+)
13
+from vllm.models.deepseek_v41.decoder_replay_layers import DecoderReplayLayers
14
+
15
+pytestmark = pytest.mark.skipif(not torch.cuda.is_available(), reason="needs CUDA")
16
+
17
+DEVICE = torch.device("cuda")
18
+NUM_TOKENS = 401
19
+HC, HIDDEN = 2, 3
20
+# decode (1 token), trimmed prefill (300 of 300), untrimmed prefill (100 of 300)
21
+WINDOW = 128
22
+REPLAY_ROWS = [0, *range(301 - WINDOW, 301), *range(301, 401)]
23
+
24
+
25
+def _context(metadata):
26
+ return ForwardContext(
27
+ no_compile_layers={},
28
+ attn_metadata=metadata,
29
+ slot_mapping={},
30
+ is_padding=torch.zeros(NUM_TOKENS, dtype=torch.bool, device=DEVICE),
31
+ )
32
+
33
+
34
+def _set_replay_batch(layers, rows, metadata):
35
+ layers.rows = torch.tensor(rows, device=DEVICE)
36
+ layers.forward_context = ForwardContext(
37
+ no_compile_layers={}, attn_metadata=metadata, slot_mapping={}
38
+ )
39
+
40
+
41
+def _states(num_tokens: int):
42
+ """(hidden_states, positions, input_ids, pre_mix, post_mix, res_mix, residual),
43
+ shaped as the model's: hidden states [T, H], mixes [T, HC], residual [T, HC, H]."""
44
+ hidden = torch.arange(num_tokens, device=DEVICE, dtype=torch.float32)
45
+ hidden = hidden[:, None].expand(num_tokens, HIDDEN).contiguous()
46
+ mix = hidden[:, :HC].contiguous()
47
+ residual = hidden[:, None, :].expand(num_tokens, HC, HIDDEN).contiguous()
48
+ return (
49
+ hidden,
50
+ torch.arange(num_tokens, device=DEVICE),
51
+ None,
52
+ mix + 1,
53
+ mix + 2,
54
+ mix + 3,
55
+ residual,
56
+ )
57
+
58
+
59
+def test_run_gathers_states_and_realigns_shared_indexer_buffers():
60
+ topk = torch.arange(NUM_TOKENS * 4, device=DEVICE).view(NUM_TOKENS, 4).int()
61
+ candidates = torch.arange(NUM_TOKENS * 3, device=DEVICE).view(NUM_TOKENS, 3).int()
62
+ topk_before, candidates_before = topk.clone(), candidates.clone()
63
+ seen = {}
64
+
65
+ def run_layers(hidden_states, positions, input_ids, pre_mix, post, res, residual):
66
+ seen["hidden_states"] = hidden_states.clone()
67
+ replay_context = get_forward_context()
68
+ seen["attn_metadata"] = replay_context.attn_metadata
69
+ seen["is_padding"] = replay_context.is_padding
70
+ return residual, pre_mix
71
+
72
+ layers = DecoderReplayLayers(WINDOW, run_layers, [topk, candidates])
73
+ states = _states(NUM_TOKENS)
74
+ hidden, residual = states[0], states[-1]
75
+ full, sub = object(), object()
76
+ _set_replay_batch(layers, REPLAY_ROWS, sub)
77
+ context = _context(full)
78
+ with override_forward_context(context):
79
+ outputs = layers(*states)
80
+ assert get_forward_context() is context
81
+
82
+ rows = torch.tensor(REPLAY_ROWS, device=DEVICE)
83
+ assert torch.equal(seen["hidden_states"], hidden[rows])
84
+ assert seen["attn_metadata"] is sub and seen["is_padding"] is None
85
+ assert torch.equal(topk[: len(REPLAY_ROWS)], topk_before[rows])
86
+ assert torch.equal(candidates[: len(REPLAY_ROWS)], candidates_before[rows])
87
+ assert len(outputs) == 2 and outputs[0].shape == residual.shape
88
+ assert torch.equal(outputs[0][rows], residual[rows])
89
+ assert outputs[0][1:173].abs().sum() == 0
90
+
91
+
92
+def test_no_replay_batch_runs_the_whole_batch():
93
+ seen = {}
94
+
95
+ def run_layers(hidden_states, *rest):
96
+ seen["attn_metadata"] = get_forward_context().attn_metadata
97
+ return (hidden_states,)
98
+
99
+ layers = DecoderReplayLayers(WINDOW, run_layers, [])
100
+ states = _states(NUM_TOKENS)
101
+ full = object()
102
+ with override_forward_context(_context(full)):
103
+ (output,) = layers(*states)
104
+ assert output is states[0] and seen["attn_metadata"] is full
tests/models/test_deepseek_v41_replay_batch.py
new
+259
@@ -0,0 +1,259 @@
1
+# SPDX-License-Identifier: Apache-2.0
2
+# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
+"""The decoder-side SWA bounded replay batch DeepseekV41ModelState prepares."""
4
+
5
+from dataclasses import replace
6
+from types import SimpleNamespace
7
+from unittest.mock import MagicMock
8
+
9
+import numpy as np
10
+import pytest
11
+import torch
12
+
13
+from vllm.config import CUDAGraphMode
14
+from vllm.models.deepseek_v41.nvidia.model_state import DeepseekV41ModelState
15
+from vllm.v1.worker.gpu.input_batch import InputBatch, InputBuffers
16
+from vllm.v1.worker.gpu.model_states.default import DefaultModelState
17
+
18
+pytestmark = pytest.mark.skipif(
19
+ not torch.cuda.is_available(), reason="the replay buffers live on the GPU"
20
+)
21
+
22
+WINDOW = 128
23
+DEVICE = torch.device("cuda")
24
+GROUPS = SimpleNamespace(
25
+ kv_cache_groups=[
26
+ SimpleNamespace(
27
+ layer_names=["swa"],
28
+ kv_cache_spec=SimpleNamespace(
29
+ prefix_cacheable=False, prefix_replay_tokens=WINDOW
30
+ ),
31
+ ),
32
+ SimpleNamespace(
33
+ layer_names=["mla"],
34
+ kv_cache_spec=SimpleNamespace(
35
+ prefix_cacheable=True, prefix_replay_tokens=0
36
+ ),
37
+ ),
38
+ ]
39
+)
40
+BLOCK_TABLES = (torch.zeros(4, 4, device=DEVICE),) * 2
41
+
42
+# decode (1 token), trimmed prefill (300 of 300), untrimmed prefill (100 of 300)
43
+QUERY_LENS = [1, 300, 100]
44
+SEQ_LENS = [500, 300, 300]
45
+PREFILLING = [False, True, True]
46
+REPLAY_ROWS = [0, *range(301 - WINDOW, 301), *range(301, 401)]
47
+
48
+
49
+@pytest.fixture
50
+def state(monkeypatch):
51
+ """A DeepseekV41ModelState whose attention builds are recorded, not run."""
52
+ cfg = MagicMock()
53
+ cfg.model_config.enable_prompt_embeds = False
54
+ cfg.model_config.uses_mrope = False
55
+ cfg.model_config.is_multimodal_model = False
56
+ cfg.scheduler_config.max_num_seqs = 16
57
+ cfg.scheduler_config.max_num_batched_tokens = 1024
58
+ cfg.parallel_config.data_parallel_size = 1
59
+ cfg.compilation_config.fast_moe_cold_start = False
60
+ layers = SimpleNamespace(window=WINDOW, rows=None, forward_context=None)
61
+ model = SimpleNamespace(token_lookback_depth=0, decoder_replay_layers=layers)
62
+ builds: list = []
63
+
64
+ def prepare_attn(self, input_batch, cg_mode, block_tables, slot_mappings, *a, **kw):
65
+ build = SimpleNamespace(
66
+ batch=input_batch,
67
+ cg_mode=cg_mode,
68
+ slot_mappings=slot_mappings.clone(),
69
+ replay_start=kw["model_specific_attn_metadata"].replay_start,
70
+ attn_metadata=object(),
71
+ )
72
+ builds.append(build)
73
+ return {"swa": build.attn_metadata}
74
+
75
+ monkeypatch.setattr(DefaultModelState, "prepare_attn", prepare_attn)
76
+ state = DeepseekV41ModelState(cfg, model, None, DEVICE)
77
+ state.builds = builds # type: ignore[attr-defined]
78
+ return state
79
+
80
+
81
+def _input_batch(
82
+ query_lens: list[int],
83
+ seq_lens: list[int],
84
+ is_prefilling: list[bool],
85
+ device_query_lens: list[int] | None = None,
86
+ num_tokens_after_padding: int | None = None,
87
+ max_query_len: int | None = None,
88
+) -> InputBatch:
89
+ num_reqs, num_tokens = len(query_lens), sum(query_lens)
90
+ num_padded = num_tokens_after_padding or num_tokens
91
+ batch = InputBatch.make_dummy(num_reqs, num_tokens, InputBuffers(16, 1024, DEVICE))
92
+ return replace(
93
+ batch,
94
+ num_tokens=num_tokens,
95
+ num_tokens_after_padding=num_padded,
96
+ query_start_loc=torch.tensor(
97
+ [0, *np.cumsum(device_query_lens or query_lens)],
98
+ dtype=torch.int32,
99
+ device=DEVICE,
100
+ ),
101
+ query_start_loc_np=np.array([0, *np.cumsum(query_lens)], dtype=np.int32),
102
+ seq_lens=torch.tensor(seq_lens, dtype=torch.int32, device=DEVICE),
103
+ seq_lens_cpu_upper_bound=torch.tensor(seq_lens, dtype=torch.int32),
104
+ is_prefilling_np=np.array(is_prefilling, dtype=np.bool_),
105
+ positions=torch.cat(
106
+ [torch.arange(s - q, s) for q, s in zip(query_lens, seq_lens)]
107
+ + [torch.zeros(num_padded - num_tokens, dtype=torch.int64)]
108
+ ).to(DEVICE),
109
+ is_padding=torch.arange(num_padded, device=DEVICE) >= num_tokens,
110
+ max_query_len=max_query_len,
111
+ )
112
+
113
+
114
+def _slot_mappings(num_tokens: int) -> torch.Tensor:
115
+ return torch.stack([torch.arange(num_tokens), torch.arange(num_tokens) * 10]).to(
116
+ DEVICE
117
+ )
118
+
119
+
120
+def _prepare(state, batch, cg_mode):
121
+ """Runs prepare_attn; returns the replay batch and the replay build's inputs."""
122
+ state.prepare_attn(
123
+ batch,
124
+ cg_mode,
125
+ BLOCK_TABLES,
126
+ _slot_mappings(batch.num_tokens_after_padding),
127
+ [],
128
+ GROUPS,
129
+ )
130
+ layers = state.decoder_replay_layers
131
+ if layers.rows is None:
132
+ assert layers.forward_context is None
133
+ return None, None
134
+ return SimpleNamespace(
135
+ rows=layers.rows, forward_context=layers.forward_context
136
+ ), state.builds[-1]
137
+
138
+
139
+def test_replay_batch_keeps_each_request_window(state):
140
+ state._replay_start_np[2] = 50 # the encoder-side replay start of request 2
141
+ batch = _input_batch(QUERY_LENS, SEQ_LENS, PREFILLING)
142
+ replay, build = _prepare(state, batch, CUDAGraphMode.NONE)
143
+ assert replay is not None
144
+ rows = torch.tensor(REPLAY_ROWS, device=DEVICE)
145
+ assert torch.equal(replay.rows, rows)
146
+ sub = build.batch
147
+ assert build.cg_mode == CUDAGraphMode.NONE
148
+ assert sub.query_start_loc.tolist() == [0, 1, 129, 229]
149
+ assert sub.query_start_loc_np.tolist() == [0, 1, 129, 229]
150
+ assert sub.num_tokens == sub.num_tokens_after_padding == 229
151
+ assert sub.max_query_len == WINDOW
152
+ # The trimmed request's window starts at its replay window; a higher
153
+ # encoder-side replay start stands.
154
+ assert build.replay_start.tolist() == [0, 300 - WINDOW, 50]
155
+ assert torch.equal(sub.positions, batch.positions[rows])
156
+ assert torch.equal(build.slot_mappings, _slot_mappings(401)[:, rows])
157
+ context = replay.forward_context
158
+ assert context.attn_metadata == {"swa": build.attn_metadata}
159
+ assert torch.equal(context.slot_mapping["mla"], _slot_mappings(401)[1, rows])
160
+ assert context.dp_metadata is None and context.is_padding is None
161
+
162
+
163
+def test_replay_batch_keeps_device_decode_boundaries(state):
164
+ """Adaptive verification resizes decodes on the GPU (CPU [2, 2], device
165
+ [1, 3]): their rows stay and the boundaries shift only by trimmed rows."""
166
+ batch = _input_batch(
167
+ [2, 2, 300], [10, 10, 300], PREFILLING, device_query_lens=[1, 3, 300]
168
+ )
169
+ replay, build = _prepare(state, batch, CUDAGraphMode.NONE)
170
+ assert replay is not None
171
+ assert replay.rows.tolist() == [0, 1, 2, 3, *range(304 - WINDOW, 304)]
172
+ assert build.batch.query_start_loc.tolist() == [0, 1, 4, 4 + WINDOW]
173
+
174
+
175
+def test_prompt_logprobs_requests_keep_their_rows(state):
176
+ """Every prompt row of a prompt-logprobs request is read, so it never
177
+ trims; the other prefills still do."""
178
+ state._keeps_rows_np[1] = True
179
+ batch = _input_batch([1, 300, 300], [500, 300, 300], PREFILLING)
180
+ replay, build = _prepare(state, batch, CUDAGraphMode.NONE)
181
+ assert replay is not None
182
+ assert replay.rows.tolist() == [0, *range(1, 301), *range(601 - WINDOW, 601)]
183
+ assert build.batch.max_query_len == 300
184
+
185
+
186
+def test_replay_batch_keeps_adaptive_verification_query_bound(state):
187
+ """Adaptive verification bounds the decodes' device-side query lengths
188
+ above their CPU lengths; the replay batch keeps that bound."""
189
+ batch = _input_batch(QUERY_LENS, SEQ_LENS, PREFILLING, max_query_len=200)
190
+ replay, build = _prepare(state, batch, CUDAGraphMode.NONE)
191
+ assert replay is not None and build.batch.max_query_len == 200
192
+
193
+
194
+def test_only_eager_steps_trim(state):
195
+ """A CUDA graph keeps the layers on its whole batch; an eager step trims a
196
+ padded batch to its real rows."""
197
+ batch = _input_batch(QUERY_LENS, SEQ_LENS, PREFILLING, num_tokens_after_padding=512)
198
+ for cg_mode in (CUDAGraphMode.PIECEWISE, CUDAGraphMode.FULL):
199
+ assert _prepare(state, batch, cg_mode) == (None, None)
200
+ replay, build = _prepare(state, batch, CUDAGraphMode.NONE)
201
+ assert replay is not None
202
+ assert replay.rows.tolist() == REPLAY_ROWS
203
+ assert build.cg_mode == CUDAGraphMode.NONE
204
+ assert build.batch.num_tokens_after_padding == len(REPLAY_ROWS)
205
+
206
+
207
+def test_whole_batch_forwards_get_no_replay_batch(state):
208
+ short = _input_batch([100, 100], [100, 100], [True, True])
209
+ for cg_mode in (CUDAGraphMode.NONE, CUDAGraphMode.PIECEWISE, CUDAGraphMode.FULL):
210
+ assert _prepare(state, short, cg_mode) == (None, None)
211
+ assert len(state.builds) == 3 # the batch's own metadata only
212
+ # Dummy batches, captures included (prepared as eager), are not prefills and
213
+ # keep their rows.
214
+ dummy = _input_batch(
215
+ QUERY_LENS, SEQ_LENS, [False] * 3, num_tokens_after_padding=512
216
+ )
217
+ assert _prepare(state, dummy, CUDAGraphMode.NONE) == (None, None)
218
+
219
+
220
+@pytest.fixture
221
+def dp_state(state, monkeypatch):
222
+ """`state` on DP rank 0 of 2; `state.other` sets what rank 1 reports
223
+ (trims, replay tokens)."""
224
+ import vllm.models.deepseek_v41.nvidia.model_state as module
225
+
226
+ state.vllm_config.parallel_config.data_parallel_size = 2
227
+ state.vllm_config.parallel_config.data_parallel_rank = 0
228
+ state.vllm_config.parallel_config.is_moe_model = True
229
+ state.other = (False, 0) # type: ignore[attr-defined]
230
+
231
+ def all_reduce(tensor, group):
232
+ tensor[:, 1] = torch.tensor(state.other, dtype=torch.int32)
233
+
234
+ monkeypatch.setattr(module.dist, "all_reduce", all_reduce)
235
+ monkeypatch.setattr(module, "get_dp_group", lambda: SimpleNamespace(cpu_group=None))
236
+ return state
237
+
238
+
239
+def test_dp_ranks_replay_together(dp_state):
240
+ """A trimming rank makes the others replay too, on every rank's count."""
241
+ short = _input_batch([100, 100], [100, 100], [True, True])
242
+ dp_state.other = (False, 200)
243
+ assert _prepare(dp_state, short, CUDAGraphMode.NONE) == (None, None)
244
+
245
+ dp_state.other = (True, 300) # rank 1 trims to 300 rows
246
+ replay, build = _prepare(dp_state, short, CUDAGraphMode.NONE)
247
+ assert replay is not None
248
+ assert replay.rows.tolist() == list(range(200)) # this rank keeps every row
249
+ dp_metadata = replay.forward_context.dp_metadata
250
+ assert dp_metadata.num_tokens_across_dp_cpu.tolist() == [200, 300]
251
+
252
+
253
+def test_idle_dp_rank_dummy_trims_with_its_peers(dp_state):
254
+ dummy = _input_batch([512], [512], [False])
255
+ dp_state.other = (True, 129)
256
+ replay, _ = _prepare(dp_state, dummy, CUDAGraphMode.NONE)
257
+ assert replay is not None and replay.rows.shape[0] == WINDOW
258
+ dp_metadata = replay.forward_context.dp_metadata
259
+ assert dp_metadata.num_tokens_across_dp_cpu.tolist() == [WINDOW, 129]
vllm/config/cache.py
+3
-1
@@ -242,7 +242,9 @@ class CacheConfig:
242
swa_bounded_replay: bool = True
243
"""Keep the sliding-window KV of models that support it (DeepSeek-V4.1)
244
out of prefix caching and rebuild it after a prefix hit by recomputing the
245
- hit's last window. Requires model runner V2."""
245
+ hit's last window. The layers past the last KV-source layer then also run
246
+ eager prefill steps on each request's trailing window only. Requires model
247
+ runner V2."""
248
249
kv_cache_memory_bytes: int | None = None
250
"""Size of KV Cache per GPU in bytes. By default, this is set to None
vllm/models/deepseek_v41/decoder_replay_layers.py
new
+67
@@ -0,0 +1,67 @@
1
+# SPDX-License-Identifier: Apache-2.0
2
+# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
+"""Decoder-side SWA bounded replay: running the replay layers on their batch.
4
+
5
+The layers past the last KV-source layer own nothing but sliding-window KV, so
6
+in eager prefill steps they run on each request's last ``window`` rows only.
7
+``DeepseekV41ModelState`` prepares those rows as a sub-batch with attention
8
+metadata and a forward context of its own, like a microbatch;
9
+``DecoderReplayLayers`` gathers the layer inputs by its rows, runs the layers
10
+under that context and scatters the outputs back to batch rows. Steps that run
11
+in a CUDA graph keep the layers on the whole batch, inside the graph.
12
+"""
13
+
14
+from collections.abc import Callable
15
+
16
+import torch
17
+
18
+from vllm.forward_context import ForwardContext, override_forward_context
19
+from vllm.model_executor.layers.fused_moe.moe_output import MoEOutput
20
+
21
+
22
+class DecoderReplayLayers:
23
+ """Runs the replay layers on the step's replay batch.
24
+
25
+ ``run_layers`` takes a batch's layer inputs and returns its per-row
26
+ outputs. ``row_buffers`` hold per-row results the source layer's indexer
27
+ publishes for the layers after it; they are compacted to the replay rows
28
+ in place.
29
+ """
30
+
31
+ def __init__(
32
+ self,
33
+ window: int,
34
+ run_layers: Callable[..., tuple[torch.Tensor, ...]],
35
+ row_buffers: list[torch.Tensor],
36
+ ) -> None:
37
+ self.window = window
38
+ self.run_layers = run_layers
39
+ self.row_buffers = row_buffers
40
+ # The replay batch, set by the model state every step: its rows of the
41
+ # batch and its forward context. None runs the layers on the batch.
42
+ self.rows: torch.Tensor | None = None
43
+ self.forward_context: ForwardContext | None = None
44
+
45
+ def __call__(
46
+ self, hidden_states: torch.Tensor | MoEOutput, *states: torch.Tensor | None
47
+ ) -> tuple[torch.Tensor, ...]:
48
+ rows = self.rows
49
+ if rows is None:
50
+ return self.run_layers(hidden_states, *states)
51
+ # A trimming step holds a prefill longer than the window, more tokens
52
+ # than any step whose MoE leaves its finalize to the next layer.
53
+ assert isinstance(hidden_states, torch.Tensor)
54
+ num_rows = rows.shape[0]
55
+ for buf in self.row_buffers:
56
+ buf[:num_rows].copy_(buf.index_select(0, rows))
57
+ with override_forward_context(self.forward_context):
58
+ row_outputs = self.run_layers(
59
+ hidden_states.index_select(0, rows),
60
+ *(None if t is None else t.index_select(0, rows) for t in states),
61
+ )
62
+ # The trimmed rows' outputs stay zero; nothing reads them.
63
+ num_tokens = hidden_states.shape[0]
64
+ return tuple(
65
+ out.new_zeros((num_tokens, *out.shape[1:])).index_copy_(0, rows, out)
66
+ for out in row_outputs
67
+ )
vllm/models/deepseek_v41/nvidia/model.py
+222
-40
@@ -83,6 +83,7 @@ from vllm.models.deepseek_v4.nvidia.model import (
83
prepare_mega_gate_routing_metadata,
84
)
85
from vllm.models.deepseek_v41.attention import DeepseekV4Attention
86
+from vllm.models.deepseek_v41.decoder_replay_layers import DecoderReplayLayers
87
from vllm.models.deepseek_v41.nvidia.flash_mla_mega_attn import (
88
DeepseekV4MegaAttnAttention,
89
)
@@ -741,6 +742,35 @@ class DeepseekV4Model(nn.Module, EagleModelMixin):
742
if successor.engram is None:
743
layer.ffn.defer_finalize()
744
745
+ # Decoder-side SWA bounded replay: in eager prefill steps the layers past
746
+ # the last KV source run on each request's trailing window only
747
+ # (decoder_replay_layers.py).
748
+ self.decoder_replay_layers: DecoderReplayLayers | None = None
749
+ self.decoder_replay_start = self.end_layer
750
+ cut = max(config.kv_source_layer_ids)
751
+ if (
752
+ cut < self.end_layer - 1
753
+ and self._decoder_replay_supported(vllm_config, cut)
754
+ and self.layers[cut].attn.swa_cache_layer.bounded_replay
755
+ ):
756
+ self.decoder_replay_start = cut + 1
757
+ self.decoder_replay_layers = DecoderReplayLayers(
758
+ config.sliding_window,
759
+ self._run_replay_layers,
760
+ [
761
+ buf
762
+ for buf in (self.topk_indices_buffer, self.candidate_block_buffer)
763
+ if buf is not None
764
+ ],
765
+ )
766
+ logger.info_once(
767
+ "Decoder SWA bounded replay: in eager prefill steps, layers "
768
+ "%d-%d run on each request's last %d tokens only.",
769
+ cut + 1,
770
+ self.end_layer - 1,
771
+ config.sliding_window,
772
+ )
773
+
774
# The n-gram hash needs a slot-keyed rolling store of compressed ids
775
# (chunked prefill / decode lookback); key it off the first local
776
# layer's sliding-window KV cache. Only PP ranks owning an engram
@@ -904,29 +934,28 @@ class DeepseekV4Model(nn.Module, EagleModelMixin):
934
hidden_states = sp_shard(hidden_states)
935
input_ids = sp_shard(input_ids)
936
907
- mega_gate_metadata = None
908
- if self.use_mega_moe:
909
- mega_gate_metadata = prepare_mega_gate_routing_metadata(
910
- input_ids,
911
- has_hash_routing=False,
912
- image_sentinel_base_id=IMAGE_SENTINEL_BASE_ID
913
- if getattr(self.config, "vision_n_layers", 0) > 0
914
- else None,
915
- )
916
-
937
residual, post_mix, res_mix = None, None, None
938
pre_mix: torch.Tensor | None = None
939
if not get_pp_group().is_first_rank:
940
assert intermediate_tensors is not None
941
pre_mix = intermediate_tensors["pre_mix"]
922
- # Every layer's post runs inside the next layer's fused pre, so aux
923
- # hidden states are read back from there instead of recomputed.
942
aux_hidden_by_layer: dict[int, torch.Tensor] = {}
925
- for idx, layer in enumerate(
926
- islice(self.layers, self.start_layer, self.end_layer),
927
- start=self.start_layer,
928
- ):
929
- hidden_states, residual, post_mix, res_mix, pre_mix, previous_aux = layer(
943
+ hidden_states, residual, post_mix, res_mix, pre_mix = self._run_layers(
944
+ range(self.start_layer, self.decoder_replay_start),
945
+ hidden_states,
946
+ positions,
947
+ input_ids,
948
+ pre_mix,
949
+ post_mix,
950
+ res_mix,
951
+ residual,
952
+ aux_hidden_by_layer,
953
+ engram_hashes,
954
+ engram_mask,
955
+ )
956
+ late_aux: list[torch.Tensor] = []
957
+ if self.decoder_replay_layers is not None:
958
+ hidden_states, pre_mix, *late_aux = self.decoder_replay_layers(
959
hidden_states,
960
positions,
961
input_ids,
@@ -934,36 +963,22 @@ class DeepseekV4Model(nn.Module, EagleModelMixin):
963
post_mix,
964
res_mix,
965
residual,
937
- engram_hashes,
938
- engram_mask,
939
- capture_previous_aux=idx in self.aux_hidden_state_layers,
940
- mega_gate_metadata=mega_gate_metadata,
966
)
942
- if previous_aux is not None:
943
- # idx is the one-based id of the layer whose post this is.
944
- if self.use_sequence_parallel:
945
- previous_aux = sp_all_gather(previous_aux)[:full_num_tokens]
946
- aux_hidden_by_layer[idx] = previous_aux
947
- # Without a successor boundary, the last layer finalized its own MoE.
948
- assert isinstance(hidden_states, torch.Tensor)
949
- if layer is not None:
950
- # The last layer has no successor to fold its post into.
951
- if self.fuse_mhc_all_reduce:
952
- hidden_states = tensor_model_parallel_all_reduce(hidden_states)
953
- hidden_states = mhc_post_tilelang(
954
- hidden_states, residual, post_mix, res_mix
967
+ else:
968
+ hidden_states = self._collapse(
969
+ hidden_states,
970
+ residual,
971
+ post_mix,
972
+ res_mix,
973
+ aux_hidden_by_layer,
974
+ full_num_tokens,
975
)
956
- if self.end_layer in self.aux_hidden_state_layers:
957
- final_aux = hidden_states.mean(dim=1)
958
- if self.use_sequence_parallel:
959
- final_aux = sp_all_gather(final_aux)[:full_num_tokens]
960
- aux_hidden_by_layer[self.end_layer] = final_aux
976
977
aux_hidden_states = [
978
aux_hidden_by_layer[layer_id]
979
for layer_id in self.aux_hidden_state_layers
980
if layer_id in aux_hidden_by_layer
966
- ]
981
+ ] + late_aux
982
983
if not get_pp_group().is_last_rank:
984
return IntermediateTensors(
@@ -992,6 +1007,169 @@ class DeepseekV4Model(nn.Module, EagleModelMixin):
1007
return hidden_states, aux_hidden_states
1008
return hidden_states
1009
1010
+ def _mega_gate_metadata(
1011
+ self, input_ids: torch.Tensor | None
1012
+ ) -> MegaGateRoutingMetadata | None:
1013
+ if not self.use_mega_moe:
1014
+ return None
1015
+ assert input_ids is not None
1016
+ return prepare_mega_gate_routing_metadata(
1017
+ input_ids,
1018
+ has_hash_routing=False,
1019
+ image_sentinel_base_id=IMAGE_SENTINEL_BASE_ID
1020
+ if getattr(self.config, "vision_n_layers", 0) > 0
1021
+ else None,
1022
+ )
1023
+
1024
+ def _run_layers(
1025
+ self,
1026
+ layer_ids: range,
1027
+ hidden_states: torch.Tensor | MoEOutput,
1028
+ positions: torch.Tensor,
1029
+ input_ids: torch.Tensor | None,
1030
+ pre_mix: torch.Tensor | None,
1031
+ post_mix: torch.Tensor | None,
1032
+ res_mix: torch.Tensor | None,
1033
+ residual: torch.Tensor | None,
1034
+ aux_hidden_by_layer: dict[int, torch.Tensor],
1035
+ engram_hashes: torch.Tensor | None = None,
1036
+ engram_mask: torch.Tensor | None = None,
1037
+ ) -> tuple[
1038
+ torch.Tensor | MoEOutput, torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor
1039
+ ]:
1040
+ # Every layer's post runs inside the next layer's fused pre, so aux
1041
+ # hidden states are read back from there instead of recomputed.
1042
+ full_num_tokens = positions.shape[0]
1043
+ mega_gate_metadata = self._mega_gate_metadata(input_ids)
1044
+ for idx in layer_ids:
1045
+ hidden_states, residual, post_mix, res_mix, pre_mix, previous_aux = (
1046
+ self.layers[idx](
1047
+ hidden_states,
1048
+ positions,
1049
+ input_ids,
1050
+ pre_mix,
1051
+ post_mix,
1052
+ res_mix,
1053
+ residual,
1054
+ engram_hashes,
1055
+ engram_mask,
1056
+ capture_previous_aux=idx in self.aux_hidden_state_layers,
1057
+ mega_gate_metadata=mega_gate_metadata,
1058
+ )
1059
+ )
1060
+ if previous_aux is not None:
1061
+ # idx is the one-based id of the layer whose post this is.
1062
+ if self.use_sequence_parallel:
1063
+ previous_aux = sp_all_gather(previous_aux)[:full_num_tokens]
1064
+ aux_hidden_by_layer[idx] = previous_aux
1065
+ assert residual is not None and post_mix is not None
1066
+ assert res_mix is not None and pre_mix is not None
1067
+ return hidden_states, residual, post_mix, res_mix, pre_mix
1068
+
1069
+ def _collapse(
1070
+ self,
1071
+ hidden_states: torch.Tensor | MoEOutput,
1072
+ residual: torch.Tensor,
1073
+ post_mix: torch.Tensor,
1074
+ res_mix: torch.Tensor,
1075
+ aux_hidden_by_layer: dict[int, torch.Tensor],
1076
+ full_num_tokens: int,
1077
+ ) -> torch.Tensor:
1078
+ # Without a successor boundary, the last layer finalized its own MoE.
1079
+ assert isinstance(hidden_states, torch.Tensor)
1080
+ # The last layer has no successor to fold its post into.
1081
+ if self.fuse_mhc_all_reduce:
1082
+ hidden_states = tensor_model_parallel_all_reduce(hidden_states)
1083
+ hidden_states = mhc_post_tilelang(hidden_states, residual, post_mix, res_mix)
1084
+ if self.end_layer in self.aux_hidden_state_layers:
1085
+ final_aux = hidden_states.mean(dim=1)
1086
+ if self.use_sequence_parallel:
1087
+ final_aux = sp_all_gather(final_aux)[:full_num_tokens]
1088
+ aux_hidden_by_layer[self.end_layer] = final_aux
1089
+ return hidden_states
1090
+
1091
+ def _run_replay_layers(
1092
+ self,
1093
+ hidden_states: torch.Tensor | MoEOutput,
1094
+ positions: torch.Tensor,
1095
+ input_ids: torch.Tensor | None,
1096
+ pre_mix: torch.Tensor,
1097
+ post_mix: torch.Tensor,
1098
+ res_mix: torch.Tensor,
1099
+ residual: torch.Tensor,
1100
+ ) -> tuple[torch.Tensor, ...]:
1101
+ """The layers past the last KV source, on whatever rows they are given;
1102
+ returns their output, the last FFN's pre-mix and the aux hidden states
1103
+ they capture."""
1104
+ aux_hidden_by_layer: dict[int, torch.Tensor] = {}
1105
+ hidden_states, residual, post_mix, res_mix, pre_mix = self._run_layers(
1106
+ range(self.decoder_replay_start, self.end_layer),
1107
+ hidden_states,
1108
+ positions,
1109
+ input_ids,
1110
+ pre_mix,
1111
+ post_mix,
1112
+ res_mix,
1113
+ residual,
1114
+ aux_hidden_by_layer,
1115
+ )
1116
+ hidden_states = self._collapse(
1117
+ hidden_states,
1118
+ residual,
1119
+ post_mix,
1120
+ res_mix,
1121
+ aux_hidden_by_layer,
1122
+ positions.shape[0],
1123
+ )
1124
+ return (
1125
+ hidden_states,
1126
+ pre_mix,
1127
+ *(
1128
+ aux_hidden_by_layer[layer_id]
1129
+ for layer_id in self.aux_hidden_state_layers
1130
+ if layer_id in aux_hidden_by_layer
1131
+ ),
1132
+ )
1133
+
1134
+ def _decoder_replay_supported(self, vllm_config: VllmConfig, cut: int) -> bool:
1135
+ """Whether this rank may trim the layers after ``cut``; warns when not."""
1136
+ parallel_config = vllm_config.parallel_config
1137
+ spec_config = vllm_config.speculative_config
1138
+ draft_config = spec_config.draft_model_config if spec_config else None
1139
+ draft_hf_config = getattr(draft_config, "hf_config", None)
1140
+ draft_window = getattr(draft_hf_config, "sliding_window", None)
1141
+ draft_layer_types = getattr(draft_hf_config, "layer_types", None) or ()
1142
+ window = self.config.sliding_window
1143
+ if self.start_layer > cut or self.end_layer < self.config.num_hidden_layers:
1144
+ reason = (
1145
+ "the pipeline stage holding the last KV source layer must also "
1146
+ "hold every layer after it"
1147
+ )
1148
+ elif (
1149
+ self.use_sequence_parallel
1150
+ or parallel_config.prefill_context_parallel_size > 1
1151
+ or parallel_config.use_ubatching
1152
+ ):
1153
+ reason = (
1154
+ "the replay-layer batch shrinks per rank, which sequence and "
1155
+ "prefill-context parallelism and microbatching cannot follow"
1156
+ )
1157
+ elif any(i > cut for i in getattr(self.config, "engram_layer_ids", ())):
1158
+ reason = "an Engram layer sits after the last KV source layer"
1159
+ elif draft_config is not None and (
1160
+ draft_window is None
1161
+ or draft_window > window
1162
+ or any(t != "sliding_attention" for t in draft_layer_types)
1163
+ ):
1164
+ reason = (
1165
+ f"the drafter (sliding window {draft_window}) reads hidden states "
1166
+ f"outside the target's {window}-token window"
1167
+ )
1168
+ else:
1169
+ return True
1170
+ logger.warning_once("Decoder SWA bounded replay is off: %s.", reason)
1171
+ return False
1172
+
1173
def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
1174
stacked_params_mapping = [
1175
# (param_name, shard_name, shard_id)
@@ -1397,6 +1575,10 @@ class DeepseekV41LLMForCausalLM(
1575
engram_hash = self.model.engram_hash
1576
return engram_hash.lookback_depth if engram_hash is not None else 0
1577
1578
+ @property
1579
+ def decoder_replay_layers(self) -> DecoderReplayLayers | None:
1580
+ return self.model.decoder_replay_layers
1581
+
1582
def forward(
1583
self,
1584
input_ids: torch.Tensor,
vllm/models/deepseek_v41/nvidia/model_state.py
+321
-2
@@ -1,17 +1,24 @@
1
# SPDX-License-Identifier: Apache-2.0
2
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
+from dataclasses import replace
4
from typing import Any
5
6
import numpy as np
7
import torch
8
+import torch.distributed as dist
9
import torch.nn as nn
10
11
from vllm.config import CUDAGraphMode, VllmConfig
12
+from vllm.distributed.parallel_state import get_dp_group
13
+from vllm.forward_context import DPMetadata, create_forward_context
14
+from vllm.models.deepseek_v41.decoder_replay_layers import DecoderReplayLayers
15
from vllm.triton_utils import tl, triton
16
from vllm.v1.attention.backends.mla.sparse_swa import DeepseekSparseSWAMetadataBuilder
17
from vllm.v1.attention.backends.utils import PAD_SLOT_ID
18
from vllm.v1.core.sched.output import NewRequestData
19
from vllm.v1.kv_cache_interface import KVCacheConfig
20
+from vllm.v1.worker.dp_utils import should_skip_dp_coordination
21
+from vllm.v1.worker.gpu.attn_utils import build_slot_mappings_by_layer
22
from vllm.v1.worker.gpu.buffer_utils import UvaBufferPool
23
from vllm.v1.worker.gpu.input_batch import InputBatch
24
from vllm.v1.worker.gpu.mm.encoder_cache import EncoderCache
@@ -81,6 +88,61 @@ def _pad_replayed_slots_kernel(
88
)
89
90
91
+@triton.jit
92
+def _gather_replay_batch_kernel(
93
+ query_start_loc_ptr, # [num_reqs + 1]
94
+ dropped_before_ptr, # [num_reqs + 1] rows trimmed before each boundary
95
+ seq_lens_ptr, # [num_reqs]
96
+ replay_start_ptr, # [num_reqs] the encoder-side replay start
97
+ positions_ptr,
98
+ slot_mappings_ptr, # [num_groups, num_tokens]
99
+ slot_mappings_stride,
100
+ rows_ptr, # out: [num_kept] batch row of every kept row
101
+ kept_query_start_loc_ptr, # out: [num_reqs + 1]
102
+ kept_positions_ptr, # out: [num_kept]
103
+ kept_slot_mappings_ptr, # out: [num_groups, num_kept]
104
+ kept_slot_mappings_stride,
105
+ kept_kv_start_ptr, # out: [num_reqs]
106
+ window,
107
+ NUM_GROUPS: tl.constexpr,
108
+ BLOCK: tl.constexpr,
109
+):
110
+ """One program per request: its kept rows are the last ones before its
111
+ boundary, and land at that boundary minus the rows trimmed before it."""
112
+ req = tl.program_id(0)
113
+ begin = tl.load(query_start_loc_ptr + req)
114
+ end = tl.load(query_start_loc_ptr + req + 1)
115
+ kept_begin = begin - tl.load(dropped_before_ptr + req)
116
+ kept_end = end - tl.load(dropped_before_ptr + req + 1)
117
+ if req == 0:
118
+ tl.store(kept_query_start_loc_ptr, kept_begin)
119
+ tl.store(kept_query_start_loc_ptr + req + 1, kept_end)
120
+
121
+ # A trimmed request holds no replay-layer window KV below its kept rows.
122
+ kv_start = tl.load(replay_start_ptr + req)
123
+ if kept_end - kept_begin < end - begin:
124
+ kv_start = tl.maximum(kv_start, tl.load(seq_lens_ptr + req) - window)
125
+ tl.store(kept_kv_start_ptr + req, kv_start)
126
+
127
+ first_row = end - (kept_end - kept_begin)
128
+ for tok in range(kept_begin, kept_end, BLOCK):
129
+ offs = tok + tl.arange(0, BLOCK)
130
+ mask = offs < kept_end
131
+ row = first_row + (offs - kept_begin)
132
+ tl.store(rows_ptr + offs, row.to(tl.int64), mask=mask)
133
+ tl.store(
134
+ kept_positions_ptr + offs,
135
+ tl.load(positions_ptr + row, mask=mask),
136
+ mask=mask,
137
+ )
138
+ for g in tl.static_range(NUM_GROUPS):
139
+ tl.store(
140
+ kept_slot_mappings_ptr + g * kept_slot_mappings_stride + offs,
141
+ tl.load(slot_mappings_ptr + g * slot_mappings_stride + row, mask=mask),
142
+ mask=mask,
143
+ )
144
+
145
+
146
class ReplayAttnMetadata(ModelSpecificAttnMetadata):
147
"""Hands the batch's replay starts to the sliding-window builders."""
148
@@ -111,7 +173,10 @@ class DeepseekV41ModelState(DefaultModelState):
173
``prepare_attn`` gathers those per batch, pads the replayed tokens' slots
174
in the prefix-cacheable groups so the cached KV stays as is, and hands the
175
starts to the sliding-window metadata builders, whose kernels read no
114
- window KV below them.
176
+ window KV below them. With the decoder side on
177
+ (``model.decoder_replay_layers``) it also prepares the replay layers'
178
+ batch in eager steps: each prefill's last ``window`` rows as a sub-batch
179
+ with metadata and a forward context of its own, like a microbatch.
180
"""
181
182
def __init__(
@@ -140,9 +205,40 @@ class DeepseekV41ModelState(DefaultModelState):
205
# the KV cache config on first use; None until then.
206
self._replay: tuple[int, torch.Tensor] | None = None
207
208
+ self.decoder_replay_layers: DecoderReplayLayers | None = getattr(
209
+ model, "decoder_replay_layers", None
210
+ )
211
+ if self.decoder_replay_layers is not None:
212
+ # Requests whose every prompt row is read (prompt logprobs) never trim.
213
+ self._keeps_rows_np = np.zeros(self.max_num_reqs, dtype=np.bool_)
214
+ self._dropped_before = UvaBufferPool(self.max_num_reqs + 1, torch.int32)
215
+ # The replay batch's inputs: its rows of the batch, query offsets,
216
+ # positions, slot mappings (sized with the KV cache groups on first
217
+ # use) and the position from which each request holds replay-layer
218
+ # window KV.
219
+ self._kept_rows = torch.zeros(
220
+ self.max_num_tokens, dtype=torch.int64, device=device
221
+ )
222
+ self._kept_query_start_loc = torch.zeros(
223
+ self.max_num_reqs + 1, dtype=torch.int32, device=device
224
+ )
225
+ self._kept_positions = torch.zeros(
226
+ self.max_num_tokens, dtype=torch.int64, device=device
227
+ )
228
+ self._kept_slot_mappings: torch.Tensor | None = None
229
+ self._kept_kv_start = torch.zeros(
230
+ self.max_num_reqs, dtype=torch.int32, device=device
231
+ )
232
+ self._replay_attn_groups: list[list[AttentionGroup]] | None = None
233
+
234
def add_request(self, req_index: int, new_req_data: NewRequestData) -> None:
235
super().add_request(req_index, new_req_data)
236
self._replay_start_np[req_index] = new_req_data.replay_start
237
+ if self.decoder_replay_layers is not None:
238
+ params = new_req_data.sampling_params
239
+ self._keeps_rows_np[req_index] = (
240
+ params is not None and params.prompt_logprobs is not None
241
+ )
242
243
def prepare_inputs(
244
self, input_batch: InputBatch, req_states: RequestState
@@ -196,7 +292,16 @@ class DeepseekV41ModelState(DefaultModelState):
292
device=self.device,
293
),
294
)
295
+ if self.decoder_replay_layers is not None:
296
+ self._kept_slot_mappings = torch.zeros(
297
+ len(specs),
298
+ self.max_num_tokens,
299
+ dtype=torch.int64,
300
+ device=self.device,
301
+ )
302
+ self._warm_up_replay_kernels(input_batch, slot_mappings)
303
window, cacheable_groups = self._replay
304
+ replay_start: torch.Tensor | None = None
305
if window:
306
num_reqs = input_batch.num_reqs
307
# Decode rows sit above the hit, so only prefills carry a replay
@@ -226,7 +331,7 @@ class DeepseekV41ModelState(DefaultModelState):
331
)
332
assert model_specific_attn_metadata is None
333
model_specific_attn_metadata = ReplayAttnMetadata(replay_start)
229
- return super().prepare_attn(
334
+ attn_metadata = super().prepare_attn(
335
input_batch,
336
cudagraph_mode,
337
block_tables,
@@ -237,3 +342,217 @@ class DeepseekV41ModelState(DefaultModelState):
342
ubatch_idx=ubatch_idx,
343
model_specific_attn_metadata=model_specific_attn_metadata,
344
)
345
+ if self.decoder_replay_layers is not None:
346
+ assert replay_start is not None
347
+ self._prepare_replay_batch(
348
+ input_batch,
349
+ cudagraph_mode,
350
+ block_tables,
351
+ slot_mappings,
352
+ attn_groups,
353
+ kv_cache_config,
354
+ replay_start,
355
+ )
356
+ return attn_metadata
357
+
358
+ def _warm_up_replay_kernels(
359
+ self, input_batch: InputBatch, slot_mappings: torch.Tensor
360
+ ) -> None:
361
+ """Compile the replay kernels on the buffers they run on: the startup
362
+ dummy batches never replay or trim, so they would not launch them."""
363
+ assert self._replay is not None
364
+ window, cacheable_groups = self._replay
365
+ if not window:
366
+ return
367
+ _pad_replayed_slots_kernel.warmup(
368
+ slot_mappings,
369
+ slot_mappings.stride(0),
370
+ cacheable_groups,
371
+ input_batch.query_start_loc,
372
+ input_batch.positions,
373
+ self._replay_start,
374
+ window,
375
+ PAD_SLOT_ID,
376
+ NUM_GROUPS=cacheable_groups.numel(),
377
+ BLOCK=1024,
378
+ grid=(1,),
379
+ )
380
+ if self.decoder_replay_layers is None:
381
+ return
382
+ assert self._kept_slot_mappings is not None
383
+ _gather_replay_batch_kernel.warmup(
384
+ input_batch.query_start_loc,
385
+ self._dropped_before.copy_to_uva(np.zeros(1, dtype=np.int32)),
386
+ input_batch.seq_lens,
387
+ self._replay_start,
388
+ input_batch.positions,
389
+ slot_mappings,
390
+ slot_mappings.stride(0),
391
+ self._kept_rows,
392
+ self._kept_query_start_loc,
393
+ self._kept_positions,
394
+ self._kept_slot_mappings,
395
+ self._kept_slot_mappings.stride(0),
396
+ self._kept_kv_start,
397
+ self.decoder_replay_layers.window,
398
+ NUM_GROUPS=slot_mappings.shape[0],
399
+ BLOCK=1024,
400
+ grid=(1,),
401
+ )
402
+
403
+ def _prepare_replay_batch(
404
+ self,
405
+ input_batch: InputBatch,
406
+ cudagraph_mode: CUDAGraphMode,
407
+ block_tables: tuple[torch.Tensor, ...],
408
+ slot_mappings: torch.Tensor,
409
+ attn_groups: list[list[AttentionGroup]],
410
+ kv_cache_config: KVCacheConfig,
411
+ replay_start: torch.Tensor,
412
+ ) -> None:
413
+ """Set the replay layers' batch for this forward, or none when nothing
414
+ trims. Only eager steps trim: a CUDA graph keeps the layers on the whole
415
+ batch, and ranks share the graph mode. Under data parallelism every rank
416
+ replays if any does."""
417
+ layers = self.decoder_replay_layers
418
+ assert layers is not None
419
+ layers.rows = layers.forward_context = None
420
+ if cudagraph_mode != CUDAGraphMode.NONE:
421
+ return
422
+
423
+ num_reqs = input_batch.num_reqs
424
+ query_lens = np.diff(input_batch.query_start_loc_np[: num_reqs + 1])
425
+ kept_lens = np.where(
426
+ self._keeps_rows_np[input_batch.idx_mapping_np[:num_reqs]],
427
+ query_lens,
428
+ np.minimum(query_lens, layers.window),
429
+ )
430
+ # Dummy batches (captures, an idle DP rank's) are not prefills: they
431
+ # keep their rows unless a rank trims.
432
+ trims = bool(
433
+ (kept_lens < query_lens)[input_batch.is_prefilling_np[:num_reqs]].any()
434
+ )
435
+ num_tokens = int(kept_lens.sum())
436
+ dp_metadata = None
437
+ if self.vllm_config.parallel_config.data_parallel_size > 1:
438
+ trims, dp_metadata = self._agree_across_dp(trims, num_tokens)
439
+ if not trims:
440
+ return
441
+
442
+ kept_batch, kept_slot_mappings = self._kept_input_batch(
443
+ input_batch, slot_mappings, replay_start, kept_lens
444
+ )
445
+ attn_metadata = super().prepare_attn(
446
+ kept_batch,
447
+ CUDAGraphMode.NONE,
448
+ block_tables,
449
+ kept_slot_mappings,
450
+ self._replay_groups(attn_groups),
451
+ kv_cache_config,
452
+ model_specific_attn_metadata=ReplayAttnMetadata(
453
+ self._kept_kv_start[:num_reqs]
454
+ ),
455
+ )
456
+ layers.rows = self._kept_rows[:num_tokens]
457
+ layers.forward_context = create_forward_context(
458
+ attn_metadata,
459
+ self.vllm_config,
460
+ dp_metadata=dp_metadata,
461
+ slot_mapping=build_slot_mappings_by_layer(
462
+ kept_slot_mappings, kv_cache_config
463
+ ),
464
+ )
465
+
466
+ def _kept_input_batch(
467
+ self,
468
+ input_batch: InputBatch,
469
+ slot_mappings: torch.Tensor,
470
+ replay_start: torch.Tensor,
471
+ kept_lens: np.ndarray,
472
+ ) -> tuple[InputBatch, torch.Tensor]:
473
+ """Build the sub-``InputBatch`` of each request's last ``kept_lens``
474
+ rows, and its slot mappings.
475
+
476
+ The kept rows follow the device boundaries minus the rows trimmed before
477
+ them: adaptive verification resizes the decodes on the GPU alone, and
478
+ decodes never trim. Like a microbatch's, the sub-batch describes the
479
+ forward only; its sampling fields are carried over and must not be read.
480
+ """
481
+ num_reqs = input_batch.num_reqs
482
+ num_tokens = int(kept_lens.sum())
483
+ kept_query_start_loc_np = np.zeros(num_reqs + 1, dtype=np.int32)
484
+ np.cumsum(kept_lens, out=kept_query_start_loc_np[1:])
485
+ dropped_before = self._dropped_before.copy_to_uva(
486
+ input_batch.query_start_loc_np[: num_reqs + 1] - kept_query_start_loc_np
487
+ )
488
+ assert self._kept_slot_mappings is not None
489
+ kept_slot_mappings = self._kept_slot_mappings[:, :num_tokens]
490
+ _gather_replay_batch_kernel[(num_reqs,)](
491
+ input_batch.query_start_loc,
492
+ dropped_before,
493
+ input_batch.seq_lens,
494
+ replay_start,
495
+ input_batch.positions,
496
+ slot_mappings,
497
+ slot_mappings.stride(0),
498
+ self._kept_rows,
499
+ self._kept_query_start_loc,
500
+ self._kept_positions,
501
+ kept_slot_mappings,
502
+ kept_slot_mappings.stride(0),
503
+ self._kept_kv_start,
504
+ self.decoder_replay_layers.window, # type: ignore[union-attr]
505
+ NUM_GROUPS=slot_mappings.shape[0],
506
+ BLOCK=1024,
507
+ )
508
+ # Adaptive verification may lengthen the decodes on the device, up to
509
+ # the batch's bound.
510
+ max_query_len = max(int(kept_lens.max()), input_batch.max_query_len or 0)
511
+ kept_batch = replace(
512
+ input_batch,
513
+ num_tokens=num_tokens,
514
+ num_tokens_after_padding=num_tokens,
515
+ query_start_loc=self._kept_query_start_loc[: num_reqs + 1],
516
+ query_start_loc_np=kept_query_start_loc_np,
517
+ max_query_len=max_query_len,
518
+ positions=self._kept_positions[:num_tokens],
519
+ fast_prefill=None,
520
+ )
521
+ return kept_batch, kept_slot_mappings
522
+
523
+ def _agree_across_dp(
524
+ self, trims: bool, num_tokens: int
525
+ ) -> tuple[bool, DPMetadata | None]:
526
+ """Whether any rank trims and, then, the replay layers' DP metadata from
527
+ every rank's replay token count."""
528
+ parallel_config = self.vllm_config.parallel_config
529
+ agreed = torch.zeros(2, parallel_config.data_parallel_size, dtype=torch.int32)
530
+ agreed[:, parallel_config.data_parallel_rank] = torch.tensor(
531
+ [trims, num_tokens]
532
+ )
533
+ if not should_skip_dp_coordination():
534
+ dist.all_reduce(agreed, group=get_dp_group().cpu_group)
535
+ if not agreed[0].any():
536
+ return False, None
537
+ return True, DPMetadata.make(parallel_config, num_tokens, agreed[1])
538
+
539
+ def _replay_groups(
540
+ self, attn_groups: list[list[AttentionGroup]]
541
+ ) -> list[list[AttentionGroup]]:
542
+ """The attention groups with metadata builders of their own, like each
543
+ microbatch's: a builder keeps the metadata it built, and the runner's
544
+ hold the batch's."""
545
+ if self._replay_attn_groups is None:
546
+ self._replay_attn_groups = []
547
+ for groups in attn_groups:
548
+ replay_groups = []
549
+ for group in groups:
550
+ replay_group = replace(group, metadata_builders=[])
551
+ replay_group.create_metadata_builders(
552
+ self.vllm_config,
553
+ self.device,
554
+ group.metadata_builders[0].kernel_block_size,
555
+ )
556
+ replay_groups.append(replay_group)
557
+ self._replay_attn_groups.append(replay_groups)
558
+ return self._replay_attn_groups
vllm/models/deepseek_v41/nvidia/vl_model.py
+5
@@ -51,6 +51,7 @@ from ..common.mm_preprocess import (
51
DeepseekV4VLProcessingInfo,
52
)
53
from ..common.vl_cudagraph import DeepseekV4VLEncoderCudaGraphMixin
54
+from ..decoder_replay_layers import DecoderReplayLayers
55
from .model import (
56
DeepseekV41LLMForCausalLM,
57
_linear_scale_param_name,
@@ -281,6 +282,10 @@ class DeepseekV41ForCausalLM(
282
def token_lookback_depth(self) -> int:
283
return self.language_model.token_lookback_depth
284
285
+ @property
286
+ def decoder_replay_layers(self) -> DecoderReplayLayers | None:
287
+ return self.language_model.decoder_replay_layers
288
+
289
def forward(
290
self,
291
input_ids: torch.Tensor,