Read OSS

llama.cpp 架构:代码库导览地图

中级

前置知识

  • 基本的 C/C++ 知识(指针、类、虚函数派发)
  • 对大语言模型的基本了解,知道它是如何生成文本的

llama.cpp 架构:代码库导览地图

llama.cpp 是 LLM 生态中影响力最深远的开源项目之一。它能让你在消费级硬件上运行大语言模型——从 MacBook 到树莓派,模型权重可量化到 2–4 bits。然而,面对 55,000 余行核心库代码、超过 120 种模型架构,初次进入这个代码库,很容易像在陌生城市里迷路一样无从下手。

这篇文章就是你的导览地图。我们将建立一套高效浏览代码库所需的心智模型:双库架构、目录布局、C API 外观模式、贯穿整个推理生命周期的三个核心类型,以及当你调用库生成一个 token 时内部究竟发生了什么。

双库架构

llama.cpp 并非铁板一块的单体代码库,而是由两个独立的库加上一层工具层构成:

graph TD
    subgraph "User-facing tools"
        CLI["tools/cli"]
        SRV["tools/server"]
        QUANT["tools/quantize"]
    end

    subgraph "Shared utilities"
        COMMON["common/"]
    end

    subgraph "Core libraries"
        LLAMA["libllama (src/)"]
        GGML["GGML (ggml/)"]
    end

    CLI --> COMMON
    SRV --> COMMON
    QUANT --> COMMON
    COMMON --> LLAMA
    LLAMA --> GGML

GGMLggml/)是一个通用张量计算库,本身对语言模型一无所知。它提供张量类型、惰性计算图、量化数据格式,以及面向 CPU、GPU 和各类加速器的可插拔 backend 系统。可以把它理解为 PyTorch 张量层的精简替代品,专为推理场景优化。

libllamasrc/)是面向 LLM 的专用库。它理解模型架构、tokenization、KV cache 和采样策略,并借助 GGML 为 120 余种模型架构(从 LLaMA 到 Mamba 再到 RWKV)构建和执行计算图。

common/ 是一层工具库,仅供命令行工具(server、CLI、quantize 等)使用,负责参数解析、聊天模板应用以及高层采样封装。它不属于公开库 API 的一部分。

这种分层设计意义重大。GGML 可以独立使用——whisper.cpp 等项目也共享它。libllama 与 common/ 之间清晰的边界意味着:如果你要把 llama.cpp 嵌入自己的应用,只需链接 libllamaggml,完全不必关心 common/

提示: 依赖方向是严格单向的:tools → common → libllama → GGML。如果你看到违反这一方向的 #include,那就是一个 bug。

目录结构一览

下表列出了每个顶层目录及其用途:

目录 用途
include/ 公开 C API 头文件(llama.hllama-cpp.h
src/ libllama 核心实现(C++ 内部)
src/models/ 每个模型架构对应一个 .cpp 文件(约 110 个)
ggml/ GGML 张量库(include/、src/、backends/)
common/ 面向 CLI 工具的共享工具库
tools/ 用户可用的二进制工具(server、cli、quantize、bench 等)
gguf-py/ 用于读写 GGUF 文件的 Python 库
tests/ 单元测试与集成测试
convert_hf_to_gguf.py 从 HuggingFace 格式转换模型的 Python 脚本

src/CMakeLists.txt 是 libllama 最权威的文件索引。库中的每一个源文件都在这里列出——从 20 余个核心 llama-*.cpp 模块,到 models/ 下 110 余个模型架构文件,一目了然。

flowchart LR
    subgraph "src/CMakeLists.txt lists all files"
        CORE["Core modules\nllama-context.cpp\nllama-model.cpp\nllama-graph.cpp\n...20+ files"]
        MODELS["Model architectures\nmodels/llama.cpp\nmodels/qwen2.cpp\nmodels/mamba.cpp\n...110+ files"]
    end
    CORE -.-> MODELS

src/ 中的命名规范非常一致:每个核心模块都遵循 llama-{module}.h / llama-{module}.cpp 的形式。例如,llama-batch.h 负责批处理逻辑,llama-kv-cache.h 定义 KV cache,llama-sampler.h 定义采样链。有了这个规律,搜索特定功能会轻松许多。

C API 外观模式

llama.cpp 通过 include/llama.h 对外暴露纯 C API。头文件声明了不透明结构体类型和自由函数,统一遵循 llama_* 的命名规范:

struct llama_model;     // opaque
struct llama_context;   // opaque
struct llama_sampler;   // opaque

具体实现位于 src/llama.cpp,这个文件充当薄薄的委托层——每个公开的 llama_* 函数都只是将调用转发给内部 C++ 类的对应方法。例如,llama_decode() 委托给 llama_context::decode()

classDiagram
    class llama_h["llama.h (C API)"] {
        <<header>>
        llama_model_load_from_file()
        llama_init_from_model()
        llama_decode()
        llama_sampler_sample()
    }
    class llama_cpp["llama.cpp (Facade)"] {
        <<delegation>>
        Forwards to C++ classes
    }
    class llama_model["llama_model (C++)"] {
        load_hparams()
        load_tensors()
        build_graph()
        create_memory()
    }
    class llama_context_impl["llama_context (C++)"] {
        decode()
        encode()
        process_ubatch()
    }
    llama_h --> llama_cpp : "declares"
    llama_cpp --> llama_model : "delegates"
    llama_cpp --> llama_context_impl : "delegates"

为什么要用 C API?原因有三。其一,C 拥有稳定的 ABI——升级 libllama 无需重新编译调用方,任何语言(Python、Rust、Go、C#)都可以直接绑定。其二,它划定了清晰的边界:C++ 的复杂性(模板、虚函数派发、RAII)全部封装在内部。其三,它避免了头文件泄漏——用户只需要 llama.h 和 GGML 头文件,而不必接触内部的 20 余个 llama-*.h 文件。

提示: 当你想查看 llama_decode() 这类公开函数的实现时,先从 src/llama.cpp 入手——它会告诉你实际完成工作的 C++ 类和方法在哪里。

三个核心类型

llama.cpp 的一切都围绕三个类型展开,它们声明在 include/llama.h 中:

llama_model 代表一个已加载的模型,包含架构元数据(超参数)、词表以及分配在 backend 缓冲区中的权重张量。模型加载完成后即为不可变状态,可以在多个 context 之间共享。其内部结构定义于 src/llama-model.h,存储了架构枚举、超参数、每层的张量指针,以及用于 offloading 的设备列表。

llama_context 代表一次推理会话,持有 KV cache(或其他内存类型)、计算缓冲区、backend 调度器,以及批大小、线程数等运行时参数。你从一个模型创建 context,所有可变状态都存放于此。其定义位于 src/llama-context.h

llama_sampler 代表一条 token 选择流水线。采样器是可组合的——你可以将温度缩放、top-k、top-p、重复惩罚等策略串联成链。采样链定义于 src/llama-sampler.h

classDiagram
    class llama_model {
        +arch: llm_arch
        +hparams: llama_hparams
        +vocab: llama_vocab
        +layers: vector~llama_layer~
        +tok_embd: ggml_tensor*
        +output: ggml_tensor*
        +build_graph()
        +create_memory()
    }
    class llama_context {
        +model: const llama_model&
        +memory: llama_memory_i*
        +sched: ggml_backend_sched_t
        +decode()
        +process_ubatch()
    }
    class llama_sampler_chain {
        +samplers: vector~info~
        +cur: vector~llama_token_data~
    }
    llama_model "1" <-- "*" llama_context : "references (immutable)"
    llama_context "1" --> "1" llama_sampler_chain : "uses"

所有权关系至关重要:模型的生命周期比所有 context 都长。多个 context 可以共享同一个模型(非常适合并行推理,同时各自维护独立的 KV cache)。context 持有的是对模型的 const 引用,绝不会修改权重。

推理生命周期全流程

下面是生成单个 token 的完整流程,从模型加载到最终输出:

sequenceDiagram
    participant App as Application
    participant API as llama.h
    participant Model as llama_model
    participant Ctx as llama_context
    participant GGML as GGML Backend

    App->>API: llama_model_load_from_file()
    API->>Model: load_hparams(), load_tensors()
    Model-->>App: llama_model*

    App->>API: llama_init_from_model()
    API->>Ctx: construct(model, params)
    Ctx->>Model: create_memory() → KV cache
    Ctx-->>App: llama_context*

    App->>API: llama_tokenize("Hello")
    API-->>App: [token_ids]

    App->>API: llama_decode(batch)
    API->>Ctx: decode(batch)
    Ctx->>Ctx: balloc->init(batch)
    Ctx->>Ctx: memory->init_batch()
    loop For each ubatch
        Ctx->>Model: build_graph(params)
        Model->>GGML: ggml_backend_sched_alloc_graph()
        Ctx->>GGML: set_inputs() + graph_compute()
        GGML-->>Ctx: logits
    end
    Ctx-->>App: logits ready

    App->>API: llama_sampler_sample()
    API-->>App: next_token

    App->>API: llama_detokenize()
    API-->>App: "world"

第一步:加载模型。 llama_model_load_from_file() 读取 GGUF 文件,从元数据中解析架构,加载超参数,将张量名称映射到内部 llama_layer 结构,并将权重数据分配到 backend 缓冲区(CPU、GPU 或两者兼有)。

第二步:创建 context。 llama_init_from_model() 构造一个 llama_context。这一步会调用模型上的 create_memory() 来选择合适的内存实现——transformer 使用 KV cache,Mamba/RWKV 使用循环状态,或者两者混合。同时还会根据最坏情况的计算图估算预留计算缓冲区。

第三步:Tokenize。 llama_tokenize() 使用模型内嵌的词表将文本转换为 token ID。词表类型(BPE、SentencePiece、WordPiece 等)决定了具体使用的算法。

第四步:Decode。 llama_decode() 是真正干活的地方。batch 会被校验、拆分成若干 micro-batch(ubatch),每个 ubatch 依次经过 process_ubatch() 处理:构建计算图 → 分配 backend 内存 → 设置输入张量 → 执行计算 → 提取 logits。

第五步:采样。 llama_sampler_sample() 接收上一次 decode 调用输出的 logits,经过采样链处理后选出下一个 token。

第六步:Detokenize。 将选出的 token 转换回文本。

代码导航技巧

当你需要在代码库中定位某个功能时,以下策略能帮你节省大量时间:

src/CMakeLists.txt 入手。 它是库中所有文件的完整索引,搜索这个文件往往比递归 grep 更高效。

利用 llama-*.h 命名规律。 想了解批处理系统?打开 llama-batch.h。KV cache?看 llama-kv-cache.h。所有 20 余个核心模块都遵循这个规律。

查找模型架构。 每个模型在 src/models/ 下都有一个对应文件,文件名与 GGUF 的架构名一致:LLaMA → models/llama.cpp,Qwen2 → models/qwen2.cpp,Mamba → models/mamba.cpp。如果不确定文件名,可以在 src/llama-arch.cpp 中搜索 LLM_ARCH_NAMES 映射表——它将 GGUF 字符串标识符翻译为枚举值。

追踪公开 API 调用。src/llama.cpp 找到对应函数,查看它委托给了哪个 C++ 类,再顺着调用链进入具体的模块文件。

你想找的内容 去哪里找
公开 API 接口 include/llama.h
API 委托层 src/llama.cpp
模型加载 / 架构分发 src/llama-model.cpp
Context 构建 / decode 循环 src/llama-context.cpp
计算图构建工具包 src/llama-graph.h
特定模型实现 src/models/{name}.cpp
架构枚举 / GGUF 键名 src/llama-arch.hsrc/llama-arch.cpp
KV cache 内部实现 src/llama-kv-cache.hsrc/llama-kv-cache.cpp
GGML 张量操作 ggml/include/ggml.h
Backend 系统 ggml/src/ggml-backend-impl.h

提示: models/llama.cpp 是大多数其他模型架构参照的参考实现。如果你想搞清楚模型构建器的运作方式,从这个文件读起是最好的选择。

下一步

现在你已经对代码库的整体结构有了清晰的认识。下一篇文章将深入 llama.cpp 设计的核心:模型架构是如何转化为 GGML 计算图的。我们会逐一剖析 llm_graph_context 工具包,逐行追踪 LLaMA 图构建器的实现,并探究包括 Mamba、RWKV 在内的 120 余种架构是如何在同一个统一框架下协同运作的。