一、先想清楚:推理优化到底在优化什么
很多人做推理优化只盯着「模型变小」,其实大模型服务的瓶颈是一个三角形:显存占用(Memory)、吞吐(Throughput)、首字延迟(TTFT / Latency)。量化主要压显存,但压完显存后瓶颈往往会转移到内存带宽上——因为 decode 阶段每生成一个 token 都要把全部权重从显存读一遍,是典型的 memory-bound 计算。这也是为什么 INT4 权重即使「计算量」没减少,生成速度依然能快 2~3 倍:权重搬运变少了。
所以在动手前先问自己三个问题:目标是单卡跑得下,还是多用户吞吐高,还是端侧低延迟?三个目标对应的优化组合完全不同。比如离线批量生成,用 GPTQ + continuous batching 收益最大;交互式聊天,则要重点压 TTFT,考虑预填(prefill)与解码(decode)分离。
提示:先用nvidia-smi或 profiler 看清楚瓶颈是「显存不够」还是「带宽打满」。用量化去解决带宽问题是对的,用量化去解决算力不足问题基本没用,那是batch size和算子融合的战场。
二、量化是什么:从 FP16 到 INT4 的数学直觉
量化的本质是用更少的比特表示权重,并把精度损失控制在可接受范围内。一个 FP16 权重占 16 bit,INT4 只占 4 bit,理论上模型体积直接缩到 1/4。核心公式很简单:q = round(w / scale) + zero_point,推理时反量化 w ≈ (q - zero_point) * scale。
精度从哪里来?关键在于 scale 的粒度。如果整个权重矩阵共用一个 scale,量化误差会非常大;而如果按 per-channel(每个输出通道一组 scale)甚至 per-group(每 32~128 个元素一组),误差就能被局部数据分布吸收。所以现代方案几乎都是 group-wise quantization,这也是 GPTQ、AWQ 的默认做法。
import torch
def quantize_int8_per_channel(w: torch.Tensor):
"""最朴素的 per-channel 对称量化,帮助理解 scale 是怎么来的"""
scales = w.abs().amax(dim=1, keepdim=True) / 127.0 # 每个输出通道一组 scale
q = torch.clamp(torch.round(w / scales), -127, 127).to(torch.int8)
return q, scales
w = torch.randn(256, 512)
q, s = quantize_int8_per_channel(w)
w_hat = q.float() * s
print("体积比:", q.numel() * 8 / (w.numel() * 16), "相对误差:", (w - w_hat).norm() / w.norm())
这里可以看到 scale 是由该组数据的绝对值最大值推出来的。AWQ 的洞察更进一步:不是所有权重同等重要,激活值 activation 大的那些通道对应的权重应该「温柔对待」,可以用逐通道缩放(per-channel scaling) 把敏感权重的量化误差压下去,而不是简单地平均分配比特。
三、主流量化方案怎么选:PTQ / GPTQ / AWQ / GGUF
量化分两大流派:QAT(训练时量化) 和 PTQ(训练后量化)。工业界落地几乎全是 PTQ,因为没人愿意为了量化重新训练一次 70B 模型。PTQ 里又分几个有代表性的方案:
- 朴素 RTN(Round-to-Nearest):直接四舍五入,不需要数据,精度掉得厉害,只适合做基线。