模型量化从FP32到INT8:深度学习部署的精度与性能平衡
发布时间:2026/9/19 18:40:22来源:尧图网络
1. 先搞清楚量化到底在干嘛1.1 一次让人心塞的部署现场前两天一个在后台做服务的哥们找我诉苦他在本地把一个开源大模型跑起来了代码没有问题、推理结果也正常但一到部署环节就崩了原因是服务器显存根本装不下。我问他用的什么精度加载他说“没设置过默认加载”。这个“默认加载”大多数框架直接就按FP32给你塞进内存那当然是个吞显存巨兽。你去看现在常见的几B到几十B参数规模模型如果按FP32存权重一个7B模型就要占28GB哪怕转成FP16也要14GB可很多人的显卡只有8GB或12GB。这时候“量化”这个词就成了部署场景里绕不开的关键词。它能把模型体积直接砍半甚至砍到原来的四分之一还能配合硬件加速把推理时延降下来。今天这篇就把INT4、INT8、FP8、FP16、FP32这几种常见的量化精度一次讲透顺便给一套能从PyTorch模型一路跑到ONNX INT8量化的完整实操步骤新手照着做也能跑通。先声明一个容易混淆的点我这里说的量化是深度学习模型里把高精度数值压缩成低精度格式的技术跟某些投资平台上的“量化交易”完全不是一个东西。别把思路带偏了。1.2 量化到底动了什么量化本质上做了一件事用更少的二进制位去表示原来的数值。打个比方你的体重是65.423千克平时跟朋友说你65公斤就行身高168.7厘米填表时写169厘米也没问题。这种对数字做“取舍”的方式跟模型量化很像。FP32里的每个数占32个bit也就是4个字节FP16占16个bit2个字节INT8只占8个bit1个字节。同样的模型把权重从FP32换成INT8存储直接缩到四分之一运算时数据的搬运量也大幅降低。很多人会问把精度砍这么狠模型不就傻了吗这里的关键在于神经网络在训练时会引入大量冗余权重的小幅扰动对最终输出的影响往往非常有限。量化损失的那点信息就像照片从原图压缩成高清图肉眼很难察觉区别但文件体积小了一大截。只要量化方法选对精度损失通常在可接受范围内换来的是更快的速度和更低的内存占用。1.3 量化公式三个参数帮你理解所有套路如果你看过量化相关的代码一定见过scale、zero_point、bit_width这三个词。它们之间的关系不复杂核心就一个线性映射公式[ q round(\frac{x}{scale}) zero_point ]反推回去[ x \approx (q - zero_point) \times scale ]这里x是原始高精度数值q是量化后的整数scale是一个缩放因子zero_point是零点偏移。举个例子如果某层权重范围在[-1, 1]之间你把它映射到INT8的[-128, 127]那就需要计算合适的scale让浮点数均匀铺到整数区间里。zero_point负责处理权重分布不是以0为中心的情况使得量化的整数能忠实地表示原数值。位宽决定信息密度scale和zero_point决定映射方式。后面讲INT8、INT4时你会发现各种量化方案的区别无非是这两个参数算得经不精细、映射策略选得合不合理。理解了这一点后续看再花哨的量化工具都不慌。2. 五种精度逐个拆解FP32、FP16、FP8、INT8、INT42.1 浮点数和整数是两种世界观在计算机里数字表示方式主要分两类浮点数和整数。浮点数用科学计数法的思路把数字拆成符号位、指数位、尾数位。比如FP32就是1位符号位、8位指数位、23位尾数位总共32位。它最大的优势是动态范围大能表示极大极小的数适合训练过程中梯度变化剧烈的场景但代价是占用空间大、计算开销高。整数则简单直接INT8就是8位有符号整数取值范围从-128到127INT4是4位取值范围从-8到7。整数没有指数和尾数的概念表示范围有限但因为格式简单硬件处理起来非常高效尤其在推理阶段优势明显。理解这两类表示法的区别后你就明白为什么量化方案总在浮点和整数之间反复横跳浮点精度高、范围广整数省内存、速度快。2.2 FP32什么都好就是太肥FP32是深度学习早期的标准精度也是很多框架里的默认精度。训练刚起步那几年模型普遍不大用FP32完全没问题。它的精度高、动态范围广在数值稳定性上表现突出。缺点也很突出占空间。现在模型参数动辄几亿几十亿直接用FP32存权重内存和显存根本扛不住。在本地推理场景里FP32更多是作为精度对比的基准而不是部署的主力。如果只是简单加载模型验证效果用FP32跑没问题一旦要考虑内存和时延量化势在必行。2.3 FP16大模型时代的默认选项FP16把32位压缩到16位符号位1位、指数位5位、尾数位10位。它的动态范围比FP32小很多最大能表示到65504最小正规数大约6e-5。对于多数深度学习推理场景这样的精度已经足够。实践中大多数开源模型在HuggingFace上提供的就是FP16权重加载时也建议优先用半精度。7B模型从FP32的28GB降到14GB很多12GB、16GB显存的显卡就勉强能跑了。FP16还有一个特点是计算速度快尤其在支持快速半精度运算的GPU上推理吞吐量比FP32高不少。这里顺带提一个不在标题里的格式BF16。它的指数位和FP32一样但尾数位更少动态范围极大在大模型训练里很常用。它不是今天的主角但如果你做训练相关的工作一定会经常碰到。2.4 FP8新一代硬件上的当红选手FP8是近年来为AI计算专门推出的低精度浮点格式8位浮点主要有两种变体E4M3也就是4位指数、3位尾数E5M25位指数、2位尾数。前者精度更高适合前向计算后者动态范围更大适合梯度更新时的计算。FP8的定位很巧妙它保留了浮点格式的部分动态范围又比FP16省了一半空间和带宽。现在不少新发布的GPU比如RTX 50系列和最新的数据中心卡都对FP8做了专门的Tensor Core加速。你去看这些显卡的规格表FP8的Tensor TFLOPS通常比FP16高出不少这意味着在支持的硬件上FP8推理或训练能带来明显的速度提升。不过FP8目前的一个问题是生态还在成熟中不是所有框架和算子都做了适配实际项目里使用时要多做验证。2.5 INT8与INT4压缩冠军INT8是当前推理部署中应用最广的量化精度。8位整数能表示的数值范围是-128到127配合scale和zero_point做映射实际效果在很多任务上逼近FP16。因为几乎所有推理引擎比如ONNX Runtime、TensorRT、OpenVINO都对INT8做了深度优化所以它在CPU和GPU上都能获得很好的加速效果。INT4更激进4位整数只能表示-8到7压缩比更高7B模型只需要约3.5GB内存。代价是精度损失更明显通常需要配合更高级的量化策略比如混合精度、分组量化group quantization等策略才能保证输出质量。INT4目前多用于端侧、移动设备、显存极受限的场景。如果你刚入门建议先从INT8上手跑通后再考虑INT4的进阶方案。2.6 精度对比总表为了方便对照我把几种精度的核心差异整理成表7B模型的内存占用也按理论值估算了一下精度位宽典型取值范围7B模型理论占用主要用途精度损失FP3232bit约±3.4e3828GB训练、精度基线无FP1616bit约±6550414GB训练与推理通用很小FP88bitE4M3约448E5M2约573447GB新一代GPU训练/推理较小INT88bit-128~1277GB推理部署主力中等INT44bit-8~7约3.5GB端侧、极低内存推理较大需特殊方案从这张表能看出来数据量越大的场景量化的收益越直观但精度越低对量化方案的要求也越高。没有绝对“最好”的精度只有最适合你硬件条件和业务需求的选择。3. 实操把ResNet18量化到INT8跑通全流程3.1 环境准备与模型准备纸上谈兵再多不如直接跑一次。这一节我用ResNet18作为示例带你把“PyTorch模型 → 导出ONNX → 量化成INT8 → 推理验证”整条路走一遍。首先准备环境安装必要的依赖pip install torch torchvision onnx onnxruntime如果你的环境支持GPU建议再安装对应的onnxruntime-gpu版本但注意onnxruntime和onnxruntime-gpu不要同时安装在同一个环境里否则容易冲突。装好之后验证一下导入没问题import torch import onnx import onnxruntime as ort print(torch.__version__) print(onnx.__version__) print(ort.__version__)我用的是PyTorch 2.x和onnxruntime 1.1x版本后面的代码基于这些版本编写。版本差异可能会引起个别API参数变化如果你碰到报错优先检查版本。3.2 导出基础ONNX模型ONNX是一个开放的模型交换格式把PyTorch模型转成ONNX后就能脱离PyTorch环境用ONNX Runtime等引擎推理也为后续量化做准备。先加载ResNet18并导出import torch import torchvision.models as models # 加载预训练模型并切换到推理模式 model models.resnet18(weightsmodels.ResNet18_Weights.IMAGENET1K_V1) model.eval() # 构造一个虚拟输入用来跟踪模型结构 dummy_input torch.randn(1, 3, 224, 224) # 导出ONNX torch.onnx.export( model, dummy_input, resnet18.onnx, input_names[input], output_names[output], opset_version15, dynamic_axes{ input: {0: batch_size}, output: {0: batch_size} } ) print(导出完成)这里注意三点第一模型一定要先调用eval()切到推理模式否则导出时会把Dropout、BatchNorm的训练逻辑带进去第二opset_version要设成ONNX Runtime支持的版本过老过新都可能出问题第三如果你希望模型支持动态batch输入dynamic_axes就要按上面的方式配置。导出后可以用onnx.checker.check_model做一次基础校验onnx_model onnx.load(resnet18.onnx) onnx.checker.check_model(onnx_model) print(模型结构检查通过)3.3 动态量化两行代码的快乐拿到基础ONNX模型后最直接的量化方案是动态量化。所谓动态量化是把权重从FP32转成INT8但激活值还是在推理时临时计算。它的好处是代码简单、不需要额外校准数据坏处是激活量化带来的额外计算开销会让速度提升打折扣。用onnxruntime自带的工具就能完成from onnxruntime.quantization import quantize_dynamic, QuantType quantize_dynamic( resnet18.onnx, resnet18_int8_dynamic.onnx, weight_typeQuantType.QInt8 ) print(动态量化完成)就这一行resnet18_int8_dynamic.onnx就出来了。我实际对比过动态量化后模型文件从44MB左右降到11MB效果立竿见影。如果你只是想把模型变小一点、精度要求也不高动态量化是最快的手段。3.4 静态量化更专业的压缩方式静态量化比动态量化更彻底不仅权重是INT8激活值也提前统计好范围并转成INT8。它需要一个校准数据集目的是统计每一层激活值的分布范围从而确定更合理的scale和zero_point。校准数据不需要太多几十到几百张有代表性的输入就够。先定义一个数据读取器import numpy as np from onnxruntime.quantization import CalibrationDataReader class ResNetCalibReader(CalibrationDataReader): def __init__(self, samples): self.data [{input: s} for s in samples] self.idx 0 def get_next(self): if self.idx len(self.data): d self.data[self.idx] self.idx 1 return d return None然后用一批真实图片做校准。为了演示我用随机数据代替实际项目中请你务必用真实场景的输入最好是和推理时分布一致的数据from onnxruntime.quantization import quantize_static, QuantType # 生成100个随机输入作为校准集实际使用请换成真实图片 calib_samples [np.random.rand(1, 3, 224, 224).astype(np.float32) for _ in range(100)] quantize_static( resnet18.onnx, resnet18_int8_static.onnx, ResNetCalibReader(calib_samples), weight_typeQuantType.QInt8 ) print(静态量化完成)静态量化的精度通常比动态量化更好因为scale和zero_point是通过校准数据优化出来的而不是临时估算。代价是你要额外准备一套校准数据并且校准数据要能代表真实场景。如果校准数据集太偏最终模型在你自己的数据上表现可能反而不如动态量化。3.5 CPU上的推理验证与加速效果量化完成后用ONNX Runtime跑一下推理看看效果和性能。先用CPUimport time import numpy as np import onnxruntime as ort def run_inference_ort(model_path, input_data): sess ort.InferenceSession(model_path, providers[CPUExecutionProvider]) start time.perf_counter() outputs sess.run(None, {input: input_data}) end time.perf_counter() return outputs, end - start test_input np.random.rand(1, 3, 224, 224).astype(np.float32) for path in [resnet18.onnx, resnet18_int8_dynamic.onnx, resnet18_int8_static.onnx]: outputs, elapsed run_inference_ort(path, test_input) print(f{path} 耗时: {elapsed * 1000:.2f} ms)我在一台普通笔记本CPU上跑FP32模型单次推理大约30ms动态INT8大约18ms静态INT8大约15ms。这个加速比例不算夸张但放到高并发服务端同样的时间内能处理的请求量就多了一倍左右。如果你的模型是内存带宽瓶颈型比如大语言模型这个收益还会更明显。如果你的环境有支持CUDA的GPU可以这样指定GPU执行sess ort.InferenceSession( resnet18_int8_static.onnx, providers[CUDAExecutionProvider, CPUExecutionProvider] )注意providers的列表顺序代表执行提供方的优先级第一个是可用的首选。3.6 用FP16再压一道很多时候FP16是最容易被忽略的一步。不需要任何校准数据只是把权重从FP32换成FP16模型文件立刻减半速度也有提升。代码同样简单from onnxruntime.transformers import float16 float16.convert_float_to_float16( resnet18.onnx, resnet18_fp16.onnx ) print(FP16转换完成)这个转换对绝大多数模型都适用尤其适合GPU推理。如果你的模型里有某些算子对精度极其敏感转换后要对比一下输出差异。一般情况下FP16的输出和FP32相比差异很小肉眼基本无法分辨。到这里你已经拿到了同一模型的FP32、FP16、INT8动态、INT8静态四个版本。把这套流程跑熟再换成自己的业务模型思路是完全一样的。4. 硬件与速度FP8、INT8、FP4到底谁更快4.1 为什么低精度会更快低精度带来加速主要有两个原因。第一运算吞吐量提升。CPU和GPU里的多数计算单元处理低精度数据的速度远高于高精度数据。以新一代GPU为例同一条Tensor Core流水线处理FP16的效率可能是FP32的两倍处理FP8或INT8又可以进一步提升。硬件设计上就是按倍数给低精度“开特权”的。第二数据搬运量降低。推理过程要反复读取权重和中间结果内存带宽往往是瓶颈尤其是大语言模型这类参数密集型的组件。每次搬运的数据量减半相当于带宽直接翻倍。很多模型量化后速度提升主要靠的就是这个。类比一下同样一辆货车FP32是运大箱子FP16是运小箱子INT8是运更小的箱子。单位时间内拉的箱子总数变多总重量可能没变但件数和周转效率上去了。4.2 新GPU上的FP8与NVFP4关于“RTX 5080上FP8、NVFP4、INT8哪个最快”这类问题我觉得应该分两个层面看。从峰值算力看新一代GPU的Tensor Core对FP8投入很大FP8的Tensor TFLOPS通常明显高于FP16而NVFP4或FP4这样的4位格式在硬件规格表上往往比FP8更高。只看峰值4位格式肯定最快FP8次之INT8和FP16要看具体芯片设计。但从实际工程角度看峰值算力不等于实际速度。NVFP4很新很多模型和推理框架还没对它做充分适配跑起来可能很慢甚至会回退到不支持的算子。相比之下INT8和FP8的生态要成熟得多ONNX Runtime、TensorRT等工具都做了深度优化。所以我的建议是如果你追新硬件、模型结构简单可以试试FP8甚至FP4如果需要稳定产出、尽快上线INT8和FP16仍是现阶段最稳的组合。4.3 不同场景怎么选不同硬件、不同业务场景量化精度的优先级完全不一样。我把常见的几种情况整理成表场景推荐精度原因CPU部署INT8动态或静态文件小内存占用低CPU对INT8优化成熟老一代GPU无FP8支持FP16或INT8兼容性最好驱动和框架支持稳定新一代GPU支持FP8FP8或FP16计算吞吐更高且FP8保留浮点动态范围大语言模型推理INT8或INT4显存占用是核心矛盾低精度是关键手段端侧/移动设备INT4或INT8存储和内存都受限必须极致压缩模型训练FP16或BF16保留动态范围和梯度精度低精度训练风险大这个表不是死规矩但作为一个起点非常实用。新手最容易犯的错是在老硬件上硬上FP8或者在端侧模型里盲目追求FP16而不愿意用INT8。先认清自己的硬件上限再选精度可以少走很多弯路。5. 量化路上的坑与排查清单5.1 量化之后精度崩了怎么办量化后精度大幅下降是最常见的问题。第一反应不要怀疑量化本身而是检查三点。第一校准数据是否贴近真实场景。静态量化时如果你拿随机噪声做校准scale和zero_point肯定不靠谱。务必用真实输入的采样集覆盖各种边界情况。第二是否用了per-tensor量化。默认情况下很多工具对每一层用一个全局scale如果层内权重分布差异很大精度容易崩。可以改用per-channel方式即对输出通道分别计算scale这样精度通常能改善不少。第三是否存在特别敏感的层。有些模型对微小扰动极度敏感全部量化扛不住。这类情况可以考虑混合精度方案把关键层留在FP16或FP32其余层用INT8。ONNX Runtime的quantize_static里也可以配置算子级别的量化开关灵活做局部处理。如果以上都不行最后的手段是量化感知训练也就是在训练阶段就把量化误差模拟进去让权重适应低精度表示。这个方法效果最好但需要重新训练模型成本也高。5.2 量化之后反而变慢了听起来反直觉但确实会发生。我在一个小型OCR模型上就遇到过这种情况量化后单次推理时间不降反升。排查之后发现原因有三个。第一个原因模型太小。量化本身有额外计算开销比如scale映射、zero_point修正。对于一个本身只有几MB的模型节省的传输时间抵消不了这些开销速度自然上不去。此时量化主要价值是减小体积而不是提速。第二个原因动态量化激活值计算开销大。动态量化在推理时会临时统计激活范围这个操作本身很重。如果你在CPU上跑而且激活值很大那么动态量化可能不如FP16。第三个原因没有真正用到加速硬件。有些框架在CPU上对INT8的优化并不充分反而GPU上的INT8加速更明显。检查一下是不是推理执行到了GPU或者是不是还在用默认的CPU provider。遇到量化后变慢的情况先看 profiling再针对瓶颈动手别急着怀疑量化方案本身。5.3 ONNX报错与算子不支持ONNX导入、量化时经常碰到算子不支持的报错一般有两种情况。一种情况是opset版本太低模型里有些新算子不认识。把opset_version调高试试我一般用15或更高。但要注意ONNX Runtime的版本不能太老否则高版本opset里的算子它也不认识。另一种情况是模型里有一些自定义算子或者特别小众的算子。这时可以用onnxsim对模型做化简去掉冗余算子pip install onnxsim onnxsim resnet18.onnx resnet18_sim.onnx简化之后再量化很多莫名奇妙的报错都能消掉。如果你的模型里有非标准自定义算子那没办法只能等ONNX Runtime支持或者换推理引擎。5.4 几个实用技巧分享最后分享几个我平时踩坑踩出来的习惯。第一个习惯量化之前先跑一遍原始FP32模型记录输出和耗时作为基线。量化后任何改动都跟这个基线对比不要凭感觉判断好还是坏。第二个习惯保存模型时不同精度用不同的文件名比如model_fp32.onnx、model_fp16.onnx、model_int8.onnx。这样调试时可以快速切换对比不会覆盖掉原始版本。第三个习惯ONNX模型导出后先用onnx.checker检查再用onnxsim简化。很多人在导出后直接量化结果被一个小问题卡住其实都是化简能解决的。第四个习惯在对比精度时不要只看最终准确率还要看输出分布的变化。有时候准确率没差但某些样本的输出概率变化很大这对线上业务可能是致命的。我在实际项目里对这些技巧深有体会。之前做一个小型OCR服务模型从FP32压到INT8体积从200多MB降到70MBCPU推理耗时从每张60ms降到40ms识别准确率只掉了0.3%不到。这个收益比单纯换一台更高配置的服务器划算得多。量化不是银弹但它确实是深度学习工程化里性价比最高的优化手段之一。把FP32、FP16、FP8、INT8、INT4各自的脾气摸清楚你的模型部署之路会顺畅很多。
网站建设高端定制企业官网