Prhub

#23650 :sparkles: [llm][npu][quant] Add W4A8 MXFP quantization support for Qwen3 Dense on Ascend NPU

原始 PR 作者 TallMessiWu 合并时间 2026-07-07 00:23 文件变更 10 提交数 6 评论 35 代码增减 +613 / -5

执行摘要

为 Ascend NPU Qwen3 Dense 添加 W4A8 MXFP 量化和离线方案

作为 Issue #21584(Ascend NPU A5 MXFP8/MXFP4 量化)的一部分,旨在为 Qwen3 Dense 模型提供更激进的权重量化(4-bit)方案,以降低内存带宽压力并提升推理吞吐量。PR 依赖前置 W8A8 实现(#22352),并复用了相同的 NPU 量化基础设施。

该 PR 是 Ascend NPU 量化路线中的重要里程碑,值得所有 NPU 用户和关注硬件后端量化的开发者精读。重点关注:

  • 在线与离线量化路径的设计区别及复用模式。
  • npu_format_cast 的扩展方式(FP4 参数传递与 ND 回退跳过逻辑)。
  • 性能测试中 .contiguous() vs strided view 的实测决策,体现了硬件相关的优化取舍。
  • 建议后续作者或团队补充 E2E 测试用例,并跟踪统一基类的重构进展。
讨论亮点
  1. 复用 npu_format_cast 而非直接调用 torch_npu 原语:ping1jing2 建议重用现有工具函数,作者扩展了 npu_format_cast 以支持 FP4 unpack 参数,并在所有 W4A8 路径中统一使用,避免了代码散布和手动格式转换。
  2. 在线配置是否需要独立文件:OrangeRedeng 询问是否可将在线逻辑合并到 modelslim.py 中以减少重复。作者解释在线(--quantization mxfp_w4a8)和离线(modelslim)是两套注册机制,各自独立,不宜合并。reviewer 提出后续可用统一基类(NPUUnquantLinearMethod)减少代码重复。
  3. .contiguous() 的使用策略:AI 审查建议在离线路径预转置权重并调用 .contiguous(),但作者通过实测发现 strided view 在 NPU 上反而更慢,因此仅在线路径使用 .contiguous()(安全),离线路径保持非连续视图以保持预量化数据的块尺度映射正确。这一讨论体现了性能测量驱动决策的原则。
  4. FP4 dtype 的来源:作者识别出 torch.float4_e2m1fn_x2torch_npu.float4_e2m1fn_x2 在不同版本下的兼容性问题,采用惰性解析 + fallback 机制 (_get_float4_e2m1fn_x2_dtype),避免对 torch_npu 版本的硬依赖。
  5. 缺少测试文件:虽未构成明确讨论,但发现 PR 未包含对应的端到端测试注释或回归测试文件,评审中未质疑但可能造成潜在覆盖风险。

实现拆解

  1. 在线量化配置与注册:新增 python/sglang/srt/layers/quantization/npu_mxfp4.py,定义 Mxfp4W4A8Config(注册为 "mxfp_w4a8"),通过 get_quant_method 为 Linear 层分发 NPU 后端线性方法 NPUMXFP4W4A8LinearMethod,并跳过 MoE 层(fallback 到未量化)。

  2. 在线核心逻辑:在 python/sglang/srt/hardware_backend/npu/quantization/linear_method_npu.py 中新增 NPUMXFP4W4A8LinearMethodprocess_weights_after_loading 调用 npu_dynamic_mx_quant(dst=float4_e2m1fn_x2) 将 BF16 权重量化为 packed FP4,再通过 npu_format_cast 转 FRACTAL_NZ 格式。apply 中激活动态量化为 MXFP8 后执行 npu_quant_matmul(x2_dtype=float4_e2m1fn_x2, group_sizes=[0,0,32])

  3. 离线量化 Scheme:新增 python/sglang/srt/layers/quantization/modelslim/schemes/modelslim_mxfp4_w4a8.py,定义 ModelSlimMXFP4W4A8Schemecreate_weights 创建 uint8 packed FP4 权重和 UE8M0 scale 参数;process_weights_after_loadingapply 委托给 NPUMXFP4W4A8OfflineLinearMethod(与在线方法共享推理内核,但权重来源不同)。

  4. 基础设施增强:扩展 python/sglang/srt/hardware_backend/npu/utils.py 中的 npu_format_cast,新增 customize_dtype/input_dtype 参数以支持 packed FP4 的 unpack 转换,并跳过 FP4 场景的 ND fallback 检查(防止对齐错误导致结果损坏)。

  5. 注册与 CLI:在 layers/quantization/__init__.py 中注册 Mxfp4W4A8Config,在 layers/quantization/modelslim/schemes/__init__.py 中导出 ModelSlimMXFP4W4A8Scheme,并在 server_args.pyQUANTIZATION_CHOICES 中添加 "mxfp_w4a8"

  6. 文档:更新 docs_new/docs/advanced_features/quantization.mdxascend_npu_quantization.mdx,添加 W4A8 使用说明。

文件 模块 状态 重要度
python/sglang/srt/hardware_backend/npu/quantization/linear_method_npu.py 量化层 modified 8.84
python/sglang/srt/layers/quantization/npu_mxfp4.py 量化配置 added 8.66
python/sglang/srt/layers/quantization/modelslim/schemes/modelslim_mxfp4_w4a8.py 量化配置 added 8.89
python/sglang/srt/hardware_backend/npu/utils.py 工具函数 modified 6.25
python/sglang/srt/layers/quantization/__init__.py 注册入口 modified 4.66
python/sglang/srt/layers/quantization/modelslim/schemes/__init__.py 注册入口 modified 4.7
python/sglang/srt/layers/quantization/modelslim/modelslim.py 量化配置 modified 4.93
python/sglang/srt/server_args.py 服务参数 modified 4.18
docs_new/docs/hardware-platforms/ascend-npus/ascend_npu_quantization.mdx 文档 modified 2.95
docs_new/docs/advanced_features/quantization.mdx 文档 modified 2.49

关键符号

_get_float4_e2m1fn_x2_dtype NPUMXFP4W4A8LinearMethod.create_weights NPUMXFP4W4A8LinearMethod.process_weights_after_loading NPUMXFP4W4A8LinearMethod.apply NPUMXFP4W4A8OfflineLinearMethod.process_weights_after_loading NPUMXFP4W4A8OfflineLinearMethod.apply Mxfp4W4A8Config.get_quant_method ModelSlimMXFP4W4A8Scheme.create_weights npu_format_cast (enhanced)

关键源码片段

python/sglang/srt/hardware_backend/npu/quantization/linear_method_npu.py core-logic

核心实现文件,新增 `NPUMXFP4W4A8LinearMethod` 和 `NPUMXFP4W4A8OfflineLinearMethod`,包含在线权重量化和推理全流程,是最关键的变更。

# python/sglang/srt/hardware_backend/npu/quantization/linear_method_npu.py
# 新增的在线 W4A8 线性方法(NPUMXFP4W4A8LinearMethod)
# 在 process_weights_after_loading 中将 BF16 权重在线量化为 packed FP4,
# 在 apply 中动态量化激活为 MXFP8 并调用 FP4 matmul。def _get_float4_e2m1fn_x2_dtype():
    # FP4 dtype 必须从 torch_npu 取(int enum,如 296),
    # torch.float4_e2m1fn_x2 对象在 op-plugin 中会导致 "output y must be
    # same shape as input x" 错误。这里惰性加载并 fallback,
    # 确保模块在非 NPU 环境可导入。
    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 NPUMXFP4W4A8LinearMethod(_NPULinearMethodBase):
    """Ascend NPU W4A8 在线量化:MXFP4 权重 + MXFP8 激活。"""
​
    def create_weights(self, layer, ...):
        # 创建 BF16 权重缓冲区(完整精度),供后续在线量化。
        # 与离线方案不同,这里不是预量化 buffer。
        weight = Parameter(torch.empty(output_size, input_size, dtype=params_dtype),
                           requires_grad=False)
        layer.register_parameter("weight", weight)
​
    def process_weights_after_loading(self, layer):
        # 在线量化权重:BF16 → packed FP4 + UE8M0 scale
        weight = layer.weight.data
        # 调用 NPU 原生双级量化 API(MXFP4:FP4 + L0 FP32 scale + L1 FP8_E8M0 scale)
        quant_weight, l0_scale, l1_scale = \
            torch.ops.npu.npu_dynamic_mx_quant(weight, dst_type=_get_float4_e2m1fn_x2_dtype())
        # 转 FRACTAL_NZ 格式(NPU 高效计算所需)
        quant_weight = npu_format_cast(quant_weight.view(torch.int8), acl_format=29,
                                       customize_dtype=torch.float8_e4m3fn,
                                       input_dtype=_get_float4_e2m1fn_x2_dtype())
        # 转置并存储
        layer.weight = Parameter(quant_weight.transpose(0, 1).contiguous(), requires_grad=False)
        # 存储双级 scale(处理转置和 contiguous)
        layer.weight_scale_0 = Parameter(l0_scale.squeeze().transpose(0, 1).contiguous(),
                                         requires_grad=False)
        layer.weight_scale_1 = Parameter(...) # 类似处理
​
    def apply(self, layer, x, bias):
        original_dtype = x.dtype
        # 动态量化激活为 MXFP8(FP8 + UE8M0 scale)
        qx, qscale = torch.ops.npu.npu_dynamic_mx_quant(x, dst_type=torch.float8_e4m3fn)
        # 执行 W4A8 matmul(实际是 W4A4 compute 但 A8 指 scale 精度)
        out = torch.ops.npu.npu_quant_matmul(
            qx, layer.weight, layer.weight_scale_0, layer.weight_scale_1,
            pertoken_scale=qscale,
            bias=bias,
            output_dtype=original_dtype,
            x2_dtype=_get_float4_e2m1fn_x2_dtype(),
            group_sizes=[0, 0, 32] # 块大小 32
        )
        return out
​
​
class NPUMXFP4W4A8OfflineLinearMethod(_NPULinearMethodBase):
    """离线 W4A8 核:加载预量化 packed FP4 权重,推理时激活动态 MXFP8。"""
​
    def process_weights_after_loading(self, layer):
        # 预量化权重已是 packed FP4(uint8, shape [out, in//2]),
        # 转换为 FRACTAL_NZ 并加载双级 scale。注意不做 .contiguous(),
        # 因为预量化数据的块尺度映射依赖于非连续视图。
        w = layer.weight.data # uint8
        # 转 NZ 格式(通过 npu_format_cast 带 unpack 参数)
        w_nz = npu_format_cast(w, customize_dtype=torch.float8_e4m3fn,
                               input_dtype=_get_float4_e2m1fn_x2_dtype())
        # 转置但不 contiguous(关键优化:stride view 在 NPU matmul 中更快)
        layer.weight.data = w_nz.transpose(0, 1).data
        # 类似的 scale 转换,均使用 .data 赋值保持非连续
        ...
​
    def apply(self, layer, x, bias):
        # 与在线方法的 apply 完全一致:激活动态 MXFP8,matmul 调用相同 API。
        ...

评论区精华

复用 `npu_format_cast` 而非直接调用 torch_npu 原语 设计

ping1jing2 建议使用已存在的 `npu_format_cast`,避免直接调用底层 NPU op。最初代码中在线路径直接调用了 `npu_format_cast`,但离线路径使用了原始 API。TallMessiWu 随后扩展了 `npu_format_cast` 以接受 FP4 unpack 参数,并将所有路径统一为其上层调用,消除了重复且确保了 NZ 格式化的一致性。

结论:采用统一入口 `npu_format_cast` 并新增 FP4 参数,所有 W4A8 路径均通过它转换权重格式。 · 已解决

是否需要单独在线配置文件 vs 合并到 modelslim.py 设计

OrangeRedeng 询问 `npu_mxfp4.py` 可否并入 `modelslim.py` 以减少重复。TallMessiWu 解释在线配置(`Mxfp4W4A8Config`)通过 `--quantization mxfp_w4a8` 触发,而离线路径通过 modelslim 自动检测,两套注册机制不同且均遵循现有模式(如 `Fp8Config` 独立文件)。讨论中 OrangeRedeng 进一步提出长远统一基类的建议。

结论:保持独立文件,因为这是在线 vs 离线两种不同的注册 / 触发机制;后续重构可考虑统一基类减少重复。 · 已解决

`.contiguous()` 的使用策略(在线 vs 离线) 性能

AI 审查(gemini-code-assist)建议在离线路径预转置权重并调用 `.contiguous()` 以提升推理性能。TallMessiWu 回应称在 NPU 上实测 strided view 反而更慢,因此在线路径(权重在线量化后重新分配)使用 `.contiguous()` 是安全的;离线路径则使用 `.data` 赋值保留非连续视图,因为 `npu_quant_matmul` 能正确读取 strides,而 `.contiguous()` 会破坏预量化数据的块尺度映射。

结论:保留两套做法:在线 `.contiguous()` 安全,离线不 `.contiguous()` 以匹配预量化数据布局。这纠正了“预转置 + contiguous 总是更快”的通用假设。 · 已解决

FP4 dtype 的惰性解析与版本兼容 正确性

TallMessiWu 在提交消息和代码注释中解释了 `torch.float4_e2m1fn_x2` 与 `torch_npu.float4_e2m1fn_x2` 的兼容问题:某些 torch_npu 构建拒绝 torch 端的 dtype 对象,必须使用 NPU 原生的 int enum。因此引入 `_get_float4_e2m1fn_x2_dtype` 并在 NPU 环境下尝试从 torch_npu 获取,否则 fallback 到 torch。此机制避免了模块在非 NPU CI 下崩溃。

结论:实现惰性解析 + 双来源 fallback,确保 FP4 dtype 始终正确且模块可跨后端导入。 · 已解决

缺少 E2E 测试文件 测试

PR 中未包含对应的测试文件(如 test_npu_w4a8_accuracy.py)。审查中未明确要求添加测试,但根据仓库规范,新量化方案通常应附带 CI 可执行的测试用例。性能数据由作者手动提供。

结论:未在本次 PR 中添加测试,建议后续补充 NPU-specific E2E 回归测试。 · unresolved

风险与影响

  1. torch_npu 版本依赖:FP4 的 NPU op(npu_dynamic_mx_quant(dst=float4_e2m1fn_x2)) 在较旧的 torch_npu 构建上可能失败。代码已通过惰性 dtype 解析 + torch 退避缓解,但仍需 Ascend 950/A5 及特定 CANN 版本。
  2. 缺少测试文件:PR 未包含相应的单元测试或 E2E 回归测试。量化路径的正确性仅依赖作者的临时验证,后续重构或版本升级可能引入回归,且 CI 无法自动拦截。
  3. 离线权重格式耦合ModelSlimMXFP4W4A8Scheme 当前假设 msmodelslim 导出的是 packed FP4(uint8, shape [out, in//2]),但 PR body 提到旧版本可能导出 float8_e4m3fn(与 MXFP8 相同布局),若 msmodelslim 导出格式变更,该 scheme 将失效。目前文档已标注这一限制。
  4. 代码复杂度增加:新增两个 LinearMethod 子类 + 一个 Config + 基础设施改动,增加了 NPU 后端的维护成本。后续若推统一基类可部分缓解。

用户影响:使用 Ascend NPU 运行 Qwen3/3.5 Dense 模型的用户可通过 --quantization mxfp_w4a8(在线)或 --quantization modelslim 配合预量化权重获得 11‑13% 吞吐提升及约 10% 时延降低,TTFT 改善尤为显著(-32%)。在线模式无需预量化权重,上手更简便。
系统影响:仅影响启用指定量化选项的模型加载和推理路径,未启用时完全无性能损耗。新增文件独立且不影响其他后端(CUDA/ROCm/CPU)。
团队影响:需要维护新的量化组件和对应的文档;后续需在重构中统一 LinearMethod 基类以降低维护成本。

缺少测试覆盖 硬件版本依赖(torch_npu) 离线权重格式耦合 路径分歧增加维护成本

关联 Issue

#21584 [RFC][NPU] Ascend NPU A5 Support for MXFP8/MXFP4 Quantization

完整报告

参与讨论