@tduncan / Nvidia-Optimization-Stack-VLLM Mirror · read-only / commits / a89209b78d
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 15, 04:45 UTC

[v1] Support mamba2 (#19327)

Signed-off-by: Chen Zhang <zhangch99@outlook.com>

Chen Zhang committed Jun 19, 2025 at 04:34 UTC a89209b78de0aa3d47a684a34861336d3ba7c0fe
9 files changed +583 -121
tests/models/language/generation/test_hybrid.py
+42 -11
@@ -17,9 +17,10 @@ SSM_MODELS = [
17 "state-spaces/mamba-130m-hf",
18 "tiiuae/falcon-mamba-tiny-dev",
19 # TODO: Compare to a Mamba2 model. The HF transformers implementation of
20 - # Mamba2 is buggy for Codestral as it doesn't handle n_groups.
20 + # Mamba2 is buggy for Codestral as it doesn't handle n_groups, so the test
21 + # doesn't compare vLLM output with HF output.
22 # See https://github.com/huggingface/transformers/pull/35943
22 - # "mistralai/Mamba-Codestral-7B-v0.1",
23 + "mistralai/Mamba-Codestral-7B-v0.1",
24 ]
25
26 HYBRID_MODELS = [
@@ -35,6 +36,10 @@ HYBRID_MODELS = [
36 "hmellor/tiny-random-BambaForCausalLM",
37 ]
38
39 +V1_SUPPORTED_MODELS = [
40 + "mistralai/Mamba-Codestral-7B-v0.1",
41 +]
42 +
43 # Avoid OOM
44 MAX_NUM_SEQS = 4
45
@@ -46,24 +51,50 @@ def test_models(
51 hf_runner,
52 vllm_runner,
53 example_prompts,
54 + monkeypatch,
55 model: str,
56 max_tokens: int,
57 num_logprobs: int,
58 ) -> None:
59 with hf_runner(model) as hf_model:
54 - hf_outputs = hf_model.generate_greedy_logprobs_limit(
55 - example_prompts, max_tokens, num_logprobs)
60 + if model != "mistralai/Mamba-Codestral-7B-v0.1":
61 + hf_outputs = hf_model.generate_greedy_logprobs_limit(
62 + example_prompts, max_tokens, num_logprobs)
63 + else:
64 + hf_outputs = None
65
66 with vllm_runner(model, max_num_seqs=MAX_NUM_SEQS) as vllm_model:
58 - vllm_outputs = vllm_model.generate_greedy_logprobs(
67 + vllm_v0_outputs = vllm_model.generate_greedy_logprobs(
68 example_prompts, max_tokens, num_logprobs)
69
61 - check_logprobs_close(
62 - outputs_0_lst=hf_outputs,
63 - outputs_1_lst=vllm_outputs,
64 - name_0="hf",
65 - name_1="vllm",
66 - )
70 + if model in V1_SUPPORTED_MODELS:
71 + with monkeypatch.context() as m:
72 + m.setenv("VLLM_USE_V1", "1")
73 + with vllm_runner(model,
74 + max_num_seqs=MAX_NUM_SEQS,
75 + enforce_eager=True,
76 + enable_prefix_caching=False) as vllm_model:
77 + vllm_v1_outputs = vllm_model.generate_greedy_logprobs(
78 + example_prompts, max_tokens, num_logprobs)
79 + else:
80 + vllm_v1_outputs = None
81 +
82 + if hf_outputs is not None:
83 + check_logprobs_close(
84 + outputs_0_lst=hf_outputs,
85 + outputs_1_lst=vllm_v0_outputs,
86 + name_0="hf",
87 + name_1="vllm-v0",
88 + )
89 +
90 + if model in V1_SUPPORTED_MODELS:
91 + ref_outputs = hf_outputs if hf_outputs is not None else vllm_v0_outputs
92 + check_logprobs_close(
93 + outputs_0_lst=ref_outputs,
94 + outputs_1_lst=vllm_v1_outputs,
95 + name_0="hf" if hf_outputs is not None else "vllm-v0",
96 + name_1="vllm-v1",
97 + )
98
99
100 @pytest.mark.parametrize("model", SSM_MODELS + HYBRID_MODELS)
tests/v1/test_oracle.py
+1 -1
@@ -12,7 +12,7 @@ from vllm.engine.async_llm_engine import AsyncLLMEngine
12 UNSUPPORTED_MODELS_V1 = [
13 "openai/whisper-large-v3", # transcription
14 "facebook/bart-large-cnn", # encoder decoder
15 - "mistralai/Mamba-Codestral-7B-v0.1", # mamba
15 + "state-spaces/mamba-130m-hf", # mamba1
16 "hmellor/tiny-random-BambaForCausalLM", # hybrid
17 "BAAI/bge-m3", # embedding
18 ]
vllm/engine/arg_utils.py
+6 -1
@@ -1355,12 +1355,17 @@ class EngineArgs:
1355 recommend_to_remove=False)
1356 return False
1357
1358 - # No Mamba or Encoder-Decoder so far.
1358 + # No Encoder-Decoder, not all Mamba so far.
1359 if not model_config.is_v1_compatible:
1360 _raise_or_fallback(feature_name=model_config.architectures,
1361 recommend_to_remove=False)
1362 return False
1363
1364 + # V1 mamba models are unoptimized.
1365 + if model_config.has_inner_state and _warn_or_fallback(
1366 + feature_name="Mamba"):
1367 + return False
1368 +
1369 # No Concurrent Partial Prefills so far.
1370 if (self.max_num_partial_prefills
1371 != SchedulerConfig.max_num_partial_prefills
vllm/model_executor/layers/mamba/mamba_mixer2.py
+175 -60
@@ -6,7 +6,9 @@ from typing import Optional, Union
6 import torch
7 from torch import nn
8
9 +from vllm import envs
10 from vllm.attention.backends.abstract import AttentionMetadata
11 +from vllm.config import get_current_vllm_config
12 from vllm.distributed import (divide, get_tensor_model_parallel_rank,
13 get_tensor_model_parallel_world_size,
14 tensor_model_parallel_all_gather,
@@ -27,6 +29,7 @@ from vllm.model_executor.model_loader.weight_utils import (
29 LoaderFunction, composed_weight_loader, sharded_weight_loader)
30 from vllm.model_executor.models.mamba_cache import MambaCacheParams
31 from vllm.model_executor.utils import set_weight_attrs
32 +from vllm.v1.attention.backends.mamba_attn import Mamba2AttentionMetadata
33
34 # Added by the IBM Team, 2024
35
@@ -227,20 +230,22 @@ class MambaMixer2(CustomOp):
230 """
231
232 def __init__(
230 - self,
231 - hidden_size: int,
232 - ssm_state_size: int,
233 - conv_kernel_size: int,
234 - intermediate_size: int,
235 - use_conv_bias: bool,
236 - use_bias: bool,
237 - n_groups: int = 1,
238 - num_heads: int = 128,
239 - head_dim: int = 64,
240 - rms_norm_eps: float = 1e-5,
241 - activation: str = "silu",
242 - use_rms_norm: bool = True,
243 - quant_config: Optional[QuantizationConfig] = None,
233 + self,
234 + hidden_size: int,
235 + ssm_state_size: int,
236 + conv_kernel_size: int,
237 + intermediate_size: int,
238 + use_conv_bias: bool,
239 + use_bias: bool,
240 + n_groups: int = 1,
241 + num_heads: int = 128,
242 + head_dim: int = 64,
243 + rms_norm_eps: float = 1e-5,
244 + activation: str = "silu",
245 + use_rms_norm: bool = True,
246 + quant_config: Optional[QuantizationConfig] = None,
247 + prefix: str = "",
248 + chunk_size: int = -1, # the chunk size used by v1
249 ):
250 super().__init__()
251
@@ -273,6 +278,7 @@ class MambaMixer2(CustomOp):
278 ), "Tensor parallel currently not supported for quantized models."
279
280 self.ssm_state_size = ssm_state_size
281 + self.conv_kernel_size = conv_kernel_size
282 self.activation = activation
283
284 self.intermediate_size = intermediate_size
@@ -411,6 +417,22 @@ class MambaMixer2(CustomOp):
417 self.use_rms_norm,
418 eps=rms_norm_eps)
419
420 + if envs.VLLM_USE_V1:
421 + compilation_config = get_current_vllm_config().compilation_config
422 + if prefix in compilation_config.static_forward_context:
423 + raise ValueError(f"Duplicate layer name: {prefix}")
424 + compilation_config.static_forward_context[prefix] = self
425 + # The outer list is for v0 PP virtual engine. Though this code path
426 + # only runs for v1, we have to do this to unify with the interface
427 + # of Attention + v0 PP.
428 + # The inner tuple is (conv_state, ssm_state)
429 + self.kv_cache = [(torch.tensor([]), torch.tensor([]))]
430 + assert chunk_size != -1, "chunk_size must be set for v1"
431 +
432 + # NOTE: chunk_size may be -1 for models without v1 support
433 + self.chunk_size = chunk_size
434 + self.prefix = prefix
435 +
436 def forward_native(
437 self,
438 hidden_states: torch.Tensor,
@@ -426,17 +448,37 @@ class MambaMixer2(CustomOp):
448 mamba2_metadata: Mamba2Metadata,
449 mup_vector: Optional[torch.Tensor] = None,
450 ):
451 + forward_context = get_forward_context()
452 # mamba2_metadata contains metadata necessary for the mamba2 triton
453 # kernels to operate in continuous batching and in chunked prefill
454 # modes; they are computed at top-level model forward since they
455 # stay the same and reused for all mamba layers in the same iteration
433 - attn_metadata: AttentionMetadata = get_forward_context().attn_metadata
434 -
435 - num_prefills = attn_metadata.num_prefills # request count
436 - num_decodes = attn_metadata.num_decode_tokens # token count (=request)
437 - num_prefill_tokens = attn_metadata.num_prefill_tokens # token count
438 - has_prefill = num_prefills > 0
439 - has_decode = num_decodes > 0
456 + attn_metadata: AttentionMetadata = forward_context.attn_metadata
457 + if envs.VLLM_USE_V1:
458 + if attn_metadata is not None:
459 + assert isinstance(attn_metadata, dict)
460 + attn_metadata = attn_metadata[self.prefix]
461 + assert isinstance(attn_metadata, Mamba2AttentionMetadata)
462 + self_kv_cache = self.kv_cache[forward_context.virtual_engine]
463 + conv_state = self_kv_cache[0]
464 + ssm_state = self_kv_cache[1]
465 + state_indices_tensor = attn_metadata.state_indices_tensor
466 + has_initial_states_p = attn_metadata.has_initial_states
467 + prep_initial_states = attn_metadata.prep_initial_states
468 + chunk_size = attn_metadata.chunk_size
469 + seq_idx_p = attn_metadata.seq_idx
470 + chunk_indices_p = attn_metadata.chunk_indices
471 + chunk_offsets_p = attn_metadata.chunk_offsets
472 + else:
473 + conv_state = mamba_cache_params.conv_state
474 + ssm_state = mamba_cache_params.ssm_state
475 + state_indices_tensor = mamba_cache_params.state_indices_tensor
476 + has_initial_states_p = mamba2_metadata.has_initial_states
477 + prep_initial_states = mamba2_metadata.prep_initial_states
478 + chunk_size = mamba2_metadata.chunk_size
479 + seq_idx_p = mamba2_metadata.seq_idx
480 + chunk_indices_p = mamba2_metadata.chunk_indices
481 + chunk_offsets_p = mamba2_metadata.chunk_offsets
482
483 groups_time_state_size = self.n_groups * self.ssm_state_size
484
@@ -459,27 +501,6 @@ class MambaMixer2(CustomOp):
501 conv_weights = self.conv1d.weight.view(self.conv1d.weight.size(0),
502 self.conv1d.weight.size(2))
503
462 - # Separate prefill and decode by splitting varlen input
463 - # Split along token dimension
464 - hidden_states_B_C_p, hidden_states_B_C_d = torch.split(
465 - hidden_states_B_C,
466 - [num_prefill_tokens, num_decodes],
467 - dim=0,
468 - )
469 - dt_p, dt_d = torch.split(
470 - dt,
471 - [num_prefill_tokens, num_decodes],
472 - dim=0,
473 - )
474 - # Split along batch dimension
475 - state_indices_tensor_p, state_indices_tensor_d = torch.split(
476 - mamba_cache_params.state_indices_tensor,
477 - [num_prefills, num_decodes],
478 - dim=0,
479 - )
480 - query_start_loc_p = (attn_metadata.query_start_loc[:num_prefills + 1]
481 - if has_prefill else None)
482 -
504 # - get hidden_states, B and C after depthwise convolution.
505 split_hidden_states_B_C_fn = lambda hidden_states_B_C: torch.split(
506 hidden_states_B_C,
@@ -491,20 +512,80 @@ class MambaMixer2(CustomOp):
512 dim=-1,
513 )
514
515 + if envs.VLLM_USE_V1 and attn_metadata is None:
516 + # V1 profile run
517 + hidden_states_B_C = (hidden_states_B_C.transpose(
518 + 0, 1).clone().transpose(0, 1)).contiguous()
519 + hidden_states, _B, _C = split_hidden_states_B_C_fn(
520 + hidden_states_B_C)
521 + hidden_states = self.norm(hidden_states, gate)
522 + out, _ = self.out_proj(hidden_states)
523 + return out
524 +
525 + num_prefills = attn_metadata.num_prefills # request count
526 + num_decodes = attn_metadata.num_decode_tokens # token count (=request)
527 + num_prefill_tokens = attn_metadata.num_prefill_tokens # token count
528 + has_prefill = num_prefills > 0
529 + has_decode = num_decodes > 0
530 +
531 + # NOTE: V0 put prefill before decode, v1 puts decode before prefill
532 + # Separate prefill and decode by splitting varlen input
533 + # Split along token dimension
534 + if envs.VLLM_USE_V1:
535 + hidden_states_B_C_d, hidden_states_B_C_p = torch.split(
536 + hidden_states_B_C,
537 + [num_decodes, num_prefill_tokens],
538 + dim=0,
539 + )
540 + dt_d, dt_p = torch.split(
541 + dt,
542 + [num_decodes, num_prefill_tokens],
543 + dim=0,
544 + )
545 + # Split along batch dimension
546 + state_indices_tensor_d, state_indices_tensor_p = torch.split(
547 + state_indices_tensor,
548 + [num_decodes, num_prefills],
549 + dim=0,
550 + )
551 + query_start_loc_p = (
552 + attn_metadata.query_start_loc[-num_prefills - 1:] -
553 + num_decodes if has_prefill else None)
554 + else:
555 + hidden_states_B_C_p, hidden_states_B_C_d = torch.split(
556 + hidden_states_B_C,
557 + [num_prefill_tokens, num_decodes],
558 + dim=0,
559 + )
560 + dt_p, dt_d = torch.split(
561 + dt,
562 + [num_prefill_tokens, num_decodes],
563 + dim=0,
564 + )
565 + # Split along batch dimension
566 + state_indices_tensor_p, state_indices_tensor_d = torch.split(
567 + state_indices_tensor,
568 + [num_prefills, num_decodes],
569 + dim=0,
570 + )
571 + query_start_loc_p = (attn_metadata.query_start_loc[:num_prefills +
572 + 1]
573 + if has_prefill else None)
574 +
575 ssd_output_list = []
576
577 # Process prefill requests
578 if has_prefill:
579 # 2. Convolution sequence transformation
580 # - "cache_indices" updates the conv_state cache in positions
500 - # pointed to by "mamba_cache_params.state_indices_tensor"
581 + # pointed to by "state_indices_tensor"
582 hidden_states_B_C_p = causal_conv1d_fn(
583 hidden_states_B_C_p.transpose(0, 1),
584 conv_weights,
585 self.conv1d.bias,
586 activation=self.activation,
506 - conv_states=mamba_cache_params.conv_state,
507 - has_initial_state=mamba2_metadata.has_initial_states,
587 + conv_states=conv_state,
588 + has_initial_state=has_initial_states_p,
589 cache_indices=state_indices_tensor_p,
590 query_start_loc=query_start_loc_p).transpose(
591 0, 1)[:num_prefill_tokens]
@@ -516,12 +597,11 @@ class MambaMixer2(CustomOp):
597
598 # 3. State Space Model sequence transformation
599 initial_states = None
519 - if (mamba2_metadata.has_initial_states is not None
520 - and mamba2_metadata.prep_initial_states):
600 + if (has_initial_states_p is not None and prep_initial_states):
601 # making a copy of the states
602 initial_states = torch.where(
523 - mamba2_metadata.has_initial_states[:, None, None, None],
524 - mamba_cache_params.ssm_state[state_indices_tensor_p], 0)
603 + has_initial_states_p[:, None, None, None],
604 + ssm_state[state_indices_tensor_p], 0)
605
606 scan_output, varlen_state = mamba_chunk_scan_combined(
607 hidden_states_p.view(1, num_prefill_tokens,
@@ -533,14 +613,14 @@ class MambaMixer2(CustomOp):
613 -1),
614 C_p.view(1, num_prefill_tokens, self.n_groups // self.tp_size,
615 -1),
536 - chunk_size=mamba2_metadata.chunk_size,
616 + chunk_size=chunk_size,
617 D=self.D,
618 z=None,
619 dt_bias=self.dt_bias,
540 - seq_idx=mamba2_metadata.seq_idx,
541 - chunk_indices=mamba2_metadata.chunk_indices,
542 - chunk_offsets=mamba2_metadata.chunk_offsets,
543 - cu_seqlens=attn_metadata.query_start_loc[:num_prefills + 1],
620 + seq_idx=seq_idx_p,
621 + chunk_indices=chunk_indices_p,
622 + chunk_offsets=chunk_offsets_p,
623 + cu_seqlens=query_start_loc_p,
624 initial_states=initial_states,
625 return_varlen_states=True,
626 return_final_states=False,
@@ -550,7 +630,7 @@ class MambaMixer2(CustomOp):
630
631 # update ssm states
632 # - varlen state is a (num_prefills, nheads, headdim, dstate) tensor
553 - mamba_cache_params.ssm_state[state_indices_tensor_p] = varlen_state
633 + ssm_state[state_indices_tensor_p] = varlen_state
634
635 # - reshape
636 ssd_output_list.append(scan_output.view(num_prefill_tokens, -1))
@@ -560,7 +640,7 @@ class MambaMixer2(CustomOp):
640 # 2. Convolution sequence transformation
641 hidden_states_B_C_d = causal_conv1d_update(
642 hidden_states_B_C_d,
563 - mamba_cache_params.conv_state,
643 + conv_state,
644 conv_weights,
645 self.conv1d.bias,
646 self.activation,
@@ -586,7 +666,7 @@ class MambaMixer2(CustomOp):
666 # using state_indices_tensor_d
667
668 hidden_states_d = selective_state_update(
589 - mamba_cache_params.ssm_state,
669 + ssm_state,
670 hidden_states_d,
671 dt_d,
672 A_d,
@@ -598,9 +678,16 @@ class MambaMixer2(CustomOp):
678 dt_softplus=True,
679 state_batch_indices=state_indices_tensor_d,
680 )
601 - ssd_output_list.append(
602 - hidden_states_d.view(-1, (self.num_heads // self.tp_size) *
603 - self.head_dim))
681 +
682 + if envs.VLLM_USE_V1:
683 + ssd_output_list.insert(
684 + 0,
685 + hidden_states_d.view(-1, (self.num_heads // self.tp_size) *
686 + self.head_dim))
687 + else:
688 + ssd_output_list.append(
689 + hidden_states_d.view(-1, (self.num_heads // self.tp_size) *
690 + self.head_dim))
691
692 # Merge prefill and decode outputs before passing to gated MLP
693 hidden_states = torch.vstack(ssd_output_list)
@@ -614,3 +701,31 @@ class MambaMixer2(CustomOp):
701 # 5. Final linear projection
702 out, _ = self.out_proj(hidden_states)
703 return out
704 +
705 + def get_state_shape(self) -> tuple[tuple[int, ...], tuple[int, ...]]:
706 + world_size = get_tensor_model_parallel_world_size()
707 +
708 + conv_state_shape, temporal_state_shape = None, None
709 +
710 + # if n_groups is not divisible by world_size, need to extend the shards
711 + # to ensure all groups needed by a head is sharded along with it
712 + n_groups = (self.n_groups +
713 + extra_groups_for_head_shards(self.n_groups, world_size))
714 +
715 + # - heads and n_groups are TP-ed
716 + conv_dim = (self.intermediate_size +
717 + 2 * n_groups * self.ssm_state_size)
718 + conv_state_shape = (
719 + divide(conv_dim, world_size),
720 + self.conv_kernel_size - 1,
721 + )
722 +
723 + # These are not TP-ed as they depend on A, dt_bias, D
724 + # - they are typically small
725 + # e.g., (h_heads, d_head, d_state) = (128, 64, 128)
726 + temporal_state_shape = (
727 + divide(self.num_heads, world_size),
728 + self.head_dim,
729 + self.ssm_state_size,
730 + )
731 + return conv_state_shape, temporal_state_shape
vllm/model_executor/models/mamba2.py
+33 -21
@@ -8,6 +8,7 @@ import torch
8 from torch import nn
9 from transformers import MambaConfig
10
11 +from vllm import envs
12 from vllm.attention.backends.abstract import AttentionMetadata
13 from vllm.config import VllmConfig
14 from vllm.distributed import divide, get_tensor_model_parallel_world_size
@@ -25,8 +26,7 @@ from vllm.model_executor.layers.vocab_parallel_embedding import (
26 DEFAULT_VOCAB_PADDING_SIZE, ParallelLMHead, VocabParallelEmbedding)
27 from vllm.model_executor.model_loader.weight_utils import default_weight_loader
28 from vllm.model_executor.models.interfaces import (HasInnerState,
28 - IsAttentionFree,
29 - SupportsV0Only)
29 + IsAttentionFree)
30 from vllm.model_executor.models.mamba_cache import (MambaCacheManager,
31 MambaCacheParams)
32 from vllm.model_executor.sampling_metadata import SamplingMetadata
@@ -44,7 +44,8 @@ class Mamba2DecoderLayer(nn.Module):
44
45 def __init__(self,
46 config: MambaConfig,
47 - quant_config: Optional[QuantizationConfig] = None) -> None:
47 + quant_config: Optional[QuantizationConfig] = None,
48 + prefix: str = "") -> None:
49 super().__init__()
50 self.config = config
51 self.mixer = MambaMixer2(hidden_size=config.hidden_size,
@@ -60,7 +61,9 @@ class Mamba2DecoderLayer(nn.Module):
61 head_dim=config.head_dim,
62 rms_norm_eps=config.layer_norm_epsilon,
63 activation=config.hidden_act,
63 - quant_config=quant_config)
64 + quant_config=quant_config,
65 + prefix=f"{prefix}.mixer",
66 + chunk_size=config.chunk_size)
67
68 self.norm = RMSNorm(config.hidden_size, eps=config.layer_norm_epsilon)
69
@@ -108,8 +111,8 @@ class Mamba2Model(nn.Module):
111
112 self.start_layer, self.end_layer, self.layers = make_layers(
113 config.num_hidden_layers,
111 - lambda prefix: Mamba2DecoderLayer(config,
112 - quant_config=quant_config),
114 + lambda prefix: Mamba2DecoderLayer(
115 + config, quant_config=quant_config, prefix=prefix),
116 prefix=f"{prefix}.layers")
117
118 self.norm_f = RMSNorm(config.hidden_size,
@@ -142,10 +145,14 @@ class Mamba2Model(nn.Module):
145
146 attn_metadata: AttentionMetadata = get_forward_context().attn_metadata
147
145 - mamba2_metadata = prepare_mamba2_metadata(
146 - chunk_size=self.config.chunk_size,
147 - attn_metadata=attn_metadata,
148 - )
148 + if not envs.VLLM_USE_V1:
149 + mamba2_metadata = prepare_mamba2_metadata(
150 + chunk_size=self.config.chunk_size,
151 + attn_metadata=attn_metadata,
152 + )
153 + else:
154 + # v1 get mamba2_metadata from forward_context
155 + mamba2_metadata = None
156
157 for i in range(len(self.layers)):
158 layer = self.layers[i]
@@ -155,7 +162,7 @@ class Mamba2Model(nn.Module):
162 hidden_states=hidden_states,
163 residual=residual,
164 mamba_cache_params=mamba_cache_params.at_layer_idx(
158 - i - self.start_layer),
165 + i - self.start_layer) if mamba_cache_params else None,
166 mamba2_metadata=mamba2_metadata)
167
168 if not get_pp_group().is_last_rank:
@@ -190,8 +197,7 @@ class Mamba2Model(nn.Module):
197 return loaded_params
198
199
193 -class Mamba2ForCausalLM(nn.Module, HasInnerState, IsAttentionFree,
194 - SupportsV0Only):
200 +class Mamba2ForCausalLM(nn.Module, HasInnerState, IsAttentionFree):
201
202 def __init__(self, *, vllm_config: VllmConfig, prefix: str = ""):
203 config = vllm_config.model_config.hf_config
@@ -242,14 +248,20 @@ class Mamba2ForCausalLM(nn.Module, HasInnerState, IsAttentionFree,
248 intermediate_tensors: Optional[IntermediateTensors] = None,
249 inputs_embeds: Optional[torch.Tensor] = None,
250 **kwargs):
245 - if self.mamba_cache is None:
246 - num_mamba_layers = self.model_config.get_num_layers_by_block_type(
247 - self.vllm_config.parallel_config, LayerBlockType.mamba)
248 - self.mamba_cache = MambaCacheManager(
249 - self.vllm_config, self.lm_head.weight.dtype, num_mamba_layers,
250 - *self._get_mamba_cache_shape())
251 -
252 - mamba_cache_params = self.mamba_cache.current_run_tensors(**kwargs)
251 + if not envs.VLLM_USE_V1:
252 + if self.mamba_cache is None:
253 + num_mamba_layers = (
254 + self.model_config.get_num_layers_by_block_type(
255 + self.vllm_config.parallel_config,
256 + LayerBlockType.mamba))
257 + self.mamba_cache = MambaCacheManager(
258 + self.vllm_config, self.lm_head.weight.dtype,
259 + num_mamba_layers, *self._get_mamba_cache_shape())
260 +
261 + mamba_cache_params = self.mamba_cache.current_run_tensors(**kwargs)
262 + else:
263 + # NOTE: mamba_cache_params is not needed for v1
264 + mamba_cache_params = None
265
266 hidden_states = self.backbone(input_ids, positions, mamba_cache_params,
267 intermediate_tensors, inputs_embeds)
vllm/v1/attention/backends/mamba_attn.py new
+192
@@ -0,0 +1,192 @@
1 +# SPDX-License-Identifier: Apache-2.0
2 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 +from dataclasses import dataclass
4 +from typing import TYPE_CHECKING
5 +
6 +import torch
7 +
8 +from vllm.attention.backends.abstract import AttentionBackend
9 +from vllm.config import VllmConfig, get_layers_from_vllm_config
10 +from vllm.model_executor.layers.mamba.mamba2_metadata import (
11 + _query_start_loc_to_chunk_indices_offsets)
12 +from vllm.v1.attention.backends.utils import (AttentionMetadataBuilder,
13 + CommonAttentionMetadata)
14 +from vllm.v1.kv_cache_interface import MambaSpec
15 +from vllm.v1.worker.block_table import BlockTable
16 +
17 +if TYPE_CHECKING:
18 + from vllm.v1.core.sched.output import SchedulerOutput
19 + from vllm.v1.worker.gpu_input_batch import InputBatch
20 + from vllm.v1.worker.gpu_model_runner import GPUModelRunner
21 +
22 +
23 +def get_mamba2_chunk_size(vllm_config: VllmConfig) -> int:
24 + from vllm.model_executor.layers.mamba.mamba_mixer2 import MambaMixer2
25 + layers = get_layers_from_vllm_config(vllm_config, MambaMixer2)
26 + chunk_sizes = set(layer.chunk_size for layer in layers.values())
27 + assert len(
28 + chunk_sizes) == 1, "All Mamba2 layers must have the same chunk size"
29 + return chunk_sizes.pop()
30 +
31 +
32 +class Mamba2AttentionBackend(AttentionBackend):
33 +
34 + @staticmethod
35 + def get_builder_cls() -> type["Mamba2AttentionMetadataBuilder"]:
36 + return Mamba2AttentionMetadataBuilder
37 +
38 +
39 +@dataclass
40 +class Mamba2AttentionMetadata:
41 + num_prefills: int
42 + num_prefill_tokens: int
43 + num_decodes: int
44 + num_decode_tokens: int
45 + query_start_loc: torch.Tensor
46 + seq_lens: torch.Tensor
47 +
48 + has_initial_states: torch.Tensor
49 + prep_initial_states: bool
50 + chunk_size: int
51 + seq_idx: torch.Tensor
52 + chunk_indices: torch.Tensor
53 + chunk_offsets: torch.Tensor
54 +
55 + state_indices_tensor: torch.Tensor # shape: [batch,]
56 +
57 +
58 +class Mamba2AttentionMetadataBuilder(
59 + AttentionMetadataBuilder[Mamba2AttentionMetadata]):
60 +
61 + def __init__(self, runner: "GPUModelRunner", kv_cache_spec: MambaSpec,
62 + block_table: BlockTable):
63 + self.runner = runner
64 + self.kv_cache_spec = kv_cache_spec
65 + self.block_table = block_table
66 + self.chunk_size = get_mamba2_chunk_size(runner.vllm_config)
67 +
68 + def reorder_batch(self, input_batch: "InputBatch",
69 + scheduler_output: "SchedulerOutput") -> bool:
70 + # NOTE (Chen): Copied from MLACommonMetadataBuilder and
71 + # FlashInferMetadataBuilder. Should be refactored later to avoid code
72 + # duplication of these 3 functions.
73 + # We now want to reorder the batch so that the "decode" requests are and
74 + # the front and the "prefill" requests are at the using the least amount
75 + # swaps possible. (NOTE for now we loosely use "decode" to mean requests
76 + # where attention is likely memory-bound and "prefill" to mean requests
77 + # where attention is likely compute-bound, TODO(lucas): figure out a
78 + # better naming here)
79 + decodes = []
80 + prefills = []
81 + num_decode_tokens = 0
82 + num_prefill_tokens = 0
83 +
84 + for i, req_id in enumerate(input_batch.req_ids):
85 + num_tokens = scheduler_output.num_scheduled_tokens[req_id]
86 + # for now treat 1 scheduled token as "decode" even if its not,
87 + # we should update this to something like < 8 in the future but
88 + # currently the decode run only supports num_tokens = 1
89 + if num_tokens == 1:
90 + decodes.append(i)
91 + num_decode_tokens += num_tokens
92 + else:
93 + prefills.append(i)
94 + num_prefill_tokens += num_tokens
95 +
96 + # We hope that this is fairly minimal since decodes
97 + # should be around for a number of iterations so hopefully they are
98 + # relatively stationary (and new request are generally appended to the
99 + # persistent batch so already should be at the back)
100 + # To achieve this we loop over the decodes in descending order and
101 + # the prefills in ascending order. We swap decodes from the "back"
102 + # i.e. past where the last decode should be in the reodorered with
103 + # prefills from the front of the batch.
104 + # `decodes` and `prefills` are already in ascending order just based on
105 + # the above loop
106 + num_decodes = len(decodes)
107 + num_prefills = len(prefills)
108 + modified_batch = False
109 +
110 + for i in range(1, min(num_decodes, num_prefills) + 1):
111 + # If the decode is at the "back" of the batch, i, we can swap it
112 + # with the prefill closest to the front of the batch
113 + decode_idx = decodes[num_decodes - i]
114 + if decode_idx < num_decodes:
115 + break
116 +
117 + input_batch.swap_states(prefills[i - 1], decode_idx)
118 + modified_batch = True
119 +
120 + # Save for next `build` call
121 + # TODO(lucas): this is a bit of a hack, we should probably have a
122 + # better way of doing this
123 + self._num_decodes = num_decodes
124 + self._num_prefills = num_prefills
125 + self._num_decode_tokens = num_decode_tokens
126 + self._num_prefill_tokens = num_prefill_tokens
127 +
128 + return modified_batch
129 +
130 + def build(self, common_prefix_len: int,
131 + common_attn_metadata: CommonAttentionMetadata):
132 + num_reqs = common_attn_metadata.num_reqs
133 + query_start_loc = common_attn_metadata.query_start_loc
134 + seq_lens = common_attn_metadata.seq_lens
135 +
136 + seq_idx = None
137 + chunk_indices, chunk_offsets = None, None
138 + # Need flags to indicate if there are initial states
139 + # currently we really only support the FlashAttention backend
140 + has_initial_states = None
141 + prep_initial_states = False
142 +
143 + state_indices_tensor = self.block_table.block_table[:num_reqs, 0]
144 +
145 + # Compute seq_idx, chunk_indices and chunk_offsets for prefill only
146 + if self._num_prefills > 0:
147 + #[batch,]
148 + has_initial_states_cpu = (
149 + self.runner.input_batch.
150 + num_computed_tokens_cpu_tensor[num_reqs -
151 + self._num_prefills:num_reqs]
152 + > 0)
153 + prep_initial_states = torch.any(has_initial_states_cpu).item()
154 + has_initial_states = has_initial_states_cpu.to(
155 + query_start_loc.device)
156 +
157 + query_start_loc_p = common_attn_metadata.query_start_loc[
158 + -self._num_prefills - 1:] - self._num_decode_tokens
159 +
160 + seq_idx = torch.repeat_interleave(
161 + torch.arange(self._num_prefills,
162 + dtype=torch.int32,
163 + device=query_start_loc_p.device),
164 + query_start_loc_p.diff(),
165 + output_size=self._num_prefill_tokens)
166 + seq_idx.unsqueeze_(0)
167 +
168 + # We compute metadata for chunked prefill once at the top level
169 + # model forward and reuse them in mamba layers. If not needed,
170 + # they will be ignored inside mamba kernels.
171 + if prep_initial_states:
172 + chunk_indices, chunk_offsets = (
173 + _query_start_loc_to_chunk_indices_offsets(
174 + query_start_loc_p, self.chunk_size,
175 + self._num_prefill_tokens))
176 +
177 + attn_metadata = Mamba2AttentionMetadata(
178 + num_prefills=self._num_prefills,
179 + num_prefill_tokens=self._num_prefill_tokens,
180 + num_decodes=self._num_decodes,
181 + num_decode_tokens=self._num_decode_tokens,
182 + query_start_loc=query_start_loc,
183 + seq_lens=seq_lens,
184 + has_initial_states=has_initial_states,
185 + prep_initial_states=prep_initial_states,
186 + chunk_size=self.chunk_size,
187 + seq_idx=seq_idx,
188 + chunk_indices=chunk_indices,
189 + chunk_offsets=chunk_offsets,
190 + state_indices_tensor=state_indices_tensor,
191 + )
192 + return attn_metadata
vllm/v1/core/single_type_kv_cache_manager.py
+42 -1
@@ -8,7 +8,7 @@ from vllm.utils import cdiv
8 from vllm.v1.core.block_pool import BlockPool
9 from vllm.v1.core.kv_cache_utils import BlockHash, KVCacheBlock
10 from vllm.v1.kv_cache_interface import (FullAttentionSpec, KVCacheSpec,
11 - SlidingWindowSpec)
11 + MambaSpec, SlidingWindowSpec)
12 from vllm.v1.request import Request
13
14
@@ -52,6 +52,7 @@ class SingleTypeKVCacheManager(ABC):
52
53 self.caching_hash_fn = caching_hash_fn
54 self.kv_cache_group_id = kv_cache_group_id
55 + self._null_block = block_pool.null_block
56
57 def get_num_blocks_to_allocate(
58 self, request_id: str, num_tokens: int,
@@ -390,9 +391,49 @@ class SlidingWindowManager(SingleTypeKVCacheManager):
391 return 0
392
393
394 +class MambaManager(SingleTypeKVCacheManager):
395 +
396 + @classmethod
397 + def find_longest_cache_hit(
398 + cls,
399 + block_hashes: list[BlockHash],
400 + max_length: int,
401 + kv_cache_group_ids: list[int],
402 + block_pool: BlockPool,
403 + kv_cache_spec: KVCacheSpec,
404 + use_eagle: bool,
405 + ) -> tuple[list[KVCacheBlock], ...]:
406 + assert isinstance(
407 + kv_cache_spec,
408 + MambaSpec), ("MambaManager can only be used for mamba groups")
409 + # Prefix caching is not supported for mamba now. Always return empty
410 + # list.
411 + computed_blocks: tuple[list[KVCacheBlock], ...] = tuple(
412 + [] for _ in range(len(kv_cache_group_ids)))
413 + return computed_blocks
414 +
415 + def remove_skipped_blocks(self, request_id: str,
416 + num_computed_tokens: int) -> None:
417 + # Each request will always have 1 block at this moment, so no need to
418 + # remove blocks.
419 + pass
420 +
421 + def get_num_common_prefix_blocks(self, request_id: str,
422 + num_running_requests: int) -> int:
423 + return 0
424 +
425 + def allocate_new_blocks(self, request_id: str,
426 + num_tokens: int) -> list[KVCacheBlock]:
427 + new_blocks = super().allocate_new_blocks(request_id, num_tokens)
428 + assert len(self.req_to_blocks[request_id]) == 1, (
429 + "MambaManager should only allocate 1 block for each request.")
430 + return new_blocks
431 +
432 +
433 spec_manager_map: dict[type[KVCacheSpec], type[SingleTypeKVCacheManager]] = {
434 FullAttentionSpec: FullAttentionManager,
435 SlidingWindowSpec: SlidingWindowManager,
436 + MambaSpec: MambaManager,
437 }
438
439
vllm/v1/kv_cache_interface.py
+24
@@ -3,6 +3,7 @@
3
4 import copy
5 from dataclasses import dataclass
6 +from math import prod
7 from typing import Optional
8
9 import torch
@@ -154,6 +155,29 @@ class SlidingWindowSpec(AttentionSpec):
155 return (cdiv(num_tokens, self.block_size) + 1) * self.page_size_bytes
156
157
158 +@dataclass
159 +class MambaSpec(KVCacheSpec):
160 + shapes: tuple[tuple[int, ...], ...]
161 + dtype: torch.dtype
162 +
163 + def __post_init__(self):
164 + self.num_elements = sum(prod(shape) for shape in self.shapes)
165 +
166 + @property
167 + def type_id(self) -> str:
168 + return f"mamba_{self.shapes}_{self.dtype}"
169 +
170 + @property
171 + def page_size_bytes(self) -> int:
172 + return self.num_elements * get_dtype_size(self.dtype)
173 +
174 + def max_memory_usage_bytes(self, vllm_config: VllmConfig) -> int:
175 + # We allocate 1 block for each request now, so max_memory_usage_bytes is
176 + # the same as page_size_bytes.
177 + # Need to update this when supporting prefix caching.
178 + return self.page_size_bytes
179 +
180 +
181 @dataclass
182 class KVCacheTensor:
183 """
vllm/v1/worker/gpu_model_runner.py
+68 -26
@@ -29,6 +29,7 @@ from vllm.distributed.parallel_state import (
29 from vllm.forward_context import (DPMetadata, get_forward_context,
30 set_forward_context)
31 from vllm.logger import init_logger
32 +from vllm.model_executor.layers.mamba.mamba_mixer2 import MambaMixer2
33 from vllm.model_executor.layers.rotary_embedding import MRotaryEmbedding
34 from vllm.model_executor.model_loader import TensorizerLoader, get_model_loader
35 from vllm.multimodal import MULTIMODAL_REGISTRY
@@ -38,12 +39,14 @@ from vllm.sampling_params import SamplingType
39 from vllm.sequence import IntermediateTensors
40 from vllm.utils import (STR_DTYPE_TO_TORCH_DTYPE, DeviceMemoryProfiler,
41 GiB_bytes, LazyLoader, async_tensor_h2d, cdiv,
41 - check_use_alibi, is_pin_memory_available)
42 + check_use_alibi, get_dtype_size,
43 + is_pin_memory_available)
44 +from vllm.v1.attention.backends.mamba_attn import Mamba2AttentionBackend
45 from vllm.v1.attention.backends.utils import (AttentionMetadataBuilder,
46 CommonAttentionMetadata)
47 from vllm.v1.core.encoder_cache_manager import compute_encoder_budget
48 from vllm.v1.kv_cache_interface import (AttentionSpec, FullAttentionSpec,
46 - KVCacheConfig, KVCacheSpec,
49 + KVCacheConfig, KVCacheSpec, MambaSpec,
50 SlidingWindowSpec)
51 from vllm.v1.outputs import (EMPTY_MODEL_RUNNER_OUTPUT, LogprobsTensors,
52 ModelRunnerOutput)
@@ -2093,28 +2096,31 @@ class GPUModelRunner(LoRAModelRunnerMixin):
2096 for i, kv_cache_group_spec in enumerate(
2097 kv_cache_config.kv_cache_groups):
2098 kv_cache_spec = kv_cache_group_spec.kv_cache_spec
2096 - if not isinstance(kv_cache_spec, AttentionSpec):
2097 - raise NotImplementedError(
2098 - "Only AttentionSpec is supported for now.")
2099 - attn_backend_i = get_attn_backend(
2100 - kv_cache_spec.head_size,
2101 - self.dtype,
2102 - kv_cache_spec.dtype,
2103 - kv_cache_spec.block_size,
2104 - self.model_config.is_attention_free,
2105 - use_mla=kv_cache_spec.use_mla,
2106 - )
2107 - if attn_backend_i is None:
2108 - error_msg = (
2109 - f"Error with get_attn_backend: {kv_cache_spec.head_size=}, "
2110 - f"{self.dtype=}, {kv_cache_spec.dtype=}, "
2111 - f"{kv_cache_spec.block_size=}, "
2112 - f"{self.model_config.is_attention_free=}, "
2113 - f"{kv_cache_spec.use_mla=}")
2114 - logger.error(error_msg)
2115 - raise NotImplementedError(
2116 - "Non-Attention backend is not supported by V1 "
2117 - "GPUModelRunner.")
2099 + if isinstance(kv_cache_spec, AttentionSpec):
2100 + attn_backend_i = get_attn_backend(
2101 + kv_cache_spec.head_size,
2102 + self.dtype,
2103 + kv_cache_spec.dtype,
2104 + kv_cache_spec.block_size,
2105 + self.model_config.is_attention_free,
2106 + use_mla=kv_cache_spec.use_mla,
2107 + )
2108 + if attn_backend_i is None:
2109 + error_msg = (f"Error with get_attn_backend: "
2110 + f"{kv_cache_spec.head_size=}, "
2111 + f"{self.dtype=}, {kv_cache_spec.dtype=}, "
2112 + f"{kv_cache_spec.block_size=}, "
2113 + f"{self.model_config.is_attention_free=}, "
2114 + f"{kv_cache_spec.use_mla=}")
2115 + logger.error(error_msg)
2116 + raise NotImplementedError(
2117 + "Non-Attention backend is not supported by V1 "
2118 + "GPUModelRunner.")
2119 + elif isinstance(kv_cache_spec, MambaSpec):
2120 + attn_backend_i = Mamba2AttentionBackend
2121 + else:
2122 + raise ValueError(
2123 + f"Unknown KV cache spec type: {type(kv_cache_spec)}")
2124
2125 block_table_i = self.input_batch.block_table[i]
2126 attn_metadata_builder_i = attn_backend_i.get_builder_cls()(
@@ -2242,6 +2248,22 @@ class GPUModelRunner(LoRAModelRunnerMixin):
2248 kv_caches[layer_name] = kv_cache_raw_tensors[
2249 layer_name].view(dtype).view(kv_cache_shape).permute(
2250 *inv_order)
2251 + elif isinstance(kv_cache_spec, MambaSpec):
2252 + raw_tensor = kv_cache_raw_tensors[layer_name]
2253 + dtype = kv_cache_spec.dtype
2254 + state_tensors = []
2255 + start_pos = 0
2256 + for shape in kv_cache_spec.shapes:
2257 + target_shape = (num_blocks, *shape)
2258 + size_in_bytes = np.prod(shape) * get_dtype_size(
2259 + dtype) * num_blocks
2260 + tensor = raw_tensor[start_pos:start_pos +
2261 + size_in_bytes]
2262 + tensor = tensor.view(dtype).view(target_shape)
2263 + state_tensors.append(tensor)
2264 + start_pos += size_in_bytes
2265 + assert start_pos == raw_tensor.numel()
2266 + kv_caches[layer_name] = tuple(state_tensors)
2267 else:
2268 raise NotImplementedError
2269 return kv_caches
@@ -2307,11 +2329,11 @@ class GPUModelRunner(LoRAModelRunnerMixin):
2329 format. Layers that do not need KV cache are not included.
2330 """
2331
2310 - layers = get_layers_from_vllm_config(self.vllm_config, Attention)
2332 block_size = self.vllm_config.cache_config.block_size
2333 use_mla = self.vllm_config.model_config.use_mla
2334 kv_cache_spec: dict[str, KVCacheSpec] = {}
2314 - for layer_name, attn_module in layers.items():
2335 + attn_layers = get_layers_from_vllm_config(self.vllm_config, Attention)
2336 + for layer_name, attn_module in attn_layers.items():
2337 if (kv_tgt_layer :=
2338 attn_module.kv_sharing_target_layer_name) is not None:
2339 # The layer doesn't need its own KV cache and will use that of
@@ -2351,4 +2373,24 @@ class GPUModelRunner(LoRAModelRunnerMixin):
2373 raise ValueError(
2374 f"Unknown attention type: {attn_module.attn_type}")
2375
2376 + mamba_layers = get_layers_from_vllm_config(self.vllm_config,
2377 + MambaMixer2)
2378 + if len(mamba_layers) > 0:
2379 + if self.vllm_config.speculative_config is not None:
2380 + raise NotImplementedError(
2381 + "Mamba with speculative decoding is not supported yet.")
2382 + if not self.vllm_config.model_config.enforce_eager:
2383 + raise NotImplementedError(
2384 + "Mamba with cuda graph is not supported yet.")
2385 + if self.vllm_config.cache_config.enable_prefix_caching:
2386 + raise NotImplementedError(
2387 + "Prefix caching is not supported for Mamba yet.")
2388 + max_model_len = self.vllm_config.model_config.max_model_len
2389 + # Set block_size to max_model_len, so that mamba model will always
2390 + # have only one block in the KV cache.
2391 + for layer_name, mamba_module in mamba_layers.items():
2392 + kv_cache_spec[layer_name] = MambaSpec(
2393 + shapes=mamba_module.get_state_shape(),
2394 + dtype=self.kv_cache_dtype,
2395 + block_size=max_model_len)
2396 return kv_cache_spec