前阵子有朋友问我,为什么同样的模型,别人8G显存跑得飞起,我16G反而卡成PPT。聊了一圈才发现,问题出在他们用了量化后的模型,而我还在拿原版浮点模型硬扛。“模型量化”这几年几乎成了显存急救的代名词——把浮点模型从FP32换成FP16、INT8甚至INT4的低比特表示,体积和显存占用直接降一个量级,速度还能往上走,代价则是精度上的一点妥协。这篇文章就把这件事从头到尾捋一遍,适合刚接触量化、想在本地跑大模型或者正在做推理部署的同学,看完你至少能回答三个问题:量化到底在做什么、有什么坑、以及在自己的机器上怎么落地。
1. 为什么要把浮点模型压成低比特
1.1 浮点模型的“体重”和“食量”
先算一笔账。一个70亿参数的模型,如果全部用FP32存储,光是权重就要占 7B × 4字节 ≈ 28GB 空间。换成FP16,省一半,约14GB;用INT8存,约7GB;再激进一点用INT4,大约3.5GB。这就是为什么很多人在8G显存上能跑7B、13B甚至更大模型的原因——模型体积被压下来了。
但显存占用只是第一层,速度同样重要。大模型推理是带宽密集型任务,每生成一个token都要把模型权重从HBM读一遍,能存下多少、读多快,直接决定生成速度。拿两个同规格的7B模型对比,FP16那个每生成一个token要读14GB权重,INT4那个只要读3.5GB,显存带宽相同的情况下,后者理论上可以获得接近4倍的速度提升。虽然实际中硬件算子还有其他开销,但这个趋势是确定的。
所以量化解决的根本问题是:在有限显存和设备带宽下,如何把更大的模型高效地跑起来,或者让同一张卡跑得更快、同时塞下更多并发请求。
1.2 低比特表示到底改变了什么
低比特表示不是说简单地把数值“截断”。拿FP16来说,它还是浮点数,只是尾数和指数位少了,数值范围缩小、精度降低;而INT8、INT4是完全不同的玩法——把连续浮点范围映射到有限整数集合上,然后用整数矩阵乘法替代浮点矩阵乘法。
这一替换带来两个收益。第一,整数运算单元在大多数现代GPU和CPU上效率远高于浮点单元,尤其NVIDIA GPU上Tensor Core对INT8有专门加速;第二,带宽压力和显存占用同步下降。代价是信息有损,就像一张高清照片用高压缩率JPG保存,肉眼可能看不出差别,但稍微放大看细节就能感受到涂抹感。量化本质上是“有损压缩”,但只要你把压缩误差控制在可接受范围内,工程收益非常可观。
2. 量化背后的数学:对称、非对称和校准
2.1 从FP32到INT8的映射公式
量化的核心就一个公式:浮点值 r 和整数 q 之间的映射关系可以写成 r = S × (q - Z)。其中S是缩放因子,也叫scale,表示一个整数刻度代表多大的浮点范围;Z是零点,表示浮点0映射到哪个整数。反过来说,量化过程就是 q = round(r / S) + Z。
举一个具体例子。假设某层权重分布范围是 [-1.0, 2.0],要量化到INT8,INT8的取值范围是 [-128, 127],那么 S = (2.0 - (-1.0)) / (127 - (-128)) = 3 / 255 ≈ 0.01176。零点 Z = -128 - (-1.0 / 0.01176) ≈ -42.98,四舍五入取 -43。于是原本浮点范围内的任何值,都会被映射到一个整数上。推理时如果需要还原到浮点,就用 r = 0.01176 × (q - (-43)) 计算。
这里面有个细节很多人会忽略:量化后的整数再反量化回浮点,得到的值与原值是有误差的,这种误差称为量化误差,来源有两个。一是舍入误差,round必然产生;二是裁剪误差,如果某个权重值超出表示范围,会被直接截断到边缘。理解这两类误差来源,后面排查精度问题会很有帮助。
2.2 对称量化与非对称量化
接着上面的公式说,Z的取值直接决定量化方式的区别。
对称量化要求零点Z固定为0,公式简化为 r = S × q。这种情况下,浮点正负范围必须对称,比如 [-2.0, 2.0],正的用正整数表示,负的用负整数表示,0就固定在整数0。对称量化的好处是推理时不需要考虑零点偏移,硬件实现简单,所以很多面向GPU的INT8方案都用它。缺点是如果数据分布不均匀,比如某个分布整体在0到正数之间,对称量化会浪费一半的表示范围——为了表示很小的正数,把很大的负数范围也用上了,分辨率会被浪费。
非对称量化允许Z不为0,也就是零点可以偏移。这种方式很契合ReLU激活函数的输出分布,因为ReLU输出全是非负值,用非对称量化可以把表示范围完全对齐到正数区间,分辨率更高。代价是推理时要多做一次减法,算子复杂度更高。
实际选择时,权重一般用对称量化,因为DNN训练后的权重分布通常接近0中心的对称分布;激活值则要根据分布特点来选择,如果分布在0以上就用非对称,如果大致对称也可以用对称。
2.3 校准算法:MinMax、Percentile、KL散度
知道了映射公式,下一步就是确定S和Z。这个确定过程叫校准。校准需要用一小部分有代表性的真实数据在模型上正向跑一遍,统计每一层的数值分布,然后根据分布特征算出最优缩放因子。常用的方法有3种。
MinMax最简单,直接取观测到的最大值和最小值作为范围。它的缺点是容易被异常值带偏。想象某个激活层99.9%的数据都落在 [0, 1] 区间,但偶尔有一个值冲到10,MinMax会强制把量化范围拉到 [0, 10],此时[0, 1]区间内的量分辨率就变得很差。
Percentile稍微好一点,取99.9%或99.99%分位点作为边界,超过边界的一小撮异常值直接截断,有效降低了异常值影响。KL散度方法(也叫熵校准)更精细,它的思路是:尝试不同的裁剪边界,计算裁剪后量化分布与原始浮点分布之间的KL散度,选择信息损失最小的那个边界。TensorRT的INT8校准用的就是类似思路。
校准集的选择也很有讲究。一般建议用几百到几千条与真实业务场景分布一致的样本,宁可少而精准,不要多而杂乱。我之前见过有人拿训练集全量数据去校准,结果耗时巨大,精度没见更好,反而因为类别不平衡被带偏了。
2.4 per-tensor与per-channel的选择
缩放因子S和零点Z还有一个作用范围的维度:是整层共用一个,还是每个通道各用一个。
per-tensor就是整个层只有一个S和一个Z,简单、存储省、算子效率高,但如果层内不同输出通道的数据范围差异很大,精度就会受影响。per-channel是每个输出通道独立算一组S和Z,精度更好,尤其对权重而言,不同卷积核的输出分布往往差异很大。代价是标量存储变成向量,且某些硬件的算子实现会复杂。
在大模型4bit量化中,还有一个概念叫group size。比如GPTQ和AWQ常用的group size是128或32,意思是每128个权重共享一组量化参数。group size越小,参数越细,精度越高,但中间参数占的空间也越多。这块属于精度和压缩率的平衡艺术,后面实操章节还会提到。
3. 主流量化方案:PTQ与QAT
3.1 训练后量化(PTQ):最省事的路线
训练后量化,英文Post-Training Quantization,简称PTQ,是绝大多数人的第一选择。流程很清晰:先有一个训练好的浮点模型,然后准备一小批校准数据,跑一遍统计分布,确定量化参数,最后把模型转换成低比特表示。全程不需要重新训练,成本极低。
PTQ的缺点是,当比特数降到4bit时,精度下降会比较明显,尤其是小模型和敏感任务上。8bit场景下,PTQ通常能把精度损失控制在1%以内,很多任务甚至可以忽略;但4bit场景下直接做PTQ往往扛不住。这也是为什么大模型领域后来发展出了GPTQ、AWQ这些更精细的量化算法——它们本质上也是PTQ的一种,只不过在权重调整和处理上做了更聪明的优化。
3.2 量化感知训练(QAT):精度优先的路线
量化感知训练Quantization-Aware Training就不一样了。它在训练阶段就往模型里插入“伪量化”节点,前向传播时模拟量化-反量化过程,让权重学会在低比特约束下仍然保持足够的表达能力。反向传播时用的是直通估计器,即忽略量化函数的不可导性,让梯度能穿过去更新权重。
QAT效果确实好,尤其适合对精度要求高的任务,但它有两个门槛:一是需要完整的训练流程、训练数据和GPU资源;二是对工程能力有要求,调学习率、调量化参数,都很考验经验。通常情况下,只有当你已经把PTQ调到极限还不够时才需要上QAT,而不是一开始就做。
3.3 大模型量化的特殊方案:GPTQ、AWQ、LLM.int8()
大模型参数动辄几十亿上百亿,逐层校准的代价很高,而且4bit量化时的精度损失问题更突出,所以近年出现了一批专门针对LLM的量化方案。
GPTQ的思路是逐层优化。它利用二阶信息(近似海森矩阵)来估计每个权重的重要性,量化时优先保证重要权重的误差小,并不断用未量化权重的误差去修正已量化的结果。它能把175B参数的模型压到4bit,而且推理还能有不错的精度。
AWQ的思路不同,它观察到一个现象:即使同一个层里,不同的激活通道重要性也不同,少数“显著通道”对精度影响很大。AWQ的做法是先扫描一遍激活值,找出这些重要通道,给它们单独分配更高的精度或更小的缩放范围。它以激活感知为基础去选择保护哪些权重,所以叫Activation-aware Weight Quantization。
LLM.int8()则是另一个思路,它发现大模型激活值中会出现极端大的离群特征,这些特征对精度至关重要。LLM.int8()的做法是把大部分特征用INT8高效量化计算,而离群部分单独拆出来用浮点高精度计算,并在最后合并结果。它做的有一些混合精度分解的意味,好处是能跑8bit不损失精度,速度通常比FP16略慢,但显存占用降了约一半。
3.4 如何选型
量化方案的选型没有银弹。就我自己的经验,简单总结一下:
- 7B以下的中小模型、CPU推理或需要先快速验证量化效果的,优先走PTQ;在PyTorch或ONNX Runtime里做8bit量化,半天就能拿到结果。
- 10B以上的大模型,优先看GPTQ和AWQ的4bit推理,配合llama.cpp或对应推理框架,消费级显卡基本跑得动。
- 对精度要求极其苛刻、同时有训练资源的,上QAT。
- 只是想在自己的Stable Diffusion流程里省显存,试试ComfyUI加载GGUF量化模型,这条后面专门讲。
4. 实操:从PyTorch到llama.cpp
4.1 PyTorch静态量化动手
PyTorch官方的量化工具链已经比较成熟了,做静态量化大致分四步:fuse模型、准备量化配置、校准、转换。
先看一个最小案例,这里拿一个简单的卷积模型举例:
import torch from torch.ao.quantization import get_default_qconfig, prepare_qat, convert model = MyModel().eval() # 1. 融合层:把Conv+BN+ReLU这种结构合并,减少量化误差来源 model_fused = torch.ao.quantization.fuse_modules_qat(model, [['conv', 'bn', 'relu']]) # 2. 配置量化 model_fused.qconfig = get_default_qconfig('qnnpack') # 或 'fbgemm' model_prepared = torch.ao.quantization.prepare(model_fused, inplace=False) # 3. 校准 with torch.no_grad(): for data in calibration_loader: model_prepared(data) # 4. 转换 model_quantized = torch.ao.quantization.convert(model_prepared, inplace=False)注意几个关键点。层融合不是可选项,它是把Conv、BN、ReLU这种连续的算子合并成一个算子,减少中间激活的精度损失。校准的时候模型要跑在eval模式,BatchNorm的统计量不能更新。另外PyTorch的静态量化主要针对CPU优化,x86上用fbgemm后端,ARM上用qnnpack后端;如果只想快速做GPU量化,建议直接用TensorRT或ONNX Runtime。
4.2 ONNX Runtime动态量化
如果你的模型已经导出成ONNX格式,想用最少的代码做量化,ONNX Runtime的动态量化最合适。
from onnxruntime.quantization import quantize_dynamic, QuantType quantize_dynamic( model_input='model_fp32.onnx', model_output='model_int8.onnx', weight_type=QuantType.QUInt8 )就这么几行。动态量化的意思是,它只量化权重部分,激活值在推理时按输入动态计算缩放因子,省掉了校准环节,所以无需准备校准数据。缺点是动态计算激活的scale会增加运行时开销,速度提升不如静态量化,但它胜在简单、不需要数据,适合快速试水。
如果需要在ONNX Runtime里做静态量化,流程跟PyTorch类似,需要额外准备一个校准数据集,并用CalibrationDataReader来喂数据。量化后记得用onnxruntime.quantization.shape_inference.quantize做一次模型校验和shape推断,否则有些算子会因为shape信息缺失转换失败。
4.3 llama.cpp的GGUF量化
LLM圈里现在聊量化,最常出现的名词就是GGUF和llama.cpp。GGUF是llama.cpp项目定义的模型格式,它把模型权重和量化参数一起封装成一个文件,使用时不需要额外加载浮点模型再转换,一步到位。
llama.cpp的量化命令很直接:
# 使用llama.cpp仓库内编译好的quantize工具 ./quantize ./models/model-f16.gguf ./models/model-q4_k_m.gguf Q4_K_M命令的最后一个参数是量化类型。常见的有Q2_K、Q3_K、Q4_K_S、Q4_K_M、Q5_K_M、Q6_K、Q8_0。其中_K表示这些量化基于k-quants方案,它是一种将权重按重要性分块处理的量化策略;_S和_M分别表示small和middle,_M的量化粒度更细,精度更好,文件也更大一点。Q4_K_M可以说是最常用的“甜点档”,体积合理、精度损失可控。Q8_0则接近无损,文件约是FP16的一半,适合那些不差显存但想在速度上有提升的场景。
实际跑的时候,8G显存可以比较流畅地跑7B模型Q4量化,13B模型Q4在16G显存下也可以跑。速度方面,取决于GPU是否做了llama.cpp的CUDA编译,如果没有编译GPU版本,默认走CPU会慢很多,建议自己重新编译一次,加上-DGGML_CUDA=ON,显存不足时也可以开启--n-gpu-layers参数,把一部分算子放在GPU。
4.4 bitsandbytes 4bit加载
如果你不追求极致推理速度,只是想快速在显存有限的机器上加载一个Hugging Face大模型做微调或推理,bitsandbytes是最快的路径。它的特点是“免转换”:无需预先量化模型文件,加载时实时把权重映射成4bit表示,例如:
from transformers import AutoModelForCausalLM, BitsAndBytesConfig import torch bnb_config = BitsAndBytesConfig( load_in_4bit=True, bnb_4bit_quant_type="nf4", bnb_4bit_compute_dtype=torch.bfloat16, bnb_4bit_use_double_quant=True ) model = AutoModelForCausalLM.from_pretrained( "some/base/model", quantization_config=bnb_config, device_map="auto" )这里的nf4是bitsandbytes提出的NF4数据类型,它基于信息论设计了非均匀的4bit分布,专门适配权重分布后的大模型量化,效果比普通均匀INT4要好。use_double_quant是二次量化的标志,意思是对量化参数本身再做一次量化,进一步省显存。很多大模型微调方案比如QLoRA,底层用的就是这套配置。
5. ComfyUI本地开启模型量化实操
5.1 ComfyUI的量化入口和版本区别
ComfyUI是当前很火的Stable Diffusion工作流工具,它本身不是一个传统意义上的“模型量化工具”,而是通过加载不同的模型和插件来实现低比特推理。很多人想在本地跑SDXL或SD1.5,但被显存卡住,这时候给ComfyUI上量化模型是性价比很高的方案。
在ComfyUI里谈“开启量化”,目前主流有两条路。一条是使用GGUF格式的扩散模型权重,类似LLM里的做法,把Stable Diffusion的UNet权重压成Q4、Q5、Q8,然后通过ComfyUI-GGUF插件加载;另一条是直接使用FP8格式的模型文件,FP8是比FP16更小的浮点格式,虽然不完全是整数量化,但从显存角度它能省一半。区分这两条路很重要,因为很多网上教程把FP8和量化混为一谈,导致新手配置时对不上号。
5.2 加载量化版模型的两种常见姿势
先说GGUF这条路。具体操作大概是:
- 在ComfyUI的custom_nodes目录下安装ComfyUI-GGUF插件。
- 下载量化好的GGUF格式UNet模型,一般从Hugging Face或者模型作者发布页能搜到,文件后缀是
.gguf。 - 把文件放到
ComfyUI/models/diffusion_models目录。 - 在ComfyUI工作流中,用
Unet Loader (GGUF)节点替代默认的Load Checkpoint,并在该节点里选择GGUF文件。 - 因为GGUF通常只量化了UNet部分,CLIP和VAE还是需要额外加载普通模型,可以继续用默认的CLIP Loader和VAE Loader。
这条路最明显的效果是显存占用大幅下降。我自己在8G显存的卡上跑SDXL,默认FP16模型经常爆显存,换成Q4_K_M的GGUF后,基础出图基本稳定在7G以内,速度也没有明显变慢,画质差异肉眼看不太出来,放大看细节会有一点点损失。
FP8这条路的操作要简单一点,很多模型作者直接发布FP8版本的safetensors文件。下载后放进models/checkpoints目录,然后在ComfyUI里正常用Load Checkpoint节点加载这个文件就行,不需要额外插件。关键是确认你的GPU支持FP8计算,目前主要是NVIDIA Ada Lovelace架构(比如RTX 40系)和部分新卡支持得比较好,老卡可能得用兼容模式,否则会掉回FP16。
5.3 实际参数建议与显存占用经验
结合自己多次折腾的经验,给几条实际的建议。
- 如果你用的是8G显存跑SDXL,优先试GGUF的Q4_K_M;如果画质接受不了,再往上试Q5_K_M或Q8_0。不要一上来就追求Q8,先用最小能满足画质的档位。
- 加载GGUF模型后,如果仍然爆显存,可以在启动参数里加上
--lowvram,ComfyUI会主动控制显存使用,不让它一次性把所有模块都加载进显存。注意这跟量化是两码事,但两者叠加效果不错。 - 无论哪条路,如果出图时出现黑图、噪点,先检查CLIP和VAE是否正常加载,而不是怀疑模型量化有问题。
- 不同版本ComfyUI界面差异较大,如果你找不到Unet Loader (GGUF)节点,更新插件和ComfyUI本身,通常就能解决。
6. 常见问题与排查技巧
6.1 量化后精度崩了
精度崩了大概率是以下原因:校准集不合适、量化范围被异常值带偏、层融合遗漏、或者权重分布本身就不适合低比特。
排查顺序建议这样来。先看输出是否“错得离谱”,如果是,多半是量化范围被异常值污染,换Percentile校准;如果只是轻微下降,优先换per-channel或减小group size;如果校准集数据跟真实业务完全不是一个分布,那就要重新准备数据。另外要确认每一层是否都成功融合了,存在没融合的层,它前后会产生额外的精度损失,这种问题光看代码很难发现,需要打印量化后的模型结构逐一比对。
6.2 体积没变小或速度变慢
我自己就踩过这个坑。明明是做了量化,保存的模型文件还是接近原始大小,后来发现是框架默认不做真量化,只是模拟计算。比如PyTorch如果用fake_quantize跑完没有执行convert,模型文件保存的仍然是浮点权重。另一个常见问题是,模型体积确实小了,但推理速度反而变慢。这在小模型上尤其明显——当模型本身很小、算子开销主要消耗在内存搬运和调度上时,INT8量化减少的那点带宽根本不足以抵消量化算子本身的调度开销。解决思路是:先确认模型真实加载内存而不是文件大小,然后用同一输入多次测时延取平均。如果确认是速度问题,看看是否用上了支持INT8的kernel,比如在llama.cpp里确认有没有把CUDA编译进去。
6.3 环境相关的坑
环境问题是最磨人的一类。GPU太老不支持INT8 Tensor Core,或者CUDA版本不匹配导致bitsandbytes报错,这类问题往往会出现在最开始安装时。建议装之前先查清楚自己的显卡和驱动信息,确认软件包版本支持矩阵。llama.cpp如果自己编译,记得多看编译日志里的CUDA选项是否真的启用了;ComfyUI插件报错则先看custom_nodes目录下依赖是否安装完整。环境问题排查时,一个万能方法是把报错信息完整地复制到搜索引擎里搜,通常能比你自己猜测更快定位到原因。
6.4 常见问题速查表
| 问题现象 | 可能原因 | 处理建议 |
|---|---|---|
| 精度严重下降 | 校准集与业务数据分布不符 | 重新采集代表性校准数据,建议100-1000条 |
| 输出质量局部劣化 | 某一层量化参数不合理 | 该层改为per-channel或更高精度 |
| 模型文件没有变小 | 没有执行真正的convert转换 | 检查量化流程是否完整走完 |
| 推理速度没有提升 | 模型太小,被调度开销覆盖 | 量化对超小模型收益有限,测大模型 |
| 加载报“CUDA error” | CUDA版本或显存不足 | 检查显存占用,升级驱动或降低模型档位 |
| ComfyUI加载GGUF报错 | 插件版本与ComfyUI不匹配 | 更新ComfyUI和ComfyUI-GGUF插件 |
7. 一点个人经验收尾
量化和蒸馏、剪枝这些压缩技巧一样,本质都是拿一点点精度换巨大的工程收益。我自己做量化项目,习惯永远从8bit PTQ开始,先看当前方案的瓶颈到底在显存、带宽还是精度,再决定要不要往4bit或者QAT走。很多时候真正的问题不是“量化后模型不行”,而是“校准没做好”,别一上来就把锅甩给低比特。另一个体会是,量化的最佳实践强烈依赖硬件平台,同样的INT8量化在CPU、老GPU和新GPU上的表现可能差很多,所以不能只盯理论数值,最终要用目标机器上的实测数据说话。希望你读完这篇文章,能在自己的机器上顺利跑起来第一版低比特模型。