本站内容由 AI 生成,可能存在错误。如发现问题,欢迎到 GitHub Issues 反馈。

计算图构建与架构分发

计算图构建与架构分发

更新于 2026-04-15

系列定位:本文是 llama.cpp 源码精读系列 #5。llama.cpp 的计算图基于 GGML 的惰性求值模型。如果你对这个模型还不熟悉,建议先阅读计算图与推理引擎。本文直接进入 build_graph() 的 C++ 实现。

每个 ubatch 被送入计算之前,需要先构建一张计算图(computation graph)——它描述了所有 tensor 操作及其依赖关系。llama.cpp 支持 125 种不同的模型架构,每种架构有自己的计算图拓扑,但它们共享一套通用的”积木”接口。


Part A:125 种架构,一个入口

llm_arch 枚举

src/llama-arch.h 定义了 llm_arch 枚举,列举了所有支持的模型架构:

enum llm_arch {
    LLM_ARCH_CLIP,
    LLM_ARCH_LLAMA,
    LLM_ARCH_LLAMA4,
    LLM_ARCH_FALCON,
    LLM_ARCH_GPT2,
    LLM_ARCH_GPTJ,
    LLM_ARCH_GPTNEOX,
    LLM_ARCH_BERT,
    LLM_ARCH_QWEN2,
    LLM_ARCH_GEMMA3,
    // ... 共 125 种已知架构 ...
    LLM_ARCH_UNKNOWN,
};

每种架构对应 src/models/ 目录下的一个独立实现文件(共 113 个 .cpp 文件),例如 llama.cppgpt2.cppqwen2.cpp 等。

build_graph():巨大的 switch 分发

所有架构汇聚到一个入口——llama_model::build_graph()(位于 src/llama-model.cpp):

ggml_cgraph * llama_model::build_graph(const llm_graph_params & params) const {
    std::unique_ptr<llm_graph_context> llm;

    switch (arch) {
        case LLM_ARCH_LLAMA:
            llm = std::make_unique<llm_build_llama<false>>(*this, params);
            break;
        case LLM_ARCH_GPT2:
            llm = std::make_unique<llm_build_gpt2>(*this, params);
            break;
        case LLM_ARCH_QWEN2:
            llm = std::make_unique<llm_build_qwen2>(*this, params);
            break;
        // ... 125 个 case ...
        default:
            GGML_ABORT("fatal error");
    }

    // 所有架构共享的后处理
    llm->build_pooling(cls, cls_b, cls_out, cls_out_b, cls_norm);
    llm->build_sampling();
    llm->build_dense_out(dense_2_out_layers, ...);

    llm->res->set_outputs();
    return llm->res->get_gf();
}

关键点:每个 case 创建一个架构专用的图构建器(如 llm_build_llamallm_build_gpt2),在其构造函数中完成计算图的构建。构建完成后,build_graph() 再统一追加 pooling、sampling 等后处理节点。

build_graph() 分发流程
build_graph()
llama_model 方法
switch (arch)
125 个 case 分支
llm_build_llama
构造函数建图
llm_build_gpt2
构造函数建图
... 其他架构
同样模式
后处理
pooling / sampling / dense_out
set_outputs() → get_gf()
返回计算图

Part B:llm_graph_context 积木接口

所有架构的图构建器都继承自 llm_graph_context 基类(定义在 src/llama-graph.h)。这个基类提供了一组可复用的”积木”方法。继承关系如下:

图构建器继承关系
llm_graph_context
基类:提供所有积木方法
llm_build_llama
Llama 系列
llm_build_gpt2
GPT-2
llm_build_qwen2
Qwen2 系列

核心积木方法

方法功能支持的变体
build_norm()归一化LLM_NORM(标准 LayerNorm)、LLM_NORM_RMS(RMSNorm)
build_attn()注意力KV cache、无 cache(encoder)、ISWA(滑动窗口)、cross-attention
build_ffn()前馈网络SiLU、GELU、ReLU 等激活函数;并行/顺序 gate
build_moe_ffn()MoE FFN支持 softmax/top-k gating,按需加载 expert

每种架构的图构建器在构造函数中调用这些积木方法,按照该架构的 transformer 结构组合成完整的计算图。积木方法通过枚举参数(如 LLM_NORM_RMSLLM_FFN_SILULLM_FFN_PAR)来选择变体,不同架构只需传不同的参数即可复用同一套实现。

积木方法与变体llm_graph_contextbuild_norm()LLM_NORM (LayerNorm)LLM_NORM_RMS (RMSNorm)build_attn()KV cacheEncoder (无 cache)ISWA (滑动窗口)Cross-attnbuild_ffn()SiLU / GELU / ReLU并行 gate / 顺序 gatebuild_moe_ffn()softmax / top-k gating按需加载 expert

Part C:实例对比——Llama vs GPT-2

通过对比两种经典架构的图构建,可以直观理解积木的组合方式。

Llama 架构

src/models/llama.cpp 中的 Llama 图构建器:

llm_build_llama::llm_build_llama(const llama_model & model,
                                  const llm_graph_params & params)
    : llm_graph_context(params) {

    // 输入层
    inpL = build_inp_embd(model.tok_embd);
    auto * inp_pos  = build_inp_pos();
    auto * inp_attn = build_attn_inp_kv();  // KV cache 注意力

    for (int il = 0; il < n_layer; ++il) {
        // 注意力分支
        cur = build_norm(inpL, model.layers[il].attn_norm, NULL,
                         LLM_NORM_RMS, il);                    // ← RMSNorm

        Qcur = build_lora_mm(model.layers[il].wq, cur, ...);  // Q = Wx + LoRA
        Kcur = build_lora_mm(model.layers[il].wk, cur, ...);
        Vcur = build_lora_mm(model.layers[il].wv, cur, ...);

        Qcur = ggml_rope_ext(ctx0, Qcur, inp_pos, ...);       // ← RoPE 位置编码
        Kcur = ggml_rope_ext(ctx0, Kcur, inp_pos, ...);

        cur = build_attn(inp_attn, model.layers[il].wo, NULL,
                         Qcur, Kcur, Vcur, ..., kq_scale, il);

        // 残差连接 + FFN
        ffn_inp = ggml_add(ctx0, cur, inpL);
        cur = build_norm(ffn_inp, model.layers[il].ffn_norm, NULL,
                         LLM_NORM_RMS, il);

        if (model.layers[il].ffn_gate_inp == nullptr) {
            cur = build_ffn(cur, ..., LLM_FFN_SILU, LLM_FFN_PAR, il);  // ← SiLU + 并行 gate
        } else {
            cur = build_moe_ffn(cur, ..., LLM_FFN_SILU, ...);  // MoE 变体
        }

        inpL = ggml_add(ctx0, cur, ffn_inp);  // 残差
    }

    // 输出
    cur = build_norm(inpL, model.output_norm, NULL, LLM_NORM_RMS, -1);
    cur = build_lora_mm(model.output, cur);   // logits
    res->t_logits = cur;
    ggml_build_forward_expand(gf, cur);
}

Llama 的特征:RMSNorm 归一化、RoPE 旋转位置编码(应用于 Q 和 K)、分别投影 Q/K/V(三次矩阵乘法)、SiLU + 并行 gate 的 FFN,以及支持 MoE 变体。

GPT-2 架构

src/models/gpt2.cpp 中的 GPT-2 图构建器——同样的积木,不同的组合:

llm_build_gpt2::llm_build_gpt2(const llama_model & model,
                                const llm_graph_params & params)
    : llm_graph_context(params) {

    inpL = build_inp_embd(model.tok_embd);
    auto * inp_pos = build_inp_pos();

    // GPT-2 用可学习的位置 embedding,不用 RoPE
    pos = ggml_get_rows(ctx0, model.pos_embd, inp_pos);
    inpL = ggml_add(ctx0, inpL, pos);

    auto * inp_attn = build_attn_inp_kv();

    for (int il = 0; il < n_layer; ++il) {
        cur = build_norm(inpL, model.layers[il].attn_norm,
                         model.layers[il].attn_norm_b,
                         LLM_NORM, il);            // ← 标准 LayerNorm(非 RMSNorm)

        // GPT-2 用合并的 QKV 投影(一次矩阵乘法)
        cur = build_lora_mm(model.layers[il].wqkv, cur);
        cur = ggml_add(ctx0, cur, model.layers[il].bqkv);
        // 用 view 把 QKV 拆出来(零拷贝)
        Qcur = ggml_view_3d(ctx0, cur, n_embd_head, n_head, n_tokens, ...);
        Kcur = ggml_view_3d(ctx0, cur, ...);
        Vcur = ggml_view_3d(ctx0, cur, ...);

        cur = build_attn(inp_attn, ..., 1.0f/sqrtf(float(n_embd_head)), il);

        ffn_inp = ggml_add(ctx0, cur, inpL);
        cur = build_norm(ffn_inp, ..., LLM_NORM, il);  // ← 标准 LayerNorm

        cur = build_ffn(cur, ..., LLM_FFN_GELU, LLM_FFN_SEQ, il);  // ← GELU + 顺序 gate

        inpL = ggml_add(ctx0, cur, ffn_inp);
    }

    cur = build_norm(inpL, model.output_norm, model.output_norm_b, LLM_NORM, -1);
    cur = build_lora_mm(model.output, cur);
    res->t_logits = cur;
    ggml_build_forward_expand(gf, cur);
}

GPT-2 的特征:标准 LayerNorm(带偏置参数)、可学习位置 embedding(直接加到输入上)、合并 QKV 一次投影(用 ggml_view_3d 零拷贝拆分)、GELU + 顺序 FFN。

关键差异

特性LlamaGPT-2
归一化RMSNorm标准 LayerNorm
位置编码RoPE(旋转位置编码)可学习位置 embedding
QKV 投影分别投影 Q、K、V合并 QKV 一次投影
FFN 激活SiLU + 并行 gateGELU + 顺序
偏置通常无偏置有偏置

但从代码结构来看,两者的框架完全一致:输入 → 层循环(norm → attn → residual → norm → ffn → residual)→ 输出。这就是积木设计的威力。

Llama vs GPT-2 层结构实线高亮 = 架构差异点,虚线 = 相同步骤LlamaRMSNormQ / K / V 分别投影RoPE (Q, K)Attention + ResidualRMSNormSiLU + Parallel Gate FFNResidualGPT-2LayerNorm (+ bias)合并 QKV 一次投影 + view 拆分可学习位置 embeddingAttention + ResidualLayerNorm (+ bias)GELU + Sequential FFNResidual

交互对比

下面的交互组件并排展示了 Llama 和 GPT-2 的构建流程,高亮显示两者的差异步骤:

架构对比:Llama vs GPT-2

Llama
输入阶段
build_inp_embd
token → embedding
build_inp_pos
RoPE 位置输入
层循环(x N)
build_norm (RMSNorm)
注意力前归一化
分别 Q/K/V 投影
build_lora_mm × 3
RoPE 旋转编码
ggml_rope_ext(Q, K)
build_attn
注意力计算 + 输出投影
残差连接
cur + inpL
build_norm (RMSNorm)
FFN 前归一化
SiLU FFN (并行 gate)
build_ffn / build_moe_ffn
残差连接
cur + ffn_inp
输出阶段
build_norm (RMSNorm)
最终归一化
build_lora_mm → logits
输出投影
GPT-2
输入阶段
build_inp_embd
token → embedding
build_inp_pos + 加法
可学习位置 embedding 叠加
层循环(x N)
build_norm (LayerNorm)
注意力前归一化
合并 QKV 一次投影
build_lora_mm + view 拆分
(无 RoPE)
位置已在 embedding 中编码
build_attn
注意力计算 + 输出投影
残差连接
cur + inpL
build_norm (LayerNorm)
FFN 前归一化
GELU FFN (顺序)
build_ffn (LLM_FFN_SEQ)
残差连接
cur + ffn_inp
输出阶段
build_norm (LayerNorm)
最终归一化
build_lora_mm → logits
输出投影
相同不同
关键差异对比
特性LlamaGPT-2
归一化RMSNorm标准 LayerNorm
位置编码RoPE(旋转位置编码)可学习位置 embedding
QKV 投影分别投影 Q、K、V合并 QKV 一次投影
FFN 激活SiLU + 并行 gateGELU + 顺序
偏置通常无偏置有偏置
尽管积木选择不同,两者共享相同的宏观框架:输入 → 层循环(norm → attn → residual → norm → ffn → residual)→ 输出。

Part D:图复用机制

构建计算图有一定开销(创建 ggml tensor 对象、建立依赖关系),尤其在自回归解码阶段,每个 ubatch 可能只包含几个 token,图拓扑完全一样。llama.cpp 通过图复用机制来避免重复构建。

process_ubatch() 中的复用逻辑

process_ubatch() 中:

// src/llama-context.cpp(简化)
llm_graph_result * llama_context::process_ubatch(
        const llama_ubatch & ubatch, ...) {

    auto * res = gf_res_prev.get();   // 上一次的图结果
    const auto gparams = graph_params(res, ubatch, mctx, gtype);

    if (!graph_reuse_disable && res->can_reuse(gparams)) {
        // 图拓扑不变,直接复用!只需更新输入数据
        n_reused++;
    } else {
        // 需要重建图
        res->reset();
        ggml_backend_sched_reset(sched.get());
        gf = model.build_graph(gparams);           // 重新构建
        ggml_backend_sched_alloc_graph(sched.get(), gf);  // 重新分配
    }

    res->set_inputs(&ubatch);  // 无论复用与否,都要设置新的输入数据
    graph_compute(res->get_gf(), ubatch.n_tokens > 1);  // 执行
    return res;
}

复用条件

can_reuse() 检查的条件包括:

  • ubatch 形状相同n_tokensn_seq_tokensn_seqs 一致
  • 输入模式相同:都是 token 或都是 embedding
  • 序列 ID 不变:在 equal_seqs 模式下
  • 输出数量相同
  • 模型配置不变causal_attnembeddings、LoRA 等

在典型的自回归解码中(每次生成 1 个 token),连续的 ubatch 通常形状完全一致,因此图复用率非常高。

图复用条件can_reuse() 检查项n_tokensubatch token 数相同n_seq_tokens每序列 token 数相同n_seqs序列数相同input_mode输入模式一致 (token/embd)output_count输出数量相同config模型配置不变 (attn/LoRA)全部通过 → 复用跳过 build_graph()任一失败 → 重建build_graph() + alloc
图复用判定流程
新 ubatch 到达
process_ubatch()
can_reuse()?
检查形状/模式/配置
复用旧图
跳过 build_graph
重建图
build_graph() + alloc
set_inputs()
设置新输入数据
graph_compute()
执行计算

小结

本文走完了 llama.cpp 计算图构建的完整路径:

  1. 一个入口build_graph() 通过巨大的 switch 将 125 种架构分发到各自的图构建器
  2. 共享积木:所有图构建器继承 llm_graph_context,复用 build_norm/build_attn/build_ffn 等方法
  3. 灵活组合:不同架构通过传递不同枚举参数选择归一化类型、激活函数、投影方式等变体
  4. 图复用:自回归解码阶段,形状不变的 ubatch 可以跳过图构建,直接复用上次的计算图

下一篇将进入 #6 后端调度与内存管理,探索构建好的计算图如何被分配到 CPU/GPU 后端并执行。