llama.cpp:一个人、一个周末、一个改变 AI 推理格局的 C++ 文件

发布于 2026年06月08日 13:18 #Models 原文链接

llama.cpp:一个人、一个周末、一个改变 AI 推理格局的 C++ 文件 封面图

2023 年 3 月,Meta 发布 LLaMA 模型权重后不到两周,一个保加利亚开发者 Georgi Gerganov 用一个周末的时间,写出了一个纯 C/C++ 的 LLaMA 推理实现。没有 Python,没有 PyTorch,没有任何重量级依赖——只有一个编译器和标准库。

这个项目叫 llama.cpp。

三年后的今天,它已经成为 GitHub 上 Star 数最多的 AI 项目之一,贡献者超过 3000 人,支持的模型架构从 LLaMA 扩展到了 Mistral、Qwen、Phi、Gemma 等几乎所有主流开源模型。Ollama、LM Studio、GPT4All 这些耳熟能详的工具,底层全部基于 llama.cpp。

一个 C++ 文件,凭什么改变整个 AI 推理的格局?

先搞清楚一个问题:为什么需要 llama.cpp

训练大模型用的是 PyTorch、JAX 这些框架,跑得飞快,但有个前提——你得有 GPU 集群。一块 A100 显卡大概 15 万人民币,跑一个 70B 参数的模型至少需要两块。普通开发者?想都别想。

llama.cpp 解决的是一个被大多数人忽略的问题:推理民主化

PyTorch 本质上是为训练设计的,推理只是它的副产品。这导致它携带了大量训练时才需要的 baggage:自动求导引擎、优化器状态管理、动态计算图……这些在推理时完全是废物,但它们占据了内存、拖慢了启动速度、增加了依赖复杂度。

llama.cpp 做了一件极其简单但极难的事:把推理这条路径单独拎出来,从零开始用 C/C++ 重写,去掉一切不需要的东西。

结果是什么?一个 70B 参数的模型,原本需要多卡 GPU,现在可以在一台 32GB 内存的 MacBook Pro 上跑起来。速度不快,但确实能跑。对于一个想在自己机器上实验 LLM 的开发者来说,这就是从 0 到 1 的区别。

核心架构:三层蛋糕

llama.cpp 的架构可以分成三层来理解:

第一层:GGML 张量库

GGML 是整个项目的地基。它是一个用纯 C 写的张量运算库,设计哲学非常明确:

懒执行 + 计算图。GGML 不像 PyTorch 那样立即执行每个操作,而是先把所有张量操作构建成一个有向无环图(DAG),然后一次性编译执行。这样做的好处是可以做全局优化——比如算子融合、内存复用——而不是在每个操作上独立决策。

具体来说,GGML 的工作流程是:

  1. 定义张量(tensor),包括形状、数据类型、存储位置
  2. 通过操作(op)把张量连接成计算图
  3. 调用 ggml_graph_compute 一次性执行整个图

这个设计让 GGML 在推理场景下特别高效。计算图只构建一次,之后每次推理都是重复执行同样的图结构。不需要动态图的开销,不需要 Python 解释器的调度延迟。

第二层:量化引擎

这是 llama.cpp 最核心的竞争力。先看一组数字:

精度70B 模型大小需要的内存
FP32(原始)~280 GB多卡 GPU 集群
FP16~140 GB2-4 张 A100
Q8_0(8 位量化)~70 GB1 张 A100 或大内存机器
Q4_K_M(4 位 K-量化)~40 GB一台 M2 Ultra Mac
Q2_K(2 位量化)~25 GB一台普通笔记本

量化是怎么做到的?

核心思想是分块量化。把权重分成小块(通常是 32 个权重一个块),每个块共享一个缩放因子(scale)。存储时只存量化后的整数和缩放因子,计算时再反量化回浮点数。

原始权重:[0.23, -0.15, 0.87, -0.42, ...](32 个 FP16 数)
     ↓ 量化
量化结果:[3, -2, 11, -5, ...](32 个 4-bit 整数)+ scale = 0.08
     ↓ 反量化(推理时)
恢复权重:[0.24, -0.16, 0.88, -0.40, ...](近似值)

但 llama.cpp 不止于此。它引入了 K-quant(K-量化) 方案,采用了两级层次结构:

  • 超级块(super-block):256 个权重
  • 子块(sub-block):每个超级块内 8 个子块,每块 32 个权重

不同子块可以使用不同的精度。关键权重用更高精度,不那么重要的权重用更低精度。这就是为什么 Q4_K_M 的 “M”(Medium)比 Q4_0 的质量好得多——同样的平均位宽,但精度分配更聪明。

再进一步是 imatrix(重要性矩阵)。它通过在校准数据上运行模型,统计每层激活值的二阶信息(近似 Fisher 信息矩阵),得到每个权重的重要性分数。量化时,重要权重获得更高有效精度。这让 Q4_K_M + imatrix 的效果可以逼近 FP16。

第三层:多后端执行

llama.cpp 从一开始就是 CPU-first 的设计,但它不局限于 CPU:

后端适用场景
CPU(AVX2/AVX-512/NEON)基础运行,所有平台
Apple MetalMac/iOS GPU 加速
CUDANVIDIA GPU
Vulkan跨平台 GPU
SYCLIntel GPU
OpenCL通用 GPU
WASM浏览器里跑模型

关键设计是后端抽象层。GGML 定义了统一的张量操作接口,不同后端只需要实现各自版本的 kernel。这意味着添加新硬件支持不需要改动上层推理逻辑。

CPU 端的 SIMD 优化尤其值得说。GGML 为 x86(AVX2/AVX-512)和 ARM(NEON)手写了大量量化/反量化的 intrinsic 代码。这些代码看起来很丑,但性能惊人。一个矩阵乘法的关键路径是:从量化格式反量化 → 执行 FP32 矩阵乘 → 写回结果。SIMD 指令让这个反量化和乘法可以流水线化,隐藏反量化的延迟。

GGUF 格式:一个文件装下整个模型

早期的 GGML 格式有个问题:权重和元数据是分开的,而且格式不够灵活。GGUF(GPT-Generated Unified Format)是它的继任者,用一个文件搞定一切。

GGUF 的文件结构很干净:

[GGUF magic (4 bytes)]
[version (uint32)]
[tensor count (uint64)]
[metadata KV count (uint64)]
[metadata KV pairs...]     ← 模型名称、架构、上下文长度、词表……
[tensor infos...]          ← 每个张量的名称、维度、类型、偏移
[padding/alignment]
[tensor data...]           ← 实际的量化权重

几个巧妙的设计:

内存映射加载(mmap)。GGUF 文件的张量数据是对齐的,可以直接通过 mmap() 映射到进程地址空间。操作系统按需加载页面,模型启动几乎瞬间完成——不需要把整个文件读进内存。对于 40GB 的模型文件,这意味着从点击到第一个 token 输出可能只需要几秒钟。

可扩展的元数据。键值对设计意味着任何人都可以添加新的元数据字段,不会破坏向后兼容性。这比硬编码格式灵活得多。

端序感知。GGUF 明确支持大小端,确保模型文件在不同架构间可移植。

内存管理:推理时零分配

这是一个容易被忽略但极其重要的设计决策。

在 llama.cpp 中,计算图和所有张量缓冲区在推理开始前就全部预分配好了。推理过程中不会发生任何动态内存分配。这意味着:

  1. 可预测的内存使用——不会因为意外的内存分配导致 OOM
  2. 零 GC 压力——没有垃圾回收,没有内存碎片
  3. 确定性的延迟——没有分配器锁竞争

对比 PyTorch 推理:即使开了 torch.no_grad(),PyTorch 仍然会为中间张量动态分配内存。在高并发场景下,这会导致显著的延迟波动。

llama.cpp 还支持 KV Cache 的量化。KV Cache 是推理时存储注意力机制中 Key 和 Value 的缓冲区,随着上下文长度线性增长。对它做 8-bit 甚至 4-bit 量化,可以把长上下文推理的内存占用再砍掉一半以上。

关键代码路径:一次推理的全过程

让我们跟踪一次完整的推理过程,看看 llama.cpp 内部发生了什么:

用户输入 "Hello, "

1. Tokenize(分词)
   "Hello, " → [15496, 29901]

2. 构建 Prompt 的计算图
   对每个 Transformer 层:
   - RMSNorm → Self-Attention → RMSNorm → FFN
   - 每步都是 GGML tensor op

3. ggml_graph_compute() 执行
   - 调度到 CPU/CUDA/Metal 后端
   - SIMD 加速的反量化 + 矩阵乘
   - 输出 logits 向量

4. 采样下一个 token
   logits → temperature → top-k/top-p → 采样
   得到 token id: 366 ("world")

5. 更新 KV Cache
   保存当前层的 K/V 到缓存

6. 回到第 2 步,用 "world" 作为新输入
   直到输出结束符或达到长度限制

每一步都是 C/C++ 的直接调用,没有 Python 解释器的开销,没有框架的调度延迟。这就是为什么 llama.cpp 在纯 CPU 上也能达到可以接受的推理速度。

对 AI 研究者的启发

拆解完技术细节,我想聊聊这个项目对做 AI 研究的人有什么启发。

1. 简单到极致就是竞争力

llama.cpp 的核心代码量远小于 PyTorch 的推理路径。但它做了一件事:把推理这条路径上的每一行代码都打磨到极致。没有抽象工厂模式,没有层层继承,没有为了扩展性预留的接口。就是直接的、手写的、面向硬件的计算代码。

这不是在推崇”过度工程”的反面。而是在说:当你对问题域有足够深的理解时,最简单的方案往往就是最好的方案。Georgi 理解 LLaMA 的推理路径就是一连串矩阵乘法和归一化操作,所以他不需要一个通用框架,只需要针对这些操作写最快的实现。

2. 量化不是”压缩”,是一种重新设计精度分配的思维方式

很多人把量化理解为”牺牲精度换空间”。但在 llama.cpp 的实践中,量化更像是一种重新思考精度分配的方式:

  • 不是所有权重都同等重要——imatrix 告诉我们哪些权重重要
  • 不是所有层都需要同样精度——不同层的量化策略可以不同
  • 不是所有推理阶段都需要全精度——prefill 和 decode 可以用不同精度

这种思路可以迁移到其他领域:在资源受限时,不要均匀分配资源,而是找到重要性分布,做非均匀分配

3. 内存带宽 > 计算能力

在推理场景下,瓶颈往往不是计算能力(FLOPS),而是内存带宽(memory bandwidth)。一个 70B 模型的单次推理需要从内存读取约 40GB 的权重数据,即使是最快的 CPU 内存带宽(~100 GB/s),这也需要 0.4 秒。

量化之所以有效,本质上是因为它减少了需要从内存传输的数据量。4-bit 量化意味着内存传输量减少到 FP16 的 1/4,即使反量化需要额外计算,整体还是更快。

这个洞察对模型设计也有影响:一个设计良好的小模型(如 Phi-3)可能比一个量化后的大模型更实用,因为小模型天然就有更好的内存带宽利用率。

4. 计算图思维的普适价值

GGML 的计算图设计——先构建 DAG,再一次性执行——不仅适用于 ML 推理。任何可以表示为 DAG 的计算任务都可以用这种模式:

  • 数据处理管道
  • 编译器的 IR 优化
  • 渲染引擎的 pass 调度

关键收益是全局优化。当你看到整个计算图时,你可以做跨操作的优化(如算子融合、内存复用),而这是逐步执行无法做到的。

5. 工程实现能力是研究者的超能力

llama.cpp 的成功部分归功于 Georgi 对底层硬件的理解:SIMD 指令集、缓存层次结构、内存对齐。这些”工程细节”在学术论文中很少被提及,但在实际系统中往往决定了方案的成败。

如果你的研究结果只能在大规模 GPU 集群上跑,它的落地场景就被限制在了少数大公司。但如果你能让它在普通笔记本上跑起来,你的研究就能触达每一个开发者。这就是工程实现能力的乘数效应。

实践建议

如果你想深入理解 llama.cpp,我建议按这个顺序阅读源码:

  1. ggml.h — 理解张量类型和操作定义
  2. ggml.c — 核心计算图的构建和执行逻辑
  3. ggml-quants.c — 各种量化格式的具体实现
  4. llama.cpp — 模型加载和推理的主逻辑
  5. gguf.c — GGUF 文件格式的解析代码

每个文件都是独立的、可理解的。没有跨 10 个文件的继承链,没有魔法宏展开。这在大型 C++ 项目中极其罕见。

如果你只是想用 llama.cpp 跑模型,Ollama 是最简单的入口。它的底层就是 llama.cpp,加了一层友好的 CLI 和 API。

写在最后

llama.cpp 的故事让我想起 Unix 哲学:做好一件事。在一个越来越复杂的 AI 工具生态中,有人选择用最简单的方式解决一个具体的问题,然后把这件事做到极致。

这个项目证明了一件事:你不需要一个庞大的框架来运行大模型。你需要的只是一个对问题足够深入的理解,和把每一行代码写到恰到好处的耐心。

这不是在说 PyTorch 不好。PyTorch 在训练和研究中依然是不可替代的。但在推理这个特定场景下,llama.cpp 展示了一种不同的可能性:当你把所有不必要的抽象都去掉之后,剩下的东西既简单又强大。

对于一个正在研究 AI 的人来说,这可能是最值得学习的工程思维。

评论互动

© 2026 王若风的技术博客 · Powered by Astro