Prhub

#56176 [ROCm] [Bugfix] Enable Load and Inference of GLM-5.3-Flash Quark MXFP4 Checkpoint

原始 PR 作者 ColinZ22 合并时间 2026-09-16 09:25 文件变更 8 提交数 9 评论 14 代码增减 +164 / -4

执行摘要

修复 GLM-5.3-Flash Quark MXFP4 权重加载崩溃与 scale 名误匹配

Quark checkpoint amd/GLM-5.3-Flash-Quark-MXFP4 此前完全无法加载(先抛 AssertionError 再抛 KeyError)。PR body 明确指出“Two independent bugs break weight load”,而 Issue #54641 复现了第一个 bug:identity entry 会让 gate_up_proj 静默回退到 global_quant_config(MXFP4),与 checkpoint 中 block-FP8 张量形状不匹配。第二个 bug 是 MLA 投影 scale 名不一致:Quark 导出 weight_scale,而加载器只认 weight_scale_inv。

值得精读,尤其对量化加载链路(Quark、AutoWeightsLoader)和模型目录组织感兴趣的开发者。本 PR 展示了“以 checkpoint 事实驱动修复”的典型路径:先用 config.json 验证 layer_quant_config 的键形式,再决定修模型映射还是修量化库,避免在通用层引入不当行为。测试设计(用 recording param 隔离加载路径)也值得借鉴。

讨论亮点
  1. #54641 的修复范围争议:fxmarty-amd 质疑“identity 映射导致 layer_quant_config 失效”是合法且更普遍的问题,询问本 PR 为何不直接在 quark.py 修复,并要求补测试。ColinZ22 回应:本 PR 只解决该 issue 中复现的 GLM-5.3-Flash 实际阻塞(reproduction 中的崩溃),checkpoint 的 layer_quant_config 用的是 split 名称,因此模型内 override 是正确路径;liulanze 对照发布版 config.json 确认没有 gate_up_proj 键,佐证了该决策。
  2. 目录结构讨论:BowenBao 转述 AndreasKaratzas 建议,要求把 model.py 移到 common/,因为修复在 amd/nvidia 通用;作者照做并 rebase 后重新验证两个 checkpoint 的 GSM8k。
  3. NV 适用范围确认:AndreasKaratzas 分别对 packed_modules_mapping 和 is_scale 两处改动问“是否同样适用于 NV”。作者确认 nvidia/GLM-5.3-Flash-NVFP4 也存分离 gate/up 张量(H100 gsm8k 0.9371),但 NVFP4 的 MLA 投影已在 checkpoint 内反量化为 BF16,不会进入 _try_load_fp8_attn_proj 路径。

实现拆解

  1. 变更入口:核心修复集中在 vllm/models/glm5next/common/model.py(由 nvidia/model.py 移动而来)。
    - 在 Glm5NextForConditionalGeneration 上覆盖 packed_modules_mapping = {"gate_up_proj": ["gate_proj", "up_proj"]},替换从 Glm4vForConditionalGeneration 继承的 identity 映射。GLM-5.3-Flash 的 checkpoint(Quark、NVFP4、zai 基座)均以分离张量存储 gate/up,展开为真实 shard 名后,Quark 的 layer_quant_config 才能命中 per-layer block-FP8 方案,避免回退到全局 MXFP4。
    - _try_load_fp8_attn_proj 的 scale 识别从 is_scale = "weight_scale_inv" in name 扩展为 "weight_scale_inv" in name or name.endswith(".weight_scale"),使 Quark 命名的 scale 进入同一套 buffer 配对 + block 反量化逻辑。
  2. 目录重构:model.py、attention.py、kda.py、mtp.py、multimodal.py 从 glm5next/nvidia/ 平移至 glm5next/common/,glm5next/__init__.py 的导入同步改为 common 路径;mtp.py 中对 fused_eh_norm 的导入改为显式 vllm.models.glm5next.nvidia.ops.fused_eh_norm(ops 目录仍留在 nvidia 下)。该调整由 review 推动,目的是表达修复对 AMD/NVIDIA 都适用。
  3. 测试配套:tests/quantization/test_quark.py 新增 4 个测试和辅助设施(_glm5_mixed_precision_config、_GLM5RecordingParam、_glm5_block_fp8),覆盖映射真实性、scheme 决议、Quark weight_scale 识别以及 DeepSeek weight_scale_inv 回归;测试不依赖完整模型实例,通过 recording param 验证加载路径正确性。
  4. 验证:作者在 4x AMD MI355X 上以 VLLM_ROCM_USE_AITER=1 服务 Quark checkpoint,gsm8k 5-shot strict exact_match 0.9219;同时在 H100 上验证 NVFP4 变体不受影响并受益(0.9371)。
文件 模块 状态 重要度
vllm/models/glm5next/common/model.py 模型加载 renamed 6.78
tests/quantization/test_quark.py 量化 modified 7.05
vllm/models/glm5next/__init__.py 模型入口 modified 4.88
vllm/models/glm5next/common/mtp.py 推测解码 renamed 5.11
vllm/models/glm5next/common/attention.py 注意力 renamed 4.16
vllm/models/glm5next/common/kda.py 模型代码 renamed 4.16
vllm/models/glm5next/common/multimodal.py 多模态 renamed 4.16
vllm/models/glm5next/common/__init__.py 模型入口 added 4.37

关键符号

Glm5NextForConditionalGeneration.packed_modules_mapping _try_load_fp8_attn_proj _glm5_mixed_precision_config test_glm5next_gate_up_proj_mapping_is_genuine_fusion test_glm5next_genuine_fusion_resolves_gate_up_to_block_fp8 test_glm5next_attn_loader_accepts_quark_weight_scale test_glm5next_attn_loader_accepts_deepseek_weight_scale_inv

关键源码片段

vllm/models/glm5next/common/model.py core-logic

承载两处核心修复:覆盖 `packed_modules_mapping` 使 gate/up 展开为真实 shard 名,以及 `_try_load_fp8_attn_proj` 接受 Quark 的 `weight_scale` scale 名。文件同时由 `nvidia/model.py` 移动至 `common/`。

class Glm5NextForConditionalGeneration(Glm4vForConditionalGeneration, HasInnerState, IsHybrid, MixtureOfExperts):
    # GLM-5.3-Flash 把 dense-MLP 的 gate/up 存成两个独立张量,
    # 与 Glm4vMoeForConditionalGeneration、glm4_moe、deepseek_v2 一致。
    # 因此 fused `gate_up_proj` 必须展开成真实 shard 名 `gate_proj` / `up_proj`,
    # Quark 的 per-layer 量化配置(layer_quant_config)才能命中。
    # 从 Glm4vForConditionalGeneration 继承的 identity 映射只适用
    # GLM-4.1V 那种预融合 checkpoint,会让层回退到全局 MXFP4 方案,
    # 加载 FP8 权重时因形状不匹配而崩溃(即 #54641 复现的 AssertionError)。
    packed_modules_mapping = {
        "gate_up_proj": ["gate_proj", "up_proj"],
    }
def _try_load_fp8_attn_proj(
    name,
    tensor,
    buf,
    params_dict,
    loaded_params,
    kv_a_pad_size: int,
) -> bool:
    # 仅处理 checkpoint 里以 block-FP8 存储、但模型侧保持 BF16 的
    # q_a_proj / kv_a_proj_with_mqa / q_b_proj / o_proj 投影;
    # 返回 True 表示已接管该 tensor 的加载。
    matched = None
    for suffix, info in _FP8_ATTN_PROJS.items():
        if suffix in name:
            matched = (suffix, info)
            break
    if matched is None:
        return False
​
    suffix, (key, target_base, shard_id, is_kva) = matched
    is_weight = name.endswith(".weight") and tensor.dtype == torch.float8_e4m3fn
    # DeepSeek 原生 scale 名是 weight_scale_inv,Quark checkpoint 导出的是
    # weight_scale;这里两者都接受,统一进入下面的 buffer 配对逻辑。
    is_scale = "weight_scale_inv" in name or name.endswith(".weight_scale")
    if not is_weight and not is_scale:
        return False
​
    layer_prefix = name.rsplit(suffix, 1)[0]
    target_w = f"{layer_prefix}.{target_base}.weight"
    target_s = f"{layer_prefix}.{target_base}.weight_scale_inv"
    # 若模型侧真的保留了 FP8(存在 weight_scale_inv 参数),
    # 说明不该反量化,直接返回 False 交给常规 stacked/direct 加载路径。
    if target_s in params_dict:
        return False
​
    # weight 与 scale 到达顺序不固定,先到的一半进 buf 缓冲,
    # 等另一半到齐再一次性反量化,避免破坏 fused_qkv_a_proj 的 shard 语义。
    entry = buf.setdefault(layer_prefix, {}).setdefault(key, {})
    entry["weight" if is_weight else "scale"] = tensor
    if "weight" not in entry or "scale" not in entry:
        return True
​
    weight_fp8, scale_inv = entry["weight"], entry["scale"]
    buf[layer_prefix].pop(key, None)
    block_size = weight_fp8.shape[1] // scale_inv.shape[1]
    weight_bf16 = _dequant_fp8_block(weight_fp8, scale_inv, block_size)
    # NoPE 场景下 kv_a 需要补零 rope 段(宽度对齐),再写入目标参数。
    if is_kva and kv_a_pad_size > 0:
        pad = torch.zeros(
            kv_a_pad_size,
            weight_bf16.shape[1],
            dtype=weight_bf16.dtype,
            device=weight_bf16.device,
        )
        weight_bf16 = torch.cat([weight_bf16, pad], dim=0)
​
    param = params_dict[target_w]
    # 随后调用 param.weight_loader(param, weight_bf16, shard_id) 写入目标:
    # shard_id 决定 fused_qkv_a_proj 内部是 q_a(0)还是 kv_a(1)分支;
    # 完成后把 target_w 加进 loaded_params,并返回 True。
tests/quantization/test_quark.py test-coverage

新增 145 行回归测试,用 recording param 隔离验证映射解析、scheme 决议、Quark scale 识别与 DeepSeek scale 回归,是本次修复正确性的关键保障。

# 全局 MXFP4、dense-MLP 的 gate/up 单独标记 block-FP8 的混合精度配置,
# 复刻 amd/GLM-5.3-Flash-Quark-MXFP4 的 layer_quant_config 形状。
def _glm5_mixed_precision_config() -> QuarkConfig:
    fp8 = {"weight": _GLM5_BLOCK_FP8_WEIGHT, "input_tensors": _GLM5_BLOCK_FP8_INPUT}
    return QuarkConfig(
        {
            "global_quant_config": {"weight": _GLM5_MXFP4_WEIGHT, "input_tensors": None},
            "layer_type_quant_config": {},
            "layer_quant_config": {"*mlp.gate_proj": fp8, "*mlp.up_proj": fp8},
            "exclude": [],
        }
    )
​
​
def test_glm5next_genuine_fusion_resolves_gate_up_to_block_fp8():
    # fused 模块必须展开成真实 shard 名,每个 shard 才能命中
    # per-layer 的 block-FP8 条目,而不是回退到全局 MXFP4 方案。
    config = _glm5_mixed_precision_config()
    config.packed_modules_mapping = {"gate_up_proj": ["gate_proj", "up_proj"]}
    _, _, scheme_cls = config.get_scheme_cls(LinearBase, _GLM5_GATE_UP)
    assert scheme_cls is QuarkW8A8Fp8PerBlock
​
​
def test_glm5next_attn_loader_accepts_quark_weight_scale():
    # 回归 KeyError(...kv_a_proj_with_mqa.weight_scale):fused q_a/kv_a
    # 保持 BF16 并在加载时反量化,Quark 的 scale 名必须被识别,
    # 且路由到 fused_qkv_a_proj 的正确 shard。
    from vllm.models.glm5next.common.model import _dequant_fp8_block, _try_load_fp8_attn_proj
​
    prefix = "layers.3.self_attn"
    target = _GLM5RecordingParam()
    params_dict = {f"{prefix}.fused_qkv_a_proj.weight": target}
    buf, loaded = {}, set()
    weight, scale = _glm5_block_fp8(256, 256)
​
    # fp8 weight 先到达 -> 只进缓冲,此时还未触发加载。
    assert _try_load_fp8_attn_proj(
        f"{prefix}.kv_a_proj_with_mqa.weight", weight, buf, params_dict, loaded, 0
    )
    assert target.loaded_weight is None
    # Quark 命名的 scale 补齐配对 -> 反量化并加载。
    assert _try_load_fp8_attn_proj(
        f"{prefix}.kv_a_proj_with_mqa.weight_scale", scale, buf, params_dict, loaded, 0
    )
    assert target.loaded_weight is not None
    assert target.loaded_weight.dtype == torch.bfloat16
    assert target.loaded_shard == 1 # kv_a 是 fused_qkv_a_proj 的 shard 1
    assert torch.equal(target.loaded_weight, _dequant_fp8_block(weight, scale, 128))
    assert f"{prefix}.fused_qkv_a_proj.weight" in loaded

评论区精华

#54641 的修复范围与测试要求 question

fxmarty-amd 认为 #54641 描述的 identity entry 问题合法且影响面广,询问本 PR 为何不直接在 quark.py 修复,并要求补充 mocked config 驱动的测试。

结论:ColinZ22 澄清本 PR 只解决 issue 中复现的 GLM-5.3-Flash 实际阻塞,checkpoint 的 layer_quant_config 使用 split 名称,模型内 override 才是正确修复;liulanze 对照发布版 config.json 确认无 gate_up_proj 键。测试随后补充进 test_quark.py。 · 已解决

共享文件移至 common 目录 设计

BowenBao 转述 AndreasKaratzas 的建议:修复对 amd/nvidia 通用,应把 model.py 放到 common 文件夹,若只针对 amd 则 fork 出 amd/model.py。

结论:作者将 model.py、attention.py、kda.py、mtp.py、multimodal.py 全部移入 common/,rebased 后重新验证两个 checkpoint 的 GSM8k。 · 已解决

NV 平台适用范围确认 question

AndreasKaratzas 分别对 packed_modules_mapping 覆盖和 is_scale 扩展提问,确认这两处改动是否同样适用于 NVIDIA。

结论:作者确认 nvidia/GLM-5.3-Flash-NVFP4 同样分离存储 gate/up(H100 gsm8k 0.9371);而 NVFP4 的 MLA 投影已在 checkpoint 内反量化为 BF16,不会进入 _try_load_fp8_attn_proj 路径。 · 已解决

风险与影响

  1. 量化方案解析敏感性:packed_modules_mapping 覆盖只对 Glm5NextForConditionalGeneration 生效,不影响继承源 Glm4vForConditionalGeneration,但若未来出现 GLM-5.3-Flash 变体以预融合 gate_up_proj 存储,本修复会导致 shard 展开后找不到目标名,需要 checkpoint 事实驱动演进。
  2. scale 匹配放宽:name.endswith(".weight_scale") 比之前的子串匹配更宽,虽被限制在 _FP8_ATTN_PROJS 后缀内,但若 checkpoint 同时携带其他语义的 weight_scale 张量,可能被误当作反量化 scale 缓冲。
  3. 目录重构影响:nvidia/ 与 common/ 的移动会破坏仓库外部对旧路径的直接 import;仓库内部导入均已更新,但需要 CI(Buildkite #89206)覆盖确认。
  4. 与 #54702 的方案分歧:若日后在 quark.py 中加入 identity 映射容忍逻辑,与本 PR 的模型内 override 可能产生双重解析,需要协调语义。

影响范围集中在 GLM-5.3-Flash 系列模型:AMD Quark MXFP4 checkpoint 从完全不可用变为可加载、可服务,gsm8k 精度与源 checkpoint 对齐(0.9219 vs 0.9121);NVIDIA NVFP4 变体不受回归影响并在 H100 上验证达标。对团队而言,common/ 目录的建立明确了 glm5next 代码跨厂商共享的组织方式,降低后续 ROCm/NVIDIA 双线维护成本。对 vLLM 其他模型无直接行为影响。

量化方案解析敏感性 scale 名称匹配放宽 跨厂商目录重构 仅覆盖 Glm5Next 路径

关联 Issue

#54641 [Bug]: Identity entry in packed_modules_mapping silently disables layer_quant_config (Quark)

完整报告

参与讨论