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