1. 项目概述量化算法到底在解决什么问题量化这件事说穿了就两步先统计再映射。统计是搞清楚模型权重和激活值“长什么样”映射是找到一种高效方式把这些连续浮点数塞进低精度整数的格子里。大模型量化技术里所有花哨的方法——GPTQ、AWQ、SmoothQuant——本质上都是在优化这两个环节中的某一个。所以第二章先讲基础量化算法把统计与映射的每个细节抠清楚后面看那些高级方案就会顺畅得多。我当年第一次接触量化的时候被各种术语绕得晕头转向什么对称量化、非对称量化、零点、缩放因子、校准集每个词单独看都懂合在一起就不知道谁是谁。后来自己动手做了几次量化实验才真正明白这些东西就是一个编解码过程编码器把float32的数值转成int8解码器把int8按误差最小的方式还原回float32。中间决定精度损失多少的就是统计环节怎么找范围、映射环节怎么定缩放。这篇内容适合正在学习模型量化原理的开发者、准备落地端侧推理的工程师以及被精度下降问题困扰的调参选手。我会用一次实际的小模型量化过程贯穿全文把统计和映射这两个核心环节的每个决策点都拆开讲清楚包括为什么这么做、不这么做会怎样、实际跑起来会遇到什么坑。这次实验用的是某开源的小型Transformer模型参数量不大但足够把量化流程完整走一遍。2. 内容整体设计与思路拆解2.1 为什么量化必须是“统计”和“映射”两步走先说统计。统计的目标是回答一个问题这组浮点数它们的取值范围到底在哪里最小是多少最大是多少分布集中在哪个区域有没有特别离谱的离群值这些信息决定了后续映射的“边界”。边界选大了精度区间被浪费在很少出现的极端值上边界选小了超出范围的值全部被裁剪信息直接丢失。所以统计环节看起来只是找max和min实际上要做的远不止取个最大值那么简单。再说映射。映射的目标是把统计得到的浮点范围线性变换到整数范围。以int8为例可表示的整数值是-128到127总共256个格子。映射要做的事情就是找一个缩放因子scale让浮点范围里的最大值对应整数范围里的最大值最小值对应最小值中间的每个小数的四舍五入到这个整数格子里的某一位。这两步之所以必须分开是因为它们解决的是两个完全不同的风险。统计环节面对的是“信息覆盖不全”的风险映射环节面对的是“精度颗粒度不够”的风险。如果你的权重范围是-1.5到1.5但大部分值集中在-0.1到0.1之间那么按-1.5到1.5做映射大多数数值会被映射到0附近的几个整数上有效精度极低。反过来如果强行把范围压缩到-0.1到0.1那所有绝对值大于0.1的权重都会被裁剪成边界值这些权重虽然是少数但可能恰恰是决定模型行为的关键部分。统计和映射必须联合优化这也是为什么后面会出现百分位截断、KL散度校准这些进阶统计方法的原因。2.2 基础量化公式与实际直觉量化过程的数学表达其实非常简洁。设原始浮点值为r量化后的整数值为q那么对称量化q round(r / scale) zero_point非对称量化q round(r / scale) zero_point其中zero_point不为零反量化就是逆操作r_hat (q - zero_point) * scale。这里的scale就是缩放因子zero_point是零点偏移。大家刚接触时最容易困惑的就是zero_point为什么有了scale还不够因为浮点数范围不一定关于0对称。举个例子神经网络经过激活函数后输出值经常是0到6之间的正数根本没有负数。如果强行用对称量化让正数的最大范围去匹配整数的正负两半那有一半的整数格子永远是闲置的精度浪费了一半。这时引入zero_point把0映射到某个非零整数上就相当于把整个浮点区间平移了一下让256个整数格子能完全覆盖实际用到的浮点区间。理解了公式之后很多东西就通了。后面我们遇到的各种量化方法其实都是在解决同一个问题怎么选择scale和zero_point让量化后的误差最小。一种极端是让误差的最大值最小另一种是让误差的期望最小再一种是从信息论角度让分布差异最小。不同目标对应不同算法没有绝对的好坏取决于你的模型对误差的敏感程度。2.3 训练后量化与量化感知训练的选择逻辑基础量化算法通常是在训练后的模型上做这就是Post-Training QuantizationPTQ。还有一个方案是Quantization-Aware TrainingQAT在训练过程中就模拟量化误差让模型自己去适应低精度带来的扰动。这两个方案的选择逻辑很清晰如果你的模型已经训好了不想动训练流程只想快速部署那就走PTQ如果你有训练资源且模型对量化特别敏感精度掉得很厉害那就上QAT。PTQ是我们这章的主角因为它不需要改动训练流程是最快见效的方案。QAT里那些模拟量化的技巧很多也是基于PTQ的统计和映射原理衍生出来的。所以把基础量化算法吃透两个方向都能快速上手。我个人的建议是任何项目第一次做量化都先尝试PTQ用校准集统计激活分布选择合适算法看精度表现。只有PTQ实在救不回来的时候再考虑QAT。3. 统计环节的核心细节与实操要点3.1 校准数据集统计的“采样对象”统计的对象不只是权重还有激活值。权重是静态的训练完就固定了统计起来很直接读一遍权重就能拿到min和max。但激活值是动态的同一个模型输入不同数据中间层的激活值分布完全不同。所以我们需要一个校准数据集通过把一些有代表性的样本喂给模型记录各层激活值的分布然后基于这个分布去做量化决策。校准数据集的选择有讲究。不能太少否则统计出的分布不具有代表性也不能太多否则校准过程耗时太长。实际经验是两三百条典型样本就能覆盖绝大部分情况。关键是要保证校准数据的分布与真实部署数据接近。比如你的模型部署在图像识别任务上校准集就应该用与真实业务场景相近的图像不要用风格差异巨大的公开数据集否则量化后的模型在真实数据上精度会莫名劣化。校准过程中模型的所有参数保持固定只把中间激活值记录下来。这个记录过程通常放在推理模式下进行确保Dropout、BatchNorm等训练时行为不会干扰统计结果。BatchNorm在推理模式下用滑动均值在训练模式下用batch统计量如果搞错了模式统计出的分布会偏。3.2 min/max统计、百分位截断与优化目标最简单的统计方法就是直接取最小值到最大值作为映射范围。这个方法计算量最小但往往会因为个别离群值而浪费大量量化精度。大模型里权重分布经常出现肥尾少数权重绝对值特别大但大量权重集中在零附近。如果按真正的max去量化大部分值被压缩到很少的整数格子里精度惨不忍睹。这就是很多人量化后精度大幅下降的第一个原因。解决思路是引入百分位截断。做法是把激活值的分布从低到高排序取0.1%分位和99.9%分位作为截断边界把极端的离群点直接裁剪掉。裁剪意味着这部分信息被舍弃但换来的是大多数值能有更细的量化颗粒度。关键问题就是截断点选在哪里业界有几种不同策略直接按固定百分位截断比如1%到99%简单但武断。KL散度方法计算不同截断阈值下的量化分布与原始分布的KL散度选散度最小的阈值相当于从信息论角度找最优截断点。MSE方法直接最小化量化前后数值的均方误差可以直接用网格搜索或梯度优化找到最佳截断点。实际项目中我自己用得最多的是先画出权重和激活值的分布直方图目测分布形态再试用KL或百分位方法交叉验证。模型量化的体验很大程度取决于你对分布形态的判断而不是盲目套方法。3.3 权重与激活的统计差异权重和激活值的统计特征完全不同必须分开对待。权重的分布是训练收敛后的静态结果通常接近零中心的钟形分布对称性较好。激活值的分布则跟输入数据强相关经过ReLU或类似激活函数后往往是非负的偏态分布极少数场景下激活值的动态范围会非常大出现明显离群值。这种差异直接影响量化方案选择。权重通常适合用对称量化因为分布本来就近似关于零对称引入zero_point反而增加推理时多一步减法操作。激活值则应该用非对称量化动态范围偏在一侧不用zero_point会在零点附近浪费太多整数量化台阶。一个经验法则是卷积层或全连接层的权重用对称int8量化激活值用非对称量化这个组合在大量模型上都表现稳定。3.4 校准过程的实操经验与注意事项校准过程表面上简单实际操作中会遇到很多细节问题。我总结几个特别注意的地方第一校准必须覆盖模型的不同分支。如果模型有多个输入分支或注意力机制里有多个需要统计的张量确保校准集能激活每一个分支让统计结果覆盖到所有路径。只覆盖主路径会导致冷门分支的量化阈值严重偏离。第二动态形状输入对统计有影响。如果部署时输入序列长度变化范围大校准集最好包含短、中、长不同长度的样本避免统计结果被单一样长的分布带偏。第三校准数据过拟合现象是真实存在的。用校准集调整量化阈值时理论上阈值会为了适配校准集而优化但这些阈值对另一个数据集的泛化能力可能不理想。印象比较深的一次实验里用训练集前几百张图做校准量化后的模型在训练集上精度几乎无损到了测试集上精度掉了一大截后来换用单独的校准集才解决。4. 映射环节的核心细节与实操要点4.1 scale和zero_point的精确计算与舍入陷阱确定了映射范围后scale的计算是机械的。对称量化时scale 2 * max_abs / 255比如权重范围在-1.28到1.28之间max_abs取1.28那么scale 2.56 / 255 ≈ 0.01004。一个float值比如0.5量化后的整数就是round(0.5 / 0.01004) ≈ round(49.8) 50。反量化回来就是50 * 0.01004 0.502。量化误差大概是0.002。非对称量化时scale (r_max - r_min) / 255 zero_point round(-r_min / scale) q_min这里有个细节容易算错zero_point是浮点0映射到整数坐标的取值。如果量化范围是q_min到q_maxint8就是-128到127那么浮点0应该落在整数坐标的某个位置计算公式需要把q_min偏移进去。有些库实现时零点表示方式不同转换时要格外小心。更隐蔽的坑是浮点数舍入误差。r_min和r_max本身是浮点除255后scale可能是一个除不尽的循环小数。在量化参数保存和加载过程中如果做了额外的浮点运算可能引入毫厘级的偏移。某些场景下这个偏移会在大量张量累加时被放大最终影响模型输出。所以我建议scale和zero_point在量化时就用高精度存储推理时再转换成低精度。4.2 对称量化与非对称量化选型逻辑对称量化的最大优势是推理时的计算可以简化。卷积或矩阵乘法的核心运算中对称量化可以省去zero_point相关的修正项。具体来说两个对称量化后的整数做乘加结果可以直接作为浮点结果的缩放版本不需要额外的偏移修正。这在高性能推理引擎里能省不少指令周期。非对称量化的优势在于表达效率。当浮点数据的动态范围完全偏向一侧时比如0到6的正激活值对称量化把一半的整数范围浪费在负数一侧有效精度会打折扣。非对称量化通过zero_point把0映射到合适的整数位置让整个int8范围都服务于实际分布区间精度通常比对称量化高不少。选型的实务判断标准如果数据的最大绝对值与均值的比例适中比如3倍以内对称量化问题不大。如果这个比例很大激活值经ReLU或GELU后常见就有必要考虑非对称方案。不同框架对两种模式的支持程度不同某些硬件加速单元对非对称量化的支持不友好这时即使非对称精度更好也要权衡推理速度。4.3 取整策略四舍五入以外的选择常规做法是round-to-nearest四舍五入到最近的整数。这个方法的期望误差是零但个体误差可能达到0.5个scale单位。对于模型参数来说这种误差通常是可以接受的。但有一种情况例外当权重值恰好落在两个整数的正中间时round在不同语言中的行为可能不一致Python的round是银行家舍入遇五取偶C的round是远离零的舍入如果训练框架和推理框架混用可能产生莫名其妙的偏差。进阶思路是随机舍入。在量化感知训练里有时会用概率性舍入误差越小的方向被选择的概率越大。期望值同样是原始值但引入的随机性有助于训练过程把量化误差当作噪声来适应。对PTQ来说随机舍入意义不大因为它会引入不可复现的不确定性。实际工程中还有一个不太起眼但很重要的点overflow的检查。scale算出来后理论上所有浮点值都在映射范围内但如果统计环节给出的r_min或r_max恰好有误或者某些输入在推理时超出了校准时的观察范围量化后的整数值会超过int8边界。超界后乘加溢出在C里是未定义行为在Python里可能毫无异常但数值已经错了。所以推理引擎里必须在量化时对输入做clamp把越界值截到边界上这一点很多自制量化工具都会漏掉。4.4 逐层映射与per-channel量化映射还有一个粒度问题是每一层用一个全局的scale还是每个通道单独一个scale。per-tensor量化简单一层一个scale存储和计算开销最小但表达力不足。比如一个卷积层有64个输出通道每个通道的权重范围可能差异较大全局scale会被最大范围通道主导其他通道的量化误差会被放大。per-channel量化给每个通道单独的scale和zero_point明显更精确但会增加存储和计算的复杂度。实际部署中权重通常用per-channel激活值用per-tensor。原因有二权重是固定不变的为每个通道保存一个scale只增加极少的存储开销激活值是动态计算的如果在每个通道都做不同尺度的量化计算复杂度会大幅提高。所以常见的工程配置是per-channel权重量化 per-tensor激活量化也被称为混合粒度量化。这种配置在大量模型上与全精度对比精度损失通常在可接受范围。映射环节最后一步是验证误差。量化完成之后用同一份输入跑一遍量化模型和原始模型对比各层输出差异和最终输出差异。正常的量化误差表现为数值上微小的扰动最终模型输出可能在小数点后第三或第四位变化。如果误差在第一位有效数字就变化往往说明某个层的量化参数不合理需要回去检查统计范围和scale计算过程。5. 实操过程与核心环节实现5.1 搭建实验环境与模型准备我做实验用的是Python 3.10配合PyTorch和相关量化库模型选择了一个某开源小型Transformer模型参数量大约在几千万级别包含多个自注意力层和全连接层。选择这个模型的原因是它结构典型、训练速度快、量化效果容易观察适合用来跑完整流程。环境准备阶段需要安装的依赖包括必要的深度学习框架、量化库和数值计算库。值得注意的是量化库的版本兼容性某些旧版本库对最新框架的支持不好可能导致量化模型推理时出现莫名错误。建议在动手前先确认环境兼容性用一段简单的量化示例跑通全流程。模型准备阶段需要下载预训练权重。我这里使用了一个已经训练好的基础模型因为PTQ不需要训练过程直接用预训练权重做量化。如果你手头只有未训练或没完全收敛的模型请先完成充分训练再量化否则量化误差和训练不充分的误差会叠加在一起很难排查问题。5.2 校准数据集的构造与预处理我构造校准集的方式是从验证集中随机抽取200张不同类别的图像做一个简单的预处理流程包括缩放、归一化等。这200张图像被送到模型里做前向推理过程中记录每个量化层输入激活值的分布。构造校准集时有个容易忽视的点校准集的样本数据量最好是推理批次大小的整数倍避免最后一个批次样本不完整导致BatchNorm统计异常。虽然推理模式下BatchNorm不计算batch统计量但某些框架在校准过程中还是会受到batch构建逻辑的影响。预处理流程要跟实际部署时保持一致。如果你部署时用的是某种特定的图像预处理方式比如中心裁剪加标准化校准过程也必须用同样的方式。预处理不一致会直接导致输入分布偏移统计出来的激活分布对真实场景没有参考价值。5.3 统计环节的实施直方图观察与直方图统计加载模型后我用钩子函数把中间层的激活值记录下来然后绘制直方图。这一步非常直观能帮助你快速判断激活的大致分布形态。观察结果第一层注意力层的激活值在0到6之间分布偏右后面几层全连接层的激活值则分布在-2到2之间近似对称。权重方面各层基本是零中心的钟形分布尾部略有肥尾特征。根据观察结果确定统计方案激活值用非对称量化权重用对称量化。非对称量化需要额外的zero_point但能更好地覆盖偏态分布权重对称性较好不需要zero_point。然后执行实际统计逐层计算权重和激活的min/max对比各层数据分布范围。这里发现一个有意思的现象某些注意力层的输出激活最大值显著超过其他层如果不加处理这些离群值会拉大scale让其他所有值都挤在少数整数格里。我尝试用0.5%和99.5%百分位截断效果立竿见影量化后的模型精度比直接min/max统计高了不少。5.4 映射环节的实施逐层量化与反量化验证统计阶段确定每层的截断范围后进入映射阶段。我写了一个逐层量化的脚本对每一层的权重读取浮点值计算scale和zero_point然后做整数化处理对每一层的激活值同样处理。量化后的整数权重被保存下来同时记录每个张量的scale和zero_point参数。单层量化误差验证的方法是把某一层的原始浮点权重取出来与量化后反量化的权重对比计算均方误差和最大绝对误差。这个验证很有价值它能帮你快速找出哪些层是精度瓶颈。跑完整模型流程后对比量化模型和原始模型在同一批验证集上的输出差异。我这次实验的量化配置为权重用per-channel int8对称量化激活用per-tensor int8非对称量化结果量化后的模型在验证集上的准确率损失在0.3%以内完全可接受。5.5 不同映射方案的效果差异对比为了说明映射决策的影响我做了一组对照实验分别在相同模型上应用三种方案方案一权重和激活都用per-tensor对称量化。方案二权重用per-tensor对称激活用per-tensor非对称。方案三权重用per-channel对称激活用per-tensor非对称。结果显示方案三的准确率最高方案一次之方案二居中。方案的差异集中在极少数拥有较大动态范围的通道上per-tensor对称量化把这些通道的范围强行拉伸到全局最大值导致其他通道精度下降。这个实验结果验证了映射粒度和模式选择对量化精度的影响。我还进一步尝试了per-channel非对称量化但发现收益很小且推理时的计算复杂度明显增加。这个对比也说明了为什么工程上常用per-channel权重per-tensor激活的组合它在精度和计算效率之间找到了较好的平衡点。6. 常见问题与排查技巧实录6.1 量化后精度大幅下降的排查顺序精度大幅下降是最常见的问题排查顺序通常是确认量化范围统计是否合理。先用简单方式检查min/max如果有异常的大值看是否是离群点导致的。确认激活分布与校准集匹配。用真实部署数据抽样看与校准集分布是否相似如果差异明显重新构造校准集。检查量化参数是否保存和加载正确。scale和zero_point在写入和读取过程中如果发生精度损失会带来系统性偏移。逐层对比量化模型与原始模型输出定位问题出在哪一层。一个经常被忽视的原因是某些层包含的偏置项没有参与量化。偏置在全精度推理时数值相对较小量化后如果被截断或精度丢失可能对后续层产生较大影响。有些推理库用int32保存偏置但如果你用的是自研量化实现需要特别注意偏置的精度处理。6.2 zero_point相关问题的定位技巧zero_point相关的错误往往表现为输出整体偏大或偏小而不是随机波动。排查技巧是在量化模型的某一层输入一个已知的常数张量比较量化前和量化后的输出。如果输出差了一个固定常量几乎可以肯定是zero_point处理有误。还有一类情况是zero_point与输出的数据类型不匹配。某些硬件上zero_point被强制视为无符号整数当实际值为负时会发生回绕导致所有值偏移128个单位。这个坑在跨平台部署时特别容易踩解决方案是在保存量化参数时明确指定zero_point的数据类型或者在加载时做一次强制转换检查。6.3 数值溢出与饱和截断问题数值溢出在量化推理中比预想中更容易出现。特别是当输入数据分布与校准集分布差异较大时输入矩阵中的某些值会超出量化范围。推理引擎如果对这些值不做饱和截断乘加运算会得到远超预期的结果。实测中我遇到过一种典型的溢出场景模型在长序列输入时的attention激活值比校准集短序列分布更极端结果量化后的模型在长序列上的表现明显劣于短序列。解决办法是在校准集中加入长序列样本重新做统计。如果部署场景无法完全覆盖可以在输入量化之前加一个全局的clip操作。6.4 常见问题速查表问题现象可能原因排查方法精度大幅下降统计范围被离群值拉偏画直方图观察分布尝试百分位截断输出整体偏移zero_point错误或类型回绕用常量输入逐层测试检查参数加载长输入表现劣化校准集与部署输入分布不匹配校准集覆盖多种序列长度个别层误差特别大该层动态范围异常或权重通道差异大逐层对比误差切换per-channel量化模型输出为NaN溢出处未做饱和截断检查各层是否对量化结果做clamp校准耗时过长校准集规模太大或钩子函数效率低缩小校准集只统计关键层数值一致性波动随机舍入或推理引擎中非线性处理改用确定性舍入检查推理选项激活分布统计不准BatchNorm模式或预处理与部署不一致确保推理模式、统一预处理流程6.5 排查过程中积累的实操心得量化排查中最重要的是把问题分层拆解先看权重量化的独立误差再看激活量化的独立误差最后看两者叠加后的误差。这样能快速定位问题所在而不是对着整个模型发呆。另一个心得体会是保存实验配置的重要性。我每次做量化实验都会记录校准集大小、截断策略、量化粒度、scale的计算方式、取整策略。有时候模型表现得很差回看配置就发现某一步设置错了。量化流程的每个环节都可能成为精度损失的来源记录配置就是保留回溯的能力。7. 实操总结与个人经验做完这一系列实验我的感受是基础量化算法最难的地方不在数学公式而在工程细节的取舍。统计环节需要你理解数据的分布特性并选择合理的范围压缩策略映射环节需要你权衡精度表达与推理效率。很多人量化效果差不是公式写错而是在统计阶段偷了懒直接用min/max而没有分析分布形态或者在映射阶段选了不当的粒度和模式。最后分享一个实用的小技巧量化完成后不要只看最终模型的整体准确率要同时关注每一层的重建误差。如果某一层的重建误差特别大即使整体准确率看起来还行这个层也可能在边界输入时成为不稳定因素。花一点时间逐层分析误差来源往往能提前发现未来部署中的隐患。如果你在量化实践中遇到了有意思的问题或者对某个具体环节有不同的处理经验欢迎在评论区交流。量化的世界很大基础算法是起点但每一步探索都可能带来新的发现。
阅读完成 · 觉得有帮助?