[llm][npu][quant] Add W4A4 MXFP4 quantization support for Qwen3 Dense on Ascend NPU (#23795)

This commit is contained in:
Junlin Wu
2026-07-17 09:06:30 +03:00
committed by GitHub
parent 1ac1ffea0c
commit bbd2a3fe4a
8 changed files with 553 additions and 2 deletions
@@ -625,3 +625,278 @@ class NPUMXFP4W4A8OfflineLinearMethod(_NPULinearMethodBase):
# Restore original shape (replace last dim with output features).
output_shape = list(input_shape[:-1]) + [output.shape[-1]]
return output.reshape(output_shape)
class NPUSingleLevelMXFP4LinearMethod(_NPULinearMethodBase):
"""Ascend NPU W4A4 online quantization: single-level MXFP4.
True W4(weight) A4(activation): both weights and activations are quantised to
single-level MXFP4 (``float4_e2m1fn_x2``), unlike the W4A8 path which keeps FP8
activations. All NPU ops go through ``torch.ops.npu.*`` (no top-level
``torch_npu``) and the fp4 dtype comes from ``_get_float4_e2m1fn_x2_dtype()``.
Weight quantization (process_weights_after_loading):
BF16/FP16 weight → npu_dynamic_mx_quant(dst=float4_e2m1fn_x2)
→ (packed FP4 [out, in//2], UE8M0 block scale) → transpose [in//2, out]
Inference (apply):
BF16/FP16 activation → npu_dynamic_mx_quant(dst=float4_e2m1fn_x2) (A4)
→ npu_quant_matmul(x1_dtype = x2_dtype = float4_e2m1fn_x2,
group_sizes=[1, 1, MXFP4_BLOCK_SIZE])
Triggered by ``--quantization mxfp4`` on Ascend NPU. Hardware: Ascend 950 (A5)
with a recent torch_npu exposing ``float4_e2m1fn_x2``.
"""
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.
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.
"""
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. All NPU ops go through
# torch.ops.npu.* (no torch_npu); the fp4 dtype comes from the shared
# _get_float4_e2m1fn_x2_dtype() helper (the torch_npu int enum).
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.
qw, w_scale = torch.ops.npu.npu_dynamic_mx_quant(
weight_fp, dst_type=fp4_dtype, round_mode="round"
)
# Pre-transpose the weight to [in//2, out] for npu_quant_matmul; use
# .data= to preserve the non-contiguous transpose view (npu_quant_matmul
# reads strides directly — .contiguous() would reorder data and break
# block-scale alignment).
layer.weight = Parameter(qw, requires_grad=False)
layer.weight.data = layer.weight.data.transpose(0, 1)
# weight_scale -> [in//64, out, 2] (3D), matching the offline W4A4 path,
# the W4A8 path and vllm-ascend's W4A4_MXFP4 layout. npu_dynamic_mx_quant
# already returns the scale as [out, in//64, 2] (3D) on current builds;
# older builds may return [out, in//32] (2D) — reshape those first so the
# transpose always yields the 3D layout npu_quant_matmul requires.
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)
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 single-level MXFP4 activation quantisation (A4 — FP4).
qx, input_scale = torch.ops.npu.npu_dynamic_mx_quant(
x_2d, dst_type=fp4_dtype, round_mode="round"
)
# Single-level MXFP4 matmul (weight & scale already transposed at load
# time): x1_dtype = x2_dtype = fp4, group_sizes=[1, 1, block].
output = torch.ops.npu.npu_quant_matmul(
qx,
layer.weight,
layer.weight_scale,
scale_dtype=e8m0_dtype,
pertoken_scale=input_scale,
pertoken_scale_dtype=e8m0_dtype,
bias=bias.to(torch.float32) if bias is not None else None,
output_dtype=original_dtype,
x1_dtype=fp4_dtype,
x2_dtype=fp4_dtype,
group_sizes=[1, 1, 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 NPUSingleLevelMXFP4OfflineLinearMethod(NPUSingleLevelMXFP4LinearMethod):
"""Ascend NPU offline W4A4 (ModelSlim ``W4A4_MXFP4``): fp8-container FP4 weights.
Kernel for the offline ``ModelSlimMXFP4Scheme`` (delegated as ``self.kernel``).
The msmodelslim ``W4A4_MXFP4`` checkpoint stores weights as **fp4-in-fp8
container** (``float8_e4m3fn`` [out, in], one FP4 value per byte) plus UE8M0
block scales (``uint8`` [out, in//32]). The weight is re-packed to
``float4_e2m1fn_x2`` (two FP4 per byte) and the scale reshaped to 3D; it then
shares the online :class:`NPUSingleLevelMXFP4LinearMethod` matmul (``apply``)
exactly — only the weight source differs (msmodelslim checkpoint vs online RTN).
Mirrors vllm-ascend's single-level W4A4 MXFP4 layout.
"""
def process_weights_after_loading(self, layer: torch.nn.Module) -> None:
# Re-pack fp8-container FP4 to float4_e2m1fn_x2 and pre-transpose to match
# the online path's layout. All NPU ops go through torch.ops.npu.* (no
# torch_npu); the fp4 dtype must be the torch_npu enum (helper).
fp4_dtype = _get_float4_e2m1fn_x2_dtype()
weight = layer.weight.data
if not weight.is_npu:
weight = weight.to(f"npu:{torch.npu.current_device()}")
# fp8 container -> float4_e2m1fn_x2 (2 FP4 per byte): [out, in] -> [out, in//2].
weight_fp4 = torch.ops.npu.npu_dtype_cast(weight, fp4_dtype)
# Transpose to [in//2, out]; no .contiguous() (preserve the strided view so
# the block-scale mapping stays intact).
layer.weight = Parameter(weight_fp4.transpose(0, 1), requires_grad=False)
weight_scale = layer.weight_scale.data
if not weight_scale.is_npu:
weight_scale = weight_scale.to(f"npu:{torch.npu.current_device()}")
# npu_quant_matmul with float4_e2m1fn_x2 requires x2Scale to be 3D:
# [out, in/32] -> [out, in/64, 2] -> transpose to [in/64, out, 2].
n_dim, k_dim = weight_scale.shape
layer.weight_scale = Parameter(
weight_scale.reshape(n_dim, k_dim // 2, 2).transpose(0, 1),
requires_grad=False,
)
class NPUDualLevelMXFP4LinearMethod(NPUSingleLevelMXFP4LinearMethod):
"""Ascend NPU W4A4 online quantization: dual-level MXFP4 (higher accuracy).
This is the sole online ``--quantization mxfp4`` linear path. Instead of a single
UE8M0 (power-of-2) block scale, dual-level MX quant produces a finer L0 (FP8 E4M3)
block scale plus a coarser L1 scale, so per-block dynamic range is captured far
more accurately — this fixed the online-RTN degradation that made single-level
decoding loop (never emitting EOS) under greedy sampling. (The single-level
:class:`NPUSingleLevelMXFP4LinearMethod` is retained only as the offline path's
base — msmodelslim checkpoints ship single-level UE8M0 scales.)
All NPU ops go through ``torch.ops.npu.*`` (no top-level ``torch_npu``). Only
``create_weights`` (the BF16/FP16 placeholder) is shared with the single-level
base; weight post-processing and the matmul are fully dual-level.
Weight quantization (process_weights_after_loading):
BF16/FP16 weight → npu_dynamic_dual_level_mx_quant
→ (packed FP4 weight, L0 scale, L1 scale); weight cast to FRACTAL_NZ,
L0 scale transposed to [in//l0_block, out].
Inference (apply):
BF16/FP16 activation → npu_dynamic_dual_level_mx_quant (A4, dual-level)
→ npu_dual_level_quant_matmul(act, weight, act_l0, w_l0, act_l1, w_l1)
Reference: Diffusion ``NPUMXFP4DiffusionLinearMethod`` / MindIE-SD
``W4A4MXFP4DualQuantLinear``. Hardware: Ascend 950 (A5) only — the
``DualLevelQuantBatchMatmul`` op is unavailable on A2/A3.
"""
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):
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()}")
# Dual-level MXFP4 weight quant: packed FP4 weight + L0 (fine, FP8 E4M3)
# and L1 (coarse) block scales.
qw, w_l0_scale, w_l1_scale = torch.ops.npu.npu_dynamic_dual_level_mx_quant(
weight_fp, smooth_scale=None
)
# npu_dual_level_quant_matmul requires the weight (x2) in FRACTAL_NZ.
# View the packed FP4 as int8 first (npu_format_cast takes int dtypes).
qw_nz = npu_format_cast(
qw.view(torch.int8),
NPUACLFormat.ACL_FORMAT_FRACTAL_NZ,
customize_dtype=torch.int8,
)
# L0 scale -> [in//l0_block, out] (op returns [out, in//l0_block, 1]).
w_l0_scale = w_l0_scale.squeeze(-1).transpose(0, 1).contiguous()
layer.weight = Parameter(qw_nz, requires_grad=False)
layer.weight_l0_scale = Parameter(w_l0_scale, requires_grad=False)
layer.weight_l1_scale = Parameter(w_l1_scale, requires_grad=False)
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 the quant operators.
input_shape = x.shape
x_2d = x.reshape(-1, x.shape[-1])
# Dynamic dual-level MXFP4 activation quant (A4): packed FP4 + L0/L1 scales.
qx, act_l0_scale, act_l1_scale = torch.ops.npu.npu_dynamic_dual_level_mx_quant(
x_2d, smooth_scale=None
)
# Dual-level matmul. Arg order (act, weight, act_l0, w_l0, act_l1, w_l1);
# the weight is NOT transposed here (unlike the single-level path).
output = torch.ops.npu.npu_dual_level_quant_matmul(
qx,
layer.weight,
act_l0_scale,
layer.weight_l0_scale,
act_l1_scale,
layer.weight_l1_scale,
bias=bias.to(torch.float32) if bias is not None else None,
output_dtype=original_dtype,
)
# Restore original shape (replace last dim with output features).
output_shape = list(input_shape[:-1]) + [output.shape[-1]]
return output.reshape(output_shape)
@@ -46,6 +46,7 @@ from sglang.srt.layers.quantization.modelslim.modelslim import ModelSlimConfig
from sglang.srt.layers.quantization.moe_wna16 import MoeWNA16Config
from sglang.srt.layers.quantization.mxfp4 import Mxfp4Config
from sglang.srt.layers.quantization.npu_mxfp4 import Mxfp4W4A8Config
from sglang.srt.layers.quantization.npu_mxfp4_w4a4 import Mxfp4W4A4Config
from sglang.srt.layers.quantization.nvfp4_online import NvFp4OnlineConfig
from sglang.srt.layers.quantization.petit import PetitNvFp4Config
from sglang.srt.layers.quantization.qoq import QoQConfig
@@ -117,6 +118,10 @@ if is_npu():
BASE_QUANTIZATION_METHODS.update(
{
"gptq": GPTQAscendConfig,
# On NPU, `mxfp4` means single-level W4A4 MXFP4 for dense LLM (the
# upstream `Mxfp4Config` OCP-MoE path is only registered on
# cpu/cuda/hip above, so there is no collision here).
"mxfp4": Mxfp4W4A4Config,
}
)
@@ -16,6 +16,7 @@ from sglang.srt.layers.quantization.base_config import (
QuantizationConfig,
)
from sglang.srt.layers.quantization.modelslim.schemes import (
ModelSlimMXFP4Scheme,
ModelSlimMXFP4W4A8Scheme,
ModelSlimMXFP8Scheme,
ModelSlimW4A4Int4,
@@ -208,6 +209,7 @@ class ModelSlimConfig(QuantizationConfig):
("W8A8_DYNAMIC", ModelSlimW8A8Int8),
("W8A8_MXFP8", ModelSlimMXFP8Scheme),
("W4A8_MXFP", ModelSlimMXFP4W4A8Scheme),
("W4A4_MXFP4", ModelSlimMXFP4Scheme),
]
quant_schemes = [self.quant_description.get(prefix + ".weight", "")]
@@ -7,6 +7,7 @@
from .modelslim_scheme import ModelSlimLinearScheme, ModelSlimMoEScheme
from .modelslim_mxfp8 import ModelSlimMXFP8Scheme
from .modelslim_mxfp4_w4a8 import ModelSlimMXFP4W4A8Scheme
from .modelslim_mxfp4 import ModelSlimMXFP4Scheme
# isort: on
from .modelslim_w4a4_int4 import ModelSlimW4A4Int4
@@ -20,6 +21,7 @@ __all__ = [
"ModelSlimMoEScheme",
"ModelSlimMXFP8Scheme",
"ModelSlimMXFP4W4A8Scheme",
"ModelSlimMXFP4Scheme",
"ModelSlimW8A8Int8",
"ModelSlimW4A4Int4",
"ModelSlimW4A4Int4MoE",
@@ -0,0 +1,96 @@
"""ModelSlim W4A4_MXFP4 scheme for pre-quantized weight inference on Ascend NPU (SRT).
The msmodelslim ``W4A4_MXFP4`` checkpoint stores weights in an **fp8 container**:
weight: float8_e4m3fn shape [out, in] (one FP4 value per byte)
weight_scale: uint8 (UE8M0) shape [out, in//32] (block scales, group_size=32)
This is a true W4(weight) A4(activation) scheme: both weights and activations are
single-level MXFP4. Weight post-processing and the matmul are delegated to
``NPUSingleLevelMXFP4OfflineLinearMethod`` (``self.kernel``): the fp8-container FP4
is re-packed to ``float4_e2m1fn_x2`` (two FP4 per byte) + transposed and the scale
reshaped to 3D, then ``npu_quant_matmul`` runs with ``x1_dtype = x2_dtype =
float4_e2m1fn_x2`` and ``group_sizes=[1, 1, 32]`` — sharing the online
``NPUSingleLevelMXFP4LinearMethod`` matmul exactly (only the weight source differs).
This differs from ``W4A8_MXFP`` (packed-uint8 FP4 weights + FP8 activations) and
from ``W8A8_MXFP8`` (float8_e4m3fn weights of shape [out, in]).
"""
from typing import Dict, List, Optional
import torch
from sglang.srt.hardware_backend.npu.quantization.linear_method_npu import (
NPUSingleLevelMXFP4OfflineLinearMethod,
)
from sglang.srt.layers.parameter import GroupQuantScaleParameter, ModelWeightParameter
from sglang.srt.layers.quantization.modelslim.schemes import ModelSlimLinearScheme
# Fixed by the msmodelslim W4A4_MXFP4 export format (group_size=32).
MXFP4_BLOCK_SIZE = 32
class ModelSlimMXFP4Scheme(ModelSlimLinearScheme):
"""W4A4_MXFP4 offline scheme — fp8-container FP4 weights, MXFP4 activations."""
def __init__(
self,
quant_config: Optional[Dict[str, any]] = None,
prefix: Optional[str] = None,
):
# quant_config / prefix accepted to match ModelSlimConfig.get_linear_scheme's
# dispatch signature; W4A4_MXFP4 needs no per-layer config beyond create_weights.
del quant_config, prefix
self.kernel = NPUSingleLevelMXFP4OfflineLinearMethod()
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] — one FP4
# value per byte (fp8 container). The kernel re-packs it to
# float4_e2m1fn_x2 (2 FP4 per byte) in process_weights_after_loading.
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)
# UE8M0 block scales: uint8, shape [out, in//32].
scale_dim = input_size_per_partition // MXFP4_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):
self.kernel.process_weights_after_loading(layer)
def apply_weights(
self,
layer: torch.nn.Module,
x: torch.Tensor,
bias: Optional[torch.Tensor] = None,
) -> torch.Tensor:
return self.kernel.apply(layer, x, bias)
@@ -0,0 +1,140 @@
"""MXFP4 W4A4 online quantization config (dual-level MXFP4 weights + activations).
Triggered by ``--quantization mxfp4`` on the Ascend NPU backend. On CUDA / AMD /
CPU the ``mxfp4`` key resolves to the upstream :class:`Mxfp4Config` (OCP MXFP4
MoE) instead; the per-device split is done at registration time in
``sglang.srt.layers.quantization.__init__`` (this config is only registered
inside the ``is_npu()`` block, mirroring ``GPTQAscendConfig``).
Online mode: FP16/BF16 weights are quantised to **dual-level** MXFP4 in
``process_weights_after_loading`` (a finer FP8 E4M3 L0 block scale plus a coarser
L1 scale); activations are dynamically quantised the same way and the matmul runs
via ``npu_dual_level_quant_matmul`` (see :class:`NPUDualLevelMXFP4LinearMethod`).
Dual-level is the sole online path — it captures per-block dynamic range far more
accurately than a single-level UE8M0 scale, avoiding the RTN degradation that made
single-level online decoding loop under greedy sampling. Requires Ascend 950 (A5).
Offline (msmodelslim ``W4A4_MXFP4``) checkpoints are single-level (the checkpoint
stores UE8M0 scales) and are handled separately by the ``modelslim`` config
(``ModelSlimMXFP4Scheme`` → ``NPUSingleLevelMXFP4OfflineLinearMethod``), not this
class.
"""
from __future__ import annotations
import logging
from typing import Dict, List, Optional
import torch
from sglang.srt.layers.quantization.base_config import (
QuantizationConfig,
QuantizeMethodBase,
)
from sglang.srt.layers.quantization.unquant import (
UnquantizedFusedMoEMethod,
UnquantizedLinearMethod,
)
from sglang.srt.layers.quantization.utils import is_layer_skipped
from sglang.srt.utils import is_npu
logger = logging.getLogger(__name__)
class Mxfp4W4A4Config(QuantizationConfig):
"""Single-level MXFP4 W4A4 online quantization config for Ascend NPU.
True W4(weight) A4(activation): both weights and activations are quantised
to single-level MXFP4 (``float4_e2m1fn_x2``). The device-specific linear
method is selected in ``get_quant_method``; only Ascend NPU is wired up
today (on other devices ``mxfp4`` maps to the upstream ``Mxfp4Config``).
"""
def __init__(
self,
ignored_layers: Optional[List[str]] = None,
packed_modules_mapping: Optional[Dict[str, str]] = None,
):
super().__init__()
self.ignored_layers = ignored_layers or []
self.packed_modules_mapping = packed_modules_mapping or {}
@classmethod
def get_name(cls) -> str:
return "mxfp4"
@classmethod
def get_supported_act_dtypes(cls) -> List[torch.dtype]:
return [torch.bfloat16, torch.half]
@classmethod
def get_min_capability(cls) -> int:
return 0 # NPU bypasses CUDA capability checks
@classmethod
def get_config_filenames(cls) -> List[str]:
return []
@classmethod
def from_config(cls, config: Dict) -> Mxfp4W4A4Config:
ignored_layers = cls.get_from_keys_or(
config, ["ignored_layers", "modules_to_not_convert"], None
)
if ignored_layers:
normalized: List[str] = []
for layer in ignored_layers:
base = layer.removeprefix("model.")
normalized.append(base)
normalized.append(f"model.{base}")
ignored_layers = normalized
packed_modules_mapping = (
cls.get_from_keys_or(config, ["packed_modules_mapping"], {}) or {}
)
return cls(
ignored_layers=ignored_layers,
packed_modules_mapping=packed_modules_mapping,
)
def get_quant_method(
self, layer: torch.nn.Module, prefix: str
) -> Optional[QuantizeMethodBase]:
from sglang.srt.layers.linear import LinearBase
from sglang.srt.layers.moe.fused_moe_triton import FusedMoE
if isinstance(layer, LinearBase):
if is_layer_skipped(
prefix,
self.ignored_layers,
fused_mapping=self.packed_modules_mapping,
):
return UnquantizedLinearMethod()
if is_npu():
from sglang.srt.hardware_backend.npu.quantization.linear_method_npu import (
NPUDualLevelMXFP4LinearMethod,
)
# Online W4A4 always uses dual-level MXFP4 (finer FP8 L0 scales):
# single-level RTN was too lossy and degenerated under greedy
# decoding. Requires Ascend 950 (A5). The single-level kernel is
# retained only for the offline msmodelslim path.
return NPUDualLevelMXFP4LinearMethod(self)
raise NotImplementedError(
"mxfp4 W4A4 (single-level MXFP4 weights + activations) is currently "
"only implemented for the Ascend NPU backend; no CUDA/other-device "
"kernel exists in this config. Add a device branch here when one lands."
)
elif isinstance(layer, FusedMoE):
# MoE single-level MXFP4 W4A4 not yet implemented; fall back to unquantised
logger.warning(
"MXFP4 W4A4 quantization is not yet supported for FusedMoE layers "
"(prefix=%s). Falling back to unquantized MoE — MoE weights will "
"run in full precision (BF16/FP16).",
prefix,
)
return UnquantizedFusedMoEMethod(
layer.use_triton_kernels, layer.use_flashinfer_trtllm_moe
)
return None
def get_scaled_act_names(self) -> List[str]:
return []