先把话说在前头,这篇文章不是为了给你背一张“FP32为什么比FP16精度高”的教科书表格,而是为了让真正跑模型、部署模型的人,在看到int8、fp16、fp8、int4这些参数时不再发怵。量化(quantization)这几年几乎成了模型发布帖的标配词,从蒸馏压缩到本地部署,谁都要提一嘴“量化后显存减半”。但数字变小了,背后到底发生了什么变化,很多人其实是懵的:明明模型原本跑得好好的,凭什么int4就能塞进小显卡?为什么有时候量化完速度反而不升反降?这些问题如果你也有,那这篇文章应该能给你捋清楚。
这篇内容适合两类人:一类是刚接触模型部署,打算把开源模型放在自己电脑或服务器上跑的小白;另一类是已经在用FP16/INT8,但遇到精度下降、算子不兼容、速度不达标等问题,想系统搞明白原因的朋友。我会先把五种格式的原理讲透,再带你把ONNX、GPTQ、AWQ这些实操路线走一遍,最后把我在实际部署中踩过的坑和排查思路全部摊开讲。
1. 为什么模型要量化,量化的本质是什么
1.1 先搞清树根:FP32在底层长什么样子
FP32就是32位浮点数,计算机里用一个符号位、8个指数位、23个尾数位来表示一个数。它能表示的范围大约是1e-38到3e38,精度大概是小数点后7位有效数字。深度学习模型为什么默认用它?一个很现实的原因是训练时梯度变化幅度极大,有时候会非常小,有时候又会突然涨好几个数量级,FP32的动态范围大,能兜住这种剧烈变化。可以简单理解为FP32是一把能伸缩的尺子,既能量亿分之一的小数,也能量万亿级的大数。
但这种灵活是有代价的:一个权重就要占4字节。以现在的主流大语言模型为例,7B模型光权重就有70亿个参数,FP32格式下需要28GB存储空间,一块24GB的消费级显卡直接放不下。所以实际训练中通常用混合精度,推理时则要想办法把体积进一步压下来。这也是量化首先解决的第一件事:让模型塞得进显存。
1.2 量化的数学原理:把连续的尺子换成固定刻度
量化的核心逻辑并不复杂,就是把连续浮点数映射到一组离散的整数或低精度浮点数上。拿最常见的INT8举个例子,INT8只有256个整数等级,范围是[-128, 127]。如果一组FP32权重分布在[-1.0, 1.0],我需要找一个scale值把浮点数值缩放成整数:
- scale = 1.0 / 127 ≈ 0.00787
- 量化后的整数 q = round(r / scale)
- 反量化 r_hat = q * scale
比如原始数值0.5,除以scale得到63.5,四舍五入成64,再乘scale得到约0.5039,和原始值差了一点点。这个差值就是量化误差。所有量化算法本质上都是在做同一件事:想办法让这个误差尽量小,小到不影响最终输出。
神经网络对这类误差的容忍度比想象中高很多。因为网络不是精确计算器,更像一个鲁棒的近似器,权重和激活里混入一点噪声时,通常只改变输出的置信度,而不改变最终的分类标签或生成的语义。这也解释了为什么我们可以放心把FP32模型压到INT8甚至INT4。
1.3 为什么量化能带来实打实的性能收益
量化带来的收益主要有两个来源。第一是存储和带宽降低。模型推理时需要从内存或显存里反复读取权重,权重从FP32变成INT8后体积缩到四分之一,读取时间也会大幅缩短。尤其大语言模型是典型的“带宽饥饿型”应用,生成每个token时都要把全部权重扫一遍,权重缩小四份之一,生成速度很可能直接翻倍。第二是硬件算力提升。现在主流的CPU、GPU、NPU都对低精度计算有专门优化,比如GPU上的Tensor Core、CPU上的AMX指令,INT8的峰值算力通常能达到FP32的数倍。两个因素叠加,量化后的收益自然非常明显。
当然,量化不是免费午餐。精度下降、算子不兼容、额外反量化开销都是代价。接下来把五种格式一个一个拆开看,你就能理解每个数字背后到底该怎么选。
2. INT4、INT8、FP8、FP16、FP32五种格式逐一拆解
2.1 FP32与FP16/BF16:训练与推理的分岔路口
FP32虽然精度最高,但在实际推理部署中的性价比已经越来越低。FP16(半精度浮点数)用16位存储,包含1个符号位、5个指数位、10个尾数位。它的动态范围比FP32小很多,大概在6e-5到65504之间,训练时容易出现梯度下溢的问题。所以行业内才发明了“损失缩放(loss scaling)”技巧,在反向传播时把梯度放大,更新参数时再缩小回来。推理阶段没有这个问题,因为前向传播的数值范围通常比较稳定,FP16表现很好,显存占用直接减半。
BF16是另一种16位格式,用1个符号位、8个指数位、7个尾数位,指数位数量和FP32一样,所以它能覆盖的动态范围跟FP32几乎一致,但精度低不少。BF16主要用于训练,避免梯度下溢,推理场景用得不多。另一个值得注意的点是,现代GPU对FP16/BF16有Tensor Core加速,计算吞吐通常是FP32的两倍以上。所以如果你的显卡支持,且模型不太大,FP16往往是推理部署的最低成本起点。
2.2 INT8:目前最普及的推理量化格式
INT8这些年一直是推理部署的绝对主力。一个很重要的原因是硬件支持最广:从NVIDIA GPU到Intel CPU,再到各种NPU芯片,几乎都内置了针对INT8计算的高效指令或硬件单元。软件生态也最成熟,ONNX Runtime、TensorRT、OpenVINO、llama.cpp等主流框架都有完善支持。
INT8量化分成动态和静态两种。动态量化会提前把权重离线量化为INT8,但激活值(比如矩阵乘法中的输入张量)在推理时实时计算scale再量化,因此不需要校准数据,实现简单,适合以权重开销为主的线性层、矩阵乘操作。静态量化则把权重和激活都提前量化,在推理前通过一组校准数据确定激活的量化参数,推理时不额外计算scale,性能更好,但需要额外准备校准集。实际使用时,小模型或CV模型一般优先考虑静态量化;大模型的很多场景则选择动态量化或带校准数据的weight-only方案。
校准集的选择直接决定量化效果。常见方法有minmax、百分位、KL散度等。TensorRT的校准器就使用KL散度来选择阈值,尽可能保留原始分布的相对熵。经验上,校准集不需要太大,几百到几千条有代表性的数据就足够;但覆盖面一定要广,如果只有少数几种固定模板,量化后的模型在处理真实输入时就会现出原形。
2.3 FP8:新一代硬件上的甜点
FP8是这两年随着Ada、Hopper、Blackwell等新架构显卡出现的新格式。它并不是一个单精度格式,而是包含两种变体:E4M3(4位指数、3位尾数)和E5M2(5位指数、2位尾数)。简单说,E4M3精度更高,适合前向计算和推理;E5M2动态范围更大,适合反向传播时保存梯度。
FP8相比INT8有一个天然优势:动态范围大不少,不太容易出现某个极端权重直接被clip掉的情况。所以在很多模型上FP8量化后的效果比INT8更稳,尤其在权重分布存在明显长尾的模型中。硬件上,RTX 40系列、RTX 50系列以及专业卡都提供了FP8 Tensor Core加速,实测中部分模型的FP8运行速度甚至能接近INT8,同时精度损失更小。如果你的显卡较新且框架支持,FP8是下一个值得优先尝试的量化格式。
2.4 INT4:把权重压到极限的做法
INT4听起来很诱人,4bit存储意味着比FP16小四倍,比INT8小两倍。但直接做naive四舍五入量化,精度会崩得没法看。所以实际部署中使用的INT4量化,几乎都不会是简单的round操作,而是经过调优的算法,常见的有GPTQ、AWQ,以及GGUF中定义的Q4_K_M这类组合方案。
GPTQ的核心思路是利用二阶信息(Hessian矩阵)逐层调整量化权重,尽量补偿由于rounding带来的误差。AWQ的思路则不同,它发现并不是所有权重通道都同样重要,会根据激活值的大小保留某些更重要的通道的精度,通过per-channel缩放来保护这些通道不被过度量化。这些算法听上去很高深,但使用体验已经非常平民化,HuggingFace上有很多量化好的模型可以直接下载,transformers加载时也几乎无缝。
需要纠正一个常见误区:INT4推理时,权重虽然按4bit存储,但计算时通常会反量化回FP16/BF16再做矩阵乘法。这意味着INT4主要省的是显存和带宽,并不一定会直接提升纯算力。对于大语言模型这种带宽瓶颈型应用,省带宽就是省时间,收益依然明显。
| 格式 | bit数 | 7B权重存储 | 典型用途 | 备注 |
|---|---|---|---|---|
| FP32 | 32 | 28GB | 训练基线、调试 | 精度最高但体积大 |
| FP16 | 16 | 14GB | 训练/推理默认 | 普遍通用 |
| BF16 | 16 | 14GB | 训练首选之一 | 动态范围大、精度略低 |
| FP8 | 8 | 7GB | 新硬件训练/推理 | E4M3和E5M2分场景使用 |
| INT8 | 8 | 7GB | 推理部署主力 | 软硬件支持最广 |
| INT4 | 4 | 3.5GB | LLM极致压缩 | 需要GPTQ/AWQ等算法支撑 |
3. 实操:从模型文件到量化部署
3.1 用ONNX Runtime把模型变成INT8/FP16
ONNX Runtime是目前最通用的推理引擎之一,量化操作也相当简单。先把PyTorch模型导出成ONNX,然后调用量化接口。动态量化的代码通常长这样:
from onnxruntime.quantization import quantize_dynamic, QuantType quantize_dynamic( model_input='model_fp32.onnx', model_output='model_int8.onnx', weight_type=QuantType.QInt8, )这个函数会遍历模型里的算子,把能量化的算子权重转成INT8,推理时再动态反量化。优点是省事,但性能上限不高。如果你希望获得更好的性能,可以试试静态量化,先准备一个校准数据读取器:
from onnxruntime.quantization import quantize_static, CalibrationDataReader, QuantType class DataReader(CalibrationDataReader): def __init__(self, dataloader): self.iter = iter(dataloader) def get_next(self): try: return next(self.iter) except StopIteration: return None quantize_static( model_input='model_fp32.onnx', model_output='model_int8_static.onnx', calibration_data_reader=DataReader(calib_dataloader), quant_format=QuantFormat.QDQ, per_channel=True, weight_type=QuantType.QInt8, activation_type=QuantType.QInt8, )校准数据要构造到和模型输入完全一致的格式。比如图像模型要处理好归一化,文本模型要使用相同的tokenizer。静态量化的好处是激活值也离线量化,推理时不需要频繁计算scale,速度更快。
如果只想做FP16转换,可以用onnxconverter_common:
from onnxconverter_common import float16 model_fp16 = float16.convert_float_to_float16(model_fp32)这里有个常见的坑:某些算子对FP16特别敏感,比如LayerNorm之类的归一化操作,在half精度下可能数值不稳定。通常需要用block_list参数把这类算子排除在转换范围之外。
3.2 大语言模型的量化路线:GPTQ、AWQ与GGUF
大语言模型的量化工具链和传统ONNX略有不同,但现在也已经很成熟。想用GPTQ量化模型,可以借助AutoGPTQ和transformers来加载:
from transformers import AutoModelForCausalLM, AutoTokenizer from auto_gptq import AutoGPTQForCausalLM tokenizer = AutoTokenizer.from_pretrained("your_model_path") model = AutoGPTQForCausalLM.from_quantized( "your_model_path", use_triton=False, device_map="auto", )AWQ方案类似,用AutoAWQ即可。这两类量化好的模型在HuggingFace上非常多,下载时注意看模型的量化配置,其中group_size通常为128或32,desc_act决定是否按激活列排序。group_size越小,量化粒度越细,效果越好,但内存和速度开销也会增加。
如果目标平台是CPU或者想跨平台跑,GGUF格式是更稳妥的选择。llama.cpp生态里的Q4_K_M、Q5_K_M、Q8_0等量化等级各有侧重点,简单选择原则是:内存有限就选Q4_K_M,效果优先就选Q8_0,Q5_K_M则介于二者之间。这套方案的好处是运行环境非常轻量,单文件就能跑,不需要装一堆Python依赖。
3.3 量化后的效果评估:不要只盯着PPL
很多人量化完模型,拿perplexity(PPL)测一下,看到数值只涨了一点点就觉得没问题。但PPL只能反映整体分布层面的差异,并不能完全代表真实业务质量。尤其对生成式模型,PPL接近不代表具体输出不会出错,代码模型可能会少写一个分号,数学模型可能推导步骤直接乱套。
我的习惯是准备一组固定prompt,分别用原始精度模型和量化后模型跑一遍,进行人工对比。比如让模型解同一个数学题、写一段带条件的代码、做一段摘要。如果有评测集,跑一套完整benchmark自然更好。这一步虽然不能量化到每个指标,但至少能帮你拦住中位数的退化。
还有一个容易被忽略的点:量化后的模型如果继续做有监督微调,有时能把精度损失补回来一部分。虽然这在训练成本上不划算,但如果你已经有一个量化好的模型且业务指标差一截,可以适度尝试。
3.4 一个量化模型的实际性能记录模板
做量化部署时,最忌讳“凭感觉”判断速度快不快。建议按下面的表格记录几项核心指标:
| 配置 | 权重格式 | 显存占用 | 首Token延迟 | 生成速度 | PPL或任务得分 |
|---|---|---|---|---|---|
| 基线 | FP16 | 14GB | 55ms | 30 tok/s | 12.3 |
| 方案A | INT8 | 7.2GB | 42ms | 38 tok/s | 12.5 |
| 方案B | INT4 | 3.8GB | 48ms | 36 tok/s | 13.1 |
表格里每一行都要固定测试条件:输入长度、输出长度、batch大小、是否使用流式输出、测试硬件。只有控制变量,你才能判断当前瓶颈究竟在显存、内存带宽还是算子实现。
4. 不同量化格式的硬件适配与性能收益
4.1 CPU、GPU、NPU分别该用什么精度
CPU的场景里,INT8几乎是标准答案。Intel从Ice Lake开始加入VNNI指令,到Sapphire Rapids加入AMX指令,对INT8矩阵计算加速明显。用ONNX Runtime + OpenVINO跑INT8模型,吞吐通常比FP32能提升好几倍。INT4在CPU上则要看框架是否支持,目前llama.cpp对CPU做了很多优化,GGUF的INT4量化在CPU上也有不错表现,但其他框架支持度不一。
GPU上要分新旧卡。老一点的Turing、Ampere架构对INT8支持很好,Tensor Core吞吐大概是FP16的两倍;而Ada、Hopper、Blackwell这些新架构的FP8表现更亮眼。RTX 50系列在FP8上的速度优势尤其明显。要注意的是,FP16在新旧GPU上都有比较均衡的兼容性,所以如果没有特殊原因,GPU推理的最低起点建议放到FP16,而不是FP32。
NPU和各类AI加速卡的偏好也基本以INT8为主,部分新型号提供FP16支持。如果你做边缘部署,INT8永远是兼容性最好的选择。
4.2 量化后显存、延迟、吞吐的收益幅度
量化最直观的收益是显存。以7B模型为例,FP16权重约14GB,INT8约7GB,INT4约3.5GB。但这是纯权重部分,实际部署还要算上激活值、KV cache(对大语言模型而言)和运行时开销,所以总显存下降幅度会略小于这个比例。如果你的batch较大,KV cache开销会占很大比例,权重量化的总收益会被稀释一点,但仍然可观。
延迟和吞吐受多个因素影响:推理引擎、batch大小、算子实现、硬件指令。模型单次推理时首Token延迟通常对带宽不敏感,量化带来的提升有限;但长序列生成时权重读取量巨大,量化收益就会非常明显。我的实测经验是,在7B级别的LLM上,从FP16切到INT8后,生成速度普遍能提升20%到40%,显存占用下降约一半;切到INT4后生成速度提升幅度反而可能变小,因为反量化开销开始成为新的瓶颈,但显存优势仍然突出,适合塞进较小显存的设备。
4.3 如何快速记录和分析性能
分析性能时我一般分三步。第一步监控硬件状态,用nvidia-smi看显存、功耗和利用率:
nvidia-smi --query-gpu=memory.used,utilization.gpu,power.draw --format=csv -l 1第二步用推理引擎自带或自编脚本测延迟和吞吐。比如vLLM有benchmark脚本,llama.cpp有perplexity和token速度测试,ONNX Runtime可以用Python脚本连续跑几十遍求均值。第三步把不同精度的结果放到同一张表格里横向对比,重点看“变化趋势”而不是单个数字。
这里有一个经验:测延迟时一定要丢弃第一次调用,因为很多框架有warming up逻辑,第一次往往包含了初始化开销。测吞吐时batch至少要尝试4、8、16等多个档位,有些模型在小batch下INT8优势不明显,但batch提升后优势会变大。
5. 常见问题与排坑指南
5.1 量化后模型输出变差怎么办
如果你的模型量化后明显变傻、输出乱序、回答质量下降,常见原因有三个:校准集偏差、量化粒度过粗、模型本身对噪声过于敏感。校准集过于单一会导致激活量化参数不符合真实分布,解决办法是换用覆盖面更广的校准数据;量化粒度是指per-tensor还是per-channel,一般来说per-channel效果远好于per-tensor,如果框架支持,优先打开per-channel;如果这些调整后仍然不够,考虑换用FP8或者升级到GPTQ/AWQ这类更复杂算法。
还有一种情况是模型极小,比如参数量只有几千万到一两亿,这种模型对量化误差非常敏感,压缩到INT4可能直接崩掉。这时候可以先把精度底线放在INT8,不要盲目追求极致压缩。
5.2 量化后速度不升反降是怎么回事
这种情况我踩过不少次。首先要判断是不是量化没有生效。有些框架在不支持INT8的算子上会自动回退到FP32,而你又没看日志,结果模型说得头头是道,跑起来却没任何优化。解决办法:检查推理日志或profile输出,看算子的执行精度。其次,模型太小或batch太小时,量化带来的计算收益不足以抵消反量化等额外开销,反而会变慢。还有一个容易忽略的点:某些框架在没有VNNI/AMX指令的CPU上跑INT8,会比FP32还要慢。
实践中建议优先确认模型算子是否真正跑在目标精度上,再尝试增大batch。如果batch=1和batch=16的收益差异很大,说明量化确实在计算环节生效,只是batch太小时边际收益不明显。
5.3 工具链报错与算子不支持怎么办
常见的报错是“Quantization not implemented for op”或“Unsupported operator”,原因是当前版本的推理框架还没有覆盖你这个模型里的某个算子。最直接的三个处理方向:升级推理引擎版本;把模型导出的opset版本调高(比如导出ONNX时设置opset=17);把不支持的算子或者整段子图排到CPU执行。如果是在TensorRT上遇到算子不支持,可以先尝试用onnx2trt转换时开启更高精度模式。
这些报错看上去很吓人,但大多数情况都只是版本匹配问题。如果你是新手,我建议直接使用社区里已经验证过的模型文件,而不是自己从零导出,能省很多时间。
5.4 什么情况下需要做量化感知训练(QAT)
训练后量化(PTQ)处理了95%以上的场景,但也有例外。如果目标模型用于语音识别、目标检测这类任务,且精度要求极高,或者模型本身很小、对数值扰动极度敏感,PTQ可能怎么调都达不到标准。这时候就该考虑QAT(量化感知训练)了。QAT的原理是在训练过程中模拟量化噪声,让模型参数主动适应低精度的数值范围。PyTorch里可以用torch.ao.quantization的fake_quantize模块来实现,流程上先插入伪量化节点,再正常训练和导出。
QAT成本不低,因为需要重新训练,并且还要维护一套量化训练流程。所以我的建议永远是:先PTQ,把校准、粒度、精度格式这些都尝试一遍,实在不行再考虑QAT。还有一个小技巧:如果模型有微调计划,可以先做个针对性的小模型蒸馏或任务微调,再实施PTQ,效果往往比直接QAT省时间。
量化这件事,说到底是一个精度、速度、显存三方权衡的问题。我自己的习惯是,任何新模型上线前,先拿FP16跑一遍业务冒烟测试,记录baseline;再试INT8;如果显存吃紧或模型比较大,再考虑INT4或FP8。每次只改一个变量,所有结果写进表格。你最后选什么格式不是最重要的,最重要的是你能清楚地知道每一个选择换来了什么、牺牲了什么。这种可控感,才是量化部署最值钱的东西。