✨ [llm][npu][quant] Add W8A8 MXFP8 quantization for Qwen3 MoE on Ascend NPU (#30768)
Co-authored-by: Артем Савкин <58187114+OrangeRedeng@users.noreply.github.com> Co-authored-by: ronnie_zheng <zl19940307@163.com>
This commit is contained in:
co-authored by
Артем Савкин
ronnie_zheng
parent
da5528db30
commit
f05c92fb6d
@@ -2031,7 +2031,13 @@ def _moe_runner_backend_quant_constraints(view: Any) -> dict:
|
||||
"--moe-runner-backend flashinfer_trtllm or "
|
||||
"flashinfer_trtllm_routed."
|
||||
)
|
||||
if view.quantization == "mxfp8":
|
||||
# Ascend runs MXFP8 MoE on the Ascend runner; every backend selected below is
|
||||
# CUDA/ROCm-only. Forcing one here would not merely pick the wrong runner:
|
||||
# FusedMoE keys its w1/w3 shard swap ("flashinfer assumes w31") and its
|
||||
# 128-alignment round-up off flashinfer_trtllm, so the experts would silently
|
||||
# load with gate and up exchanged. Leave the backend at "auto" and let
|
||||
# create_moe_runner resolve it to ASCEND.
|
||||
if view.quantization == "mxfp8" and not is_npu():
|
||||
from sglang.srt.server_args import MXFP8_MOE_RUNNER_BACKEND_CHOICES
|
||||
|
||||
is_gfx95_mxfp8 = is_hip() and is_gfx95_supported()
|
||||
|
||||
@@ -11,6 +11,24 @@ from typing import Optional, Tuple
|
||||
|
||||
import torch
|
||||
|
||||
# ``npu_moe_init_routing_v2`` quant_mode selecting MXFP8: the op emits an
|
||||
# float8_e4m3fn payload plus an e8m0 block scale, fusing the activation quant
|
||||
# that would otherwise need a separate ``npu_dynamic_mx_quant`` pass.
|
||||
MXFP8_QUANT_MODE = 3
|
||||
|
||||
|
||||
def _normalize_mxfp_scale(scale: torch.Tensor) -> torch.Tensor:
|
||||
"""Reshape a flat 2D e8m0 block scale ``[N, M]`` into pair-split ``[N, M//2, 2]``.
|
||||
|
||||
``npu_moe_init_routing_v2(quant_mode=3)`` emits the scale flat, while the
|
||||
grouped matmul wants the pair-split view. Already-3D scales (what
|
||||
``npu_dynamic_mx_quant`` returns) pass through untouched. Mirrors
|
||||
vllm-ascend's ``maybe_normalize_mxfp_scale_layout``.
|
||||
"""
|
||||
if scale is None or scale.ndim != 2:
|
||||
return scale
|
||||
return scale.reshape(scale.shape[0], scale.shape[1] // 2, 2)
|
||||
|
||||
|
||||
class BaseInitRouting(ABC):
|
||||
"""Abstract base for NPU MoE init routing."""
|
||||
@@ -95,6 +113,8 @@ class NPUMoEInitRouting_v2(BaseInitRouting):
|
||||
)
|
||||
if self.quant_mode == -1:
|
||||
pertoken_scale = None
|
||||
elif self.quant_mode == MXFP8_QUANT_MODE:
|
||||
pertoken_scale = _normalize_mxfp_scale(pertoken_scale)
|
||||
expert_tokens = expert_tokens.to(torch.int64)
|
||||
return hidden_states, expanded_row_idx, expert_tokens, pertoken_scale
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Tuple
|
||||
|
||||
import torch
|
||||
|
||||
@@ -47,3 +48,42 @@ class GroupedMatmul(BaseMatmul):
|
||||
group_list=expert_tokens,
|
||||
output_dtype=output_dtype,
|
||||
)[0]
|
||||
|
||||
|
||||
class GroupedMatmulSwigluQuant(BaseMatmul):
|
||||
"""Grouped matmul with swiglu and requantisation fused into one kernel.
|
||||
|
||||
Used for the gate/up projection (gmm1) of block-scaled MoE: the kernel emits
|
||||
activations already quantised for the following down projection, so the
|
||||
caller has no separate activation step. Unlike ``GroupedMatmul`` it returns
|
||||
``(quantized_activations, block_scale)`` instead of a single tensor, and it
|
||||
takes no ``output_dtype`` — the output dtype is set through ``quant_dtype``
|
||||
in ``scale_args``.
|
||||
"""
|
||||
|
||||
def forward(
|
||||
self,
|
||||
layer: torch.nn.Module,
|
||||
weight_prefix: str,
|
||||
hidden_states: torch.Tensor,
|
||||
expert_tokens: torch.Tensor,
|
||||
output_dtype: torch.dtype = None,
|
||||
group_list_type: int = 1,
|
||||
transposed: bool = True,
|
||||
**scale_args,
|
||||
) -> Tuple[torch.Tensor, torch.Tensor]:
|
||||
weight = getattr(layer, f"{weight_prefix}_weight", None)
|
||||
if weight is None:
|
||||
raise AttributeError(
|
||||
f"Weight attribute '{weight_prefix}_weight' not found in layer"
|
||||
)
|
||||
# This op wants a cumulative group_list while the plain grouped matmul
|
||||
# keeps the COUNT form the dispatcher produces (group_list_type=1). The
|
||||
# asymmetry is intentional.
|
||||
group_list = expert_tokens.cumsum(0) if group_list_type == 1 else expert_tokens
|
||||
return torch.ops.npu.npu_grouped_matmul_swiglu_quant_v2(
|
||||
x=hidden_states,
|
||||
weight=[weight] if transposed else [weight.transpose(1, 2)],
|
||||
group_list=group_list,
|
||||
**scale_args,
|
||||
)
|
||||
|
||||
+15
-4
@@ -1,5 +1,5 @@
|
||||
"""
|
||||
Hidden state quantization utilities for NPU MoE.
|
||||
Quantization kernel wrappers for NPU MoE.
|
||||
|
||||
Each class quantises hidden states and returns a (quantized_tensor, scale) tuple.
|
||||
For static quantization the scale is ``None``.
|
||||
@@ -27,15 +27,26 @@ class HiddenStatesDynamicQuant(BaseHiddenStatesQuant):
|
||||
"""
|
||||
Dynamic per‑token quantisation of hidden states.
|
||||
|
||||
``torch.float8_e4m3fn`` selects the MX (block-scaled) op, whose scale is a
|
||||
``float8_e8m0fnu`` block scale ``[N, K//64, 2]`` rather than one scalar per
|
||||
token; the int8/int4 dtypes keep the plain per-token op.
|
||||
|
||||
Returns ``(quantized_hidden_states, per‑token_scale)``.
|
||||
"""
|
||||
|
||||
def __init__(self, quant_dtype: torch.dtype) -> None:
|
||||
super().__init__(quant_dtype)
|
||||
if quant_dtype == torch.float8_e4m3fn:
|
||||
self._op = torch.ops.npu.npu_dynamic_mx_quant
|
||||
elif quant_dtype in (torch.int8, torch.quint4x2):
|
||||
self._op = torch.ops.npu.npu_dynamic_quant
|
||||
else:
|
||||
raise ValueError(f"Unsupported dynamic quant dtype: {quant_dtype}")
|
||||
|
||||
def __call__(
|
||||
self, hidden_states: torch.Tensor
|
||||
) -> Tuple[torch.Tensor, torch.Tensor]:
|
||||
quantized, scale = torch.ops.npu.npu_dynamic_quant(
|
||||
hidden_states, dst_type=self.quant_dtype
|
||||
)
|
||||
quantized, scale = self._op(hidden_states, dst_type=self.quant_dtype)
|
||||
return quantized, scale
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
from typing import TYPE_CHECKING, Any, Dict, Optional
|
||||
from typing import TYPE_CHECKING, Any, Dict, Optional, Tuple
|
||||
|
||||
import numpy as np
|
||||
import torch
|
||||
from torch.nn.parameter import Parameter
|
||||
|
||||
from sglang.srt.environ import envs
|
||||
from sglang.srt.hardware_backend.npu.utils import npu_format_cast
|
||||
@@ -13,14 +14,56 @@ if TYPE_CHECKING:
|
||||
|
||||
import logging
|
||||
|
||||
from sglang.srt.hardware_backend.npu.moe.hidden_states_quant import (
|
||||
HiddenStatesDynamicQuant,
|
||||
from sglang.srt.hardware_backend.npu.moe.matmul import (
|
||||
GroupedMatmul,
|
||||
GroupedMatmulSwigluQuant,
|
||||
)
|
||||
from sglang.srt.hardware_backend.npu.moe.quant import HiddenStatesDynamicQuant
|
||||
from sglang.srt.hardware_backend.npu.quantization.linear_method_npu import (
|
||||
_get_float8_e8m0fnu_dtype,
|
||||
)
|
||||
from sglang.srt.hardware_backend.npu.moe.matmul import GroupedMatmul
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
_E8M0_DTYPE = None
|
||||
|
||||
|
||||
def _require_e8m0_dtype():
|
||||
"""Resolve the e8m0 block-scale dtype, failing loudly if it is unavailable.
|
||||
|
||||
The grouped matmuls validate their scale-dtype arguments against torch_npu's
|
||||
own dtype enum (``torch_npu.float8_e8m0fnu``, 293 on A5) and reject the torch
|
||||
dtype object with "weight_scale_dtype only supports float8_e8m0fnu or None,
|
||||
but the actual value is Float8_e8m0fnu" — hence torch_npu first, torch only
|
||||
as a fallback. Dense ``npu_quant_matmul`` accepts either, which is why
|
||||
``_get_float8_e8m0fnu_dtype`` reads it off torch.
|
||||
|
||||
The MXFP8 ops take the scale dtype explicitly; passing None silently changes
|
||||
how they interpret the scales, so a missing dtype must raise rather than
|
||||
propagate.
|
||||
|
||||
torch_npu is imported lazily (and cached) so this module stays importable on
|
||||
CUDA/CPU/AMD/XPU CI.
|
||||
"""
|
||||
global _E8M0_DTYPE
|
||||
if _E8M0_DTYPE is None:
|
||||
from sglang.srt.utils import is_npu
|
||||
|
||||
if is_npu():
|
||||
import torch_npu
|
||||
|
||||
_E8M0_DTYPE = getattr(torch_npu, "float8_e8m0fnu", None)
|
||||
if _E8M0_DTYPE is None:
|
||||
_E8M0_DTYPE = _get_float8_e8m0fnu_dtype()
|
||||
if _E8M0_DTYPE is None:
|
||||
raise RuntimeError(
|
||||
"float8_e8m0fnu dtype not found — MXFP8 MoE requires Ascend A5 "
|
||||
"with a torch_npu build exposing float8_e8m0fnu (torch_npu >= 2.9)."
|
||||
)
|
||||
return _E8M0_DTYPE
|
||||
|
||||
|
||||
# DEPRECATED METHOD
|
||||
# TODO: Remove in future realeses
|
||||
def fused_moe_npu(
|
||||
@@ -714,3 +757,196 @@ class NPUUnquantMoEMethod(_NPUMoEMethodBase):
|
||||
transposed=False,
|
||||
**self._get_bias_args(quant_info, weight_prefix),
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# NPUMXFP8MoEMethod
|
||||
# ---------------------------------------------------------------------------
|
||||
class NPUMXFP8MoEMethod(_NPUMoEMethodBase):
|
||||
"""MXFP8 MoE on Ascend A5 – float8_e4m3fn weights with e8m0 block scales.
|
||||
|
||||
Serves both the online config path (``--quantization mxfp8``, weights
|
||||
quantised at load time) and the offline ModelSlim ``W8A8_MXFP8`` scheme
|
||||
(weights already quantised); ``process_weights_after_loading`` tells the two
|
||||
apart by weight dtype.
|
||||
|
||||
gmm1 re-quantises its own output, so it is a single fused kernel rather than
|
||||
a matmul plus a separate activation: the runner calls
|
||||
``apply_fused_gmm1_swiglu`` for w13 and ``apply`` only for w2 — hence the
|
||||
per-prefix matmul chosen here.
|
||||
|
||||
Where the *activation* quant happens depends on the dispatcher. On
|
||||
``ascend_tp`` it comes for free from ``npu_moe_init_routing_v2(quant_mode=3)``,
|
||||
which emits the e4m3 payload and e8m0 scale as part of routing. DeepEP has no
|
||||
mxfp8 dispatch dtype, so it hands over bf16 and w13 quantises the hidden
|
||||
states itself before gmm1.
|
||||
"""
|
||||
|
||||
def __init__(self, weight_prefix: str):
|
||||
super().__init__(quant_config=None)
|
||||
if weight_prefix == "w13":
|
||||
self.matmul = GroupedMatmulSwigluQuant()
|
||||
self.hidden_states_quantizer = HiddenStatesDynamicQuant(
|
||||
quant_dtype=torch.float8_e4m3fn
|
||||
)
|
||||
else:
|
||||
self.matmul = GroupedMatmul()
|
||||
self.hidden_states_quantizer = None
|
||||
|
||||
@staticmethod
|
||||
def _quantize_weight_online(
|
||||
weight: torch.Tensor, weight_prefix: str
|
||||
) -> Tuple[torch.Tensor, torch.Tensor]:
|
||||
"""Quantise BF16/FP16 expert weights ``[E, N, K]`` to MXFP8 at load time.
|
||||
|
||||
Returns the e4m3 payload ``[E, N, K]`` and its e8m0 block scale
|
||||
``[E, N, K//64, 2]`` (block_size=32, already pair-split by the op).
|
||||
"""
|
||||
if weight.dtype not in (torch.float16, torch.bfloat16):
|
||||
logger.warning(
|
||||
"NPUMXFP8MoEMethod: %s_weight dtype %s is not float16/bfloat16; "
|
||||
"casting to bfloat16 before MXFP8 quantisation.",
|
||||
weight_prefix,
|
||||
weight.dtype,
|
||||
)
|
||||
weight = weight.to(torch.bfloat16)
|
||||
# cpu offload may have moved the weight back to host memory.
|
||||
if not weight.is_npu:
|
||||
weight = weight.to(f"npu:{torch.npu.current_device()}")
|
||||
return torch.ops.npu.npu_dynamic_mx_quant(weight, dst_type=torch.float8_e4m3fn)
|
||||
|
||||
def process_weights_after_loading(
|
||||
self, layer: torch.nn.Module, weight_prefix: str
|
||||
) -> None:
|
||||
self._validate_weight_prefix(layer, weight_prefix)
|
||||
|
||||
weight: torch.Tensor = getattr(layer, f"{weight_prefix}_weight").data
|
||||
if weight.dtype == torch.float8_e4m3fn:
|
||||
# Offline (ModelSlim) path: the checkpoint already holds e4m3 weights
|
||||
# and {prefix}_weight_scale holds uint8 block scales [E, N, K//32].
|
||||
# Only re-layout: split the flat scale axis into pairs to match what
|
||||
# npu_dynamic_mx_quant produces online.
|
||||
scale: torch.Tensor = getattr(layer, f"{weight_prefix}_weight_scale").data
|
||||
scale = scale.reshape(scale.shape[0], -1, scale.shape[-1] // 2, 2)
|
||||
else:
|
||||
weight, scale = self._quantize_weight_online(weight, weight_prefix)
|
||||
|
||||
# FRACTAL_NZ before the transpose, never after. gmm1 asserts that weight
|
||||
# and weight_scale carry the SAME transpose flag (CheckMXTranspose: "the
|
||||
# transposition of weightScale/weight should be equal"), and the cast
|
||||
# yields a physically retiled — hence non-transposed — tensor. Casting
|
||||
# the [E, K, N] view would therefore leave the weight at false against a
|
||||
# true scale and fail outright, which is why this cannot copy the int8
|
||||
# MoE methods above (they transpose first, but carry no MX scale to keep
|
||||
# in sync). Same order as the dense W4A8 path in linear_method_npu.py.
|
||||
#
|
||||
# A5 measurement, Qwen3-30B-A3B shapes, 128 experts (see
|
||||
# llm/probe_mxfp8_moe_nz.py): +1.4% decode, +3.8% prefill against a 0.2-
|
||||
# 0.3% noise floor, bit-identical outputs. Set
|
||||
# SGLANG_NPU_DISABLE_ACL_FORMAT_WEIGHT to fall back to plain ND.
|
||||
weight = npu_format_cast(weight)
|
||||
|
||||
# Both paths hand the grouped matmul weight [E, K, N] and scale
|
||||
# [E, K//64, N, 2] as strided transpose views — DO NOT call
|
||||
# .contiguous(). Beyond breaking the transpose-flag match above, it
|
||||
# measures slower on the same probe: making both sides contiguous costs
|
||||
# 6.2% on decode. This matches NPUMXFP8LinearMethod, msmodelslim's
|
||||
# offline layout and vllm-ascend's AscendW8A8MXFP8DynamicFusedMoEMethod.
|
||||
setattr(
|
||||
layer,
|
||||
f"{weight_prefix}_weight",
|
||||
Parameter(weight.transpose(1, 2), requires_grad=False),
|
||||
)
|
||||
setattr(
|
||||
layer,
|
||||
f"{weight_prefix}_weight_scale",
|
||||
Parameter(scale.transpose(1, 2), requires_grad=False),
|
||||
)
|
||||
|
||||
if weight_prefix == "w13":
|
||||
from sglang.srt.layers.moe import get_moe_a2a_backend
|
||||
|
||||
# DeepEP has no mxfp8 entry in its dispatch dtype table, so let it
|
||||
# keep sending bf16; apply_fused_gmm1_swiglu quantises instead.
|
||||
dispatcher_dtype = "bf16" if get_moe_a2a_backend().is_deepep() else "mxfp8"
|
||||
self._set_dispatcher_output_dtype(layer, dispatcher_dtype)
|
||||
|
||||
def apply_fused_gmm1_swiglu(
|
||||
self,
|
||||
quant_info: "AscendQuantInfo",
|
||||
hidden_states: torch.Tensor,
|
||||
expert_tokens: torch.Tensor,
|
||||
pertoken_scale: Optional[torch.Tensor],
|
||||
group_list_type,
|
||||
) -> Tuple[torch.Tensor, torch.Tensor]:
|
||||
"""Gate/up projection, swiglu and requantisation in one kernel (gmm1).
|
||||
|
||||
Returns the e4m3 activations and their e8m0 block scale, i.e. exactly
|
||||
what the w2 gmm needs, which is why the runner skips its activation step
|
||||
for MXFP8.
|
||||
|
||||
``pertoken_scale`` is None when the dispatcher handed over unquantised
|
||||
hidden states (the DeepEP path), in which case the activation quant that
|
||||
ascend_tp fuses into routing is done here instead. Both dispatchers
|
||||
therefore reach the kernel below with the same e4m3 + e8m0 input.
|
||||
"""
|
||||
if pertoken_scale is None:
|
||||
hidden_states, pertoken_scale = self.hidden_states_quantizer(hidden_states)
|
||||
|
||||
e8m0_dtype = _require_e8m0_dtype()
|
||||
return self.matmul.forward(
|
||||
quant_info,
|
||||
"w13",
|
||||
hidden_states,
|
||||
expert_tokens,
|
||||
group_list_type=group_list_type,
|
||||
transposed=True,
|
||||
weight_scale=[quant_info.w13_weight_scale],
|
||||
x_scale=pertoken_scale,
|
||||
dequant_mode=2,
|
||||
quant_mode=2,
|
||||
dequant_dtype=torch.float32,
|
||||
quant_dtype=torch.float8_e4m3fn,
|
||||
# e4m3 is implicit for these two — it is not in the op's QUANT_DTYPES.
|
||||
x_dtype=None,
|
||||
weight_dtype=None,
|
||||
weight_scale_dtype=e8m0_dtype,
|
||||
x_scale_dtype=e8m0_dtype,
|
||||
)
|
||||
|
||||
def apply(
|
||||
self,
|
||||
quant_info: "AscendQuantInfo",
|
||||
hidden_states: torch.Tensor,
|
||||
expert_tokens: torch.Tensor,
|
||||
pertoken_scale: torch.Tensor,
|
||||
output_dtype: torch.dtype,
|
||||
weight_prefix: str,
|
||||
group_list_type,
|
||||
) -> torch.Tensor:
|
||||
if weight_prefix != "w2":
|
||||
raise ValueError(
|
||||
f"NPUMXFP8MoEMethod.apply only serves the w2 gmm, got "
|
||||
f"'{weight_prefix}'. gmm1 fuses swiglu into a single op and must "
|
||||
f"go through apply_fused_gmm1_swiglu, which returns a scale too."
|
||||
)
|
||||
|
||||
e8m0_dtype = _require_e8m0_dtype()
|
||||
scale_args: Dict[str, Any] = {
|
||||
"scale": [getattr(quant_info, f"{weight_prefix}_weight_scale", None)],
|
||||
"per_token_scale": [pertoken_scale],
|
||||
"scale_dtype": e8m0_dtype,
|
||||
"per_token_scale_dtype": e8m0_dtype,
|
||||
"x_dtype": None,
|
||||
"weight_dtype": None,
|
||||
}
|
||||
return self.matmul.forward(
|
||||
quant_info,
|
||||
weight_prefix,
|
||||
hidden_states,
|
||||
expert_tokens,
|
||||
output_dtype,
|
||||
group_list_type=group_list_type,
|
||||
transposed=True,
|
||||
**scale_args,
|
||||
)
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
"""Online (config-driven) quantized FusedMoE methods for Ascend NPU.
|
||||
|
||||
These are the ``--quantization <scheme>`` entry points: the checkpoint holds
|
||||
BF16/FP16 expert weights and the per-gmm kernels quantize them at load time.
|
||||
Offline (msmodelslim) checkpoints go through the ModelSlim schemes instead and
|
||||
reuse the same kernels.
|
||||
|
||||
Kept out of ``moe_methods.py`` because ``unquant.py`` imports that module at
|
||||
module scope, so subclassing ``UnquantizedFusedMoEMethod`` there would be a
|
||||
circular import.
|
||||
"""
|
||||
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
|
||||
import torch
|
||||
|
||||
from sglang.srt.hardware_backend.npu.quantization.moe_methods import NPUMXFP8MoEMethod
|
||||
from sglang.srt.layers.moe.moe_runner import MoeRunner
|
||||
from sglang.srt.layers.moe.utils import MoeRunnerBackend, get_moe_runner_backend
|
||||
from sglang.srt.layers.quantization.unquant import UnquantizedFusedMoEMethod
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from sglang.srt.layers.moe.moe_runner.base import MoeRunnerConfig
|
||||
from sglang.srt.layers.quantization.base_config import QuantizationConfig
|
||||
|
||||
|
||||
class NPUMXFP8OnlineMoEMethod(UnquantizedFusedMoEMethod):
|
||||
"""Online MXFP8 FusedMoE entry point (``--quantization mxfp8`` on A5).
|
||||
|
||||
Weight creation, weight post-processing and the forward pass are identical
|
||||
to the unquantized Ascend path — the only difference is which per-gmm kernel
|
||||
the layer gets, so everything but ``create_moe_runner`` is inherited.
|
||||
``NPUMXFP8MoEMethod`` then quantizes the BF16 expert weights to MXFP8 in
|
||||
``process_weights_after_loading``.
|
||||
"""
|
||||
|
||||
def __init__(self, quant_config: Optional["QuantizationConfig"] = None):
|
||||
super().__init__()
|
||||
self.quant_config = quant_config
|
||||
|
||||
def create_moe_runner(
|
||||
self, layer: torch.nn.Module, moe_runner_config: "MoeRunnerConfig"
|
||||
):
|
||||
backend = get_moe_runner_backend()
|
||||
if not (backend.is_auto() or backend.is_ascend()):
|
||||
# Not merely a wrong-runner check. Because this method subclasses
|
||||
# UnquantizedFusedMoEMethod it matches FusedMoE's shard-swap list, so
|
||||
# a flashinfer backend would make the weight loader exchange the
|
||||
# w1/w3 shards ("flashinfer assumes w31 format"). Every expert would
|
||||
# then load with gate and up swapped, and gmm1's fused swiglu would
|
||||
# compute silu(up) * gate — no error, just degenerate output.
|
||||
raise ValueError(
|
||||
"MXFP8 MoE on Ascend requires --moe-runner-backend 'auto' or "
|
||||
f"'ascend', got {backend.value!r}."
|
||||
)
|
||||
|
||||
# The kernels must be attached before the runner is built:
|
||||
# AscendRunnerCore.__init__ reads layer.w2_kernel to pick its activation.
|
||||
layer.w13_kernel = NPUMXFP8MoEMethod("w13")
|
||||
layer.w2_kernel = NPUMXFP8MoEMethod("w2")
|
||||
moe_runner_config.layer = layer
|
||||
self.moe_runner_config = moe_runner_config
|
||||
self.runner = MoeRunner(MoeRunnerBackend.ASCEND, moe_runner_config)
|
||||
# Inherited apply() consults this; aiter is CUDA/ROCm-only.
|
||||
self._aiter_runner = None
|
||||
@@ -113,18 +113,25 @@ def init_npu_backend():
|
||||
def _is_nz_aligned(tensor: torch.Tensor) -> bool:
|
||||
"""Check whether the last two dims satisfy FRACTAL_NZ alignment rules.
|
||||
|
||||
Ascend FRACTAL_NZ requires:
|
||||
BF16 / FP16 : both dims divisible by 16
|
||||
INT8 : k % 16 == 0 and n % 32 == 0
|
||||
A fractal tile is 16 rows by 32 bytes (the C0_32 in the op's error strings),
|
||||
so the row rule is always k % 16 and the column rule is 32 // itemsize:
|
||||
|
||||
BF16 / FP16 : k % 16 == 0 and n % 16 == 0
|
||||
INT8 / FP8 : k % 16 == 0 and n % 32 == 0
|
||||
INT4 : k % 16 == 0 and n % 64 == 0
|
||||
FP4 : both dims divisible by 64
|
||||
|
||||
Unlisted dtypes fall through to True: this is a cheap pre-filter for known
|
||||
bad combinations, not an authority — the op itself is.
|
||||
"""
|
||||
if tensor.dim() < 2:
|
||||
return False
|
||||
k, n = tensor.shape[-2], tensor.shape[-1]
|
||||
if tensor.dtype in (torch.bfloat16, torch.float16):
|
||||
return k % 16 == 0 and n % 16 == 0
|
||||
if tensor.dtype == torch.int8:
|
||||
if tensor.dtype in (torch.int8, torch.float8_e4m3fn):
|
||||
# e4m3 is single-byte like int8, so it shares the column rule. Reached
|
||||
# only by the MXFP8 MoE weights; the packed-FP4 callers pass
|
||||
# customize_dtype and return before this check.
|
||||
return k % 16 == 0 and n % 32 == 0
|
||||
if tensor.dtype in (torch.uint8, torch.int32):
|
||||
# INT4 is typically packed into uint8/int32; be conservative
|
||||
|
||||
@@ -17,6 +17,7 @@ from sglang.srt.hardware_backend.npu.moe.activation import (
|
||||
NPUSwigluStepAndMul,
|
||||
)
|
||||
from sglang.srt.hardware_backend.npu.quantization.moe_methods import (
|
||||
NPUMXFP8MoEMethod,
|
||||
NPUW4A8Int8MoEMethod,
|
||||
NPUW8A8Int8MoEMethod,
|
||||
)
|
||||
@@ -87,7 +88,15 @@ class AscendRunnerCore(MoeRunnerCore):
|
||||
|
||||
kernel = config.layer.w2_kernel
|
||||
|
||||
if get_moe_a2a_backend().is_deepep():
|
||||
if isinstance(kernel, NPUMXFP8MoEMethod):
|
||||
# MXFP8 fuses gate/up + swiglu + requant into gmm1, so there is no
|
||||
# separate activation step — run() skips it. Left None on purpose so
|
||||
# that reaching for it fails loudly instead of silently applying an
|
||||
# unfused swiglu to already-requantised activations. This holds for
|
||||
# both dispatchers: ascend_tp gets its activation quant fused into
|
||||
# routing, DeepEP dispatches bf16 and gmm1 quantises it itself.
|
||||
self.activation = None
|
||||
elif get_moe_a2a_backend().is_deepep():
|
||||
# DeepEP path: use a unified kernel that decides quantisation
|
||||
is_quant_kernel = isinstance(
|
||||
kernel, (NPUW4A8Int8MoEMethod, NPUW8A8Int8MoEMethod)
|
||||
@@ -134,30 +143,44 @@ class AscendRunnerCore(MoeRunnerCore):
|
||||
expert_tokens = runner_input.expert_tokens
|
||||
group_list_type = runner_input.group_list_type
|
||||
|
||||
# --- w13 (gate & up) projection ---
|
||||
hidden_states = self.config.layer.w13_kernel.apply(
|
||||
quant_info,
|
||||
x,
|
||||
expert_tokens,
|
||||
pertoken_scale=runner_input.hidden_states_scale,
|
||||
output_dtype=original_dtype,
|
||||
weight_prefix="w13",
|
||||
group_list_type=group_list_type,
|
||||
)
|
||||
w13_kernel = self.config.layer.w13_kernel
|
||||
|
||||
# --- Activation ---
|
||||
# The DeepEP kernel expects extra dispatch metadata
|
||||
if isinstance(self.activation, NPUSwigluDeepEPKernel):
|
||||
hidden_states, pertoken_scale = self.activation._apply_activation(
|
||||
hidden_states,
|
||||
group_list=expert_tokens,
|
||||
if isinstance(w13_kernel, NPUMXFP8MoEMethod):
|
||||
# --- w13 projection + activation, fused into one kernel ---
|
||||
# MXFP8 gmm1 returns activations already requantised for gmm2, so
|
||||
# there is no separate activation step to run.
|
||||
hidden_states, pertoken_scale = w13_kernel.apply_fused_gmm1_swiglu(
|
||||
quant_info,
|
||||
x,
|
||||
expert_tokens,
|
||||
pertoken_scale=runner_input.hidden_states_scale,
|
||||
group_list_type=group_list_type,
|
||||
)
|
||||
else:
|
||||
hidden_states, pertoken_scale = self.activation._apply_activation(
|
||||
hidden_states
|
||||
# --- w13 (gate & up) projection ---
|
||||
hidden_states = w13_kernel.apply(
|
||||
quant_info,
|
||||
x,
|
||||
expert_tokens,
|
||||
pertoken_scale=runner_input.hidden_states_scale,
|
||||
output_dtype=original_dtype,
|
||||
weight_prefix="w13",
|
||||
group_list_type=group_list_type,
|
||||
)
|
||||
|
||||
# --- Activation ---
|
||||
# The DeepEP kernel expects extra dispatch metadata
|
||||
if isinstance(self.activation, NPUSwigluDeepEPKernel):
|
||||
hidden_states, pertoken_scale = self.activation._apply_activation(
|
||||
hidden_states,
|
||||
group_list=expert_tokens,
|
||||
group_list_type=group_list_type,
|
||||
)
|
||||
else:
|
||||
hidden_states, pertoken_scale = self.activation._apply_activation(
|
||||
hidden_states
|
||||
)
|
||||
|
||||
# --- w2 (down) projection ---
|
||||
hidden_states = self.config.layer.w2_kernel.apply(
|
||||
quant_info,
|
||||
|
||||
@@ -9,6 +9,7 @@ from sglang.srt.hardware_backend.npu.moe.finalize_routing import (
|
||||
NPUFinalizeRouting,
|
||||
)
|
||||
from sglang.srt.hardware_backend.npu.moe.init_routing import (
|
||||
MXFP8_QUANT_MODE,
|
||||
NPUMoEInitRouting_v2,
|
||||
)
|
||||
from sglang.srt.layers.moe.moe_runner.base import MoeRunnerConfig
|
||||
@@ -84,6 +85,10 @@ class AscendTPDispatcher(BaseDispatcher):
|
||||
self.init = NPUMoEInitRouting_v2(quant_mode=1)
|
||||
self.finalize = NPUFinalizeRouting(drop_pad_mode=2)
|
||||
self.group_list_type = 1
|
||||
elif self.ascend_dispatcher_output_dtype == DispatcherOutputDtype.MXFP8:
|
||||
self.init = NPUMoEInitRouting_v2(quant_mode=MXFP8_QUANT_MODE)
|
||||
self.finalize = NPUFinalizeRouting(drop_pad_mode=2)
|
||||
self.group_list_type = 1
|
||||
else:
|
||||
raise ValueError(
|
||||
f"Unsupported ascend_dispatcher_output_dtype: {self.ascend_dispatcher_output_dtype}"
|
||||
|
||||
@@ -209,12 +209,14 @@ class DispatcherOutputDtype(Enum):
|
||||
- FP8: dispatch hidden states in fp8
|
||||
- INT8: dispatch hidden states in int8
|
||||
- NVFP4: dispatch hidden states in nvfp4
|
||||
- MXFP8: dispatch hidden states in mxfp8 (fp8_e4m3 + e8m0 block scale)
|
||||
"""
|
||||
|
||||
BF16 = "bf16"
|
||||
FP8 = "fp8"
|
||||
INT8 = "int8"
|
||||
NVFP4 = "nvfp4"
|
||||
MXFP8 = "mxfp8"
|
||||
|
||||
|
||||
def get_deepep_output_dtype(self) -> DispatcherOutputDtype:
|
||||
|
||||
@@ -360,6 +360,13 @@ class Fp8Config(QuantizationConfig):
|
||||
layer.use_triton_kernels, layer.use_flashinfer_trtllm_moe
|
||||
)
|
||||
|
||||
if is_npu() and self.use_mxfp8:
|
||||
from sglang.srt.hardware_backend.npu.quantization.online_moe_methods import (
|
||||
NPUMXFP8OnlineMoEMethod,
|
||||
)
|
||||
|
||||
return NPUMXFP8OnlineMoEMethod(self)
|
||||
|
||||
fp8_method = Fp8MoEMethod(self)
|
||||
|
||||
if self.is_fp4_experts and self.dequant_fp4_to_fp8:
|
||||
|
||||
@@ -18,6 +18,7 @@ from sglang.srt.layers.quantization.base_config import (
|
||||
from sglang.srt.layers.quantization.modelslim.schemes import (
|
||||
ModelSlimMXFP4Scheme,
|
||||
ModelSlimMXFP4W4A8Scheme,
|
||||
ModelSlimMXFP8MoEScheme,
|
||||
ModelSlimMXFP8Scheme,
|
||||
ModelSlimW4A4Int4,
|
||||
ModelSlimW4A4Int4MoE,
|
||||
@@ -238,6 +239,7 @@ class ModelSlimConfig(QuantizationConfig):
|
||||
("W4A4_DYNAMIC", ModelSlimW4A4Int4MoE),
|
||||
("W4A8_DYNAMIC", ModelSlimW4A8Int8MoE),
|
||||
("W8A8_DYNAMIC", ModelSlimW8A8Int8MoE),
|
||||
("W8A8_MXFP8", ModelSlimMXFP8MoEScheme),
|
||||
]
|
||||
|
||||
# Try multiple naming conventions:
|
||||
|
||||
@@ -10,6 +10,7 @@ from .modelslim_mxfp4_w4a8 import ModelSlimMXFP4W4A8Scheme
|
||||
from .modelslim_mxfp4 import ModelSlimMXFP4Scheme
|
||||
|
||||
# isort: on
|
||||
from .modelslim_mxfp8_moe import ModelSlimMXFP8MoEScheme
|
||||
from .modelslim_w4a4_int4 import ModelSlimW4A4Int4
|
||||
from .modelslim_w4a4_int4_moe import ModelSlimW4A4Int4MoE
|
||||
from .modelslim_w4a8_int8_moe import ModelSlimW4A8Int8MoE
|
||||
@@ -22,6 +23,7 @@ __all__ = [
|
||||
"ModelSlimMXFP8Scheme",
|
||||
"ModelSlimMXFP4W4A8Scheme",
|
||||
"ModelSlimMXFP4Scheme",
|
||||
"ModelSlimMXFP8MoEScheme",
|
||||
"ModelSlimW8A8Int8",
|
||||
"ModelSlimW4A4Int4",
|
||||
"ModelSlimW4A4Int4MoE",
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
"""ModelSlim MXFP8 offline scheme for MoE layers on Ascend NPU (SRT).
|
||||
|
||||
Loads weights pre-quantised by msmodelslim: float8_e4m3fn weights + uint8 block
|
||||
scales (block_size=32). The layout transform and the forward pass are delegated
|
||||
to ``NPUMXFP8MoEMethod`` -- the same kernel the online MXFP8 MoE path uses.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Any, Dict
|
||||
|
||||
import torch
|
||||
|
||||
from sglang.srt.hardware_backend.npu.quantization.moe_methods import NPUMXFP8MoEMethod
|
||||
from sglang.srt.layers.quantization.modelslim.schemes import ModelSlimMoEScheme
|
||||
from sglang.srt.utils import set_weight_attrs
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
__all__ = [
|
||||
"ModelSlimMXFP8MoEScheme",
|
||||
]
|
||||
|
||||
# Block (group) size of the msmodelslim MXFP8 export format.
|
||||
MXFP8_BLOCK_SIZE = 32
|
||||
|
||||
|
||||
class ModelSlimMXFP8MoEScheme(ModelSlimMoEScheme):
|
||||
"""
|
||||
Offline MXFP8 MoE scheme that creates weights for either the
|
||||
w13 (gate+up) or w2 (down) projection group.
|
||||
|
||||
Two instances of this class are used per MoE layer:
|
||||
- weight_prefix="w13" → handles the fused gate_proj + up_proj weights
|
||||
- weight_prefix="w2" → handles the down_proj weights
|
||||
|
||||
The float8_e4m3fn weight dtype allocated here is what tells
|
||||
``NPUMXFP8MoEMethod.process_weights_after_loading`` to take its offline
|
||||
(re-layout only) branch instead of quantising the weights itself.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
quant_config: Dict[str, Any],
|
||||
weight_prefix: str, # "w13" or "w2"
|
||||
) -> None:
|
||||
if weight_prefix not in ("w13", "w2"):
|
||||
raise ValueError(
|
||||
f"weight_prefix must be 'w13' or 'w2', got '{weight_prefix}'"
|
||||
)
|
||||
self.quant_config = quant_config
|
||||
self.weight_prefix = weight_prefix
|
||||
self.kernel = NPUMXFP8MoEMethod(weight_prefix)
|
||||
|
||||
def create_weights(
|
||||
self,
|
||||
layer: torch.nn.Module,
|
||||
num_experts: int,
|
||||
hidden_size: int,
|
||||
intermediate_size_per_partition: int,
|
||||
**extra_weight_attrs,
|
||||
) -> None:
|
||||
from sglang.srt.layers.moe.fused_moe_triton import FusedMoeWeightScaleSupported
|
||||
|
||||
self.num_experts = num_experts
|
||||
extra_weight_attrs.update(
|
||||
{"quant_method": FusedMoeWeightScaleSupported.BLOCK.value}
|
||||
)
|
||||
|
||||
# Determine shape based on weight group
|
||||
if self.weight_prefix == "w13":
|
||||
a_dim = 2 * intermediate_size_per_partition
|
||||
b_dim = hidden_size
|
||||
else: # w2
|
||||
a_dim = hidden_size
|
||||
b_dim = intermediate_size_per_partition
|
||||
|
||||
prefix = self.weight_prefix
|
||||
|
||||
# Create and register weight: [E, N, K] float8_e4m3fn
|
||||
weight_name = f"{prefix}_weight"
|
||||
weight = torch.nn.Parameter(
|
||||
torch.empty(num_experts, a_dim, b_dim, dtype=torch.float8_e4m3fn),
|
||||
requires_grad=False,
|
||||
)
|
||||
layer.register_parameter(weight_name, weight)
|
||||
set_weight_attrs(weight, extra_weight_attrs)
|
||||
|
||||
# Create and register block scale: [E, N, K // 32] uint8 (e8m0)
|
||||
scale_name = f"{prefix}_weight_scale"
|
||||
scale = torch.nn.Parameter(
|
||||
torch.empty(
|
||||
num_experts, a_dim, b_dim // MXFP8_BLOCK_SIZE, dtype=torch.uint8
|
||||
),
|
||||
requires_grad=False,
|
||||
)
|
||||
layer.register_parameter(scale_name, scale)
|
||||
set_weight_attrs(scale, extra_weight_attrs)
|
||||
|
||||
# MXFP8 is a pure scale format: the e8m0 block exponent above carries
|
||||
# everything, there is no zero point. The int8/int4 schemes do have one,
|
||||
# so ModelSlimMoEMethod.apply reads layer.{w13,w2}_weight_offset
|
||||
# unconditionally when it builds AscendQuantInfo (where the field is
|
||||
# Optional). Register it as None so the attribute exists and resolves to
|
||||
# "no offset" rather than raising AttributeError. A None parameter is
|
||||
# skipped by named_parameters(), so no weight loader looks for it.
|
||||
layer.register_parameter(f"{prefix}_weight_offset", None)
|
||||
|
||||
def process_weights_after_loading(self, layer: torch.nn.Module) -> None:
|
||||
"""
|
||||
Delegate weight processing to the NPU kernel for the fixed weight group.
|
||||
"""
|
||||
self.kernel.process_weights_after_loading(layer, self.weight_prefix)
|
||||
@@ -438,10 +438,12 @@ class UnquantizedFusedMoEMethod(FusedMoEMethodBase, MultiPlatformOp):
|
||||
layer.num_local_experts, *new_shape_w2
|
||||
)
|
||||
if _is_npu:
|
||||
# The kernels set the dispatcher output dtype themselves -- they are
|
||||
# the ones that know what their gmms expect. NPUUnquantMoEMethod
|
||||
# already sets bf16 here, and hardcoding it a second time would
|
||||
# clobber a subclass that attached a quantized kernel instead.
|
||||
layer.w13_kernel.process_weights_after_loading(layer, "w13")
|
||||
layer.w2_kernel.process_weights_after_loading(layer, "w2")
|
||||
if hasattr(layer, "dispatcher"):
|
||||
layer.dispatcher.set_quant_config({"dispatcher_output_dtype": "bf16"})
|
||||
|
||||
return
|
||||
|
||||
|
||||
@@ -271,11 +271,21 @@ class Qwen3MoeSparseMoeBlock(nn.Module):
|
||||
routing_method_type=RoutingMethodType.Renormalize,
|
||||
)
|
||||
|
||||
# Router gate: description-driven quant, mirroring vllm-ascend. Only the
|
||||
# offline ModelSlim path (which carries a per-layer quant_model_description)
|
||||
# may quantise the gate — if the checkpoint stored it as MXFP8 it is loaded
|
||||
# and dequantised correctly instead of cast to bf16 without its block scale.
|
||||
# The online Fp8/mxfp8 path keeps the gate in bf16 (unchanged, verified).
|
||||
gate_quant_config = (
|
||||
quant_config
|
||||
if (quant_config is not None and quant_config.get_name() == "modelslim")
|
||||
else None
|
||||
)
|
||||
self.gate = ReplicatedLinear(
|
||||
config.hidden_size,
|
||||
config.num_experts,
|
||||
bias=False,
|
||||
quant_config=None,
|
||||
quant_config=gate_quant_config,
|
||||
prefix=add_prefix("gate", prefix),
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user