A high-throughput and memory-efficient inference and serving engine for LLMs
Mirrored from https://github.com/tduncan-lum/Nvidia-Optimization-Stack---VLLM.git
Synced Nov 14, 05:30 UTC
[V1] - Split Prefill and Decode for Mamba1 models (#22653)
Signed-off-by: amirk <amirk@ai21.com> Signed-off-by: asafg <asafg@ai21.com> Co-authored-by: asafg <asafg@ai21.com> Co-authored-by: Asaf Joseph Gardin <39553475+Josephasafg@users.noreply.github.com>
amirai21 committed
Aug 15, 2025 at 11:59 UTC
fe91ce9591760e3b58fc24845fa80364fbcdd07f
3 files changed
+253
-95
tests/models/language/generation/test_hybrid.py
+13
@@ -57,6 +57,13 @@ V1_SUPPORTED_MODELS = [
57
# Avoid OOM
58
MAX_NUM_SEQS = 4
59
60
+# Once we add support for FCG in Mamba1, this list will be removed and tests
61
+# all test cases will use enforce_eager=False
62
+ENFORCE_EAGER_MODELS_V1 = [
63
+ "state-spaces/mamba-130m-hf",
64
+ "ai21labs/Jamba-tiny-dev",
65
+]
66
+
67
68
@pytest.mark.parametrize("model", SSM_MODELS + HYBRID_MODELS)
69
@pytest.mark.parametrize("max_tokens", [64])
@@ -94,13 +101,19 @@ def test_models(
101
example_prompts, max_tokens, num_logprobs)
102
103
if model in V1_SUPPORTED_MODELS:
104
+ enforce_eager = False
105
with monkeypatch.context() as m:
106
m.setenv("VLLM_USE_V1", "1")
107
if model in HYBRID_MODELS:
108
# required due to reorder_batch behaviour
109
m.setenv("VLLM_ATTENTION_BACKEND", "FLASHINFER")
110
+
111
+ if model in ENFORCE_EAGER_MODELS_V1:
112
+ enforce_eager = True
113
+
114
with vllm_runner(model,
115
max_num_seqs=MAX_NUM_SEQS,
116
+ enforce_eager=enforce_eager,
117
enable_prefix_caching=False) as vllm_model:
118
vllm_v1_outputs = vllm_model.generate_greedy_logprobs(
119
example_prompts, max_tokens, num_logprobs)
vllm/model_executor/layers/mamba/mamba_mixer.py
+219
-90
@@ -1,13 +1,14 @@
1
# SPDX-License-Identifier: Apache-2.0
2
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4
-from typing import Optional
4
+from typing import NamedTuple, Optional
5
6
import torch
7
from torch import nn
8
from torch.nn.parameter import Parameter
9
10
from vllm import envs
11
+from vllm.attention.backends.abstract import AttentionMetadata
12
from vllm.config import get_current_vllm_config
13
from vllm.distributed.parallel_state import (
14
get_tensor_model_parallel_rank, get_tensor_model_parallel_world_size)
@@ -154,13 +155,38 @@ class MambaMixer(MambaBase, CustomOp):
155
156
self.prefix = prefix
157
158
+ def _ssm_transform(
159
+ self, x: torch.Tensor
160
+ ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
161
+ if self.is_lora_enabled:
162
+ # Lora kernel requires contiguous tensor.
163
+ ssm_params = self.x_proj(x.contiguous())[0]
164
+ else:
165
+ ssm_params = self.x_proj(x)[0]
166
+ time_step, B, C = torch.split(
167
+ ssm_params,
168
+ [self.time_step_rank, self.ssm_state_size, self.ssm_state_size],
169
+ dim=-1)
170
+ if self.use_rms_norm:
171
+ assert self.dt_layernorm is not None
172
+ assert self.b_layernorm is not None
173
+ assert self.c_layernorm is not None
174
+ time_step = self.dt_layernorm(time_step.contiguous())
175
+ B = self.b_layernorm(B.contiguous())
176
+ C = self.c_layernorm(C.contiguous())
177
+ discrete_time_step = self.dt_proj(time_step)[0].transpose(-2, -1)
178
+ return discrete_time_step, B, C
179
+
180
def forward(self,
181
hidden_states: torch.Tensor,
182
mamba_cache_params: Optional[MambaCacheParams] = None):
183
if not envs.VLLM_USE_V1:
184
return CustomOp.forward(self, hidden_states, mamba_cache_params)
185
else:
163
- return self.forward_cuda(hidden_states, mamba_cache_params)
186
+ return self.forward_cuda(
187
+ hidden_states,
188
+ mamba_cache_params,
189
+ )
190
191
def forward_native(self,
192
hidden_states: torch.Tensor,
@@ -170,6 +196,27 @@ class MambaMixer(MambaBase, CustomOp):
196
def forward_cuda(self,
197
hidden_states: torch.Tensor,
198
mamba_cache_params: Optional[MambaCacheParams] = None):
199
+ """
200
+ Run the Mamba-1 SSM pipeline.
201
+
202
+ Steps
203
+ -----
204
+ 1. Apply the gated-MLP linear projection to the raw input.
205
+ 2. Pass the projected sequence through the convolutional mixing layer.
206
+ 3. Feed the result into the State-Space Model (SSM) blocks.
207
+ 4. Perform the recurrence y ← SSM(A, B, C, Δ)(x)
208
+ to produce contextual representations.
209
+ 5. Project the contextualised sequence back
210
+ to the output embedding dimension.
211
+
212
+ Batch handling
213
+ --------------
214
+ Prefill and decode tokens are processed by dedicated CUDA
215
+ kernels for both the convolutional (conv1d) and SSM stages.
216
+ In the case of a mixed batch (containing both prefill and
217
+ decode tokens), both sets of kernels are executed independently
218
+ and their outputs are concatenated before the final output projection.
219
+ """
220
221
forward_context: ForwardContext = get_forward_context()
222
attn_metadata = forward_context.attn_metadata
@@ -185,126 +232,142 @@ class MambaMixer(MambaBase, CustomOp):
232
self_kv_cache = self.kv_cache[forward_context.virtual_engine]
233
conv_state = self_kv_cache[0].transpose(-1, -2)
234
ssm_state = self_kv_cache[1]
188
- has_initial_state = mamba1_metadata.has_initial_states
189
- context_lens_tensor = mamba1_metadata.context_lens_tensor
235
+ has_initial_states = mamba1_metadata.has_initial_states
236
else:
237
+ assert isinstance(attn_metadata, AttentionMetadata)
238
assert mamba_cache_params is not None
239
conv_state = mamba_cache_params.conv_state
240
ssm_state = mamba_cache_params.ssm_state
241
state_indices_tensor = mamba_cache_params.state_indices_tensor
242
query_start_loc = attn_metadata.query_start_loc
243
context_lens_tensor = attn_metadata.context_lens_tensor
197
-
244
+ has_initial_states = None
245
if context_lens_tensor is not None:
199
- has_initial_state = context_lens_tensor > 0
246
+ has_initial_states = context_lens_tensor > 0
247
248
# 1. Gated MLP's linear projection
249
projected_states = self.in_proj(hidden_states)[0].transpose(-2, -1)
203
- hidden_states, gate = projected_states.chunk(2, dim=-2)
250
+ hidden_states_BC, gate = projected_states.chunk(2, dim=-2)
251
205
- # 2. Convolution sequence transformation
252
conv_weights = self.conv1d.weight.view(self.conv1d.weight.size(0),
253
self.conv1d.weight.size(2))
254
255
if envs.VLLM_USE_V1 and attn_metadata is None:
256
# V1 profile run
211
- hidden_states = hidden_states.contiguous()
212
- return self.out_proj(hidden_states.transpose(-2, -1))[0]
213
-
214
- if query_start_loc is not None and context_lens_tensor is not None:
215
- # |---------- N-1 iteration --------|
216
- # |---------------- N iteration ---------------------|
217
- # |- tokenA -|......................|-- newTokens ---|
218
- # |---------- context_len ----------|
219
- # |-------------------- seq_len ---------------------|
220
- # |-- query_len ---|
221
- hidden_states = causal_conv1d_fn(
222
- hidden_states,
257
+ hidden_states_BC = hidden_states_BC.contiguous()
258
+ return self.out_proj(hidden_states_BC.transpose(-2, -1))[0]
259
+
260
+ num_prefill_tokens = attn_metadata.num_prefill_tokens # token count
261
+ num_decode_tokens = attn_metadata.num_decode_tokens
262
+ num_prefills = attn_metadata.num_prefills # request count
263
+ num_decodes = attn_metadata.num_decode_tokens # token count (=request)
264
+ has_prefill = num_prefill_tokens > 0
265
+ has_decode = num_decode_tokens > 0
266
+
267
+ prefill_decode_split = split_batch_to_prefill_and_decode(
268
+ hidden_states_BC,
269
+ gate,
270
+ state_indices_tensor,
271
+ query_start_loc,
272
+ has_initial_states,
273
+ num_prefill_tokens,
274
+ num_decode_tokens,
275
+ num_prefills,
276
+ num_decodes,
277
+ )
278
+ hidden_states_BC_p = prefill_decode_split.hidden_states_BC_p
279
+ hidden_states_BC_d = prefill_decode_split.hidden_states_BC_d
280
+ gate_p = prefill_decode_split.gate_p
281
+ gate_d = prefill_decode_split.gate_d
282
+ state_indices_tensor_p = prefill_decode_split.state_indices_tensor_p
283
+ state_indices_tensor_d = prefill_decode_split.state_indices_tensor_d
284
+ query_start_loc_p = prefill_decode_split.query_start_loc_p
285
+ has_initial_states_p = prefill_decode_split.has_initial_states_p
286
+
287
+ ssm_outputs = []
288
+
289
+ if has_prefill:
290
+ # 2. Convolution sequence transformation
291
+ conv_out_p = causal_conv1d_fn(
292
+ hidden_states_BC_p,
293
conv_weights,
224
- bias=self.conv1d.bias,
294
+ self.conv1d.bias,
295
activation=self.activation,
296
conv_states=conv_state,
227
- has_initial_state=has_initial_state,
228
- cache_indices=state_indices_tensor,
229
- query_start_loc=query_start_loc)
230
- else:
231
- hidden_states = causal_conv1d_update(
232
- hidden_states.transpose(0, 1),
297
+ has_initial_state=has_initial_states_p,
298
+ cache_indices=state_indices_tensor_p,
299
+ query_start_loc=query_start_loc_p)
300
+ # 3. State Space Model sequence transformations.
301
+ discrete_time_step_p, B_p, C_p = self._ssm_transform(
302
+ conv_out_p.transpose(-2, -1))
303
+ time_proj_bias = self._time_proj_bias()
304
+
305
+ # 4. Perform the recurrence y ← SSM(A, B, C, Δ)(x)
306
+ scan_out_p = selective_scan_fn(
307
+ conv_out_p,
308
+ ssm_state,
309
+ discrete_time_step_p,
310
+ self.A,
311
+ B_p.transpose(-2, -1),
312
+ C_p.transpose(-2, -1),
313
+ self.D.float(),
314
+ gate_p,
315
+ time_proj_bias,
316
+ delta_softplus=True,
317
+ cache_indices=state_indices_tensor_p,
318
+ has_initial_state=has_initial_states_p,
319
+ query_start_loc=query_start_loc_p)
320
+ ssm_outputs.append(scan_out_p)
321
+
322
+ if has_decode:
323
+ # 2. Convolution sequence transformation
324
+ conv_out_d = causal_conv1d_update(
325
+ hidden_states_BC_d.transpose(0, 1),
326
conv_state,
327
conv_weights,
328
self.conv1d.bias,
329
self.activation,
237
- conv_state_indices=state_indices_tensor)
238
- hidden_states = hidden_states.transpose(0, 1)
330
+ conv_state_indices=state_indices_tensor_d).transpose(0, 1)
331
240
- # 3. State Space Model sequence transformation
241
- # 3.a. input varying initialization of time_step, B and C
332
+ # 3. State Space Model sequence transformation.
333
+ discrete_time_step_d, B_d, C_d = self._ssm_transform(
334
+ conv_out_d.transpose(-2, -1))
335
+ time_proj_bias = self._time_proj_bias()
336
243
- if self.is_lora_enabled:
244
- # lora kernel requires contiguous tensor
245
- ssm_parameters = self.x_proj(
246
- hidden_states.transpose(-2, -1).contiguous())[0]
247
- else:
248
- ssm_parameters = self.x_proj(hidden_states.transpose(-2, -1))[0]
249
-
250
- time_step, B, C = torch.split(
251
- ssm_parameters,
252
- [self.time_step_rank, self.ssm_state_size, self.ssm_state_size],
253
- dim=-1,
254
- )
255
- if self.use_rms_norm:
256
- assert self.dt_layernorm is not None
257
- assert self.b_layernorm is not None
258
- assert self.c_layernorm is not None
259
- time_step = self.dt_layernorm(time_step.contiguous())
260
- B = self.b_layernorm(B.contiguous())
261
- C = self.c_layernorm(C.contiguous())
262
-
263
- discrete_time_step = self.dt_proj(time_step)[0].transpose(-2, -1)
264
- # 3.c perform the recurrence y ← SSM(A, B, C)(x)
265
- time_proj_bias = (self.dt_proj.bias.float() if hasattr(
266
- self.dt_proj, "bias") else None)
267
-
268
- if query_start_loc is not None and context_lens_tensor is not None:
269
- scan_outputs = selective_scan_fn(
270
- hidden_states,
271
- ssm_state,
272
- discrete_time_step,
273
- self.A,
274
- B.transpose(-2, -1),
275
- C.transpose(-2, -1),
276
- self.D.float(),
277
- gate,
278
- time_proj_bias,
279
- delta_softplus=True,
280
- cache_indices=state_indices_tensor,
281
- has_initial_state=has_initial_state,
282
- query_start_loc=query_start_loc)
283
- else:
284
- scan_outputs = torch.empty_like(hidden_states.transpose(0, 1))
337
+ # 4. Perform the recurrence y ← SSM(A, B, C, Δ)(x)
338
+ scan_outputs_d = torch.empty_like(
339
+ hidden_states_BC_d.transpose(0, 1))
340
selective_state_update(ssm_state,
286
- hidden_states.transpose(0, 1),
287
- discrete_time_step.transpose(0, 1),
341
+ conv_out_d.transpose(0, 1),
342
+ discrete_time_step_d.transpose(0, 1),
343
self.A,
289
- B,
290
- C,
344
+ B_d,
345
+ C_d,
346
self.D,
292
- gate.transpose(0, 1),
347
+ gate_d.transpose(0, 1),
348
time_proj_bias,
349
dt_softplus=True,
295
- state_batch_indices=state_indices_tensor,
296
- out=scan_outputs)
297
- scan_outputs = scan_outputs.transpose(0, 1)
298
-
299
- # 4. Final linear projection
300
- if self.is_lora_enabled:
301
- # lora kernel requires contiguous tensor
302
- contextualized_states = self.out_proj(
303
- scan_outputs.transpose(-2, -1).contiguous())[0]
350
+ state_batch_indices=state_indices_tensor_d,
351
+ out=scan_outputs_d)
352
+ scan_outputs_d = scan_outputs_d.transpose(0, 1)
353
+
354
+ if envs.VLLM_USE_V1:
355
+ ssm_outputs.insert(0, scan_outputs_d)
356
+ else:
357
+ ssm_outputs.append(scan_outputs_d)
358
+
359
+ scan_outputs_combined = ssm_outputs[0] if len(
360
+ ssm_outputs) == 1 else torch.cat(ssm_outputs, dim=-1)
361
+
362
+ # 5. Final output projection
363
+ if self.is_lora_enabled: # Lora kernel requires contiguous tensor.
364
+ scan_outputs_combined = scan_outputs_combined.transpose(
365
+ -2, -1).contiguous()
366
+ out = self.out_proj(scan_outputs_combined)[0]
367
else:
305
- contextualized_states = self.out_proj(
306
- scan_outputs.transpose(-2, -1))[0]
307
- return contextualized_states
368
+ out = self.out_proj(scan_outputs_combined.transpose(-2, -1))[0]
369
+
370
+ return out
371
372
def get_state_shape(self) -> tuple[tuple[int, ...], tuple[int, ...]]:
373
return MambaStateShapeCalculator.mamba1_state_shape(
@@ -317,3 +380,69 @@ class MambaMixer(MambaBase, CustomOp):
380
@property
381
def mamba_type(self) -> str:
382
return "mamba1"
383
+
384
+ def _time_proj_bias(self) -> Optional[torch.Tensor]:
385
+ if hasattr(self.dt_proj, "bias") and self.dt_proj.bias is not None:
386
+ return self.dt_proj.bias.float()
387
+ return None
388
+
389
+
390
+class PrefillDecodeSplit(NamedTuple):
391
+ hidden_states_BC_p: torch.Tensor
392
+ hidden_states_BC_d: torch.Tensor
393
+ gate_p: torch.Tensor
394
+ gate_d: torch.Tensor
395
+ state_indices_tensor_p: torch.Tensor
396
+ state_indices_tensor_d: torch.Tensor
397
+ query_start_loc_p: Optional[torch.Tensor]
398
+ has_initial_states_p: Optional[torch.Tensor]
399
+
400
+
401
+def split_batch_to_prefill_and_decode(
402
+ hidden_states_BC: torch.Tensor,
403
+ gate: torch.Tensor,
404
+ state_indices_tensor: torch.Tensor,
405
+ query_start_loc: torch.Tensor,
406
+ has_initial_states: Optional[torch.Tensor],
407
+ num_prefill_tokens: int,
408
+ num_decode_tokens: int,
409
+ num_prefills: int,
410
+ num_decodes: int,
411
+) -> PrefillDecodeSplit:
412
+ if envs.VLLM_USE_V1:
413
+ # In v1, decode tokens come first, then prefill tokens.
414
+ hidden_states_BC_d, hidden_states_BC_p = torch.split(
415
+ hidden_states_BC, [num_decode_tokens, num_prefill_tokens], dim=-1)
416
+ gate_d, gate_p = torch.split(gate,
417
+ [num_decode_tokens, num_prefill_tokens],
418
+ dim=-1)
419
+ state_indices_tensor_d, state_indices_tensor_p = torch.split(
420
+ state_indices_tensor, [num_decodes, num_prefills], dim=0)
421
+ query_start_loc_p = (query_start_loc[-num_prefills - 1:] -
422
+ num_decodes if num_prefills > 0 else None)
423
+ has_initial_states_p = has_initial_states[-num_prefills:] if (
424
+ has_initial_states is not None and num_prefills > 0) else None
425
+ else:
426
+ # In v0, prefill tokens come first, then decode tokens.
427
+ hidden_states_BC_p, hidden_states_BC_d = torch.split(
428
+ hidden_states_BC, [num_prefill_tokens, num_decode_tokens], dim=-1)
429
+ gate_p, gate_d = torch.split(gate,
430
+ [num_prefill_tokens, num_decode_tokens],
431
+ dim=-1)
432
+ state_indices_tensor_p, state_indices_tensor_d = torch.split(
433
+ state_indices_tensor, [num_prefills, num_decodes], dim=0)
434
+ query_start_loc_p = (query_start_loc[:num_prefills +
435
+ 1] if num_prefills > 0 else None)
436
+ has_initial_states_p = has_initial_states[:num_prefills] if (
437
+ has_initial_states is not None and num_prefills > 0) else None
438
+
439
+ return PrefillDecodeSplit(
440
+ hidden_states_BC_p=hidden_states_BC_p,
441
+ hidden_states_BC_d=hidden_states_BC_d,
442
+ gate_p=gate_p,
443
+ gate_d=gate_d,
444
+ state_indices_tensor_p=state_indices_tensor_p,
445
+ state_indices_tensor_d=state_indices_tensor_d,
446
+ query_start_loc_p=query_start_loc_p,
447
+ has_initial_states_p=has_initial_states_p,
448
+ )
vllm/v1/attention/backends/mamba1_attn.py
+21
-5
@@ -2,14 +2,15 @@
2
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4
from dataclasses import dataclass
5
-from typing import ClassVar
5
+from typing import ClassVar, Optional
6
7
import torch
8
9
from vllm.attention.backends.abstract import AttentionBackend
10
from vllm.config import VllmConfig
11
from vllm.v1.attention.backends.utils import (AttentionMetadataBuilder,
12
- CommonAttentionMetadata)
12
+ CommonAttentionMetadata,
13
+ split_decodes_and_prefills)
14
from vllm.v1.kv_cache_interface import AttentionSpec, MambaSpec
15
16
@@ -25,12 +26,15 @@ class Mamba1AttentionMetadata:
26
query_start_loc: torch.Tensor
27
context_lens_tensor: torch.Tensor
28
state_indices_tensor: torch.Tensor
28
- has_initial_states: torch.Tensor
29
+ has_initial_states: Optional[torch.Tensor]
30
+ num_prefills: int
31
+ num_prefill_tokens: int
32
+ num_decodes: int
33
+ num_decode_tokens: int
34
35
36
class Mamba1AttentionMetadataBuilder(
37
AttentionMetadataBuilder[Mamba1AttentionMetadata]):
33
-
38
reorder_batch_threshold: ClassVar[int] = 1
39
40
def __init__(
@@ -57,11 +61,23 @@ class Mamba1AttentionMetadataBuilder(
61
state_indices_tensor = common_attn_metadata.block_table_tensor[:, 0]
62
context_lens_tensor = common_attn_metadata.num_computed_tokens_cpu.to(
63
query_start_loc.device)
60
- has_initial_states = (context_lens_tensor > 0)
64
+
65
+ num_decodes, num_prefills, num_decode_tokens, num_prefill_tokens = (
66
+ split_decodes_and_prefills(common_attn_metadata,
67
+ decode_threshold=1))
68
+
69
+ has_initial_states = None
70
+
71
+ if num_prefills > 0:
72
+ has_initial_states = context_lens_tensor > 0
73
74
return Mamba1AttentionMetadata(
75
query_start_loc=query_start_loc,
76
context_lens_tensor=context_lens_tensor,
77
has_initial_states=has_initial_states,
78
state_indices_tensor=state_indices_tensor,
79
+ num_prefills=num_prefills,
80
+ num_prefill_tokens=num_prefill_tokens,
81
+ num_decodes=num_decodes,
82
+ num_decode_tokens=num_decode_tokens,
83
)