返回文章列表

量化模型从何成立:从 vLLM 的一次 FP8 故障谈起

文件里明明有低位权重,GPU 也支持 FP8,模型却不能正确计算。原因在于,量化从来不是简单地把 16 bit 改成 8 bit。低位数值只记录了编码结果,尺度、分组、张量布局和计算内核共同决定这些数值代表什么。

Admin
21 分钟阅读
3 次阅读
量化模型从何成立:从 vLLM 的一次 FP8 故障谈起

上周我提交给 vLLM 的 PR #53101 被合并。这个 PR 为 ModernBERT 补上了 FP8 量化支持。修复前,经过 LLM Compressor FP8_DYNAMIC 量化的 checkpoint 可能在加载时找不到 weight_scale;某些版本组合没有直接报错,服务接口甚至返回 HTTP 200,但 768 维 embedding 全部成了 JSON null。模型内部已经产生 NaN。

问题最终落在几处线性层上。ModernBERT 没有把运行时量化配置传给注意力和 MLP,checkpoint 里的 FP8 权重及其尺度因而没有被模型按预期解释。这是一类很有代表性的量化故障:文件里明明有低位权重,GPU 也支持 FP8,模型却不能正确计算。原因在于,量化从来不是简单地把 16 bit 改成 8 bit。低位数值只记录了编码结果,尺度、分组、张量布局和计算内核共同决定这些数值代表什么。

从这个问题出发,可以把模型量化讲得更准确一些。

量化首先是一个尺度问题

量化用有限精度数值近似原来的浮点张量。最常见的整数仿射量化把实数 xx 映射为整数 qq。设尺度为 s>0s>0,零点为 zz,整数格式可表示的范围为 [qmin⁡,qmax⁡][q_{\min},q_{\max}],量化与反量化写成:

q=clip⁡ ⁣(round⁡ ⁣(xs)+z, qmin⁡, qmax⁡)q=\operatorname{clip}\!\left(\operatorname{round}\!\left(\frac{x}{s}\right)+z,\ q_{\min},\ q_{\max}\right) x^=s(q−z)\hat{x}=s(q-z)

存进 checkpoint 或送入低精度矩阵乘法的是 qq,模型希望近似的是 xx,而 x^\hat{x} 是低位表示能够还原出的数值。尺度 ss 把二者联系起来。相同的整数 37 配上不同尺度,对应的实数可以相差几个数量级。缺少尺度的低位权重不是“精度差一点的权重”,而是一组没有确定数值含义的位模式。

对称量化通常令 z=0z=0,并取 s=max⁡∣x∣/qmax⁡s=\max|x|/q_{\max};非对称量化会根据观测到的最小值和最大值同时确定 ss 与 zz。前者映射简单,适合以零为中心的权重分布,后者能更充分地利用偏斜区间。二者都要解决同一个问题:有限数量的量化级应该覆盖多大的实数范围。

以向量 [−1,−0.2,0.3,0.9][-1,-0.2,0.3,0.9] 为例。做对称 INT8 量化时取 s=1/127≈0.00787s=1/127\approx0.00787,得到 [−127,−25,38,114][-127,-25,38,114],反量化后约为 [−1,−0.197,0.299,0.898][-1,-0.197,0.299,0.898]。这组误差很小,因为数值范围与量化网格匹配。若向量中混入一个 16,按绝对最大值取尺度会使 ss 增至约 0.126,原值 0.05 将直接舍入为 0。一个离群值占用了大量动态范围,其余数值能够使用的分辨率随之下降。

精度损失从哪里来

量化误差主要来自舍入和截断。数值位于可表示区间内、采用最近舍入时,标量误差通常满足 ∣x−x^∣≤s/2|x-\hat{x}|\leq s/2。数值超出区间后会被压到边界,此时误差不再受 s/2s/2 限制。尺度取得大,离群值更容易保留,但网格更稀;尺度取得小,主体分布更精细,尾部却会被截断。校准不是照抄张量的绝对最大值,而是在预期数据分布上选择量化区间,使舍入误差和截断误差保持在可接受范围内。

神经网络还会传播局部误差。若权重和输入的量化误差分别是 ΔW\Delta W 与 Δx\Delta x,原输出为 y=Wxy=Wx,则近似输出满足:

y^−y=(ΔW)x+W(Δx)+(ΔW)(Δx)\hat{y}-y=(\Delta W)x+W(\Delta x)+(\Delta W)(\Delta x)

误差大小取决于量化误差本身,也取决于输入幅度和权重算子的放大作用。逐元素误差很小,不能保证任务指标不变;某一层的余弦相似度略低,也不表示模型已经失去用途。模型的残差结构、归一化和后续非线性都会改变误差的传播方式。量化评估需要逐层数值检查,也需要端到端任务评测,两者不能互相替代。

离群值是低位量化中反复出现的问题。按张量量化让整个张量共享一个尺度,元数据少,内核也容易实现,但任意一个通道的极端值都可能降低其余通道的分辨率。按通道量化为每个输出通道保存尺度,按组量化则让连续若干权重共享尺度。粒度越细,尺度越贴合局部分布,代价是更多元数据和更复杂的访存、分片及内核逻辑。激活还可以按 token 动态计算尺度,以适应不同输入带来的范围变化。

“8 bit”是一句没说完的话

INT8 和 FP8 都占 8 bit,数值网格并不相同。整数在给定尺度下形成等间距网格。FP8 使用符号位、指数位和尾数位,数值间距随数量级变化。常见的 E4M3 用较多尾数位换取精度,E5M2 用较多指数位扩大动态范围。FP8 能容纳更宽的数量级,仍然需要尺度把张量分布放入格式的有效区间。只知道模型使用 FP8,无法判断它的误差。

量化对象也可能不同。权重在推理期间固定,适合提前压缩;激活取决于当前输入,可以使用校准得到的固定尺度,也可以在运行时动态求尺度;自回归模型的 KV cache 随上下文增长,压低其位宽通常是为了容纳更长上下文或更高并发。W4A16 指 4 bit 权重与 16 bit 激活,W8A8 指权重和激活都采用 8 bit。KV cache 精度是另一项独立设置。工程讨论中的“FP8 模型”至少要说明 FP8 用在权重、激活还是 KV cache。

低位格式也很少覆盖整条计算路径。矩阵乘法可以用 INT8 或 FP8 完成乘法,同时用 FP16、BF16 或 FP32 累加。归一化、Softmax、embedding 或对误差敏感的输出层往往保留较高精度。因此,量化方案应同时说明对象、数值格式、尺度粒度、尺度计算时机和累加精度。位宽只是其中一项。

低位模型是怎样得到的

预量化 checkpoint 在部署前完成权重转换,把低位权重、尺度、零点和分组信息一起写入模型文件。加载时量化则从 BF16 或 FP16 checkpoint 出发,在模型启动阶段生成低位权重。前者加载结果容易复现,启动时不必重新扫描全部权重;后者使用方便,但启动成本较高,也更依赖运行时实现。权重何时量化与激活尺度何时计算是两回事。一个 W8A8 FP8 模型可以使用预量化权重,同时在每次前向计算中为激活按 token 求尺度。

静态激活量化用校准数据提前估计范围,推理时复用固定尺度。它避免了在线归约,适合分布稳定、校准样本有代表性的场景。动态量化根据当前输入求尺度,对数据域变化更稳健,但要承担尺度计算和量化开销,也要求这些操作尽可能与矩阵乘法融合。动态量化 checkpoint 中没有持久化的 input_scale 并不异常,因为输入尺度属于运行时状态;预量化权重缺少 weight_scale 则通常无法被正确解释。

后训练量化(PTQ)在模型训练完成后确定量化参数。GPTQ 利用近似二阶信息调整权重舍入,目标是减小量化前后的层输出差异。AWQ 根据激活统计识别对输出更敏感的权重通道,再用缩放和截断保护这些通道。SmoothQuant 处理权重与激活的联合量化。对一个对角缩放矩阵 SS,线性变换可以改写为:

XW=(XS−1)(SW)XW=(XS^{-1})(SW)

适当选择 SS 可以把部分激活量化难度迁移到权重,同时保持高精度计算下的线性变换不变。GPTQ、AWQ 和 SmoothQuant 是控制误差的算法,不是新的位宽名称。

量化感知训练(QAT)把伪量化算子插入训练过程,让模型在前向传播时经历近似的舍入和截断。量化操作不可微,反向传播通常用直通估计器近似梯度。参数可以在训练中适应低位网格,因此 QAT 在极低位宽或严格误差预算下往往比 PTQ 稳定,成本是额外的数据、训练算力和实现复杂度。对不少 8 bit 模型,PTQ 已能满足部署要求;位宽继续降低后,QAT 的价值才更容易体现出来。

省下显存,不等于推理一定更快

量化的存储收益可以直接估算。若权重数为 NN,权重位宽为 bwb_w,共有 GG 个量化组,每组尺度和零点的位宽分别为 bsb_s 与 bzb_z,则权重与量化元数据占用近似为:

Mq≈Nbw8+G(bs+bz)8+MlayoutM_q\approx\frac{Nb_w}{8}+\frac{G(b_s+b_z)}{8}+M_{\mathrm{layout}}

MlayoutM_{\mathrm{layout}} 包括对齐、索引和打包布局。它解释了为什么 4 bit checkpoint 通常略大于原 BF16 权重的四分之一。尺度粒度越细,GG 越大;大矩阵中的尺度开销通常很小,小层或极小分组中的占比会更明显。

速度要结合硬件和负载分析。自回归解码常以较小批量反复读取大量权重,算术强度偏低,容易受显存带宽限制。权重变小以后,每个 token 需要搬运的数据减少,延迟有机会下降。大批量 prefill 和编码器中的大型矩阵乘法有更高的数据复用率,更可能受计算吞吐限制;只有 GPU 和内核原生支持对应的低精度矩阵乘法,低位格式才会转化为计算时间收益。

端到端延迟可以粗略写成:

Ttotal≈max⁡(Tmemory,Tmatmul)+Tscale+Tpack+TlaunchT_{\mathrm{total}}\approx\max(T_{\mathrm{memory}},T_{\mathrm{matmul}})+T_{\mathrm{scale}}+T_{\mathrm{pack}}+T_{\mathrm{launch}}

内存访问与矩阵计算可能重叠,所以主路径取二者较大值;其余项来自尺度计算、量化或解包以及内核调度。后端若没有匹配的低位内核,运行时可能先把权重反量化成 FP16/BF16 再计算。模型仍然更省存储,单请求却未必更快。动态量化没有做好融合时,额外归约和内核启动也会抵消收益。

单请求延迟与服务吞吐还应分开。量化释放的显存可以容纳更大的 batch 或更多 KV cache,即使单次前向耗时接近原模型,总吞吐仍可能提高。反过来,一个低精度 GEMM 微基准变快,也不能推出端到端请求同比加速,因为调度、通信和未量化算子没有消失。性能报告需要给出 GPU、内核、batch、序列长度、并发度和高精度基线。

回到 ModernBERT:缺失的不是一个参数名

PR #53101 处理的是 LLM Compressor FP8_DYNAMIC checkpoint。该配置保存预量化 FP8 权重,在运行时为激活动态计算尺度。修复前,ModernBERT 编码器没有把 vllm_config.quant_config 传入注意力与 MLP 中的线性层,MLP 的 Wi 仍使用普通 nn.Linear。checkpoint 声明的量化结构与运行时实际创建的层由此发生错位。

修复将 quant_config 沿 ModernBertEncoderLayer、ModernBertLayer 传入 ModernBertAttention 和 ModernBertMLP,覆盖注意力的 Wqkv、Wo 以及 MLP 的 Wi、Wo。Wi 改用 MergedColumnParallelLinear,以保留融合的 input/gate 分片边界。ModernBERT 有 22 个编码层,每层四个量化线性投影,因此修复后注册了 88 个 weight_scale 张量。模型没有持久化的 input_scale,因为激活尺度按 token 动态计算。池化层、分类头和 embedding 层不在这次量化范围内。

一份可执行的量化模型可以写成:

Mq=(Q,S,Z,G,L,K)\mathcal{M}_q=(Q,S,Z,G,L,K)

QQ 是低位张量,SS 是尺度,ZZ 是零点,GG 是分组规则,LL 描述逻辑层、分片边界和物理张量布局的对应关系,KK 是实现这些数值语义的内核。某些格式不使用显式零点,其余信息仍不能随意省略。没有 SS,QQ 无法还原为确定的实数;GG 或 LL 错了,尺度会落到错误的元素上;层没有绑定量化方法时,KK 不会按 checkpoint 声明的格式解释权重。PR 所做的事情,是让运行时的 LL 和 KK 与 checkpoint 中的 QQ、SS、GG 重新一致。

我在 PR 中加入了在线 FP8 的 MTEB 回归测试。测试确认四类线性投影都使用 Fp8PerTensorOnlineLinearMethod,并检查尺度张量与动态激活语义。RTX 4090 上实际选择的内核是 CutlassFP8ScaledMMLinearKernel。STS12 得分为 0.747408095,参考结果为 0.748193353,绝对差 0.000785258;同一环境下未量化模型得分为 0.748181615。三个示例 embedding 与参考输出的平均余弦相似度为 0.997556。

这些数据支持该测试范围内的正确性判断,不构成吞吐提升的证据。余弦相似度是发现方向性数值异常的烟雾测试,STS12 是任务级回归,两者都不能外推为所有数据集上的精度结论。PR 也没有做性能实验。ModernBERT 是编码器模型,如果要讨论 FP8 是否加速,还需要在固定输入长度和 batch 下测量显存峰值、端到端编码延迟、吞吐及内核占比。

怎样判断一个量化模型真的可用

检查应从运行时结构开始。目标层有没有绑定预期量化方法,权重、尺度和零点是否完整注册,张量形状与分组数是否一致,融合投影的边界是否保留,最终选中的又是哪一个内核。配置文件里写着 quantization: fp8,只能说明用户表达了这个意图,不能证明程序确实执行了 FP8 路径。PR 对四类投影和 88 个权重尺度的断言解决的正是这个问题。

接着看数值。NaN、Inf、全零输出和异常范数应被直接检查,再根据模型类型选择误差指标。embedding 适合比较方向与范数,语言模型可以比较 logits、困惑度或 token 一致性,长序列生成还要观察误差是否累积。接口成功返回不代表计算正确。修复前的 HTTP 200 和全 null embedding 已经给出了反例。

任务评测回答量化误差有没有改变模型用途。embedding 模型应在检索、相似度或分类基准上比较,高精度基线和量化模型要使用相同的数据与评测流程。校准样本最好不要与最终评测集重合。单个 benchmark 上的小幅差异,只能说明该条件下没有观察到明显退化,不能证明两个模型在其他语言、长度或数据域上等价。

最后才是部署指标。显存、端到端延迟、稳态吞吐、并发容量和功耗分别回答不同问题。要容纳更大的模型,就看加载后和峰值显存;要降低服务成本,就看吞吐与功耗;面向交互请求时,尾延迟通常比单次平均值更有用。所有结果都应带上硬件、软件版本、内核、batch 和序列长度。没有这些条件,“量化后更快”无法复现,也无法证伪。

写在最后

模型量化是一种有损数值编码。它用更少的位表示权重、激活或 KV cache,通过尺度和分组控制误差,再依靠匹配的内核执行低精度计算。它通常能节省显存;是否加速,要看瓶颈落在内存还是计算,也要看硬件有没有真正执行这种格式。

scale 不是 checkpoint 边角上的元数据,而是模型数值语义的一部分。低位权重、尺度、层映射和内核缺一项,程序就可能在“正常运行”的表象下给出无意义的结果。评价量化也应保持同样的边界。位宽说明表示容量,量化算法说明误差如何处理,结构检查、任务评测和系统基准才说明这个模型能不能被正确使用。

延伸阅读

本文涉及的数值格式与方法可参阅 Micikevicius 等人的 FP8 Formats for Deep Learning、Frantar 等人的 GPTQ、Xiao 等人的 SmoothQuant 和 Lin 等人的 AWQ。embedding 评测背景见 MTEB。量化配置定义见 LLM Compressor compression schemes。

发表评论

未登录评论需提供昵称和联系邮箱,提交后默认进入审核。

0/1000