GGUF-量化内幕-第1章-为什么需要-GGUF-量化
GGUF 量化内幕:第1章 为什么需要 GGUF 量化
先从一个数字讲起。一个 7B(70 亿参数)的开源大语言模型,如果用 FP32(每个权重 4 字节)保存,需要约 28 GB 磁盘;换成 FP16(每个权重 2 字节),也要约 14 GB。而一部手机、一台 8 GB 内存的笔记本、一块树莓派,物理上就放不下这样的文件——更不要说在推理时把它读进内存了。
量化(quantization)就是回答这个问题的技术:用更少的比特去表示每个权重,把文件从 14 GB 压到 3–4 GB,同时尽量少地牺牲模型能力。GGUF 是这套做法在 llama.cpp 生态里落地的文件格式——它既规定了"压缩后的权重长什么样",也规定了"这些权重如何存进一个文件、推理时又如何读出来"。
本章不写任何一行复杂的公式(那留给第 2 章),只做三件事:先解释端侧部署到底受什么物理约束,再说清楚"量化"这门语言的最小词汇表,最后给出全书的路线图——量化格式的三次范式跃迁。读完本章,你应该能回答一个问题:为什么量化格式不是越简单越好,也不是越复杂越好,而是要分场景选择?
先统一几个术语。参数(parameter)/ 权重(weight):模型在训练时学到的数字,推理时参与矩阵乘法,是文件体积的绝对大头。推理(inference):用训练好的模型做预测——大语言模型的推理通常是一个字一个字(token)生成的。token:模型处理文本的最小单位,大约是"半个词"或"一个词"。比特(bit):计算机里最小的信息单位,0 或 1;8 个 bit = 1 字节(byte)。带宽(bandwidth):单位时间内能从内存读出多少字节,好比水管每分钟能流过多少水。RSS:进程占用的物理内存总量,衡量"运行时占多少内存"。
1.1 端侧部署的"体积—精度—速度"三角
1.1.1 内存带宽是端侧解码的首要瓶颈
大语言模型的推理有个奇怪的物理规律:模型生成每一个 token,几乎都要把整个模型的权重从内存读一遍。想想它的工作方式——每一步都要计算"下一个 token 是什么",而计算要用到所有层的权重矩阵。权重每次都要从内存搬到计算单元,这个搬运量,与模型大小成正比。
于是关键的问题变成:搬运这些权重,到底受什么限制?
这里引入一个分析工具——roofline 模型(屋顶线模型)。它把处理器上的任何计算画成一条"屋顶线":横轴是运算强度(每次内存搬运配多少算术运算),纵轴是可达性能。屋顶有两条边:左边是带宽受限(内存管子的粗细决定天花板),右边是算力受限(计算单元的速度决定天花板)。你的任务落在哪一边,瓶颈就在哪。
自回归解码的算术强度极低——每个权重大约只做几次乘加运算就要被丢掉,再读下一批。换句话说,解码任务落在 roofline 的带宽受限一侧:
roofline 模型:解码落在带宽受限一侧
性能
│ ┌──── 算力上限(FLOPs/s)
│ ┌─────┘
│ ┌─────┘
│ ┌─────┘
│────┘ ←── 带宽上限(字节/s)
│
└────────────────────────────► 运算强度(FLOPs/字节)
解码任务在这里
(强度极低 → 带宽受限)
这个结论的实操含义非常直接:端侧能达到的 tok/s(每秒生成的 token 数),约等于"内存带宽 ÷ 每 token 需要读的字节数"。每 token 需要读的字节数 = 参数量 × 每个权重占的字节数。于是:
- 模型越小、每权重字节越少 → 每 token 搬运越少 → tok/s 越高;
- 内存带宽越大(芯片越好)→ tok/s 越高。
这就是为什么量化(减少每权重字节数)能直接换速度,而不仅仅是省空间。用公式写出来:
[
\text{tok/s} \approx \frac{\text{内存带宽(字节/秒)}}{\text{参数量} \times \text{每权重字节数}}
]
举个例子。一款中端手机 SoC 的有效内存带宽量级约 20 GB/s(理论带宽更高,但实际利用率通常只有三四成)。运行一个 3B 模型:
- Q4_K_M(约 4.5 bpw,即每权重约 0.56 字节):(\frac{20\text{ GB/s}}{3\times10^9 \times 0.56} \approx 11.2\ \text{tok/s})
- FP16(每权重 2 字节):(\frac{20\text{ GB/s}}{3\times10^9 \times 2} \approx 3.3\ \text{tok/s})(实际更低,约 2 个,因为还有其他开销)
一对比就很清楚:同样的芯片,换一种更省字节的量化格式,速度能差好几倍。这不是玄学,是带宽数学。
用代码把这条公式变成一个小函数,可以随时估算任意模型格式的端侧上限。三种语言实现逻辑完全一致:输入带宽、参数量、每权重比特数,返回理论 tok/s。
C++:
#include <cstdio>
// 带宽单位:字节/秒;参数量:个;bpw:每权重比特数
double theoretical_tok_per_s(double bandwidth_bps, long long n_params, double bpw) {
const double bytes_per_weight = bpw / 8.0;
const double bytes_per_token = n_params * bytes_per_weight;
return bandwidth_bps / bytes_per_token;
}
int main() {
const long long n_params = 3e9; // 3B 模型
printf("Q4_K_M (4.5 bpw): %.1f tok/s\n", theoretical_tok_per_s(20e9, n_params, 4.5));
printf("FP16 (16 bpw): %.1f tok/s\n", theoretical_tok_per_s(20e9, n_params, 16));
return 0;
}
C#(.NET 5+):
using System;
static class TokRate {
static double TheoreticalTokPerS(double bandwidthBps, long nParams, double bpw) {
double bytesPerWeight = bpw / 8.0;
double bytesPerToken = nParams * bytesPerWeight;
return bandwidthBps / bytesPerToken;
}
static void Main() {
const long nParams = 3_000_000_000L; // 3B
Console.WriteLine($"Q4_K_M (4.5 bpw): {TheoreticalTokPerS(20e9, nParams, 4.5):F1} tok/s");
Console.WriteLine($"FP16 (16 bpw): {TheoreticalTokPerS(20e9, nParams, 16):F1} tok/s");
}
}
Python:
def theoretical_tok_per_s(bandwidth_bps: float, n_params: int, bpw: float) -> float:
bytes_per_weight = bpw / 8.0
bytes_per_token = n_params * bytes_per_weight
return bandwidth_bps / bytes_per_token
n_params = 3_000_000_000 # 3B
print(f"Q4_K_M (4.5 bpw): {theoretical_tok_per_s(20e9, n_params, 4.5):.1f} tok/s")
print(f"FP16 (16 bpw): {theoretical_tok_per_s(20e9, n_params, 16):.1f} tok/s")
三种实现输出一致。这个估算的局限也要说清楚:它只算"读权重"这一项,忽略了 KV 缓存、中间激活、解码开销、并行效率等次要因素,所以真实速度通常低于这个上限——但它足够解释"为什么省字节能直接换速度",也足够在不同格式之间做相对比较。
1.1.2 存储占用与冷启动延迟
量化带来的第二个收益是"装得下"。这里要区分两个经常被混为一谈的指标:
- 文件体积(磁盘占用):模型文件在磁盘/存储上占多少空间,影响下载时间、SD 卡占用、分发成本。
- 运行时 RSS(内存占用):推理进程实际吃掉的物理内存,影响这台设备能不能跑起来、会不会被杀进程。
GGUF 的一个巧妙设计是用 mmap(内存映射) 加载:把文件直接映射进内存,需要哪块读哪块,不必把整个文件先复制进内存。这带来的一个结果是:冷启动(冷启动 latency,从点击到能产出第一个 token)主要取决于"首次读入"的缺页开销和页缓存状态,而不是文件大小本身。对于 3B 模型,一个典型的端侧对比(不同来源的实测量级,取约数)是:
| 指标 | Q4_K_M(约 4.5 bpw) | FP16 |
|---|---|---|
| 文件体积 | 约 1.7 GB | 约 6.0 GB |
| 运行时 RSS | 约 2.1 GB | 约 7.2 GB |
| 端侧 tok/s | 约 11.2 | 约 2.1 |
| 冷启动 | 明显更快 | 明显更慢 |
注意三个观察:第一,Q4_K_M 用不到 1/3 的体积,跑出 5 倍以上的速度。第二,RSS 比文件体积略大——因为推理还要给 KV 缓存、中间激活腾内存,这些不全是权重。第三,冷启动的差异不仅来自文件大小,还来自 mmap 的页缓存效率:文件小,能更快被缓存命中。
1.1.3 为什么"一种格式"打不了天下
如果 Q4_K_M 这么好,那所有模型都用它不就行了?不行,因为场景对"精度 vs 体积"的取舍完全不同:
- 手机、平板:内存可能只有 4–8 GB,必须用 <2 bpw 的极限压缩(IQ 系列甚至三值格式),哪怕牺牲质量,目标是"跑得动"。
- 工作站、服务器:内存 64 GB 起步,可以承受 Q5_K 甚至 Q8_0 的"奢侈",目标是"质量尽量接近原版"。
- 调试、研究、对比:需要 Q8_0 甚至 F16 作为"参考基线"——拿它衡量别的格式到底损失了多少。
用一个比喻:你不可能用一把刀既切西瓜又剔牙。单一格式的困境在于,无论选哪个位宽,都会在某个场景里要么浪费空间、要么牺牲质量。
GGUF 的答案不是"折中出一个万能位宽",而是把选择权下放到每个张量:同一个文件里,注意力层可以用 Q4_K,输出层可以用 Q6_K,归一化层干脆保留 F32。这正是第 3 章要讲的容器机制,也是第 14 章"逐张量混合精度"的由来。
1.2 量化术语的最小共识
1.2.1 块、超级块与 bpw 的定义
进入正文前,先把三个最常出现的词钉死。
块(block):一组共享一个量化尺度参数的权重集合。为什么需要"块"?因为整个张量的数值分布不均匀(大多接近零、少数很大),用一个全局尺度会把小值都压成零(第 4 章会详细证明)。于是把张量切成连续的若干组,每组单独算自己的尺度。Q4_0 的块是 32 个权重,Q8_0 也是 32 个。
超级块(super-block):K-quants 系列使用的一种更大单元,包含 256 个权重,内部再分成 8 个 32 值的子块。超级块拥有"全局尺度",子块拥有"局部尺度 + 局部最小值"——这是第 6、7 章的主题。
bpw(bits per weight,每权重比特数):衡量压缩程度的核心指标。它的计算方法有严格定义:
[
\text{bpw} = \frac{\text{文件/块字节数} \times 8}{\text{权重数}}
]
注意:bpw 不是"量化码的位宽"。比如 Q4_0 的码确实是 4 位,但每个 32 值块还要存 2 字节的尺度 d,所以实际 bpw 是 (\frac{18 \times 8}{32} = 4.5)。多出来的 0.5 bit/权重,就是尺度、最小值、码本索引这些"额外开销"摊到每个权重上的结果。任何格式的 bpw 都要按字节数算,而不是按"听起来是几比特"算。
1.2.2 尺度、最小值偏移与零点
量化本质上是用有限个离散值去"代表"连续的数。怎么安排这些离散值,有三种常见参数化方式:
- 对称块量化(只有尺度),如 Q4_0、Q8_0:反量化公式是 (y = d \cdot q),其中 (d) 是尺度,(q) 是有符号码。离散值围绕 0 对称分布。
- 非对称块量化(尺度 + 最小值),如 Q4_K 的子块:公式是 (y = d \cdot q - m),多了一个最小值/偏移 (m),让离散值可以整体平移,从而更贴合"最小不为负、最大不对称"的分布。
- 显式零点(zero point),如 Q4_1:显式存一个"零点",公式 (y = d(q - z)) 或 (y = d \cdot q + m),零点单独占字节。
三者在表示能力和字节开销上有区别:对称最省字节但要求分布近似对称;非对称/显式零点能多表示一个偏移,但要多花字节。
1.2.3 对称、非对称与"伪对称"——一个常见混淆
这里有一个新手最容易绕晕的地方。看两个都带"偏移"的格式:
- Q4_0 的
q - 8:Q4_0 存的是无符号 4-bit 码 (q \in [0, 15]),反量化时做 (y = d(q-8))。这个"减 8"只是解码端的固定平移,把无符号码变成近似的有符号码。它不增加任何参数开销——没有任何地方存这个 8,它是写死在代码里的。所以 Q4_0 在数学上可以看成"零点固定为 8"的格式,但它不占字节。 - Q4_K 的
d_{\min} \cdot m_j:这是逐子块学习出来的真实偏移。每个 32 值子块有一个 6-bit 的 (m_j),配合全局的 (d_{\min}) 形成子块自己的最小值。这个偏移需要额外字节(存在scales数组里),而且每个子块都不同。
一句话总结区别:"q-8"是免费的固定偏移,"dmin·m_j"是收费的逐块学习偏移。这一区别直接决定了两种格式的误差结构(第 4、7 章会展开)。
1.3 GGUF 在 llama.cpp 工具链中的位置
1.3.1 转换、量化、mmap 加载、推理:四阶段流水线
从 Hugging Face 上的一组权重文件,到你的手机上能跑,中间是一条清晰的流水线:
四阶段流水线
Hugging Face (safetensors / PyTorch)
│
▼
① 转换 convert_hf_to_gguf.py
│ 只做"容器转换",不做量化:把权重搬进 GGUF 容器
▼
F16 GGUF 文件(约 14 GB,无损基线)
│
▼
② 量化 llama-quantize(可选 + llama-imatrix 生成重要性矩阵)
│ 把每个张量压成目标格式,如 Q4_K_M
▼
量化后 GGUF 文件(约 3.8 GB)
│
▼
③ 加载 llama.cpp 运行时 mmap
│ 映射文件、读元数据、按张量类型分派解码路径
▼
④ 推理 逐 token 生成
四个阶段各自负责一个"决策点":
- convert_hf_to_gguf.py 决定张量命名与容器结构(第 3、15 章);
- llama-imatrix 决定"哪些权重更重要"——这是 IQ 系列格式的前提(第 11、15 章);
- llama-quantize 决定每个张量用什么格式,即"混合精度策略"(第 14、15 章);
- 运行时 决定"每个 ggml_type 怎么解码、怎么上后端"(第 16、17 章)。
1.3.2 GGUF 与 safetensors、PyTorch 的分工边界
GGUF 不是训练格式,也不是通用交换格式。它和另外两个格式的关系需要澄清:
- PyTorch / safetensors:训练和微调使用的格式。它们的目标是"灵活地保存任意张量、支持自动求导图的重建",体积和加载速度不是首要考虑。
- GGUF:推理格式。它的目标是在三个约束下做到最优——mmap 友好(可以零拷贝加载)、逐张量类型分派(每个张量能选自己的量化格式)、端侧推理(体积小、解码快)。
GGUF 的一些"奇怪之处"——POD 结构体、固定对齐、type id 硬编码——都不是巧合,而是这三个约束的直接产物。第 3 章会逐一解释:为什么结构体必须是 POD(为了 mmap 后能直接当内存结构体用)、为什么有对齐(为了让每个张量从对齐位置开始)、为什么 type id 是硬编码(为了运行时快速分派解码函数)。
一句话:safetensors 是"仓库",GGUF 是"战时补给箱"——一个为了长期保存、一个为了快速装卸。
1.4 量化范式的三次跃迁
全书的叙事主线是:量化格式不是一堆并列的选项,而是沿着"解决上一个范式的失效"这条线演进的。三次跃迁分别对应三组格式。
1.4.1 第一范式:块共享尺度的仿射量化(Q4_0 / Q8_0)
最朴素的做法:每个块(32 个权重)共享一个 FP16 尺度 d,权重存成 4-bit 或 8-bit 码。它的优点是实现简单、解码极快(只有位操作和一次乘法),缺点是块内异质性会惩罚小值——一个块里同时有大幅值和小幅值时,尺度被大值主导,小值全部塌缩成同一个码(第 4 章)。
第一范式:块共享尺度
┌─────────────┐
│ 32 权重共享 │ ← 一个尺度 d,一个公式 y=d·(q-8)
│ 1 个尺度 d │
└─────────────┘
简单,但块内异质时小值被牺牲
1.4.2 第二范式:超级块内的层次化尺度与最小值(K-quants)
第二范式不是换码位宽,而是改尺度的结构:把 256 个权重放进一个超级块,超级块有全局尺度 d 和全局最小值尺度 d_min,内部 8 个 32 值子块各自有 6-bit 尺度和 6-bit 最小值。误差预算从"一个尺度"变成"全局尺度 + 子块修正"。
第二范式:超级块内的层次化尺度(K-quants)
┌──────────────────────────────────────────┐
│ 超级块(256 值):d、d_min │
│ ┌────┬────┬────┬────┬────┬────┬────┬──┐ │
│ │子块0│子块1│...│子块7│ 每个子块: │
│ │ s₀ │ s₁ │ │ s₇ │ 6-bit 尺度+最小值│
│ └────┴────┴────┴────┴────┴────┴────┴──┘ │
└──────────────────────────────────────────┘
代价:多存 12 字节的参数;收益:块内异质不再由单一尺度承担
这解决了第一范式的核心失效:块内异质性不再让单一尺度"背锅"。代价是解码多了一层解包(第 6、7 章)。
1.4.3 第三范式:重要性加权码本与三值结构化稀疏(IQ / STQ1_0 / PTQ1_0)
当位宽压到 2 bpw 以下,第二范式也触顶了——均匀的码字覆盖不了重尾分布,层次化尺度也救不了。第三范式用两个新武器突破:
- 码本(grid)+ 重要性矩阵(imatrix):不再用"尺度 × 码"的算术公式,而是直接查一张预先设计好的非线性表(码本),并且让量化误差按"权重对输出的影响大小"加权(IQ 系列)。
- 三值 + 结构化稀疏:把权重硬约束到 ({-d, 0, +d}),用模式编码或 base-3 编码存储(STQ1_0 / PTQ1_0)。
第三范式:码本查表 + 三值稀疏
第二范式: y = d·s_j·q − d_min·m_j ← 算术反量化
第三范式: y = d · grid[index] ← 码本查表
y = d · trit (∈{-1,0,+1}) ← 三值
这一跃迁的代价是工程复杂度爆炸:查表在 SIMD 上比算术难实现、必须提供 imatrix、部分后端覆盖不全——这些将在第 11、13 章反复出现。
本章小结与通往数值基础的桥梁
本章可以压缩成三个事实。
第一,端侧解码受内存带宽限制,量化直接换速度。 roofline 模型告诉我们自回归解码是带宽受限任务,于是"每权重字节数"既是体积指标也是速度指标。Q4_K_M 用不到 FP16 三分之一的字节,换来端侧几倍以上的 tok/s。
第二,量化语言的最小词汇表是"块、超级块、bpw、尺度、偏移"。 bpw 按"字节数 × 8 / 权重数"算,不是按码位宽算;Q4_0 的 q-8 是免费固定偏移,Q4_K 的 d_min·m_j 是收费的逐块学习偏移。
第三,GGUF 不是单一格式,而是"逐张量选择格式"的容器,位于转换→量化→加载→推理的四阶段流水线中间。 三次范式跃迁(块尺度 → 层次化尺度 → 码本/三值)构成了全书的骨架:每一次都在解决上一次的失效,也都付出了新的代价。
下一章,我们要开始"算账"了。量化到底损失了多少信息?误差从哪里来、怎么衡量?第 2 章会给出贯穿全书的分析工具:浮点表示、量化通式、误差分解,以及 PPL、KL、TTFT 这些评价指标——它们是后面每一个实验对比的裁判。

浙公网安备 33010602011771号