diff --git a/docs_new/docs/advanced_features/quantization.mdx b/docs_new/docs/advanced_features/quantization.mdx
index 96203d352..c3350f5c7 100644
--- a/docs_new/docs/advanced_features/quantization.mdx
+++ b/docs_new/docs/advanced_features/quantization.mdx
@@ -30,7 +30,7 @@ The following table summarizes quantization method support across NVIDIA and AMD
Method |
NVIDIA GPUs |
AMD GPUs (MI300X/MI325X/MI350X) |
- Ascend NPUs (A2/A3) |
+ Ascend NPUs (A2/A3/A5) |
Notes |
@@ -49,6 +49,13 @@ The following table summarizes quantization method support across NVIDIA and AMD
WIP |
Requires CDNA3/CDNA4 with MXFP support; uses Aiter |
+
+ mxfp8 |
+ No |
+ No |
+ Yes (A5 for Diffusion and LLM Dense Linear) |
+ Ascend NPU only; online MXFP8 quantization for Diffusion models (e.g., Wan2.2) and LLM Dense Linear on A5 series; uses CANN npu_dynamic_mx_quant / npu_quant_matmul kernels |
+
blockwise_int8 |
Yes |
@@ -182,13 +189,7 @@ The following table summarizes quantization method support across NVIDIA and AMD
Yes |
Ascend quantization; Uses CANN kernels |
-
- mxfp8 (diffusion) |
- No |
- No |
- Yes (A2/A3) |
- Ascend NPU only; online MXFP8 quantization for diffusion models (e.g., Wan2.2); requires CANN ≥ 8.0.RC3 |
-
+
diff --git a/docs_new/docs/hardware-platforms/ascend-npus/ascend_npu_quantization.mdx b/docs_new/docs/hardware-platforms/ascend-npus/ascend_npu_quantization.mdx
index 2b74132dd..c9fa5dd28 100644
--- a/docs_new/docs/hardware-platforms/ascend-npus/ascend_npu_quantization.mdx
+++ b/docs_new/docs/hardware-platforms/ascend-npus/ascend_npu_quantization.mdx
@@ -45,7 +45,7 @@ SGLang support **mix-bits** quantization (independently defines and loads each l
√ |
- | MXFP8 |
+ MXFP8 (Diffusion, LLM dense) |
Linear |
x |
x |
@@ -316,6 +316,36 @@ python3 -m sglang.launch_server \
> - MoE layers use `npu_grouped_matmul` and `npu_moe_init_routing` / `npu_moe_finalize_routing` for high-performance expert computation.
> - TP (tensor parallelism) sharding is supported for both dense and MoE GGUF models.
+**MXFP8 for LLM dense models (e.g. Qwen3 / Qwen3.5):**
+
+LLM dense W8A8 MXFP8 Linear support on Ascend was added in [PR #22352](https://github.com/sgl-project/sglang/pull/22352). Requires Ascend A5 series or newer (`npu_dynamic_mx_quant` is not available on A2 / A3).
+
+- Online MXFP8 quantization (BF16/FP16 weights → MXFP8 at load time):
+
+```bash Command
+python3 -m sglang.launch_server \
+ --model-path Qwen/Qwen3-8B \
+ --quantization mxfp8 \
+ --device npu --attention-backend ascend \
+ --host 0.0.0.0 --port 30000 \
+ --mem-fraction-static 0.8 --tp-size 1
+```
+
+- Offline MXFP8 quantization (msmodelslim pre-quantized weights, `W8A8_MXFP8` scheme; no `--quantization` flag needed — auto-detected from `quant_model_description.json`):
+
+```bash Command
+python3 -m sglang.launch_server \
+ --model-path /path/to/Qwen3-8B-W8A8-MXFP8 \
+ --device npu --attention-backend ascend \
+ --host 0.0.0.0 --port 30000 \
+ --mem-fraction-static 0.8 --tp-size 1
+```
+
+> **Implementation Notes:**
+> - Online path: `Fp8Config.get_quant_method()` dispatches to `NPUMXFP8LinearMethod`. Weights are quantized once at load via `npu_dynamic_mx_quant(weight, dst_type=torch_npu.float8_e4m3fn)` and pre-transposed to `[in, out]`; activations are per-token quantized at inference and matmul runs via `npu_quant_matmul(..., group_sizes=[1, 1, 32])` (block_size = 32).
+> - Offline path: `ModelSlimMXFP8Scheme` loads `float8_e4m3fn` weights + `float8_e8m0fnu` block scales pre-exported by msmodelslim. Transpose is kept as a non-contiguous view (`.data` assignment) — calling `.contiguous()` would physically reorder the pre-quantized layout and break the block-scale mapping.
+> - MoE MXFP8 (FusedMoE/TP) for LLMs is tracked separately and not part of this PR.
+
## Diffusion Model Quantization on Ascend NPU
SGLang-Diffusion supports MXFP8 online and offline quantization for diffusion models (such as Wan2.2) on Ascend NPUs. MXFP8 requires A5; the ModelSlim W8A8/W4A4 schemes work on A2/A3.
diff --git a/python/sglang/srt/hardware_backend/npu/quantization/linear_method_npu.py b/python/sglang/srt/hardware_backend/npu/quantization/linear_method_npu.py
index 788620a31..99dfe16c7 100644
--- a/python/sglang/srt/hardware_backend/npu/quantization/linear_method_npu.py
+++ b/python/sglang/srt/hardware_backend/npu/quantization/linear_method_npu.py
@@ -1,13 +1,30 @@
+import logging
from typing import TYPE_CHECKING, Optional
import torch
+from torch.nn.parameter import Parameter
from sglang.srt.hardware_backend.npu.utils import npu_format_cast
from sglang.srt.layers.quantization.base_config import LinearMethodBase
+from sglang.srt.platforms import current_platform
+
+_is_npu = current_platform.is_npu()
+
+if _is_npu:
+ import torch_npu
if TYPE_CHECKING:
from sglang.srt.layers.quantization.base_config import QuantizationConfig
+logger = logging.getLogger(__name__)
+
+MXFP8_BLOCK_SIZE = 32
+_FLOAT8_E8M0FNU_DTYPE = (
+ getattr(torch_npu, "float8_e8m0fnu", getattr(torch, "float8_e8m0fnu", None))
+ if _is_npu
+ else getattr(torch, "float8_e8m0fnu", None)
+)
+
class _NPULinearMethodBase(LinearMethodBase):
@@ -111,6 +128,135 @@ class NPUW8A8Int8DynamicLinearMethod(_NPULinearMethodBase):
)
+class NPUMXFP8LinearMethod(_NPULinearMethodBase):
+ """Ascend NPU MXFP8 linear method for LLM (SRT) models.
+
+ Online mode: loads FP16/BF16 weights → quantises to MXFP8 at load time.
+ Inference: dynamic MXFP8 activation quant + MXFP8 matmul (block_size=32).
+ """
+
+ def create_weights(
+ self,
+ layer: torch.nn.Module,
+ input_size_per_partition: int,
+ output_partition_sizes,
+ input_size: int,
+ output_size: int,
+ params_dtype: torch.dtype,
+ **extra_weight_attrs,
+ ):
+ from sglang.srt.layers.parameter import ModelWeightParameter
+
+ output_size_per_partition = sum(output_partition_sizes)
+ weight_loader = extra_weight_attrs.get("weight_loader")
+
+ layer.logical_widths = output_partition_sizes
+ layer.input_size_per_partition = input_size_per_partition
+ layer.output_size_per_partition = output_size_per_partition
+ layer.orig_dtype = params_dtype
+
+ # Load weights in original dtype; quantise later in process_weights_after_loading
+ weight = ModelWeightParameter(
+ data=torch.empty(
+ output_size_per_partition,
+ input_size_per_partition,
+ dtype=params_dtype,
+ ),
+ input_dim=1,
+ output_dim=0,
+ weight_loader=weight_loader,
+ )
+ layer.register_parameter("weight", weight)
+
+ def process_weights_after_loading(self, layer: torch.nn.Module) -> None:
+ weight_fp = layer.weight.data
+ if weight_fp.dtype not in (torch.float16, torch.bfloat16):
+ logger.warning(
+ "NPUMXFP8LinearMethod: weight dtype %s is not float16/bfloat16; "
+ "casting to bfloat16 before MXFP8 quantisation.",
+ weight_fp.dtype,
+ )
+ weight_fp = weight_fp.to(torch.bfloat16)
+
+ # Move weight to NPU if needed (cpu offload may have moved it back to CPU)
+ if not weight_fp.is_npu:
+ weight_fp = weight_fp.to(f"npu:{torch.npu.current_device()}")
+
+ # Online MXFP8 quantisation of weights (block_size=32).
+ # qw: [out, in] float8_e4m3fn, w_scale: [out, in//64, 2] uint8.
+ qw, w_scale = torch_npu.npu_dynamic_mx_quant(
+ weight_fp, dst_type=torch_npu.float8_e4m3fn
+ )
+ # Transpose to [in, out] / [in//64, out, 2] as a strided view — DO NOT
+ # call .contiguous(). The matmul reduction loop scans the in-dim per
+ # output column; the [out, in] row-major layout gives stride-1 access
+ # for that scan via the transpose view (matches msmodelslim's offline
+ # layout and vllm-ascend's AscendW8A8MXFP8DynamicLinearMethod). Calling
+ # .contiguous() physically reorders to [in, out] row-major, which makes
+ # the inner-loop stride = out and tanks HBM bandwidth.
+ layer.weight = Parameter(qw.transpose(0, 1), requires_grad=False)
+ layer.weight_scale_inv = Parameter(w_scale.transpose(0, 1), requires_grad=False)
+ # Cache FP32 bias once to avoid a per-forward dtype conversion + alloc.
+ if (
+ getattr(layer, "bias", None) is not None
+ and layer.bias.dtype != torch.float32
+ ):
+ layer.bias_fp32 = Parameter(
+ layer.bias.data.to(torch.float32), requires_grad=False
+ )
+ else:
+ layer.bias_fp32 = None
+
+ def apply(
+ self,
+ layer: torch.nn.Module,
+ x: torch.Tensor,
+ bias: Optional[torch.Tensor] = None,
+ ) -> torch.Tensor:
+ original_dtype = x.dtype
+ if original_dtype not in (torch.float16, torch.bfloat16):
+ x = x.to(torch.bfloat16)
+ original_dtype = torch.bfloat16
+
+ # Flatten to 2D [tokens, hidden] for npu_dynamic_mx_quant
+ input_shape = x.shape
+ x_2d = x.reshape(-1, x.shape[-1])
+
+ # Dynamic MXFP8 activation quantisation
+ qx, input_scale = torch_npu.npu_dynamic_mx_quant(
+ x_2d, dst_type=torch_npu.float8_e4m3fn
+ )
+
+ # MXFP8 matmul (weight & scale already transposed at load time)
+ # Use the cached FP32 bias from process_weights_after_loading; fall back
+ # to per-call conversion if the cache was bypassed (e.g. dynamic bias).
+ if bias is None:
+ quant_bias = None
+ elif (
+ bias is getattr(layer, "bias", None)
+ and getattr(layer, "bias_fp32", None) is not None
+ ):
+ quant_bias = layer.bias_fp32
+ else:
+ quant_bias = bias.to(torch.float32)
+
+ output = torch_npu.npu_quant_matmul(
+ qx,
+ layer.weight,
+ layer.weight_scale_inv,
+ scale_dtype=_FLOAT8_E8M0FNU_DTYPE,
+ pertoken_scale=input_scale,
+ pertoken_scale_dtype=_FLOAT8_E8M0FNU_DTYPE,
+ bias=quant_bias,
+ output_dtype=original_dtype,
+ group_sizes=[1, 1, MXFP8_BLOCK_SIZE],
+ )
+
+ # Restore original shape (replace last dim with output features)
+ output_shape = list(input_shape[:-1]) + [output.shape[-1]]
+ return output.reshape(output_shape)
+
+
class NPU_W4A4DynamicLinearMethod(_NPULinearMethodBase):
def process_weights_after_loading(self, layer):
diff --git a/python/sglang/srt/layers/quantization/fp8.py b/python/sglang/srt/layers/quantization/fp8.py
index 437ab2cf0..46c4cb3ac 100644
--- a/python/sglang/srt/layers/quantization/fp8.py
+++ b/python/sglang/srt/layers/quantization/fp8.py
@@ -205,6 +205,8 @@ class Fp8Config(QuantizationConfig):
return [torch.bfloat16, torch.half]
def get_min_capability(self) -> int:
+ if is_npu():
+ return 0 # NPU bypasses CUDA capability checks
if _is_musa:
return 31
@@ -261,6 +263,12 @@ class Fp8Config(QuantizationConfig):
prefix, self.ignored_layers, fused_mapping=self.packed_modules_mapping
):
return UnquantizedLinearMethod()
+ if is_npu() and self.use_mxfp8:
+ from sglang.srt.hardware_backend.npu.quantization.linear_method_npu import (
+ NPUMXFP8LinearMethod,
+ )
+
+ return NPUMXFP8LinearMethod(self)
return Fp8LinearMethod(self)
elif isinstance(layer, FusedMoE):
if is_layer_skipped(
diff --git a/python/sglang/srt/layers/quantization/modelslim/modelslim.py b/python/sglang/srt/layers/quantization/modelslim/modelslim.py
index 50f2647e6..d3106ec46 100644
--- a/python/sglang/srt/layers/quantization/modelslim/modelslim.py
+++ b/python/sglang/srt/layers/quantization/modelslim/modelslim.py
@@ -14,6 +14,7 @@ from sglang.srt.layers.quantization.base_config import (
QuantizationConfig,
)
from sglang.srt.layers.quantization.modelslim.schemes import (
+ ModelSlimMXFP8Scheme,
ModelSlimW4A4Int4,
ModelSlimW4A4Int4MoE,
ModelSlimW4A8Int8MoE,
@@ -183,6 +184,7 @@ class ModelSlimConfig(QuantizationConfig):
("W4A4_DYNAMIC", ModelSlimW4A4Int4),
("W8A8", ModelSlimW8A8Int8),
("W8A8_DYNAMIC", ModelSlimW8A8Int8),
+ ("W8A8_MXFP8", ModelSlimMXFP8Scheme),
]
quant_schemes = [self.quant_description.get(prefix + ".weight", "")]
diff --git a/python/sglang/srt/layers/quantization/modelslim/schemes/__init__.py b/python/sglang/srt/layers/quantization/modelslim/schemes/__init__.py
index c349fd3c4..bfc2a350c 100644
--- a/python/sglang/srt/layers/quantization/modelslim/schemes/__init__.py
+++ b/python/sglang/srt/layers/quantization/modelslim/schemes/__init__.py
@@ -1,6 +1,13 @@
# SPDX-License-Identifier: Apache-2.0
+# NOTE: Import order is critical to avoid circular dependency.
+# modelslim_mxfp8 imports ModelSlimLinearScheme from this package,
+# so the base class must be imported first.
+# isort: off
from .modelslim_scheme import ModelSlimLinearScheme, ModelSlimMoEScheme
+from .modelslim_mxfp8 import ModelSlimMXFP8Scheme
+
+# isort: on
from .modelslim_w4a4_int4 import ModelSlimW4A4Int4
from .modelslim_w4a4_int4_moe import ModelSlimW4A4Int4MoE
from .modelslim_w4a8_int8_moe import ModelSlimW4A8Int8MoE
@@ -10,6 +17,7 @@ from .modelslim_w8a8_int8_moe import ModelSlimW8A8Int8MoE
__all__ = [
"ModelSlimLinearScheme",
"ModelSlimMoEScheme",
+ "ModelSlimMXFP8Scheme",
"ModelSlimW8A8Int8",
"ModelSlimW4A4Int4",
"ModelSlimW4A4Int4MoE",
diff --git a/python/sglang/srt/layers/quantization/modelslim/schemes/modelslim_mxfp8.py b/python/sglang/srt/layers/quantization/modelslim/schemes/modelslim_mxfp8.py
new file mode 100644
index 000000000..1444c7f81
--- /dev/null
+++ b/python/sglang/srt/layers/quantization/modelslim/schemes/modelslim_mxfp8.py
@@ -0,0 +1,146 @@
+"""ModelSlim MXFP8 scheme for pre-quantized weight inference on Ascend NPU (SRT).
+
+Loads weights pre-quantized by msmodelslim (float8_e4m3fn weights,
+uint8 scales) and runs MXFP8 matmul at inference.
+"""
+
+from typing import Dict, List, Optional
+
+import torch
+
+from sglang.srt.layers.parameter import GroupQuantScaleParameter, ModelWeightParameter
+from sglang.srt.layers.quantization.modelslim.schemes import ModelSlimLinearScheme
+from sglang.srt.platforms import current_platform
+
+_is_npu = current_platform.is_npu()
+
+if _is_npu:
+ import torch_npu
+
+MXFP8_BLOCK_SIZE = 32
+_FLOAT8_E8M0FNU_DTYPE = (
+ getattr(torch_npu, "float8_e8m0fnu", getattr(torch, "float8_e8m0fnu", None))
+ if _is_npu
+ else getattr(torch, "float8_e8m0fnu", None)
+)
+
+
+class ModelSlimMXFP8Scheme(ModelSlimLinearScheme):
+
+ def __init__(
+ self,
+ quant_config: Optional[Dict[str, any]] = None,
+ prefix: Optional[str] = None,
+ ):
+ # quant_config / prefix are accepted to match the linear-scheme
+ # dispatch signature used by ModelSlimConfig.get_linear_scheme;
+ # MXFP8 needs no per-layer config beyond what create_weights derives.
+ del quant_config, prefix
+
+ def create_weights(
+ self,
+ layer: torch.nn.Module,
+ input_size_per_partition: int,
+ output_partition_sizes: List[int],
+ input_size: int,
+ output_size: int,
+ params_dtype: torch.dtype,
+ **extra_weight_attrs,
+ ):
+ weight_loader = extra_weight_attrs.get("weight_loader")
+ output_size_per_partition = sum(output_partition_sizes)
+
+ # msmodelslim exports weight as float8_e4m3fn, shape [out, in]
+ weight = ModelWeightParameter(
+ data=torch.empty(
+ (output_size_per_partition, input_size_per_partition),
+ dtype=torch.float8_e4m3fn,
+ ),
+ input_dim=1,
+ output_dim=0,
+ weight_loader=weight_loader,
+ )
+ layer.register_parameter("weight", weight)
+
+ # msmodelslim exports weight_scale as uint8, shape [out, in/32].
+ # NOTE: Named "weight_scale" (not "weight_scale_inv") to match the
+ # checkpoint key exported by msmodelslim.
+ scale_dim = input_size_per_partition // MXFP8_BLOCK_SIZE
+ weight_scale = GroupQuantScaleParameter(
+ data=torch.empty(
+ (output_size_per_partition, scale_dim),
+ dtype=torch.uint8,
+ ),
+ input_dim=1,
+ output_dim=0,
+ weight_loader=weight_loader,
+ )
+ layer.register_parameter("weight_scale", weight_scale)
+
+ def process_weights_after_loading(self, layer: torch.nn.Module):
+ # Pre-transpose weight and scale to [in, out] for npu_quant_matmul.
+ # Use .data assignment without .contiguous() to preserve the transpose
+ # view strides — npu_quant_matmul reads strides correctly and calling
+ # .contiguous() would reorder data, breaking the block-scale mapping.
+ n_dim, k_dim = layer.weight_scale.data.shape
+ layer.weight_scale.data = layer.weight_scale.data.reshape(n_dim, k_dim // 2, 2)
+ layer.weight.data = layer.weight.data.transpose(0, 1)
+ layer.weight_scale.data = layer.weight_scale.data.transpose(0, 1)
+ # Cache FP32 bias once to avoid a per-forward dtype conversion + alloc.
+ if (
+ getattr(layer, "bias", None) is not None
+ and layer.bias.dtype != torch.float32
+ ):
+ layer.bias_fp32 = torch.nn.Parameter(
+ layer.bias.data.to(torch.float32), requires_grad=False
+ )
+ else:
+ layer.bias_fp32 = None
+
+ def apply_weights(
+ self,
+ layer: torch.nn.Module,
+ x: torch.Tensor,
+ bias: Optional[torch.Tensor] = None,
+ ) -> torch.Tensor:
+ original_dtype = x.dtype
+ if original_dtype not in (torch.float16, torch.bfloat16):
+ x = x.to(torch.bfloat16)
+ original_dtype = torch.bfloat16
+
+ # npu_dynamic_mx_quant requires a 2D input [tokens, hidden_size]
+ input_shape = x.shape
+ x_2d = x.reshape(-1, x.shape[-1])
+
+ # Dynamic MXFP8 activation quantisation
+ qx, input_scale = torch_npu.npu_dynamic_mx_quant(
+ x_2d, dst_type=torch_npu.float8_e4m3fn
+ )
+
+ # MXFP8 matmul (weight & scale already transposed at load time).
+ # Use the cached FP32 bias from process_weights_after_loading.
+ if bias is None:
+ quant_bias = None
+ elif (
+ bias is getattr(layer, "bias", None)
+ and getattr(layer, "bias_fp32", None) is not None
+ ):
+ quant_bias = layer.bias_fp32
+ else:
+ quant_bias = bias.to(torch.float32)
+
+ output = torch_npu.npu_quant_matmul(
+ qx,
+ layer.weight,
+ layer.weight_scale,
+ scale_dtype=_FLOAT8_E8M0FNU_DTYPE,
+ pertoken_scale=input_scale,
+ pertoken_scale_dtype=_FLOAT8_E8M0FNU_DTYPE,
+ bias=quant_bias,
+ output_dtype=original_dtype,
+ group_sizes=[1, 1, MXFP8_BLOCK_SIZE],
+ )
+
+ # Restore original shape (replace last dim with output features)
+ output_shape = list(input_shape[:-1]) + [output.shape[-1]]
+ return output.reshape(output_shape)
diff --git a/python/sglang/srt/layers/rotary_embedding/base.py b/python/sglang/srt/layers/rotary_embedding/base.py
index 0770a5786..945e3f845 100644
--- a/python/sglang/srt/layers/rotary_embedding/base.py
+++ b/python/sglang/srt/layers/rotary_embedding/base.py
@@ -2,6 +2,7 @@
from __future__ import annotations
+import logging
from typing import TYPE_CHECKING, Dict, List, Optional, Tuple, Union
import torch
@@ -25,6 +26,8 @@ from sglang.srt.utils import (
if TYPE_CHECKING:
from sglang.jit_kernel.rope import FusedSetKVBufferArg # For type check-only
+logger = logging.getLogger(__name__)
+
_is_cuda = is_cuda()
_is_hip = is_hip()
_use_aiter = get_bool_env_var("SGLANG_USE_AITER") and _is_hip
@@ -40,7 +43,25 @@ if _is_cuda:
if _is_npu:
import torch_npu
- from sgl_kernel_npu.norm.fused_rope_qk_mqa import fused_rope_qk_mqa
+
+ # `fused_rope_qk_mqa` is an optional fast-path kernel shipped with
+ # `sgl_kernel_npu`. Older NPU CANN / sgl_kernel_npu builds may not include
+ # it. If we let the ImportError propagate, importing this module fails,
+ # which in turn causes `ModelRegistry` to silently skip every model that
+ # depends on it (and fall back to HF Transformers without quantisation
+ # awareness — see PR #22352). We tolerate the missing kernel so model
+ # loading still works; call sites must check for `None` and use the
+ # generic rope path. A warning is emitted so the missing kernel is
+ # visible in logs instead of being silently swallowed.
+ try:
+ from sgl_kernel_npu.norm.fused_rope_qk_mqa import fused_rope_qk_mqa
+ except ImportError:
+ fused_rope_qk_mqa = None
+ logger.warning(
+ "sgl_kernel_npu.norm.fused_rope_qk_mqa is unavailable; "
+ "falling back to the generic rope implementation. Upgrade "
+ "sgl_kernel_npu to enable the fused kernel."
+ )
if _is_hip:
from sglang.srt.layers.attention.utils import (
@@ -272,7 +293,10 @@ class RotaryEmbedding(MultiPlatformOp):
else:
cos_sin = self.cos_sin_cache.index_select(0, positions)
- if query.shape[0] * query.shape[1] < 65535:
+ if (
+ fused_rope_qk_mqa is not None
+ and query.shape[0] * query.shape[1] < 65535
+ ):
return fused_rope_qk_mqa(
query,
key,