Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion docs/source/user_guide/feature_guide/quantization.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,10 +104,11 @@ The quantization types currently supported by LLM-Compressor can be viewed in th

### 3. Native FP8 checkpoints (no quantization step needed)

Many models are now published directly in FP8, for example [Qwen/Qwen3.8-27B-FP8](https://modelscope.cn/models/Qwen/Qwen3.8-27B-FP8). Their `config.json` carries `"quant_method": "fp8"` together with a `weight_block_size`, meaning every quantized weight is stored as `float8_e4m3fn` plus one `float32` scale per weight block. vLLM Ascend detects and serves these checkpoints as published, so no offline re-quantization is required:
Many models are now published directly in FP8, for example [Qwen/Qwen3.8-27B-FP8](https://modelscope.cn/models/Qwen/Qwen3.8-27B-FP8) and [zai-org/GLM-5.3-Flash](https://huggingface.co/zai-org/GLM-5.3-Flash). Their `config.json` carries `"quant_method": "fp8"` together with a `weight_block_size`, meaning every quantized weight is stored as `float8_e4m3fn` plus one `float32` scale per weight block. vLLM Ascend detects and serves these checkpoints as published, so no offline re-quantization is required:

```bash
vllm serve /path/to/Qwen3.8-27B-FP8 --trust-remote-code
vllm serve /path/to/GLM-5.3-Flash --tensor-parallel-size 8 --trust-remote-code
```

How the weights are executed depends on the hardware:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,6 @@ Get the latest info here: <https://github.com/vllm-project/vllm-ascend/issues/16
|-----------------|----------|--------|------|------|------|---------|-------|------|------|--------|-------|--------|--------|-------|-------|--------|----------|---------|----------|-----|
|Qwen3.5-397B-A17B|✅ | |✅ | Ascend 950DT |✅|✅|✅||✅|✅|✅||✅|✅|✅|✅|✅|1010000|| [Qwen3.5-397B-A17B](../../tutorials/models/Qwen3.5-397B-A17B.md) |
|Qwen3.6-27B |✅ | |✅ | Ascend 950 Products |✅|✅|✅||✅|✅|✅||✅|✅|✅|✅|✅|262144|| [Qwen3.5-27B / Qwen3.6-27B](../../tutorials/models/Qwen3.5-27B-Qwen3.6-27B.md) |

=== "A2/A3"

| Model | Support | Note | BF16 | Supported Hardware | W8A8 | Chunked Prefill | Automatic Prefix Cache | LoRA | Speculative Decoding | Async Scheduling | Tensor Parallel | Pipeline Parallel | Expert Parallel | Data Parallel | Prefill-decode Disaggregation | Piecewise AclGraph | Fullgraph AclGraph | max-model-len | Doc |
Expand Down
3 changes: 3 additions & 0 deletions mypy.ini
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ ignore_missing_imports = True
[mypy-transformers.*]
ignore_missing_imports = True

[mypy-torchvision.*]
ignore_missing_imports = True

[mypy-lm_eval.*]
ignore_missing_imports = True

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
import torch_npu
from vllm.v1.attention.backends.utils import PAD_SLOT_ID

from vllm_ascend._310p.ops.causal_conv1d import causal_conv1d_fn as causal_conv1d_fn_ref
from vllm_ascend._310p.ops.causal_conv1d import causal_conv1d_update as causal_conv1d_update_ref
from vllm_ascend.ops.causal_conv1d import causal_conv1d_fn as causal_conv1d_fn_ref
from vllm_ascend.ops.causal_conv1d import causal_conv1d_update as causal_conv1d_update_ref
from vllm_ascend.utils import enable_custom_op
from vllm_ascend.utils import is_310p as is_310p_hw

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import pytest
import torch

from vllm_ascend._310p.ops.causal_conv1d import causal_conv1d_fn as causal_conv1d_fn_ref
from vllm_ascend.ops.causal_conv1d import causal_conv1d_fn as causal_conv1d_fn_ref
from vllm_ascend.ops.triton.mamba.causal_conv1d import PAD_SLOT_ID, causal_conv1d_fn
from vllm_ascend.utils import enable_custom_op

Expand Down
104 changes: 104 additions & 0 deletions tests/ut/attention/test_mla_nope_chunked_gather.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Ascend project
"""Regression tests for the MLA-NoPE chunked-context paged gather."""

from types import SimpleNamespace

import torch

from vllm_ascend.attention.mla_v1 import AscendMLAImpl
from vllm_ascend.device.device_op import DeviceOperator

NUM_TOKENS = 6
NUM_HEADS = 2
QK_NOPE_HEAD_DIM = 4
V_HEAD_DIM = 3
LATENT_KV_DIM = 8
CHUNK_TOKS = 5


def _build_impl(rope_dim: int) -> AscendMLAImpl:
impl = AscendMLAImpl.__new__(AscendMLAImpl)
impl.num_heads = NUM_HEADS
impl.qk_nope_head_dim = QK_NOPE_HEAD_DIM
impl.qk_rope_head_dim = rope_dim
impl.v_head_dim = V_HEAD_DIM
impl.head_padding = 0
impl.scale = 1.0
impl.fa_quant_layer = False
impl.kv_b_proj = lambda x: (torch.zeros(x.size(0), NUM_HEADS * (QK_NOPE_HEAD_DIM + V_HEAD_DIM)),)
impl.get_context_seq_len_npu = lambda index, metadata: torch.tensor([CHUNK_TOKS], dtype=torch.int32)
return impl


def _build_metadata():
chunked_context = SimpleNamespace(
seq_tot=[CHUNK_TOKS],
starts=[torch.zeros(1, dtype=torch.int32)],
chunk_actual_seq_lengths_kv_list=[[CHUNK_TOKS]],
)
prefill = SimpleNamespace(
chunked_context=chunked_context,
actual_seq_lengths_q=[NUM_TOKENS],
block_table=torch.zeros(1, 1, dtype=torch.int32),
)
return SimpleNamespace(prefill=prefill)


def _install_fakes(monkeypatch, captured):
def fake_kv_cache_load(cache_kv_c, cache_k_pe, block_table, context_seq_len_npu, seq_starts, key, value):
captured["pe_cache"] = cache_k_pe
captured["key"] = key
captured["value"] = value

monkeypatch.setattr(DeviceOperator, "kv_cache_load", staticmethod(fake_kv_cache_load))
monkeypatch.setattr(
"vllm_ascend.attention.mla_v1.torch_npu",
SimpleNamespace(
npu_fused_infer_attention_score=lambda *args, **kwargs: (
torch.zeros(NUM_TOKENS, NUM_HEADS, V_HEAD_DIM),
torch.zeros(NUM_TOKENS * NUM_HEADS),
),
npu_attention_update=lambda lse, out, dim: (
torch.zeros(NUM_TOKENS * NUM_HEADS, V_HEAD_DIM),
None,
),
),
)


def _run(impl, rope_dim, cache_k_pe):
return impl._compute_prefill_context(
torch.zeros(NUM_TOKENS, NUM_HEADS, QK_NOPE_HEAD_DIM),
torch.zeros(NUM_TOKENS, NUM_HEADS, rope_dim),
(torch.zeros(2, 3, 1, LATENT_KV_DIM), cache_k_pe),
rope_dim,
_build_metadata(),
torch.zeros(NUM_TOKENS, NUM_HEADS, V_HEAD_DIM),
torch.zeros(NUM_HEADS, NUM_TOKENS),
)


def test_chunked_gather_avoids_zero_width_operands_without_rope(monkeypatch):
captured: dict = {}
_install_fakes(monkeypatch, captured)

_run(_build_impl(rope_dim=0), 0, torch.zeros(2, 3, 1, 0))

# npu_gather_pa_kv_cache returns without filling the latent output when the
# rope cache and its destination are both zero-width, which fed
# uninitialised KV into every prefill longer than max_num_batched_tokens.
assert captured["pe_cache"].size(-1) != 0
assert captured["value"].size(-1) != 0
assert captured["key"].shape == (CHUNK_TOKS, 1, LATENT_KV_DIM)


def test_chunked_gather_keeps_the_rope_cache_when_rope_is_present(monkeypatch):
captured: dict = {}
_install_fakes(monkeypatch, captured)
cache_k_pe = torch.zeros(2, 3, 1, 64)

_run(_build_impl(rope_dim=64), 64, cache_k_pe)

assert captured["pe_cache"] is cache_k_pe
assert captured["value"].shape == (CHUNK_TOKS, 1, 64)
37 changes: 37 additions & 0 deletions tests/ut/attention/test_mla_v1.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
ChunkedContextMetadata,
DecodeMLAPreprocessResult,
PrefillMLAPreprocessResult,
_mla_nope_zero_rope,
)
from vllm_ascend.attention.utils import AscendCommonAttentionMetadata
from vllm_ascend.device.hardware import AscendDeviceType
Expand Down Expand Up @@ -505,6 +506,42 @@ def test_metadata_builder_uses_layer_rope_mode(self):

self.assertEqual(builder.use_mla_rope, layer_uses_rope)

def test_zero_rope_cache_only_for_nope_models(self):
mock_vllm_config = MagicMock()
mock_vllm_config.model_config.max_model_len = 1024
mock_vllm_config.model_config.get_head_size.return_value = 64
mock_vllm_config.model_config.dtype = torch.float16
mock_vllm_config.cache_config.block_size = 16
mock_vllm_config.scheduler_config.max_num_seqs = 4
mock_vllm_config.scheduler_config.enable_chunked_prefill = False
mock_vllm_config.speculative_config = None

for qk_rope_head_dim, expected in ((64, None), (0, {})):
with self.subTest(qk_rope_head_dim=qk_rope_head_dim):
mock_vllm_config.model_config.hf_text_config = SimpleNamespace(
qk_rope_head_dim=qk_rope_head_dim,
mla_use_nope=False,
)
with patch("vllm_ascend.attention.mla_v1.get_ascend_config", return_value=MagicMock()):
builder = AscendMLAMetadataBuilder(None, [], mock_vllm_config, "cpu")

self.assertEqual(builder.nope_zero_rope_cache, expected)

def test_mla_nope_zero_rope_reuses_one_buffer_per_shape(self):
cache: dict = {}
ref = torch.ones(2, 1, 4, 8)

first = _mla_nope_zero_rope(ref, 64, cache)
second = _mla_nope_zero_rope(torch.ones_like(ref), 64, cache)

self.assertIs(first, second)
self.assertEqual(first.shape, (2, 1, 4, 64))
self.assertTrue(torch.equal(first, torch.zeros_like(first)))
self.assertEqual(len(cache), 1)

_mla_nope_zero_rope(torch.ones(3, 1, 4, 8), 64, cache)
self.assertEqual(len(cache), 2)

def test_ascend_mla_metadata_builder_spec_decode(self):
mock_vllm_config = MagicMock()
mock_vllm_config.model_config.max_model_len = 1024
Expand Down
50 changes: 50 additions & 0 deletions tests/ut/ops/test_kda_gate.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# Copyright (c) 2026 Huawei Technologies Co., Ltd. All Rights Reserved.
# This file is a part of the vllm-ascend project.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import unittest

import torch

from tests.ut.base import TestBase
from vllm_ascend.ops.triton.kda.gate import DEFAULT_KDA_LOWER_BOUND, apply_kda_gate


class TestApplyKdaGate(TestBase):
def test_safe_gate_matches_glm53_formula(self):
raw_g = torch.randn(1, 4, 2, 8)
a_log = torch.randn(1, 1, 2, 1)
g_bias = torch.randn(2 * 8)
out = apply_kda_gate(raw_g, a_log, g_bias, safe_gate=True, lower_bound=DEFAULT_KDA_LOWER_BOUND)
bias = g_bias.reshape(2, 8)
expected = DEFAULT_KDA_LOWER_BOUND * torch.sigmoid(torch.exp(a_log.float()) * (raw_g.float() + bias))
torch.testing.assert_close(out, expected.to(torch.float32), rtol=1e-5, atol=1e-5)
self.assertEqual(out.shape, raw_g.shape)

def test_softplus_gate_matches_kimi_formula(self):
raw_g = torch.randn(1, 3, 2, 4)
a_log = torch.tensor([0.2, -0.1]).view(1, 1, 2, 1)
out = apply_kda_gate(raw_g, a_log, None, safe_gate=False)
expected = (-torch.exp(a_log.float())) * torch.nn.functional.softplus(raw_g.float())
torch.testing.assert_close(out, expected.to(torch.float32), rtol=1e-5, atol=1e-5)

def test_head_mismatch_raises(self):
raw_g = torch.randn(1, 2, 4, 8)
a_log = torch.randn(2)
with self.assertRaisesRegex(ValueError, "head dim mismatch"):
apply_kda_gate(raw_g, a_log, None, safe_gate=True)


if __name__ == "__main__":
unittest.main()
8 changes: 8 additions & 0 deletions tests/ut/quantization/test_fp8_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,14 @@ def test_official_visual_layer_name_is_skipped(self):
"block-fp8",
)

def test_glm53_flash_kda_layer_stays_unquantized(self):
# Official zai-org/GLM-5.3-Flash lists KDA projections in
# modules_to_not_convert; they have no weight_scale_inv.
kda_name = "model.layers.0.self_attn.q_proj"
config = self.build_config(ignored_layers=[kda_name])
method = config.get_quant_method(MagicMock(spec=LinearBase), kda_name)
self.assertIsInstance(method, AscendUnquantizedLinearMethod)

def test_ignored_moe_falls_back_to_unquantized(self):
config = self.build_config(ignored_layers=["model.layers.0.mlp.experts"])
moe_layer = MagicMock(spec=RoutedExperts)
Expand Down
9 changes: 9 additions & 0 deletions tests/ut/quantization/test_modelslim_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -684,6 +684,15 @@ def test_kimi_k3_quarot_splits_mixed_kda_projection(self):

self.assertIsInstance(method, AscendUnquantizedLinearMethod)

def test_glm5_next_packed_modules_mapping_covers_moe_mla_and_kda(self):
expected_mapping = {
"gate_up_proj": ["gate_proj", "up_proj"],
"experts": ["experts.0.gate_proj", "experts.0.up_proj", "experts.0.down_proj"],
"fused_qkv_a_proj": ["q_a_proj", "kv_a_proj_with_mqa"],
"fused_qkvbfg_a_proj": ["q_proj", "k_proj", "v_proj", "b_proj", "f_a_proj", "g_a_proj"],
}
self.assertEqual(get_packed_modules_mapping("glm5_next"), expected_mapping)

def test_gemma4_moe_experts_float_shards_are_skipped_together(self):
quant_description = {
"language_model.model.layers.0.experts.0.gate_proj.weight": "FLOAT",
Expand Down
84 changes: 83 additions & 1 deletion tests/ut/spec_decode/test_llm_base_proposer.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,10 @@
import torch
from vllm.config import CUDAGraphMode

from vllm_ascend.spec_decode.llm_base_proposer import AscendSpecDecodeBaseProposer
from vllm_ascend.spec_decode.llm_base_proposer import (
AscendSpecDecodeBaseProposer,
_draft_embed_accepts_mm,
)

# CUDAGraphMode values whose ``has_full_cudagraphs()`` is True: FULL plus the
# two composite modes that mix FULL with NONE / PIECEWISE.
Expand Down Expand Up @@ -55,6 +58,7 @@ class TestMultimodalImageTokenIndex:
"Step3p7ForConditionalGeneration",
"Gemma4ForConditionalGeneration",
"Gemma4UnifiedForConditionalGeneration",
"Glm5NextForConditionalGeneration",
],
)
def test_models_using_image_token_id(self, model_name: str):
Expand Down Expand Up @@ -265,3 +269,81 @@ def test_guard_does_not_raise_when_eager(self):
)

proposer._raise_if_padded_drafter_batch_disabled_and_full_graph_enabled()


class TestMtpTargetLmHeadLookup:
"""Where the MTP branch of ``_maybe_share_lm_head`` finds the target head.

Multimodal wrappers such as ``Glm5NextForConditionalGeneration`` own no
``lm_head``; it lives on the nested language model, so reading it off the
wrapper raises ``AttributeError``.
"""

@staticmethod
def _share(target) -> SimpleNamespace:
"""Return the resulting draft head after sharing runs."""
proposer = AscendSpecDecodeBaseProposer.__new__(AscendSpecDecodeBaseProposer)
proposer.method = "mtp"
proposer.vllm_config = SimpleNamespace(
model_config=SimpleNamespace(is_deepseek_mla=True),
compilation_config=SimpleNamespace(cudagraph_mode=CUDAGraphMode.NONE),
)
proposer.use_cuda_graph = False
layers = {"0": SimpleNamespace(shared_head=SimpleNamespace(head=SimpleNamespace(weight=torch.zeros(4, 3))))}
# The checkpoint ships no MTP head, so the draft head is shared
# whenever the target head is reachable at all.
proposer.model = SimpleNamespace(model=SimpleNamespace(layers=layers), has_own_lm_head=False)

proposer._maybe_share_lm_head(target)

return layers["0"].shared_head.head

def test_resolves_lm_head_through_get_language_model(self):
target_lm_head = SimpleNamespace(weight=torch.ones(4, 3))
target = SimpleNamespace(get_language_model=lambda: SimpleNamespace(lm_head=target_lm_head))

assert self._share(target) is target_lm_head

def test_resolves_lm_head_through_the_language_model_attribute(self):
target_lm_head = SimpleNamespace(weight=torch.ones(4, 3))
target = SimpleNamespace(language_model=SimpleNamespace(lm_head=target_lm_head))

assert self._share(target) is target_lm_head

def test_unreachable_lm_head_keeps_the_draft_head(self):
draft_head = self._share(SimpleNamespace())

assert torch.equal(draft_head.weight, torch.zeros(4, 3))


class TestDraftEmbedMmSupport:
"""Text-only MTP heads such as Glm5NextMTP expose ``embed_input_ids``
without multimodal parameters, so forwarding a multimodal target model's
multimodal kwargs to them raises ``TypeError``.
"""

def test_head_taking_multimodal_embeddings_accepts_mm(self):
def embed_input_ids(input_ids, multimodal_embeddings=None):
return input_ids

assert _draft_embed_accepts_mm(embed_input_ids) is True

def test_text_only_head_does_not_accept_mm(self):
def embed_input_ids(input_ids):
return input_ids

assert _draft_embed_accepts_mm(embed_input_ids) is False

@pytest.mark.parametrize("error", [TypeError("C-bound callable"), ValueError("no signature found")])
def test_uninspectable_callable_is_treated_as_text_only(self, error: Exception):
"""Falling back to text-only cannot raise, whereas assuming multimodal
support and forwarding the kwargs to a head that rejects them would.
"""

def embed_input_ids(input_ids, multimodal_embeddings=None):
return input_ids

with patch("vllm_ascend.spec_decode.llm_base_proposer._inspect") as fake_inspect:
fake_inspect.signature.side_effect = error

assert _draft_embed_accepts_mm(embed_input_ids) is False
2 changes: 1 addition & 1 deletion typos.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ ignore-hex = true
identifier-leading-digits = false
locale = "en"
extend-ignore-identifiers-re = [".*Unc.*", ".*_thw",
".*UE8M0.*", ".*(UE4M3|ue4m3).*", ".*eles.*", ".*fo.*", ".*ba.*",
".*(UE8M0|ue8m0).*", ".*(UE4M3|ue4m3).*", ".*eles.*", ".*fo.*", ".*ba.*",
".*ot.*", ".*[Tt]h[rR].*",
# tools/ai_qos.py: align with driver qos_mata_config / original_sdma_mata (not "meta")
".*(?i)mata.*"]
Expand Down
Loading
Loading