@tduncan / Nvidia-Optimization-Stack-VLLM Mirror · read-only / commits / b690e34824
Fork 0

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, 22:35 UTC

[Model] Mamba2 preallocate SSM output tensor to avoid d2d copy overhead (#21075)

Signed-off-by: Chih-Chieh Yang <7364402+cyang49@users.noreply.github.com> Signed-off-by: Chih-Chieh-Yang <7364402+cyang49@users.noreply.github.com>

Chih-Chieh Yang committed Aug 2, 2025 at 04:59 UTC b690e34824fd5a5c4054a0c0468ebfb6aa1dd215
9 files changed +144 -118
tests/kernels/mamba/test_mamba_ssm.py
+40 -34
@@ -365,6 +365,7 @@ def test_selective_state_update(dim, dstate, has_z, itype):
365 batch_size = 1
366 state = torch.randn(batch_size, dim, dstate, dtype=itype, device=device)
367 x = torch.randn(batch_size, dim, device=device, dtype=itype)
368 + out = torch.empty_like(x)
369 dt = torch.randn(batch_size, dim, device=device, dtype=itype)
370 dt_bias = torch.rand(dim, device=device) - 4.0
371 A = -torch.rand(dim, dstate, device=device) - 1.0
@@ -373,16 +374,17 @@ def test_selective_state_update(dim, dstate, has_z, itype):
374 D = torch.randn(dim, device=device)
375 z = torch.randn_like(x) if has_z else None
376 state_ref = state.detach().clone()
376 - out = selective_state_update(state,
377 - x,
378 - dt,
379 - A,
380 - B,
381 - C,
382 - D=D,
383 - z=z,
384 - dt_bias=dt_bias,
385 - dt_softplus=True)
377 + selective_state_update(state,
378 + x,
379 + dt,
380 + A,
381 + B,
382 + C,
383 + D=D,
384 + z=z,
385 + dt_bias=dt_bias,
386 + dt_softplus=True,
387 + out=out)
388 out_ref = selective_state_update_ref(state_ref,
389 x,
390 dt,
@@ -581,6 +583,7 @@ def test_selective_state_update_with_batch_indices(with_padding, dim, dstate,
583 ],
584 dim=0)
585 x = torch.randn(padded_batch_size, dim, device=device, dtype=itype)
586 + out = torch.empty_like(x)
587 dt = torch.randn(padded_batch_size, dim, device=device, dtype=itype)
588 dt_bias = torch.rand(dim, device=device) - 4.0
589 A = -torch.rand(dim, dstate, device=device) - 1.0
@@ -590,18 +593,19 @@ def test_selective_state_update_with_batch_indices(with_padding, dim, dstate,
593 z = torch.randn_like(x) if has_z else None
594 state_ref = state[state_indices, :].clone()
595 state_before = state.clone()
593 - out = selective_state_update(state,
594 - x,
595 - dt,
596 - A,
597 - B,
598 - C,
599 - D=D,
600 - z=z,
601 - dt_bias=dt_bias,
602 - dt_softplus=True,
603 - state_batch_indices=padded_state_indices,
604 - pad_slot_id=PAD_SLOT_ID)
596 + selective_state_update(state,
597 + x,
598 + dt,
599 + A,
600 + B,
601 + C,
602 + D=D,
603 + z=z,
604 + dt_bias=dt_bias,
605 + dt_softplus=True,
606 + state_batch_indices=padded_state_indices,
607 + pad_slot_id=PAD_SLOT_ID,
608 + out=out)
609 out_ref = selective_state_update_ref(state_ref,
610 x[:batch_size],
611 dt[:batch_size],
@@ -665,6 +669,7 @@ def test_selective_state_update_with_heads_with_batch_indices(
669 dtype=torch.int32, device=device)
670
671 x = torch.randn(batch_size, nheads, headdim, device=device, dtype=itype)
672 + out = torch.empty_like(x)
673 if not tie_hdim:
674 dt = torch.randn(batch_size,
675 nheads,
@@ -691,18 +696,19 @@ def test_selective_state_update_with_heads_with_batch_indices(
696 C = torch.randn(batch_size, ngroups, dstate, device=device)
697 z = torch.randn_like(x) if has_z else None
698 state_ref = state[state_indices, :].detach().clone()
694 - out = selective_state_update(state,
695 - x,
696 - dt,
697 - A,
698 - B,
699 - C,
700 - D=D,
701 - z=z,
702 - dt_bias=dt_bias,
703 - dt_softplus=True,
704 - state_batch_indices=state_indices,
705 - pad_slot_id=PAD_SLOT_ID)
699 + selective_state_update(state,
700 + x,
701 + dt,
702 + A,
703 + B,
704 + C,
705 + D=D,
706 + z=z,
707 + dt_bias=dt_bias,
708 + dt_softplus=True,
709 + state_batch_indices=state_indices,
710 + pad_slot_id=PAD_SLOT_ID,
711 + out=out)
712 out_ref = selective_state_update_ref(state_ref,
713 x,
714 dt,
tests/kernels/mamba/test_mamba_ssm_ssd.py
+13 -10
@@ -212,15 +212,16 @@ def test_mamba_chunk_scan_single_example(d_head, n_heads, seq_len_chunk_size,
212
213 Y_min, final_state_min = ssd_minimal_discrete(X * dt.unsqueeze(-1), A * dt,
214 B, C, chunk_size)
215 -
216 - Y, final_state = mamba_chunk_scan_combined(X,
217 - dt,
218 - A,
219 - B,
220 - C,
221 - chunk_size,
222 - D=None,
223 - return_final_states=True)
215 + Y = torch.empty_like(X)
216 + final_state = mamba_chunk_scan_combined(X,
217 + dt,
218 + A,
219 + B,
220 + C,
221 + chunk_size,
222 + D=None,
223 + return_final_states=True,
224 + out=Y)
225
226 # just test the last in sequence
227 torch.testing.assert_close(Y[:, -1], Y_min[:, -1], atol=atol, rtol=rtol)
@@ -292,7 +293,8 @@ def test_mamba_chunk_scan_cont_batch(d_head, n_heads, seq_len_chunk_size_cases,
293 _query_start_loc_to_chunk_indices_offsets(
294 cu_seqlens, chunk_size, cu_seqlens[-1])
295
295 - Y, new_states = mamba_chunk_scan_combined(
296 + Y = torch.empty_like(X)
297 + new_states = mamba_chunk_scan_combined(
298 X,
299 dt,
300 A,
@@ -306,6 +308,7 @@ def test_mamba_chunk_scan_cont_batch(d_head, n_heads, seq_len_chunk_size_cases,
308 chunk_offsets=chunk_offsets,
309 return_varlen_states=True,
310 initial_states=states,
311 + out=Y,
312 )
313
314 # just test the last in sequence
vllm/model_executor/layers/mamba/mamba_mixer.py
+4 -2
@@ -220,7 +220,8 @@ class MambaMixer(CustomOp):
220 has_initial_state=attn_metadata.context_lens_tensor > 0,
221 query_start_loc=attn_metadata.query_start_loc)
222 else:
223 - scan_outputs = selective_state_update(
223 + scan_outputs = torch.empty_like(hidden_states.transpose(0, 1))
224 + selective_state_update(
225 mamba_cache_params.ssm_state,
226 hidden_states.transpose(0, 1),
227 discrete_time_step.transpose(0, 1),
@@ -231,7 +232,8 @@ class MambaMixer(CustomOp):
232 gate.transpose(0, 1),
233 time_proj_bias,
234 dt_softplus=True,
234 - state_batch_indices=mamba_cache_params.state_indices_tensor)
235 + state_batch_indices=mamba_cache_params.state_indices_tensor,
236 + out=scan_outputs)
237 scan_outputs = scan_outputs.transpose(0, 1)
238
239 # 4. Final linear projection
vllm/model_executor/layers/mamba/mamba_mixer2.py
+32 -22
@@ -541,7 +541,6 @@ class MambaMixer2(MambaBase, CustomOp):
541 # NOTE: V0 put prefill before decode, v1 puts decode before prefill
542 # Separate prefill and decode by splitting varlen input
543 # Split along token dimension
544 - # NOTE: V0 put prefill before decode, v1 puts decode before prefill
544 if envs.VLLM_USE_V1:
545 hidden_states_B_C_d, hidden_states_B_C_p = torch.split(
546 hidden_states_B_C[:num_actual_tokens],
@@ -583,7 +582,28 @@ class MambaMixer2(MambaBase, CustomOp):
582 1]
583 if has_prefill else None)
584
586 - ssd_output_list = []
585 + # Preallocate output tensor to avoid memcpy cost for merging prefill
586 + # and decode outputs
587 + preallocated_ssm_out = torch.empty(
588 + [
589 + num_prefill_tokens + num_decodes,
590 + (self.num_heads // self.tp_size) * self.head_dim
591 + ],
592 + dtype=hidden_states.dtype,
593 + device=hidden_states.device,
594 + )
595 + if envs.VLLM_USE_V1:
596 + preallocated_ssm_out_d, preallocated_ssm_out_p = torch.split(
597 + preallocated_ssm_out,
598 + [num_decodes, num_prefill_tokens],
599 + dim=0,
600 + )
601 + else:
602 + preallocated_ssm_out_p, preallocated_ssm_out_d = torch.split(
603 + preallocated_ssm_out,
604 + [num_prefill_tokens, num_decodes],
605 + dim=0,
606 + )
607
608 # Process prefill requests
609 if has_prefill:
@@ -623,7 +643,8 @@ class MambaMixer2(MambaBase, CustomOp):
643 has_initial_states_p[:num_prefills, None, None, None],
644 ssm_state[state_indices_tensor_p], 0)
645
626 - scan_output, varlen_state = mamba_chunk_scan_combined(
646 + # NOTE: final output is an in-place update of out tensor
647 + varlen_state = mamba_chunk_scan_combined(
648 hidden_states_p.view(1, num_prefill_tokens,
649 self.num_heads // self.tp_size,
650 self.head_dim),
@@ -646,15 +667,14 @@ class MambaMixer2(MambaBase, CustomOp):
667 return_final_states=False,
668 dt_softplus=True,
669 dt_limit=(0.0, float("inf")),
670 + out=preallocated_ssm_out_p.view(1, num_prefill_tokens, -1,
671 + self.head_dim),
672 )
673
674 # update ssm states
675 # - varlen state is a (num_prefills, nheads, headdim, dstate) tensor
676 ssm_state[state_indices_tensor_p] = varlen_state
677
655 - # - reshape
656 - ssd_output_list.append(scan_output.view(num_prefill_tokens, -1))
657 -
678 # Process decode requests
679 if has_decode:
680 # 2. Convolution sequence transformation
@@ -684,8 +704,8 @@ class MambaMixer2(MambaBase, CustomOp):
704 # - the hidden is reshaped into (bs, num_heads, head_dim)
705 # - mamba_cache_params.ssm_state's slots will be selected
706 # using state_indices_tensor_d
687 -
688 - hidden_states_d = selective_state_update(
707 + # NOTE: final output is an in-place update of out tensor
708 + selective_state_update(
709 ssm_state,
710 hidden_states_d,
711 dt_d,
@@ -697,26 +717,16 @@ class MambaMixer2(MambaBase, CustomOp):
717 dt_bias=dt_bias,
718 dt_softplus=True,
719 state_batch_indices=state_indices_tensor_d,
720 + out=preallocated_ssm_out_d.view(num_decodes, -1,
721 + self.head_dim),
722 )
723
702 - if envs.VLLM_USE_V1:
703 - ssd_output_list.insert(
704 - 0,
705 - hidden_states_d.view(-1, (self.num_heads // self.tp_size) *
706 - self.head_dim))
707 - else:
708 - ssd_output_list.append(
709 - hidden_states_d.view(-1, (self.num_heads // self.tp_size) *
710 - self.head_dim))
711 -
712 - # Merge prefill and decode outputs before passing to gated MLP
713 - hidden_states = torch.vstack(ssd_output_list)
714 -
724 # 4. gated MLP
725 # GatedRMSNorm internally applying SiLU to the gate
726 # SiLU is applied internally before normalization, unlike standard
727 # norm usage
719 - hidden_states = self.norm(hidden_states, gate[:num_actual_tokens])
728 + hidden_states = self.norm(preallocated_ssm_out,
729 + gate[:num_actual_tokens])
730
731 # 5. Final linear projection
732 output[:num_actual_tokens], _ = self.out_proj(hidden_states)
vllm/model_executor/layers/mamba/ops/mamba_ssm.py
+8 -8
@@ -205,7 +205,8 @@ def selective_state_update(state,
205 dt_bias=None,
206 dt_softplus=False,
207 state_batch_indices=None,
208 - pad_slot_id=PAD_SLOT_ID):
208 + pad_slot_id=PAD_SLOT_ID,
209 + out=None):
210 """
211 Argument:
212 state: (batch, dim, dstate) or (batch, nheads, dim, dstate)
@@ -223,10 +224,9 @@ def selective_state_update(state,
224 for example: cache_indices = [pad_slot_id, 1, 20, pad_slot_id]
225 in this case, the kernel will not process entries at
226 indices 0 and 3
226 - Return:
227 - out: (batch, dim) or (batch, nheads, dim)
227 + out: Preallocated ssm output tensor. Assume same shape as x.
228 + In-place updated.
229 """
229 - has_heads = state.dim() > 3
230 if state.dim() == 3:
231 state = state.unsqueeze(1)
232 if x.dim() == 2:
@@ -245,6 +245,8 @@ def selective_state_update(state,
245 z = z.unsqueeze(1)
246 if dt_bias is not None and dt_bias.dim() == 1:
247 dt_bias = dt_bias.unsqueeze(0)
248 + if out.dim() == 2:
249 + out = out.unsqueeze(1)
250
251 _, nheads, dim, dstate = state.shape
252 batch = x.shape[0]
@@ -264,7 +266,8 @@ def selective_state_update(state,
266 assert dt_bias.shape == (nheads, dim)
267 if state_batch_indices is not None:
268 assert state_batch_indices.shape == (batch, )
267 - out = torch.empty_like(x)
269 + assert out.shape == x.shape
270 +
271 grid = lambda META: (triton.cdiv(dim, META['BLOCK_SIZE_M']), batch, nheads)
272 z_strides = ((z.stride(0), z.stride(1), z.stride(2)) if z is not None else
273 (0, 0, 0))
@@ -328,9 +331,6 @@ def selective_state_update(state,
331 BLOCK_SIZE_M,
332 num_warps=num_warps,
333 )
331 - if not has_heads:
332 - out = out.squeeze(1)
333 - return out
334
335
336 def selective_scan_fn(u,
vllm/model_executor/layers/mamba/ops/ssd_chunk_scan.py
+5 -14
@@ -454,6 +454,7 @@ def _chunk_scan_fwd(
454 chunk_indices=None,
455 chunk_offsets=None,
456 initial_states=None,
457 + out=None,
458 ):
459 batch, seqlen, nheads, headdim = x.shape
460 _, _, nchunks, chunk_size = dt.shape
@@ -483,20 +484,10 @@ def _chunk_scan_fwd(
484 else:
485 chunk_indices, chunk_offsets = None, None
486
486 - # Allocates output.
487 - out = torch.empty(batch,
488 - seqlen,
489 - nheads,
490 - headdim,
491 - device=x.device,
492 - dtype=x.dtype)
487 + assert out.shape == x.shape
488 +
489 if z is not None:
494 - out_x = torch.empty(batch,
495 - seqlen,
496 - nheads,
497 - headdim,
498 - device=x.device,
499 - dtype=x.dtype)
490 + out_x = torch.empty_like(x)
491 assert out_x.stride() == out.stride()
492 else:
493 out_x = None
@@ -579,4 +570,4 @@ def _chunk_scan_fwd(
570 IS_TRITON_22=TRITON_22,
571 HAS_INITSTATES=initial_states is not None,
572 )
582 - return out, out_x
573 + return out_x
vllm/model_executor/layers/mamba/ops/ssd_combined.py
+16 -12
@@ -36,7 +36,8 @@ def _mamba_chunk_scan_combined_fwd(x,
36 chunk_offsets=None,
37 cu_seqlens=None,
38 dt_softplus=False,
39 - dt_limit=(0.0, float("inf"))):
39 + dt_limit=(0.0, float("inf")),
40 + out=None):
41 batch, seqlen, nheads, headdim = x.shape
42 _, _, ngroups, dstate = B.shape
43 assert nheads % ngroups == 0
@@ -134,7 +135,7 @@ def _mamba_chunk_scan_combined_fwd(x,
135 # - in each (pseudo) chunk, we detect if the previous (pseudo) chunk had
136 # a seq_idx change, in which case we take states information from
137 # init_states.
137 - out, out_x = _chunk_scan_fwd(
138 + out_x = _chunk_scan_fwd(
139 CB,
140 x,
141 dt,
@@ -147,9 +148,10 @@ def _mamba_chunk_scan_combined_fwd(x,
148 chunk_indices=chunk_indices,
149 chunk_offsets=chunk_offsets,
150 initial_states=initial_states,
151 + out=out,
152 )
153 if cu_seqlens is None:
152 - return out, out_x, dt, dA_cumsum, states, final_states
154 + return out_x, dt, dA_cumsum, states, final_states
155 else:
156 assert batch == 1, "passing cu_seqlens to get the varlen states is only supported if batch dimension is 1"
157 varlen_states = chunk_state_varlen(
@@ -161,7 +163,7 @@ def _mamba_chunk_scan_combined_fwd(x,
163 states.squeeze(0),
164 initial_states=initial_states,
165 )
164 - return out, out_x, dt, dA_cumsum, states, final_states, varlen_states
166 + return out_x, dt, dA_cumsum, states, final_states, varlen_states
167
168
169 def mamba_chunk_scan_combined(x,
@@ -180,6 +182,7 @@ def mamba_chunk_scan_combined(x,
182 cu_seqlens=None,
183 dt_softplus=False,
184 dt_limit=(0.0, float("inf")),
185 + out=None,
186 return_final_states=False,
187 return_varlen_states=False):
188 """
@@ -197,15 +200,14 @@ def mamba_chunk_scan_combined(x,
200 seq_idx: (batch, seqlen)
201 cu_seqlens: (num_sequences + 1) or None, only used if return_varlen_states is True
202 dt_softplus: Whether to apply softplus to dt
200 - Return:
201 - out: (batch, seqlen, nheads, headdim)
203 + out: Preallocated output tensor
204 """
205
206 if not return_varlen_states:
207 cu_seqlens = None
208 else:
209 assert cu_seqlens is not None, "cu_seqlens must be provided if return_varlen_states is True"
208 - out, out_x, dt_out, dA_cumsum, states, final_states, *rest = _mamba_chunk_scan_combined_fwd(
210 + out_x, dt_out, dA_cumsum, states, final_states, *rest = _mamba_chunk_scan_combined_fwd(
211 x,
212 dt,
213 A,
@@ -221,12 +223,14 @@ def mamba_chunk_scan_combined(x,
223 chunk_offsets=chunk_offsets,
224 cu_seqlens=cu_seqlens,
225 dt_softplus=dt_softplus,
224 - dt_limit=dt_limit)
226 + dt_limit=dt_limit,
227 + out=out)
228 if not return_varlen_states:
226 - return out if not return_final_states else (out, final_states)
229 + if not return_final_states:
230 + return
231 + else:
232 + return final_states
233 else:
234 varlen_states = rest[0]
229 - return (out,
230 - varlen_states) if not return_final_states else (out,
231 - final_states,
235 + return (varlen_states) if not return_final_states else (final_states,
236 varlen_states)
vllm/model_executor/models/phi4flash.py
+4 -2
@@ -387,7 +387,8 @@ class Phi4Mamba(nn.Module):
387 has_initial_state=attn_metadata.context_lens_tensor > 0,
388 query_start_loc=attn_metadata.query_start_loc)
389 else:
390 - scan_outputs = selective_state_update(
390 + scan_outputs = torch.empty_like(hidden_states.transpose(0, 1))
391 + selective_state_update(
392 mamba_cache_params.ssm_state,
393 hidden_states.transpose(0, 1),
394 discrete_time_step.transpose(0, 1),
@@ -400,7 +401,8 @@ class Phi4Mamba(nn.Module):
401 None if self.yoco_kv else gate.transpose(0, 1),
402 time_proj_bias,
403 dt_softplus=True,
403 - state_batch_indices=mamba_cache_params.state_indices_tensor)
404 + state_batch_indices=mamba_cache_params.state_indices_tensor,
405 + out=scan_outputs)
406 scan_outputs = scan_outputs.transpose(0, 1)
407
408 # 4. Final linear projection
vllm/model_executor/models/plamo2.py
+22 -14
@@ -257,7 +257,21 @@ class Plamo2MambaMixer(nn.Module):
257 query_start_loc_p = (attn_metadata.query_start_loc[:num_prefills + 1]
258 if has_prefill else None)
259
260 - ssd_output_list = []
260 + # Preallocate output tensor to avoid memcpy cost for merging prefill
261 + # and decode outputs
262 + preallocated_ssm_out = torch.empty(
263 + [
264 + num_prefill_tokens + num_decodes,
265 + (self.num_heads // self.tp_size) * self.head_dim
266 + ],
267 + dtype=hidden_states.dtype,
268 + device=hidden_states.device,
269 + )
270 + preallocated_ssm_out_p, preallocated_ssm_out_d = torch.split(
271 + preallocated_ssm_out,
272 + [num_prefill_tokens, num_decodes],
273 + dim=0,
274 + )
275
276 # Process prefill requests
277 if has_prefill:
@@ -290,7 +304,7 @@ class Plamo2MambaMixer(nn.Module):
304 initial_states = torch.where(
305 mamba2_metadata.has_initial_states[:, None, None, None],
306 mamba_cache_params.ssm_state[state_indices_tensor_p], 0)
293 - scan_output, varlen_state = mamba_chunk_scan_combined(
307 + varlen_state = mamba_chunk_scan_combined(
308 hidden_states_p.view(1, num_prefill_tokens,
309 self.num_heads // self.tp_size,
310 self.head_dim),
@@ -312,15 +326,14 @@ class Plamo2MambaMixer(nn.Module):
326 return_final_states=False,
327 dt_softplus=True,
328 dt_limit=(0.0, float("inf")),
329 + out=preallocated_ssm_out_p.view(1, num_prefill_tokens, -1,
330 + self.head_dim),
331 )
332
333 # update ssm states
334 # - varlen state is a (batch, nheads, headdim, dstate) tensor
335 mamba_cache_params.ssm_state[state_indices_tensor_p] = varlen_state
336
321 - # - reshape
322 - ssd_output_list.append(scan_output.view(num_prefill_tokens, -1))
323 -
337 # Process decode requests
338 if has_decode:
339 # 2. Convolution sequence transformation
@@ -349,8 +362,7 @@ class Plamo2MambaMixer(nn.Module):
362 # - the hidden is reshaped into (bs, num_heads, head_dim)
363 # - mamba_cache_params.ssm_state's slots will be selected
364 # using state_indices_tensor_d
352 -
353 - hidden_states_d = selective_state_update(
365 + selective_state_update(
366 mamba_cache_params.ssm_state,
367 hidden_states_d,
368 dt,
@@ -362,17 +374,13 @@ class Plamo2MambaMixer(nn.Module):
374 dt_bias=dt_bias,
375 dt_softplus=True,
376 state_batch_indices=state_indices_tensor_d,
377 + out=preallocated_ssm_out_d.view(num_decodes, -1,
378 + self.head_dim),
379 )
380 assert self.num_heads % self.tp_size == 0
367 - ssd_output_list.append(
368 - hidden_states_d.view(-1, (self.num_heads // self.tp_size) *
369 - self.head_dim))
370 -
371 - # Merge prefill and decode outputs before passing to MLP
372 - hidden_states = torch.vstack(ssd_output_list)
381
382 # 4. Final linear projection
375 - out = self.out_proj(hidden_states)
383 + out = self.out_proj(preallocated_ssm_out)
384 return out
385
386