模型量化这个词,这两年出现的频率越来越高。只要你在做模型部署,不管是端侧、边缘盒子还是服务端推理加速,几乎绕不开它。但很多人对量化的理解停留在“把 float32 换成 int8”这一句话上,真到动手的时候,发现精度掉了、速度没起来、算子不支持,一堆问题冒出来。我自己在几个实际项目里踩过不少坑,从最开始照着文档跑 PTQ 结果精度崩掉,到后来慢慢摸清楚浮点表示、量化误差来源、PTQ 和 QAT 各自的适用边界,才算真正把这条链路走通。
这篇内容我打算把模型量化这件事从头讲透,重点放在两个地方:一是浮点数到底是怎么表示的,为什么量化会带来误差;二是 PTQ(训练后量化)和 QAT(量化感知训练)这两种范式各自的逻辑、适用场景和实操细节。整篇会偏原理加实操结合,适合已经做过模型训练、准备做部署优化的同学,也适合刚接触量化、想把底层逻辑搞明白的工程师。读完你应该能自己判断:我手上这个模型,该用 PTQ 还是 QAT,量化位宽怎么选,精度掉了该往哪个方向排查。
1. 浮点表示:量化误差的源头就在这里
要理解量化为什么会产生误差,得先搞清楚浮点数在计算机里是怎么存的。很多人跳过这一步直接调 API,结果遇到精度问题完全不知道从哪下手。我一开始也是这样,后来被一个激活值溢出问题卡了两天,才回头把浮点表示重新捋了一遍。
1.1 IEEE 754 浮点格式的组成
现在主流的浮点表示遵循 IEEE 754 标准,单精度 float32 占 32 位,分成三段:1 位符号位、8 位指数位、23 位尾数位。双精度 float64 则是 1 位符号、11 位指数、52 位尾数。这个结构决定了浮点数能表示的动态范围非常大,但精度是有限的。
符号位决定正负,指数位决定数量级,尾数位决定有效精度。举个具体的例子,float32 能表示的数值范围大约在 1.4e-45 到 3.4e38 之间,但有效数字只有大约 7 位十进制。也就是说,一个 float32 数能精确到小数点后六七位,再往后就是近似了。
这里有个关键点:浮点数的精度是相对的,不是绝对的。数值越大,两个相邻可表示浮点数之间的间隔就越大。比如在 1.0 附近,float32 的最小间隔大约是 1.19e-7;但到了 1000000 附近,最小间隔就变成了 0.0625 左右。这个特性直接影响了量化的设计思路。
1.2 定点与浮点的本质差异
量化本质上是把浮点数映射到定点整数上。定点数的特点是小数点位置固定,整数部分和小数部分的位数是预先分配好的。int8 就是 8 位定点整数,能表示 -128 到 127 这 256 个离散值。
浮点和定点的核心差异在于:浮点用指数来动态调整精度,定点用固定的缩放因子来映射。量化的时候,我们需要确定一个 scale(缩放因子)和一个 zero_point(零点),把浮点区间线性映射到整数区间。公式大致是这样:
real_value = scale * (quantized_value - zero_point)这个映射是线性的,意味着原本在浮点空间里分布不均匀的数值,被强行拉到了均匀的整数网格上。误差就来自这里:原本两个很接近的浮点数,量化后可能落到同一个整数格点上,信息就丢了。
1.3 为什么量化误差不可避免
理解了浮点的相对精度特性,就能明白量化误差为什么不可避免。假设一个层的权重分布范围是 [-3.5, 3.5],用 int8 表示,256 个格点平摊到 7 的区间上,每个格点间隔约 0.027。原本浮点能区分 1e-7 级别的差异,量化后只能区分 0.027 级别的差异,精度损失是必然的。
但这里有个反直觉的地方:量化误差不一定导致模型精度下降。神经网络本身有一定的冗余和鲁棒性,很多权重对最终输出的影响很小,量化带来的扰动在可接受范围内。真正的问题在于那些对精度敏感的层,比如某些归一化层、注意力里的 softmax 输入,量化后误差会被放大。
提示:判断一个模型能不能量化,先看它的权重和激活值分布。如果分布很集中、离群点少,量化友好;如果分布很散、有极端离群值,量化难度就大。
1.4 对称量化与非对称量化的选择逻辑
量化方案分对称和非对称两种。对称量化把零点固定在 0,正负区间对称映射,公式简单,计算快,适合权重这种大致对称分布的张量。非对称量化的零点可以移动,能更好地拟合非对称分布,比如 ReLU 之后的激活值全是非负的,用非对称量化能减少浪费。
实际项目里,权重通常用对称量化,激活值用非对称量化,这是大多数推理框架的默认策略。原因很直接:权重的分布一般围绕 0 对称,对称量化不浪费整数区间;激活值经过 ReLU 后偏向一侧,非对称量化能更充分利用 256 个格点。
选择哪种方案,核心看数据的分布形态。你可以先统计一下张量的 min、max、均值、标准差,如果均值接近 0 且分布对称,对称量化更合适;如果分布明显偏斜,非对称量化误差更小。
2. 量化粒度:从 per-tensor 到 per-channel 的取舍
确定了量化方案,接下来要决定量化粒度。粒度决定了 scale 和 zero_point 是每个张量共享一个,还是每个通道、每个组各有一个。这个选择对精度影响很大,也是很多人调量化时容易忽略的一环。
2.1 per-tensor 量化的适用与局限
per-tensor 量化是整个张量共用一个 scale。优点是简单、存储开销小、计算快,因为所有元素用同一套参数。缺点是如果张量内部各通道的数值范围差异很大,用统一的 scale 会导致小范围的通道精度严重损失。
我遇到过一个卷积层,某些通道的权重范围是 [-0.1, 0.1],另一些是 [-2, 2]。用 per-tensor 量化,scale 由最大的范围决定,小范围通道的权重几乎全被压到 0 附近,精度直接崩了。这种情况就得换更细的粒度。
2.2 per-channel 量化的精度优势
per-channel 量化给每个输出通道单独算 scale,能显著缓解通道间范围差异带来的精度问题。代价是存储和计算开销增加,因为每个通道都要存一组量化参数。对于卷积层,per-channel 通常按输出通道维度来做。
实测下来,per-channel 量化在大多数视觉模型上能把精度损失从几个百分点降到零点几个百分点,代价是推理时多一点点反量化开销。现在主流的推理引擎对 per-channel 的支持都比较好,除非有特殊硬件限制,我一般优先选 per-channel。
2.3 分组量化与更细粒度的探索
再往细走还有分组量化(group-wise),把通道分成若干组,每组一个 scale。这在一些大模型量化里比较常见,比如按 128 个元素一组。粒度越细,精度越好,但元数据开销越大,计算也越复杂。
粒度选择本质上是在精度、存储、速度之间做权衡。我的经验是:小模型、通道差异不大的,per-tensor 够用;中等模型、通道差异明显的,per-channel 是甜点;大模型或者对精度极敏感的,考虑分组量化。
| 量化粒度 | 精度 | 存储开销 | 计算复杂度 | 适用场景 |
|---|---|---|---|---|
| per-tensor | 低 | 最小 | 最低 | 通道分布均匀的小模型 |
| per-channel | 中高 | 中等 | 中等 | 大多数卷积/线性层 |
| group-wise | 高 | 较大 | 较高 | 大模型、精度敏感场景 |
2.4 粒度选择的实操判断方法
怎么判断该用哪种粒度?我的做法是先跑一遍 per-tensor,看精度掉多少。如果掉得在可接受范围内,就用 per-tensor,省事。如果掉得厉害,换成 per-channel 再测。还不行,再考虑分组。
具体操作上,可以逐层统计权重的通道间范围差异,算一个比值:最大通道范围除以最小通道范围。如果这个比值超过 10,per-tensor 大概率会出问题,直接上 per-channel。这个判断方法我在好几个项目里验证过,比较靠谱。
3. PTQ:训练后量化的完整链路与踩坑记录
PTQ 是最常用的量化方式,因为不需要重新训练,拿训练好的浮点模型直接量化就行。但“直接量化”这四个字背后有不少细节,处理不好精度会掉得很难看。
3.1 PTQ 的基本流程
PTQ 的核心步骤是:拿一批校准数据跑一遍模型,统计各层的激活值分布,据此算出 scale 和 zero_point,然后把权重和激活都量化成整数。校准数据不需要标签,通常几百张图片或者几百条样本就够。
流程大致是:准备浮点模型 → 准备校准数据集 → 插入观测节点统计分布 → 计算量化参数 → 转换模型 → 验证精度。每一步都有坑,下面逐个说。
3.2 校准数据集的选择策略
校准数据的分布必须和真实推理数据接近,这是 PTQ 精度的关键。我见过有人图省事拿训练集的前 100 张图做校准,结果那 100 张恰好都是某一类,量化后其他类的精度全崩了。
正确的做法是从验证集或真实业务数据里随机采样,保证类别均衡、场景覆盖全面。数量上,几百到一千条通常够用,太少统计不准,太多没必要。如果业务数据有多个分布(比如白天和夜间图像),校准集要都覆盖到。
注意:校准集不要用训练集,训练集可能有过拟合痕迹,统计出的分布和推理时不一致。用验证集或独立采样的业务数据更稳。
3.3 观测节点的插入与分布统计
观测节点负责在推理时记录张量的 min、max 或者直方图。不同框架的观测方式不一样,有的用 min-max,有的用移动平均,有的用 KL 散度。min-max 简单但对离群值敏感,一个极端值就能把 scale 拉大,导致其他值精度损失。
KL 散度方法会找一个最优的截断阈值,把离群值裁掉,让主体分布映射得更充分。实测下来,KL 散度在激活值量化上通常比 min-max 好,尤其是激活值有长尾分布的时候。但 KL 计算慢一些,需要更多校准样本。
3.4 精度掉点后的排查链路
PTQ 精度掉了,别急着换 QAT,先按这个链路排查:
第一步,看是哪一层掉的。逐层对比量化前后的输出,找到误差最大的层。通常问题集中在少数几层,不是全局问题。
第二步,看那层的数值分布。如果是激活值有极端离群值,考虑换 KL 散度或者加截断。如果是权重的通道差异大,换 per-channel。
第三步,看是不是某些算子不支持量化,被回退成了浮点。这种混合精度的情况有时反而更慢,因为要来回转换。
第四步,检查校准集是否代表性不足。换一批校准数据再试。
我遇到过一次,精度掉了 5 个点,排查半天发现是某个 concat 操作的输入范围差异太大,量化后小范围那路信息全丢了。后来给那一路单独做了处理,精度就回来了。
3.5 PTQ 的适用边界
PTQ 不是万能的。对于大模型、对精度极敏感的模型、或者权重分布特别不友好的模型,PTQ 可能怎么调都达不到要求。这时候就得上 QAT。判断标准很简单:如果 PTQ 调完精度还是差 2 个点以上,且业务不能接受,就转 QAT。
4. QAT:量化感知训练为什么能救回精度
QAT 的思路是在训练过程中模拟量化误差,让模型学会适应这种误差。相比 PTQ 的“事后补救”,QAT 是“事前适应”,精度通常更好,但代价是要重新训练,成本高。
4.1 QAT 的核心机制:伪量化
QAT 的关键是伪量化(fake quantization)节点。前向传播时,它把浮点值量化再反量化,模拟量化误差;反向传播时,用直通估计器(STE)把梯度直接传过去,绕过量化操作的不可导问题。
这个设计很巧妙:模型在训练时“感受”到了量化带来的扰动,权重会朝着对量化更鲁棒的方向更新。训练完再把伪量化节点替换成真正的量化算子,精度损失就小很多。
4.2 直通估计器的作用与局限
STE 解决了量化不可导的问题,但它本身是个近似。梯度直接穿过量化操作,没有考虑量化函数的真实导数,这会导致梯度估计有偏差。实践中这个偏差通常可以接受,但在某些敏感层上可能引起训练不稳定。
我的经验是,QAT 训练时学习率要比正常训练小,通常小一个数量级。因为伪量化引入了噪声,学习率太大会震荡。另外,QAT 的 warm-up 阶段可以先不插入伪量化,让模型先适应一下,再逐步开启。
4.3 QAT 的训练配置要点
QAT 训练有几个关键配置:
- 学习率:比原始训练小,通常 1e-5 到 1e-4 量级
- 训练轮数:不需要从头训,在预训练权重基础上微调几个 epoch 就行
- 伪量化插入位置:权重和激活都要插,但某些敏感层可以跳过
- Batch Norm 处理:QAT 时 BN 的统计量要冻结或者特殊处理,否则会引入额外误差
4.4 PTQ 与 QAT 的选型决策
到底用哪个?我总结了一个判断流程:
先跑 PTQ,看精度。如果满足要求,直接用 PTQ,省时省力。如果差一点,先调 PTQ 的粒度、校准方法、截断策略,看能不能救回来。如果调完还差 2 个点以上,或者模型本身对量化很敏感(比如一些轻量级网络、检测模型的小目标分支),直接上 QAT。
还有一个现实因素:QAT 需要训练资源和时间。如果项目周期紧、没有 GPU 资源,PTQ 是唯一选择,那就得在模型设计阶段就考虑量化友好性。
| 维度 | PTQ | QAT |
|---|---|---|
| 是否需要训练 | 否 | 是 |
| 精度 | 一般 | 更好 |
| 成本 | 低 | 高 |
| 适用场景 | 大模型、精度要求不苛刻 | 小模型、精度敏感 |
| 调参难度 | 中 | 高 |
4.5 QAT 实操中的常见问题
QAT 训练时最常见的问题是精度不升反降。原因通常有几个:学习率太大、伪量化插入位置不对、BN 统计量没处理好。我遇到过一次,QAT 后精度比 PTQ 还差,排查发现是 BN 的 running mean 和 variance 在量化后失配了,冻结 BN 统计量后就正常了。
另一个坑是 QAT 训练完导出模型时,伪量化节点没正确替换成量化算子,导致推理时还是浮点计算,速度没提升。导出后一定要检查模型结构,确认量化算子真的生效了。
5. 量化位宽与混合精度的实战权衡
位宽选择是量化里另一个关键决策。int8 是主流,但 int4、int16 也有各自的应用场景。位宽越低,压缩率和加速越明显,但精度损失越大。
5.1 int8 为什么是当前的主流
int8 在精度和效率之间取得了很好的平衡。相比 float32,模型体积压缩 4 倍,内存带宽需求降低 4 倍,在支持 int8 加速的硬件上推理速度能提升 2 到 4 倍。精度损失在大多数模型上可以控制在 1 个点以内。
硬件支持也是 int8 普及的重要原因。主流推理芯片、移动端 NPU、服务端 GPU 都对 int8 有专门优化。相比之下,int4 的硬件支持还不完善,很多平台跑不起来或者加速比不明显。
5.2 int4 与更低比特的探索
int4 能把模型再压缩一半,对大模型部署很有吸引力。但 int4 的精度损失明显,通常需要配合分组量化、混合精度等技术。目前 int4 主要用在权重上,激活值还是保持 int8 或更高。
更低比特比如 int2、二值化,学术研究里有,工业落地很少,因为精度损失太大,除非模型本身极度冗余。我的建议是,除非有极端的存储或带宽限制,否则不要轻易上 int4 以下。
5.3 混合精度量化的设计思路
混合精度是指不同层用不同位宽。敏感层用高位宽,鲁棒层用低位宽。比如第一层和最后一层通常对精度敏感,保持 int8 甚至 float;中间层可以尝试 int4。
设计混合精度方案需要逐层做敏感度分析:把某一层单独量化,看精度掉多少,掉得多的就是敏感层。这个分析比较耗时,但能显著提升整体压缩率。实操中可以用自动化工具跑一遍敏感度扫描,生成每层的敏感度排序,再据此分配位宽。
5.4 位宽选择的决策依据
位宽选择没有标准答案,取决于你的约束条件。如果存储和带宽是瓶颈,优先考虑更低位宽;如果精度是硬指标,保守用 int8。我的经验是,先明确业务对精度、延迟、体积的硬性要求,再倒推位宽方案,而不是反过来。
6. 量化落地的工程细节与避坑清单
原理讲完了,最后说说工程落地时那些文档里不会写的细节。这些是我在实际项目里踩出来的经验,能帮你少走弯路。
6.1 量化友好型模型设计
如果项目还在模型设计阶段,提前考虑量化能省很多事。几个原则:避免使用量化不友好的算子,比如某些自定义激活函数;控制权重的动态范围,避免极端离群值;BN 层的位置和参数要合理,因为它影响激活值分布。
6.2 推理引擎的算子支持差异
不同推理引擎对量化算子的支持不一样。有的引擎某些算子只支持 per-tensor,有的对非对称量化支持不好。选引擎之前,先确认它对你模型里用到的算子支持到什么程度,否则量化完发现一堆算子回退到浮点,加速效果大打折扣。
6.3 量化模型的验证方法
量化后不能只看整体精度,还要看逐层误差、输出分布变化、极端样本的表现。我通常会做三件事:整体精度对比、逐层输出余弦相似度、bad case 分析。逐层相似度能快速定位问题层,bad case 分析能发现量化在特定场景下的失效。
6.4 常见问题速查表
| 问题现象 | 可能原因 | 排查方向 |
|---|---|---|
| 精度大幅下降 | 校准集不具代表性 | 换校准数据 |
| 某层误差特别大 | 该层分布有离群值 | 换 KL 散度或截断 |
| 速度没提升 | 算子回退浮点 | 检查算子支持 |
| QAT 不升反降 | 学习率过大或 BN 失配 | 调小学习率、冻结 BN |
| 输出全为常数 | scale 计算错误 | 检查量化参数 |
6.5 从 PTQ 到 QAT 的平滑过渡
如果 PTQ 效果不理想准备转 QAT,不要从头训。在 PTQ 得到的量化参数基础上初始化 QAT 的伪量化节点,能让训练更快收敛。另外,QAT 训练时可以先用较小的量化位宽做课程学习,逐步收紧,效果比一步到位好。
我个人在实际操作中的体会是,量化这件事没有银弹,PTQ 和 QAT 各有各的适用场景,关键是把浮点表示、量化误差来源、粒度选择这几个底层逻辑搞清楚,遇到问题才能有的放矢地排查。很多时候精度掉点不是量化方法的问题,而是校准数据、算子支持、模型结构这些外围因素导致的。把排查链路走顺了,大部分问题都能定位到具体原因。