想象一下,你为一场比赛买了一双全新的碳板跑鞋。鞋很轻、弹性好,理论上能帮你提速,但如果比赛当天才拆开包装,你的脚踝大概率会被磨得血肉模糊。聪明的运动员怎么做?他们会提前三周穿上新鞋,在小区里遛弯、跑坡、走石板路,让鞋楦记住脚型,让脚掌适应鞋底。比赛日到来时,这双鞋已经成了“老朋友”——轻快,又不磨脚。这就是量化感知训练(QAT)的全部智慧。 浮点模型(FP32)就像光脚跑步——准确但沉重;低比特量化模型(INT8)像那双碳板鞋——轻快但容易“伤脚”。训练后量化(PTQ)是直接穿新鞋上赛场,精度难免暴跌;而 QAT 是在训练阶段就让模型“穿着”量化后的低比特鞋走每一步,通过伪量化算子模拟量化噪声,再借 STE 直通让梯度照常流动。这样训练出来的模型,部署时真正量化,精度却几乎不掉。
掌握 QAT 是模型压缩与端侧部署的必修课,它决定了你的模型能否在手机、芯片上以低比特无损落地,是算法工程师从“炼丹”走向“量产”的核心能力。
可视化演示
展示FP32预训练、插入FakeQuantize模拟量化误差、QAT微调恢复精度、导出4位量化模型四阶段,对比PTQ与QAT精度差异。
本节配有交互动效(进入视口后自动加载,约 3KB)。
FP32预训练:基准模型精度 FP32全精度训练得到基准模型,top-1准确率76% 模型体积98MB,作为后续量化的起点 FP32模型是QAT的基线
PTQ精度痛点
有个偷懒的选手,比赛日早上才拆开新鞋,套上就跑。前两公里风驰电掣,第八公里脚踝开始火辣辣地疼,到十五公里干脆一瘸一拐。训练后量化(PTQ)就是这个画面:模型已经训练完毕,权重一个字不动,直接把 FP32 数值硬塞进 INT8 的格子——鞋码根本没量,鞋帮硬磨脚踝。
宽量化时误差还能忍,就像新鞋磨出的红印两天就消;但一旦位宽变低,或者网络本身很小、数据分布很极端,量化噪声会在网络层与层之间不断叠加,敏感层的微小偏差被逐层放大成灾难。浮点模型里权重分布是连续光滑的,PTQ 硬生生把连续值钉在离散格点上,原本的分布被“锯齿化”,推理结果自然跟着跑偏。
量化误差可以用一个简洁的模型描述:
PTQ 的悲剧在于,鞋码没量对就硬穿。而 QAT 的做法完全不同——它不等到比赛日才穿,而是把新鞋穿进每一次日常训练里。要理解它,得先认识那双“会伪装的鞋”:伪量化算子。
- 量化重建值$\hat{x}$ 表示输入 x 经过“量化再反量化”后得到的重建值,网络前向用它替代原始的浮点数值。
- 量化噪声$\epsilon_q$ 是四舍五入产生的误差,均匀分布在 $[-\Delta/2, \Delta/2]$ 区间内,这是 PTQ 精度损失的直接来源。
- 噪声上界$\Delta$ 是量化尺度,每个整数格子的宽度越大,噪声的最大振幅 $|\epsilon_q|$ 越大,模型输出偏离就越远。
伪量化算子
训练场上的运动员会做一个聪明的动作:穿上新鞋跑两步,感受哪里挤脚,然后脱下来调整自己的步态。伪量化算子(FakeQuant)就是这个“穿上又脱掉”的过程——前向计算时,它真的把权重和激活压到低比特网格里走一遭,让模型感受量化噪声;反向传播时,它又回到浮点世界,仿佛什么都没发生。
为什么叫“伪”量化?因为它只模拟误差,并不会真的把权重存储成 INT8。它是一个插在网络里的算子,前向做一次完整的量化-反量化往返:先把连续浮点值 x 映射到整数刻度,再乘回尺度回到浮点域。模型看到的入参已经“被挤过一遍”,梯度却依然在浮点世界里流动。
伪量化的前向计算可以写成:
伪量化像是给模型穿上一双“看得见摸得着”的鞋:前向每一步都在体验挤脚的感觉,为的就是让模型学会在鞋型限制内调整姿态。但这里立刻冒出一个难题——round 的导数在数学上几乎处处为零,梯度一碰就死,模型还怎么更新?答案就在 STE 直通估计。
- 除法归一$x / s$ 把浮点值除以量化尺度 s,映射到“以 s 为一个单位”的整数域,得到真实量化时的整数坐标。
- 取整与截断$\text{round}$ 模拟整数化时的四舍五入,$\text{clamp}(\cdot, q_{min}, q_{max})$ 把超出低比特整数范围的离群值截断掉,这正是量化中“溢出”损失的来源。
- 乘法还原最后乘回尺度 $s$,把整数 q 还原到浮点数值域,得到 $\hat{x}$——一个带着量化噪声的“伪浮点值”。
STE直通估计
跑者穿着新鞋,脚下有点挤,但他不会停下来挑刺,而是照样迈开步子——心里清楚“这点小挤不影响我跑完全程”。这就是 STE(Straight-Through Estimator,直通估计器)的精髓:对 round 这种不可导操作,干脆假装它的导数等于恒等函数,让梯度直接“穿透”过去。
为什么不能按真实导数算?想想 round(x) 的数学性质:它的图像是一级级台阶,台阶中间的平地上导数全是 0。如果梯度走到这里全是 0,权重就永远得不到更新信号,整个 QAT 训练直接瘫痪。STE 选择了一条务实路线:前向严格模拟量化,反向则把量化函数近似成“直通”——你来了,我就放你过去。
STE 的反向传播公式是:
STE 就像赛场上的裁判规则:步子落在跑道内,随便你跑;步子一旦踩出跑道,立即判罚出局。有了 STE,量化噪声就不再是训练的拦路虎,而成了让模型“磨出茧子”的日常训练。但要准确模拟量化,还得先量好脚的尺码——这就轮到 Observer 登场了。
- 直通近似$\frac{\partial L}{\partial \hat{x}}$ 是损失对量化输出的梯度,STE 把它直接当作对输入的梯度,等价于把取整函数当成恒等函数求导。
- 指示函数$\mathbf{1}_{[x_{min}, x_{max}]}(x)$ 在 x 位于量化范围内时取值为 1,表示允许梯度通过。
- 截断保护当 x 超出量化范围时指示函数为 0,梯度被阻断,防止权重被推到无效区间继续累积误差。
Observer统计
新鞋合不合脚,关键看鞋码。伪量化中的尺度 s 就是“鞋码”,它告诉模型:一个整数刻度代表多大的浮点数值范围。这个 s 不能拍脑袋定,需要一个专门的“量脚工具”——Observer(观察器)——在训练过程中持续观察权重和激活的实际分布,实时算出合适的尺度。
Observer 做的事,说穿了就是统计张量的动态范围。每训练几个 batch,它就扫描一遍当前层的权重或激活值,记下最大值和最小值,然后用总宽度除以整数格数,得到每个刻度对应的浮点宽度 s。MinMax 是最朴素的算法,直接取全体数据的最小最大值;但极端离群值会把范围撑得很大,导致整体精度下降,所以工程上更常用 Percentile(百分位法),只保留 99.999% 的数据范围,把那些“过尖的脚趾”修剪掉。
Observer 计算尺度的核心公式:
量脚不能只在早上量一次,脚会肿、会出汗、会随着训练变化。Observer 用滑动窗口不断刷新统计,让鞋码始终贴合当前状态。有了合适的尺码,QAT 的训练才算真正就位——但怎么训,还得有一套磨合计划。
- 动态范围$x_{max} - x_{min}$ 是当前张量观测到的数值跨度,相当于脚的“总长度”。
- 整数格数$q_{max} - q_{min}$ 是低比特整数域的总跨度,例如 INT8 非对称量化时从 0 到 255,共 255 个区间。
- 均分刻度用总跨度除以格数,得到每一个整数刻度对应的浮点宽度 s——也就是量化的“最小分辨率”。
QAT训练配置
磨合新鞋不能一上来就冲 42 公里。聪明的运动员会安排三周计划:第一周散步,第二周慢跑,第三周间歇跑,让脚和鞋逐渐契合。QAT 的训练配置也是同一个道理:在预训练好的浮点模型上,插入伪量化算子,然后用更小的学习率微调几个 epoch,让权重在低比特网格附近“重新落稳脚跟”。
训练时,伪量化后的权重 $\hat{W}$ 和激活 $\hat{x}$ 取代原始浮点值进入前向计算,损失函数用普通的交叉熵,反向借 STE 传递梯度。由于模型已经预训练过,学习率不宜过大,一般取预训练末期学习率的十分之一到百分之一,epoch 数控制在 1~5 轮,否则会破坏已有的特征表达——就像新鞋还没磨合好,就疯狂加训练量,反而把脚跑伤了。
QAT 训练的目标函数:
训练结束后,模型已经像穿习惯的跑鞋一样,对量化噪声“脱敏”了。接下来真正关键的问题:QAT 比 PTQ 强多少?这笔账得好好算一算。
- 伪量化输入$\hat{W}$ 和 $\hat{x}$ 是经过伪量化算子处理后的权重与激活,模型在每一步都感受到低比特量化带来的数值扰动。
- 交叉熵损失$\mathcal{L}_{CE}(y, f(\hat{W}, \hat{x}))$ 用量化模拟后的模型输出与真实标签计算分类损失,优化目标依然是最小化任务误差。
- 正则约束$\lambda \|\theta\|_2^2$ 用 L2 正则限制权重幅度,防止某些权重过大超出量化范围,让模型更容易被压缩到低比特。
QAT与PTQ对比
比赛日成绩单出来了。穿磨合过新鞋的选手(QAT)跑出 2 小时 10 分,几乎追平光脚训练的纪录;直接穿新鞋的选手(PTQ)后半程掉速,勉强完赛。训练场上的差距,会在实战中被放大成致命的胜负差。
量化模型的精度保持率是一个直观指标:把量化模型的精度除以浮点基线的精度,得到它保留了原模型多少“战斗力”。
| 维度 | PTQ | QAT |
|------|------|------|
| 精度保持 | 8bit 尚可,低比特明显掉点 | 贴近 FP32,掉点 1%~2% |
| 所需数据 | 少量无标注校准集即可 | 需要训练集与标签 |
| 训练成本 | 几乎为零,几分钟完成 | 数小时 GPU 微调 |
| 部署复杂度 | 低,算子替换即可 | 稍高,需同步 scale 参数 |
QAT 的“额外磨合期”换来的是比赛日的从容;PTQ 的“省事”换来的是精度风险。但问题来了——不是每场比赛都需要磨合新鞋,到底什么时候该上 QAT,什么时候 PTQ 就够用?
- 精度保持率$\eta$ 表示量化模型相对 FP32 浮点模型的精度百分比,数值越高说明量化损失越小。
- 对比参照分母换成 PTQ 的精度,就能得到 PTQ 的保持率,用于和 QAT 做横向对比。
场景选型策略
小区里慢跑,穿旧鞋就行;但如果是越野赛、暴雨天、脚踝还带伤,那必须精心磨合新鞋。给模型选量化方案,也得先看看赛道条件。选错了,轻则训练资源白烧,重则模型上线就崩。
业界一般用两个信号做决策:一是看量化后的精度落差,二是看位宽。如果 PTQ 量化后精度掉幅超过业务可接受的阈值 θ,说明这双“新鞋”直接穿不行;如果目标位宽低于 8bit(比如 INT4、INT2),PTQ 的风险会指数级上升,QAT 几乎是必选项。反之,网络很宽、位宽足够、精度敏感度低时,PTQ 完全够用,没必要花大价钱去磨合。
选型判据:
选型决策表,就是跑步教练手边的那本手册:什么赛道、什么天气、什么鞋底,决定要不要花三周去磨合。而 QAT 这双“磨合好的鞋”,正是低比特时代让模型既快又准的关键装备。
- 精度落差$\Delta acc = acc_{FP32} - acc_{PTQ}$ 表示 PTQ 相对浮点模型掉了几点精度,这是最直接的信号。
- 阈值判断$\Delta acc > \theta$ 说明掉点超出业务容忍限度,方案切换为 QAT,θ 一般取 1%~3%。
- 低比特强制$bit < 8$ 时量化噪声剧烈,PTQ 大概率崩精度,系统直接走 QAT 通道。
AI 应用场景
端侧语音唤醒芯片
阶段:模型压缩与端侧部署优化
解决的问题:智能音箱、手机的 always-on 语音唤醒模型需要常驻内存,FP32 版本占用约 4MB,INT8 只需 1MB,但直接 PTQ 量化后,唤醒词在嘈杂环境下误触发率飙升——就像新鞋在雨天打滑。QAT 让模型在训练阶段就适应 8bit 数值网格,唤醒率几乎无损。
具体案例:某智能音箱厂商把一个 6MB 的唤醒 CNN 量化到 INT8,PTQ 后唤醒率从 97% 掉到 88%;改用 QAT 在带噪训练集上微调 2 个 epoch,唤醒率回到 96.2%,内存占用直降 75%,电池续航显著提升。
自动驾驶感知模型 INT8 部署
阶段:感知模型落地与车端推理
解决的问题:车端推理芯片的 INT8 算力是 FP32 的 3 倍以上,但 PTQ 会让感知模型 mAP 掉 2~3 个百分点,对 30 米外的行人、障碍物检测是致命的。QAT 像让模型提前在“低比特的雨天赛道”上训练,部署时才能稳稳识别。
具体案例:某 L2+ 辅助驾驶项目将 YOLO 系列检测模型量化为 INT8,PTQ 后 mAP 从 0.82 跌至 0.76,远距离小目标漏检明显;用 QAT 在自动驾驶数据集上微调 3 个 epoch,mAP 恢复到 0.81,推理延迟从 30ms 降到 11ms。
大模型低比特权重量化
阶段:大模型推理优化与部署
解决的问题:7B 参数的大模型 FP16 需要 14GB 显存,4bit 量化后仅需 3.5GB,能塞进单张消费级显卡。但 PTQ 的 4bit 量化会让困惑度(PPL)显著恶化,回答质量明显下降。QAT(配合 LoRA 微调)让低比特权重“重新磨合”,接近 16bit 的生成质量。
具体案例:开源 LLaMA/ChatGLM 类模型采用 W4A16 方案时,PTQ 后 PPL 增加 0.3~0.5,生成明显胡言乱语;用 QAT 对注意力权重做量化感知微调后,PPL 增加仅 0.02,KVCache 显存占用减少约 40%。
Python 演示
import torchimport torch.nn as nnimport torch.nn.functional as F class FakeQuant(torch.autograd.Function): """伪量化算子:前向走量化-反量化,反向走 STE 直通。""" @staticmethod def forward(ctx, x, scale): qmin, qmax = 0, 255 xq = torch.clamp(torch.round(x / scale), qmin, qmax) ctx.save_for_backward(x, scale) ctx.qmin, ctx.qmax = qmin, qmax return xq * scale @staticmethod def backward(ctx, grad_output): x, scale = ctx.saved_tensors low = scale * ctx.qmin high = scale * ctx.qmax mask = ((x >= low) & (x <= high)).float() return grad_output * mask, None class QuantLinear(nn.Module): def __init__(self, in_f, out_f, bits=8): super().__init__() self.weight = nn.Parameter(torch.randn(in_f, out_f) * 0.1) self.bits = bits def forward(self, x): qmax = 2 ** (self.bits - 1) - 1 scale = self.weight.abs().max() / qmax wq = FakeQuant.apply(self.weight, scale) return F.linear(x, wq.t()) def main(): torch.manual_seed(42) model = QuantLinear(4, 1, bits=8) opt = torch.optim.SGD(model.parameters(), lr=0.05) x = torch.randn(128, 4) y = (2.0 * x[:, 0] - 1.0 * x[:, 1] + 0.3).unsqueeze(1) for epoch in range(300): opt.zero_grad() loss = F.mse_loss(model(x), y) loss.backward() opt.step() if epoch % 60 == 0: print("epoch", epoch, "loss", round(loss.item(), 5)) qmax = 2 ** (model.bits - 1) - 1 scale = model.weight.abs().max() / qmax wq = torch.round(model.weight / scale) * scale print("量化后权重样例:", wq.detach().numpy().ravel()[:4]) if __name__ == "__main__": main()
xq = torch.clamp(torch.round(x / scale), qmin, qmax)伪量化前向的“除-舍-截”三连击,把浮点值钉到整数网格。return xq * scale反量化回浮点域,让前向数值带上量化噪声,但不改变张量类型。mask = ((x >= low) & (x <= high)).float()计算 STE 的指示函数,量化范围外为 0,范围内为 1。return grad_output * mask, None反向梯度直通:范围内梯度原样传递,范围外梯度置零。scale = self.weight.abs().max() / qmax用权重的最大绝对值推导对称量化尺度,等价于 Observer 的 MinMax 策略。wq = FakeQuant.apply(self.weight, scale)将伪量化施加到训练中的权重上,每次前向都“试穿”量化鞋。loss = F.mse_loss(model(x), y)QAT 不需要特殊损失函数,普通任务损失加反向传播即可更新权重。
常见误区
误区很多人觉得 QAT 无非是在训练时往模型里注入一些随机噪声,当成一种数据增强手段,跑几个 epoch 就算完事。
正解QAT 的核心是伪量化算子:前向把权重和激活真正投影到低比特网格再反量化,反向用 STE 让梯度穿越不可导的取整函数。它让权重在 INT8 网格附近“安家落户”,而不是简单地增强鲁棒性。数据增强是让模型见多识广,QAT 是让模型在限定格子里学会走路,两者机制完全不同。
为何常见:伪量化前后数值差异确实像加了噪声,加上“感知训练”这个词容易让人联想到扰动训练,从而忽略其“网格约束”的本质。
误区以为 Straight-Through 就是不管三七二十一,把损失对量化输出的梯度当作对输入的梯度,量化误差对训练毫无影响。
正解STE 有一个重要的“护栏”——指示函数。当输入 x 超出量化范围时,梯度会被置零,权重不再更新。这相当于跑者步子踩出跑道就被裁判拦下。如果没有这个截断保护,权重会被推到离群区,量化误差越滚越大。
为何常见:“直通”这个名字太有误导性,让人以为梯度全程畅通无阻,忽略了公式里那个默默把关的指示函数。
误区初学者常以为 QAT 跑完,模型文件里的权重全是 INT8 整数,直接拷到推理引擎就能跑。
正解QAT 后的权重依然是 FP32 浮点。伪量化只在训练的前向中“模拟”量化效果,并不真正把权重存成低比特整数。部署时,工具链会根据训练时统计的 scale 和 zero point,把浮点权重做真实量化转换。这也是 FakeQuant 中"Fake"一词的来源——模拟,而非真做。
为何常见:伪量化算子名字里带“量化”,训练过程又全程在低比特“感受”,很容易让人误以为权重已经被真实量化存储。
误区有人在宽模型、8bit 下做了几次实验,发现 QAT 和 PTQ 精度相近,于是断言“QAT 纯属浪费时间”。
正解8bit 宽量化时噪声本来就小,两者差距容易被掩盖。但位宽降到 4bit/2bit,或者网络很小、数据分布极不均匀时,PTQ 的精度会断崖式下跌,QAT 能补回大部分损失。选型要看精度差阈值和位宽,不能拿一个宽松场景的结论套所有场景。
为何常见:人的直觉容易基于有限的几次实验以偏概全,忽视了“位宽越低、网络越敏感,QAT 优势越大”的单调趋势。
误区听到“量化感知训练”就以为要像预训练那样从头训一个大模型,几周 GPU 算力烧不起,于是干脆放弃 QAT。
正解QAT 几乎总是在预训练权重上做轻量微调:插入伪量化算子,学习率缩小一到两档,训练 1~5 个 epoch 即可。成本远低于预训练,通常只需几个小时 GPU 时间,却能换回低比特部署的精度保障。它不是另起炉灶,而是给老模型“换鞋磨合”。
为何常见:名字里的“训练”两个字给了人“重头再来”的心理暗示,忽略了它本质上是微调(fine-tuning)的一种特殊形式。
面试题
Q1. 伪量化算子的“伪”主要体现在哪里?
- A.训练中模拟量化误差← 正确答案
- B.量化结果完全不准
- C.无需任何超参数参与
- D.只在前向推理时执行
答案:A。伪量化在训练前向中完整执行量化-反量化,但模型权重仍以浮点存储,只是“模拟”低比特效果,故称为“伪”。B 说它不准则误解了模拟的意义;C 错,scale 等超参数必不可少;D 错,它主要在训练时执行,推理时是真实量化。
Q2. PTQ 与 QAT 的量化时机差别是什么?
- A.PTQ 在训练完成后量化← 正确答案
- B.QAT 在推理时动态量化
- C.两者都在训练中量化
- D.两者都在部署后量化
答案:A。PTQ 是训练完毕再做量化,不动原始权重;QAT 是在训练过程中就插入伪量化让模型适应低比特。这就像新鞋是比赛日才穿,还是提前三周穿,差别全在时机。
Q3. STE 中 round 函数反向求导如何近似处理?
- A.直接视为恒等函数求导← 正确答案
- B.导数记为零不更新权重
- C.用 sigmoid 替代取整函数
- D.对导数取绝对值再回传
答案:A。STE 的核心是把不可导的取整近似成恒等函数,让梯度直接穿过。B 是 round 的真实导数,那样梯度会全灭;C、D 是其他领域的技巧,与 STE 无关。
Q4. 下列哪些属于伪量化前向计算的操作?
- A.除以尺度得到整数域← 正确答案
- B.四舍五入模拟取整← 正确答案
- C.截断到限定整数范围← 正确答案
- D.直接存储 int8 权重
答案:ABC。伪量化前向是“除-舍-截-乘”四步,A/B/C 分别对应前三步。D 是部署时的真实量化操作,伪量化训练阶段并不会真的把权重存成 int8。
Q5. Observer 在 QAT 中的核心作用是什么?
- A.统计张量动态范围求尺度← 正确答案
- B.计算交叉熵损失值
- C.自动调整网络学习率
- D.裁剪冗余的神经元
答案:A。Observer 就像量鞋码的尺子,统计权重/激活的 min-max 或分位点,再算出量化尺度 s。B 是损失函数职责,C 是优化器职责,D 是剪枝相关,都与 Observer 无关。
Q6. 当权重数值超出量化范围时,STE 的反向梯度会发生什么?
- A.被指示函数置为零← 正确答案
- B.放大十倍再继续回传
- C.保持原值继续传播
- D.只更新对应的偏置项
答案:A。STE 中的指示函数在范围外取 0,梯度被阻断,权重不再更新。这相当于跑者步子踩出跑道就被裁判拦下,防止权重被推向无效区间。
Q7. 相比 PTQ,QAT 需要付出哪些额外代价?
- A.需要额外训练多个 epoch← 正确答案
- B.需要带标签的训练数据← 正确答案
- C.部署流程完全不需要变化
- D.需要更多计算资源开销← 正确答案
答案:ABD。QAT 要在预训练模型上微调,自然需要训练数据和算力开销。C 是错的——QAT 部署时仍要同步 scale 等量化参数,流程比 PTQ 略复杂。
Q8. QAT 微调时学习率一般如何设置?
- A.取预训练末期的较小值← 正确答案
- B.比预训练用更大的学习率
- C.学习率设为固定常数 1
- D.学习率必须设置为零
答案:A。QAT 是轻量微调,学习率取预训练末期的小值(约为其 1/10~1/100),避免破坏已学特征。B 会导致灾难性遗忘,C/D 无法正常优化。
Q9. 哪些场景下应当优先选择 QAT 而非 PTQ?
- A.低比特如 INT4 精度敏感场景← 正确答案
- B.PTQ 精度掉幅超出业务阈值← 正确答案
- C.完全没有任何训练数据可用
- D.只追求最短的部署时间
答案:AB。位宽低于 8bit 或 PTQ 掉点超阈值(通常 1%~3%)时,QAT 是保精度首选。C 没数据 QAT 无法训练;D 只求快应选 PTQ,而不是 QAT。
Q10. QAT 训练完成后,部署时权重如何变成真正的低比特?
- A.仍是浮点,按尺度转为整数← 正确答案
- B.权重会直接保存为整数
- C.部署时不再需要尺度参数
- D.权重会变成二进制表示
答案:A。QAT 训练后的权重是 FP32,部署时由推理工具链按训练时统计的 scale/zero point 做真量化。B 是训练阶段的误解,C 错,尺度恰恰是关键参数,D 描述不准确。