chore: import upstream snapshot with attribution
PR Test (NPU) / check-changes (push) Has been cancelled
PR Test (NPU) / pr-gate (push) Has been cancelled
PR Test (NPU) / set-image-config (push) Has been cancelled
PR Test (NPU) / stage-b-test-1-npu-a2 (0) (push) Has been cancelled
PR Test (NPU) / stage-b-test-1-npu-a2 (1) (push) Has been cancelled
PR Test (NPU) / stage-b-test-2-npu-a2 (0) (push) Has been cancelled
PR Test (NPU) / stage-b-test-2-npu-a2 (1) (push) Has been cancelled
PR Test (NPU) / stage-b-test-4-npu-a3 (push) Has been cancelled
PR Test (NPU) / stage-b-test-16-npu-a3 (push) Has been cancelled
PR Test (NPU) / multimodal-gen-test-1-npu-a3 (push) Has been cancelled
PR Test (NPU) / multimodal-gen-test-2-npu-a3 (push) Has been cancelled
PR Test (Arm64) / pr-gate (push) Has been cancelled
PR Test (Arm64) / check-changes (push) Has been cancelled
PR Test (Arm64) / build-test (push) Has been cancelled
PR Test (sgl-router) / gate (push) Has been cancelled
PR Test (sgl-router) / tier-1 — lint (push) Has been cancelled
PR Test (sgl-router) / tier-2 — build + test (push) Has been cancelled
PR Test (sgl-router) / tier-3 — docker (placeholder) (push) Has been cancelled
PR Test (sgl-router) / tier-3 — k8s integration (push) Has been cancelled
PR Test (sgl-router) / tier-3 — e2e (push) Has been cancelled
PR Test (sgl-router) / finish (push) Has been cancelled
PR Test (NPU) / single-node-poc (map[name:qwen3_6_27b_w8a8_1p_in64k_out1k_50ms runner:linux-aarch64-a3-2 test_case:test/registered/ascend/performance/qwen3_6_27b/test_npu_qwen3_6_27b_w8a8_1p_in64k_out1k_50ms.py test_type:perf]) (push) Has been cancelled
PR Test (NPU) / pr-test-npu-finish (push) Has been cancelled
PR Test (Xeon) / pr-gate (push) Has been cancelled
PR Test (Xeon) / check-changes (push) Has been cancelled
PR Test (Xeon) / build-test (, xeon-gnr, base-b-test-cpu) (push) Has been cancelled
PR Test (XPU) / check-changes (push) Has been cancelled
PR Test (XPU) / pr-gate (push) Has been cancelled
PR Test (XPU) / stage-a-test-1-gpu-xpu (push) Has been cancelled
PR Test (XPU) / wait-for-stage-a (push) Has been cancelled
PR Test (XPU) / stage-b-test-1-gpu-xpu (push) Has been cancelled
PR Test (XPU) / finish (push) Has been cancelled
CI Model Inventory / build-inventory (push) Has been cancelled
Lint / lint (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark Compilation Check (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark - Manual Policy (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark - Request Processing (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark Summary (push) Has been cancelled
PR Test (SMG) / build-wheel (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on windows (x86_64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on macos (x86_64 - auto) (push) Has been cancelled
PR Test (SMG) / python-unit-tests (push) Has been cancelled
PR Test (SMG) / unit-tests (push) Has been cancelled
PR Test (SMG) / benchmarks (push) Has been cancelled
PR Test (SMG) / chat-completions (push) Has been cancelled
PR Test (SMG) / chat-completions-4gpu (push) Has been cancelled
PR Test (SMG) / e2e (push) Has been cancelled
PR Test (SMG) / docker-build-test (push) Has been cancelled
PR Test (SMG) / k8s-integration (push) Has been cancelled
PR Test (SMG) / finish (push) Has been cancelled
PR Test (SMG) / summarize-benchmarks (push) Has been cancelled
Release SGLang Model Gateway Docker Image / publish (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on macos (aarch64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (aarch64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (x86_64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (aarch64 - musllinux_1_1) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (x86_64 - musllinux_1_1) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / Build SDist (push) Has been cancelled
Release SGLang Model Gateway to PyPI / Upload to PyPI (push) Has been cancelled
Release SGLang Kernels / build-cu129-matrix (aarch64, 12.9, 3.10, arm-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / build-cu129-matrix (x86_64, 12.9, 3.10, x64-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / release-cu129 (push) Has been cancelled
Release SGLang Kernels / build-cu130-matrix (aarch64, 13.0, 3.10, arm-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / build-cu130-matrix (x86_64, 13.0, 3.10, x64-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / release-cu130 (push) Has been cancelled
Release SGLang Kernels / build-rocm-matrix (3.10, 700) (push) Has been cancelled
Release SGLang Kernels / build-rocm-matrix (3.10, 720) (push) Has been cancelled
Release SGLang Kernels / release-rocm700 (push) Has been cancelled
Release SGLang Kernels / release-rocm720 (push) Has been cancelled
Release SGLang Kernels / build-musa43 (43, 3.10) (push) Has been cancelled
Release SGLang Kernels / release-musa43 (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 12:38:16 +08:00
commit 94057c3d3e
7152 changed files with 2120455 additions and 0 deletions
@@ -0,0 +1,174 @@
from __future__ import annotations
from typing import TYPE_CHECKING, Optional
import torch
from sglang.srt.hardware_backend.npu.quantization.fused_moe_method_npu import (
NPUW4A16Int4DynamicMoEMethod,
)
from sglang.srt.layers.quantization.utils import replace_parameter
if TYPE_CHECKING:
from sglang.srt.layers.moe.token_dispatcher import StandardDispatchOutput
from sglang.srt.layers.quantization.base_config import QuantizationConfig
import torch_npu
class AWQAscendLinearKernel:
def __init__(self, quant_config: Optional[QuantizationConfig] = None):
self.quant_config = quant_config
def process_weights_after_loading(self, layer: torch.nn.Module) -> None:
layer.scales = torch.nn.Parameter(layer.scales.data, requires_grad=False)
qweight_tmp = torch.zeros_like(layer.qweight.data)
qzeros_tmp = layer.qzeros.data
qzeros_list = []
shifts = [0, 4, 1, 5, 2, 6, 3, 7]
for i in range(0, self.quant_config.pack_factor):
shift_num = shifts[i] * 4
qzeros_list.append((qzeros_tmp.reshape(-1, 1) >> shift_num) & 0xF)
qweight_tmp.bitwise_or_(
((layer.qweight.data >> shift_num) & 0xF) << (4 * i)
)
qweight_tmp.bitwise_xor_(0x88888888)
qzeros_tmp = torch.cat(qzeros_list, dim=-1).reshape(qzeros_tmp.shape[0], -1)
qzeros_tmp = -(qzeros_tmp - 8)
qzeros_tmp = qzeros_tmp.to(layer.scales.data.dtype)
layer.zeros = torch.nn.Parameter(qzeros_tmp, requires_grad=False)
layer.weight = torch.nn.Parameter(qweight_tmp, requires_grad=False)
def apply(
self,
layer: torch.nn.Module,
x: torch.Tensor,
bias: Optional[torch.Tensor] = None,
) -> torch.Tensor:
qweight = layer.weight
scales = layer.scales
qzeros = layer.zeros
pack_factor = self.quant_config.pack_factor
out_shape = x.shape[:-1] + (qweight.shape[-1] * pack_factor,)
reshaped_x = x.reshape(-1, x.shape[-1])
if bias is not None and bias.dtype == torch.bfloat16:
bias = bias.float()
out = torch_npu.npu_weight_quant_batchmatmul(
reshaped_x,
qweight,
antiquant_scale=scales,
antiquant_offset=qzeros,
antiquant_group_size=self.quant_config.group_size,
bias=bias,
)
return out.reshape(out_shape)
class AWQAscendMoEKernel:
def __init__(self, quant_config: Optional[QuantizationConfig] = None):
self.quant_config = quant_config
self.kernel = NPUW4A16Int4DynamicMoEMethod()
@staticmethod
def _register_or_replace_parameter(
layer: torch.nn.Module, name: str, tensor: torch.Tensor
) -> None:
if hasattr(layer, name):
replace_parameter(layer, name, tensor)
else:
layer.register_parameter(
name, torch.nn.Parameter(tensor, requires_grad=False)
)
def _convert_awq_weight_to_npu_layout(self, qweight: torch.Tensor) -> torch.Tensor:
num_experts, input_size, _ = qweight.shape
unpacked_weight = (
self.kernel._unpack_from_int32(qweight.flatten(0, 1), 4)
.view(num_experts, input_size, -1)
.transpose(1, 2)
.contiguous()
.int()
)
return self.kernel._pack_to_int32(unpacked_weight)
def _convert_awq_qzeros_to_npu_offset(
self, qzeros: torch.Tensor, dtype: torch.dtype
) -> torch.Tensor:
num_experts, num_groups, _ = qzeros.shape
offset = (
-self.kernel._unpack_from_int32(qzeros.flatten(0, 1), 4)
.view(num_experts, num_groups, -1)
.transpose(1, 2)
.contiguous()
)
return offset.to(dtype)
def process_weights_after_loading(self, layer: torch.nn.Module) -> None:
self._register_or_replace_parameter(
layer,
"w13_weight",
self._convert_awq_weight_to_npu_layout(layer.w13_qweight.data),
)
self._register_or_replace_parameter(
layer,
"w2_weight",
self._convert_awq_weight_to_npu_layout(layer.w2_qweight.data),
)
self._register_or_replace_parameter(
layer,
"w13_weight_scale",
layer.w13_scales.data.transpose(1, 2).contiguous(),
)
self._register_or_replace_parameter(
layer,
"w2_weight_scale",
layer.w2_scales.data.transpose(1, 2).contiguous(),
)
self._register_or_replace_parameter(
layer,
"w13_weight_offset",
self._convert_awq_qzeros_to_npu_offset(
layer.w13_qzeros.data, layer.w13_scales.data.dtype
),
)
self._register_or_replace_parameter(
layer,
"w2_weight_offset",
self._convert_awq_qzeros_to_npu_offset(
layer.w2_qzeros.data, layer.w2_scales.data.dtype
),
)
self.kernel.process_weights_after_loading(layer)
def apply(
self,
layer: torch.nn.Module,
dispatch_output: StandardDispatchOutput,
) -> torch.Tensor:
return self.kernel.apply(layer, dispatch_output)
def apply_without_routing_weights(
self,
layer,
hidden_states,
hidden_states_scale,
group_list_type,
group_list,
output_dtype,
):
return self.kernel.apply_without_routing_weights(
layer,
hidden_states,
hidden_states_scale,
group_list_type,
group_list,
output_dtype,
)
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,315 @@
from __future__ import annotations
from typing import TYPE_CHECKING, Optional
import torch
import torch_npu
from sglang.srt.hardware_backend.npu.quantization.fused_moe_method_npu import (
npu_fused_experts,
)
if TYPE_CHECKING:
from sglang.srt.layers.moe import MoeRunnerConfig
from sglang.srt.layers.moe.token_dispatcher import StandardDispatchOutput
from sglang.srt.layers.quantization.base_config import QuantizationConfig
def unpack_from_int32(
weight: torch.Tensor,
num_bits: int,
packed_dim: int = 1,
) -> torch.Tensor:
"""
Unpacks quantized weights from int32 format back to original bits.
:param weight: The packed int32 tensor containing quantized weights
:param num_bits: The number of bits used for quantization (<= 8)
:param packed_dim: Dimension along which weights are packed (0 or 1), defaults to 1
:return: Unpacked tensor with int8 dtype after applying offset correction
"""
assert (
weight.dtype == torch.int32
), f"Expecting `weight.dtype` is torch.int32 but got {weight.dtype}."
assert (
num_bits <= 8
), f"Expecting `num_bits` should not be larger than 8 but got {num_bits}."
pack_factor = 32 // num_bits
mask = (1 << num_bits) - 1
if packed_dim == 1:
unpacked_weight = torch.zeros(
(weight.shape[0], weight.shape[1] * pack_factor),
device=weight.device,
dtype=torch.int32,
)
for i in range(pack_factor):
unpacked_weight[:, i::pack_factor] = (weight >> (num_bits * i)) & mask
else:
unpacked_weight = torch.zeros(
(weight.shape[0] * pack_factor, weight.shape[1]),
device=weight.device,
dtype=torch.int32,
)
for i in range(pack_factor):
unpacked_weight[i::pack_factor, :] = (weight >> (num_bits * i)) & mask
offset = pow(2, num_bits) // 2
unpacked_weight = (unpacked_weight - offset).to(torch.int8)
return unpacked_weight
class GPTQLinearAscendKernel:
def __init__(self, quant_config: Optional[QuantizationConfig] = None):
self.quant_config = quant_config
self.use_v2_format = quant_config.checkpoint_format == "gptq_v2"
def process_weights_after_loading(self, layer: torch.nn.Module) -> None:
layer.qzeros = torch.nn.Parameter(
unpack_from_int32(
layer.qzeros.data.contiguous(),
self.quant_config.weight_bits,
packed_dim=1,
).to(layer.scales.dtype),
requires_grad=False,
)
if not self.use_v2_format:
layer.qzeros += 1
qweight_tmp = unpack_from_int32(
layer.qweight.data.contiguous(), self.quant_config.weight_bits, packed_dim=0
)
# use int8 to store weight by default
if self.quant_config.weight_bits != 4:
layer.qweight = torch.nn.Parameter(
qweight_tmp,
requires_grad=False,
)
return
# for 4bit case we need to pack 4bit weight to int32 to save memory
layer.qweight = torch.nn.Parameter(
torch_npu.npu_convert_weight_to_int4pack(qweight_tmp.to(torch.int32)),
requires_grad=False,
)
def apply(
self,
layer: torch.nn.Module,
x: torch.Tensor,
bias: Optional[torch.Tensor] = None,
) -> torch.Tensor:
qweight = layer.qweight
scales = layer.scales
qzeros = layer.qzeros
reshaped_x = x.reshape(-1, x.shape[-1])
if bias is not None and bias.dtype == torch.bfloat16:
bias = bias.float()
# 4bit weight is packed to int32(8 x int4)
if self.quant_config.weight_bits == 4:
out_shape = x.shape[:-1] + (qweight.shape[-1] * 8,)
else:
out_shape = x.shape[:-1] + (qweight.shape[-1],)
out = torch_npu.npu_weight_quant_batchmatmul(
reshaped_x,
qweight,
antiquant_scale=scales,
antiquant_offset=qzeros,
antiquant_group_size=self.quant_config.group_size,
bias=bias,
)
return out.reshape(out_shape)
class GPTQMoEAscendKernel:
def __init__(self, quant_config: Optional[QuantizationConfig] = None):
self.quant_config = quant_config
self.use_v2_format = quant_config.checkpoint_format == "gptq_v2"
self.moe_runner_config: Optional[MoeRunnerConfig] = None
def create_moe_runner(
self,
layer: torch.nn.Module,
moe_runner_config: MoeRunnerConfig,
**extra_weight_attrs,
):
self.moe_runner_config = moe_runner_config
def process_weights_after_loading(self, layer: torch.nn.Module) -> None:
w13_qzeros_2d = layer.w13_qzeros.data.contiguous().reshape(
-1, layer.w13_qzeros.shape[-1]
)
layer.w13_qzeros = torch.nn.Parameter(
unpack_from_int32(
w13_qzeros_2d,
self.quant_config.weight_bits,
packed_dim=1,
)
.reshape(layer.w13_qzeros.shape[0], layer.w13_qzeros.shape[1], -1)
.to(layer.w13_scales.dtype),
requires_grad=False,
)
if not self.use_v2_format:
layer.w13_qzeros += 1
w2_qzeros_2d = layer.w2_qzeros.data.contiguous().reshape(
-1, layer.w2_qzeros.shape[-1]
)
layer.w2_qzeros = torch.nn.Parameter(
unpack_from_int32(
w2_qzeros_2d,
self.quant_config.weight_bits,
packed_dim=1,
)
.reshape(layer.w2_qzeros.shape[0], layer.w2_qzeros.shape[1], -1)
.to(layer.w2_scales.dtype),
requires_grad=False,
)
if not self.use_v2_format:
layer.w2_qzeros += 1
w13_qweight_2d = (
layer.w13_qweight.data.transpose(-1, -2)
.contiguous()
.reshape(-1, layer.w13_qweight.shape[-2])
)
w13_qweight_tmp = unpack_from_int32(
w13_qweight_2d, self.quant_config.weight_bits, packed_dim=1
)
if self.quant_config.weight_bits == 4:
group_size = self.quant_config.group_size
scale_expanded = layer.w13_scales.data.repeat_interleave(group_size, dim=1)
neg_mask = scale_expanded < 0
if neg_mask.any():
neg_mask = neg_mask.transpose(-1, -2)
neg_mask = neg_mask.contiguous().reshape(w13_qweight_tmp.shape)
w13_qweight_tmp[neg_mask] = -w13_qweight_tmp[neg_mask]
if w13_qweight_tmp.max() > 7:
w13_qweight_tmp.clamp_(max=7)
layer.w13_scales.data.abs_()
layer.w13_qweight = torch.nn.Parameter(
torch_npu.npu_convert_weight_to_int4pack(
w13_qweight_tmp.reshape(
layer.w13_qweight.shape[0], layer.w13_qweight.shape[2], -1
)
.transpose(-1, -2)
.contiguous()
.reshape(-1, layer.w13_qweight.shape[2])
.to(torch.int32)
)
.reshape(layer.w13_qweight.shape[0], layer.w13_qweight.shape[1] * 8, -1)
.contiguous(),
requires_grad=False,
)
# use int8 to store weight by default
else:
layer.w13_qweight = torch.nn.Parameter(
w13_qweight_tmp.reshape(
layer.w13_qweight.shape[0], layer.w13_qweight.shape[2], -1
)
.transpose(-1, -2)
.contiguous(),
requires_grad=False,
)
w2_qweight_2d = (
layer.w2_qweight.data.transpose(-1, -2)
.contiguous()
.reshape(-1, layer.w2_qweight.shape[-2])
)
w2_qweight_tmp = unpack_from_int32(
w2_qweight_2d, self.quant_config.weight_bits, packed_dim=1
)
if self.quant_config.weight_bits == 4:
group_size = self.quant_config.group_size
scale_expanded = layer.w2_scales.data.repeat_interleave(group_size, dim=1)
neg_mask = scale_expanded < 0
if neg_mask.any():
neg_mask = neg_mask.transpose(-1, -2)
neg_mask = neg_mask.contiguous().reshape(w2_qweight_tmp.shape)
w2_qweight_tmp[neg_mask] = -w2_qweight_tmp[neg_mask]
if w2_qweight_tmp.max() > 7:
w2_qweight_tmp.clamp_(max=7)
layer.w2_scales.data.abs_()
layer.w2_qweight = torch.nn.Parameter(
torch_npu.npu_convert_weight_to_int4pack(
w2_qweight_tmp.reshape(
layer.w2_qweight.shape[0], layer.w2_qweight.shape[2], -1
)
.transpose(-1, -2)
.contiguous()
.reshape(-1, layer.w2_qweight.shape[2])
.to(torch.int32)
)
.reshape(layer.w2_qweight.shape[0], layer.w2_qweight.shape[1] * 8, -1)
.contiguous(),
requires_grad=False,
)
# use int8 to store weight by default
else:
layer.w2_qweight = torch.nn.Parameter(
w2_qweight_tmp.reshape(
layer.w2_qweight.shape[0], layer.w2_qweight.shape[2], -1
)
.transpose(-1, -2)
.contiguous(),
requires_grad=False,
)
def apply(
self,
layer: torch.nn.Module,
dispatch_output: StandardDispatchOutput,
) -> torch.Tensor:
from sglang.srt.layers.moe.token_dispatcher import StandardCombineInput
assert self.moe_runner_config is not None, (
"moe_runner_config is not set. "
"Did you forget to call create_weights/create_moe_runner?"
)
assert self.moe_runner_config.activation in ("silu", "swiglu"), (
f"Only SiLU/Swiglu activation is supported, "
f"got {self.moe_runner_config.activation!r}."
)
x = dispatch_output.hidden_states
topk_output = dispatch_output.topk_output
topk_weights, topk_ids, _ = topk_output
topk_ids = topk_ids.to(torch.int32)
topk_weights = topk_weights.to(x.dtype)
output = npu_fused_experts(
hidden_states=x,
w13=layer.w13_qweight,
w13_scale=layer.w13_scales,
w13_offset=layer.w13_qzeros,
w2=layer.w2_qweight,
w2_scale=layer.w2_scales,
w2_offset=layer.w2_qzeros,
topk_weights=topk_weights,
topk_ids=topk_ids,
top_k=topk_ids.shape[1],
use_wna16=True,
)
return StandardCombineInput(hidden_states=output)
@@ -0,0 +1,622 @@
import logging
from typing import TYPE_CHECKING, Optional
import torch
from torch.nn.parameter import Parameter
from sglang.srt.hardware_backend.npu.utils import NPUACLFormat, npu_format_cast
from sglang.srt.layers.quantization.base_config import LinearMethodBase
if TYPE_CHECKING:
from sglang.srt.layers.quantization.base_config import QuantizationConfig
logger = logging.getLogger(__name__)
MXFP8_BLOCK_SIZE = 32
# W4A8_MXFP block (group) size — fixed at 32 by the msmodelslim export format.
MXFP4_BLOCK_SIZE = 32
# NPU ops are reached via torch.ops.npu.* (registered when torch_npu is imported
# by the runtime), so this module needs no top-level `import torch_npu` and stays
# importable on CUDA/CPU/AMD/XPU CI.
def _get_float8_e8m0fnu_dtype():
# Resolve lazily rather than as a module-level constant: this module is
# imported early (during quant-scheme registration), so reading the dtype at
# call time keeps it correct regardless of import order / platform.
return getattr(torch, "float8_e8m0fnu", None)
def _get_float4_e2m1fn_x2_dtype():
# The packed-FP4 dtype MUST come from torch_npu (an int enum, e.g. 296), not
# from torch. The NPU ops that consume it -- npu_dynamic_mx_quant(dst_type=),
# npu_quant_matmul(x2_dtype=), npu_format_cast(input_dtype=) -- REJECT the
# torch dtype object torch.float4_e2m1fn_x2 in op-plugin on recent torch_npu
# builds (it raises, or with None gives "output y must be same shape as input
# x"), even though torch.float4_e2m1fn_x2 exists. This is fp4-specific: fp8 /
# float8_e8m0fnu is accepted from torch either way. Verified on A5 /
# torch_npu 2.10.0.post2.dev20260704 (see llm/probe_fp4_w4a8_chain.py: dst=296
# passes the full quant->format_cast->matmul chain, dst=torch dtype fails).
#
# Lazy import so this NPU-only path keeps the module importable on
# CUDA/CPU/AMD/XPU CI (no top-level torch_npu; see AGENTS.md known pitfalls).
from sglang.srt.utils import is_npu
if is_npu():
import torch_npu
npu_dtype = getattr(torch_npu, "float4_e2m1fn_x2", None)
if npu_dtype is not None:
return npu_dtype
return getattr(torch, "float4_e2m1fn_x2", None)
class _NPULinearMethodBase(LinearMethodBase):
def __init__(
self,
quant_config: Optional["QuantizationConfig"] = None,
):
self.quant_config = quant_config
class NPUW8A8Int8LinearMethod(_NPULinearMethodBase):
def process_weights_after_loading(self, layer: torch.nn.Module):
layer.weight.data = layer.weight.data.transpose(0, 1).contiguous()
layer.weight.data = npu_format_cast(layer.weight.data)
layer.weight_scale.data = layer.weight_scale.data.flatten()
# Compressed-tensors format doesn't have this field
if hasattr(layer, "weight_offset"):
layer.weight_offset.data = layer.weight_offset.data.flatten()
expanding_factor = layer.weight.data.shape[0]
layer.aclnn_input_scale = torch.nn.Parameter(
layer.input_scale.data.repeat(expanding_factor).to(device="npu"),
requires_grad=False,
)
layer.aclnn_input_scale_reciprocal = 1 / torch.nn.Parameter(
layer.input_scale.data.repeat(expanding_factor).to(device="npu"),
requires_grad=False,
)
layer.aclnn_input_offset = torch.nn.Parameter(
layer.input_offset.data.repeat(expanding_factor).to(device="npu"),
requires_grad=False,
)
def apply(
self,
layer: torch.nn.Module,
x: torch.Tensor,
bias: Optional[torch.Tensor] = None,
) -> torch.Tensor:
from sglang.srt.layers.linear import RowParallelLinear
original_dtype = x.dtype
if original_dtype != torch.int8:
x = torch.ops.npu.npu_quantize(
x,
layer.aclnn_input_scale_reciprocal,
layer.aclnn_input_offset,
torch.qint8,
-1,
False,
)
# Only fuse bias add into GEMM for rank 0 (this ensures that
# bias will not get added more than once in Attention TP>1 case)
if isinstance(layer, RowParallelLinear) and layer.tp_rank > 0:
quant_bias = None
else:
quant_bias = layer.quant_bias
return torch.ops.npu.npu_quant_matmul(
x,
layer.weight,
layer.deq_scale,
bias=quant_bias,
output_dtype=original_dtype,
)
class NPUW8A8Int8DynamicLinearMethod(_NPULinearMethodBase):
def process_weights_after_loading(self, layer: torch.nn.Module):
layer.weight.data = layer.weight.data.transpose(0, 1).contiguous()
layer.weight.data = npu_format_cast(layer.weight.data)
layer.weight_scale.data = layer.weight_scale.data.flatten()
# Compressed-tensors format doesn't have this field
if hasattr(layer, "weight_offset"):
layer.weight_offset.data = layer.weight_offset.data.flatten()
def apply(
self,
layer: torch.nn.Module,
x: torch.Tensor,
bias: Optional[torch.Tensor] = None,
) -> torch.Tensor:
if isinstance(x, tuple):
"""dynamic_scale is calculated in malprolog kernel"""
original_dtype = torch.bfloat16
quant_out, dynamic_scale = x
else:
original_dtype = x.dtype
quant_out, dynamic_scale = torch.ops.npu.npu_dynamic_quant(x)
return torch.ops.npu.npu_quant_matmul(
quant_out,
layer.weight,
layer.weight_scale,
pertoken_scale=dynamic_scale.flatten(),
bias=bias,
output_dtype=original_dtype,
)
class NPUMXFP8LinearMethod(_NPULinearMethodBase):
"""Ascend NPU MXFP8 linear method for LLM (SRT) models.
Shared kernel for both the online config path (``--quantization mxfp8``) and
the offline ModelSlimMXFP8Scheme (which delegates to this as ``self.kernel``).
process_weights_after_loading branches on weight dtype: FP16/BF16 weights are
quantised to MXFP8 at load time (online); pre-quantised float8_e4m3fn weights
are only re-laid-out (offline). 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 = layer.weight.data
if weight.dtype == torch.float8_e4m3fn:
# Offline (ModelSlim) path: weight is already MXFP8-quantised and
# layer.weight_scale holds the uint8 block scales [out, in/32]. Only
# re-layout to [in, out] / [in//64, out, 2] strided views below.
n_dim, k_dim = layer.weight_scale.data.shape
scale = layer.weight_scale.data.reshape(n_dim, k_dim // 2, 2)
layer.weight = Parameter(weight.transpose(0, 1), requires_grad=False)
layer.weight_scale_inv = Parameter(
scale.transpose(0, 1), requires_grad=False
)
# weight_scale is now folded into weight_scale_inv (which keeps the
# underlying storage alive via its view); drop the stale parameter so
# it doesn't linger in named_parameters() / state_dict().
del layer.weight_scale
else:
# Online path: quantise FP16/BF16 weights to MXFP8 at load time.
if weight.dtype not in (torch.float16, torch.bfloat16):
logger.warning(
"NPUMXFP8LinearMethod: weight dtype %s is not float16/bfloat16; "
"casting to bfloat16 before MXFP8 quantisation.",
weight.dtype,
)
weight = weight.to(torch.bfloat16)
# Move weight to NPU if needed (cpu offload may move it back to CPU).
if not weight.is_npu:
weight = weight.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.ops.npu.npu_dynamic_mx_quant(
weight, dst_type=torch.float8_e4m3fn
)
layer.weight = Parameter(qw.transpose(0, 1), requires_grad=False)
layer.weight_scale_inv = Parameter(
w_scale.transpose(0, 1), requires_grad=False
)
# Both paths produce weight [in, out] and weight_scale_inv [in//64, out,
# 2] as strided transpose views — DO NOT call .contiguous(). The matmul
# reduction loop scans the in-dim per output column; the [out, in]
# row-major source 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, making the inner-loop stride = out and
# tanking HBM bandwidth.
# 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.ops.npu.npu_dynamic_mx_quant(
x_2d, dst_type=torch.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)
e8m0_dtype = _get_float8_e8m0fnu_dtype()
output = torch.ops.npu.npu_quant_matmul(
qx,
layer.weight,
layer.weight_scale_inv,
scale_dtype=e8m0_dtype,
pertoken_scale=input_scale,
pertoken_scale_dtype=e8m0_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):
layer.weight.data = layer.weight.data.transpose(0, 1).contiguous()
layer.weight_scale.data = layer.weight_scale.data.flatten()
layer.weight_scale_fp32 = layer.weight_scale.data.to(torch.float32)
layer.weight_offset.data = layer.weight_offset.data.flatten()
layer.weight.data = torch.ops.npu.npu_convert_weight_to_int4pack(
layer.weight.data.to(torch.int32)
)
def apply(
self,
layer: torch.nn.Module,
x: torch.Tensor,
bias: Optional[torch.Tensor] = None,
tp_rank: Optional[int] = 0,
) -> torch.Tensor:
original_dtype = x.dtype
quant_out, dynamic_scale = torch.ops.npu.npu_dynamic_quant(
x, dst_type=torch.quint4x2
)
return torch.ops.npu.npu_quant_matmul(
quant_out,
layer.weight,
layer.weight_scale,
pertoken_scale=dynamic_scale.flatten(),
bias=bias,
output_dtype=original_dtype,
)
class NPUMXFP4W4A8LinearMethod(_NPULinearMethodBase):
"""Ascend NPU W4A8 online quantization: MXFP4 weights + MXFP8 activations.
This is a *true* W4(weight) A8(activation) path: it mirrors the offline
``W4A8_MXFP`` kernel (``NPUMXFP4W4A8OfflineLinearMethod``) exactly — the only
difference is that the FP4 weights are produced online from BF16/FP16
(round-to-nearest, no calibration) instead of being loaded from a msmodelslim
checkpoint. An earlier version of this method ran a *dual-level* scheme that
also compressed the activation to FP4 (W4A4 compute via
``npu_dual_level_quant_matmul``); that was a large accuracy regression — 4-bit
activations — so it was replaced with the single-level FP8-activation path
below, aligned with the offline W4A8 implementation.
Weight quantization (process_weights_after_loading):
BF16/FP16 weight → npu_dynamic_mx_quant(dst=float4_e2m1fn_x2) → packed FP4
+ UE8M0 block scale → npu_format_cast to FRACTAL_NZ → transpose [in//2, out]
Inference (apply):
BF16/FP16 activation → npu_dynamic_mx_quant(dst=float8_e4m3fn) (A8, FP8)
→ npu_quant_matmul(x2_dtype=float4_e2m1fn_x2, group_sizes=[0, 0, block])
Hardware: Ascend 950 (A5) + a recent torch_npu with the FP4 npu_quant_matmul
(same requirement as the offline W4A8 path — see that class's docstring).
"""
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,
):
"""Register an unquantized (``params_dtype``) weight placeholder.
Online quantization needs its own ``create_weights`` because the
checkpoint still holds full-precision BF16/FP16 weights: the loader
fills this buffer, then ``process_weights_after_loading`` quantizes it to
MXFP4 in place. This differs from the offline/int8 methods, whose weights
are created by the scheme's own ``create_weights`` to match the
already-quantized (FP8 / uint8-packed) layout the checkpoint provides.
"""
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 to MXFP4 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:
# Online single-level MXFP4 weight quant, then lay the weight out exactly
# like the offline W4A8 path so the same npu_quant_matmul(x2_dtype=fp4)
# kernel accepts it. All NPU ops go through torch.ops.npu.* (no torch_npu).
fp4_dtype = _get_float4_e2m1fn_x2_dtype()
weight_fp = layer.weight.data
if weight_fp.dtype not in (torch.float16, torch.bfloat16):
weight_fp = weight_fp.to(torch.bfloat16)
# Move to NPU if needed (cpu offload may have put it on CPU).
if not weight_fp.is_npu:
weight_fp = weight_fp.to(f"npu:{torch.npu.current_device()}")
# BF16 -> packed FP4 (float4_e2m1fn_x2, [out, in//2]) + UE8M0 block scale.
# npu_dynamic_mx_quant returns the scale as [out, in//64, 2] (3D); older
# builds may return [out, in//32] (2D) — handle both before the transpose.
qw, w_scale = torch.ops.npu.npu_dynamic_mx_quant(
weight_fp, dst_type=fp4_dtype, round_mode="round"
)
# weight: packed FP4 -> FRACTAL_NZ (float8_e4m3fn view) -> transpose
# [in//2, out]. Mirror the offline path (no .contiguous() on the NZ view);
# view as uint8 first because npu_format_cast only accepts int-dtype tensors.
qw_nz = npu_format_cast(
qw.view(torch.uint8),
NPUACLFormat.ACL_FORMAT_FRACTAL_NZ,
customize_dtype=torch.float8_e4m3fn,
input_dtype=fp4_dtype,
)
layer.weight = Parameter(qw_nz.transpose(-1, -2), requires_grad=False)
# weight_scale -> [in//64, out, 2] to match npu_quant_matmul.
if w_scale.dim() == 2:
n, k = w_scale.shape
w_scale = w_scale.reshape(n, k // 2, 2)
layer.weight_scale = Parameter(w_scale.transpose(-3, -2), 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:
e8m0_dtype = _get_float8_e8m0fnu_dtype()
fp4_dtype = _get_float4_e2m1fn_x2_dtype()
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 (A8 — FP8, not FP4).
quantized_x, dynamic_scale = torch.ops.npu.npu_dynamic_mx_quant(
x_2d, dst_type=torch.float8_e4m3fn
)
# 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)
# True W4(weight)A8(activation) matmul, identical to the offline path.
output = torch.ops.npu.npu_quant_matmul(
quantized_x,
layer.weight,
layer.weight_scale,
scale_dtype=e8m0_dtype,
pertoken_scale=dynamic_scale,
pertoken_scale_dtype=e8m0_dtype,
bias=quant_bias,
output_dtype=original_dtype,
x2_dtype=fp4_dtype,
group_sizes=[0, 0, MXFP4_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 NPUMXFP4W4A8OfflineLinearMethod(_NPULinearMethodBase):
"""Ascend NPU offline W4A8 (ModelSlim ``W4A8_MXFP``): packed-FP4 weights + MXFP8 activations.
Kernel for the offline ModelSlimMXFP4W4A8Scheme (delegated as ``self.kernel``).
The msmodelslim ``W4A8_MXFP`` checkpoint stores weights as *packed FP4*
(``pack_fp4_to_uint8`` → ``uint8`` shape ``[out, in//2]``) plus UE8M0 block
scales (``uint8`` shape ``[out, in//group_size]``):
process_weights_after_loading:
weight (uint8 packed FP4 [out, in//2]) → npu_format_cast(29,
customize_dtype=float8_e4m3fn, input_dtype=float4_e2m1fn_x2) → FRACTAL_NZ
→ transpose [in//2, out]
weight_scale [out, in/32] → reshape [out, in/64, 2] → transpose → [in/64, out, 2]
apply:
BF16/FP16 activation → npu_dynamic_mx_quant(dst=float8_e4m3fn) (A8, MXFP8)
→ npu_quant_matmul(x2_dtype=float4_e2m1fn_x2, group_sizes=[0, 0, block])
Mirrors vllm-ascend ``AscendW4A8MXFPDynamicLinearMethod`` exactly (Ascend 950/A5).
The weight is cast to FRACTAL_NZ then transposed; ``npu_dynamic_mx_quant`` already
returns a 3D ``[tokens, in//64, 2]`` block scale so the matmul needs no extra
scale-layout normalization.
⚠️ REQUIRES a recent torch_npu build for the FP4 ``npu_quant_matmul``. On the
A5 this device forces ``allow_internal_format=False`` (the NZ cast still produces
a ``FRACTAL_NZ_C0_16`` tensor, which is fine). Older torch_npu (e.g.
``2.10.0.dev20260320``) had a broken FP4 matmul that rejected the NZ weight in
*prefill* with ``x2 should be in ... nz format, but it is 2``;
``2.10.0.post1.dev20260624`` (and later) runs the vllm-aligned NZ path
correctly. If you hit ``it is 2``, update torch_npu — do NOT "fix" it by
switching the weight to ND.
⚠️ A ``atb::OperationSetup`` *segfault during decode* (not prefill) is a
DIFFERENT, unrelated issue: it is the eager-decode ``ascend`` attention
backend, NOT this matmul (verified by stage-sync bisection — qkv's matmul
syncs clean, the fault surfaces at the entry-sync of the next layer, i.e. the
decode attention between qkv and o_proj). Run with the NPU decode graph (do
NOT pass ``--disable-cuda-graph``); graph mode is the NPU default and what
vllm uses. This attention issue is model-agnostic and out of scope for W4A8.
This is a true W4(weight) A8(activation) single-level matmul. The *online*
``NPUMXFP4W4A8LinearMethod`` now uses this exact apply path — the only
difference is that it quantizes BF16/FP16 weights to FP4 at load time instead
of loading them from a msmodelslim checkpoint. ``group_size`` is fixed at 32
by the ``W4A8_MXFP`` export format.
"""
def process_weights_after_loading(self, layer: torch.nn.Module) -> None:
# Mirror vllm-ascend AscendW4A8MXFPDynamicLinearMethod: cast the packed-FP4
# weight to FRACTAL_NZ then transpose. All NPU ops go through
# torch.ops.npu.* (no torch_npu). Requires a recent torch_npu build (see
# class docstring): older builds reject the NZ weight ("x2 ... it is 2").
fp4_dtype = _get_float4_e2m1fn_x2_dtype()
# weight: packed-FP4 uint8 [out, in//2] -> FRACTAL_NZ (float8_e4m3fn view)
# -> transpose to [in//2, out].
layer.weight.data = npu_format_cast(
layer.weight.data,
NPUACLFormat.ACL_FORMAT_FRACTAL_NZ,
customize_dtype=torch.float8_e4m3fn,
input_dtype=fp4_dtype,
)
layer.weight.data = layer.weight.data.transpose(-1, -2)
# weight_scale: [out, in/32] uint8 -> [in/64, out, 2].
n, k = layer.weight_scale.data.shape
layer.weight_scale.data = layer.weight_scale.data.reshape(
n, k // 2, 2
).transpose(-3, -2)
def apply(
self,
layer: torch.nn.Module,
x: torch.Tensor,
bias: Optional[torch.Tensor] = None,
) -> torch.Tensor:
e8m0_dtype = _get_float8_e8m0fnu_dtype()
fp4_dtype = _get_float4_e2m1fn_x2_dtype()
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 (A8).
quantized_x, dynamic_scale = torch.ops.npu.npu_dynamic_mx_quant(
x_2d, dst_type=torch.float8_e4m3fn
)
if bias is not None and bias.dtype != torch.float32:
bias = bias.to(torch.float32)
# W4(weight)A8(activation) matmul, mirroring vllm-ascend exactly.
output = torch.ops.npu.npu_quant_matmul(
quantized_x,
layer.weight,
layer.weight_scale,
scale_dtype=e8m0_dtype,
pertoken_scale=dynamic_scale,
pertoken_scale_dtype=e8m0_dtype,
bias=bias,
output_dtype=original_dtype,
x2_dtype=fp4_dtype,
group_sizes=[0, 0, MXFP4_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)