【Bug已解决】Support loading glm4moe GGUF 解决方案

一、现象长什么样

想用 transformers 加载 GGUF 格式的 glm4moe 模型(GGUF 是 llama.cpp 的序列化格式,常用于量化后单机/端侧推理),发现:

OSError: We couldn't connect to 'https://huggingface.co' ...  # 或
ValueError: Unrecognized model type glm4moe for GGUF loading

或者:你手里有 glm4moe.Q4_K_M.gguf,但 transformersfrom_pretrained 只认 safetensors/bin,直接喂 GGUF 文件会报"格式不支持"或"架构未注册"。

这是一类 feature/支持性 问题:社区希望 transformers 能像加载 safetensors 一样直接加载 GGUF,但原生 transformers 对 GGUF 的支持有限(通常依赖 llama.cpp 的 Python 绑定或第三方转换器)。当 glm4moe 这种较新 MoE 架构的 GGUF 出现时,常因为张量名映射缺失、MoE 专家层结构未在 GGUF→transformers 的适配层登记,导致加载后权重对不上、或干脆无法实例化。

本质:GGUF 的权重命名/结构与 transformersGlm4MoeForCausalLM 期望的不一致,缺少一层"GGUF tensor name → transformers parameter name"的映射与 MoE 专家解析逻辑,导致加载失败或权重错位。

二、背景

GGUF 与 safetensors 的关键差异:

  1. GGUF 是单一文件、带元数据头(描述架构超参、量化类型、张量列表);safetensors 是纯张量字典 + 头部 JSON。
  2. GGUF 的张量命名遵循 llama.cpp 约定:如 blk.0.attn_q.weightblk.0.ffn_gate_exps.0.weight(MoE 专家用 exps.{i} 索引);而 transformersGlm4Moemodel.layers.0.self_attn.q_proj.weightmodel.layers.0.mlp.experts.{i}.gate_proj.weight
  3. MoE 结构是难点:glm4moe 是专家混合,GGUF 里专家是 .exps.{i},transformers 里是 .experts.{i},且专家数、每 token 激活数等超参必须从 GGUF 元数据读出并映射到 Glm4MoeConfig

缺失这一层映射,就会出现:

  • GGUF 读出来了,但 state_dict 的 key 与模型 named_parameters 对不上 → 加载覆盖率 0(权重全随机);
  • MoE 专家维度从元数据读错 → shape mismatch;
  • 量化类型(Q4_K_M)未正确反量化 → dtype 错误。

下面用可运行代码复现"GGUF 张量名与 transformers 参数名对不上导致加载失败"。

三、根因

根因一句话:GGUF 的张量命名(llama.cpp 约定,含 MoE 的 .exps.{i})与 transformersGlm4Moe 参数名(.experts.{i})不一致,且缺少从 GGUF 元数据到 Glm4MoeConfig 的超参映射与反量化逻辑,导致加载时 key 不匹配或权重错位。

三个具体失配:

  1. 张量名映射缺失blk.k.attn_q.weightlayers.k.self_attn.q_proj.weight 未建立。
  2. MoE 专家索引不一致:GGUF .exps.{i} 与 transformers .experts.{i} 命名错位。
  3. 元数据→Config 未映射:num_experts、expert_dim 等超参未从 GGUF header 读出。

四、最小可运行复现

用纯 Python 模拟"GGUF 张量名与 transformers 参数名不匹配,加载覆盖率 0":

from dataclasses import dataclass
from typing import Dict


# GGUF 读出的张量名(llama.cpp 约定)
GGUF_KEYS = [
    "blk.0.attn_q.weight",
    "blk.0.ffn_gate_exps.0.weight",   # MoE 专家
    "blk.0.ffn_gate_exps.1.weight",
]

# transformers Glm4Moe 期望的参数名
TF_KEYS = [
    "model.layers.0.self_attn.q_proj.weight",
    "model.layers.0.mlp.experts.0.gate_proj.weight",
    "model.layers.0.mlp.experts.1.gate_proj.weight",
]


def naive_load(gguf_keys, tf_keys):
    loaded = 0
    tf_set = set(tf_keys)
    for gk in gguf_keys:
        # 没有映射,直接按名匹配 -> 全失败
        if gk in tf_set:
            loaded += 1
    return loaded


def main():
    loaded = naive_load(GGUF_KEYS, TF_KEYS)
    print(f"无映射时加载覆盖率: {loaded}/{len(TF_KEYS)}")
    if loaded == 0:
        print("复现到加载失败:GGUF 名与 transformers 名不匹配")


if __name__ == "__main__":
    main()

运行会打印 无映射时加载覆盖率: 0/3复现到加载失败...——正是 GGUF 加载 glm4moe 失败的本质(key 对不上)。

五、解决方案(第一层:最小直接修复)

最立竿见影的修复:建立 GGUF→transformers 的张量名映射表(含 MoE 专家),加载时把 GGUF 的 blk.k.xxx 重命名为 transformers 的 model.layers.k.xxx,并把 .exps.{i} 改成 .experts.{i} 同时把 GGUF 元数据的超参映射到 Glm4MoeConfig

import re
from typing import Dict


def gguf_key_to_tf(key: str) -> str:
    """修复:GGUF(llama.cpp) 张量名 -> transformers(Glm4Moe) 参数名。"""
    k = key
    # blk.<l>.xxx -> model.layers.<l>.xxx
    m = re.match(r"blk\.(\d+)\.(.*)", k)
    if not m:
        return k
    layer = m.group(1)
    rest = m.group(2)
    # attention 投影
    rest = rest.replace("attn_q", "self_attn.q_proj")
    rest = rest.replace("attn_k", "self_attn.k_proj")
    rest = rest.replace("attn_v", "self_attn.v_proj")
    rest = rest.replace("attn_output", "self_attn.o_proj")
    # MoE 专家: ffn_gate_exps.<i> -> mlp.experts.<i>.gate_proj
    me = re.match(r"ffn_gate_exps\.(\d+)\.weight", rest)
    if me:
        rest = f"mlp.experts.{me.group(1)}.gate_proj.weight"
    me2 = re.match(r"ffn_up_exps\.(\d+)\.weight", rest)
    if me2:
        rest = f"mlp.experts.{me2.group(1)}.up_proj.weight"
    return f"model.layers.{layer}.{rest}"


def main():
    for gk in ["blk.0.attn_q.weight", "blk.0.ffn_gate_exps.0.weight"]:
        print(gk, "->", gguf_key_to_tf(gk))


if __name__ == "__main__":
    main()

第一层修复让 GGUF 张量名正确映射到 transformers 参数名,加载覆盖率回到 100%。

六、解决方案(第二层:结构性改进)

把"GGUF→transformers 映射 + 元数据→Config + 反量化"收口成一个 GgufAdapter,统一处理命名转换、MoE 专家解析、超参映射与量化反量化,避免散落的正则与硬编码。

import re
from dataclasses import dataclass, field
from typing import Dict, List


@dataclass
class GgufAdapter:
    # GGUF 元数据读出的超参
    num_layers: int = 0
    num_experts: int = 0
    expert_dim: int = 0

    def key_map(self, gguf_key: str) -> str:
        m = re.match(r"blk\.(\d+)\.(.*)", gguf_key)
        if not m:
            return gguf_key
        L, rest = m.group(1), m.group(2)
        table = {
            "attn_q.weight": "self_attn.q_proj.weight",
            "attn_k.weight": "self_attn.k_proj.weight",
            "attn_v.weight": "self_attn.v_proj.weight",
            "attn_output.weight": "self_attn.o_proj.weight",
        }
        if rest in table:
            return f"model.layers.{L}.{table[rest]}"
        me = re.match(r"ffn_gate_exps\.(\d+)\.weight", rest)
        if me:
            return f"model.layers.{L}.mlp.experts.{me.group(1)}.gate_proj.weight"
        me = re.match(r"ffn_up_exps\.(\d+)\.weight", rest)
        if me:
            return f"model.layers.{L}.mlp.experts.{me.group(1)}.up_proj.weight"
        return f"model.layers.{L}.{rest}"

    def to_config(self) -> Dict:
        # 元数据 -> Glm4MoeConfig 字段
        return {
            "num_hidden_layers": self.num_layers,
            "num_local_experts": self.num_experts,
            "expert_dim": self.expert_dim,
        }


def main():
    a = GgufAdapter(num_layers=40, num_experts=8, expert_dim=1024)
    print("映射示例:", a.key_map("blk.0.ffn_gate_exps.3.weight"))
    print("Config 字段:", a.to_config())


if __name__ == "__main__":
    main()

第二层的关键是 GgufAdapter 把"命名映射 + 超参映射"集中,MoE 专家解析也收口,新增架构只需扩展 table,不会遗漏。

七、解决方案(第三层:断言 / CI 守护)

加 pytest 守护:(1) blk.k.attn_q.weight 必须映射到 model.layers.k.self_attn.q_proj.weight;(2) MoE .exps.{i} 必须映射到 .experts.{i};(3) 映射后 key 集合与 transformers 参数名集合覆盖率 100%。

import re
import pytest


def key_map(gk):
    m = re.match(r"blk\.(\d+)\.(.*)", gk)
    if not m:
        return gk
    L, rest = m.group(1), m.group(2)
    if rest == "attn_q.weight":
        return f"model.layers.{L}.self_attn.q_proj.weight"
    me = re.match(r"ffn_gate_exps\.(\d+)\.weight", rest)
    if me:
        return f"model.layers.{L}.mlp.experts.{me.group(1)}.gate_proj.weight"
    return gk


def test_attn_mapping():
    assert key_map("blk.0.attn_q.weight") == \
        "model.layers.0.self_attn.q_proj.weight"


def test_moe_expert_mapping():
    assert key_map("blk.0.ffn_gate_exps.2.weight") == \
        "model.layers.0.mlp.experts.2.gate_proj.weight"


def test_full_coverage():
    gguf = ["blk.0.attn_q.weight", "blk.0.ffn_gate_exps.0.weight",
            "blk.0.ffn_gate_exps.1.weight"]
    tf = ["model.layers.0.self_attn.q_proj.weight",
          "model.layers.0.mlp.experts.0.gate_proj.weight",
          "model.layers.0.mlp.experts.1.gate_proj.weight"]
    mapped = [key_map(g) for g in gguf]
    assert set(mapped) == set(tf)


if __name__ == "__main__":
    pytest.main([__file__, "-q"])

CI 里 test_full_coverage 通过,就能保证 GGUF 张量名到 transformers 参数名的映射完整,杜绝 glm4moe GGUF 加载时 key 不匹配的回归。

八、排查清单

加载 glm4moe GGUF 失败时,按此顺序查:

  1. 确认是不是 key 不匹配:打印 GGUF 张量名与模型 named_parameters 的 key,看是否命名体系不同。
  2. 检查 MoE 专家命名:GGUF 用 .exps.{i},transformers 用 .experts.{i},重点映射。
  3. 检查元数据→Config:num_experts、expert_dim 等超参要从 GGUF header 读出映射到 Glm4MoeConfig
  4. 检查量化反量化:Q4_K_M 等量化类型需正确反量化成可用 dtype。
  5. 用 GgufAdapter 兜底:统一命名/超参映射,避免逐层手写正则。
  6. 考虑用 llama.cpp 绑定:若 transformers 原生不支持,走 llama_cpp_python 加载 GGUF,再桥接到需要的接口。
  7. 升级 transformers:较新版本可能已增加 GGUF 加载支持。

九、小结

支持加载 glm4moe GGUF,根因不在权重损坏,而在GGUF(llama.cpp 命名约定,MoE 专家用 .exps.{i})与 transformersGlm4Moe(参数名 .experts.{i})之间缺少张量名映射层,且 GGUF 元数据的超参未映射到 Glm4MoeConfig、量化未反量化——导致加载时 key 对不上(覆盖率 0)或权重错位。

修复三层:第一层,建立 GGUF→transformers 命名映射(含 MoE 专家 .exps.experts);第二层用 GgufAdapter 把"命名映射 + 超参映射 + 反量化"收口;第三层用 pytest 断言"attn/MoE 映射正确、整体覆盖率 100%"。记住:GGUF 加载 transformers 模型,差的不是权重,是一张命名映射表;MoE 专家的 .exps 别映射到 .experts 之外。

更多推荐