量化这个事,圈子里聊得不少,但很多同学对 QAT(量化感知训练)始终停留在“听说过、没跑通”的状态。要么是在 PTQ 上被精度回落搞得焦头烂额,要么是翻了几天文档也不知道该把量化节点插在哪儿。我最早接触 QAT 是在一个端侧视觉模型的落地项目上,当时手上的模型浮点精度表现不错,一旦切到 int8 推理就掉了不少点,最后迫不得已才认真研究量化感知训练。折腾几轮之后发现,QAT 本身并不玄乎,难的是整个链路里杂碎细节太多,而像 MQBench 这样的统一评估框架,恰好能把“量化方案设计”和“后端部署验证”这两件事拧在一起,省掉大量重复造轮子的时间。
这篇文章就围绕 MQBench 的 QAT 实践展开,重点讲清楚三件事:量化感知训练到底改了什么,MQBench 是怎么把量化建模和后端解耦的,以及我实际跑通一套 QAT 实验时踩过哪些坑、调过哪些参数。不管是刚入门量化的新人,还是已经做过 PTQ 想再往前一步的工程师,顺着这篇文章的操作路径走一遍,应该都能对 QAT 建立起一套完整可复用的认知。
1. 为什么需要认真看待 QAT 量化,而不是只靠推理时补救
1.1 PTQ 的精度损失到底出在哪里
后训练量化,也就是 PTQ,通常是很多人接触量化的第一站。做法很简单:加载一个训练好的浮点模型,拿一批样本跑一遍前向,统计每一层激活值的分布,然后据此算出 scale 和 zero point,把权重和激活从 float32 映射到 int8,之后在推理引擎里用整数算子来完成计算。
听起来很顺畅,但真实落地时往往不是这么回事。浮点模型训练的时候,每一层的权重和激活分布都是在连续空间中演化出来的,很多层对数值扰动非常敏感。一旦用统计出来的量化参数做截断和round,相当于给每个张量加了一点点噪声,这些噪声在浅层可能无关痛痒,但经过多层累积之后,在像目标检测的边框回归分支或者分割模型的边界细节上,就会被放大得很明显。PTQ 常见的补救手段是调校准集、调校准算法、做逐通道量化,折腾一圈有时能把精度拉回来一些,但某些结构复杂或者分布特别不均匀的模型,PTQ 的精度就是死活补不回来。
1.2 QAT 的本质是让模型“学会”容忍量化噪声
QAT 的思路和 PTQ 完全不同。PTQ 是把训练好的模型拿过来硬生生做一次“截肢手术”,而 QAT 是在模型还活着的时候就给它的关键部位装上传感器,让它一点点适应低精度表示。具体来说,就是在训练或者微调过程中,把浮点模型里的权重和激活计算替换成带伪量化节点的版本,前向推理时模拟量化误差,反向传播时又按浮点梯度去更新参数。这样模型在整个优化过程中就知道自己的数值会在低精度下被“打折”,于是会主动调整权重分布,让自己在 int8 表示下也能保持较好的精度。
我用一句话概括两者差异:PTQ 是让模型忍耐损失,QAT 是让模型学会回避损失。如果你手里模型的结构对量化特别不友好,或者目标硬件平台对精度要求很高,PTQ 压不住的情况下,QAT 几乎是唯一还能保住模型的出路。
1.3 MQBench 在量化实践中扮演什么角色
光有 QAT 的概念还不够,落地的时候马上会撞上两个问题:一是量化方案太多,不知道该选哪种;二是即使选好了方案,不同硬件后端的支持能力也不一样,容易在评估时得出互相矛盾的结论。MQBench 这类框架要解决的就是这两件事。它把量化策略、校准策略、伪量化节点实现都做成了可插拔的模块,同时在后端抽象上做了统一封装,让同一套量化配置可以很自然地切换目标设备进行验证。
在我自己的实践中,MQBench 最大的价值不是某个单独算法的精度有多高,而是它把“量化建模”和“后端评测”分开了。以前我想对比对称量化和非对称量化在某个设备上的表现,得分别调两套接口、写两套评测逻辑,现在在 MQBench 里改配置就行。这让做量化实验的人能把主要精力放在方案分析和调参上,而不是反复写胶水代码。
2. 动手之前,先把量化方案这几个问题想清楚
2.1 伪量化节点是怎么插入到训练图里的
真实 QAT 里你并不会直接丢掉浮点权重,而是通过伪量化节点模拟“量化再反量化”的过程。伪量化节点在前向计算时做两件事:先把浮点数值映射到有限的整数范围,比如 int8 就是从 -128 到 127,然后再把整数映射回浮点尺度。整个过程可以写成:
# 伪量化的核心逻辑,简化示例 def fake_quantize(x, scale, zero_point, qmin, qmax): x = x / scale + zero_point x = torch.clamp(torch.round(x), qmin, qmax) x = (x - zero_point) * scale return x注意,前向推理真的是在模拟误差,但反向传播时,round 这个操作几乎是不可导的。常规做法是使用 STE,也就是直通估计:梯度不通过 round 层反向传播,而是直接把上游传来的梯度当作这个节点的梯度继续往下传。这样权重更新就能顺利进行,同时模型又能感知到量化边界带来的截断效应。理解这一点非常关键,因为很多调参问题都出在 STE 的梯度近似和量化边界之间的平衡上。
2.2 对称量化、非对称量化以及逐张量与逐通道的区别
选择量化方案不是越复杂越好,而是要匹配硬件和模型的实际分布。对称量化不需要 zero point,实现简单,很多硬件的整数乘加单元能因此省掉一部分计算。如果权重分布比较接近以零为中心的对称形状,用对称量化基本没有损失。但激活值往往不是对称的,特别是经过 ReLU 之后全为正数,这时候非对称量化能更精细地利用整数表示范围。
逐张量和逐通道又有讲究。权重一般更适合做逐通道量化,因为不同输出通道的权重数值范围可能差很多,如果强制用一个全局 scale,数值范围较小的通道会被浪费大量表示精度。激活值因为要在推理时动态计算统计信息,逐层量化是主流,逐通道激活量化对硬件要求更高,不是所有后端都支持。MQBench 让我觉得方便的地方就在这里,这些组合都可以通过配置项自由切换,不用为每一种排列单独写一套训练循环。
2.3 量化范围统计的更新节奏,直接影响收敛质量
伪量化节点里还有一个看不见但很重要的参数:scale 和 zero point 怎么更新。在 QAT 训练中,这些统计值可以随着每个 batch 动态计算,也可以用移动平均的方式持续更新,还可以在训练若干轮后重新用校准集刷新一次。我自己的经验是训练刚开始时模型还在剧烈变化,如果每个 batch 都让量化范围跟着乱跳,梯度信号会很不稳定。比较稳妥的做法是训练初期固定量化范围,或者让 observer 的更新率很慢,等模型的权重分布大致稳定后再放开更新。这个细节很多教程不会写,但影响非常大,我在 4 位量化实验里曾经因为统计更新太激进,Loss 反复震荡,调低更新率之后才开始正常收敛。
3. 用 MQBench 跑通一次 QAT 实验的完整过程
3.1 环境准备与依赖安装
MQBench 本身是面向研究和工程验证的量化框架,依赖主流的深度学习训练库。我在本地环境里用 Python 3.8,训练部分基于 PyTorch,另外还需要安装 mqbench 本体以及用于评测的推理后端工具。这里需要提醒一点,因为训练框架和推理后端的版本迭代都比较快,建议在开始前先确认版本兼容性,最好用虚拟环境隔离,免得后面被依赖冲突卡住。按照仓库的说明,通常是先下载源码,再执行 pip 安装:
git clone https://github.com/mqbench/mqbench.git cd mqbench pip install -e .装好之后可以快速验证一下导入是否正常,把基础依赖跑通再开始后续操作。
3.2 准备模型与数据集
这次实验我用的是一个图像分类模拟项目,模型结构选择了在移动端比较常见的轻量级网络。为什么不用超大模型?因为 QAT 的验证需要反复迭代调参,模型太大训练一轮就要半天,根本没法快速试错。先用一个小模型把整个链路跑通,确认方案有效后,再迁移到大模型上会更高效。
数据集选择了常用的公开小猫小狗数据集,类别数量不多,但数据量足够让量化微调看到明显收益。注意这里我说的“量化微调”是从一个已经训练好的浮点模型开始,而不是从头开始训练。从头训练 QAT 也不是不行,只是耗时更长、超参更难控制,工程上通常先拿到一个精度不错的浮点模型,再进入量化感知微调。
3.3 配置 MQBench 的量化方案
MQBench 的配置逻辑是通过一个字典或者配置文件来描述的。以我当时的使用经验为例,核心要配置的字段包括:量化位宽、量化策略、量化种类是权重还是激活、是否开启逐通道量化、是否使用混合精度等。一个参考配置如下:
qat_config = { "quant_dict": { "weight": { "bit": 8, "symmetry": True, "per_channel": True, }, "activation": { "bit": 8, "symmetry": False, "per_channel": False, } }, "observer": "minmax", "backend": "generic", }为什么权重用对称逐通道、激活用非对称逐层?这不是随手写的。权重通常以零为中心分布,对称可以少算一个 zero point;而激活经过激活函数后普遍偏正,非对称能多利用一点表示范围。这套组合在大多数通用后端上都是精度和效率之间比较平衡的默认值。
3.4 把浮点模型转换成带伪量化节点的 QAT 模型
配置好方案之后,最关键的一步就是把浮点模型转换成为可训练的量化感知模型。在 MQBench 里,这个过程通常是调用 prepare_qat 接口完成的,它会遍历模型结构,把卷积、全连接、归一化层等关键算子替换为支持伪量化节点的新版本。下面是我的参考代码:
from mqbench.prepare import prepare_qat qat_model = prepare_qat( float_model, qat_config, backend="generic" )转换完成后,模型结构里会多出很多 QuantWrapper 或者说 Observer 节点。训练之前最好先打印一遍模型结构,确认需要量化的算子都被正确替换了。我遇到过某些自定义算子不被框架识别的情况,此时需要额外注册或者手动替换,这个后面再展开。
3.5 训练循环与学习率调整策略
QAT 模型的训练循环看起来和普通训练差不多,但有几个明显的差异。首先,学习率不能沿用浮点模型从头训练时的大学习率,一般要缩小一个数量级甚至更多,因为是微调而不是重新训练。其次,损失函数通常保持原任务的损失,但也可以加上一些针对量化层权重的约束。第三,训练轮数不需要太长,我通常控制在浮点模型训练轮数的 10% 到 20% 之间,重点观察验证集精度能否快速回到浮点基线附近。
一个我习惯用的训练骨架大致是这样的:
optimizer = torch.optim.SGD(qat_model.parameters(), lr=0.0001, momentum=0.9) scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=epochs) for epoch in range(epochs): for images, labels in train_loader: optimizer.zero_grad() outputs = qat_model(images) loss = criterion(outputs, labels) loss.backward() optimizer.step()这里有一个容易踩的坑:不要直接照搬浮点模型的优化器配置。QAT 的梯度因为经过 STE 的截断,本身就比正常浮点训练更“粗糙”,如果再加上过大的学习率和冲量,很容易让权重分布跑偏。我的建议是先用 1e-4 左右的学习率跑 2 到 3 个 epoch,观察 loss 变化,如果 loss 不降或者下降极慢,再适当调大,而不要一上来就大学习率猛冲。
3.6 导出量化模型并在目标后端上完成精度验证
QAT 训练完之后,还需要把训练态的模型导出成真正的低精度推理模型。MQBench 提供了 convert 之类的转换接口,会把伪量化节点里的 scale 和 zero point 固定下来,并且完成一些算子融合操作,把 BatchNorm 融合到卷积里。这一步做完之后,模型就不再依赖训练框架的运行机制了,可以直接交给后端的量化推理引擎去跑。
随后要做的就是用验证集在目标后端上重新做一次精度评估。这一步非常关键,因为训练时的 QAT 模拟和真实后端算子实现并不是完全一致的,比如某些后端的量化方式带自己特定的对齐规则,或者某些算子对 scale 的归一化方式有细微差别。如果只停留在框架自带的模拟精度上,很容易被表面上的“精度恢复”骗过去。
我最后落地时还会做一步:在真实开发板上用实际推理时间来衡量性能收益。有些方案看起来精度不错,但某些后端的算子效率特别差,实际加速比并不理想。性能评估要结合目标平台的算子支持和内存带宽条件,不能只看理论计算量。
4. 量化训练中的“手感”与参数调优记录
4.1 第一次跑 QAT 我的 Loss 曲线为什么是歪的
第一次认真跑 QAT 的时候,我发现 loss 曲线非常奇怪,前几个 batch 下降得很快,但中途会突然跳高一下,然后又缓慢下降。后来定位到原因是:量化范围 observer 在训练初期更新太频繁,权重还在剧烈更新,scale 也跟着每个 batch 跳,梯度就变得飘忽不定。
解决方式是把 observer 的更新频率降低,或者让量化范围在头几个 epoch 固定不变。具体实现上是在配置里关闭 observer 的动态更新,等模型分布稳定后再打开。经过这个调整之后,loss 曲线变得平滑很多,验证集精度也随之回升。这个经验在后来的多个模型上都得到了验证,如果你在 QAT 时遇到 loss 锯齿状抖动,优先检查量化范围统计的更新节奏。
4.2 学习率到底用多少合适
很多同学会问 QAT 微调用什么学习率比较好。我的经验是看你的浮点模型是怎么训练出来的。如果浮点模型已经收敛得不错,QAT 更像是在原最优解附近做“小范围平移”,学习率太大容易把模型推离原来的优势区域;学习率太小呢,量化误差的适应过程又太慢。我一般从浮点模型最终学习率的十分之一开始试,然后根据前几个 epoch 的 loss 走向决定微调方向。
在 MQBench 的默认配置下,我试过固定学习率、余弦退火、以及阶段性下降三种策略。固定学习率最省事,适合快速验证;余弦退火在训练轮数较多的长任务里表现更稳;阶段下降手调痕迹重但能精确控制。实际项目里,我会先用固定小学习率跑通正确性,确认精度达标后再去优化 schedule,这样能减少调试变量。
4.3 要不要从头训练还是先浮点后 QAT
有一段时间我喜欢把 QAT 和训练混在一起,直接从头训一个量化感知模型,觉得这样会让模型更适应量化。但实际效果并不总是更好。从头训练 QAT 意味着伪量化节点从初始化开始就参与梯度计算,模型的收敛空间会更复杂,再加上 STE 的梯度噪声,超参选择难度明显上升。
如果想追求稳定的精度,我建议还是走“浮点预训练 + QAT 微调”这条主流路线。先得到一个好的浮点模型,再用 QAT 微调让它适应量化,这个流程最可控。MQBench 的示例代码也大多按这个路径组织,毕竟工程化项目里,浮点预训练模型往往早就有了,QAT 只是最后的适配环节。
5. 容易被忽略的坑与排查速查表
5.1 量化范围统计抖动导致精度不稳定
这个问题在 2.3 和 4.1 里都提到过,这里再深挖一层。训练过程中如果量化范围统计策略是逐 batch 计算的话,遇到数据分布波动比较大的批次,scale 会产生明显跳变。跳变之后,同一层权重在不同 batch 里对应着不同的映射方式,相当于训练样本在系统性地换标尺,模型很难学得稳。
排查时可以先把这个开关关掉,手动固定一个统计范围,确认 loss 是否变稳。如果是,再改成 EMA 滑动平均的方式让范围平滑变化。在 MQBench 的 observer 配置里,EMA momentum 默认值如果偏高,也建议适当调低,比如取 0.9 而不是 0.99,这样既能跟踪分布变化又不容易被异常 batch 带偏。
5.2 BatchNorm 融合带来的精度落差
QAT 训练完成后,从训练态模型切到推理态模型,经常发现推理精度比训练时模拟的精度低不少,一个常见原因就是 BatchNorm 融合策略不对。量化感知训练时,BatchNorm 通常是保留在浮点域里的,推理部署时会把 BatchNorm 参数折叠进卷积权重里。这个折叠过程会改变原始权重分布,如果统计折叠之后的分布再设置量化参数,和训练时统计的分布可能不一致。
所以标准做法是在训练完成后先做 BatchNorm 折叠融合,再基于融合后的模型重新校准一次量化参数,然后再导出推理模型。MQBench 的转换流程通常已经把折叠包括进去了,但不同版本行为可能不同,最好在导出后打印模型的量化参数,验证一下 scale 是否针对折叠后的权重重新计算过。
5.3 自定义算子没有进入量化列表
实际项目里很少全是标准卷积、全连接这些常规算子,像一些检测头里的特殊计算、注意力模块里的矩阵乘加,经常会以自定义算子的形式出现。MQBench 在自动替换时不一定能覆盖到这些算子,如果没有显式注册,它们就一直留在浮点精度里。这样的话模型加载和训练都没问题,但导出低级推理模型时会发现要么不支持该算子,要么该算子仍然是浮点,导致整个推理链路被拖慢。
排查办法很简单:打印替换后的模型结构,逐一检查关键算子是否被量化包装托管。没有覆盖的自定义算子,要根据后端支持情况决定是改写成支持的形式,还是单独做量化适配。这一步不提前处理,全部训练做完才发现,返工成本很高。
5.4 常见问题速查表
| 问题现象 | 可能原因 | 快速排查与处理 |
|---|---|---|
| Loss 锯齿状抖动 | 量化范围统计更新太快 | 关闭 observer 动态更新,改为 EMA 或固定范围初始化 |
| 验证精度始终低于浮点基线 | 学习率太大或训练轮数不足 | 降低学习率至浮点模型十分之一,增加微调轮数 |
| 训练态精度正常,推理态精度下降 | BatchNorm 折叠后未重新校准 | 折叠融合后重新统计激活量化范围 |
| 自定义算子仍是浮点 | 算子未注册到量化替换表 | 检查模型结构,注册算子或改写为可量化形式 |
| 内存占用比浮点训练明显增大 | 伪量化节点过多或 batch 过大 | 精简监视节点,按比例降低 batch size |
| QAT 不收敛,loss 不下降 | 初始化量化范围异常 | 打印 scale 初值,确认从浮点校准起步 |
6. 再往深走一步,如何做混合精度与敏感层分析
6.1 先找到量化敏感层,再谈混合精度
做完一整轮 QAT 实验后,如果精度还是差一点,比如分类任务差 0.5 个点,或者检测任务 mAP 差不到 1 个点,这时候直接全部切成 4 位通常风险很大。更稳的做法是找到模型里对量化最敏感的那些层,针对性地保持更高位宽,其余层位宽压到更低。这个思路在业界习惯叫混合精度量化。
找敏感层可以先做敏感性分析:把所有层都量化到目标位宽,然后逐层把某一层恢复回浮点精度,跑一遍验证集,看精度回升多少。回升幅度大的层就是对量化最敏感的层。这个过程比较耗时,但可以借助 MQBench 里已经搭好的实验框架来批量化跑,只需要循环遍历不同层配置就行。
6.2 混合精度配置如何落到 MQBench 里
MQBench 的配置是支持逐层指定位宽或者量化策略的。我在实验里最常用的做法是,先按位宽把所有层分成两组,例如敏感层保持 8 位,其余压到 4 位,然后在配置里统一覆盖这些层的参数。
这里我要提醒一点:不要为了好看强行混合精度,一定要先确认目标硬件后端是否支持逐层不同位宽。很多端侧推理引擎出厂时就固定好整模型只支持 8 位或者支持有限的混合位宽组合,不支持的话你再怎么配置也落不了地。所以做混合精度实验之前,最好先把目标平台的算子能力列表拿来看一眼,避免后期推倒重来。
6.3 蒸馏辅助与正则化暴躁调试经验
在 QAT 的过程中,有时候还可以引入知识蒸馏来进一步稳定收敛。做法是让带伪量化节点的学生模型去逼近原始浮点教师模型的输出,这样量化误差对梯度的影响会变得更平滑。我在某些检测模型上试过,确实能让量化微调后的精度再提升一点,但代价是训练复杂度增加一倍不止。如果项目时间紧张,建议先不碰蒸馏,把 QAT 基本功跑好再说。
QAT 过程中的数据增强也要克制。有时为了缓解过拟合,加了很重的随机裁剪和色彩抖动,反而让量化范围统计变得更加飘忽。QAT 微调时我习惯使用比较轻的增强,主要目的是模拟部署时的真实输入分布,而不是追求泛化收益。
最后说点我在实际操作中的感受:量化感知训练并不是一个“跑完就结束”的步骤,它更像是一次对模型、数据和部署后端关系的重新审视。每一次精度回升的背后,都是对某些原理的更深理解。做 QAT 这一路,我最大的心得就是不要迷信默认配置,也不要跳过中间验证步骤——反复检查模型结构、量化参数和后端支持列表,才是真正能让你从“能用”走到“好用”的关键。希望这份基于 MQBench 的 QAT 实践笔记,能帮你少走一段弯路。