ARTICLE DETAIL

资讯详情

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

模型量化全解析:从INT8校准到QAT与LLM部署优化

模型量化全解析:从INT8校准到QAT与LLM部署优化 1. 量化是部署优化里回报最高的一刀做推理优化的人迟早会撞上“量化”这个词。我第一次被量化逼着动手是在一个边缘盒子的项目里模型 FP32 跑一遍要 40ms客户却要求 10ms 以内内存只给了 512MB。最后把主要算子切成 INT8延迟掉到 12ms内存也省了四分之三。从那时起量化就不只是论文里的概念而是部署链路里最实在的杠杆。这篇文章想聊清楚一整条链路INT8 矩阵乘怎么做、校准Calibration到底在干什么、QAT 什么时候必须上以及 LLM 量化为什么是另一套玩法。适合正在做模型部署和推理加速的工程师也适合刚入门、想系统搞懂量化原理的同学。1.1 FP32、FP16、INT8 到底差在哪浮点数和整数之间差的不是“能不能表达小数”这么简单。FP32 有 8 位指数、23 位尾数动态范围极大FP16 砍掉一半但依然保留指数结构所以数值范围仍然很宽只是精度下降INT8 只有 256 种取值既没有指数也没有小数表达能力与前两者完全不在一个量级上。这就是为什么很多人第一次听到“把模型量化成 INT8”时会觉得不靠谱一个权重范围可能是 [-10, 10] 的浮点张量硬要用 256 个整数刻度去表示信息量丢得厉害。但量化的核心思想是换一种数制而不是简单粗暴地截断。量化做的事情是用一个缩放因子 S 和一个零点 Z把连续的浮点范围映射到一套整数刻度上。用生活化的类比来说浮点像是一只带小数的高精度温度计INT8 像是只标整数的粗糙表盘。只要量程选得对绝大多数场景看整数就够用误差在可接受范围内。为什么量化能加速因为整数乘法在硬件里就是一条简单指令而浮点乘加要处理指数对齐和舍入。更关键的是芯片厂商为低精度专门设计了并行计算单元英伟达 Tensor Core 上 INT8 的峰值吞吐通常是 FP32 的好几倍最新的消费级显卡也开始强调 FP8INT8 更是几乎所有平台x86 的 AVX2/VNNI、ARM、NPU都具备的基础能力。这也是为什么部署优化第一刀往往砍向 INT8而不是直接上 INT4。1.2 量化的数学语言Scale 与 Zero-Point量化公式不复杂但一定要吃透因为后面所有方案都围绕这两个参数展开。设实数向量为 r量化后的整数为 q则有q clamp(round(r / S) Z) r ≈ S × (q - Z)其中 S 是缩放因子Z 是零点。对称量化时 Z 0非对称量化时 Z 是一个偏移量。为什么非对称要加 Z因为激活值常常是正值比如 ReLU 的输出集中在 [0, 6] 之间。如果硬用 [-128, 127] 去表达大部分整数刻度都被浪费了加上一个零点后可以把整数刻度的起始位置“挪”到浮点区间的起点分辨率立刻提高。量化误差主要来自三个方面第一是 round 取整误差第二是 clamp 截断误差超出范围的值会被切掉第三是 scale 选得不合适造成的系统性误差。一个好的 scale本质上是在“分辨率”和“覆盖范围”之间做权衡。覆盖范围越大每个刻度代表的真实数值就越大分辨率越低覆盖范围太小离群点又会被截断。校准Calibration干的事就是替每一层找这个平衡点。1.3 为什么先考虑 INT8而不是 INT4 或 FP16很多初学者会问FP16 也是低精度为什么不直接用原因是收益不对等。从算力角度看FP16 相比 FP32 在带宽和存储上省一半但计算吞吐提升远不如 INT8 明显。从精度角度看FP16 几乎无损INT8 则需要通过校准来保住精度。而 INT4 收益最大但误差显著上升硬件支持也更挑剔不少平台对 INT4 没有原生算子只能拆成多次查表或者数值补偿实际加速未必理想。我的经验是INT8 是绝大多数视觉模型和中小型模型的甜点位INT4 则更适合参数量动辄几十亿上百亿的大语言模型因为内存瓶颈远比计算瓶颈更致命。这个判断在后面讲 LLM 量化时还会展开。一句话总结先把 INT8 吃透再考虑要不要往更低比特走。2. INT8 矩阵乘计算过程与工程实现2.1 对称量化与非对称量化怎么选矩阵乘里最常用的组合是权重用对称量化激活用非对称量化。原因很实际权重分布通常大致以 0 为中心对称量化 Z0 的实现最简单不需要额外保存零点硬件支持也更完整。激活分布则往往偏在一边非对称量化通过 Z 能把整数刻度映射到有效的浮点区间精度更高。除了 per-tensor整张量共用一个 scale和 per-channel每个输出通道各一个 scale还有 per-group按 block 分组。一般经验是权重至少用 per-channel激活先用 per-tensor如果精度不够再考虑更细粒度。维度对称量化非对称量化零点 Z固定为 0额外记录实现复杂度低略高适合对象权重激活硬件友好度高取决于算子实现2.2 手写一个 INT8 矩阵乘不用框架我们用 NumPy 模拟一遍 INT8 矩阵乘你就能直观看到量化误差是怎么产生的。import numpy as np def symmetric_quantize(x, bits8): # 取最大绝对值作为量程对称量化 qmax 2**(bits - 1) - 1 # 127 scale np.max(np.abs(x)) / qmax q np.clip(np.round(x / scale), -qmax, qmax).astype(np.int8) return q, scale # 示例X shape (4, 8)W shape (8, 16) x np.random.randn(4, 8).astype(np.float32) w np.random.randn(8, 16).astype(np.float32) xq, sx symmetric_quantize(x) wq, sw symmetric_quantize(w) # 整数矩阵乘用 int32 累加 acc xq.astype(np.int32) wq.astype(np.int32) # 反量化输出 scale 是两个输入 scale 的乘积 y_quant acc * (sx * sw) y_fp32 x w err np.abs(y_quant - y_fp32).mean() print(quantized output mean abs error:, err)这个例子展示的是最朴素的 per-tensor 对称量化。实际工程中反量化通常不会显式做一遍而是把 scale 融合到下一层算子或者输出层里避免额外开销。2.3 工程实现里的几个关键点真正写推理引擎时比“量化公式”更重要的是这几个细节第一累加器必须用 INT32。两个 INT8 相乘最大是 127×12716129如果在 K 维度上累加比如 K128理论最大累加值超过 206 万INT16 一定溢出INT32 则绰绰有余。这就是为什么硬件上 INT8 矩阵乘的累加器通常固定为 INT32。第二权重的 per-channel 量化需要处理 scale 的广播。一个 (K, N) 的权重矩阵per-channel 量化会得到 N 个 scale对应输出 y 的每一列。这要求在反量化时把 scale 向量乘到输出矩阵的每一列上不能直接用标量。第三算子融合顺序。比如 ConvBN 部署时通常先做 BN 折叠fold因为 BN 的均值和方差可以折进卷积权重里否则量化后误差会叠加。激活函数ReLU、GELU、SiLU的位置也要注意是先量化再做激活还是先激活再量化不同推理引擎有不同约定搞反了精度会莫名其妙掉一截。第四也是容易忽略的一点量化不只是数值计算还牵扯内存布局。有些硬件要求 INT8 数据按特定格式排布比如 4 个 INT8 打包成一个 32 位寄存器如果布局没对齐加速效果会大打折扣。这也是为什么实际部署时优先用推理引擎提供的量化算子而不是自己写纯 C 矩阵乘。3. 校准CalibrationPTQ 里最容易被低估的一步3.1 为什么要单独做校准先明确一个概念Post-Training QuantizationPTQ也就是训练后量化不要求重新训练模型只需要喂一批数据统计激活的分布。权重是静态的打开模型直接能算出 min/max但激活的范围取决于输入数据同一个卷积层喂一张夜景图片和一张白墙图片输出分布可能差很远。所以必须用数据去“校准”把每一层激活的实际动态范围摸清楚。现在很多框架支持“直接量化”没猜错的话背后其实已经偷偷跑了一小段校准流程只是被封装了。最朴素的校准是直接取激活的 min 和 max 作为量程但这在真实场景里经常踩坑只要校准数据里出现一两个离群点整个 scale 就会被拉大导致绝大多数正常值被压到几个整数刻度上精度崩溃。校准的本质就是决定“到底要覆盖多大范围以及如何容忍边缘离群点”。3.2 MinMax、Percentile、MSE、KL 散度目前主流的校准策略主要有四类各有取舍。MinMax直接用激活的全局最小值和最大值定 scale。实现简单但对离群点零容忍。Percentile取激活分布的某个百分位比如 99.99%把最极端的 0.01% 数据剪掉。用算子统计直方图实现不算复杂通常比 MinMax 稳。MSE 最优遍历候选的 scale选一个让量化前后输出的均方误差最小的值。这个思路直观但计算成本高实际中常用直方图近似。KL 散度TensorRT 的 entropy calibration 用的就是它。核心思想是找一个截断阈值 T使得原始分布和量化后分布之间的 KL 散度最小。通俗理解就是“尽量保持形状相似”而不是只盯单个点误差。这里重点说下 KL 校准的直觉。假设我们先把某一层的激活收集成 2048 个 bin 的直方图然后尝试不同的阈值 T大于 T 的值全部截断到 T小于 -T 的截断到 -T再把截断后的分布合并映射到 128 个整数刻度上。每次映射后计算原始直方图和量化分布之间的 KL 散度选择散度最小的 T 作为最终阈值。这个过程中“保护分布形状”是最终目标因此它对长尾分布特别有效。3.3 校准数据集怎么选校准数据集的选择直接决定 PTQ 的效果但很多文档只会含糊地说“准备一些有代表性的数据”。结合我的实践经验几条原则供参考。第一覆盖真实场景而不是覆盖“好看”的样本。做图像分类就把暗光、过曝、不同角度、不同背景的样本都放进去做 NLP 模型就把长文本和短文本、不同风格的问题都放进去。千万不要只挑同一类数据比如全部是白色背景的产品图。第二数量不是越多越好但太少一定不行。经验区间是 100 到 1000 个样本太少了统计不稳定太多了校准过程慢而且冗余样本对分布估计的贡献递减。一般我先取 256 个左右验证集效果不满意再扩。第三校准不需要 label。因为校准只统计激活分布不反传梯度所以标注信息用不上。数据预处理要和训练阶段完全一致包括归一化参数、图像尺寸、padding 方式这一点最容易出问题。第四校准集千万不能用测试集也不要有重叠。否则量化后的指标会被高估上线后性能落差非常大。3.4 校准阶段的坑校准阶段我踩过的坑不少列几个高频的校准数据里的“脏样本”。比如图像里出现纯色块或者是损坏图片会直接把最大值拉爆。建议先对校准集做异常值检测剔除噪声样本。动态输入形状导致 scale 乱跳。如果模型支持动态 batch 或动态分辨率激活分布的统计会不稳定。做法是固定一组代表性 shape 做校准或者在多组 shape 下分别校准再取保守值。校准过程不可复现。有的框架在跑校准时会用多线程收集激活顺序不同导致直方图有细微差异最后量化结果也会漂。固定随机种子、固定数据顺序、固定线程数是上线前必做的三件事。校准后一定要用验证集回归。量化模型在验证集上的指标和校准集分布差异往往被低估如果验证集掉点超过预期第一件事不是调位数而是回去重新审视校准集。4. QAT量化感知训练精度不够时的正式解法4.1 PTQ 为什么会有精度天花板PTQ 的优势是快、不需要标签、不需要训练环境但它有一个天然天花板它只能被动适配已有模型权重不能修正量化带来的分布偏移。对于层数多、冗余大的大模型PTQ 通常损失很小但对于小模型、或者某些对数值敏感的任务PTQ 掉点可能非常明显。原因是小模型参数量少每一层的输出信息都更“金贵”量化噪声会逐层累积放大。还有一些结构的层对 scale 特别敏感比如长尾分布的 Embedding、带特殊 loss 回归头往往一量化就崩。这时如果还想用 INT8就得考虑 QAT。4.2 假量化与 STEQuantization-Aware TrainingQAT的思路是让模型在训练阶段就“体验”量化误差这样网络可以学出对量化更鲁棒的权重。它并不真的把权重存成 INT8而是引入一个“假量化”Fake Quant操作前向传播时模拟量化的 round 和 clamp反向传播时假装这个操作没有改变梯度让梯度能正常回传。这里的关键技巧叫 STEStraight-Through Estimator直通估计器。因为 round 函数在数学上不可导我们就在反向传播时直接用恒等映射代替它的导数。代码实现可以很简单import torch class FakeQuant(torch.autograd.Function): staticmethod def forward(ctx, x, scale): # 对称量化模拟 qmax 127 x_q torch.clamp(torch.round(x / scale), -qmax, qmax) return x_q * scale staticmethod def backward(ctx, grad_output): # STE直接透传梯度 return grad_output, None def fake_quantize(x, scale): return FakeQuant.apply(x, scale)训练时把关键层的前向计算前插入 fake_quantize权重和激活都会被模拟量化反向传播时梯度绕过取整操作。这个方法看着“粗暴”但实践中极其有效是如今 QAT 的绝对主流。4.3 QAT 训练策略与注意事项QAT 不是从零开始训练而是在预训练模型的基础上“微调”几个 epoch。我一般建议学习率要比正常训练小一个数量级通常从 1e-4 起步配合 warmup 和余弦退火。数据集可以复用原始训练集但不需要完整跑完两到三个 epoch 往往就够。批量大小可以适当减小因为 QAT 本身只是在微调。BatchNorm 是 QAT 里最容易翻车的地方。训练和推理时 BN 统计口径不一致会导致量化模型在验证集上表现诡异。建议训练早期固定 BN 的 running statistics或者干脆把 BN 和卷积先折叠再量化总之要保证部署时和训练时行为一致。另一个经验是“不要一刀切”。有些层量化后掉点极严重比如第一层卷积和最后的分类层可以保留 FP16只量化中间层。敏感性分析在很多框架里已经能自动做了结论往往很明确离输入输出越近的层对量化越敏感。4.4 用不用 QAT 的判断标准给一个粗粒度的判断方法以 ImageNet 分类或对应任务准确率/指标为例PTQ 后指标变化建议掉点小于 0.5%直接 PTQ 上线无需 QAT掉点 0.5% 到 2%先做敏感层分析对敏感层单独保护掉点 2% 以上直接上 QAT避免反复试错浪费人力这个阈值不是绝对的小模型会更苛刻大模型会更宽容。核心原则是QAT 的成本比 PTQ 贵一个数量级能用 PTQ 解决的问题不值得为“压榨最后一点精度”上 QAT。5. LLM 量化从 INT8 到 4bit5.1 大模型量化的难点在哪里语言模型量化比传统视觉模型难不是因为它“大”而是因为数值分布更极端。研究者在 OPT-175B、LLaMA 等模型上发现激活中存在少量数值极大的 outlier 通道这些输出通道的值可以达到几百甚至上千而其他通道基本停留在个位数。如果用 MinMax 校准scale 会被 outlier 拉大等于把所有正常通道全部压扁。另一个问题是权重分布不均匀。传统假设里量化最舒适的场景是数值均匀分布或者接近高斯分布但 LLM 的权重有大量值集中在 0 附近同时夹杂少量大值用均匀量化去贴这种分布误差必然集中在大值上。LLM 量化的第三个瓶颈是内存带宽。几十亿参数的模型即使全部塞进显存推理时也要一遍遍从 HBM 读取权重单位时间能读多少参数就决定了生成速度上限。所以 LLM 量化的目标不仅是加速矩阵乘更是压缩权重体积让模型能塞进更小的显存、减少带宽占用。这也是为什么 LLM 场景更愿意用 4bit而不是停在 INT8。5.2 GPTQ、AWQ 与 SmoothQuant 的思路应对 LLM 量化业界先后出现了几套代表性方案。GPTQ基于二阶信息的逐层量化是当前 GPU 推理里最常用的后训练量化方法。它的前身是 OBS/OBQ思想是逐列量化权重时根据 Hessian 矩阵的二阶信息动态调整尚未量化的权重从而补偿已经量化列带来的误差。GPTQ 把它改造成可以并行处理一整列的版本速度大幅提升。实际应用中GPTQ 在 4bit 精度下能把 LLaMA 系列模型压到很低损失。AWQActivation-aware Weight Quantization激活感知量化走的是另一条路。它观察到激活 outlier 通道对应的权重通道非常重要如果这些通道在量化时被精确保留整体误差就小。AWQ 根据激活统计找到“重要通道”通过缩放因子把权重整列放大让量化误差集中在不太重要的通道上。重要的是 AWQ 不需要重新训练速度比 GPTQ 更快在 vLLM 等框架里支持度也很好。SmoothQuant 是前两者的重要思想来源。它发现激活难量化就把激活的难度“转移”到权重上用激活的 per-channel scale 去缩放激活同时反过来缩放权重矩阵从而让激活分布不再有离群点变得平滑。这样激活可以用 INT8权重虽然变大但依然好量化。这个思想后来被 AWQ 扩充为“只挑重要通道保护”。5.3 部署选型GGUF、vLLM 与 q8_0聊到实际部署绕不开 GGUF 和 vLLM 这两套主流方案。GGUF 是 llama.cpp 生态的模型格式专为本地推理设计支持 CPU、Apple Silicon更多面向低资源环境。它里面有一堆量化档位比如 q8_0、q4_0、q4_K_M、q5_K_M。命名里的 K_M 表示混合量化一些敏感层用更高比特其他层用低比特是一种折中方案。很多人下载模型时会看到“qwen3.8-27bq8_0 量化版”这样的字样说明这个模型是用 8bit block 量化打包的适合本地低显存设备。vLLM 更侧重 GPU 高吞吐推理官方支持 AWQ、GPTQ 格式的模型。如果你的场景是并发请求多、GPU 资源充足优先考虑 AWQ 或 GPTQ 的 4bit 版本吞吐收益非常明显。我的习惯是先看服务端的 batch 规模再决定量化档位而不是无脑上最低比特。补充一点GGUF 的量化并不是所有算子都真正跑在 INT8/INT4 上部分算子加载时反量化回浮点再计算所以加速效果没有想象中那么理想但内存占用实实在在地降下来了这是 GGUF 的核心价值。5.4 KV Cache 量化也别忽视LLM 推理时除了权重KV Cache 也是内存大户。序列越长KV Cache 越大甚至能超过权重本身。现在主流框架都支持 KV Cache 量化比如把 key 和 value 压成 FP8 或 INT8某些场景还能用 INT4。这个优化对长上下文场景收益巨大实测内存占用能降 50% 以上。KV Cache 量化也需要校准但计算量比权重量化小得多。一般直接用 min/max 或者 percentile 就能获得不错效果因为 KV Cache 的分布相对稳定。5.5 怎么判断 LLM 量化后的效果LLM 量化效果不能只看一两个 prompt 的生成结果太片面。业界最常用的指标是困惑度PerplexityPPL在留出的文本集上计算模型预测下一个词的平均负对数似然PPL 越低越好。一般 INT8 量化后的 PPL 增长控制在 0.1 以内4bit 量化增长在 0.3 到 0.5 以内都能接受。除了 PPL建议跑一遍常见任务集比如 MMLU 或 GSM8K 的采样版看看推理能力有没有明显衰退。日常快速验证时我会固定 20 个不同风格的 prompt在 FP16 基线和量化模型之间逐个对比输出重点看是否有重复、胡言乱语、逻辑断裂。这个 A/B 测试虽然不严谨但能快速暴露“降智”问题。6. 实战工作流与排查速记6.1 一套可复用的量化上线流程完整做完一次量化部署我的标准流程是先跑一版 FP16 基线记录精度、延迟、显存/内存占用。用 256 个左右样本、KL 或 percentile 校准跑一版 INT8 PTQ。在验证集上对比基线和量化模型确认掉点幅度。如果掉点过多做敏感性分析找出高敏感层只对这些层保留 FP16 或改用更高精度。如果仍然不达标再考虑 QAT在预训练权重基础上有针对性地微调敏感层。用推理引擎的 profiling 工具确认所有关键算子确实跑在 INT8 kernel 上。固定模型版本、量化参数、校准数据形成可复用的配置模板方便下次部署。这套流程看着繁琐但每一步都有明确目的。跳过任何一步都会在后期花更长时间排查。6.2 常见问题排查表现象可能原因处理方式量化后精度大幅下滑校准集不具代表性或数据预处理不一致重建校准集对齐预处理流程精度小幅下滑激活存在离群点或敏感层未保护换 percentile/MSE 校准做敏感层保留 FP16完全没加速算子没落到 INT8 内核检查推理引擎日志确认算子融合和硬件支持内存没下降部分层仍以 FP32 运行用 profiling 工具检查每层算子类型结果不稳定、每次推理有波动随机种子、线程数、校准顺序不一致固定种子、线程数和校准顺序量化后出现 NaN数值溢出或 scale 为 0检查激活是否有全零列scale 设为最小阈值6.3 几条性价比很高的经验最后分享几条实操经验。第一校准数据的第一版尽量用百分位法而不是 MinMax。MinMax 在干净数据集上表现可能不错但一旦混入异常值就满盘皆输百分位法天然抗离群点。第二量化前先确认框架和硬件是否原生支持 INT8 算子。有些环境即便用了量化模型内部也只是模拟 INT8速度没有任何提升反而纯属浪费时间。第三QAT 的训练周期不要拉太长通常三五个 epoch 就足够再长反而可能过拟合校准分布导致真实场景掉点。第四也是我反复提醒团队的一条任何量化结果都要绑定“基准版本”。不要只记录 INT8 模型的指标要让 FP16 基线、校准数据版本、模型权重 hash、推理框架版本都一一对应。量化项目最怕的就是“复现不出来”版本管理做扎实很多玄学问题会自己消失。量化做了这几年我最大的体会是它不是一个非黑即白的开关而是精度、内存、延迟之间的三方谈判。接手一个部署任务时我第一件事永远是先把 FP16 基线钉死再考虑量化收益否则很容易被测试数据里的噪声干扰判断。最后再提一个实操建议遇到量化掉点别急着把摊子扩大去重训整个模型先用敏感性分析找出真正掉点的层只对它们做特殊处理往往花最少力气拿到最大收益。量化不是玄学它只是把精度和速度的权衡放到台面上让你能算清这笔账。
返回列表