Read OSS

为 llama.cpp 贡献新的模型架构

高级

前置知识

  • 第 1–5 篇文章
  • 熟悉至少一种 transformer 架构的前向传播过程
  • 具备基础 Python 知识,用于编写转换器

为 llama.cpp 贡献新的模型架构

经过本系列前几篇文章的积累,我们已经对 llama.cpp 的内部机制有了较为深入的理解:双库结构、计算图上下文工具集、GGML 的 backend 系统、解码流水线,以及应用层的设计。现在,是时候把这些知识付诸实践了。本文是向 llama.cpp 添加新模型架构的完整实践指南,涵盖需要修改的每一个文件,以及操作的先后顺序。

添加一个模型架构需要按照特定顺序修改五到六个文件。在整个过程中,我们将以现有的 LLaMA 模型作为参考实现,因为它是大多数架构所遵循的典型范例。

添加模型的完整流程

以下是从第一行改动到 PR 合并的完整路径:

flowchart TD
    S1["Step 1: Architecture Registration\nllama-arch.h + llama-arch.cpp"] --> S2["Step 2: Python Converter\nconvert_hf_to_gguf.py"]
    S2 --> S3["Step 3: Tensor Loading\nllama-model.cpp\n(load_hparams + load_tensors)"]
    S3 --> S4["Step 4: Graph Builder\nsrc/models/mymodel.cpp"]
    S4 --> S5["Step 5: Registration\nllama-model.cpp\n(build_graph + create_memory)"]
    S5 --> S6["Step 6: Build & Test\nCMakeLists.txt + validation"]
步骤 修改文件 目的
1 src/llama-arch.hsrc/llama-arch.cpp 注册架构标识符和 tensor 名称
2 convert_hf_to_gguf.py 编写从 HuggingFace 格式转换的 Python 转换器
3 src/llama-model.cpp 添加超参数和 tensor 加载逻辑
4 src/models/{name}.cpp(新文件) 实现计算图构建器
5 src/llama-model.cpp build_graph()create_memory() 添加分发入口
6 src/CMakeLists.txt 将新源文件加入构建

第一步:注册架构

首先,在 src/llama-arch.hllm_arch 枚举 中添加你的架构:

enum llm_arch {
    // ... existing entries ...
    LLM_ARCH_MY_MODEL,
    LLM_ARCH_UNKNOWN, // keep this last
};

然后在 src/llama-arch.cpp 中添加对应的字符串映射:

static const std::map<llm_arch, const char *> LLM_ARCH_NAMES = {
    // ... existing entries ...
    { LLM_ARCH_MY_MODEL, "my-model" },
};

这里的字符串 "my-model" 必须与 Python 转换器写入 GGUF 文件的 general.architecture 值保持一致。

接下来还需要在 llama-arch.cpp 中注册 tensor 名称映射。这是一张名为 LLM_TENSOR_NAMES 的大型映射表,告诉加载器如何通过 GGUF 名称查找对应的 tensor。示例如下:

{ LLM_ARCH_MY_MODEL, {
    { LLM_TENSOR_TOKEN_EMBD,      "token_embd" },
    { LLM_TENSOR_OUTPUT_NORM,     "output_norm" },
    { LLM_TENSOR_OUTPUT,          "output" },
    { LLM_TENSOR_ATTN_NORM,      "blk.%d.attn_norm" },
    { LLM_TENSOR_ATTN_Q,         "blk.%d.attn_q" },
    // ... all weight tensors
}},

提示: 参考 LLaMA 架构的 tensor 名称条目作为模板。大多数 transformer 模型使用相同的 tensor 集合,主要的差异在于命名方式,以及哪些可选 tensor(bias、gate projection、MoE 权重等)实际存在。

第二步:编写 Python 转换器

转换器 convert_hf_to_gguf.py 采用类继承的方式组织代码,每种模型架构对应一个独立的转换器类。新增一个类的方式如下:

@ModelBase.register("MyModelForCausalLM")
class MyModelModel(TextModel):
    model_arch = gguf.MODEL_ARCH.MY_MODEL
    
    def set_gguf_parameters(self):
        super().set_gguf_parameters()
        # Write architecture-specific hyperparameters
        self.gguf_writer.add_block_count(self.hparams["num_hidden_layers"])
        self.gguf_writer.add_embedding_length(self.hparams["hidden_size"])
        self.gguf_writer.add_head_count(self.hparams["num_attention_heads"])
        # ... etc
    
    def modify_tensors(self, data_torch, name, bid):
        # Map HuggingFace tensor names to GGUF names
        # Return list of (new_name, tensor) pairs
        return [(self.map_tensor_name(name), data_torch)]

@ModelBase.register("MyModelForCausalLM") 装饰器根据 HuggingFace config.json 中的 architectures 字段来注册转换器类。model_arch 属性必须与你在 gguf-py/gguf/constants.py 中添加的枚举值保持一致。

还需要在 gguf-py 的常量文件中添加对应的架构:

# In gguf-py/gguf/constants.py
class MODEL_ARCH(IntEnum):
    # ...
    MY_MODEL = auto()

并在同一文件中添加其 tensor 名称映射。

flowchart LR
    HF["HuggingFace Checkpoint\nconfig.json + model.safetensors"] --> CONV["convert_hf_to_gguf.py"]
    CONV --> ARCH["MyModelModel class"]
    ARCH --> PARAMS["set_gguf_parameters()\nWrite hyperparameters"]
    ARCH --> TENSORS["modify_tensors()\nRename + transform weights"]
    PARAMS --> GGUF["Output: model.gguf"]
    TENSORS --> GGUF

第三步:加载超参数和 Tensor

回到 C++ 侧,需要让 llama.cpp 学会读取 GGUF 元数据并加载 tensor。这部分逻辑位于 src/llama-model.cpp

加载超参数 — 找到 load_hparams() 方法,在其 switch 语句中为你的架构添加一个 case。在这里读取 GGUF 元数据键,并填充 hparams 结构体:

case LLM_ARCH_MY_MODEL:
    ml.get_key(LLM_KV_BLOCK_COUNT,       hparams.n_layer);
    ml.get_key(LLM_KV_EMBEDDING_LENGTH,  hparams.n_embd);
    ml.get_key(LLM_KV_ATTENTION_HEAD_COUNT, hparams.n_head());
    // ... model-specific hyperparameters
    break;

加载 tensor — 找到 load_tensors() 方法,添加一个 case,为每个权重创建对应的 ggml_tensor 指针。其模式与 llama_layer 保持一致:

case LLM_ARCH_MY_MODEL:
    model.tok_embd = create_tensor(tn(LLM_TENSOR_TOKEN_EMBD), {n_embd, n_vocab});
    for (int i = 0; i < n_layer; ++i) {
        auto & layer = model.layers[i];
        layer.attn_norm = create_tensor(tn(LLM_TENSOR_ATTN_NORM, i), {n_embd});
        layer.wq = create_tensor(tn(LLM_TENSOR_ATTN_Q, i), {n_embd, n_embd});
        // ... all per-layer tensors
    }
    break;

提示: create_tensor() 调用本身不会分配内存,它只是注册 tensor 的形状和名称,以便模型加载器从 GGUF 文件中填充数据。实际的内存分配和设备分配(CPU 还是 GPU)由加载器根据 n_gpu_layers 统一处理。

第四步:编写计算图构建器

创建新文件 src/models/mymodel.cpp。在这里,你将使用第 2 篇文章介绍的 llm_graph_context 工具集方法来实现模型的前向传播。

建议以 LLaMA 参考实现 src/models/llama.cpp 为起点,根据你的架构拓扑进行修改。通用模式如下:

#include "models.h"

struct llm_build_my_model : public llm_graph_context {
    llm_build_my_model(const llama_model & model, 
                       const llm_graph_params & params) 
        : llm_graph_context(params) {
        
        ggml_tensor * cur;
        ggml_tensor * inpL = build_inp_embd(model.tok_embd);
        ggml_tensor * inp_pos = build_inp_pos();
        auto * inp_attn = build_attn_inp_kv();
        ggml_tensor * inp_out_ids = build_inp_out_ids();
        
        for (int il = 0; il < n_layer; ++il) {
            // 1. Pre-attention norm
            cur = build_norm(inpL, model.layers[il].attn_norm, 
                           NULL, LLM_NORM_RMS, il);
            
            // 2. Q/K/V projections + RoPE
            // ... (specific to your architecture)
            
            // 3. Attention
            cur = build_attn(inp_attn, model.layers[il].wo, NULL,
                           Qcur, Kcur, Vcur, NULL, NULL, NULL, 
                           kq_scale, il);
            
            // 4. Residual + FFN
            cur = ggml_add(ctx0, cur, inpL);
            // ... FFN via build_ffn() or build_moe_ffn()
            
            inpL = cur;
        }
        
        // Final norm + LM head
        cur = build_norm(inpL, model.output_norm, NULL, 
                        LLM_NORM_RMS, -1);
        res->t_embd = cur;
        cur = build_lora_mm(model.output, cur);
        res->t_logits = cur;
        
        ggml_build_forward_expand(gf, cur);
    }
};

同时,还需要在 src/models/models.h 中声明该构建器:

struct llm_build_my_model : public llm_graph_context {
    llm_build_my_model(const llama_model & model, 
                       const llm_graph_params & params);
};
flowchart TD
    subgraph "Toolkit methods you compose"
        A["build_inp_embd()"]
        B["build_inp_pos()"]
        C["build_norm(RMS/LayerNorm)"]
        D["build_lora_mm(Q/K/V projections)"]
        E["ggml_rope_ext(RoPE)"]
        F["build_attn(attention + KV cache)"]
        G["build_ffn(SiLU/GELU/ReLU)"]
        H["build_moe_ffn(MoE)"]
    end
    
    A --> C
    B --> E
    C --> D
    D --> E
    E --> F
    F --> C
    C --> G
    C --> H

第五步:注册与内存类型选择

还需要在 src/llama-model.cpp 中添加两处入口。

添加到 build_graph() — 在第 8288 行的 switch 语句中添加:

case LLM_ARCH_MY_MODEL:
    llm = std::make_unique<llm_build_my_model>(*this, params);
    break;

添加到 create_memory() — 如果你的模型是标准的 transformer,create_memory() 中的 default 分支已经能处理这种情况——它会自动创建 llama_kv_cache。只有在以下情形下才需要添加显式的 case:

  • 不需要缓存(BERT 类模型)→ 返回 nullptr
  • 循环状态(Mamba/RWKV)→ 使用 llama_memory_recurrent
  • 混合架构 → 使用 llama_memory_hybrid
  • 滑动窗口 → 如果 hparams.swa_type != NONE,会自动处理

最后,将新源文件添加到 src/CMakeLists.txt

add_library(llama
    # ... existing files ...
    models/mymodel.cpp
)

测试与贡献流程

构建完成后,按以下步骤验证实现是否正确:

1. 转换模型:

python convert_hf_to_gguf.py /path/to/hf-model --outfile model.gguf

2. 验证 tensor 形状: 使用 gguf-py 导出文件内容,检查所有 tensor 的形状是否正确:

python -m gguf.scripts.gguf_dump model.gguf

3. 运行推理:

./build/bin/llama-cli -m model.gguf -p "Hello, world!" -n 32

4. 对比输出: 用相同的 prompt 在 HuggingFace 参考实现上运行,并比较 logits 和输出结果。由于浮点运算顺序的差异,少量误差(< 0.01)属于正常现象;如果差异较大,则说明 tensor 映射或计算图拓扑存在问题。

5. 运行困惑度测试: 如需定量验证,可在标准基准上计算困惑度:

./build/bin/llama-perplexity -m model.gguf -f wiki.test.raw

将结果与 HuggingFace 模型的困惑度进行比较——F16 格式下,两者差距应在 0.1–0.5 以内;量化格式下误差会更大。

flowchart LR
    CONVERT["Convert model\nHF → GGUF"] --> VERIFY["Verify tensors\ngguf_dump"]
    VERIFY --> RUN["Run inference\nllama-cli"]
    RUN --> COMPARE["Compare outputs\nvs. HF reference"]
    COMPARE --> PPL["Perplexity test\nllama-perplexity"]
    PPL --> PR["Submit PR"]

贡献规范: 项目提供了 CONTRIBUTING.md,涵盖编码规范、PR 要求和审查流程。几个关键要点:

  • 添加新模型架构的 PR 应附上该模型的论文或文档链接
  • 转换器需正确处理 tokenizer 的转换
  • 新模型在提交前应通过基本的推理测试
  • 遵循现有代码风格——.clang-format.clang-tidy 配置文件会自动检查

提示: 添加新模型最快的方式,是找到与之最相似的现有架构,直接复制其转换器类、tensor 加载代码块和计算图构建器。对于大多数仅解码器(decoder-only)的 transformer 来说,LLaMA 的实现已经覆盖了 80% 的工作量——你主要需要调整的是归一化的位置、激活函数的类型,以及 attention 的变体。

系列总结

在这六篇文章中,我们完整地追踪了推理请求在 llama.cpp 中流转的全过程——从 GGUF 文件格式和 GGML tensor 操作,到计算图上下文工具集和架构分发,再到 KV cache 与解码流水线,最终延伸至 HTTP server 和 CLI 应用层。我们看到,一套精心设计的抽象机制(计算图上下文工具集、内存接口层次结构、backend vtable 系统),是如何支撑起 120 余种模型架构在十余种硬件 backend 上运行的。

仔细阅读 llama.cpp 的代码,会让你受益匪浅。它的设计将性能和可移植性置于首位,而非过度追求抽象,这意味着代码有时会比你预期的更为冗长——但每一个设计决策背后,都有其在多样化硬件上实现高效推理的深层考量。理解这些取舍,才是高效参与贡献的关键所在。