🦙 llama.cpp 图解教程llama.cpp Visual Guide 第一部分 · 宏观全景Part 1 · The Big Picture 01 / 40
第一部分 · 宏观全景Part 1 · The Big Picture

llama.cpp 是什么What is llama.cpp

llama.cpp 是一个用纯 C/C++ 写的大模型推理引擎:把已经训练好的大语言模型 (以 GGUF 格式存放)高效地跑起来,在普通 CPU、甚至手机上也能推理, 有 GPU 就更快。它不训练模型,只专注"把模型跑出字来"。一个可执行文件加一个 .gguf 文件,就能在本地、离线、低成本地和大模型对话——这就是它最迷人的地方。

🔌 生活类比
把训练好的大模型想成一张乐谱(权重)。PyTorch 像录音棚:能作曲、能录、设备重。 llama.cpp 像一台便携播放器:不作曲,只把乐谱高保真地播放出来,还特别省电、到处能用。

它到底解决什么问题

研究界的模型大多用 Python + PyTorch,依赖重、显存吃紧、难以部署到普通设备:想在自己的笔记本上跑一个 7B 模型,常被"装不上环境""显存不够""要下载几十 GB 权重"挡在门外。llama.cpp 的目标正相反——把推理这一件事做到极致地

🌍 宏观理解
零外部依赖的 C/C++ + 量化(把权重压成 4/5/8 bit 等低位宽,K-quant 甚至能到 2/3/6 bit)+ 自研张量引擎 ggml, 让大模型能在消费级硬件上本地、离线、低成本地推理。不需要 Python、不需要 CUDA 工具链、不需要联网, 一个可执行文件 + 一个 .gguf 文件即可运行。

研究界常见栈

Python + PyTorch · 依赖重 · 吃显存 · 常要 CUDA · 难部署到普通设备

llama.cpp

零依赖 C/C++ · 量化省显存 · CPU 也能跑 · 离线 · 一个可执行文件 + 一个 .gguf

为什么偏偏是 C/C++,而不是 Python、Rust 或 Go?推理引擎最在意两件事——零运行时可嵌入, 而 C/C++ 恰好两样都占。它直接编译成机器码,不背 Python 解释器、也没有 Go/Java 那种垃圾回收和虚拟机运行时,启动即算、延迟可控; 它能静态链接成一个独立可执行文件,拷到另一台同架构机器上、不装任何依赖就能跑。更关键的是几乎所有语言都能通过 C ABI 调用它,于是这套引擎可以被嵌进桌面 App、安卓/iOS、浏览器(编译成 WASM)甚至嵌入式设备, 这是重运行时语言很难做到的。Rust 其实也能达成类似目标,但项目起步时 C/C++ 的编译器与各家 GPU 工具链最成熟、移植阻力最小。 官方 README 开宗明义就把目标写成"以最小的依赖实现顶尖性能",纯 C/C++ 正是这句话的支点。

三大支柱:GGUF · 量化 · ggml

llama.cpp 能"一个文件到处跑",靠的是三块拼在一起的基石。这里先各看一眼,后面每一块都会有专门的课展开:

① GGUF:一个文件装下整个模型

GGUF 是 llama.cpp 的模型文件格式:把权重、超参数(层数、维度、词表大小)、 分词器、聊天模板等全部打包进一个文件。加载时直接 mmap 进内存,不再需要额外的配置文件或 Python 代码——拿到一个 .gguf,引擎就知道"这是什么模型、该怎么跑"。

"单文件"到底好在哪?传统做法要同时凑齐权重分片、config.jsontokenizer.json、 生成配置等一堆文件,少一个就跑不起来、版本错一个就对不上;GGUF 把它们全焊进一个文件,于是免配置—— 引擎读文件头里以键值对存放的元数据(超参、词表、chat 模板)就知道该怎么跑。加载时用 mmap 把文件 按需映射进内存,用到哪一页操作系统才读哪一页,启动快、还能让多个进程共享同一份只读权重省内存。 最实在的好处是换模型只换一个文件:把 .gguf 一换、命令行参数原封不动,跑的就是另一个模型了,分发和管理都简单到极点。

② 量化:把权重压小,精度几乎不掉

原始权重通常是 16 bit 浮点(FP16),一个 7B 模型就要约 14 GB。量化把每个权重压成更低的位宽 (如 4 bit),体积直接降到约 1/4,普通笔记本的内存也装得下。代价是一点点精度损失,但靠"按块共享缩放" 的设计,损失小到几乎察觉不到:

FP16 原始权重:每个数 16 bit,精度高但占空间
0.12-0.340.080.51 一块 32 个 × 16 bit
Q4_0 量化后:整块共享 1 个 scale,每个权重只存 4 bit 档位;反量化 = scale × (码值 − 8)
scale× 0110100100111100 ≈ 4.5 bit/权重,约 1/4 大小

为什么能"既省又几乎不掉质量"?三件事叠在一起:其一,模型里权重的数量远多于运行时的激活值, 把权重压成低位宽,省下的空间最多、对计算精度的牵连却最小;其二,"预测下一个词"看的是各候选 logits 的相对高低, 对单个权重的微小误差并不敏感,低位宽带来的抖动大多被淹没;其三,按块共享 scale 让每一小块都贴合自己那段数值的范围, 牢牢保住动态范围。代价确实存在——一点点质量损失,但可用重要性矩阵把更关键的权重保留得更准,把损失再压下去。

把这套压缩放进真实场景,就是一条"从重到轻"的流水线:同一个模型,量化后体积骤降,再配上一个可执行文件,就能从数据中心搬到你的笔记本上:

FP16 模型
7B ≈ 14 GB
->
量化 Q4
≈ 4 GB
->
.gguf 单文件
+ 一个可执行文件
->
本地跑
笔记本 / 手机 / 服务器

③ ggml:底层的张量计算引擎

ggml 是 llama.cpp 自研的张量库:定义张量、把一次推理描述成计算图, 再把图里的算子(矩阵乘、softmax、rope……)派发到不同后端(CPU 的 SIMD、CUDA、Metal、Vulkan……)真正算出来。 它把“描述运算”和“在硬件上执行”分开,于是同一套模型代码不改,就能跑遍 CPU 和各种 GPU。

整体结构图:四层自底向上

把上面三块支柱按"谁依赖谁"摞起来,llama.cpp 就是一座清晰的四层塔,从底层硬件一路往上到用户工具:

工具tools/ · examples/
面向用户:llama-cli 命令行、llama-server HTTP 服务、llama-quantize 量化器
推理src/llama-*
模型加载 · 计算图 · KV cache · 采样 · 分词 · 聊天模板(把"模型"变成"会话")
引擎ggml
张量 · 计算图 · 算子(matmul/rope/softmax…)· 后端调度 · 量化格式
后端CPU · CUDA · Metal · Vulkan …
把算子真正算在硬件上(SIMD / GPU kernel)

读源码时记住这条线:后端提供算力,ggml 把计算组织成图,src/llama-* 把图拼成"会话级"的推理逻辑,最外面的 tools/ 才是你直接敲的命令。下一课会把这四层对应到具体目录。

方向上有个对称的美感:你的请求自上而下穿过四层(工具收到提示词 -> 推理层组织成计算图 -> ggml 安排算子 -> 后端落到硬件), 算出的结果再自下而上冒回来、最终变成屏幕上的文字。读源码时若一时迷路,回到这张图、先认清"自己正站在哪一层",往往就不慌了。

训练 vs 推理:llama.cpp 站在哪一边

一个大模型的一生分两段:先训练(把它教会),再推理(用它干活)。这是需求完全不同的两件事:

🏋️ 训练(PyTorch 等)

  • 前向 + 反向传播、算梯度、更新权重
  • 优化器状态,显存吃紧(常需多卡)
  • Python 生态,依赖重
  • 目标:把模型练出来

⚡ 推理(llama.cpp)

  • 只前向:权重固定,算一遍出 logits
  • 量化压显存,CPU 也能跑
  • 纯 C/C++,几乎零依赖
  • 目标:把模型跑出字

llama.cpp 只做推理这一半。正因为不必支持反向传播和优化器,它能彻底丢掉训练框架的重依赖, 把整个引擎压成一份纯 C/C++ 代码——这正是它能在你电脑上轻装跑起来的前提。模型怎么"练出来"不归它管, 那是 PyTorch 等训练框架的活。

"只前向"具体意味着什么?训练时为了在反向传播里回算梯度,要把前向过程中每一层的中间结果都留着, 还要给每个权重维护优化器状态(如 Adam 的一阶、二阶动量),算下来显存常是权重本身的好几倍。推理把这些全砍掉了: 没有反向、没有梯度、没有优化器状态,权重在加载后只读、不再变化,显存里实质上只剩两样东西——权重KV cache。 正因为权重只读,才可以放心地把它量化成低位宽而不必担心影响训练;也正因为卸掉了训练那套重负担,整件事才轻到 能在一颗普通 CPU 上跑起来。这就是"只做推理"换来的全部底气。

和 PyTorch / transformers / vLLM 的区别

同样和大模型打交道,这几个项目其实站在不同的位置。横向对比一下,就能看清 llama.cpp 独特的生态位:

项目定位语言 / 依赖典型场景
PyTorch训练 + 推理框架Python,重科研、训练
transformers模型库 / 高层封装Python,重快速实验
vLLMGPU 高吞吐服务Python + CUDA云端大并发
llama.cpp轻量本地推理C/C++,几乎零依赖本地 / 边缘 / 嵌入

一句话总结:要训练 / 做研究选 PyTorch,要云端高并发服务选 vLLM,要在本地 / 边缘 / 嵌入式设备上轻量地把模型跑起来,就选 llama.cpp。它们不是互相取代,而是各司其职。

还有个常被忽略的事实,最能说明它的"地基"地位:许多你熟悉的本地大模型桌面工具——OllamaLM Studio、Jan、KoboldCpp、LocalAI 等—— 底层很大程度上就在调用 llama.cpp(它们大多就列在 llama.cpp 自己 README 的"UIs"清单里)。也就是说,你也许从没直接敲过它的命令, 却很可能每天都在间接用它;它更像整条本地推理生态的发动机,而不是一个孤立的命令行玩具。

怎么真正跑起来

最快的方式不用写一行代码:下载一个 .gguf,用命令行工具 llama-cli 直接对话:

# 最快跑起来:一个可执行文件 + 一个 .gguf
llama-cli -m model.gguf -p "用一句话解释量化"

其中 -m 指定模型文件,-p 给出提示词;回车之后模型就开始一个字一个字地往外蹦。 想要一个能用浏览器访问的"本地 ChatGPT",把 llama-cli 换成 llama-server 即可——它把同一套推理逻辑包成 HTTP 接口,对外提供兼容 OpenAI 的 API。 那这条命令背后到底发生了什么?拆开看,就是 C API 里的几步:

🔬 细节 / 源码对应
一次最小推理在 C API 里就是这几步(简化自 include/llama.h,伪代码骨架):
// 简化自 include/llama.h 的最小推理流程
llama_backend_init();

llama_model   *model = llama_model_load_from_file("model.gguf", mparams);
llama_context *ctx   = llama_init_from_model(model, cparams);  // 新接口

const llama_vocab *vocab = llama_model_get_vocab(model);
llama_sampler     *smpl  = llama_sampler_chain_init(llama_sampler_chain_default_params());
llama_sampler_chain_add(smpl, llama_sampler_init_greedy());  // 最简:贪心采样

// 1) prompt 切成 token
int n = llama_tokenize(vocab, prompt, /*...*/, tokens, /*...*/);

// 2) 自回归解码循环
llama_batch batch = llama_batch_get_one(tokens, n);
while (generating) {
    llama_decode(ctx, batch);                 // 前向:算出下一 token 的 logits
    llama_token id = llama_sampler_sample(smpl, ctx, -1);   // 采样
    if (llama_vocab_is_eog(vocab, id)) break; // 结束符
    batch = llama_batch_get_one(&id, 1);      // 新 token 喂回去
}

llama_model_free(model);

这条主线(加载 -> 分词 -> 解码循环 -> 采样)就是后面所有课的骨架,后面会专门用一课展开完整生命周期。

加载模型
llama_model_load
_from_file
->
分词
llama_tokenize
文本 -> token
->
解码循环
llama_decode
算下一 token
->
采样
sampler_sample
挑一个词

这里有个容易忽略的细节:解码循环每一轮只把上一个新 token 喂回去,而不是把整段历史重算一遍——靠的正是 KV cache 把先前每个 token 算出的键/值缓存了下来。所以第一次要把整段 prompt 整体过一遍(prefill),首个字出现前要稍等一下(这就是“首 token 延迟”); 之后逐字生成(decode)却很快,每步只做一个 token 的前向。llama_decode 负责"前向算一步", llama_sampler_sample 负责"按概率挑一个词",两者一前一后交替,就织出了你看到的逐字输出。

深入一点(选读)

下面三个常见问题,想深究的同学点开看;只想抓主线的可以先跳过。

1 量化为什么几乎不掉精度? 点击展开

示例:上面的 Q4_0 把 32 个权重分成一,整块共享一个 scale;块内每个权重只存一个 4 bit 的"档位", 用时再乘回 scale 还原。关键在于缩放是按小块算的,每块都能贴合自己那段数值的范围。

为什么够用:神经网络权重大多挤在 0 附近、对单个权重的微小误差并不敏感;按块缩放 + 低位宽, 就能在"省 4 倍空间"和"几乎不掉精度"之间取得平衡。更进一步的 K-quant可选配合重要性矩阵(imatrix): 用它给每个权重的量化误差加权,让更关键的权重被更精确地保留(位宽不变——是误差被加权,而非给它更多比特)。

算笔账:Q4_0 的一块正好是 QK4_0 = 32 个权重,存储上是 1 个 FP16 的 scale(2 字节)+ 32 个 4 bit 码值(16 字节)= 共 18 字节; 平摊到每个权重就是 18×8÷32 = 4.5 bit,这正是"约 1/4 大小"的由来(原始 FP16 是 16 bit)。块大小取 32 是个折中:太大则一个 scale 盖不住整块的数值范围、误差变大,太小则每块都要单存一个 scale、开销摊不薄。

实战里怎么选:真正常用的往往不是最朴素的 Q4_0,而是 Q4_K_M 这类 K-quant—— 它对不同层用不同位宽、并把 scale 本身也量化,质量/体积比更好;想更接近原始精度就上 Q5_K_M、Q6_K,想更省内存就降到 Q3_K_M。 一句话:位宽越高越像原模型、越低越省内存,Q4 附近通常是体感上的"甜点档"。

源码:量化与反量化的核心实现在 ggml/src/ggml-quants.c;重要性矩阵由 tools/imatrix 统计产出。

替代:GPTQ、AWQ 等也是主流量化方案,思路类似(按块 / 按通道缩放),只是格式与具体算法不同。

2 ggml 到底是什么? 点击展开

一句话:ggml 是一个张量 + 计算图 + 多后端的小引擎——定义数据(张量)、把运算组织成图, 再把图调度到 CPU / GPU 上执行。

为什么自研:为了零依赖、可嵌入、可移植。不绑定庞大的深度学习框架,一份 C 代码就能编译进任何程序、 跑遍各种硬件——这是它能"到处跑"的工程基础。

换个角度看自研的必要性:若直接搬 PyTorch 这类训练框架,会背上几百 MB 的依赖和一整套 Python 运行时,根本塞不进手机或嵌入式设备; 而推理真正用到的其实只是一小撮算子(矩阵乘、softmax、rope、各种归一化……)。ggml 索性只实现这一小撮,再给每种硬件配一套后端—— CPU 的 SIMD 指令、CUDA、Metal、Vulkan、HIP 等——同一张计算图换个后端就能换硬件。这种可移植性,是绑死某一家厂商的闭源库怎么都换不来的。

源码:核心在 ggml/ 下的 ggml.c(张量与计算图)和 ggml-backend(后端抽象与调度)。

替代:也可以直接调用 cuBLAS、oneDNN 这类厂商库,但会绑死特定硬件、失去可移植性。

3 我的电脑能跑多大的模型? 点击展开

有个粗略但好用的估算:权重占用 ≈ 参数量 × 每权重位宽 ÷ 8(字节)。把位宽代进去就能心算:

  • 7B、FP16(16 bit)≈ 7e9 × 16 ÷ 8 ≈ 14 GB——多数笔记本扛不住。
  • 7B、Q4(约 4.5 bit)≈ 4 GB 上下——普通笔记本就能本地跑。

这就是量化最直接的意义:把"装不下"变成"装得下"。实际还要再留一点余量给 KV cache 和上下文开销 (上下文越长占用越多),但量级上,Q4 让 7B 从"需要显卡"降到"普通内存即可"

再往大了看:同一条公式套到 70B 上,FP16 约 140 GB、即便 Q4 也要 约 40 GB——单张消费级显卡根本装不下; 这时要么换内存更大的机器,要么把一部分层卸载(offload)到 CPU 内存,或干脆用多机/多卡来分担。还要记住 KV cache随上下文长度线性增长: 上下文拉得很长时,它的占用甚至能和权重本身同一个量级,所以"能跑多大"从来不只看权重,得给上下文留足余量。

✅ 本课要点
  • llama.cpp = 纯 C/C++ 的大模型推理引擎,只负责"跑",不负责"训练"。
  • 三大支柱:GGUF 格式(一个文件装下整个模型)+ 量化(压小体积)+ ggml 引擎(多后端张量计算)。
  • 整体四层:后端 -> ggml -> llama 推理 -> 工具,自底向上。
  • 训练 vs 推理是两件事:它只做推理(只前向),所以能甩掉训练框架的重依赖。
  • 量化按块共享 scale,约省 4 倍空间而几乎不掉精度(Q4 让 7B 从约 14 GB 降到约 4 GB)。
  • 定位:本地 / 边缘 / 低成本,对照 PyTorch(训练)、vLLM(云端高并发)。
💡 设计亮点
把"推理"从"训练框架"里彻底剥离,再用量化 + 自研引擎压掉对 Python 生态与大显存的依赖—— 于是模型准备(Python 转 GGUF)和模型运行(C/C++ 推理)完全解耦,引擎得以编译成一份零依赖的可执行文件。 这就是它能"一个文件到处跑"的根本原因。

🧪 自测 · 想一想为什么这么设计

1. llama.cpp 把“推理”从“训练框架”里彻底剥离出来。这个定位选择,最主要换来了什么?
  1. 自动帮你下载模型
  2. 甩掉对 Python 生态与大显存的依赖,能在消费级硬件上本地、离线、低成本跑
  3. 更高的训练精度
  4. 让模型变得更聪明
看答案与解析 点击展开
答案:B。只做推理,就能用零依赖 C/C++ + 量化 + 自研 ggml 引擎压掉重依赖——这正是它“一个文件到处跑”的根本原因。
2. 整体四层结构里,真正“把算子算在硬件上(SIMD / GPU kernel)”的是哪一层?
  1. 后端层 CPU/CUDA/Metal/Vulkan
  2. 工具层 tools/
  3. 引擎层 ggml
  4. 推理层 src/llama-*
看答案与解析 点击展开
答案:A。ggml 负责描述张量与计算图、调度算子;真正落到硬件上的乘加由各“后端”实现。
3. Q4_0 量化为什么能大幅压缩体积,却几乎不掉精度?
  1. 整块权重共享一个 scale、按小块贴合数值范围,低位宽档位已够用
  2. 因为模型本来就不需要精度
  3. 因为它直接丢掉了不重要的网络层
  4. 因为它用 GPU 重新训练了权重
看答案与解析 点击展开
答案:A。量化按块共享缩放因子,每块贴合自己的数值范围;权重对微小误差不敏感,4 bit 档位足够,于是省约 4 倍空间而精度几乎不变。进一步的 K-quant 还可选配合重要性矩阵(imatrix)给量化误差加权,让更关键的权重被更精确地保留——位宽不变,只是误差被加权,并非分到更多比特。
💭 发散思考(没有标准答案,动手或动脑想想)
  • 什么场景你会选 llama.cpp,而不是 vLLM?反过来又是什么场景?把你的判断依据写下来。

llama.cpp is an LLM inference engine written in plain C/C++: it takes an already-trained model (stored as a GGUF file) and runs it efficiently - on an ordinary CPU, even a phone, and faster with a GPU. It does not train models; it only focuses on "turning a model into text". One executable plus one .gguf file lets you chat with an LLM locally, offline, and cheaply - and that is what makes it so appealing.

🔌 Analogy
Think of a trained model as a music score (the weights). PyTorch is the recording studio: it can compose and record, but it is heavy. llama.cpp is a portable player: it does not compose, it just plays the score faithfully - using little power and running almost anywhere.

What problem does it solve

Research models mostly use Python + PyTorch: heavy dependencies, hungry for VRAM, hard to deploy on ordinary devices. Trying to run a 7B model on your own laptop, you often hit "can't install the environment", "not enough VRAM", or "tens of GB of weights to download". llama.cpp aims for the opposite - making inference, that one job, extremely light:

🌍 Big picture
With zero-dependency C/C++ + quantization (compressing weights to e.g. 4/5/8 bits, down to 2/3/6-bit K-quants) + its own tensor engine ggml, it makes LLMs run locally, offline, and cheaply on consumer hardware. No Python, no CUDA toolchain, no network needed: one executable plus one .gguf file is enough.

Typical research stack

Python + PyTorch - heavy deps - VRAM-hungry - often needs CUDA - hard to deploy on ordinary devices

llama.cpp

zero-dep C/C++ - quantization saves VRAM - runs on CPU too - offline - one executable + one .gguf

Why C/C++ specifically, and not Python, Rust, or Go? An inference engine cares about two things above all - a zero runtime and being embeddable - and C/C++ delivers both. It compiles straight to machine code, with no Python interpreter and none of the garbage collection or VM runtime of Go/Java, so it computes the moment it starts with predictable latency. It can be statically linked into one standalone executable that runs on another same-architecture machine with zero dependencies installed. Crucially, almost any language can call it through the C ABI, so the engine can be embedded into desktop apps, Android/iOS, the browser (compiled to WASM), even embedded devices - hard to pull off in a heavy-runtime language. Rust could reach a similar goal, but when the project started the C/C++ compilers and each vendor's GPU toolchains were the most mature and the easiest to port to. The official README states the goal up front as "minimal setup with top performance", and plain C/C++ is the linchpin of that sentence.

Three pillars: GGUF - quantization - ggml

The reason llama.cpp can "run anywhere from a single file" is three building blocks fitting together. Here is a first glance at each; later lessons expand every one of them:

(1) GGUF: one file holds the whole model

GGUF is llama.cpp's model file format: it bundles the weights, hyper-parameters (layers, dimensions, vocab size), tokenizer, chat template, and more into a single file. Loading just mmaps it into memory - no extra config files or Python code. Hand the engine one .gguf and it knows "what model this is and how to run it".

What is so good about "one file"? The traditional way needs weight shards, config.json, tokenizer.json, a generation config and more all present at once - miss one and nothing runs, mismatch a version and things break. GGUF welds them all into a single file, so it is config-free: the engine reads the metadata stored as key-value pairs in the file header (hyper-parameters, vocab, chat template) and knows exactly how to run. Loading mmaps the file in on demand - the OS reads a page only when it is touched - so startup is fast and multiple processes can share one read-only copy of the weights to save memory. The most practical payoff: swapping models means swapping one file - replace the .gguf, leave the command-line flags untouched, and you are running a different model. Distribution and management become trivially simple.

(2) Quantization: shrink the weights, keep the accuracy

Raw weights are usually 16-bit floats (FP16), so a 7B model needs about 14 GB. Quantization packs each weight into a lower bit-width (e.g. 4 bits), cutting the size to roughly 1/4 - small enough for an ordinary laptop's RAM. The cost is a tiny accuracy loss, but a "per-block shared scale" design keeps that loss almost unnoticeable:

FP16 raw weights: each number 16 bits - high precision, but bulky
0.12-0.340.080.51... a block of 32 x 16 bit
After Q4_0: the whole block shares one scale; each weight stores just a 4-bit level; dequant = scale x (code - 8)
scalex 0110100100111100... ~4.5 bit/weight, about 1/4 the size

Why can it "save space yet barely lose quality"? Three things stack up. First, a model has far more weights than runtime activations, so compressing the weights to a low bit-width saves the most space while touching compute precision the least. Second, "predicting the next token" depends on the relative ranking of the candidate logits, which is insensitive to tiny per-weight errors - the jitter from low bit-width mostly washes out. Third, a per-block shared scale lets each small block hug the value range of its own slice, firmly preserving the dynamic range. There is a cost - a little quality loss - but an importance matrix can keep the more critical weights more accurate and push that loss down further.

Put that compression in a real setting and it is a "heavy-to-light" pipeline: the same model shrinks sharply after quantization, and paired with one executable it can move from the data center onto your laptop:

FP16 model
7B ~ 14 GB
->
Quantize Q4
~ 4 GB
->
.gguf single file
+ one executable
->
Run locally
laptop / phone / server

(3) ggml: the low-level tensor engine

ggml is llama.cpp's in-house tensor library: it defines tensors, describes one inference run as a compute graph, then dispatches the ops in that graph (matmul, softmax, rope...) to different backends (CPU SIMD, CUDA, Metal, Vulkan...) to actually compute. By separating "describing the math" from "running it on hardware", the same model code runs unchanged across CPU and all kinds of GPUs.

Structure map: four layers, bottom-up

Stack those three pillars by "who depends on whom" and llama.cpp becomes a clean four-layer tower, from the hardware at the bottom up to the user-facing tools:

toolstools/ - examples/
User-facing: llama-cli, the llama-server HTTP service, the llama-quantize tool
infersrc/llama-*
Model loading - compute graph - KV cache - sampling - tokenizer - chat templates
engineggml
Tensors - compute graph - ops (matmul/rope/softmax...) - backend scheduling - quant formats
backendCPU - CUDA - Metal - Vulkan ...
Actually runs the ops on hardware (SIMD / GPU kernels)

Keep this line in mind when reading the source: the backend provides compute, ggml organizes the math into a graph, src/llama-* assembles that graph into "session-level" inference logic, and the outermost tools/ is what you actually type. The next lesson maps these four layers onto concrete directories.

There is a pleasing symmetry to the direction: your request flows top-down through the four layers (a tool receives the prompt -> the inference layer organizes it into a compute graph -> ggml arranges the ops -> a backend lands them on hardware), and the computed result flows bottom-up back into text on your screen. If you ever get lost reading the source, return to this picture and first pin down "which layer am I in" - it usually settles the panic.

Training vs inference: which side is llama.cpp on

An LLM's life has two phases: first training (teaching it), then inference (putting it to work). These are two jobs with completely different needs:

🏋️ Training (PyTorch, etc.)

  • Forward + backprop, compute gradients, update weights
  • Needs optimizer state, VRAM-hungry (often multi-GPU)
  • Python ecosystem, heavy dependencies
  • Goal: produce the model

⚡ Inference (llama.cpp)

  • Forward only: weights are fixed, one pass yields logits
  • Can quantize to save memory; even a CPU runs it
  • Plain C/C++, near-zero dependencies
  • Goal: turn the model into text

llama.cpp does only the inference half. Precisely because it need not support backprop or optimizers, it can drop the heavy training-framework dependencies and compress the whole engine into a single slab of plain C/C++ - the prerequisite for running light on your machine. How the model is "produced" is not its concern; that is the job of training frameworks like PyTorch.

What does "forward only" concretely mean? To recompute gradients during backprop, training keeps every layer's intermediate results from the forward pass, and maintains optimizer state per weight (e.g. Adam's first- and second-moment momentum) - adding up to several times the memory of the weights themselves. Inference cuts all of that: no backward pass, no gradients, no optimizer state, and the weights are read-only and never change after loading, so memory really only holds two things - weights and the KV cache. Because the weights are read-only, you can safely quantize them to a low bit-width without worrying about training; and because the heavy training burden is gone, the whole thing is light enough to run on a single ordinary CPU. That is the entire confidence that "inference only" buys.

How it differs from PyTorch / transformers / vLLM

All of these deal with LLMs, yet they sit at different positions. A side-by-side comparison makes llama.cpp's distinct niche clear:

ProjectRoleLang / depsTypical use
PyTorchTraining + inference frameworkPython, heavyResearch, training
transformersModel library / high-level wrapperPython, heavyFast experiments
vLLMHigh-throughput GPU servingPython + CUDACloud, high concurrency
llama.cppLightweight local inferenceC/C++, near-zero depsLocal / edge / embedded

In one line: pick PyTorch to train / do research, vLLM for high-concurrency cloud serving, and llama.cpp to run a model lightly on local / edge / embedded devices. They do not replace each other - each has its job.

One often-overlooked fact best captures its "foundation" status: many of the local-LLM desktop tools you know - Ollama, LM Studio, Jan, KoboldCpp, LocalAI and more - are, to a large degree, calling llama.cpp underneath (most of them are listed right in llama.cpp's own README under "UIs"). In other words, you may never have typed its commands directly, yet you very likely use it indirectly every day; it is less a standalone command-line toy and more the engine under the whole local-inference ecosystem.

How to actually run it

The fastest way needs no code at all: download a .gguf and chat right from the command line with llama-cli:

# fastest way to run: one executable + one .gguf
llama-cli -m model.gguf -p "explain quantization in one sentence"

Here -m points at the model file and -p gives the prompt; press enter and the model starts emitting text token by token. Want a browser-accessible "local ChatGPT"? Swap llama-cli for llama-server - it wraps the same inference logic into an HTTP service exposing an OpenAI-compatible API. So what actually happens behind that command? Unpacked, it is just these few steps in the C API:

🔬 Details / source
A minimal inference run is just these steps in the C API (simplified from include/llama.h, pseudo-code skeleton):
// simplified minimal inference flow from include/llama.h
llama_backend_init();

llama_model   *model = llama_model_load_from_file("model.gguf", mparams);
llama_context *ctx   = llama_init_from_model(model, cparams);  // new API

const llama_vocab *vocab = llama_model_get_vocab(model);
llama_sampler     *smpl  = llama_sampler_chain_init(llama_sampler_chain_default_params());
llama_sampler_chain_add(smpl, llama_sampler_init_greedy());  // simplest: greedy

// 1) split the prompt into tokens
int n = llama_tokenize(vocab, prompt, /*...*/, tokens, /*...*/);

// 2) autoregressive decode loop
llama_batch batch = llama_batch_get_one(tokens, n);
while (generating) {
    llama_decode(ctx, batch);                 // forward: logits for the next token
    llama_token id = llama_sampler_sample(smpl, ctx, -1);   // sample
    if (llama_vocab_is_eog(vocab, id)) break; // end-of-generation
    batch = llama_batch_get_one(&id, 1);      // feed the new token back
}

llama_model_free(model);

This main line (load -> tokenize -> decode loop -> sample) is the skeleton for every later lesson; a later lesson expands it into the full lifecycle.

load model
llama_model_load
_from_file
->
tokenize
llama_tokenize
text -> token
->
decode loop
llama_decode
next token
->
sample
sampler_sample
pick a word

One easily missed detail: each turn of the decode loop only feeds back the single new token rather than recomputing the whole history - thanks to the KV cache, which stores the keys/values already computed for every prior token. That is why the first token only appears after the whole prompt has been passed through once (prefill - the "time to first token"), while generating word by word afterward (decode) is fast, each step doing the forward pass for just one token. llama_decode is "run one forward step" and llama_sampler_sample is "pick a word by probability"; the two alternate to weave the token-by-token output you see.

Go deeper (optional)

Three common questions below - open them if you want to dig in; skip them if you just want the main line.

1 Why does quantization barely lose accuracy? click to expand

Example: the Q4_0 above splits 32 weights into one block that shares a single scale; each weight in the block stores only a 4-bit "level", multiplied back by the scale when used. The key is that scaling is done per small block, so each block hugs the value range of its own slice.

Why it is enough: network weights mostly cluster near 0 and are insensitive to tiny per-weight errors; per-block scaling + low bit-width strikes a balance between "4x smaller" and "barely any accuracy loss". K-quants can optionally pair with an importance matrix (imatrix): it weights each weight's quantization error so the more important weights are preserved more faithfully (bit-width is unchanged - the error is weighted, bits are not reallocated).

Do the math: a Q4_0 block is exactly QK4_0 = 32 weights, stored as one FP16 scale (2 bytes) + 32 four-bit codes (16 bytes) = 18 bytes total; amortized per weight that is 18x8/32 = 4.5 bit, which is where "about 1/4 the size" comes from (raw FP16 is 16 bit). A block size of 32 is a compromise: too large and one scale cannot cover the block's value range so error grows; too small and every block must store its own scale, so the overhead does not amortize.

What people actually pick: the common choice is usually not the plain Q4_0 but a K-quant like Q4_K_M - it uses different bit-widths for different layers and quantizes the scales themselves too, for a better quality/size ratio; go to Q5_K_M or Q6_K to get closer to the original precision, or down to Q3_K_M to save more memory. In a line: higher bit-width is closer to the original model, lower is more memory-thrifty, and around Q4 is usually the sweet spot.

Source: the core quant/dequant code lives in ggml/src/ggml-quants.c; the importance matrix is produced by tools/imatrix.

Alternatives: GPTQ and AWQ are mainstream too, with a similar idea (per-block / per-channel scaling) - only the format and exact algorithm differ.

2 What exactly is ggml? click to expand

In one line: ggml is a small engine of tensors + compute graph + multiple backends - it defines data (tensors), organizes the math into a graph, then schedules that graph onto CPU / GPU to execute.

Why in-house: for zero dependencies, embeddability, and portability. By not binding to a huge deep-learning framework, a single slab of C can compile into any program and run across all kinds of hardware - the engineering basis for "running anywhere".

Another angle on why in-house: dragging in a training framework like PyTorch would mean hundreds of MB of dependencies and a whole Python runtime - it would never fit into a phone or an embedded device; yet inference really uses only a small handful of ops (matmul, softmax, rope, various normalizations...). ggml simply implements that handful, then gives each kind of hardware its own backend - CPU SIMD, CUDA, Metal, Vulkan, HIP and so on - so the same compute graph swaps hardware just by swapping the backend. That portability is something binding yourself to one vendor's closed library can never buy.

Source: the core is under ggml/: ggml.c (tensors and compute graph) and ggml-backend (backend abstraction and scheduling).

Alternatives: you could call vendor libraries like cuBLAS or oneDNN directly, but that locks you to specific hardware and loses portability.

3 How big a model can my machine run? click to expand

A rough but handy estimate: weight footprint ~= parameters x bits-per-weight / 8 (bytes). Plug in the bit-width and you can do it in your head:

  • 7B, FP16 (16 bit) ~= 7e9 x 16 / 8 ~= 14 GB - too much for most laptops.
  • 7B, Q4 (~4.5 bit) ~= 4 GB or so - an ordinary laptop runs it locally.

That is quantization's most direct payoff: turning "won't fit" into "fits". In practice leave some headroom for the KV cache and context overhead (longer context uses more), but in order of magnitude, Q4 takes 7B from "needs a GPU" down to "ordinary RAM is fine".

Scaling up: apply the same formula to 70B and FP16 is about 140 GB, while even Q4 still needs about 40 GB - more than a single consumer GPU can hold; then you either move to a machine with more memory, or offload some layers to CPU RAM, or simply split the work across multiple machines/GPUs. Remember too that the KV cache grows linearly with context length: stretch the context very long and its footprint can reach the same order of magnitude as the weights themselves, so "how big can I run" is never about the weights alone - leave enough headroom for context.

✅ Key points
  • llama.cpp = an LLM inference engine in plain C/C++ - it only "runs", it does not "train".
  • Three pillars: GGUF format (one file holds the whole model) + quantization (shrink the size) + the ggml engine (multi-backend tensor compute).
  • Four layers: backend -> ggml -> llama inference -> tools, bottom-up.
  • Training vs inference are two jobs: it does inference only (forward only), so it sheds the training framework's heavy deps.
  • Quantization shares a scale per block, ~4x smaller with barely any accuracy loss (Q4 takes 7B from ~14 GB to ~4 GB).
  • Niche: local / edge / low-cost, vs PyTorch (training) and vLLM (cloud, high concurrency).
💡 Design insight
It cleanly separates inference from the training framework, then uses quantization + a custom engine to drop the dependency on the Python ecosystem and large VRAM - so model prep (Python to GGUF) and model run (C/C++ inference) fully decouple, and the engine compiles into a single zero-dependency executable. That is why it can "run anywhere from a single file".

🧪 Self-test - think about the design

1. llama.cpp deliberately separates inference from the training framework. What does that choice mainly buy?
  1. It downloads models for you automatically
  2. Dropping the Python-ecosystem and large-VRAM dependency, so it runs locally/offline/cheaply on consumer hardware
  3. Higher training accuracy
  4. It makes the model itself smarter
Show answer & explanation click to expand
Answer: B. By doing inference only, it can use zero-dependency C/C++ + quantization + the ggml engine to shed heavy deps - the root reason it 'runs anywhere from a single file'.
2. In the four-layer structure, which layer actually 'runs the ops on hardware (SIMD / GPU kernels)'?
  1. the backend layer CPU/CUDA/Metal/Vulkan
  2. tools/ layer
  3. the ggml engine layer
  4. inference layer src/llama-*
Show answer & explanation click to expand
Answer: A. ggml describes tensors/graphs and schedules ops; the actual hardware math is implemented by each backend.
3. Why can Q4_0 quantization shrink the size so much yet barely lose accuracy?
  1. A whole block shares one scale and hugs that block's value range, so the low-bit levels are enough
  2. Because the model never needed precision anyway
  3. It simply drops the unimportant network layers
  4. It retrains the weights on a GPU
Show answer & explanation click to expand
Answer: A. Quantization shares a scale per block, each fitting its own value range; weights tolerate tiny errors and 4-bit levels suffice, so it saves ~4x space with almost no accuracy change. K-quants can optionally pair with an importance matrix (imatrix) that weights the quantization error so important weights are preserved more faithfully - the bit-width is unchanged, the error is weighted rather than bits reallocated.
💭 Open questions (no single right answer - just think or try)
  • When would you pick llama.cpp over vLLM, and when the reverse? Write down the criteria you'd use.