量化不是压缩包:从模型分层到 4-bit 推理的工程真相
量化为什么既能让大模型装得下,也可能跑得更快;为什么 4-bit 常是甜点位;以及分层、校准、质量评估、生成围栏与思考链失真的工程边界。
大模型量化最容易被讲成一句话:把 16-bit 权重压到 4-bit,模型缩小四倍。 这句话没有错,却漏掉了几乎所有真正困难的部分。
量化首先解决的是容量问题:权重太大,显存或统一内存放不下。它还有一个 潜在收益——速度可能更快:decode 常常受内存带宽限制,权重每生成一个 token 都要重新读一遍,4-bit 权重比 BF16 少搬很多字节。但“文件更小”不自动等于 “推理更快”。如果运行时先把 4-bit 全量反量化成 F16/F32,或者设备没有对应的 packed kernel,容量与带宽收益会被部分甚至全部抹掉。
所以量化不是一个文件转换选项,而是一条贯穿模型规格、权重装配、设备驻留、 算子和质量验证的完整执行路径。
一、先分层:模型是什么,权重怎样存,是两件事
在 zLLM 中,模型层没有被做成一个包办一切的 Model 黑盒。它被拆成几类稳定职责:
model_spec/<model> 架构常量:层数、hidden、attention、MoE、tokenizer id
runtime/<model> LayerSpec 展开与完整 prefill/decode 编排
weight/model checkpoint 张量名、shape 校验、模型装配
weight/format FP8、W4A16、GGUF K-quant 等字节布局
weight/codec 无 IO 的 reference decode 与布局转换
backend + kernel 权重驻留、格式分派与设备直接计算
这套分层有一个关键结果:模型架构不等于权重编码。同一个线性层可以来自 BF16、 FP8、NVFP4、W4A16 或 Q4_K;格式层只解释字节,模型适配层只认名字和 shape, runtime 只描述数据流,backend 才根据设备能力选择 kernel。
这也避免了两种常见污染:格式解析代码里写满某个模型的张量名,或者 GPU kernel
里硬编码 6144、256 之类的模型维度。前者使新格式无法跨模型复用,后者使
“支持一种模型”伪装成了“实现一个通用算子”。
二、量化到底解决什么
1. 第一目标:让权重装得下
忽略 scale、元数据和未量化张量,一个 (N) 参数模型仅权重的理论体积约为:
BF16 / FP16:2N bytes
INT8: 1N bytes
INT4: 0.5N bytes
因此 70B 模型的裸 BF16 权重约 140 GB,理想 4-bit 约 35 GB。实际 GGUF Q4_K_M 会高于严格的 4 bit/weight,因为分块 scale、minimum、对齐,以及被保留为更高精度 的敏感张量都占空间。量化也只压缩它所覆盖的数据:KV Cache、activation、临时 buffer 和运行时本身仍要单独预算。
在离散 GPU 上,这决定模型能否进入显存;在 Apple UMA 上,它决定模型、KV、 系统和应用能否共享同一物理内存而不触发严重内存压力。UMA 省掉了传统的 Host→VRAM staging,不代表内存容量变成无限。
2. 第二目标:减少搬运,换取速度
单 token decode 的大矩阵乘常接近 GEMV:计算复用低,瓶颈往往是读取权重,而不是 乘加次数。若 4-bit 权重保持 packed,kernel 在寄存器或局部块内解码并立即累加, 一次读取能带来接近四倍于 BF16 的有效权重密度。这是量化可能加速的根本原因。
但加速有三个前提:
- 设备有适合该格式的直接 kernel;
- 解码、scale 处理、activation 转换和调度开销没有吃掉带宽收益;
- 测的是完整 prefill/decode,而不是孤立的解码函数。
zLLM 在真实 Qwen3.8 CPU decode 路径中,Q4_K packed AVX2/FMA 直接点积曾把 16-token decode 从 107.483 秒降到 7.578 秒;继续融合相邻 gate/up 后为 7.183 秒。这个约 15 倍的变化包含“删除逐块 F32 展开”和专用向量化的共同收益, 不能解释成 4-bit 对 BF16 的普遍加速倍数。反例同样重要:Gemma 4 的某个 Q4_K 双输出 kernel 虽通过 CPU oracle,完整链路却从 35.9 降到 35.5 tok/s,最终 被回退。量化只提供更低的带宽下限,能否兑现要看整条路径。
三、基本思路:不是全局舍入,而是利用权重局部性
最粗糙的量化,是拿整张矩阵的最大最小值建立一个比例尺:
q = round(w / scale) # 对称量化
w_hat = scale * q
q = round((w - offset) / scale) # 非对称量化
w_hat = offset + scale * q
问题在于,权重分布并不均匀。少数离群值会把全局量程拉大,让大部分普通值挤在 少数几个整数格子里。现代权重量化因此依赖三种“分层、分级、局部”的选择。
第一层是块内局部性。 不给整张矩阵共用一个 scale,而是按 32、64、128 或 256 个权重分组。每组有自己的 scale,组越小越能贴合局部分布,但元数据占比、 访存复杂度和 kernel 成本也越高。
第二层是张量和通道的敏感度。 attention 的 value/output、FFN down projection、 embedding、LM head 对误差的敏感度可能不同,甚至同一张量不同通道也不同。 因此 “Q4_K_M” 里的 M 不是所有权重都机械使用同一精度,而是让重要张量用更高 档位。AWQ 的核心观察也类似:由 activation 分布识别少量 salient weights,保护 它们比只最小化权重自身误差更有效。
第三层是层级局部性。 误差会沿深度传播,前层造成的 hidden 偏移会改变后层 路由、attention 和最终 token 排序。逐层或逐块校准比一次全局量化更能控制累积 误差;MoE 还要覆盖真实的 expert 路由分布,否则“没有在校准集里被选中的专家” 可能被草率量化。
这就是为什么量化格式通常不仅保存低 bit code,还保存分组 scale、minimum, 甚至 importance matrix。有效 bit 数也因此不是标签上的整数:以 llama.cpp 的 Llama 2 表为例,Q4_K_M 的实际平均约为 4.80–4.84 bits/weight,而不是正好 4。
常见 GGUF 量化格式怎么选
先澄清一个经常被混用的概念:GGUF 是容器格式,不是某一种量化算法。一个
GGUF 文件保存 metadata、tokenizer、tensor directory 和张量数据;其中不同张量
可以分别使用 F32、F16、Q4_K、Q6_K、IQ4_XS 等类型。因此文件名写着
Q4_K_M,不代表里面每个 tensor 都是同一种 4-bit block。
常见格式可以分成四组:
| 家族 | 常见名称 | 结构与用途 | 选择提示 |
|---|---|---|---|
| 经典 block | Q4_0、Q5_0、Q8_0 | 一组整数 code 配一个 scale,布局简单、kernel 覆盖广 | Q4_0 适合兼容优先;Q8_0 常作高质量对照或量化点积的 activation 格式 |
| K-quant | Q3_K、Q4_K、Q5_K、Q6_K | 256 权重 super-block 内再保存子块 scale/minimum | Q4_K_M 是常见起点;Q5_K_M、Q6_K 用容量换质量 |
| I-quant | IQ2_、IQ3_、IQ4_NL、IQ4_XS | importance-aware、非线性码本或查表解码 | 同 BPW 下质量可能更好,但必须确认目标 backend 有直接 kernel |
| 混合配方 | Q3_K_S/M/L、Q4_K_S/M、Q5_K_S/M、UD-* | 按 tensor 敏感度分配不同类型 | 后缀描述整模型配方;实际类型应读取 GGUF tensor directory 确认 |
S/M/L 最容易被误读。它们通常表示 Small、Medium、Large 的混合精度方案:
例如某些 attention 或 FFN 张量升到更高 bit,其他张量保持主档位。它们不是简单的
“同一种 Q4 多三个压缩级别”,实际 BPW 也会随模型结构而变。
IQ4_XS 与 Q4_K_M 都在 4-bit 附近,却不是可随意互换的同一布局。前者使用
非线性量化/码本思路,把有限 code 更贴近真实权重分布;后者使用成熟的 K-quant
分层 scale。前者可能以更低 BPW 获得很强质量,后者通常拥有更广泛、稳定的设备
kernel。选择时要同时看模型质量、文件大小和当前 backend 的直接执行白名单。
实际选型可以从下面的顺序开始:资源非常紧才进入 IQ2/Q2/Q3;大多数本地部署先测
Q4_K_M 或已有高质量校准的 IQ4_XS;质量敏感且内存允许则测 Q5_K_M/Q6_K;Q8_0
适合作为接近高精度的量化基线。任何发布者自定义的 UD-Q4_K_XL 等名称,都应先
检查内部 tensor types——它可能在不同层混用 Q4_K、Q5_K、Q6_K 和 IQ4_XS,
只支持 Q4_K 的 kernel 并不一定能加载整份模型。
四、为什么 4-bit 常是甜点位
“甜点位”不是说 4-bit 永远最好,而是说它经常处在三条曲线的交点:容量已经大幅 下降,设备仍容易高效解码,质量损失还没有像 2/3-bit 那样陡增。
llama.cpp 对 Mistral 7B、WikiText-2、512 context 的一组同口径测试很直观:
| 格式 | perplexity | 相对 FP16 增幅 |
|---|---|---|
| Q3_K_S | 6.0021 | 5.44% |
| Q3_K_M | 5.8489 | 2.75% |
| Q4_K_S | 5.7349 | 0.75% |
| Q4_K_M | 5.7259 | 0.59% |
| Q5_K_S | 5.7100 | 0.31% |
从 Q3_K_M 到 Q4_K_M,困惑度增幅从 2.75% 降到 0.59%;再从 Q4_K_M 加到 Q5_K_S,只改善到 0.31%,但权重带宽和体积继续增加。这正是典型的“膝点”。 另一组 Llama 33B 数据中,FP16、Q6_K、Q5_K_M、Q4_K_M、Q3_K_M 的 perplexity 分别为 4.1557、4.1598、4.1675、4.2081、4.3594,也呈现相似趋势。
这些数字只能证明该模型、该数据集、该 context、该量化器下的趋势。perplexity 不是聊天质量、代码正确率或长思考稳定性的同义词;0.59% 也不表示每个回答只差 0.59%。接近决策边界的 token、长链条中的早期分叉、MoE router 的 Top-K 变化, 都可能把很小的局部误差放大成完全不同的输出。
因此更稳妥的经验是:
- 资源够时,Q5/Q6 更接近基线;
- 容量和 decode 带宽都紧时,优先从现代 grouped/mixed 4-bit 开始;
- 3-bit 以下需要更强的校准、重要性加权或训练补偿,不能只做朴素舍入;
- embedding、LM head、norm、router、视觉投影等敏感部分是否保留高精度,要由 模型结构与任务测试决定。
五、为什么模型“可以被量化”
神经网络不是一套每一步都要求 bit-exact 的符号程序。大量参数共同形成冗余表示, 相邻的权重值常能映射到相同量化格点而不改变宏观能力;LayerNorm/RMSNorm、残差 连接和过参数化也给小扰动留下了容忍空间。量化利用的是这种统计冗余。
生成本身又是概率过程。模型输出的是下一个 token 的 logits,经 softmax、temperature、 top-k/top-p 和随机采样才成为文本。温度大于零时,即使权重完全相同,两次回答也 可能不同;即使 greedy 解码,两个非常接近的 logit 也可能因微小数值扰动交换顺序。
但这不能推出“量化误差无所谓”。正确说法是:开放式生成通常没有唯一字符串答案, 评价对象往往是“好”和“更好”,而不只是“对”和“错”;量化质量必须用分布和任务 指标判断,而不能只做逐字比较。数学、代码、工具参数、JSON schema 等任务仍然有 硬正确性边界。
量化模型偶尔还会在某个 benchmark 上高于浮点基线。这可能来自量化噪声打破了 原有错误偏好,也可能只是有限样本、采样方差或评测器噪声。它说明质量不是逐位 单调的,不说明量化普遍提升模型。只有跨 seed、跨数据集、带置信区间的重复结果, 才值得称为反向收益。
六、训练与校准在补偿什么
训练后量化(PTQ)不改模型参数,只用少量代表性样本估计 scale、clipping、通道 重要性或 Hessian 近似。GPTQ 逐层补偿量化一个权重对其余权重造成的误差;AWQ 利用 activation 识别并保护显著权重。它们的共同点是:优化目标不再是“每个权重 离原值最近”,而是“这一层在真实输入上输出尽量不变”。
量化感知训练(QAT)则在训练/微调时模拟舍入与裁剪,让参数主动适应低精度格点。 它成本更高,但在 3-bit、2-bit、activation 量化或高敏感任务上更重要。校准集也必须 贴近部署分布:代码模型用纯百科文本校准,长推理模型只用短句校准,MoE 模型遗漏 大量 expert,都可能得到漂亮的平均误差和糟糕的真实输出。
还要区分权重量化、activation 量化与 KV Cache 量化。W4A16 只把权重降到 4-bit, activation 仍以 16-bit 参与计算;W4A8 会进一步减少流量,却引入动态范围和融合 kernel 的新约束;KV 量化影响长上下文容量,也可能持续扰动 attention。三者不能 用一个“4-bit 模型”标签混在一起。
七、围栏:约束结构,但不要伪造正确性
量化后最危险的质量问题,不一定表现为乱码。更常见的是 logit margin 变小、思考链 提前分叉、长推理逐步漂移,或者工具调用在某个标点上选错 token。
工程上需要三层围栏:
- 算子围栏:reference decode、CPU oracle、逐层 hidden/logit 误差、真实 shape 校验,先证明低精度路径实现正确;
- 生成围栏:JSON Schema、工具名、结束标签和 stop token 在采样前进入 token fence,保证结构合法;重复循环由独立的 generation guard 处理;
- 质量围栏:固定 prompt 的 greedy 回归、perplexity、任务集、长上下文、 多 seed 开放式评测,以及关键任务的人工审查。
生成围栏只能保证“格式允许”,不能保证参数语义正确。更不能因为加了 JSON 修复器, 就把量化造成的 tool selection 漂移藏起来。对于思考链,也不应把“逐字一致”设为唯一 目标:chain-of-thought 本来就可能因采样而变化,而且可见推理文本不等于模型内部 计算。更可靠的办法是同时检查最终答案/工具结果、关键中间约束、结束原因、循环率、 长度分布,以及同一解码策略下的成功率变化。
尤其要防止一种假通过:短题、greedy、最终答案碰巧一致,于是宣布量化无损。长链条 中每一步的小 logit 偏移都可能累积;应加入需要数千 token 的推理样本,记录量化前后 的答案正确率、异常重启、重复、提前 EOS 和超长尾。围栏负责拦住协议级灾难,评测 负责发现语义退化,两者不能相互替代。
八、工程中最容易踩的坑
“支持”必须拆成四种状态
能解析文件
≠ CPU reference 能解码
≠ 设备存在 packed 直接 kernel
≠ 某模型默认端到端路径已经使用
这四种状态必须分别记录。zLLM 在装配期验证完整执行路径:量化权重没有目标设备 kernel 就加载失败,Metal 路径禁止偷偷反量化成 F16 来掩盖能力缺口。
packed 必须一直保持到 kernel
加载时展开 F32 也许最容易写,却把体积和访存优势清零。正确的数据生命周期是: 容器定位 packed bytes,format 解释布局,model 校验名字和 shape,backend 让它驻留, kernel 按块读取 code/scale 并直接累加。临时解码只在寄存器、SIMD lane 或短生命周期 scratch 中发生。
不要忽略非权重内存
权重从 16-bit 降到 4-bit,不代表峰值内存恰好除以四。长上下文 KV、prefill activation、MoE expert 工作集、视觉 encoder、output logits 和 command buffer 都可能 成为新峰值。容量规划必须列出每份数据的位置、格式、消费者和生命周期。
性能必须端到端测
分别记录 TTFT、prefill tok/s、decode tok/s、峰值内存、SSD 等待、GPU 利用率与温度。 量化 GEMV 快了,不代表 command submission、反量化 activation 或 output head 没有成为 新瓶颈。移动设备和无风扇 Mac 还要区分冷机与热稳态。
正确性不能只看“输出通顺”
至少要依次验证:codec 对拍、单算子 oracle、逐层差分、固定 token 回归、完整任务集。 shape 与 dtype 以 checkpoint 实测为准;norm 是一维还是二维、某个投影是 BF16 还是 FP8,任何“按道理应该”都不如加载时断言。
结语
量化的真正价值不是把模型文件做小,而是重新安排“精度、容量、带宽和计算”之间的 预算。4-bit 常成为甜点位,是因为它在许多模型上跨过了容量门槛,也仍能保留接近 高精度的统计质量,并且适合现代设备做 packed 直算;它不是一个脱离模型、数据、 硬件和任务的魔法数字。
一条可信的量化路径应当同时回答四个问题:哪些权重以什么粒度被量化,哪些敏感部分 被保护;设备是否真的直接消费 packed 格式;速度收益是否出现在完整请求中;质量 围栏是否覆盖长思考、结构化输出和真实任务。只有这四个答案都明确,“模型能跑”才 能升级为“模型值得被部署”。
