两个日常场景。其一:在 HuggingFace 搜一个模型,正主仓库是十几个 safetensors 分片加一堆 json;旁边总跟着个「-GGUF」仓库,里面躺着一排单文件——Q4_K_M.gguf(4.08 GB)、Q8_0.gguf(7.16 GB)……其二:ollama run qwen3 一行命令,笔记本就把模型跑起来了。这两件事背后是同一位主角:GGUF。
这篇讲清楚三件事:这个文件里装了什么;Q4_K_M 这串黑话怎么读;以及它和 vLLM、SGLang、Triton 们是什么关系——先剧透:不是竞品,是不同楼层的住户。
一、一只自带说明书的行李箱
GGUF(GPT-Generated Unified Format)是 llama.cpp 作者在 2023 年 8 月定下的模型文件格式(规格见 ggml 仓库,PR #302 定稿),取代此前几度破坏兼容的 GGML/GGJT。设计目标写得很直白:一个文件、自带一切、可以扩展、可以 mmap。
解剖开看是四段结构:
- 文件头:magic 就是
GGUF四个字节,加上版本号(当前是 3)和张量、元数据的数量; - 带类型的 KV 元数据:架构(
general.architecture = "llama")、上下文长度、RoPE 参数……连分词器的整个词表(tokenizer.ggml.tokens)都存在文件里。前代格式的教训正在这里:超参数是一列无类型的裸值,加个字段就得整个换格式;改成带类型的 KV 后,新字段随便加,旧程序照读不误; - 张量目录:每个张量的名字、形状、类型、以及在数据区的偏移;
- 张量数据:按目录偏移直接寻址,默认 32 字节对齐(可用
general.alignment调整)。
对齐加偏移是为了一件事:mmap。操作系统把文件直接映射进虚拟内存,用到哪页读哪页——启动飞快,几个程序加载同一个模型共享一份页缓存,权重还能被 SIMD 指令和 GPU DMA 直接取用,省掉「先整个读进内存再摆好」。
对比一下就明白单文件的价值:HF 原生格式是一家子文件——config.json、tokenizer.json、外加 N 个权重分片,谁都不能丢;GGUF 是一只箱子,拎走就走。另外它是纯数据——和 safetensors 一样,不像 pickle 那样能夹带可执行代码。
二、Q4_K_M 黑话解码
GGUF 文件名里那串后缀是一套量化命名系统。它压缩的是模型里最占空间的权重张量。如果“张量、浮点数、量化”这些词还很陌生,先从模型到底存了什么讲起。
2.1 模型权重,本质上是一大堆小数
LLM 的绝大部分计算都是矩阵乘法。矩阵里的每个数叫一个权重,例如:
[-1.20, -0.86, -0.11, 0.03, 0.28, 0.83, 1.09, 1.40, ...]
训练结束后,这些权重在推理时基本不再变化。假设用 fp16 保存,每个权重固定占 16 bit,也就是 2 字节。一个 7B 模型光权重的理论下限便约为:
70 亿 × 2 字节 ≈ 14 GB
问题在于,fp16 能表示六万多个不同的有限数值,而推理未必需要每个权重都保留这么细。量化做的事,就是把许多相近的小数归到有限的几个“刻度”上。4 bit 只能编码 2^4 = 16 个刻度,但再给它配一把记录实际间距的尺子(scale),就能覆盖不同大小的数。
最简单的近似可以写成:
整数码 q = round(原权重 w ÷ 比例尺 s)
近似值 ŵ = s × q
假设 s = 0.2,可用整数范围是 -8~7,那么:
原权重 w | 4 bit 整数码 q | 还原值 ŵ = 0.2 × q | 误差 |
|---|---|---|---|
| -1.20 | -6 | -1.20 | 0 |
| -0.86 | -4 | -0.80 | +0.06 |
| -0.11 | -1 | -0.20 | -0.09 |
| 0.28 | 1 | 0.20 | -0.08 |
| 1.40 | 7 | 1.40 | 0 |
“量化有损”指的就是最后这列舍入误差。推理时并不会神奇地找回原值;计算内核会在乘加过程中按 scale 解释这些整数码,或边算边反量化。之所以还能工作,是因为神经网络有大量参数和冗余,对许多微小扰动并不敏感。但误差会沿着几十层传播,所以位宽不能无止境地降。
2.2 为什么不让整个模型共用一把尺
如果几百亿个权重只共用一个 scale,一个特别大的离群值就会把尺子拉得很粗:大数装得下了,密集分布在零附近的小数却都挤到同一两个刻度里。反过来,如果每个权重各带一个 scale,虽然精确,却比原文件还大。
块量化(block quantization) 是折中方案:把相邻的一小组权重放进同一个块,每块共用少量辅助数据。块越小,尺子越贴合局部分布、误差通常越小;但尺子数量越多,元数据开销也越大。
2.3 先看 Q4_0:数字 4 不等于实际只占 4 bit
Q4_0 是最容易看懂的基准。它每 32 个权重分成一块:
- 32 个 4 bit 整数码,占
32 × 4 = 128 bit = 16 字节; - 全块共用一个 fp16 scale,占 2 字节;
- 合计 18 字节,而不是理想中的 16 字节。
所以 Q4_0 的实际成本是:
18 字节 × 8 ÷ 32 个权重 = 4.5 bit/weight
这就是常见的 bpw(bits per weight,每权重平均位数)。名字里的 4 只说每个整数码的主体位宽;文件还要支付 scale、min、少量未量化张量和 GGUF 元数据,因此一个“4 bit 模型”并不等于总参数数乘以 4 bit,也不保证整个文件恰好是 fp16 的四分之一。
Q4_0 中的 _0 是这一基础块格式的版本/变体名,不是“零误差”。另一个常见的 Q4_1 会额外保存块的最小值,相当于使用:
近似值 ŵ = scale × q + offset
这种带偏移的仿射量化能让 16 个刻度更贴合不以零为中心的分布,代价是再多一份辅助数据。
2.4 再看 Q4_K:把“尺子本身”也压缩
一把尺管 32 个权重仍然可能太粗,但直接给更多小组各存一个 fp16 scale 又很贵。K-quants 用了一层“总尺管小尺”的层级结构。
以当前 ggml 里的 Q4_K 块为例:
- 256 个权重组成一个 super-block(超块);
- 超块再分成 8 组,每组 32 个权重;
- 每组都有自己的 scale 和 min,能贴合这一小段权重的范围;
- 这 8 组的 scale 和 min 不直接用 fp16 保存,而是各自量化成 6 bit;
- 整个超块再用两个 fp16 值
d、dmin,负责把这些 6 bit 小尺还原出来。
因此一个 Q4_K 超块的字节账本是:
256 个 4 bit 权重码 128 字节
8 个 6 bit scale + 8 个 6 bit min 12 字节
2 个 fp16 总尺 d / dmin 4 字节
────────────────────────────────────────
合计 144 字节
144 × 8 ÷ 256 = 4.5 bpw
有趣的是,它和 Q4_0 都是 4.5 bpw,但预算花法不同。Q4_0 是“32 个数 + 一把直接可用的尺”;Q4_K 是“256 个数 + 8 组局部 scale/min + 两把总尺”。后者用层级量化压低了辅助数据成本,同时让每组拥有独立的范围,通常能用相近体积换来更小的误差。
这里的 K 只是在说它属于 K-quant 超块家族,不是 kilo、KV cache,也不是“第 K 个版本”。Q2_K / Q3_K / Q4_K / Q5_K / Q6_K 的具体块布局并不完全相同,不能简单理解成只把同一个结构的位宽从 2 改到 6。
2.5 最后看 _M:它是一张混合精度配方
现在可以逐段读 Q4_K_M 了:
| 片段 | 含义 |
|---|---|
Q | 常规量化类型 |
4 | 主体权重码以 4 bit 为主,不等于整文件恰好 4 bpw |
K | 使用 K-quant 的超块布局 |
M | Medium 混合配方:多数张量用 Q4_K,敏感张量使用更高精度 |
一个模型不是一整块权重,而是由词嵌入、Attention 的 Q/K/V/O 投影、FFN 的 up/gate/down 投影、输出层等许多张量组成。它们对误差的敏感程度不同。与其让所有张量一刀切成 Q4_K,llama-quantize 会给更敏感或体积相对较小的张量多花几位。
在典型 Q4_K_M 配方里,多数二维权重是 Q4_K,部分 Attention V、FFN down、输出/词嵌入等会按模型架构和张量位置升到 Q5_K、Q6_K 或保留其他类型。具体名单是 llama.cpp 量化器的策略,不同架构和版本可能调整;因此文件名描述的是主要类型和整套配方,不是承诺“每一个权重都是同一种 4 bit 编码”。
S / M / L 可以直观理解为 Small / Medium / Large 的质量—体积档位,但不是每个量化家族都同时提供三档。通常同一位宽下,档位越往后,更多张量会得到较高精度,文件稍大、质量也更稳。Q4_K_M 常被推荐,正是因为它处在体积、速度和质量的甜点区。
2.6 imatrix 又解决了什么
上面的分块策略主要根据权重本身的数值范围控制误差,但“数值误差小”不一定等于“对模型输出影响小”。某个权重看起来很小,乘上经常出现的巨大激活值后,仍可能非常重要。
imatrix(importance matrix,重要性矩阵) 来自一批校准文本:先让未量化模型跑这些样本,记录各权重输入方向的激活强度,再把有限的量化精度优先留给更影响输出的位置。它不是存储用户聊天内容的运行时缓存,而是量化模型时使用的统计信息。
低位宽的 I-quants(尤其 IQ1、IQ2 和部分 IQ3 类型)会更依赖 imatrix,因为 1~3 bit 可选的码字太少,仅按数值距离舍入很容易选错。K-quants 也能从 imatrix 获益,但它们通常不以 imatrix 作为基本前提。
2.7 三个最容易误解的结论
- Q4_K_M 不是一种单独的 4 bit 数据结构。
Q4_K是块结构,M是全模型的混合精度配方。 - 量化主要压的是静态权重。 实际运行还需要 KV cache、激活值、计算图和程序本身,所以“5 GB 模型文件”不代表只要 5 GB 内存;上下文越长,KV cache 通常越大。
- 更小不必然更快。 权重更小能减少内存带宽压力和磁盘读取,但速度还取决于硬件是否有对应量化内核、是否需要额外反量化、多少层在 GPU 上,以及提示词长度等因素。
这套梯子的真实效果,用 TheBloke 发布的同一个 Llama-2-7B 不同量化文件大小说话:
规律很清楚:位宽每降一档,体积近乎线性地掉;质量损失却是非线性的——4 bit 一带通常是甜点区,而 2 bit 一带被该发布页标注为「significant quality loss」。
三、为什么端侧世界都围着它转
- mmap 的体感:模型「秒开」;关掉再开几乎瞬时(页缓存还热着);两个程序加载同一个模型,内存只占一份;
- 块结构对 CPU 友好:32/256 的块长贴合 SIMD 指令(AVX、NEON)的批处理宽度——这是 llama.cpp 在纯 CPU 上也能像样跑的底气;
- 层卸载:显存装得下几层就搬几层进 GPU(
-ngl N),其余留在内存里由 CPU 算——8GB 显存的机器也能跑 13GB 的模型,拿速度换可行性;
- 生态滚雪球:Ollama、LM Studio、llamafile、koboldcpp……端侧工具几乎全是 llama.cpp 家族,认的都是 GGUF;HuggingFace 原生托管 GGUF,网页上能直接预览文件里的元数据。
四、它和 vLLM、SGLang、Triton 是什么关系
这几个名字经常被放在一起比,其实它们住在不同楼层:
- 权重格式层:GGUF、safetensors——装数据的箱子;
- 内核层:CUDA、Metal,以及 OpenAI 的 Triton——一种写 GPU 算子的语言,vLLM 和 SGLang 的不少内核用它实现;
- 推理引擎层:llama.cpp、vLLM、SGLang、TensorRT-LLM——真正跑 attention 的人;
- 服务平台层:NVIDIA 的 Triton Inference Server——数据中心里管多模型部署的框架。
先把重名解掉:两个 Triton 不是一回事——一个是写核的语言(OpenAI),一个是管服务的平台(NVIDIA),撞名纯属巧合。
真正的选型分野在引擎层,而引擎的分工跟着场景走:
| llama.cpp(+ GGUF) | vLLM / SGLang(+ safetensors 等) | |
|---|---|---|
| 主场 | 端侧、单机、个人使用 | 数据中心、高并发 API 服务 |
| 硬件 | CPU、消费级 GPU、Apple Silicon | 服务器 GPU 集群 |
| 内存招式 | mmap + 量化 + 层卸载 | PagedAttention 把 KV 缓存分页管理(vLLM);RadixAttention 用基数树跨请求复用前缀(SGLang) |
| 吞吐招式 | 单人低延迟 | 连续批处理:请求逐条进出,不等整批凑齐 |
| 量化生态 | GGUF 的 K / I 系列(CPU 友好) | GPTQ、AWQ、FP8 等(GPU 友好) |
那 GGUF 能不能直接进数据中心?vLLM 确实做了 GGUF 加载,但官方文档原话是:「高度实验性且欠优化」,可能与其他特性不兼容。这不是谁不努力——GGUF 的块状布局本来就是为 CPU 的 SIMD 设计的,GPU 引擎要么把它反量化回浮点、要么重排数据,两头不讨好。格式从来不是中立的,它总是偏向自己的主场。
五、写在最后
回头看,GGUF 做对的事情很简单:把「跑一个模型」的门槛降到「下载一个文件」。格式里的每个设计——单文件、带类型的 KV、对齐、mmap——都在为这一个目标服务。
选型也就一句话:自己用、设备五花八门,投 llama.cpp 家族和 GGUF;对外提供高并发服务,投 vLLM / SGLang 和它们的量化生态。两个世界各自繁荣,中间隔着的不是优劣,是场景。
参考
- GGUF 规格(ggml 官方文档)
- llama.cpp quantize 工具说明与量化讨论(k-quants、i-quants)
- ggml 量化块结构定义与 llama.cpp 混合量化策略
- TheBloke/Llama-2-7B-GGUF(体积阶梯与发布者推荐标注的数据来源)
- vLLM: Efficient Memory Management with PagedAttention(arXiv:2309.06180)
- SGLang: Efficient Execution of Structured Language Model Programs(arXiv:2312.07104,RadixAttention)
- vLLM 的 GGUF 支持(官方文档,标注高度实验性)
- OpenAI Triton、NVIDIA Triton Inference Server