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 2, 12:15 UTC
[Model]Support Step-3.7-Flash (#43859)
Signed-off-by: luotingdan <luotingdan@stepfun.com> Signed-off-by: Isotr0py <Isotr0py@outlook.com> Signed-off-by: Jee Jee Li <jeejeelee@inferact.ai> Co-authored-by: luotingdan <luotingdan@stepfun.com> Co-authored-by: Isotr0py <Isotr0py@outlook.com> Co-authored-by: Yu Huang <yuhuang@nvidia.com> Co-authored-by: Jee Jee Li <jeejeelee@inferact.ai>
ltd0924 committed
May 29, 2026 at 08:01 UTC
b690b2bb672b8a762b22a095cee1102082a58d42
15 files changed
+696
-25
docs/models/supported_models.md
+1
@@ -633,6 +633,7 @@ These models primarily accept the [`LLM.generate`](./generative_models.md#llmgen
633
| `SmolVLMForConditionalGeneration` | SmolVLM2 | T + I | `SmolVLM2-2.2B-Instruct` | ✅︎ | |
634
| `Step3VLForConditionalGeneration` | Step3-VL | T + I<sup>+</sup> | `stepfun-ai/step3` | | ✅︎ |
635
| `StepVLForConditionalGeneration` | Step3-VL-10B | T + I<sup>+</sup> | `stepfun-ai/Step3-VL-10B` | | ✅︎ |
636
+| `Step3p7ForConditionalGeneration` | Step-3.7-Flash | T + I<sup>+</sup> | `stepfun-ai/Step-3.7-Flash` | | ✅︎ |
637
| `TarsierForConditionalGeneration` | Tarsier | T + I<sup>E+</sup> | `omni-search/Tarsier-7b`, `omni-search/Tarsier-34b` | | ✅︎ |
638
| `Tarsier2ForConditionalGeneration`<sup>^</sup> | Tarsier2 | T + I<sup>E+</sup> + V<sup>E+</sup> | `omni-research/Tarsier2-Recap-7b`, `omni-research/Tarsier2-7b-0115` | | ✅︎ |
639
| `UltravoxModel` | Ultravox | T + A<sup>E+</sup> | `fixie-ai/ultravox-v0_5-llama-3_2-1b` | ✅︎ | ✅︎ |
tests/models/registry.py
+3
@@ -1368,6 +1368,9 @@ _MULTIMODAL_EXAMPLE_MODELS = {
1368
"StepVLForConditionalGeneration": _HfExamplesInfo(
1369
"stepfun-ai/Step3-VL-10B", trust_remote_code=True
1370
),
1371
+ "Step3p7ForConditionalGeneration": _HfExamplesInfo(
1372
+ "stepfun-ai/Step-3.7-Flash", is_available_online=False, trust_remote_code=True
1373
+ ),
1374
"UltravoxModel": _HfExamplesInfo(
1375
"fixie-ai/ultravox-v0_5-llama-3_2-1b",
1376
trust_remote_code=True,
vllm/config/speculative.py
+23
-2
@@ -485,7 +485,16 @@ class SpeculativeConfig:
485
{"n_predict": n_predict, "architectures": ["LongCatFlashMTPModel"]}
486
)
487
488
- if hf_config.model_type == "step3p5":
488
+ if hf_config.model_type in ("step3p5", "step3p7") or hf_config.architectures[
489
+ 0
490
+ ] in ("Step3p5ForCausalLM", "Step3p7ForConditionalGeneration"):
491
+ quantization_config = getattr(hf_config, "quantization_config", None)
492
+ hf_config = getattr(hf_config, "text_config", hf_config)
493
+ if (
494
+ quantization_config is not None
495
+ and getattr(hf_config, "quantization_config", None) is None
496
+ ):
497
+ hf_config.update({"quantization_config": quantization_config})
498
hf_config.model_type = "step3p5_mtp"
499
n_predict = getattr(hf_config, "num_nextn_predict_layers", 1)
500
hf_config.update({"n_predict": n_predict, "architectures": ["Step3p5MTP"]})
@@ -705,7 +714,11 @@ class SpeculativeConfig:
714
MTPModelTypes
715
):
716
self.method = "mtp"
708
- if self.num_speculative_tokens > 1:
717
+ if (
718
+ self.num_speculative_tokens > 1
719
+ and self.draft_model_config.hf_config.model_type
720
+ != "step3p5_mtp"
721
+ ):
722
logger.warning(
723
"Enabling num_speculative_tokens > 1 will run "
724
"multiple times of forward on same MTP layer"
@@ -1056,6 +1069,14 @@ class SpeculativeConfig:
1069
== "gemma4_mtp"
1070
)
1071
1072
+ def use_step3p5_mtp(self) -> bool:
1073
+ return (
1074
+ self.method == "mtp"
1075
+ and self.draft_model_config is not None
1076
+ and getattr(self.draft_model_config.hf_config, "model_type", None)
1077
+ == "step3p5_mtp"
1078
+ )
1079
+
1080
def use_eagle(self) -> bool:
1081
return self.method in ("eagle", "eagle3", "mtp", "dflash")
1082
vllm/model_executor/layers/fused_moe/experts/trtllm_fp8_moe.py
+1
-1
@@ -260,7 +260,7 @@ class TrtLlmFp8ExpertsMonolithic(TrtLlmFp8ExpertsBase, mk.FusedMoEExpertsMonolit
260
router_logits_dtype: torch.dtype | None,
261
routing_method: RoutingMethodType,
262
) -> bool:
263
- return True
263
+ return router_logits_dtype != torch.float32
264
265
@staticmethod
266
def _supports_routing_method(
vllm/model_executor/layers/fused_moe/experts/trtllm_nvfp4_moe.py
+1
-1
@@ -363,7 +363,7 @@ class TrtLlmNvFp4ExpertsMonolithic(
363
router_logits_dtype: torch.dtype | None,
364
routing_method: RoutingMethodType,
365
) -> bool:
366
- return True
366
+ return router_logits_dtype != torch.float32
367
368
def apply(
369
self,
vllm/model_executor/layers/fused_moe/layer.py
+13
@@ -974,6 +974,19 @@ class FusedMoE(PluggableLayer):
974
# this is needed for compressed-tensors only
975
loaded_weight = loaded_weight.to(param.data.device)
976
977
+ # ModelOpt NVFP4 stores w13 input scales as two logical shards.
978
+ # The generic assignment below would broadcast w1/w3 into the
979
+ # whole expert row, so the second shard would overwrite the first.
980
+ if (
981
+ "ModelOpt" in quant_method_name
982
+ and param.data.ndim == 2
983
+ and shard_id in ("w1", "w3")
984
+ ):
985
+ scale_expert_id = global_expert_id if use_global_sf else expert_id
986
+ scale_shard_id = 0 if shard_id == "w1" else 1
987
+ param.data[scale_expert_id][scale_shard_id] = loaded_weight.reshape(())
988
+ return True if return_success else None
989
+
990
if (
991
"compressed" in quant_method_name.lower()
992
and param.data[expert_id] != 1
vllm/model_executor/layers/quantization/modelopt.py
+13
-1
@@ -192,7 +192,11 @@ class ModelOptQuantConfigBase(QuantizationConfig):
192
# exclude_modules config. But need to keep them for loading quantized
193
# checkpoints generated by older versions. Then check substring matching
194
# for patterns not caught by exact match
195
- if "vision_tower" in prefix or "vision_model" in prefix:
195
+ if (
196
+ "vision_tower" in prefix
197
+ or "vision_model" in prefix
198
+ or "vit_large_projector" in prefix
199
+ ):
200
return UnquantizedLinearMethod()
201
202
# now, the layer is quantized, handle it here
@@ -2340,6 +2344,14 @@ class ModelOptMixedPrecisionConfig(ModelOptQuantConfigBase):
2344
if key.startswith(prefix_dot):
2345
return info["quant_algo"].upper()
2346
2347
+ # FusedMoE expert prefix is e.g. "...moe.experts", while ModelOpt's
2348
+ # quantized_layers entries use "...moe.gate_proj" / "...moe.up_proj".
2349
+ if prefix.endswith(".experts"):
2350
+ parent_dot = prefix.rsplit(".experts", 1)[0] + "."
2351
+ for key, info in self.quantized_layers.items():
2352
+ if key.startswith(parent_dot):
2353
+ return info["quant_algo"].upper()
2354
+
2355
return None
2356
2357
@staticmethod
vllm/model_executor/models/registry.py
+1
@@ -567,6 +567,7 @@ _MULTIMODAL_MODELS = {
567
"SmolVLMForConditionalGeneration": ("smolvlm", "SmolVLMForConditionalGeneration"),
568
"StepVLForConditionalGeneration": ("step_vl", "StepVLForConditionalGeneration"),
569
"Step3VLForConditionalGeneration": ("step3_vl", "Step3VLForConditionalGeneration"),
570
+ "Step3p7ForConditionalGeneration": ("step3p7", "Step3p7ForConditionalGeneration"),
571
"TarsierForConditionalGeneration": ("tarsier", "TarsierForConditionalGeneration"),
572
"Tarsier2ForConditionalGeneration": (
573
"qwen2_vl",
vllm/model_executor/models/step3p5.py
+53
-1
@@ -637,6 +637,54 @@ class Step3p5Model(nn.Module):
637
(f".moe.experts.{base_layer}w13_weight", ".moe.gate_proj.weight", "w1"),
638
(f".moe.experts.{base_layer}w13_weight", ".moe.up_proj.weight", "w3"),
639
(f".moe.experts.{base_layer}w2_weight", ".moe.down_proj.weight", "w2"),
640
+ (
641
+ f".moe.experts.{base_layer}w13_weight_scale_2",
642
+ ".moe.gate_proj.weight_scale_2",
643
+ "w1",
644
+ ),
645
+ (
646
+ f".moe.experts.{base_layer}w13_weight_scale_2",
647
+ ".moe.up_proj.weight_scale_2",
648
+ "w3",
649
+ ),
650
+ (
651
+ f".moe.experts.{base_layer}w2_weight_scale_2",
652
+ ".moe.down_proj.weight_scale_2",
653
+ "w2",
654
+ ),
655
+ (
656
+ f".moe.experts.{base_layer}w13_weight_scale",
657
+ ".moe.gate_proj.weight_scale",
658
+ "w1",
659
+ ),
660
+ (
661
+ f".moe.experts.{base_layer}w13_weight_scale",
662
+ ".moe.up_proj.weight_scale",
663
+ "w3",
664
+ ),
665
+ (
666
+ f".moe.experts.{base_layer}w2_weight_scale",
667
+ ".moe.down_proj.weight_scale",
668
+ "w2",
669
+ ),
670
+ # Required due to the Step3 HF model's packed expert format:
671
+ # input scales are stored as moe.{gate,up,down}_proj.input_scale
672
+ # rather than the standard per-expert format handled generically.
673
+ (
674
+ f".moe.experts.{base_layer}w13_input_scale",
675
+ ".moe.gate_proj.input_scale",
676
+ "w1",
677
+ ),
678
+ (
679
+ f".moe.experts.{base_layer}w13_input_scale",
680
+ ".moe.up_proj.input_scale",
681
+ "w3",
682
+ ),
683
+ (
684
+ f".moe.experts.{base_layer}w2_input_scale",
685
+ ".moe.down_proj.input_scale",
686
+ "w2",
687
+ ),
688
]
689
690
# New per-expert format: .moe.experts.E.gate_proj.weight_packed [out, in]
@@ -756,7 +804,11 @@ class Step3p5Model(nn.Module):
804
# Per-tensor global scales (e.g. weight_global_scale)
805
# have shape [1] in compressed-tensors NVFP4 checkpoints.
806
# Expand to per-expert before the iteration loop.
759
- if (
807
+ if loaded_weight.ndim == 0:
808
+ loaded_weight = loaded_weight.unsqueeze(0).expand(
809
+ moe_expert_num
810
+ )
811
+ elif (
812
loaded_weight.shape[0] == 1
813
and loaded_weight.shape[0] != moe_expert_num
814
):
vllm/model_executor/models/step3p7.py
new
+90
@@ -0,0 +1,90 @@
1
+# SPDX-License-Identifier: Apache-2.0
2
+# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
+"""Inference-only Jurassic model."""
4
+
5
+import torch
6
+
7
+from vllm.config import VllmConfig
8
+from vllm.logger import init_logger
9
+from vllm.model_executor.layers.activation import get_act_fn
10
+from vllm.model_executor.layers.linear import ColumnParallelLinear
11
+
12
+from .step3_vl import Step3VLForConditionalGeneration
13
+from .step_vl import PerceptionEncoder
14
+from .utils import WeightsMapper, init_vllm_registered_model, maybe_prefix
15
+from .vision import run_dp_sharded_vision_model
16
+
17
+logger = init_logger(__name__)
18
+
19
+
20
+class Step3p7ForConditionalGeneration(Step3VLForConditionalGeneration):
21
+ hf_to_vllm_mapper = WeightsMapper(
22
+ orig_to_new_prefix={
23
+ "model.vision_model.": "vision_model.",
24
+ "model.vit_large_projector.": "vit_large_projector.",
25
+ "model.vit_large_projector": "vit_large_projector",
26
+ "model.language_model.": "language_model.model.",
27
+ "model.language_model": "language_model.model",
28
+ "model.": "language_model.model.",
29
+ "lm_head.": "language_model.lm_head.",
30
+ "lm_head": "language_model.lm_head",
31
+ },
32
+ orig_to_new_substr={
33
+ ".attn.in_proj_weight": ".attn.qkv_proj.weight",
34
+ ".attn.in_proj_bias": ".attn.qkv_proj.bias",
35
+ ".mlp.c_fc": ".mlp.fc1",
36
+ ".mlp.c_proj": ".mlp.fc2",
37
+ },
38
+ )
39
+
40
+ def __init__(self, *, vllm_config: VllmConfig, prefix: str = "") -> None:
41
+ super(Step3VLForConditionalGeneration, self).__init__()
42
+
43
+ config = vllm_config.model_config.hf_config
44
+ multimodal_config = vllm_config.model_config.multimodal_config
45
+ quant_config = vllm_config.quant_config
46
+
47
+ self.config = config
48
+ self.multimodal_config = multimodal_config
49
+ self.use_data_parallel = multimodal_config.mm_encoder_tp_mode == "data"
50
+
51
+ with self._mark_tower_model(vllm_config, "image"):
52
+ self.vision_model = PerceptionEncoder(
53
+ config.vision_config,
54
+ get_act_fn(config.vision_config.hidden_act),
55
+ quant_config=quant_config,
56
+ prefix=maybe_prefix(prefix, "vision_model"),
57
+ )
58
+ self.vit_large_projector = ColumnParallelLinear(
59
+ config.vision_config.width * 4,
60
+ config.text_config.hidden_size,
61
+ bias=config.projector_bias,
62
+ gather_output=True,
63
+ quant_config=quant_config,
64
+ prefix=maybe_prefix(prefix, "vit_large_projector"),
65
+ disable_tp=self.use_data_parallel,
66
+ )
67
+
68
+ with self._mark_language_model(vllm_config):
69
+ self.language_model = init_vllm_registered_model(
70
+ vllm_config=vllm_config,
71
+ hf_config=config.text_config,
72
+ prefix=maybe_prefix(prefix, "language_model"),
73
+ )
74
+
75
+ self.make_empty_intermediate_tensors = (
76
+ self.language_model.make_empty_intermediate_tensors
77
+ )
78
+
79
+ def _get_vision_model_output(
80
+ self, input_tensor: torch.Tensor | None
81
+ ) -> torch.Tensor | None:
82
+ if input_tensor is None:
83
+ return None
84
+ if self.use_data_parallel:
85
+ return run_dp_sharded_vision_model(input_tensor, self.vision_model)
86
+ return self.vision_model(input_tensor)
87
+
88
+ def _process_image_features(self, image_features: torch.Tensor) -> torch.Tensor:
89
+ image_features, _ = self.vit_large_projector(image_features)
90
+ return image_features
vllm/tokenizers/registry.py
+5
-1
@@ -38,7 +38,7 @@ logger = init_logger(__name__)
38
# temporary workaround and better long term solutions are:
39
# - Add model type to MODELS_WITH_INCORRECT_HUB_TOKENIZER_CLASS in transformers (better)
40
# - Fix tokenizer_class on the hub for the affected models (best)
41
-_MODEL_TYPES_WITH_INCORRECT_TOKENIZER_CLASS: set[str] = {"step3_vl"}
41
+_MODEL_TYPES_WITH_INCORRECT_TOKENIZER_CLASS: set[str] = {"step3_vl", "step3p7"}
42
43
_VLLM_TOKENIZERS = {
44
"deepseek_v32": ("deepseek_v32", "DeepseekV32Tokenizer"),
@@ -249,6 +249,10 @@ def get_tokenizer(
249
tokenizer_cls_ = tokenizer_cls
250
251
tokenizer = tokenizer_cls_.from_pretrained(tokenizer_name, *args, **kwargs)
252
+ if model_type in _MODEL_TYPES_WITH_INCORRECT_TOKENIZER_CLASS:
253
+ from vllm.tokenizers.hf import get_cached_tokenizer
254
+
255
+ tokenizer = get_cached_tokenizer(tokenizer)
256
if not tokenizer.is_fast:
257
logger.warning(
258
"Using a slow tokenizer. This might cause a significant "
vllm/transformers_utils/model_arch_config_convertor.py
+23
-17
@@ -502,6 +502,11 @@ class Qwen3_5MTPModelArchConfigConvertor(ModelArchConfigConvertorBase):
502
return getattr(self.hf_text_config, "mtp_num_hidden_layers", 0)
503
504
505
+class Step3p5MTPModelArchConfigConvertor(ModelArchConfigConvertorBase):
506
+ def get_num_hidden_layers(self) -> int:
507
+ return getattr(self.hf_text_config, "num_nextn_predict_layers", 0)
508
+
509
+
510
class PanguUltraMoeMTPModelArchConfigConvertor(ModelArchConfigConvertorBase):
511
def get_num_hidden_layers(self) -> int:
512
return getattr(self.hf_text_config, "num_nextn_predict_layers", 0)
@@ -543,31 +548,32 @@ class Gemma4ModelArchConfigConvertor(ModelArchConfigConvertorBase):
548
# hf_config.model_type -> convertor class
549
MODEL_ARCH_CONFIG_CONVERTORS = {
550
"cohere_asr": CohereAsrModelArchConfigConvertor,
546
- "mamba": MambaModelArchConfigConvertor,
547
- "falcon_mamba": MambaModelArchConfigConvertor,
548
- "timm_wrapper": TerratorchModelArchConfigConvertor,
549
- "medusa": MedusaModelArchConfigConvertor,
550
- "zamba2": Zamba2ModelArchConfigConvertor,
551
- "mpt": MPTModelArchConfigConvertor,
551
"dbrx": DbrxModelArchConfigConvertor,
552
+ "deepseek_mtp": DeepSeekMTPModelArchConfigConvertor,
553
+ "ernie_mtp": ErnieMTPModelArchConfigConvertor,
554
"falcon": FalconModelArchConfigConvertor,
555
+ "falcon_mamba": MambaModelArchConfigConvertor,
556
"gemma4": Gemma4ModelArchConfigConvertor,
555
- "gemma4_text": Gemma4ModelArchConfigConvertor,
557
"gemma4_mtp": Gemma4MTPModelArchConfigConvertor,
557
- "RefinedWeb": FalconModelArchConfigConvertor,
558
- "RefinedWebModel": FalconModelArchConfigConvertor,
559
- "nemotron-nas": NemotronNasModelArchConfigConvertor,
560
- "deepseek_mtp": DeepSeekMTPModelArchConfigConvertor,
561
- "qwen3_next_mtp": Qwen3NextMTPModelArchConfigConvertor,
562
- "qwen3_5_mtp": Qwen3_5MTPModelArchConfigConvertor,
558
+ "gemma4_text": Gemma4ModelArchConfigConvertor,
559
+ "glm4_moe_mtp": GLM4MoeMTPModelArchConfigConvertor,
560
+ "glm_ocr_mtp": GLM4MoeMTPModelArchConfigConvertor,
561
+ "longcat_flash_mtp": LongCatFlashMTPModelArchConfigConvertor,
562
+ "mamba": MambaModelArchConfigConvertor,
563
+ "medusa": MedusaModelArchConfigConvertor,
564
"mimo_mtp": MimoMTPModelArchConfigConvertor,
565
"mimo_v2": MimoV2ModelArchConfigConvertor,
566
"mimo_v2_flash": MimoV2ModelArchConfigConvertor,
567
"mimo_v2_mtp": MimoV2MTPModelArchConfigConvertor,
568
"mimo_v2_omni_mtp": MimoV2MTPModelArchConfigConvertor,
568
- "glm4_moe_mtp": GLM4MoeMTPModelArchConfigConvertor,
569
- "glm_ocr_mtp": GLM4MoeMTPModelArchConfigConvertor,
570
- "ernie_mtp": ErnieMTPModelArchConfigConvertor,
569
+ "mpt": MPTModelArchConfigConvertor,
570
+ "nemotron-nas": NemotronNasModelArchConfigConvertor,
571
"pangu_ultra_moe_mtp": PanguUltraMoeMTPModelArchConfigConvertor,
572
- "longcat_flash_mtp": LongCatFlashMTPModelArchConfigConvertor,
572
+ "qwen3_5_mtp": Qwen3_5MTPModelArchConfigConvertor,
573
+ "qwen3_next_mtp": Qwen3NextMTPModelArchConfigConvertor,
574
+ "RefinedWeb": FalconModelArchConfigConvertor,
575
+ "RefinedWebModel": FalconModelArchConfigConvertor,
576
+ "step3p5_mtp": Step3p5MTPModelArchConfigConvertor,
577
+ "timm_wrapper": TerratorchModelArchConfigConvertor,
578
+ "zamba2": Zamba2ModelArchConfigConvertor,
579
}
vllm/v1/spec_decode/llm_base_proposer.py
+1
@@ -1232,6 +1232,7 @@ class SpecDecodeBaseProposer:
1232
"Qwen3VLForConditionalGeneration",
1233
"Qwen3VLMoeForConditionalGeneration",
1234
"Gemma4ForConditionalGeneration",
1235
+ "Step3p7ForConditionalGeneration",
1236
]:
1237
self.model.config.image_token_index = target_model.config.image_token_id
1238
elif self.get_model_name(target_model) == "PixtralForConditionalGeneration":
vllm/v1/spec_decode/step3p5.py
new
+459
@@ -0,0 +1,459 @@
1
+# SPDX-License-Identifier: Apache-2.0
2
+# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
+
4
+from copy import copy
5
+
6
+import torch
7
+
8
+from vllm.config import VllmConfig, get_layers_from_vllm_config, replace
9
+from vllm.forward_context import set_forward_context
10
+from vllm.model_executor.layers.attention_layer_base import AttentionLayerBase
11
+from vllm.model_executor.models.utils import get_draft_quant_config
12
+from vllm.v1.attention.backend import CommonAttentionMetadata
13
+from vllm.v1.kv_cache_interface import (
14
+ KVCacheConfig,
15
+ KVCacheSpec,
16
+ UniformTypeKVCacheSpecs,
17
+)
18
+from vllm.v1.sample.metadata import SamplingMetadata
19
+from vllm.v1.spec_decode.eagle import EagleProposer
20
+from vllm.v1.spec_decode.utils import PADDING_SLOT_ID
21
+from vllm.v1.worker.utils import AttentionGroup
22
+
23
+
24
+class Step3p5MTPProposer(EagleProposer):
25
+ """Step3.5 MTP proposer with per-layer draft-step selection."""
26
+
27
+ def __init__(
28
+ self,
29
+ vllm_config: VllmConfig,
30
+ device: torch.device,
31
+ runner=None,
32
+ ):
33
+ super().__init__(vllm_config, device, runner)
34
+ self._per_group_block_tables: dict[int, torch.Tensor] = {}
35
+ self._per_group_slot_mappings: dict[int, torch.Tensor] = {}
36
+ # Slot-mapping buffers for non-primary KV cache groups (the primary
37
+ # group reuses self._slot_mapping_buffer from the base class).
38
+ self._per_group_slot_mapping_buffers: dict[int, torch.Tensor] = {}
39
+
40
+ def set_per_group_attn_metadata(
41
+ self,
42
+ gid: int,
43
+ block_table: torch.Tensor,
44
+ slot_mapping: torch.Tensor,
45
+ ) -> None:
46
+ self._per_group_block_tables[gid] = block_table
47
+ self._per_group_slot_mappings[gid] = slot_mapping
48
+
49
+ def _slot_mapping_buffer_for(self, gid: int) -> torch.Tensor:
50
+ if gid == self.kv_cache_gid:
51
+ return self._slot_mapping_buffer
52
+ buf = self._per_group_slot_mapping_buffers.get(gid)
53
+ if buf is None:
54
+ buf = torch.zeros(self.max_positions, dtype=torch.int64, device=self.device)
55
+ self._per_group_slot_mapping_buffers[gid] = buf
56
+ return buf
57
+
58
+ def _get_slot_mapping(
59
+ self,
60
+ num_tokens: int,
61
+ slot_mapping: torch.Tensor | None = None,
62
+ ) -> dict[str, torch.Tensor]:
63
+ """Per-layer slot_mapping with one buffer per KV cache group."""
64
+ per_layer: dict[str, torch.Tensor] = {}
65
+ for attn_group in self.draft_attn_groups:
66
+ gid = attn_group.kv_cache_group_id
67
+ buf = self._slot_mapping_buffer_for(gid)
68
+ source = self._per_group_slot_mappings.get(gid, slot_mapping)
69
+ if source is not None and buf.data_ptr() != source.data_ptr():
70
+ n = source.shape[0]
71
+ buf[:n].copy_(source)
72
+ if num_tokens > n:
73
+ buf[n:num_tokens].fill_(PADDING_SLOT_ID)
74
+ view = buf[:num_tokens]
75
+ for layer_name in attn_group.layer_names:
76
+ per_layer[layer_name] = view
77
+ return per_layer
78
+
79
+ def _update_positions_dependent_metadata(
80
+ self,
81
+ positions: torch.Tensor,
82
+ common_attn_metadata: CommonAttentionMetadata,
83
+ batch_size: int,
84
+ input_batch_size: int,
85
+ block_size: int,
86
+ ) -> torch.Tensor:
87
+ old_positions_1d = positions[0] if self.uses_mrope else positions
88
+ positions = super()._update_positions_dependent_metadata(
89
+ positions,
90
+ common_attn_metadata,
91
+ batch_size,
92
+ input_batch_size,
93
+ block_size,
94
+ )
95
+ # Parent already produced slot_mapping for the primary gid.
96
+ self._per_group_slot_mappings[self.kv_cache_gid] = (
97
+ common_attn_metadata.slot_mapping
98
+ )
99
+ # Recompute slot_mapping for the remaining gids using their own block tables.
100
+ new_positions_1d = positions[0] if self.uses_mrope else positions
101
+ exceeds = old_positions_1d + 1 >= self.max_model_len
102
+ for attn_group in self.draft_attn_groups:
103
+ gid = attn_group.kv_cache_group_id
104
+ if gid == self.kv_cache_gid:
105
+ continue
106
+ block_table = self._per_group_block_tables.get(gid)
107
+ if block_table is None:
108
+ continue
109
+ n_blocks = block_table.shape[1]
110
+ bn = (new_positions_1d // block_size).clamp(max=n_blocks - 1).to(torch.long)
111
+ block_ids = block_table[:batch_size].gather(1, bn.unsqueeze(1)).squeeze(1)
112
+ sm = block_ids * block_size + (new_positions_1d % block_size)
113
+ sm.masked_fill_(exceeds, PADDING_SLOT_ID)
114
+ buf = self._slot_mapping_buffer_for(gid)
115
+ buf[:batch_size].copy_(sm)
116
+ if input_batch_size > batch_size:
117
+ buf[batch_size:input_batch_size].fill_(PADDING_SLOT_ID)
118
+ self._per_group_slot_mappings[gid] = buf[:batch_size]
119
+ return positions
120
+
121
+ def build_per_group_and_layer_attn_metadata(
122
+ self,
123
+ common_attn_metadata: CommonAttentionMetadata,
124
+ draft_index: int = 0,
125
+ ) -> tuple[list[object], dict[str, object]]:
126
+ per_group_attn_metadata: list[object] = []
127
+ per_layer_attn_metadata: dict[str, object] = {}
128
+ # The proposer always works in unpadded shape. Per-group block tables
129
+ # registered via set_per_group_attn_metadata are stored at the model
130
+ # runner's padded shape; slice them to match cm's num_reqs.
131
+ num_reqs = common_attn_metadata.num_reqs
132
+ num_actual_tokens = common_attn_metadata.num_actual_tokens
133
+ for attn_group in self.draft_attn_groups:
134
+ gid = attn_group.kv_cache_group_id
135
+ if gid in self._per_group_block_tables:
136
+ cm = copy(common_attn_metadata)
137
+ cm.block_table_tensor = self._per_group_block_tables[gid][:num_reqs]
138
+ if gid in self._per_group_slot_mappings:
139
+ sm = self._per_group_slot_mappings[gid]
140
+ if sm.shape[0] >= num_actual_tokens:
141
+ sm = sm[:num_actual_tokens]
142
+ cm.slot_mapping = sm
143
+ else:
144
+ cm = common_attn_metadata
145
+ attn_metadata = attn_group.get_metadata_builder().build_for_drafting(
146
+ common_attn_metadata=cm,
147
+ draft_index=draft_index,
148
+ )
149
+ per_group_attn_metadata.append(attn_metadata)
150
+ for layer_name in attn_group.layer_names:
151
+ per_layer_attn_metadata[layer_name] = attn_metadata
152
+ return per_group_attn_metadata, per_layer_attn_metadata
153
+
154
+ def _maybe_share_lm_head(self, target_language_model: torch.nn.Module) -> None:
155
+ """Step3.5 MTP uses the lm_head stored in each MTP layer."""
156
+
157
+ # The base MTP path shares target lm_head into shared_head.head.
158
+ # Step3.5 checkpoints carry per-MTP-layer shared_head weights.
159
+ return
160
+
161
+ def _create_draft_vllm_config(self) -> VllmConfig:
162
+ base = super()._create_draft_vllm_config()
163
+ return replace(
164
+ base,
165
+ model_config=self.draft_model_config,
166
+ quant_config=get_draft_quant_config(base),
167
+ )
168
+
169
+ def validate_same_kv_cache_group(self, kv_cache_config: KVCacheConfig) -> None:
170
+ """Step3.5 MTP draft layers may span multiple KV cache groups."""
171
+ return
172
+
173
+ def initialize_attn_backend(
174
+ self,
175
+ kv_cache_config: KVCacheConfig,
176
+ kernel_block_sizes: list[int] | None = None,
177
+ ) -> None:
178
+ all_attn_layers = get_layers_from_vllm_config(
179
+ self.vllm_config,
180
+ AttentionLayerBase, # type: ignore[type-abstract]
181
+ )
182
+
183
+ layer_to_gid: dict[str, int] = {}
184
+ layer_to_spec: dict[str, KVCacheSpec] = {}
185
+ for gid, group in enumerate(kv_cache_config.kv_cache_groups):
186
+ group_spec = group.kv_cache_spec
187
+ for layer_name in group.layer_names:
188
+ layer_to_gid[layer_name] = gid
189
+ if isinstance(group_spec, UniformTypeKVCacheSpecs):
190
+ if layer_name in group_spec.kv_cache_specs:
191
+ layer_to_spec[layer_name] = group_spec.kv_cache_specs[
192
+ layer_name
193
+ ]
194
+ else:
195
+ target_layer_name = getattr(
196
+ all_attn_layers.get(layer_name),
197
+ "kv_sharing_target_layer_name",
198
+ None,
199
+ )
200
+ if (
201
+ target_layer_name
202
+ and target_layer_name in group_spec.kv_cache_specs
203
+ ):
204
+ layer_to_spec[layer_name] = group_spec.kv_cache_specs[
205
+ target_layer_name
206
+ ]
207
+ else:
208
+ layer_to_spec[layer_name] = group_spec
209
+ else:
210
+ layer_to_spec[layer_name] = group_spec
211
+
212
+ attention_groups: dict[tuple[tuple[str, str], int], AttentionGroup] = {}
213
+ for layer_name in sorted(self._draft_attn_layer_names):
214
+ if layer_name not in layer_to_spec:
215
+ continue
216
+ attn_layer = all_attn_layers[layer_name]
217
+ attn_backend = attn_layer.get_attn_backend()
218
+ spec = layer_to_spec[layer_name]
219
+ gid = layer_to_gid[layer_name]
220
+ group_key = (attn_backend.full_cls_name(), gid)
221
+
222
+ if group_key not in attention_groups:
223
+ kernel_block_size = (
224
+ kernel_block_sizes[gid]
225
+ if kernel_block_sizes is not None and gid < len(kernel_block_sizes)
226
+ else None
227
+ )
228
+ attn_group = AttentionGroup(
229
+ backend=attn_backend,
230
+ layer_names=[layer_name],
231
+ kv_cache_spec=spec,
232
+ kv_cache_group_id=gid,
233
+ )
234
+ attn_group.create_metadata_builders(
235
+ self.vllm_config,
236
+ self.device,
237
+ kernel_block_size=kernel_block_size,
238
+ )
239
+ attention_groups[group_key] = attn_group
240
+ else:
241
+ attention_groups[group_key].layer_names.append(layer_name)
242
+
243
+ self.draft_attn_groups = list(attention_groups.values())
244
+ if self.draft_attn_groups:
245
+ self.kv_cache_gid = self.draft_attn_groups[0].kv_cache_group_id
246
+ self.block_size = (
247
+ self.draft_attn_groups[0]
248
+ .get_metadata_builder()
249
+ .kv_cache_spec.block_size
250
+ )
251
+ else:
252
+ self.kv_cache_gid = 0
253
+ self.block_size = kv_cache_config.kv_cache_groups[
254
+ 0
255
+ ].kv_cache_spec.block_size
256
+
257
+ def _sample_draft_tokens_for_step(
258
+ self,
259
+ hidden_states: torch.Tensor,
260
+ sampling_metadata: SamplingMetadata,
261
+ spec_step_idx: int,
262
+ ) -> tuple[torch.Tensor, torch.Tensor | None]:
263
+ if not self._enable_probabilistic_draft_probs or sampling_metadata.all_greedy:
264
+ if self.use_local_argmax_reduction:
265
+ return self.model.get_top_tokens(hidden_states), None
266
+ logits = self.model.compute_logits(
267
+ hidden_states, spec_step_idx=spec_step_idx
268
+ )
269
+ return logits.argmax(dim=-1), None
270
+
271
+ logits = self.model.compute_logits(hidden_states, spec_step_idx=spec_step_idx)
272
+ return self._sample_from_logits(logits, sampling_metadata)
273
+
274
+ def propose(
275
+ self,
276
+ target_token_ids: torch.Tensor,
277
+ target_positions: torch.Tensor,
278
+ target_hidden_states: torch.Tensor,
279
+ next_token_ids: torch.Tensor,
280
+ token_indices_to_sample: torch.Tensor | None,
281
+ common_attn_metadata: CommonAttentionMetadata,
282
+ sampling_metadata: SamplingMetadata,
283
+ mm_embed_inputs: tuple[list[torch.Tensor], torch.Tensor] | None = None,
284
+ num_rejected_tokens_gpu: torch.Tensor | None = None,
285
+ slot_mappings: dict[str, torch.Tensor]
286
+ | list[dict[str, torch.Tensor]]
287
+ | None = None,
288
+ ) -> torch.Tensor:
289
+ self._last_draft_probs = None
290
+ batch_size = common_attn_metadata.batch_size()
291
+
292
+ num_tokens, token_indices_to_sample, common_attn_metadata = (
293
+ self.set_inputs_first_pass(
294
+ target_token_ids=target_token_ids,
295
+ next_token_ids=next_token_ids,
296
+ target_positions=target_positions,
297
+ target_hidden_states=target_hidden_states,
298
+ token_indices_to_sample=token_indices_to_sample,
299
+ cad=common_attn_metadata,
300
+ num_rejected_tokens_gpu=num_rejected_tokens_gpu,
301
+ )
302
+ )
303
+
304
+ per_group_attn_metadata, per_layer_attn_metadata = (
305
+ self.build_per_group_and_layer_attn_metadata(common_attn_metadata)
306
+ )
307
+
308
+ cudagraph_runtime_mode, num_input_tokens, num_tokens_across_dp = (
309
+ self._determine_batch_execution_and_padding(num_tokens)
310
+ )
311
+
312
+ model_kwargs, slot_mapping_size = self.build_model_inputs_first_pass(
313
+ num_tokens, num_input_tokens, mm_embed_inputs
314
+ )
315
+ model_kwargs["spec_step_idx"] = 0
316
+
317
+ with set_forward_context(
318
+ per_layer_attn_metadata,
319
+ self.vllm_config,
320
+ num_tokens=num_input_tokens,
321
+ num_tokens_across_dp=num_tokens_across_dp,
322
+ cudagraph_runtime_mode=cudagraph_runtime_mode,
323
+ slot_mapping=self._get_slot_mapping(
324
+ slot_mapping_size, common_attn_metadata.slot_mapping
325
+ ),
326
+ ):
327
+ ret_hidden_states = self.model(**model_kwargs)
328
+ if not self.model_returns_tuple():
329
+ last_hidden_states = ret_hidden_states
330
+ hidden_states = last_hidden_states
331
+ else:
332
+ last_hidden_states, hidden_states = ret_hidden_states
333
+
334
+ sample_hidden_states = last_hidden_states[token_indices_to_sample]
335
+
336
+ if self.num_speculative_tokens == 1 or self.parallel_drafting:
337
+ draft_token_ids, draft_probs = self._sample_draft_tokens_for_step(
338
+ sample_hidden_states, sampling_metadata, spec_step_idx=0
339
+ )
340
+ if draft_probs is not None:
341
+ self._last_draft_probs = draft_probs.view(
342
+ -1, self.num_speculative_tokens, draft_probs.shape[-1]
343
+ ).contiguous()
344
+ return draft_token_ids.view(-1, self.num_speculative_tokens)
345
+
346
+ if self.uses_mrope:
347
+ positions = self.mrope_positions[:, token_indices_to_sample]
348
+ else:
349
+ positions = self.positions[token_indices_to_sample]
350
+ hidden_states = hidden_states[token_indices_to_sample]
351
+
352
+ if self.constant_draft_positions:
353
+ self.positions[:batch_size] = positions
354
+
355
+ draft_token_ids, draft_probs = self._sample_draft_tokens_for_step(
356
+ sample_hidden_states, sampling_metadata, spec_step_idx=0
357
+ )
358
+ draft_probs_list = None if draft_probs is None else [draft_probs]
359
+
360
+ if self.allowed_attn_types is not None:
361
+ for group_md in per_group_attn_metadata:
362
+ if not isinstance(group_md, self.allowed_attn_types):
363
+ raise ValueError(
364
+ f"Unsupported attention metadata type for speculative "
365
+ "decoding with num_speculative_tokens > 1: "
366
+ f"{type(group_md)}. Supported types are: "
367
+ f"{self.allowed_attn_types}"
368
+ )
369
+
370
+ draft_token_ids_list = [draft_token_ids]
371
+
372
+ cudagraph_runtime_mode, input_batch_size, batch_size_across_dp = (
373
+ self._determine_batch_execution_and_padding(batch_size)
374
+ )
375
+
376
+ common_attn_metadata.num_actual_tokens = batch_size
377
+ common_attn_metadata.max_query_len = 1
378
+ common_attn_metadata.query_start_loc = self.arange[: batch_size + 1]
379
+ common_attn_metadata.query_start_loc_cpu = torch.from_numpy(
380
+ self.token_arange_np[: batch_size + 1]
381
+ ).clone()
382
+
383
+ if self.num_speculative_tokens > 1 and num_rejected_tokens_gpu is not None:
384
+ common_attn_metadata.seq_lens -= num_rejected_tokens_gpu
385
+ common_attn_metadata._seq_lens_cpu = None
386
+ common_attn_metadata._num_computed_tokens_cpu = None
387
+
388
+ block_size = self.block_size
389
+ assert block_size > 0, "block_size has not been initialized."
390
+ for token_index in range(self.num_speculative_tokens - 1):
391
+ spec_step_idx = token_index + 1
392
+ input_ids = draft_token_ids_list[-1].int()
393
+
394
+ if not self.constant_draft_positions:
395
+ positions = self._update_positions_dependent_metadata(
396
+ positions,
397
+ common_attn_metadata,
398
+ batch_size,
399
+ input_batch_size,
400
+ block_size,
401
+ )
402
+
403
+ if not self.constant_draft_positions or token_index == 0:
404
+ _, per_layer_attn_metadata = (
405
+ self.build_per_group_and_layer_attn_metadata(
406
+ common_attn_metadata, draft_index=spec_step_idx
407
+ )
408
+ )
409
+
410
+ self.input_ids[:batch_size] = input_ids
411
+ self.hidden_states[:batch_size] = hidden_states
412
+ if self.supports_mm_inputs:
413
+ self.inputs_embeds[:batch_size] = self.model.embed_input_ids(input_ids)
414
+
415
+ input_ids = None
416
+ inputs_embeds = self.inputs_embeds[:input_batch_size]
417
+ else:
418
+ input_ids = self.input_ids[:input_batch_size]
419
+ inputs_embeds = None
420
+
421
+ model_kwargs = {
422
+ "input_ids": input_ids,
423
+ "positions": self._get_positions(input_batch_size),
424
+ "inputs_embeds": inputs_embeds,
425
+ "spec_step_idx": spec_step_idx,
426
+ }
427
+ if self.pass_hidden_states_to_model:
428
+ model_kwargs["hidden_states"] = self.hidden_states[:input_batch_size]
429
+
430
+ with set_forward_context(
431
+ per_layer_attn_metadata,
432
+ self.vllm_config,
433
+ num_tokens=input_batch_size,
434
+ num_tokens_across_dp=batch_size_across_dp,
435
+ cudagraph_runtime_mode=cudagraph_runtime_mode,
436
+ slot_mapping=self._get_slot_mapping(input_batch_size),
437
+ ):
438
+ ret_hidden_states = self.model(**model_kwargs)
439
+ if not self.model_returns_tuple():
440
+ last_hidden_states = ret_hidden_states
441
+ hidden_states = ret_hidden_states
442
+ else:
443
+ last_hidden_states, hidden_states = ret_hidden_states
444
+
445
+ hidden_states = hidden_states[:batch_size]
446
+ draft_token_ids, draft_probs = self._sample_draft_tokens_for_step(
447
+ last_hidden_states[:batch_size],
448
+ sampling_metadata,
449
+ spec_step_idx=spec_step_idx,
450
+ )
451
+ if draft_probs is not None:
452
+ assert draft_probs_list is not None
453
+ draft_probs_list.append(draft_probs)
454
+ draft_token_ids_list.append(draft_token_ids)
455
+
456
+ draft_token_ids = torch.stack(draft_token_ids_list, dim=1)
457
+ if draft_probs_list is not None:
458
+ self._last_draft_probs = torch.stack(draft_probs_list, dim=1).contiguous()
459
+ return draft_token_ids
vllm/v1/worker/gpu_model_runner.py
+9
-1
@@ -186,6 +186,7 @@ from vllm.v1.spec_decode.ngram_proposer_gpu import (
186
update_ngram_gpu_tensors_incremental,
187
update_scheduler_for_invalid_drafts,
188
)
189
+from vllm.v1.spec_decode.step3p5 import Step3p5MTPProposer
190
from vllm.v1.spec_decode.suffix_decoding import SuffixDecodingProposer
191
from vllm.v1.spec_decode.utils import update_num_computed_tokens_for_batch_change
192
from vllm.v1.structured_output.utils import apply_grammar_bitmask
@@ -547,6 +548,7 @@ class GPUModelRunner(
548
| MedusaProposer
549
| ExtractHiddenStatesProposer
550
| Gemma4Proposer
551
+ | Step3p5MTPProposer
552
)
553
if self.speculative_config.method == "custom_class":
554
self.drafter = create_custom_proposer( # type: ignore[assignment]
@@ -581,6 +583,8 @@ class GPUModelRunner(
583
)
584
elif self.speculative_config.use_gemma4_mtp():
585
self.drafter = Gemma4Proposer(self.vllm_config, self.device, self)
586
+ elif self.speculative_config.use_step3p5_mtp():
587
+ self.drafter = Step3p5MTPProposer(self.vllm_config, self.device, self)
588
elif self.speculative_config.use_dflash():
589
self.drafter = DFlashProposer(self.vllm_config, self.device, self)
590
self.use_aux_hidden_state_outputs = True
@@ -2428,7 +2432,11 @@ class GPUModelRunner(
2432
else:
2433
spec_decode_common_attn_metadata = cm
2434
# Capture per-group block tables for multi-group proposers.
2431
- if self.speculative_config and isinstance(self.drafter, Gemma4Proposer):
2435
+ if self.speculative_config and isinstance(self.drafter, Step3p5MTPProposer):
2436
+ self.drafter.set_per_group_attn_metadata(
2437
+ kv_cache_gid, cm.block_table_tensor, cm.slot_mapping
2438
+ )
2439
+ elif self.speculative_config and isinstance(self.drafter, Gemma4Proposer):
2440
self.drafter.set_per_group_block_table(
2441
kv_cache_gid, cm.block_table_tensor
2442
)