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 的工作流程是:
- 定义张量(tensor),包括形状、数据类型、存储位置
- 通过操作(op)把张量连接成计算图
- 调用
ggml_graph_compute一次性执行整个图
这个设计让 GGML 在推理场景下特别高效。计算图只构建一次,之后每次推理都是重复执行同样的图结构。不需要动态图的开销,不需要 Python 解释器的调度延迟。
第二层:量化引擎
这是 llama.cpp 最核心的竞争力。先看一组数字:
| 精度 | 70B 模型大小 | 需要的内存 |
|---|---|---|
| FP32(原始) | ~280 GB | 多卡 GPU 集群 |
| FP16 | ~140 GB | 2-4 张 A100 |
| Q8_0(8 位量化) | ~70 GB | 1 张 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 Metal | Mac/iOS GPU 加速 |
| CUDA | NVIDIA GPU |
| Vulkan | 跨平台 GPU |
| SYCL | Intel 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 中,计算图和所有张量缓冲区在推理开始前就全部预分配好了。推理过程中不会发生任何动态内存分配。这意味着:
- 可预测的内存使用——不会因为意外的内存分配导致 OOM
- 零 GC 压力——没有垃圾回收,没有内存碎片
- 确定性的延迟——没有分配器锁竞争
对比 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,我建议按这个顺序阅读源码:
ggml.h— 理解张量类型和操作定义ggml.c— 核心计算图的构建和执行逻辑ggml-quants.c— 各种量化格式的具体实现llama.cpp— 模型加载和推理的主逻辑gguf.c— GGUF 文件格式的解析代码
每个文件都是独立的、可理解的。没有跨 10 个文件的继承链,没有魔法宏展开。这在大型 C++ 项目中极其罕见。
如果你只是想用 llama.cpp 跑模型,Ollama 是最简单的入口。它的底层就是 llama.cpp,加了一层友好的 CLI 和 API。
写在最后
llama.cpp 的故事让我想起 Unix 哲学:做好一件事。在一个越来越复杂的 AI 工具生态中,有人选择用最简单的方式解决一个具体的问题,然后把这件事做到极致。
这个项目证明了一件事:你不需要一个庞大的框架来运行大模型。你需要的只是一个对问题足够深入的理解,和把每一行代码写到恰到好处的耐心。
这不是在说 PyTorch 不好。PyTorch 在训练和研究中依然是不可替代的。但在推理这个特定场景下,llama.cpp 展示了一种不同的可能性:当你把所有不必要的抽象都去掉之后,剩下的东西既简单又强大。
对于一个正在研究 AI 的人来说,这可能是最值得学习的工程思维。
评论互动