Skip to content

Commit 7cd9e12

Browse files
LiuYi-Upliuyiyichaowang1999lilongbo-hawkmenzearthmanylf
authored
[Feature] Support Step3P7/Step3P5 with Step3P5 MTP on Ascend (vllm-project#10697)
### What this PR does / why we need it? This PR adds Ascend support for Step3P5 and Step3P7 models with MTP speculative decoding. With this change, Step3P5 and Step3P7 checkpoints can run on Ascend with `--speculative_config '{"method": "mtp", "num_speculative_tokens": 3}'` enabled for Step3p5 MTP, allowing the model to draft multiple tokens and use speculative decoding to improve generation performance. This is needed to support Step-family MTP checkpoints on Ascend and align Ascend speculative decoding capability with the model architecture used by Step3P5/Step3P7. ### Does this PR introduce _any_ user-facing change? ### How was this patch tested? e2e: ``` export HCCL_CONNECT_TIMEOUT=7200 export ASCEND_RT_VISIBLE_DEVICES=0,1,2,3,4,5,6,7 export PYTORCH_NPU_ALLOC_CONF="expandable_segments:True" export HCCL_OP_EXPANSION_MODE="AIV" export HCCL_BUFFSIZE=512 export CPU_AFFINITY_CONF=1 export TASK_QUEUE_ENABLE=0 export SHM_BARRIER="true" export VLLM_ASCEND_ENABLE_FLASHCOMM1=0 LD_PRELOAD=/lib/x86_64-linux-gnu/libjemalloc.so.2 capture_size=$(echo {1..64} | tr ' ' ',') vllm serve path/to/model \ --trust-remote-code \ --tensor-parallel-size 8 \ --pipeline-parallel-size 1 \ --port 8000 \ --compilation-config "{\"cudagraph_mode\": \"FULL_DECODE_ONLY\", \"cudagraph_capture_sizes\": [$capture_size]}" \ --reasoning-parser step3p5 \ --enable-auto-tool-choice \ --tool-call-parser step3p5 \ --gpu-memory-utilization 0.96 \ --no-enable-prefix-caching \ --served-model-name "step3p7" \ --max-num-seqs 16 \ --async-scheduling \ --max-num-batched-tokens 65536 \ --enable-expert-parallel \ --additional-config '{"weight_prefetch_config":{"enabled":true}}' \ --speculative_config '{"method": "mtp", "num_speculative_tokens": 3}' ``` - 精度 ``` evalscope eval \ --model step3p7 \ --api-url http://0.0.0.0:8000/v1/chat/completions \ --generation-config do_sample=true,temperature=1.0,top_p=0.95,stream=true \ --eval-batch-size 16 \ --dataset-args '{"aime25":{"shuffle": false}}' \ --datasets aime25 \ --repeat 16 \ --timeout 5400 ``` <img width="1364" height="230" alt="image" src="https://github.com/user-attachments/assets/0c2469e7-bce1-45eb-bca8-804b2bb60931" /> <img width="2494" height="348" alt="image" src="https://github.com/user-attachments/assets/890dfdad-8d7c-4bb5-93a2-5b88dc1c374d" /> - vLLM version: v0.23.0 - vLLM main: vllm-project/vllm@dc68bd8 Signed-off-by: liuyi <liuyi@stepfun.com> Co-authored-by: liuyi <liuyi@stepfun.com> Co-authored-by: yichaowang1999 <yichaowang1999@qq.com> Co-authored-by: lilongbo-hawkmenz <lilongbo-hawkmenz@qq.com> Co-authored-by: yulinfeng2 <yulinfeng2@huawei.com>
1 parent 15d7ae0 commit 7cd9e12

15 files changed

Lines changed: 1238 additions & 57 deletions

File tree

.github/workflows/scripts/test_config.yaml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -516,6 +516,13 @@
516516
- vllm_ascend/spec_decode/medusa_proposer.py
517517
tests: []
518518

519+
- name: spec_decode_step3p5
520+
optional: false
521+
base: spec_decode_base
522+
source_file_dependencies:
523+
- vllm_ascend/spec_decode/step3p5.py
524+
tests: []
525+
519526
# Worker
520527
- name: worker_v1
521528
optional: false

tests/ut/ops/a3_2/test_activation.py

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
AscendQuickGELU,
2525
AscendSiluAndMul,
2626
AscendSwigluOAIAndMul,
27-
swiglustep_and_mul,
27+
AscendSwigluStepAndMul,
2828
)
2929
from vllm_ascend.utils import is_310p as is_310p_hw
3030

@@ -207,7 +207,7 @@ def test_swiglu_oai_forward_large_input(self):
207207
class TestSwiglustepAndMul:
208208
def test_swiglustep_and_mul_matches_reference_formula(self):
209209
x = torch.tensor([[1.0, 2.0, -3.0, 4.0, 5.0, -6.0, 7.0, -8.0]], dtype=torch.float32)
210-
result = swiglustep_and_mul(x)
210+
result = AscendSwigluStepAndMul.swiglustep_forward(x)
211211
expected = _swiglustep_and_mul_reference(x)
212212

213213
assert result.shape == (1, x.shape[-1] // 2)
@@ -218,7 +218,7 @@ def test_swiglustep_and_mul_uses_contiguous_gate_up_layout(self):
218218
# gate = first half, up = second half (contiguous split via chunk),
219219
# NOT the interleaved layout used by SwigluOAI.
220220
x = torch.tensor([[1.0, 2.0, 3.0, 4.0, 10.0, 20.0, 30.0, 40.0]], dtype=torch.float32)
221-
result = swiglustep_and_mul(x, limit=100.0)
221+
result = AscendSwigluStepAndMul.swiglustep_forward(x, limit=100.0)
222222
expected = _swiglustep_and_mul_reference(x, limit=100.0)
223223
interleaved = torch.nn.functional.silu(x[..., ::2]) * x[..., 1::2]
224224

@@ -228,7 +228,7 @@ def test_swiglustep_and_mul_uses_contiguous_gate_up_layout(self):
228228
def test_swiglustep_and_mul_with_custom_limit_matches_reference(self):
229229
x = torch.tensor([[9.0, 8.0, -5.0, -9.0]], dtype=torch.float32)
230230
limit = 3.0
231-
result = swiglustep_and_mul(x, limit=limit)
231+
result = AscendSwigluStepAndMul.swiglustep_forward(x, limit=limit)
232232
expected = _swiglustep_and_mul_reference(x, limit=limit)
233233

234234
assert torch.allclose(result, expected, atol=1e-6)
@@ -237,7 +237,7 @@ def test_swiglustep_and_mul_clamps_gate_and_up_values(self):
237237
# gate = [100, 100] -> silu(~100) clamped to 7.0;
238238
# up = [-100, -100] -> clamped to -7.0 => 7.0 * -7.0 = -49.0
239239
x = torch.tensor([[100.0, 100.0, -100.0, -100.0]], dtype=torch.float32)
240-
result = swiglustep_and_mul(x)
240+
result = AscendSwigluStepAndMul.swiglustep_forward(x)
241241
expected = _swiglustep_and_mul_reference(x)
242242

243243
assert torch.allclose(result, expected, atol=1e-5)
@@ -247,13 +247,18 @@ def test_swiglustep_and_mul_clamps_gate_and_up_values(self):
247247

248248
def test_swiglustep_and_mul_large_input(self):
249249
x = torch.randn(64, 128, dtype=torch.float32)
250-
result = swiglustep_and_mul(x)
250+
result = AscendSwigluStepAndMul.swiglustep_forward(x)
251251
expected = _swiglustep_and_mul_reference(x)
252252

253253
assert result.shape == (64, 64)
254254
assert torch.allclose(result, expected, atol=1e-5)
255255
assert not torch.isnan(result).any()
256256

257+
def test_swiglustep_and_mul_validates_limit(self):
258+
x = torch.tensor([[8.0, 9.0, -2.0, -8.0]], dtype=torch.float32)
259+
with pytest.raises(ValueError, match="requires limit"):
260+
AscendSwigluStepAndMul.swiglustep_forward(x, limit=None)
261+
257262

258263
class TestActivationNPUPrecision:
259264
@pytest.mark.parametrize(
@@ -331,7 +336,7 @@ def test_swiglustep_and_mul_matches_cpu_reference_on_npu(self, dtype, atol, rtol
331336
x_cpu = torch.randn(16, 16, dtype=torch.float32) * 4
332337
x_npu = x_cpu.to(dtype=dtype, device="npu")
333338

334-
result = swiglustep_and_mul(x_npu).cpu()
339+
result = AscendSwigluStepAndMul.swiglustep_forward(x_npu).cpu()
335340
expected = _swiglustep_and_mul_reference(x_cpu.to(dtype=dtype)).float()
336341

337342
assert result.shape == (16, 8)

tests/ut/ops/test_moe_mlp.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from unittest.mock import patch
44

55
import torch
6+
from vllm.model_executor.layers.fused_moe.activation import MoEActivation
67

78
from vllm_ascend.ops.fused_moe.moe_mlp import cumsum_group_list, unified_apply_mlp
89
from vllm_ascend.ops.fused_moe.moe_runtime_args import (
@@ -181,6 +182,38 @@ def test_request_quant_path_passes_w4a8_per_channel_flag(self):
181182
self.assertTrue(quant_kwargs["use_w4a8_per_channel_gmm_swiglu"])
182183
mock_unquant.assert_not_called()
183184

185+
def test_request_quant_path_passes_swiglustep_activation(self):
186+
expected = torch.randn(1, 2)
187+
mlp_compute_input = MoEMlpComputeInput(
188+
hidden_states=torch.ones((1, 2), dtype=torch.float32),
189+
group_list=torch.tensor([1], dtype=torch.int64),
190+
group_list_type=1,
191+
dynamic_scale=None,
192+
topk_scales=None,
193+
weights=MoEWeights(
194+
w1=[torch.ones((1, 2, 4), dtype=torch.float32)],
195+
w2=[torch.ones((1, 2, 2), dtype=torch.float32)],
196+
w1_scale=[torch.ones((1,), dtype=torch.float32)],
197+
w2_scale=[torch.ones((1,), dtype=torch.float32)],
198+
),
199+
quant=MoEQuantParams(quant_type=QuantType.W8A8),
200+
fusion=True,
201+
activation=MoEActivation.SWIGLUSTEP,
202+
swiglu_limit=5.0,
203+
)
204+
205+
with (
206+
patch("vllm_ascend.ops.fused_moe.moe_mlp.quant_apply_mlp", return_value=expected) as mock_quant,
207+
patch("vllm_ascend.ops.fused_moe.moe_mlp.unquant_apply_mlp") as mock_unquant,
208+
):
209+
output = unified_apply_mlp(mlp_compute_input=mlp_compute_input)
210+
211+
self.assertTrue(output is expected)
212+
quant_kwargs = mock_quant.call_args.kwargs
213+
self.assertEqual(quant_kwargs["activation"], MoEActivation.SWIGLUSTEP)
214+
self.assertEqual(quant_kwargs["swiglu_limit"], 5.0)
215+
mock_unquant.assert_not_called()
216+
184217

185218
if __name__ == "__main__":
186219
unittest.main(verbosity=2)

tests/ut/quantization/test_modelslim_config.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -273,10 +273,14 @@ def test_apply_extra_quant_adaptations_shared_head(self):
273273
config = AscendModelSlimConfig()
274274
config.quant_description = {
275275
"model.layers.0.shared_head.weight": "INT8",
276+
"transformer.shared_head.output.weight": "INT8",
277+
"transformer.shared_head.norm.weight": "INT8",
276278
}
277279
config._apply_extra_quant_adaptations()
278280
self.assertIn("model.layers.0.weight", config.quant_description)
279281
self.assertEqual(config.quant_description["model.layers.0.weight"], "INT8")
282+
self.assertIn("shared_head.head.weight", config.quant_description)
283+
self.assertIn("shared_head.norm.weight", config.quant_description)
280284

281285
def test_apply_extra_quant_adaptations_weight_packed(self):
282286
config = AscendModelSlimConfig()
@@ -320,6 +324,34 @@ def test_lm_head_keeps_original_prefix_when_quant_key_exists(self):
320324

321325
self.assertEqual(prefix, "lm_head")
322326

327+
def test_step3p5_mtp_maps_direct_and_step3p7_wrapped_quant_keys(self):
328+
cases = [
329+
(
330+
"model.layers.45.self_attn",
331+
"model.layers.45.self_attn.qkv_proj",
332+
),
333+
(
334+
"language_model.model.layers.45.self_attn",
335+
"language_model.model.layers.45.self_attn.qkv_proj",
336+
),
337+
]
338+
for quant_prefix, expected in cases:
339+
with self.subTest(quant_prefix=quant_prefix):
340+
config = AscendModelSlimConfig(
341+
{
342+
f"{quant_prefix}.q_proj.weight": "FLOAT",
343+
f"{quant_prefix}.k_proj.weight": "FLOAT",
344+
f"{quant_prefix}.v_proj.weight": "FLOAT",
345+
}
346+
)
347+
348+
prefix = config.quant_prefix_mapper(
349+
"step3p5_mtp",
350+
"model.layers.45.mtp_block.self_attn.qkv_proj",
351+
)
352+
353+
self.assertEqual(prefix, expected)
354+
323355

324356
class TestGetCacheScale(TestBase):
325357
def test_c8_kv_cache_type_k_proj_scale(self):
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
# SPDX-License-Identifier: Apache-2.0
2+
"""Source-level regressions for Step3.5 MTP Ascend glue.
3+
4+
Importing the Step3.5 proposer can initialize runtime/device state in this
5+
branch. Keep these checks focused on cross-file contracts that are hard to
6+
exercise in a lightweight unit test, and avoid pinning the exact implementation
7+
sequence inside the proposer.
8+
"""
9+
10+
from __future__ import annotations
11+
12+
import ast
13+
from pathlib import Path
14+
15+
ROOT = Path(__file__).resolve().parents[3]
16+
STEP3P5 = ROOT / "vllm_ascend" / "spec_decode" / "step3p5.py"
17+
BASE_PROPOSER = ROOT / "vllm_ascend" / "spec_decode" / "llm_base_proposer.py"
18+
PATCH_SPEC_CFG = ROOT / "vllm_ascend" / "patch" / "platform" / "patch_speculative_config.py"
19+
WORKER_PATCH_INIT = ROOT / "vllm_ascend" / "patch" / "worker" / "__init__.py"
20+
LEGACY_STEP3P7_PATCH = ROOT / "vllm_ascend" / "patch" / "worker" / "patch_step3p5_mtp.py"
21+
22+
23+
def _tree(path: Path) -> ast.Module:
24+
return ast.parse(path.read_text())
25+
26+
27+
def _class(path: Path, name: str) -> ast.ClassDef:
28+
for node in _tree(path).body:
29+
if isinstance(node, ast.ClassDef) and node.name == name:
30+
return node
31+
raise AssertionError(f"class {name} not found in {path}")
32+
33+
34+
def _method(path: Path, cls_name: str, method_name: str) -> ast.FunctionDef:
35+
cls = _class(path, cls_name)
36+
for node in cls.body:
37+
if isinstance(node, ast.FunctionDef) and node.name == method_name:
38+
return node
39+
raise AssertionError(f"method {cls_name}.{method_name} not found")
40+
41+
42+
def _func(path: Path, name: str) -> ast.FunctionDef:
43+
for node in _tree(path).body:
44+
if isinstance(node, ast.FunctionDef) and node.name == name:
45+
return node
46+
raise AssertionError(f"function {name} not found in {path}")
47+
48+
49+
def _src(node: ast.AST) -> str:
50+
return ast.unparse(node)
51+
52+
53+
def test_step3p5_first_pass_forwards_rejected_token_counts() -> None:
54+
# set_inputs_first_pass is inherited from the base proposer; the base
55+
# simple-path is the canonical Step3.5 behaviour. Step3.5 only needs to
56+
# forward num_rejected_tokens_gpu through _propose.
57+
set_inputs = _method(BASE_PROPOSER, "AscendSpecDecodeBaseProposer", "set_inputs_first_pass")
58+
propose = _method(STEP3P5, "AscendStep3p5MTPProposer", "_propose")
59+
60+
assert "num_rejected_tokens_gpu" in [arg.arg for arg in set_inputs.args.args]
61+
assert "num_rejected_tokens_gpu=num_rejected_tokens_gpu" in _src(propose)
62+
63+
# Guard against the override creeping back: the previous step3p5 simple-path
64+
# was byte-equivalent to the base's `not needs_extra_input_slots and
65+
# pcp_size <= 1` branch, so a re-override is almost certainly redundant.
66+
step_methods = {n.name for n in _class(STEP3P5, "AscendStep3p5MTPProposer").body if isinstance(n, ast.FunctionDef)}
67+
assert "set_inputs_first_pass" not in step_methods
68+
69+
70+
def test_step3p5_draft_window_and_config_contracts() -> None:
71+
base_run = _method(BASE_PROPOSER, "AscendSpecDecodeBaseProposer", "_run_merged_draft")
72+
step_run = _method(STEP3P5, "AscendStep3p5MTPProposer", "_run_merged_draft")
73+
run_window = _src(_method(STEP3P5, "AscendStep3p5MTPProposer", "_run_window_draft_steps"))
74+
build_metadata = _src(_method(STEP3P5, "AscendStep3p5MTPProposer", "_build_step_attn_metadatas"))
75+
roll_inputs = _src(_method(STEP3P5, "AscendStep3p5MTPProposer", "_roll_window_inputs_only"))
76+
ensure_layer_types = _src(
77+
_method(
78+
STEP3P5,
79+
"AscendStep3p5MTPProposer",
80+
"_ensure_draft_layer_types_cover_mtp_layers",
81+
)
82+
)
83+
create_config = _src(_method(STEP3P5, "AscendStep3p5MTPProposer", "_create_draft_vllm_config"))
84+
85+
assert [arg.arg for arg in step_run.args.args] == [arg.arg for arg in base_run.args.args]
86+
assert "multi_steps_attn_metadata.append(per_step_attn_metadata)" in build_metadata
87+
assert "multi_steps_attn_metadata[spec_step_idx]" in run_window
88+
assert "self.input_ids[token_indices_to_sample]" in roll_inputs
89+
assert "_ensure_draft_layer_types_cover_mtp_layers()" in create_config
90+
assert "self.draft_model_config.hf_config" in ensure_layer_types
91+
assert "self.vllm_config.model_config.hf_config" not in ensure_layer_types
92+
assert "sliding_attention" in ensure_layer_types
93+
94+
95+
def test_step3p7_uses_step3p5_mtp_override_without_legacy_runtime_patch() -> None:
96+
override_src = _src(_func(PATCH_SPEC_CFG, "hf_config_override"))
97+
98+
assert "step3p7" in override_src
99+
assert "Step3p7ForConditionalGeneration" in override_src
100+
assert "step3p5_mtp" in override_src
101+
assert "Step3p5MTP" in override_src
102+
assert "patch_step3p5_mtp" not in WORKER_PATCH_INIT.read_text()
103+
assert not LEGACY_STEP3P7_PATCH.exists()

0 commit comments

Comments
 (0)