ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

INT8量化实战:从矩阵乘、校准到QAT与LLM量化全解析

INT8量化实战:从矩阵乘、校准到QAT与LLM量化全解析 模型跑起来只是第一步能不能跑得快、跑得省、跑得稳才是真正拉开差距的地方。我做了几年推理优化见过太多团队把 FP32 模型直接扔到线上结果单次推理延迟高得离谱GPU 利用率却上不去最后只能靠堆卡来扛流量。其实大部分场景下把模型从 FP32 压到 INT8精度损失可以控制在 1% 以内而推理吞吐能翻 2 到 4 倍显存占用直接砍掉四分之三。这篇就围绕INT8 矩阵乘、校准、QAT 和 LLM 量化这几件事把量化从原理到落地的完整链路拆开讲清楚不管你是刚接触量化的新手还是已经在做推理优化的老手都能从中找到可以直接复用的方法和踩坑经验。1. 量化到底在做什么从浮点到定点的本质转换1.1 为什么 FP32 是奢侈的先算一笔账。一个 FP32 参数占 4 字节一个 7B 参数的模型光权重就要 28GB 显存这还没算激活值和 KV Cache。而 INT8 每个参数只占 1 字节同样的模型权重降到 7GB一张消费级显卡就能装下。除了存储计算开销的差异更关键FP32 的矩阵乘需要浮点运算单元而 INT8 矩阵乘可以用整数运算单元后者在同样芯片面积下能塞进更多的计算核心单位时间能处理的乘加操作MAC数量高出一个数量级。但量化不是简单地把小数点后面的数扔掉。浮点数的优势在于动态范围大能表示 1e-38 到 1e38 之间的数而 INT8 只能表示 -128 到 127 这 256 个整数值。量化的核心任务就是找到一个映射关系把这 256 个整数合理地铺在原始浮点数的取值范围内让信息损失最小。1.2 仿射量化的数学表达最常用的量化方式是仿射量化公式很简洁q round(x / scale zero_point) x_hat (q - zero_point) * scale其中x是原始浮点值q是量化后的整数scale是缩放因子浮点数zero_point是零点偏移整数。scale决定了量化的粒度zero_point保证了浮点零能精确映射到某个整数上——这一点对卷积和矩阵乘里的 padding 操作特别重要因为 padding 用的就是零。反量化就是把整数还原回浮点但注意这个过程是有损的。x_hat和x之间的差距就是量化误差误差大小取决于scale。scale越大相邻整数之间的间隔越大误差越大scale越小能表示的动态范围越窄超出范围的值会被截断clamp。1.3 对称量化与非对称量化的取舍对称量化强制zero_point 0量化范围是[-127, 127]或[-128, 127]正负对称。它的好处是计算简单反量化时不需要减零点矩阵乘的累加过程也更干净。权重通常用对称量化因为权重分布一般以零为中心对称量化不会浪费表示能力。非对称量化的zero_point可以是任意整数量化范围是[0, 255]或[-128, 127]。它适合激活值因为 ReLU 之后的激活值全是非负的用非对称量化能把整个范围都利用起来精度更高。实际部署中权重用对称 INT8激活用非对称 INT8这是最常见的组合。提示如果你的硬件只支持对称量化部分推理加速器就是这样激活值也需要强制对称这时候建议在 ReLU 之前做量化或者用带符号的激活函数来缓解精度损失。2. INT8 矩阵乘量化推理的性能核心2.1 为什么矩阵乘是量化的主战场Transformer 和 CNN 里超过 90% 的计算量都集中在矩阵乘和卷积上而卷积在 im2col 之后本质上也是矩阵乘。所以量化能不能带来加速关键看矩阵乘能不能用上 INT8 指令。以 NVIDIA 的 GPU 为例Turing 架构之后的 Tensor Core 都支持 INT8 矩阵乘理论算力是 FP16 的 2 倍、FP32 的 4 倍甚至更多。CPU 这边Intel 的 VNNI 指令集和 ARM 的 dotprod 指令也都是为 INT8 矩阵乘设计的。但这里有个容易被忽略的点INT8 矩阵乘的累加器通常是 INT32。为什么因为两个 INT8 相乘结果范围是[-128*127, 127*127]大约是[-16256, 16129]而 K 维累加之后结果可能超出 INT16 的范围。用 INT32 累加可以保证在 K 不超过 2^15 左右时不溢出这对绝大多数模型层都够用。2.2 量化矩阵乘的完整计算流程假设我们要计算C A * B其中 A 和 B 都已经量化成 INT8A_q round(A / s_a) z_a B_q round(B / s_b) z_b C_q A_q * B_q (INT8 乘 INT8INT32 累加) C (C_q - z_a * sum(B_q) - z_b * sum(A_q) K * z_a * z_b) * s_a * s_b最后那一步反量化看起来复杂但实际推理时z_a、z_b、s_a、s_b都是提前算好的常量sum(B_q)和sum(A_q)也可以在预处理阶段算出来。真正在推理时做的就是一次 INT8 矩阵乘加一次带常量的仿射变换开销很小。如果是对称量化z_a z_b 0公式直接简化成C C_q * s_a * s_b干净利落。这也是为什么很多推理框架在权重侧坚持用对称量化的原因——省掉了零点修正的计算。2.3 实测中的性能表现与瓶颈我在一张 T4 上做过对比测试ResNet-50 的单个卷积层FP32 推理耗时约 2.3msINT8 量化后降到 0.7ms加速比约 3.3 倍。但并不是所有层都能拿到这个加速比。深度可分离卷积MobileNet 系列大量使用的加速比通常只有 1.5 到 2 倍因为它的计算密度低瓶颈在内存带宽而不是计算单元量化带来的计算优势被内存访问开销吃掉了。另一个瓶颈是反量化操作。如果每一层做完 INT8 矩阵乘都立刻反量化回 FP32再传给下一层重新量化那来回转换的开销会抵消掉不少收益。好的推理引擎会把连续的量化层融合在一起中间保持 INT8 状态只在必要的时候才反量化。TensorRT 和 ONNX Runtime 都做了这种图优化。层类型FP32 耗时INT8 耗时加速比瓶颈标准卷积 3x32.3ms0.7ms3.3x计算深度可分离卷积1.1ms0.6ms1.8x内存带宽全连接层0.9ms0.3ms3.0x计算LayerNorm0.4ms0.4ms1.0x不适合量化注意LayerNorm、Softmax 这类对数值精度敏感的算子通常保持 FP16 或 FP32强行量化会导致精度断崖式下跌。量化方案里要明确哪些层跳过。3. 校准决定量化精度的关键一步3.1 校准在解决什么问题训练好的模型权重是已知的可以直接统计出 min/max 来确定 scale。但激活值是推理时动态产生的训练阶段拿不到真实的推理输入分布。校准Calibration就是用一批有代表性的样本跑一遍前向传播统计每一层激活值的分布从而确定激活的 scale 和 zero_point。校准样本的选择直接决定量化精度。我见过有人随便拿几十张图做校准结果量化后精度掉了 5 个点换成从验证集里分层采样的 500 张图精度损失降到 0.8%。校准集不需要标注但必须和真实推理数据的分布一致。如果你的线上数据有季节性变化校准集也要覆盖这些变化。3.2 三种主流校准算法对比Min-Max 校准是最简单的方式直接取激活值的全局最小值和最大值作为量化范围。它的优点是实现简单、无超参缺点是容易被离群值outlier带偏。如果某一层激活里有个别极大的值整个量化范围会被拉大导致大部分正常值挤在很小的整数区间里精度严重损失。Moving Average Min-Max在 Min-Max 基础上做了滑动平均用多批数据的统计量平滑极值对离群值有一定鲁棒性。TensorRT 默认用的就是这种。KL 散度校准也叫 Entropy 校准是精度最好的方案。它的思路是不直接用 min/max而是找一个截断阈值让截断后的分布和原始分布的 KL 散度最小。换句话说它主动把那些极端的离群值砍掉用牺牲少量极端值的方式换取整体分布的量化精度。NVIDIA 的 TensorRT 和很多量化工具都支持这种校准。校准算法精度实现复杂度对离群值鲁棒性适用场景Min-Max低简单差分布均匀、无离群值Moving Average中中等中通用场景KL 散度高复杂好精度要求高的场景3.3 校准实操从数据准备到参数导出以 ONNX Runtime 的静态量化为例完整流程是这样的from onnxruntime.quantization import quantize_static, CalibrationDataReader import numpy as np class MyCalibrationReader(CalibrationDataReader): def __init__(self, calibration_data): self.data calibration_data self.index 0 def get_next(self): if self.index len(self.data): return None batch self.data[self.index] self.index 1 return {input: batch} # 准备 200-500 个校准样本 calib_data [np.random.randn(1, 3, 224, 224).astype(np.float32) for _ in range(300)] reader MyCalibrationReader(calib_data) quantize_static( model_inputmodel.onnx, model_outputmodel_int8.onnx, calibration_data_readerreader, quant_formatQuantFormat.QDQ, per_channelTrue, activation_typeQuantType.QUInt8, weight_typeQuantType.QInt8 )几个关键参数值得说明。per_channelTrue表示权重按通道分别量化每个输出通道有独立的 scale比全局量化精度高不少代价是多了少量存储。activation_typeQUInt8用非对称量化激活weight_typeQInt8用对称量化权重这是最稳的组合。quant_formatQDQ会在模型里插入 QuantizeLinear/DequantizeLinear 节点兼容性好方便调试。校准样本数量上我的经验是 200 到 500 个就够再多收益递减。但样本的多样性比数量更重要要覆盖不同的输入模式。校准完之后一定要在验证集上跑一遍精度对比如果掉点超过 2%就得回头检查校准集或者换校准算法。4. QAT把量化误差训练掉4.1 PTQ 的天花板在哪里训练后量化PTQ不用重新训练流程简单是大多数场景的首选。但它有个天花板当模型对量化误差特别敏感时比如检测模型的小目标分支、分割模型的边界区域PTQ 怎么调都救不回来。我做过一个语义分割模型PTQ 之后 mIoU 掉了 4 个点换了几种校准算法都只能压到 3 个点最后上 QAT 才把损失控制在 0.5 个点以内。PTQ 精度不够的根因在于量化误差是推理时才引入的训练阶段模型完全不知道量化的存在所以学到的权重对量化噪声没有鲁棒性。QAT 的思路就是在训练阶段模拟量化过程让模型提前适应量化误差。4.2 QAT 的核心机制伪量化节点QAT 的关键是伪量化Fake Quantization。它在训练的前向传播里插入量化-反量化的模拟操作x_fake_quant dequantize(quantize(x))注意quantize里有round操作而round的梯度几乎处处为零直接反向传播会导致梯度消失。解决办法是用直通估计器STEStraight-Through Estimator前向传播时正常做 round反向传播时把梯度直接透传过去假装 round 不存在。这样权重就能在量化约束下继续更新。QAT 的完整流程分三步加载预训练模型在模型里插入伪量化节点通常放在权重和激活之后。微调训练用较小的学习率通常是原始学习率的 1/100 到 1/10训练几个 epoch让模型适应量化噪声。导出量化模型训练完成后把伪量化节点替换成真正的量化算子生成 INT8 模型。4.3 QAT 实操中的坑与调参经验QAT 听起来美好实操里有几个坑必须提前知道。第一个坑是学习率。学习率太大模型会在量化约束下震荡精度反而比 PTQ 还差学习率太小模型来不及适应量化噪声。我的经验是从原始学习率的 1/100 开始训 5 到 10 个 epoch观察验证集精度曲线如果还在上升就继续训如果震荡就降学习率。第二个坑是 BatchNorm 的统计量。QAT 微调时BatchNorm 的 running mean 和 variance 会随着量化后的激活分布变化如果还用训练前的统计量推理时会出问题。正确做法是在 QAT 训练时让 BatchNorm 继续更新统计量或者在训练结束后用一批数据重新校准 BN 统计量。第三个坑是伪量化节点的位置。不是所有层都适合插伪量化。LayerNorm、Softmax、残差连接的加法操作这些地方插了反而伤精度。主流框架PyTorch 的torch.quantization、TensorFlow 的tf.quantization都有默认的量化配置会跳过这些敏感层不要自己乱改。import torch.quantization as tq model.qconfig tq.get_default_qat_qconfig(fbgemm) model_fused tq.fuse_modules(model, [[conv1, bn1, relu]]) model_prepared tq.prepare_qat(model_fused, inplaceFalse) # 微调训练 for epoch in range(10): train_one_epoch(model_prepared, train_loader, optimizer) # 转换为量化模型 model_prepared.eval() model_int8 tq.convert(model_prepared, inplaceFalse)提示QAT 训练时一定要用model.train()模式让伪量化节点生效转换前必须切到model.eval()否则 BatchNorm 会用 batch 统计量导致推理结果不稳定。5. LLM 量化大模型时代的特殊挑战5.1 为什么 LLM 量化比 CNN 难LLM 量化和传统 CNN 量化有本质区别。CNN 的激活值经过 ReLU 之后分布相对规整而 LLM 的激活值里存在大量离群值outlier。这些离群值可能比正常值大几十倍甚至上百倍用 Min-Max 校准的话整个量化范围被这几个离群值撑大其他 99% 的值全挤在几个整数格子里精度直接崩掉。更麻烦的是LLM 的离群值不是随机分布的而是集中在某些特定的通道channel上。研究发现Transformer 的某些注意力头和 FFN 的特定维度会系统性地产生大激活值这跟模型的训练动态有关。所以 LLM 量化不能简单套用 CNN 的方案。5.2 权重量化GPTQ 与 AWQ 的路线差异LLM 量化的第一波浪潮是权重量化也就是只量化权重激活保持 FP16。这样虽然拿不到 INT8 矩阵乘的全部加速但显存占用能砍掉一半以上对 LLM 这种显存瓶颈明显的场景收益巨大。GPTQ的思路是逐层做量化用二阶信息Hessian 矩阵来指导权重的舍入方向。它不是简单地把权重 round 到最近的整数而是考虑量化误差对输出的影响把误差补偿到其他权重上。GPTQ 能把 7B 模型量化到 4bit 而精度损失很小是早期最流行的方案。AWQActivation-aware Weight Quantization的洞察是不是所有
返回列表