两个日常场景。其一:在 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" · version=3 · 张量/KV 计数元数据(带类型的 KV 对)general.architecture = "llama"llama.context_length = 4096tokenizer.ggml.tokens = [……整套词表]张量目录名字 · 形状 · 类型 · 数据区偏移……对齐填充(默认 32 字节)……张量数据按偏移直接寻址,可整块 mmap改架构、加字段=加一条 KV分词器也在箱子里,单文件即全部对齐+偏移 ⇒ mmap 零拷贝GGUF = 权重 + 分词器 + 架构元数据 + 量化信息:一只自带说明书的行李箱
GGUF 文件解剖:文件头、带类型的 KV 元数据、张量目录、对齐的张量数据

解剖开看是四段结构:

  • 文件头: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,那么:

原权重 w4 bit 整数码 q还原值 ŵ = 0.2 × q误差
-1.20-6-1.200
-0.86-4-0.80+0.06
-0.11-1-0.20-0.09
0.2810.20-0.08
1.4071.400

“量化有损”指的就是最后这列舍入误差。推理时并不会神奇地找回原值;计算内核会在乘加过程中按 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:32 个权重一块的朴素账本32 个 fp16 权重64 字节32 个 4bit 码16 字节+1 个 fp16 尺2 字节18 字节 ÷ 32 = 4.5 bit/权重(原 16 bit → 体积 28%)还原:权重 ≈ 4bit 码 × 尺;一块共用一个比例尺,块内细节被抹平Q4_K:256 个一超块,8 组各有局部 scale 和 min32 个32 个32 个32 个32 个32 个32 个32 个scale/minscale/minscale/minscale/minscale/minscale/minscale/minscale/min16 个小参数各压成 6 bit+2 个 fp16 总尺128 B 权重码 + 12 B 局部参数 + 4 B 总尺 = 144 B144 字节 × 8 ÷ 256 = 4.5 bpw;_M 是模型级混合精度配方,不是块内字段
块量化的账本:Q4_0 每 32 个权重花 18 字节;Q4_K 每 256 个权重用 4 bit 码、分块 scale/min 和两个总尺,共 144 字节

所以 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 块为例:

  1. 256 个权重组成一个 super-block(超块);
  2. 超块再分成 8 组,每组 32 个权重;
  3. 每组都有自己的 scale 和 min,能贴合这一小段权重的范围;
  4. 这 8 组的 scale 和 min 不直接用 fp16 保存,而是各自量化成 6 bit;
  5. 整个超块再用两个 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 的超块布局
MMedium 混合配方:多数张量用 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 不同量化文件大小说话:

体积(GB)F1613.4 GBQ8_07.16 GB · 8bitQ6_K5.53 GBQ5_K_M4.78 GBQ4_K_M4.08 GB ★ 甜点区(发布者推荐)Q3_K_M3.30 GBQ2_K2.83 GB · 质量明显损失数据:TheBloke/Llama-2-7B-GGUF 文件表。位宽线性降体积,质量非线性掉——4bit 一带最划算
同一个 7B 模型的体积阶梯:从 F16 的 13.4 GB 到 Q2_K 的 2.83 GB(数据:TheBloke/Llama-2-7B-GGUF)

规律很清楚:位宽每降一档,体积近乎线性地掉;质量损失却是非线性的——4 bit 一带通常是甜点区,而 2 bit 一带被该发布页标注为「significant quality loss」。

三、为什么端侧世界都围着它转

  • mmap 的体感:模型「秒开」;关掉再开几乎瞬时(页缓存还热着);两个程序加载同一个模型,内存只占一份;
  • 块结构对 CPU 友好:32/256 的块长贴合 SIMD 指令(AVX、NEON)的批处理宽度——这是 llama.cpp 在纯 CPU 上也能像样跑的底气;
  • 层卸载:显存装得下几层就搬几层进 GPU(-ngl N),其余留在内存里由 CPU 算——8GB 显存的机器也能跑 13GB 的模型,拿速度换可行性;
磁盘model.ggufmmap内存(按页映射)layer 0–3(CPU 算)layer 4–7(CPU 算)layer 8–11 ↗ 搬去 GPUlayer 12–15 ↗ 搬去 GPUlayer 16–… ↗ 搬去 GPU-ngl N显存(装得下的层)GPU 快算这几层显存装几层就搬几层(-ngl N),其余留 CPU:8GB 显存也能跑 13GB 模型——拿速度换可行性mmap 的另一红利:多个进程映射同一文件,共享一份页缓存;关掉再开几乎瞬时
mmap + 层卸载:单文件按页映射进内存,前若干层搬进显存,其余 CPU 慢慢算
  • 生态滚雪球:Ollama、LM Studio、llamafile、koboldcpp……端侧工具几乎全是 llama.cpp 家族,认的都是 GGUF;HuggingFace 原生托管 GGUF,网页上能直接预览文件里的元数据。

四、它和 vLLM、SGLang、Triton 是什么关系

这几个名字经常被放在一起比,其实它们住在不同楼层:

服务平台层数据中心里管多模型部署NVIDIA Triton Inference Server ②推理引擎层真正跑 attentionllama.cppvLLMSGLangTensorRT-LLM吃 GGUF吃 safetensors / GPTQ / AWQ …内核语言层怎么写 GPU 算子CUDAMetalOpenAI Triton ①权重格式层装数据的箱子GGUFsafetensors① 写核的语言 ② 管服务的平台——两个 Triton 分住两层,撞名纯属巧合,别混为一谈
四层积木:权重格式 → 内核语言 → 推理引擎 → 服务平台;两个 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 和它们的量化生态。两个世界各自繁荣,中间隔着的不是优劣,是场景。

参考