Prhub

#17883 [NPU] Support GGUF quantization for Ascend NPU (dense + MoE)

原始 PR 作者 TheKonka 合并时间 2026-04-25 22:16 文件变更 12 提交数 20 评论 21 代码增减 +834 / -15

执行摘要

在 Ascend NPU 上支持 GGUF 量化模型推理

在 Ascend NPU 上运行 GGUF 格式的量化 LLM 模型(如 Q4_K_M、Q8_0、Q5_K_M 等),实现原生 NPU 支持并优化性能。PR body 说明:"Enable GGUF quantized models to run on Ascend NPU hardware. GGUF is a popular format for quantized LLM models, and this PR adds native NPU support with optimized performance."

该 PR 功能完整,设计上采用预去量化策略简化 NPU 推理路径,是一个合理的权衡。建议关注以下设计决策:预去量化的时机选择(加载时而非推理时)和 MoE 权重在 FusedMoE 中的延迟材料化。对于 NPU 平台的用户,这是一个关键的功能补充。建议团队在未来的量化重构中对 NPU 变体进行统一抽象,以降低维护成本。

讨论亮点
  1. 代码重复问题:gemini-code-assist[bot] 指出 materialize_gguf_weightsprocess_weights_after_loading 中 w13/w2 的处理逻辑高度相似,建议提取辅助函数。该建议未在后续提交中显著重构,属于设计债务被记录。

  2. 内联导入风格:ping1jing2 要求将 materialize_gguf_weights 方法内的 from torch.nn.parameter import UninitializedParameter 移到文件顶部。该问题已在最终代码中修复(文件顶部已有该导入)。

  3. GPU 兼容性疑虑:TamirBaydasov 担心修改 gguf_quant_weights_iterator 会破坏 GPU 上的 GGUF 模型加载。TheKonka 回复称 GPU+GGUF+MoE 原有错误,修改后行为不变;GPU+GGUF+DENSE 正常。团队确认无回归。

  4. 测试覆盖建议:ping1jing2 在 qwen2_moe.py 的 review 中建议添加单元测试。本 PR 仅包含集成测试,未添加单元测试。

实现拆解

实现拆解如下:

  1. 新增 NPU 专用量化方法:在 python/sglang/srt/layers/quantization/gguf.py 中新增三个 Ascend 特化类:GGUFLinearAscendMethod(线性层)、GGUFMoEAscendMethod(MoE 层)和 GGUFEmbeddingAscendMethod(Embedding 层)。它们继承自 LinearMethodBaseFusedMoEMethodBase 等基类,并在 process_weights_after_loading 中执行预去量化(pre-dequantization),将量化参数还原为全精度 halfbfloat16 张量,加载到 NPU 后推理时不再需要量化计算。新增 ggml_dequantize_ascend 函数封装 gguf.dequantize 的 CPU 参考实现。

  2. MoE 权重加载适配:在 python/sglang/srt/layers/moe/fused_moe_triton/layer.py 中新增 _load_gguf_weight 方法,在 _weight_loader_impl 中优先处理 GGUF 类型的参数:将权重数据暂存在 param.data_containerparam.expert_data_map 中,并处理 TP 分片。新增 materialize_gguf_weights 方法,在所有权重加载完成后组装 w13(gate+up 合并)和 w2 专家权重。

  3. 权重加载器扩展:在 python/sglang/srt/model_loader/weight_utils.py 中重写了 gguf_quant_weights_iterator,使其能够识别 MoE 专家权重的打包格式(blk.{layer_id}.ffn_{gate/up/down}_exps.weight),将其拆分为每个专家的独立权重并生成相应的 qweightqweight_type 张量名称。

  4. 模型代码微调:修改 python/sglang/srt/models/qwen3_moe.py,在 GGUF 场景下将 norm_topk_prob 设置为 False,避免与预去量化权重不兼容;修改 python/sglang/srt/models/qwen2_moe.pypython/sglang/srt/layers/linear.py 以传递 quant_config 参数。

  5. 测试与文档:新增两个集成测试文件 test_npu_gguf.py(密集模型)和 test_npu_gguf_moe.py(MoE 模型),基于 gsm8k 数据集验证精度,准确率分别达到 0.80 和 0.85 以上。文档更新了 Ascend NPU 量化指南和通用量化页面。

文件 模块 状态 重要度
python/sglang/srt/layers/quantization/gguf.py 量化层 modified 8.84
python/sglang/srt/layers/moe/fused_moe_triton/layer.py MoE 层 modified 7.91
python/sglang/srt/model_loader/weight_utils.py 权重加载 modified 7.28
test/registered/ascend/basic_function/quant/test_npu_gguf_moe.py NPU 测试 added 6.86
test/registered/ascend/basic_function/quant/test_npu_gguf.py NPU 测试 added 6.85

关键符号

ggml_dequantize_ascend GGUFLinearAscendMethod.create_weights GGUFLinearAscendMethod.process_weights_after_loading GGUFMoEAscendMethod.apply GGUFEmbeddingAscendMethod.apply GGUFConfig.get_quant_method _load_gguf_weight materialize_gguf_weights gguf_quant_weights_iterator

关键源码片段

python/sglang/srt/layers/quantization/gguf.py core-logic

核心实现文件:添加了 NPU 专用的 GGUF 量化方法(线性层、MoE 层、Embedding 层)和去量化函数,以及在 GGUFConfig 中根据设备选择不同方法的调度逻辑。

# 文件 : python/sglang/srt/layers/quantization/gguf.py
# 关键变更 1: 设备感知的量化方法分派def get_quant_method(self, layer, prefix):
    """根据层类型和设备返回合适的量化方法"""
    from sglang.srt.layers.moe.fused_moe_triton import FusedMoE
    from sglang.srt.layers.vocab_parallel_embedding import VocabParallelEmbedding
​
    if isinstance(layer, LinearBase):
        if is_layer_skipped_gguf(prefix, self.modules_to_not_convert):
            return UnquantizedLinearMethod()
        # NPU 使用预去量化方案,CUDA/MUSA 使用原方案
        if _is_npu:
            return GGUFLinearAscendMethod(self)
        return GGUFLinearMethod(self)
    elif isinstance(layer, VocabParallelEmbedding):
        if _is_npu:
            return GGUFEmbeddingAscendMethod(self)
        return GGUFEmbeddingMethod(self)
    elif isinstance(layer, FusedMoE):
        if _is_npu:
            return GGUFMoEAscendMethod(self)
        return GGUFMoEMethod(self)
    return None# 关键变更 2: NPU 专用的去量化函数(在 CPU 上执行参考实现)
def ggml_dequantize_ascend(qweight, qweight_type, rows, cols, dtype):
    """
    使用 gguf 库的参考实现在 CPU 上进行去量化,
    支持所有 GGML 格式,保证正确性。
    结果转移到 NPU 设备后供推理使用。
    """
    # 将量化权重移到 CPU 并通过 numpy 调用 gguf 库
    qweight_cpu = qweight.cpu().numpy()
    dequant_np = gguf_dequantize(qweight_cpu, qweight_type)
    # 转换回 torch 张量并移到 NPU
    result = torch.from_numpy(dequant_np).to(dtype=dtype, device=qweight.device)
    return result.reshape(rows, cols)
python/sglang/srt/layers/moe/fused_moe_triton/layer.py core-logic

MoE 关键路径:新增 GGUF 权重加载钩子 _load_gguf_weight 和材料化方法 materialize_gguf_weights,实现 FusedMoE 对 GGUF 权重的支持。

# 文件 : python/sglang/srt/layers/moe/fused_moe_triton/layer.py
# 关键方法 : 在权重加载时劫持 GGUF 参数,暂存到 data_container 中def _load_gguf_weight(self, param, loaded_weight, shard_id, expert_id, tp_rank):
    """尝试将 loaded_weight 作为 GGUF 权重处理。
    如果处理成功返回 True,否则返回 False(由普通加载逻辑继续)。
    """
    is_gguf_weight = getattr(param, 'is_gguf_weight', False)
    is_gguf_weight_type = getattr(param, 'is_gguf_weight_type', False)
​
    if is_gguf_weight_type:
        # 存储该专家的量化类型(如 Q4_K_M),用于后续去量化
        param.weight_type = loaded_weight.item()
        return True
​
    if is_gguf_weight:
        output_dim = getattr(param, 'output_dim', None)
        if self.moe_tp_size > 1:
            # 如果模型并行,对第一个维度进行分片
            if shard_id in ['w1', 'w3', 'w2'] and output_dim == 0:
                shard_size = loaded_weight.size(0) // self.moe_tp_size
                start_idx = tp_rank * shard_size
                loaded_weight = loaded_weight.narrow(0, start_idx, shard_size).clone()
​
        # 暂存到 expert_data_map 和 data_container 中
        if not hasattr(param, 'expert_data_map'):
            param.expert_data_map = {}
        key = (expert_id, shard_id)
        param.expert_data_map[key] = loaded_weight
        param.data_container.append(loaded_weight)
        return True
​
    return False
python/sglang/srt/model_loader/weight_utils.py data-contract

权重加载器:重写 gguf_quant_weights_iterator 以支持 MoE 专家权重的打包格式,将其解析为每个专家的独立权重。

# 文件 : python/sglang/srt/model_loader/weight_utils.py
# 关键变更 : gguf_quant_weights_iterator 新增 MoE 专家权重解析def gguf_quant_weights_iterator(gguf_file, gguf_to_hf_name_map):
    reader = gguf.GGUFReader(gguf_file)
    # MoE 权重在 GGUF 中以打包格式存储 : blk.{layer_id}.ffn_gate_exps.weight
    MOE_WEIGHT_PATTERNS = {
        'ffn_gate_exps': 'gate_proj',
        'ffn_up_exps': 'up_proj',
        'ffn_down_exps': 'down_proj',
    }
​
    # 第一遍 : 输出权重类型 (qweight_type)
    for tensor in reader.tensors:
        # 判断是否为 MoE 专家权重
        if any(pattern in tensor.name for pattern in MOE_WEIGHT_PATTERNS):
            import re
            match = re.match(r'blk\.(\d+)\.(ffn_\w+_exps)\.weight', tensor.name)
            if match:
                layer_id = int(match.group(1))
                pattern = match.group(2)
                hf_name = MOE_WEIGHT_PATTERNS.get(pattern)
                if hf_name and tensor.tensor_type.name != 'F32':
                    # 对打包中的每个专家输出一个 qweight_type
                    num_experts = tensor.data.shape[0]
                    for expert_id in range(num_experts):
                        hf_type_name = f'model.layers.{layer_id}.mlp.experts.{expert_id}.{hf_name}.qweight_type'
                        yield hf_type_name, torch.tensor(tensor.tensor_type)
        elif tensor.name in gguf_to_hf_name_map:
            # 常规权重保持不变
            name = gguf_to_hf_name_map[tensor.name]
            if tensor.tensor_type.name != 'F32':
                yield name.replace('weight', 'qweight_type'), torch.tensor(tensor.tensor_type)
​
    # 第二遍 : 输出实际权重,MoE 权重拆分为每个专家
    for tensor in reader.tensors:
        # 类似逻辑,根据 pattern 拆分为独立权重
        # ...(省略重复代码)

评论区精华

代码重复:materialize_gguf_weights 中 w13 和 w2 处理逻辑高度相似 设计

gemini-code-assist[bot] 指出 w13 和 w2 的材料化逻辑几乎相同,建议提取辅助函数以减少重复。

结论:建议未被采纳,在最终实现中仍存在重复代码。 · unresolved

内联 import 风格问题 style

ping1jing2 要求将 materialize_gguf_weights 内部的 `from torch.nn.parameter import UninitializedParameter` 移到文件顶部。

结论:已在最终代码中修正:文件顶部添加了该导入语句。 · 已解决

GPU GGUF 加载兼容性风险 正确性

TamirBaydasov 询问修改 `gguf_quant_weights_iterator` 是否会影响 GPU 上的 GGUF 模型加载。TheKonka 回应称 GPU+GGUF+MoE 原来就有错误,行为不变;GPU+GGUF+DENSE 正常。

结论:作者确认兼容性无变化,GPU+GGUF+MoE 的原有错误需另行修复。 · 已解决

缺少针对 NPU GGUF 路径的单元测试 测试

ping1jing2 在 qwen2_moe.py 的 review 中建议为 GGUF 相关逻辑添加单元测试。

结论:PR 仅包含端到端集成测试,未添加单元测试,建议未完全采纳。 · partially resolved

风险与影响

  1. GPU GGUF MoE 兼容性:作者指出 GPU+GGUF+MoE 组合在修改前后均报错,但未解释具体原因或提供修复方案。这可能导致用户在 GPU 上使用 GGUF MoE 模型时仍然失败,尽管这可能是一个预先存在的问题。
  2. 预去量化内存开销:预去量化策略在加载时将所有权重转为全精度,对于大模型会增加显存占用。用户需关注 --mem-fraction-static 参数的设置。
  3. 模型兼容性有限:仅使用 Qwen3-14B 和 Qwen3-30B-A3B 进行了验证,其他 GGUF 模型(尤其是不同架构或量化类型)可能遇到未预期的错误。
  4. 缺少单元测试:未针对 ggml_dequantize_ascend 等核心函数编写单元测试,仅依赖端到端集成测试,调试成本较高。
  5. 代码维护债务:review 指出的代码重复问题未被修复,未来重构量化文件夹时可能增加技术债务。
  • 用户影响:Ascend NPU 用户现在可以直接加载 GGUF 格式的量化模型,无需额外转换步骤。支持 Q4_K_M、Q8_0 等常见量化类型,覆盖密集和 MoE 模型架构。
  • 系统影响:不影响 CUDA 和 MUSA 后端,仅当 device='npu' 时启用 Ascend 专用路径。预去量化过程发生在模型加载阶段,推理延迟不受影响。
  • 团队影响:新增的 NPU 特异性代码增加了量化模块的维护分支。未来重构(如 Issue 中提及的量化文件夹重构)需考虑这些 Ascend 变体。
新功能仅验证两种模型 GPU GGUF MoE 原有错误未修复 预去量化增加加载内存开销 缺少核心函数单元测试 代码重复债务被记录

关联 Issue

未识别关联 Issue

当前没有检测到明确关联的 Issue 链接,后续同步到相关引用后会出现在这里。

完整报告

参与讨论