这篇要解决一个绕不开的岔路口:同样是把模型量化,为什么有的方法几分钟就搞定,有的却要重新训练好几天?这就是 PTQ 和 QAT 两条路线的区别。搞懂它俩,你才能明白后面要学的 AWQ、GPTQ 为什么都归在 PTQ 这一派,以及什么时候你才真的需要动用 QAT。
目录
- 一、先看两个真实场景
- 二、一句话说清 PTQ 和 QAT
- 三、原理拆解:两条路线到底差在哪
- 四、关键权衡:成本、精度、门槛
- 五、上手实测:跑一个最小的 QAT 循环
- 六、PTQ vs QAT 横向对比
- 七、常见误区与避坑
- 八、小结
一、先看两个真实场景
先不讲定义,看两个你大概率会遇到的场景。
场景 A:你从 HuggingFace 下了个 Qwen2.5-7B,想塞进自己的 4090 跑推理。你不想训练,也没有训练数据,就想让它占的显存小一点、还能正常回答问题。你需要的是——拿到模型权重,几分钟内量化完,直接用。
场景 B:你在做一个端侧语音唤醒模型,要部署到一块算力极弱的 MCU 上,只能跑 INT8 甚至更低。这个模型量化后精度掉得厉害,业务不能接受。你手上有训练数据和训练流程,愿意花时间重新训练来把精度捞回来。
场景 A 就是 PTQ 的地盘,场景 B 才轮到 QAT 出场。绝大多数玩大模型的人,一辈子只会用到场景 A。这也是为什么这个系列的重头戏 AWQ、GPTQ、GGUF 全都是 PTQ 方法。但 QAT 的原理你还是得懂,因为它能帮你理解"量化误差到底能不能被训练消化掉"这件事。
二、一句话说清 PTQ 和 QAT
PTQ(Post-Training Quantization,训练后量化):模型已经训练好了,拿过来直接量化。不碰训练过程,顶多用一小批数据"校准"一下,统计统计激活值的分布。
QAT(Quantization-Aware Training,量化感知训练):在训练(或微调)的时候,就假装模型已经被量化了,让模型在训练过程中提前"适应"量化带来的误差,训完再真正量化。
打个不太严谨但好记的比方:
- PTQ 像是衣服做好了再改小一号——快,但可能这里紧那里绷,将就着穿。
- QAT 像是量着你的身材从头做一件——合身,但费工费料。
大模型时代 PTQ 之所以成主流,道理很简单:一个 70B 模型重新训练一遍的成本高到离谱,没人愿意为了量化去烧那么多卡。而 PTQ 只要几张校准样本、几十分钟就能出结果,精度还能靠好算法(AWQ/GPTQ)保住。性价比这一项,PTQ 赢麻了。
三、原理拆解:两条路线到底差在哪
3.1 PTQ 在干什么
PTQ 的流程短得可怜:
1. 加载训练好的 FP16 权重 2.(可选)喂一小批校准数据,前向跑一遍,统计每层激活值的范围 3. 根据统计到的范围,给每层算 scale 和 zero-point 4. 把权重量化成低比特,存起来关键在第 2 步的校准(calibration)。为什么权重量化不太需要校准、激活量化却需要?因为权重是静态的、训练完就固定了,拿过来直接看数值分布就行;而激活值是动态的,输入不同、激活就不同,你得拿一批有代表性的数据跑一遍,才知道激活大概落在什么范围,才能定合理的 scale。
校准数据不用多,通常几百条就够,但质量要有代表性——你拿一堆乱码去校准一个中文对话模型,量化出来肯定拉胯。这点第 20 篇讲调优时会展开。
PTQ 里又分两种细法:
- 动态量化(dynamic):权重提前量化好,激活在推理时实时算 scale。灵活,但每次推理都要算一次,有开销。
- 静态量化(static):权重和激活的 scale 都提前用校准数据定好。推理时不用现算,更快,但依赖校准数据的质量。
大模型权重量化(AWQ/GPTQ)基本都是静态那一挂。
3.2 QAT 在干什么
QAT 的核心动作叫伪量化(fake quantization)。听着玄乎,其实就一句话:训练的时候,在前向传播里插入"量化再反量化"这一步,让模型看到的是量化后的近似值,但梯度还是照常回传。
前向传播长这样:
正常训练: x → 权重 W → 输出 QAT 训练: x → W 先量化再反量化(W_hat = D(Q(W))) → 输出模型在训练时就"尝到"了量化误差的苦,于是会自己调整权重,让这些误差对最终结果的影响变小。等训练完,真正量化的时候,精度损失就小很多。
3.3 QAT 绕不过的一个坎:round 不可导
这里有个数学上的麻烦。QAT 要在训练里插量化操作,但量化里的round(取整)这个函数,梯度几乎处处为 0——你想想,round 是个阶梯函数,平的地方斜率是 0,跳变的地方又不可导。梯度是 0,反向传播就传不下去,模型根本没法学。
解决办法叫STE(Straight-Through Estimator,直通估计器),思路简单粗暴到有点耍赖:
前向: 正常做 round,该量化量化 反向: 假装 round 不存在,梯度直接原样穿过去(把 round 的梯度当成 1)就是"前向该干嘛干嘛,反向时对 round 睁一只眼闭一只眼"。这招虽然理论上不严谨,但工程上非常好使,是几乎所有 QAT 实现的标配。等下第五节的代码里你会亲眼看到它怎么写。
四、关键权衡:成本、精度、门槛
选 PTQ 还是 QAT,本质是在三件事上做取舍。
第一是成本。PTQ 便宜到几乎可以忽略——一张校准数据集、一次前向,搞定。QAT 要跑完整的训练循环,大模型上这意味着大量的卡时和电费。这一条基本就把 QAT 挡在了大多数人门外。
第二是精度。在极低比特(INT4 以下、甚至 INT2)或者对精度极度敏感的场景,QAT 确实能比 PTQ 多捞回几个点,因为模型有机会在训练中主动适应量化误差。但在 INT8、INT4 这些主流档位上,现代 PTQ 算法(AWQ/GPTQ)已经把精度做得很接近原模型了,QAT 那点额外收益往往不值得它的成本。
第三是门槛和数据。QAT 要求你有完整的训练流程和训练数据。很多情况下你根本拿不到——比如你用的是别人开源的模型,人家没给训练数据,你也没法复现训练。PTQ 就没这问题,有模型权重就能干活。
一句话总结这个权衡:能用 PTQ 解决就别碰 QAT。只有当 PTQ 精度实在不达标、你又有训练资源和数据时,QAT 才是那根救命稻草。
五、上手实测:跑一个最小的 QAT 循环
光说不练假把式。我们用 PyTorch 手写一个最小的 QAT 例子,把伪量化和 STE 都跑一遍,让你看清 QAT 到底在代码层面做了什么。为了看得清楚,我们不用现成的量化库,而是自己实现伪量化层。
5.1 先实现一个带 STE 的伪量化函数
importtorchimporttorch.nnasnnclassFakeQuantSTE(torch.autograd.Function):"""伪量化 + 直通估计器(STE) 前向:正常量化再反量化 反向:梯度原样穿过(假装 round 不存在) """@staticmethoddefforward(ctx,x,num_bits=8):qmax=2**(num_bits-1)-1# INT8 -> 127s=x.abs().max()/qmax# 对称量化 scaleq=torch.round(x/s).clamp(-qmax,qmax)x_hat=q*s# 反量化returnx_hat@staticmethoddefbackward(ctx,grad_output):# STE 的精髓:梯度直接透传,round 当作不存在returngrad_output,Nonedeffake_quant(x,num_bits=8):returnFakeQuantSTE.apply(x,num_bits)注意backward里直接把grad_output原样返回——这就是 STE。如果这里老老实实按 round 求导,梯度全是 0,模型一步都学不动。
5.2 搭一个带伪量化的小网络
classQATLinear(nn.Module):"""训练时对权重做伪量化的全连接层"""def__init__(self,in_features,out_features,num_bits=8):super().__init__()self.linear=nn.Linear(in_features,out_features)self.num_bits=num_bitsdefforward(self,x):# 关键:用伪量化后的权重做前向w_quant=fake_quant(self.linear.weight,self.num_bits)returntorch.nn.functional.linear(x,w_quant,self.linear.bias)classTinyNet(nn.Module):def__init__(self,num_bits=8):super().__init__()self.fc1=QATLinear(20,64,num_bits)self.fc2=QATLinear(64,2,num_bits)self.relu=nn.ReLU()defforward(self,x):x=self.relu(self.fc1(x))returnself.fc2(x)5.3 造点数据,跑 QAT 训练
torch.manual_seed(0)# 造一个简单的二分类数据集N=2000X=torch.randn(N,20)true_w=torch.randn(20,1)y=(X @ true_w>0).long().squeeze()X_train,y_train=X[:1600],y[:1600]X_test,y_test=X[1600:],y[1600:]deftrain_and_eval(num_bits,epochs=200,tag=""):model=TinyNet(num_bits=num_bits)opt=torch.optim.Adam(model.parameters(),lr=0.01)loss_fn=nn.CrossEntropyLoss()forepinrange(epochs):model.train()opt.zero_grad()out=model(X_train)loss=loss_fn(out,y_train)loss.backward()opt.step()# 评估model.eval()withtorch.no_grad():pred=model(X_test).argmax(dim=1)acc=(pred==y_test).float().mean().item()print(f"[{tag}]{num_bits}-bit QAT 测试准确率:{acc:.4f}")returnmodel# 对比不同比特下 QAT 的效果train_and_eval(num_bits=8,tag="QAT")train_and_eval(num_bits=4,tag="QAT")train_and_eval(num_bits=2,tag="QAT")典型输出(数字会因随机性略有波动):
[QAT] 8-bit QAT 测试准确率: 0.9725 [QAT] 4-bit QAT 测试准确率: 0.9600 [QAT] 2-bit QAT 测试准确率: 0.9075看这组数字:即使量化到 2-bit,QAT 训练出来的模型还有 90% 准确率。为什么这么抗造?因为模型在训练时就一直"戴着量化的镣铐跳舞",早就学会了怎么在低精度下把活干好。
5.4 对照实验:PTQ 会掉多少
我们再做个对照。先正常训练一个不带量化的模型,训完再直接量化(模拟 PTQ),看它掉多少:
classPlainNet(nn.Module):def__init__(self):super().__init__()self.fc1=nn.Linear(20,64)self.fc2=nn.Linear(64,2)self.relu=nn.ReLU()defforward(self,x):x=self.relu(self.fc1(x))returnself.fc2(x)defptq_experiment(num_bits):# 1. 正常训练(不带量化)model=PlainNet()opt=torch.optim.Adam(model.parameters(),lr=0.01)loss_fn=nn.CrossEntropyLoss()forepinrange(200):opt.zero_grad()loss=loss_fn(model(X_train),y_train)loss.backward()opt.step()# 2. 训完直接量化权重(PTQ)defquant_weight(w,num_bits):qmax=2**(num_bits-1)-1s=w.abs().max()/qmaxreturn(torch.round(w/s).clamp(-qmax,qmax))*swithtorch.no_grad():forlayerin[model.fc1,model.fc2]:layer.weight.data=quant_weight(layer.weight.data,num_bits)# 3. 评估withtorch.no_grad():pred=model(X_test).argmax(dim=1)acc=(pred==y_test).float().mean().item()print(f"[PTQ]{num_bits}-bit PTQ 测试准确率:{acc:.4f}")ptq_experiment(num_bits=8)ptq_experiment(num_bits=4)ptq_experiment(num_bits=2)典型输出:
[PTQ] 8-bit PTQ 测试准确率: 0.9700 [PTQ] 4-bit PTQ 测试准确率: 0.9325 [PTQ] 2-bit PTQ 测试准确率: 0.6875把两组数字摆一块看:
| 比特 | PTQ 准确率 | QAT 准确率 | 差距 |
|---|---|---|---|
| 8-bit | 0.9700 | 0.9725 | 几乎没差 |
| 4-bit | 0.9325 | 0.9600 | QAT 略胜 |
| 2-bit | 0.6875 | 0.9075 | QAT 大胜 |
结论一目了然:比特越低,QAT 的优势越明显。8-bit 时两者几乎打平,根本没必要费劲搞 QAT;可到了 2-bit,PTQ 直接崩到接近瞎猜(二分类瞎猜是 50%),而 QAT 还稳稳站在 90%。这就是"低比特场景 QAT 才值得出手"这句话的实验依据。
提醒一句:这是个玩具级实验,真实大模型上的表现会复杂得多,PTQ 有 AWQ/GPTQ 这些强算法加持,4-bit 的差距会比这里小很多。但"低比特 QAT 更抗造"这个定性结论是成立的。
六、PTQ vs QAT 横向对比
把这两条路线放一张表里,方便你以后查:
| 维度 | PTQ(训练后量化) | QAT(量化感知训练) |
|---|---|---|
| 是否需要训练 | 不需要 | 需要完整训练/微调 |
| 时间成本 | 分钟级 | 小时到天级 |
| 是否需要训练数据 | 只需少量校准数据 | 需要完整训练数据 |
| 精度(高比特 INT8) | 很好,接近原模型 | 很好,优势不明显 |
| 精度(低比特 INT4↓) | 靠好算法能保住 | 明显更优 |
| 落地门槛 | 低,有权重就能干 | 高,要训练流程 |
| 大模型适用性 | 主流选择 | 少数极端场景 |
| 代表方法 | AWQ、GPTQ、GGUF、bitsandbytes | 各类 QAT 框架 |
一句话记住:大模型量化,先 PTQ;PTQ 不行,再考虑 QAT。
七、常见误区与避坑
误区 1:QAT 一定比 PTQ 精度高,所以能用 QAT 就用 QAT。
不对。高比特(INT8)下两者几乎没差,QAT 白白多花几十倍成本。只有低比特或极端精度要求时 QAT 才有意义。别为了那点边际收益去烧卡。
误区 2:PTQ 不需要任何数据。
不全对。权重量化确实基本不用数据,但激活量化和 AWQ/GPTQ 这类方法需要校准数据。所以说 PTQ"不用训练数据"是对的,"不用任何数据"是错的。
误区 3:校准数据越多越好。
不对。校准数据讲究代表性而不是数量,几百条覆盖典型分布的样本通常就够了。堆几万条同质数据既慢又没额外收益,还可能因为分布偏了反而拖累效果。
误区 4:QAT 里 round 不可导没关系,反正框架会自动处理。
要知其所以然。框架确实帮你处理了,靠的就是 STE(直通估计器)。你得明白它是"前向真量化、反向假装 round 不存在",这样当 QAT 效果异常时你才知道从哪查起。
误区 5:把 QAT 当成"训练时就量化,能省训练显存"。
恰恰相反。QAT 训练时因为要同时维护浮点权重和伪量化操作,显存和计算开销比普通训练还高。QAT 省的是推理的资源,不是训练的。
八、小结
- PTQ 是训练后量化,拿现成模型直接量化,快、便宜、门槛低,是大模型量化的绝对主流,AWQ/GPTQ/GGUF 全在这一派。
- QAT 是量化感知训练,训练时就让模型适应量化误差,精度更好但成本高、要训练数据,只在低比特或极端精度场景才划算。
- PTQ 的关键动作是校准,尤其是激活量化,需要有代表性的校准数据。
- QAT 的关键技术是伪量化 + STE,用"前向真量化、反向透传梯度"绕过 round 不可导的问题。
- 我们用 PyTorch 手写实验实测了两者:8-bit 打平、4-bit QAT 略胜、2-bit QAT 大胜,验证了"比特越低 QAT 优势越大"。
- 选型原则一句话:能 PTQ 就别 QAT,PTQ 精度不达标且有训练资源时才上 QAT。