☰
模型量化落地实战:从PTQ到QAT的精度修复与MQBench评测
2026/10/14 9:07:15 网站建设 项目流程

把模型量化落地到推理后端这件事,我前前后后折腾了大半年。起因很简单:模型在GPU上F1跑得挺好,一转到边缘盒子上的INT8,精度直接掉到没法看。试遍了各种PTQ后处理,精度始终差着一截,最后老老实实走上了QAT这条路,并把测评框架换成了MQBench。这篇就是我当时实操过程的完整记录,包括原理、配置、实验设计,以及那些在官方文档里根本找不到的坑。适合正在做模型压缩、想把量化模型真正部署到ONNX Runtime、MNN这类后端的同学参考,也适合那些在PTQ和QAT之间犹豫不决的人。

1. 为什么我们需要QAT:一次部署翻车现场

1.1 量化为什么会掉点

先把量化的家底摊开说。理论上,8bit量化做的事情很简单:把一个浮点张量r映射到整数域,公式是q = round(r / scale) + zero_point,推理的时候再反量化回浮点。这个过程中必然引入两类误差,一是舍入误差,round这个操作天然丢精度;二是裁剪误差,超出量化范围的值被硬生生截断。

这两类误差在权重上通常还好说,因为训练收敛后权重分布往往比较集中,范围好估算。真正麻烦的是激活值。激活值在逐层传播过程中范围不断变化,尤其是深度网络中间层的分布可能非常宽,一旦某些极端值落在量化范围之外,裁剪掉的信息量可能远超你想象。

我之前那个模型就是典型症状:FP32精度93.6%,直接转INT8掉到91.1%,看起来只差两个多点,但具体到业务场景里,误判率增加了将近一半,这完全没法接受。

1.2 PTQ与QAT的本质差异:被动接受误差与主动适应误差

PTQ的思路是“事后修补”:先用一小部分数据跑一遍前向,统计出每层权重和激活的范围,把scale和zero_point定死,然后整个模型转成整数运算。这套流程快,不需要重训,但对分布敏感的网络很吃亏,尤其是低比特场景,误差一旦落在敏感层,没有任何机制去纠正它。

QAT则完全不同,它是在训练阶段就把量化误差模拟进去。模型在前向计算时,权重和激活会经过一个伪量化节点,先量化再反量化,让网络“亲眼看到”量化噪声长什么样,然后通过反向传播调整参数,让权重分布主动往“量化友好”的方向走。说白了,PTQ是让模型被动接受误差,QAT是逼模型主动学会在误差中生存。

1.3 MQBench在这个问题里扮演什么角色

当时我面临两个选择:一是直接用PyTorch原生的QAT接口,二是用MQBench这种统一评测框架。PyTorch官方接口本身没问题,但它只解决“训练时模拟量化”这一步,无法告诉我模型在ONNX Runtime、MNN这类真实后端的表现。

MQBench吸引我的核心就一点:**它的评测链是后端感知的。**同一条量化管线,可以分别跑到不同推理框架里做端到端验证,而不是在PyTorch里自嗨。这对我来说是刚需——因为量化的最终目的是部署,不是发论文。

2. QAT的底层机制:伪量化与STE直通

2.1 伪量化节点在forward里做了什么

理解QAT,先理解伪量化节点。以8bit为例,每个伪量化节点做的事情如下:

def fake_quantize(r, scale, zero_point, bit_width=8): q = torch.clamp( torch.round(r / scale) + zero_point, min=0, max=(2**bit_width - 1) ) r_fake = (q - zero_point) * scale return r_fake

输入一个浮点张量,输出一个经过量化再反量化的浮点张量。**数值上它还是浮点,但已经包含了量化误差。**模型训练时的损失函数会把这些误差如实反映出来,梯度据此更新参数。

这里有个关键细节:为什么训练时用伪量化,不直接上真正的整数算子?因为整数算子在反向传播里压根没有梯度定义。伪量化节点保留了浮点运算的骨架,只是为了在训练阶段“预演”量化噪声。

2.2 STE为什么能用:梯度近似与缩放

伪量化的前向公式里,round函数本身是不可导的,数学上它的导数几乎处处为零。如果不做处理,梯度传到伪量化节点就断了。

解决方案是STE,straight-through estimator,直通估计器。它做的事可以说相当粗暴:前向传播时老老实实量化,反向传播时把round的梯度直接当作恒等映射,让上游的梯度原封不动穿过去。

在MQBench的实现里,伪量化层的梯度行为通常类似这样:

class FQ(torch.autograd.Function): @staticmethod def forward(ctx, x, scale, zero_point): ctx.save_for_backward(x, scale) q = torch.clamp(torch.round(x / scale) + zero_point, 0, 255) return (q - zero_point) * scale @staticmethod def backward(ctx, grad_output): x, scale = ctx.saved_tensors grad_input = grad_output.clone() # STE:直接让梯度通过,对超出量化范围的值可追加裁剪 grad_input[x < -scale * zero_point] = 0 grad_input[x > scale * (255 - zero_point)] = 0 return grad_input, None, None

意思很简单:**被裁剪掉的极端值梯度清零,落在量化范围内的值梯度原样通过。**这个近似看起来粗糙,但实验已经证明它足够引导模型往低误差方向收敛。

我自己理解STE有效性的一层逻辑是:round的误差本身是零均值的高频噪声,在大量参数和大量迭代下,这种噪声方向各异、互相抵消,不会把模型带到沟里去;反倒是裁剪误差是系统性的、有方向的,对训练影响更大。所以STE配合裁剪区域梯度置零,等于把最有害的那部分信号屏蔽掉了。

2.3 训练流程设计:先baseline再QAT的两个阶段

QAT不是随便train from scratch。我踩过两次直接从头训的坑,结果都是收敛慢、精度还不如PTQ。正确做法是两个阶段:

  • 阶段一:普通训练或微调,得到一个稳定的全精度baseline。
  • 阶段二:在baseline权重上开启伪量化节点,用小学习率做若干轮适配训练。

为什么必须这样?因为QAT的全程都伴随量化噪声,参数空间被扰动,网络收敛速度会明显变慢。如果一开始就让它负重前行,模型可能根本没见过“结构完整的特征”,一直在跟误差搏斗。先拿到一份靠谱的全精度权重,再来做量化适应,等于给模型一个很好的起点,让它只需要在原有基础上微调参数,而不是重新学。

我那次的配置是:baseline阶段用标准SGD训了90轮,收敛到93.6%;QAT阶段选择性地冻结前几层,只开启后面敏感层的伪量化,初始学习率设为1e-4,跑30轮,最后精度到93.2%,比PTQ的91.1%好了足足两个点。

3. MQBench的工程化优势:让评测从“看起来准”变成“真的准”

3.1 四个抽象层与统一评测管线

MQBench的工程设计思路非常适合量化研究人员,它把整条评测链路抽象成四个部分:数据集、模型、量化器、后端。

我在另外一个项目里自定义了一个小型分类网络,接进去只需要实现一个函数签名。对比一下,如果你在PyTorch原生的量化流程里写,backward要自己管,评测要自己写,后端适配更要自己对接,MQBench把这些全部收编了。

它的核心价值是横向对比的公平性。同一份模型权重、同一个校准集合、同一个量化器,分别在ONNX Runtime和PyTorch上跑,结果差异一目了然。这种“统一变量”的思路,让“哪个量化算法更好”这个问题第一次有了可复现的答案。

3.2 后端感知:PyTorch模拟和真实部署的精度差距

这是我最看重的部分。PyTorch里的伪量化测出来的精度,和真正导出到ONNX Runtime之后跑出来的值,从来不是一回事。造成差距的原因有很多,比如算子融合方式不同、CPU指令集对量化算子的支持差异、各种维度对齐和内存布局的细节。

当初我用原生PyTorch QAT测出来精度93.4%,导出到ONNX Runtime一测,只有92.6%。不是量化算法不行,是后端实现有差异。MQBench直接在目标后端上做端到端评测,这个信息差就消失了。

做量化的同学一定要记住一个原则:部署精度永远是最终裁判,训练模拟精度只用来做内部参考。

3.3 同一套配置做PTQ和QAT:从零到一跑通最小实验

MQBench的配置系统做得很克制的优雅,PTQ和QAT跑的是同一套配置,差别只在一个开关。我当时的做法大致如下:

# 先克隆仓库并安装依赖 git clone https://github.com/example/mqbench.git cd mqbench pip install -e . # PTQ模式:跑一个ResNet18的快速评测 python tools/run.py --config configs/ptq/resnet18_imagenet.yaml # QAT模式:同一个模型,开启量化感知训练 python tools/run.py --config configs/qat/resnet18_imagenet.yaml

跑通之后你会得到一个模型文件和一个评测报告,报告里包含了多个后端的精度指标。整个过程没有额外写一行训练逻辑,全部由框架编排。

我当时用ImageNet的一个子集做了最小化验证,大概100个类别的图片,两三分钟就能跑完一轮PTQ评测,非常适合做快速迭代。等方案成熟了,再上完整数据集做最终验证。

4. 一次完整QAT实验的配置解读

4.1 先定实验目标:三步走的方案

很多人在QAT里容易乱调参数,我后来沉淀了一个固定的三步走方案:

  1. 先跑PTQ摸底。用MinMax校准器,快速得到量化精度的下限。如果PTQ精度本来就过得去,那QAT未必需要。
  2. 再做敏感层分析。逐层关闭伪量化节点,看哪几层对精度影响最大,这些就是QAT要重点照顾的层。
  3. 最后上QAT。小学习率、短周期,让模型适应量化噪声。

这个顺序帮我省了很多无谓的实验。之前有次我直接上QAT,训了一个星期,回头一看发现瓶颈根本不在量化误差,而是分类头结构对噪声太敏感,换一下结构就解决了。

4.2 量化配置逐行看:位宽、粒度、校准器、量化器

以我当时一个模拟项目X的配置为例,大致长这样:

backend: type: onnxruntime dataset: type: imagenet_subset root: /data/datasets/imagenet_sub batch_size: 128 model: type: resnet18 pretrained: true quantizer: type: lsq # 可选:lqs / doReFa / fixed bit: weight: 8 activation: 8 calibrator: type: minmax # PTQ阶段使用,QAT阶段可忽略 per_channel: true # weight通常per-channel效果好

这里每个字段背后都有讲究:

  • bit:8bit起步,想做更低比特(4bit以下),建议换更先进的量化器,MinMax校准基本不够用。
  • per_channel:权重多采用per-channel,即每输出通道独立一个scale。激活则通常per-tensor,因为很多后端算子不支持per-channel激活量化。
  • 量化器:fixed就是普通的STE,lsq代表Learnable Step Size Quantization,scale本身是一个可学习参数,会在训练中更新,QAT首选。

4.3 QAT训练超参:学习率、epoch数与BN处理

QAT阶段最常见的失误就是把学习率设太大。我实测下来:

  • 学习率:1e-4到1e-5量级比较稳。大于1e-3基本必翻车,预训练权重会被冲乱。
  • epoch数:10到30轮之间。太短模型来不及适应,太长则容易过拟合校准数据。
  • 优化器:Adam在QAT阶段表现得比SGD稳,尤其是LSQ量化器需要更新scale参数时,Adam对scale的梯度处理更平顺。

{% hint style="warning" %} BN层是个容易被忽略的坑。QAT训练时BN的running_mean、running_var会不断更新,这些统计量是FP32的,它们对量化噪声非常敏感。如果训练结束时不处理,导出的scale可能是基于一个不稳定的激活统计算出来的。稳妥做法是训练结束后把BN层折叠进卷积层,或者对BN统计量做一次额外校准。 {% endhint %}

4.4 实验结果怎么解读:精度、算子和后端匹配

我那次实验的结果参考如下:

方案精度备注
FP32 baseline93.6%无量化,直接推理
PTQ (MinMax, INT8)91.1%校准集1000张图
QAT (LSQ, INT8)93.2%微调30轮,lr=1e-4
QAT (LSQ, INT8) + ONNX Runtime92.9%导出后实测

从表里能看出来,QAT比PTQ涨了两个多点,和FP32的差距基本小到可以接受。但导出到ONNX Runtime之后又掉了一点,这就要去查后端算子支持情况了。精度数字不是终点,两端对齐才是。

5. 实战中的坑与排查链路

5.1 激活范围漂移:训练结束了scale还在变

第一次跑QAT,我发现训练过程挺顺利,loss稳定下探,但模型每次推理结果都不一样,同一张图两次预测能差出两个类别。

排查了半天,最后定位到激活scale上。原来是LSQ量化器的scale参数从头到尾都在更新,训练结束时scale还没收敛,推理阶段scale每秒都在变,导致模型行为不稳定。

解决方案是在训练尾部把scale参数冻住:在最后五轮把scale的learning rate设为0,只更新权重。这招很有效,模型立刻稳定了。

5.2 BN统计与量化噪声耦合:收敛不稳定的根因

另一个坑是训练loss一直在震荡,怎么调学习率都不见好转。后来看了每一层的输出分布才发现,BN层的统计量始终没稳定下来。

原因在于:伪量化节点改变了激活值分布,BN的running_mean/running_var也跟着持续调整,而这两个统计量又反过来影响下一轮的量化范围,形成了一个耦合振荡。收敛难度直线上升。

解决办法:在训练初期就启用伪量化,但BN统计量用更大的动量(比如0.99改成0.999),让统计量变化得更平滑。效果立竿见影,loss曲线稳定了很多。

5.3 后端算子差异:PyTorch里99%到ONNX Runtime只有97%

这是个最让人崩溃的场景。整个训练流程调得毫无问题,PyTorch模拟精度99.1%,兴冲冲导出到ONNX Runtime,一路跌到97.3%。

排查后才明白,问题出在两处:

  • 某些自定义注意力算子在ONNX Runtime上不支持INT8量化,直接回退成FP32了。
  • 部分卷积层在ONNX Runtime下采用了不同的融合策略,量化误差的累积路径和PyTorch不一致。

应对方法有两个:一个是养成开测前先跑一遍MQBench后端评测的习惯,尽早发现算子缺口;另一个是导出的onnx模型用Netron看一下节点类型,重点确认每个量化算子都落在了预期实现上。

5.4 排查五步法:如何一步步定位问题

经历了这些坑之后,我整理了一套排查链路:

  1. 先确认数据流:量化开关是否真的生效?有些工具默认关闭QAT,只做了PTQ。
  2. 逐层对比激活分布:开启伪量化后,记录每层的输入输出分布,和FP32对比,定位误差最大的层。
  3. 冻结参数缩小范围:先冻结scale只跑权重适配,再固定权重只更新scale,看哪一步出了效果。
  4. 单层复测:只对候选问题层开启量化,其余层保持浮点,逐个验证。
  5. 后端对照:同一个模型在PyTorch和ONNX Runtime里分别测,精度差超过1个点就要查算子表。

这套方法帮我解决过至少三个疑难杂症,包括上面提到的激活漂移和BN振荡。

6. 进阶:从能跑路到好用的量化器选择与部署建议

6.1 量化器对比:从普通量化到LSQ再进阶

MQBench里支持多种量化器,我建议从简到繁逐步尝试:

量化器特点适用场景
Fixed量化(STE)原始QAT,scale固定起步实验,理解机制
LSQscale可学习,精度上限高常规QAT首选
DoReFa对低比特更友好4bit以下场景可以考虑

LSQ之所以效果好,在于它把量化步长scale变成了一组可训练参数,和权重一起更新。本质上,LSQ让量化误差不再是被动遭遇的,而是参与了整体损失函数的优化。用LSQ训QAT时,梯度会同时更新权重、scale和zero_point,整个量化粒度体系都被拉进训练闭环。

但LSQ的代价是训练变慢,且对学习率更敏感。所以我在用LSQ时通常会把QAT阶段拉长,同时建议用cosine学习率调度。

6.2 训练完成后的导出链路:别等部署再开香槟

QAT训练完成,精度达标,后面还有一整套导出链路等着你,包括:

  • 把PyTorch模型转成ONNX格式;
  • 检查ONNX图中的量化节点类型,确认卷积、全连接、激活层都被正确量化;
  • 在目标推理引擎上重新校准一遍scale,防止训练统计量和运行时统计量不一致;
  • 打开指令集优化选项,比如基于CPU的向量化指令,不同的后端配置对性能影响巨大。

每一步都可能暗藏变量。我经历过一次:模型导出后精度在PyTorch完全正常,但ONNX Runtime里慢了30%,打开Netron一看,量化算子根本没被映射到加速实现上,回退成了普通浮点算子。这种问题不端到端跑一遍,永远发现不了。

6.3 我的经验总结:什么时候该上QAT,什么时候用PTQ就够了

说来也实在:QAT不是万能药,它只是把PTQ做不动的场景补上。

就我而言,是否上QAT,主要看三个条件:

  • PTQ的精度损失是否超过业务红线。差0.3个点,精调一下校准集也许就够了;差两个点,直接上QAT比瞎调校准强得多。
  • 模型是否固定。如果是每天都在迭代的模型,QAT的训练成本就得计入;如果是固定版本上线,那训练成本只是一次性的。
  • 是否有足够的数据做校准和验证。QAT对数据质量的要求比PTQ更高。

如果你正在做8bit量化,建议先跑PTQ,摸清底细再决定要不要上QAT。如果业务方要求4bit以下,就直接跳过PTQ,从LSQ+QAT这套组合开始吧。

提示:无论什么时候,都建议在训练流程里把后端评测脚本先写好。量化这件事,训练和部署从第一天起就应该是连在一起的。别等训完了再去对接后端,那样你的坑会多到怀疑人生。

最后分享一个实操心得:我后来再没在QAT上栽过大跟头,靠的就是把“先PTQ摸底、再敏感层分析、最后QAT适配”这套流程固定下来。量化是系统工程,设计好自己的制作工序,比无脑堆性能重要得多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询