:从 FP32 到 INT8,量化到底在“量“什么?)
前言为什么你的模型在云端跑得飞快到了端侧就卡壳了作为一名算法工程师你可能遇到过这样的场景在实验室的 GPU 服务器上你的 YOLOv5 模型以 60 FPS 的速度流畅运行检测精度 mAP 达到了 0.85。但当这个模型被部署到 IPC 摄像头或嵌入式设备上时帧率突然掉到了 5 FPS甚至根本跑不起来。这不是你的模型有问题而是因为量化这个看似简单但暗藏玄机的技术在端侧 AI 落地中扮演着关键角色。在这篇文章中我将深入剖析量化的本质带你理解从 FP32 到 INT8 的转换过程中到底发生了什么以及为什么这个过程对端侧 AI 至关重要。一、量化的本质从游标卡尺到塑料尺的转变1.1 FP32 vs INT8精度与效率的权衡想象一下你在进行精密测量**FP3232 位浮点数**就像一把高精度的游标卡尺可以测量到 0.001 毫米的精度但使用起来需要仔细校准计算复杂。**INT88 位整数**就像一把普通的塑料尺只能精确到 1 毫米但使用简单快捷。在计算世界中这种差异更加明显特性FP32INT8内存占用4 字节1 字节计算速度慢需要浮点运算单元快整数运算单元功耗高低精度高约 7 位有效数字低256 个离散值量化的核心目标就是在保持模型精度的前提下将模型从 FP32 转换为 INT8从而实现内存占用减少 75%从 4 字节降到 1 字节推理速度提升 4–8 倍整数运算比浮点运算快得多功耗大幅降低对电池供电设备至关重要1.2 量化的数学原理连续值到离散值的映射量化的本质是将连续的浮点数值映射到有限的整数空间。以对称量化为例核心公式如下整数 round(浮点数 / scale) 浮点数 ≈ 整数 × scale其中scale是量化尺子的刻度密度决定了数值的映射范围。举个具体例子假设某个卷积层的权重值范围是[-2.54, 2.54]我们使用 INT8 对称量化范围[-128, 127]计算 scalescale max(|权重|) / 127 2.54 / 127 0.02量化过程权重值 1.27 →round(1.27 / 0.02)round(63.5)64权重值 -0.51 →round(-0.51 / 0.02)round(-25.5)-26反量化过程整数 64 →64 × 0.021.28与原始值 1.27 相差 0.01整数 -26 →-26 × 0.02-0.52与原始值 -0.51 相差 0.01这个例子展示了量化的两个关键点舍入误差连续值到离散值的转换必然产生精度损失截断风险如果数值超出[-128, 127]范围会被强制截断二、量化对象我们到底在量什么在深度学习模型中需要量化的对象主要有两大类它们占据了量化误差来源的 90%。2.1 权重Weight静态的印好的书权重是神经网络中可学习的参数对于卷积层和全连接层来说权重在训练完成后就固定不变。特点固定不变训练完成后权重值不再变化可提前量化可以在模型部署前就完成量化分布相对稳定权重分布通常比较规律量化时机离线提前量化实际例子在 YOLOv5 的 Backbone 网络中第一个卷积层可能有3×3×3×64 1728个权重参数。这些参数在训练完成后就固定了我们可以一次性计算它们的scale然后量化为 INT8存储到模型文件中。# 权重量化伪代码defquantize_weight(weight_fp32):max_absnp.max(np.abs(weight_fp32))scalemax_abs/127# 对称量化weight_int8np.round(weight_fp32/scale).astype(np.int8)returnweight_int8,scale2.2 激活值Activation动态的读书时的想法激活值是神经网络中每一层的中间输出它们随着输入数据的变化而变化。特点动态变化每张输入图片产生的激活值都不同无法提前预知必须在实际推理时才能确定分布不稳定可能存在极端值outlier量化时机在线量化或校准时量化实际例子当你在 IPC 摄像头中检测摔倒行为时白天场景激活值分布可能集中在[0, 10]范围夜晚场景激活值分布可能集中在[0, 5]范围逆光场景可能出现极端值激活值达到 50 甚至更高这种动态性使得激活值的量化变得复杂需要特殊处理。2.3 其他受影响的算子除了权重和激活值以下算子也会受到量化的影响Add / Residual / Concat这些算子涉及多个输入的合并需要对齐输入输出的scale否则数值会溢出。例子在 ResNet 的残差连接中# 主分支输出main_branchconv1(x)# scale_main 0.05# 残差分支输出residual_branchconv2(x)# scale_residual 0.08# 相加前需要对齐 scalealigned_mainmain_branch*(scale_main/scale_residual)outputaligned_mainresidual_branchBias偏置通常不单独量化而是提升到 INT32 参与累加防止溢出。原因Bias 的值通常比权重小直接量化到 INT8 会损失太多精度。Softmax / Sigmoid这些激活函数有时保留 FP16 以避免精度崩塌特别是在分类任务中。实际案例在人脸识别模型中Softmax 层的输出直接影响识别准确率很多工程师会选择保留 FP16 精度。BatchNormBatchNorm 在训练后会被融合进 Conv不参与独立量化。融合公式W_fused W * gamma / sqrt(var eps) b_fused (beta - mean) * gamma / sqrt(var eps) bias三、Scale量化尺子的诞生过程scale是量化过程中最重要的参数它决定了数值的映射范围和精度。3.1 Scale 的本质scale的作用是将浮点数值映射到整数范围。对于对称量化scale的计算公式为scale max(|x|) / 127这个公式的含义是找到数据的最大绝对值将其映射到 INT8 的最大值 127。3.2 Observer观察员谁来统计 Scale在 PyTorch 等框架中Observer 是专门用于统计数据分布的模块。它的唯一职责是盯着数据流记录最大绝对值。Observer 的工作流程在模型前向传播过程中插入 ObserverObserver 记录每个张量的最大值和最小值根据统计结果计算scale3.3 权重 Scale一次统计终身使用权重的scale计算非常简单max_absmax(abs(weight))scale_weightmax_abs/127特点权重固定不变量化后可永久存储为 INT8类似量一次全班最高身高以后校服都按这个做实际例子假设 YOLOv5 模型的第一个卷积层权重范围是[-1.27, 1.27]那么scale_weight 1.27 / 127 0.01这个scale一旦确定就可以永久使用不需要重新计算。3.4 激活 Scale必须动态统计关键难点激活值的scale统计是量化中最复杂的部分因为激活值随输入图像变化无法提前预知。方式 A校准集统计PTQ / 工具常用这是最常见的激活值scale统计方法准备校准集收集 100–1000 张代表性图片运行模型用这些图片跑一遍模型统计最大值记录每层激活的最大绝对值计算 scale取这批数据的 max 作为最终scale配置示例{calibration_dataset_dir:/path/to/calibration_images,num_calibration_images:500}实际案例在摔倒检测项目中我们收集了 500 张包含不同场景白天、夜晚、室内、室外的图片作为校准集。通过统计这些图片的激活值分布我们得到了稳定的scale。方式 B滑动平均QAT 训练时用在 QAT量化感知训练过程中使用滑动平均来统计激活值的scalerunning_max 0.9 × old_max 0.1 × current_max优势避免单张异常图片过亮/过暗影响scale让scale更稳定这是 QAT 效果好的原因之一实际例子假设在训练过程中第 1 张图片的激活最大值是 10第 2 张图片的激活最大值是 50异常值第 3 张图片的激活最大值是 12使用滑动平均running_max_1 0.9 × 0 0.1 × 10 1 running_max_2 0.9 × 1 0.1 × 50 5.9 running_max_3 0.9 × 5.9 0.1 × 12 6.51可以看到异常值 50 被平滑了最终的running_max更接近正常值。四、量化误差从哪来理解量化误差的来源对于优化量化精度至关重要。4.1 舍入误差舍入误差是由于连续值到离散值的转换产生的。例子真实值1.27量化后round(1.27 / 0.01)127反量化127 × 0.011.27完美但如果真实值是 1.275量化后round(1.275 / 0.01)128超出 INT8 范围反量化127 × 0.011.27丢失了 0.0054.2 截断误差更严重截断误差是由于数值超出量化范围而被强制截断产生的。例子假设某个激活值是 5.0但scale是 0.02理论量化值round(5.0 / 0.02)250但 INT8 范围是[-128, 127]所以被截断为127反量化127 × 0.022.54误差高达 2.46截断误差的后果权重分布太宽 → 截断严重激活值有极端 outlier → 截断严重小目标特征响应弱 → 量化后容易被当成噪声抹除4.3 小目标/弱特征最吃亏在目标检测任务中小目标的特征响应通常很弱。量化后这些弱信号很容易被量化为 0导致检测失败。实际案例在摔倒检测中人体目标特征响应强度约 10–20小目标如远处的行人特征响应强度约 2–3如果scale设置不当小目标的 2–3 可能被量化为 0量化前后对比原始激活值[15, 12, 3, 2, 1] scale 0.05 量化后[300, 240, 60, 40, 20] → 截断为 [127, 127, 60, 40, 20] 反量化[6.35, 6.35, 3.0, 2.0, 1.0]可以看到大目标的特征从 15 降到 6.35小目标的特征从 3 降到 3.0保持不变。但如果scale更大小目标的特征可能直接变成 0。4.4 核心洞察奇异值撑大 Scale这是量化中最关键的洞察奇异值会撑大scale导致绝大多数正常值被挤在一起。例子假设激活值分布如下正常值95% 的数据在 [0, 10] 范围内 奇异值5% 的数据在 [50, 100] 范围内如果使用 max 统计scalescale 100 / 127 0.787量化后正常值 10 →round(10 / 0.787)13正常值 1 →round(1 / 0.787)1奇异值 100 →round(100 / 0.787)127问题正常值[1, 10]被压缩到[1, 13]量化分辨率急剧下降结论scale不合理只是症状奇异值才是病因。五、PTQ vs QAT为什么 PTQ 不够5.1 PTQ训练后量化简单但不够PTQ 的流程很简单正常训练 FP32 模型训练完直接用校准集统计scale并量化部署量化模型类比一个只会用游标卡尺的工程师突然让他用塑料尺干活必然出错。PTQ 的问题模型从未学过如何适应量化权重和激活分布可能不适合量化对奇异值敏感容易产生截断误差实际案例在摔倒检测项目中我们尝试使用 PTQFP32 模型 mAP0.85PTQ 后 mAP0.72下降 13%精度下降的主要原因是小目标检测失败因为小目标的弱特征被量化噪声淹没。5.2 QAT量化感知训练让模型适应量化QAT 的核心思想是在训练的前向传播中插入 FakeQuant伪量化x → round(x / scale) × scale → 继续计算QAT 的优势模型在训练时就知道自己将来会被量化反向传播时通过 STE直通估计器更新权重主动抑制奇异值让数值分布天生适合量化类比提前让工程师练习用塑料尺他学会了如何在这个限制下保持精度。实际案例同样的摔倒检测项目使用 QATFP32 模型 mAP0.85QAT 后 mAP0.83仅下降 2%精度大幅提升的原因是权重分布被压缩集中在[-120, 120]内激活值的奇异值被抑制小目标的弱特征得到保留和增强六、QAT 如何降低量化误差6.1 压缩权重分布QAT 训练后权重分布会发生显著变化。训练阶段对比阶段权重分布特点FP32 预训练宽、尖峰、长尾易出奇异值QAT 后集中在[-120, 120]内分布紧凑实际例子YOLOv5 第一个卷积层的权重分布FP32 预训练 范围[-2.5, 2.5] 99% 的权重在 [-1.5, 1.5] 奇异值2.3, -2.1占总数 0.1% QAT 后 范围[-1.8, 1.8] 99% 的权重在 [-1.2, 1.2] 奇异值基本消失效果工具量化时无需剧烈截断精度损失更小。6.2 规整激活分布QAT 对激活值分布的影响更加显著。主要变化抑制极端 outlier通过梯度更新主动减少极端激活值放大弱信号对小目标检测至关重要对称化分布让激活分布更接近对称分布实际案例摔倒检测模型中某个检测头的激活分布FP32 预训练 范围[0, 50] 小目标激活2–5 大目标激活10–30 奇异值45–500.5% QAT 后 范围[0, 25] 小目标激活3–8增强 大目标激活12–20 奇异值基本消失关键效果小目标的激活值从 2–5 提升到 3–8量化后不易被淹没。6.3 对齐量化配置QAT 的配置必须与硬件支持的量化方案严格对齐。AT5050 芯片要求{feature:[symm,8],weight:[symm,per_tensor,8]}对应的 QAT 配置activation_quantquantizer.FakeQuantize.with_args(observerquantizer.MovingAverageMinMaxObserver,dtypetorch.qint8,qschemetorch.per_tensor_symmetric,# ✅ 对称对齐硬件quant_min-128,quant_max127,)weight_quantquantizer.FakeQuantize.with_args(observerquantizer.MovingAverageMinMaxObserver,dtypetorch.qint8,qschemetorch.per_tensor_symmetric,# ✅ per-tensor对齐硬件quant_min-128,quant_max127,)如果配置不对齐训练模型学习的是假分布部署硬件执行的是真量化结果精度崩塌七、scales.json 的作用与局限7.1 它是什么scales.json是 QAT 训练结束后Observer 统计到的每层scale记录{model.0.conv.weight:{scale:0.0123,zero_point:0},model.2.act:{scale:0.0456,zero_point:0}}7.2 它的作用理想情况工具直接加载scales.json完全对齐 QAT 的量化参数精度最佳。辅助作用作为工具校准的初始值或约束。非必需品多数自研工具会重新统计scale。7.3 为什么你的工具不加载也有效很多工程师困惑为什么我的工具不加载scales.json量化效果也很好原因工具通过校准集重新统计激活scaleQAT 已经把数值分布塑形好了即使scale略有差异误差也很小关键认知QAT 的价值在于改变数值分布而非仅仅传递scale。实际案例在摔倒检测项目中使用scales.jsonmAP 0.83不使用scales.jsonmAP 0.825差异仅 0.005几乎可以忽略这说明 QAT 塑形后的数值分布本身就适合量化scale的精确值不是最关键的。八、量化转换过程从 FP32 到 INT8 模型8.1 输入准备量化转换需要三个输入FP32 权重训练好的模型权重best.pt校准数据几十上千张代表性图片量化配置对称、per-tensor、int88.2 核心步骤步骤 1统计 Scale权重scalescale_wmax(|W_fp32|)/127激活scale# 跑校准集统计每层激活的最大值forimageincalibration_dataset:activationmodel(image)max_absmax(max_abs,max(abs(activation)))scale_actmax_abs/127步骤 2量化权重W_int8round(W_fp32/scale_w)步骤 3生成部署模型包含 INT8 权重包含 INT32 Bias包含 Scale 表或查表此时模型已从数学公式变为整数查表运算。8.3 数据格式变化FP32 Weights (4 bytes per weight) ↓ 统计 scale ↓ 量化 INT8 Weights (1 byte per weight) Scale Table → 部署模型INT8 Scale Table内存节省示例YOLOv5s 模型FP32 权重14 MBINT8 权重3.5 MB节省75%九、部署时推理过程芯片上的数据流9.1 输入与预处理输入摄像头uint8 [0, 255]图片预处理CPU/ISPResize 到模型输入尺寸如640×640Normalize归一化到[0, 1]BGR→RGB如果需要HWC→CHW维度转换格式变化uint8→FP32部分高级芯片支持 INT8 输入可省略此步9.2 激活量化首次量化用校准得到的scale_act_input将 FP32 输入特征图量化为 INT8x_int8round(x_fp32/scale_act_input)从此以后数据一路都是 INT8。9.3 INT8 卷积核心计算芯片最擅长的部分本质是INT8 × INT8 → INT32的乘加MAC运算y_int32x_int8 ⊗ W_int8其中⊗代表卷积运算。为什么快整数运算比浮点运算简单硬件可以并行执行多个 MAC 操作功耗低9.4 Scale 矫正关键查表实现由于y_int32的实际物理意义是(x / scale_x) × (W / scale_w)需要将其还原到正确的数值量级y_real ≈ y_int32 ×(scale_x × scale_w)工具配置{weight_op_process_scale:table}含义scale_x * scale_w这个乘法因子被预先算好存放在查找表中芯片直接查表获取结果无需实时计算。9.5 Bias 加法与激活函数Biasy_int32y_int32bias_int32使用 INT32 防止溢出。激活函数如 ReLUmax(0, y)仍在 INT32 域完成。9.6 重新量化回到 INT8为了下一层 Conv 的输入仍是 INT8需要将 INT32 结果重新压缩y_int8round(y_int32/scale_act_next)实现 INT8-in, INT8-out 的流水级联。9.7 循环与输出重复步骤 9.3–9.6直到 Detect Head。最后一层通常会转回 FP32以便后续 NMS 和后处理CPU/ARM 完成。9.8 推理全过程数据流总览uint8 图片 [0, 255] ↓ FP32预处理 ↓ INT8激活量化 ↓ INT8 Conv ( × INT8 权重) ↓ INT32累加 ↓ Scale 矫正查表 ↓ INT8重新量化 ↓ ...重复 ↓ FP32输出层 ↓ CPU 后处理NMS十、实战经验量化精度优化的关键技巧10.1 摔倒检测必做 QAT小目标对量化极度敏感PTQ 往往无法满足精度要求。实际数据FP32mAP 0.85PTQmAP 0.72下降 13%QATmAP 0.83下降 2%10.2 学习率要小QAT 阶段的学习率通常为预训练的1/10 ~ 1/20。原因量化引入了非线性大学习率会导致训练不稳定需要精细调整权重分布推荐配置# 预训练阶段lr0.01# QAT 阶段lr0.001# 降为 1/1010.3 关闭 EMAQAT 期间禁用 ModelEMA指数移动平均。原因EMA 会平滑权重分布与 QAT 的目标冲突QAT 需要直接优化当前权重10.4 校准集质量决定上限校准集必须覆盖典型场景不同光照条件白天、夜晚、逆光不同尺度近景、远景不同遮挡情况无遮挡、部分遮挡实际案例在摔倒检测项目中使用 100 张随机图片mAP 0.78使用 500 张精心挑选的图片mAP 0.8310.5 精度验收标准QAT PTQ 的 mAP 下降应控制在 1% 以内。如果下降超过 1%检查 QAT 配置是否与硬件对齐检查校准集质量检查是否有奇异值未被抑制10.6 关注奇异值QAT 的核心收益之一就是抑制权重和激活中的极端值。检查方法# 统计权重分布weight_maxnp.max(np.abs(weight))weight_99thnp.percentile(np.abs(weight),99)ratioweight_max/weight_99thifratio2.0:print(警告存在奇异值)10.7 善用 error_analysis通过layer_error_args定位量化引起的精度热点。示例error_analysisanalyze_quantization_error(model_fp32,model_int8,calibration_dataset,layer_error_args[model.0.conv,model.2.conv])十一、常见误区澄清误区正确理解QAT 必须导出 INT8 模型❌ 只需导出 float 权重scales.json 必须被工具加载❌ 它是辅助非必需激活值可以提前量化❌ 必须在线/校准统计QAT 只是为了传 scale❌ 核心是改变数值分布PTQ 加校准就够了❌ 小目标检测 QAT 必选训练时没 fuse 就没融合❌ 融合发生在部署工具QAT 配置随便选❌ 必须严格对齐硬件物理约束十二、总结量化不是简单的数据类型转换而是一个涉及数学原理、硬件约束、工程实践的复杂过程。核心要点量化本质连续值到离散值的映射必然产生精度损失量化对象权重和激活值是两大核心激活值量化更复杂Scale 生成权重scale固定激活scale需动态统计误差来源舍入误差和截断误差奇异值是主要病因PTQ vs QATQAT 让模型适应量化效果远好于 PTQ部署流程从 FP32 到 INT8 的转换涉及多个步骤工程实践校准集质量、学习率调整、奇异值控制是关键下一步在下一篇文章中我将深入探讨为什么 QAT 有时也救不了精度揭示层融合机制和硬件物理约束对量化精度的深层影响。