1. MQBench是什么?为什么需要它
我先说个真实场景。某次模型上线前,我负责把一个图像分类网络从FP32压到INT8。配置好量化感知训练流程,反复调了几轮,离线测试精度掉了不到1个点,感觉挺稳。结果到了目标推理框架上,速度达标了,线上指标直接崩了3个点。我一度觉得是某框架的INT8实现有bug,折腾了两周才发现,问题其实出在我自己评估的方式上——我用的是某平台的分支量化路径,压根没有经过目标后端的真实数值行为。
那段时间我在想,如果有一个东西能统一描述“量化算法怎么改模型结构”、“推理前应该做哪些变换”、“后端期望什么样的量化格式”,并且让我在训练阶段就尽可能逼近真实部署环境,就好了。后来我接触到MQBench,惊讶于它把这件事做得这么系统。
MQBench简单说,是一个开源的模型量化评测基准平台,它同时覆盖量化算法和推理后端两类对象,把“训练、校准、拟真推理、精度评估”串成一条流水线。对我这种做实际部署的人来说,最直观的价值是它提供了统一的量化抽象层,我不用再为每个硬件平台各写一套伪量化逻辑。用一份模型定义加几行环境描述,就能在不同的模拟后端上评估量化效果。
它解决的痛点是:传统QAT实践里,大家往往只盯着精度的下降幅度,但忽略了“你的伪量化算子到底模拟的是哪一套硬件语义”。不同芯片对量化尺度、bias处理、激活收集时机都有各自的约定,如果训练阶段就和后端行为脱节,训练得再认真也是自欺欺人。
这篇文字就基于MQBench,讲一讲我完整做一轮QAT量化实践的过程、原理和踩过的坑。适合三类人看:一是刚接触模型量化、不知道从哪下手的同学,二是已经在做PTQ但精度始终救不回来的工程师,三是对“训练框架与推理后端对齐”这个细节有疑惑的从业者。
2. QAT的技术脉络与MQBench的设计思路
2.1 为什么非要用QAT,PTQ不够吗
很多刚接触量化的同学有个疑问:现在PTQ工具这么成熟,尤其是带校准集的方案,大部分场景精度掉得不多,何必费劲做QAT。
我的看法是,PTQ和QAT解决的是不同层面的问题。PTQ的核心是“在已经训练好的模型上找一组最优的量化参数”,本质是个优化问题,通常依赖校准集在部分层上做误差补偿。QAT的核心是“把量化误差当成训练过程的一部分”,让权重和激活逐步适应离散取值带来的扰动,本质是个鲁棒化训练过程。
直观类比:PTQ是给一辆车换不同规格的轮胎,然后去测量哪套胎最稳,但你不会去调悬挂和变速箱;QAT是直接在赛道上换好轮胎后,把悬挂、转向、动力输出都按这套轮胎的特性重新调校。
什么时候必须QAT?我的经验是三类情况。第一,模型对数值扰动非常敏感,比如注意力机制很重的Transformer结构,PTQ后个别层的激活范围波动极大,校准集很难覆盖全部形态。第二,目标硬件对激活量化要求严格,例如某些平台不允许per-channel激活,或者要求激活的零点为固定值,PTQ根本没法满足约束。第三,量化位宽压到4bit甚至更低的时候,PTQ的搜索空间和精度表达力往往撑不住,QAT配合直通估计才能把损失拉回来。
2.2 MQBench的抽象层次:Backend、Algorithm与Quantizer
我之前用过一些量化工具,它们多数是“一个算法对应一套训练脚本,再对应一个后端的导出工具”。效果是能出结果,但每次遇到新硬件,整个链路几乎要重写。MQBench不一样,它把整个流程拆成了几个稳定层次。
用我自己的理解来描述这套抽象:
- Backend(后端模拟层):它定义了一组“拟真推理接口”,比如量化卷积、量化矩阵乘、量化激活等。MQBench支持多种后端模拟,比如某些通用CPU模拟、某些特定芯片指令模拟、还有异构设备模拟。这一层负责回答“假如我的模型跑在某硬件上,数值会怎么变”。
- Algorithm(量化算法层):负责产生量化参数,包括observer策略、量化粒度、量化格式选择等。PTQ算法、QAT训练的调度方式都在这一层定义。
- Quantizer(量化器层):把具体的量化策略绑定到模型中的张量上。在QAT模式下,它会往模型里插入伪量化节点,并管理这些节点在前向/反向中的行为。
这层抽象的最大收益是解耦。我可以在算法层用同一套QAT流程去适配不同后端;也可以固定一个后端,横向对比不同量化算法的优劣。一旦某次部署遇到新芯片,我只需要写一个新的后端描述或者查一下它支持哪个模拟层,不需要把训练代码推倒重来。
2.3 训练与推理的一致性:FakeQuant节点与直通估计
QAT里绕不开一个概念:FakeQuant节点。它做的事情是“在前向计算时模拟量化误差,但在反向计算时可以自行定义梯度”。
默认最简单的方式是直通估计,即前向时把浮点张量先量化再反量化回到浮点,虽然数值上产生了离散误差,但反向时直接把损失对量化前张量的梯度当作对输出张量的梯度回传,忽略量化取整的影响。
MQBench里对这类节点做了标准化。它允许你配置FakeQuant的量化范围怎么确定、量化/反量化何时生效、统计观测窗口多长。我刚开始做QAT时有个错误理解,觉得只需要把FakeQuant节点插进去就结束了。实际上,要让直通估计真正起作用,必须保证前向数值和实际推理时后端看到的行为一致,否则梯度方向会偏离。
举个具体例子。某个后端对卷积的计算顺序是“先对输入做量化,再做整数卷积,最后累加并反量化”,而训练框架如果默认“浮点卷积,后再加FakeQuant”,二者在数值路径上就有差异。尤其当bias存在时,是bias先加到整数累加器里再反量化,还是反量化之后再加浮点bias,最终结果可能差几个ULP,但累积到深层网络就可能被放大。
2.4 蒸馏与量化:MQBench如何配合知识蒸馏
单独做QAT,如果模型本身容量小、任务复杂,量化后精度仍然可能不够。我通常的路径是“QAT + 知识蒸馏”。蒸馏提供一个高质量教师信号,帮助量化模型在离散权重约束下找到更优解。
MQBench本身不是专门的蒸馏框架,但它提供了干净的量化模型构造接口,配合常见蒸馏工具并不别扭。我的做法是,加载一个训练好的FP32教师模型,用它输出软标签;学生模型则用QAT方式构建,损失函数设计为“硬标签交叉熵 + 蒸馏KL散度 + 特征层MSE(可选)”。MQBench负责的,是把学生模型中需要插入伪量化节点的位置管理好,确保训练时数值行为接近目标推理后端。
这里有个细节:蒸馏的“教学强度”不宜一开始就拉满。我习惯前几个epoch不加蒸馏损失,让QAT先稳定收敛,然后再以较低权重引入教师信号。原因很现实,量化训练初期,学生模型本身的输出分布还在大幅波动,如果教师信号过强,学生会盲目模仿教师的高精度分布,反而忽略了离散量化带来的结构性偏差。
3. 环境准备与基础配置
3.1 安装与版本选择
MQBench对环境要求不算苛刻,但我踩过几个版本匹配的坑,先说一下参考组合。
我使用的环境是:Python 3.8,某深度学习框架2.1.0版本,MQBench主分支代码。硬件方面,训练用了一张普通消费级GPU,因为QAT的显存开销比普通训练略高,尤其插入FakeQuant节点后中间变量会多一些,建议显存不低于12GB。
安装不要太激进,建议用虚拟环境管理。大概率会遇到MQBench依赖中的某个子模块与深度学习框架版本不兼容,比如旧版本插件的API依赖。我的经验是先创建干净环境,再按MQBench的README顺序安装基础依赖,最后安装框架组件。
安装完成后,强烈建议先跑一遍自带的端到端demo。我见过不少人在环境上卡住,跑demo是验证安装是否完整最快的方法。如果demo能出精度结果,说明量化器、后端模拟、评估工具链都已经正常,后面换自己的模型会顺利很多。
3.2 模型定义与数据集组织
QAT对数据的要求比PTQ高不少。PTQ的校准数据集几百张甚至几十张图可能够用,但QAT是完整训练过程,数据不足会过拟合,数据分布和部署场景差异太大会让伪量化统计偏掉。
我的建议是分三份数据:训练集、验证集、模拟部署集。训练集负责权重更新和量化范围学习,验证集用于早停与超参选择,模拟部署集则模仿线上真实输入分布,用来做最终精度评估。模拟部署集很重要,经常被忽略。它不需要很大,但必须贴近真实场景,比如图像的亮度、对比度、噪声水平等。
数据集组织上,我用的是Map-style数据集类,返回预处理后的输入张量。这里有一个细节:训练阶段和评估阶段的预处理必须完全一致,尤其是归一化参数和尺寸缩放方式。出现过我在训练时用了随机裁剪增大泛化,但评估时直接中心裁剪,导致激活分布统计偏差很大。MQBench内部的校准器也有类似要求,前后处理不一致会让校准结果不可信。
3.3 指定后端:到底选哪种模拟策略
在MQBench里,选后端不是随便填个名字。不同后端决定了FakeQuant节点的放置方式、量化参数作用的位置、甚至反量化的精度。
如果目标部署平台是通用CPU或者GPU,我通常选择它的通用后端模拟,这类模拟和标准量化推理的数值行为最接近。如果目标是特定移动芯片或边缘设备,需要确认该芯片是否有对应的具体后端后端。我的经验是,找不到精确模拟时,选择数值约束更严格的模拟方式——宁可训练阶段更“悲观”,也不要低估部署误差。
为什么这样选?量化训练和普通训练最大的不同是,它必须容忍离散噪声。如果模拟后端比真实硬件“更理想”,训练出来的模型在真实设备上就会遇到意外误差。反过来,如果模拟得更苛刻,模型会提前学会应对更恶劣的量化扰动,部署时反而有余量。这是我在几次上线后总结出的选择原则。
4. 基于MQBench的QAT完整实操
4.1 构建量化模型:observer的选择与初始化
正式训练前,第一步是把浮点模型转换成量化感知模型。MQBench提供了一种配置对象来描述量化策略,包括量化位宽、量化粒度、observer类型等。
我推荐的做法是使用范围统计作为初始observer。量化范围直接决定FakeQuant节点的截断边界,太大会浪费有效量化区间,太小会让数值频繁饱和。范围统计通过前向过程收集张量的实际min/max值,或者使用各种平滑策略来抑制离群点的影响。
量化粒度的选择也必须提前考虑。对于权重,我一般选per-channel;对于激活,在没有特殊限制时选per-tensor。原因很直接,权重per-channel能保留每个输出通道的动态范围,精度损失会小很多;激活per-channel在后端支持不普遍,而且激活张量的统计意义在通道维度不一定稳定,使用per-tensor更利于部署对齐。
observer还有一个重要参数,就是更新时机。我在QAT训练中会让observer在初步预热阶段持续更新,等量化范围稳定后再冻结。冻结的时机用验证集来监控:如果连续几个epoch验证精度不再明显下降,就可以考虑固定量化范围,专心优化权重。有些工具默认训练过程中持续更新range,但我发现,到训练中后期如果range还在大幅变化,网络的损失曲线会抖得厉害,很难收敛。
4.2 训练参数:学习率、批次大小与梯度累积
QAT的学习率设置和普通微调不同。我通常从浮点模型微调学习率的五分之一到十分之一起步。原因是FakeQuant节点的直通估计本质上是给优化过程增加了扰动噪声,学习率太大会让权重在量化边界上来回跳动,学不到稳定的优化方向。
批次大小方面,尽量保持和原模型预训练时接近。QAT的训练稳定性偏低,尤其使用BN层时,小批次会让batch内的统计量波动很大,进而扰动量化范围的更新。如果显存有限,我不建议把批次调低来迁就,而是使用梯度累积来模拟较大批次。比如目标批次是128,显存只能放32,那就每4个step累积一次梯度再更新。
训练epoch数也别贪多。实践经验是,中等规模模型做QAT微调,15到20个epoch足够,更多反而出现过拟合迹象。重点不是训练多久,而是让权重在量化扰动下重新找到低损失区域。早停策略以验证集精度为准,一旦连续几个epoch没有提升,就保存当前模型继续观察,避免在波动中丢失已经学到的好解。
4.3 蒸馏损失与量化训练的联合调度
这里说我常用的联合训练配方。
损失函数为三类之和:硬标签交叉熵、教师软标签KL散度、可选中间特征对齐损失。刚开始时蒸馏权重设为0,训练几个epoch,让QAT自身先稳住。然后逐步增加蒸馏权重,比如从0.1开始,训练几个step后看验证效果,再过渡到0.5。如果有多个任务维度,比如目标检测的分类和回归头,蒸馏权重可能需要分开设置,各自调节。
为了调这个比例,我参照了一个简单原则:当量化学生模型训练初期的输出分布和教师差异极大时,蒸馏权重不要超过0.3;当误差主要来自量化噪声而非模型欠拟合时,可以提高蒸馏权重。通俗说,学生模型如果自己还没站稳,就不要让它分心去追老师。等它自己能把训练损失降下去,再让“老师傅”来纠正那些量化噪声导致的错误倾向。
中间特征对齐(如MSE损失)不是必须的,而且它对网络结构敏感。MQBench提供的是模型级的量化能力,没有强行绑定特征蒸馏,所有需要自己实现。我的建议是尽量不用,除非模型精度怎么都过不去。原因是在QAT训练中,特征对齐会进一步约束隐藏空间,一旦约束过强,可能让量化模型丧失一部分对量化噪声的鲁棒性。
4.4 导出与验证:从训练到部署的最后一公里
训练结束不等于部署完成。MQBench虽然做了很多对齐工作,但导出到具体后端时,仍然需要做数值核验。
我的流程是三步。第一步,用源码中的评估函数跑一遍量化模型的离线精度,先看训练阶段模拟后端的预期。第二步,导出成目标部署格式,比如某些常见的量化格式文件,再用目标后端运行时跑一遍同一份评估集。第三步,逐层对比某一层输出的数值,找出累计误差来源。
这一步最常出现的问题是层融合。比如卷积+BN+ReLU在训练时是三个节点,导出时如果能融合成一个算子,精度通常不会变差,但数值顺序会变。如果训练框架里FakeQuant插在ReLU前面,而部署后端的融合规则把ReLU和量化合并了,激活分布就会对不上。解决办法是在构建量化模型时,提前对融合友好型结构做“预折叠”,而不是完全依赖导出工具自动变换。
我在MQBench上使用比较多的是“预量化后折叠BN到卷积”,并确保FakeQuant节点插入的是折叠之后的新卷积。这一条看似简单,但对精度的帮助极其明显。
5. 常见问题与量化排查实录
5.1 FakeQuant节点“看起来没生效”
这是我遇到最多的问题。表象是训练日志里的损失曲线很平滑,精度几乎没有变化,甚至和浮点模型完全一样。这种“平稳”一般不是好事,反而说明量化仿真没有真正参与计算。
排查思路:检查FakeQuant节点的量化范围是否初始化正确。如果范围没有初始化,它的量化输出就等于输入本身,做了个“无损直通”,模型根本看不到量化误差。我通常做法是在训练开始前手动跑几个batch收集activations统计,确认范围处于合理区间,再开始训练。
另外一种可能是observer被设置成了固定范围,但初始值设成了极大值。比如用了一个默认的全局范围,导致所有数值都落在中间,实际量化步长过大,但相对误差很小,模型感受不到扰动。这种情况把范围缩小到统计范围即可。
5.2 BN层在QAT中的失效与处置
QAT训练中BN层有特别的反直觉问题。训练时BN会持续计算当前batch的均值和方差,并使用滑动平均维护全局统计量;推理时则固定使用全局统计量。部署后,量化模型经常会把BN折叠进卷积层,减少计算。
问题出在训练中后期:量化范围已经冻结,但BN的全局统计量还在缓慢更新,导致FakeQuant节点看到的激活分布和实际部署不符。典型表现是离线验证精度还行,导出的模型在目标后端上用不同的输入分布,精度掉得莫名其妙。
处置方案:经过约80%的训练epoch后,可以停止BN统计量的更新,把它们固定下来。原理是后期模型的主体结构已经收敛,继续更新统计量只会增加不确定噪声。这个操作在某些框架里通过训练模式的切换实现,做一些参数层的重新设置即可。
5.3 量化范围过窄导致的梯度消失
QAT训练中,如果量化范围的对称性设得不好,某些层的输出可能大部分时间处于饱和区,反量化后数值变化很小,梯度自然趋近于零。这就是所谓“饱和性梯度消失”。
排查时我会打印每层FakeQuant节点的“裁剪比例”,即有多少比例的数值落到量化边界之外。如果某个节点的裁剪比例长期超过5%,就要考虑调整量化范围或改用其他策略。不要只看损失数值,损失数值可能会因为其他层正常而掩盖掉某层的异常。
修复办法通常有两个:一是扩大该层的量化范围,牺牲一点低位宽精度换取梯度流通性,代价是量化步长变大;二是改用非对称量化,让负半轴和正半轴各有独立范围,更适合ReLU后分布极度偏斜的情况。这需要在构建模型时就把量化策略从对称改成非对称。
5.4 蒸馏过程中教师主导权过强
有些同学加了蒸馏之后,发现学生模型精度比不加还低。起初我很困惑,后来意识到,这是教师信号过强导致的。当蒸馏损失权重过大,学生模型会像一个“没有自己判断力”的复读机,把教师输出的预测分布强行模仿。但学生本身因为量化噪声,无法精确拟合教师分布,结果就是两头不到岸,对硬标签的真实监督信号却被压制了。
解决办法是给蒸馏损失设置一个温度系数。温度越高,教师输出的分布越平滑,提供的信息更偏“暗知识”,对离散量化模型的压力更小。同时,蒸馏损失的权重不要用常数,可以按训练进度衰减。比如前几个epoch高,让模型从教师那里获得结构性的分布信息,后续epoch衰减,让学生模型自己打磨决断边界。
6. 实操案例:一个图像分类模型的量化全流程
6.1 模型与配置
我用了某公开图像分类数据集上的一个轻量卷积网络,大概两百多万参数。浮点模型预训练好了,Top-1准确率基准是76.8%。
构建量化感知模型时,配置如下:
- 权重量化:8bit,per-channel,对称
- 激活量化:8bit,per-tensor,非对称
- 初始化方式:前向数据统计范围,预热若干个step
- QAT训练:初轮训练不含蒸馏,学习率0.0001
- 训练后段再组合蒸馏损失
6.2 训练过程与精度表现
整个训练只跑了若干epoch,显存占用比普通训练多了大概四分之一,主要是FakeQuant节点的中间结果。初期损失下降得比较稳,量化范围在前期也有微调。第三轮加入蒸馏后,验证精度有一个短暂的回落,这是正常的耦合震荡,不要急于回滚权重。
我记录的两次关键精度检查如下:
| 阶段 | Top-1准确率 | 说明 |
|---|---|---|
| FP32基线 | 76.8% | 原始浮点模型 |
| 纯QAT(不含蒸馏) | 74.3% | 仅靠量化感知训练 |
| QAT+蒸馏(模拟后端验证) | 75.9% | 精度损失已控制到1点内 |
| 导出到目标后端复测 | 75.7% | 额外0.2个点损失,属于后端数值差异 |
从74.3到75.9这个提升过程中,蒸馏贡献很大,但最终尾段我又花了比较多时间做后端数值对齐,说明哪怕训练模拟做得再细,最后一步的校验还是不能省。
6.3 性能收益与部署评估
把量化模型部署到目标后端后,单样本推理耗时比FP32版本降低大约2.8倍,内存占用削减到原来的约35%。对延时苛刻的场景,这是很实际的收益。我没有专门做电池功耗测试,但从算子类型和访存量来看,INT8计算单元的低功耗优势在边缘设备上会更明显。
值得提醒的是,性能收益不等于一定能跑通所有算子。检查目标后端的支持列表,凡是量化算子不支持的层,要么回退到浮点计算,要么重新设计网络结构。我在这个案例中就遇到了一个LayerNorm算子没有被量化支持,只能让它走浮点路径,这直接影响了整体加速比。
7. 一些经验沉淀
MQBench这套工具带给我的,不只是“把量化模型训出来”,而是一套“量化误差的系统性思考方式”。以前我拿到一个新芯片,第一反应是去找它的量化工具包,然后按文档跑一遍。现在我会先想清楚:它的量化语义在训练阶段该怎么建模,我的伪量化节点应该放在模型计算路径的哪个位置,评估方式又该如何设计。
实践中我形成了几条很个人的经验,不一定适用所有项目,但值得参考。
第一,QAT训练数据一定要有“代表性”,但不要刻意加入极端噪声。有些团队为了让模型更鲁棒,在QAT阶段加入很强增强,导致量化范围统计失真。QAT的目标是维持精度,不是做数据增强竞赛。
第二,蒸馏是QAT的伙伴但不是救世主。如果PTQ阶段精度已经掉得很厉害,先分析掉点来源,再去考虑QAT和蒸馏。如果源头是某些层动态范围异常,盲目上蒸馏是浪费时间。
第三,模拟后端和真实后端永远存在差异,要在流程中预留“后端校验”环节。我见过项目因为省掉这个环节,上线后才发现精度损伤,排查成本比早校验高得多。
MQBench本身也在演进,但它把“量化算法”和“推理硬件”解耦的设计思路,本质上是帮从业者建立了一个更通用的量化实践框架。即使某天换到别的新工具,这套“模拟—训练—对齐—校验”的方法论同样能用。