最近大半年一直在跟大模型推理部署较劲,LLM量化这块算是从原理到踩坑都过了一遍。每次看到群里还有人问“量化之后模型是不是就废了”“INT4到底能不能用”,我就想干脆把这段时间的实践整理成一篇完整的东西。这篇文章不聊那种纯理论推导,更多是我实际跑模型、调参数、排查问题时的真实记录,包括方案怎么选、步骤怎么做、哪些坑必须避开,希望对准备上手量化的朋友有点实际帮助。
我先把结论放在前面:LLM量化不是一个“要不要做”的问题,而是“怎么做才不亏”的问题。7B模型FP16权重就要占14GB,量化到INT4只有3.5GB左右,一台16GB显存的消费级卡就能轻松带起来,甚至CPU都能较流畅地跑。代价是精度损失,但这个损失可以通过选对方案、校准数据和量化粒度控制在几乎看不出来的程度。下面我会把原理、方案、实操和排查分开来写。
1. 量化到底在干什么:把精度换成显存和速度
1.1 浮点数与低比特的换算逻辑
大模型的权重和激活值默认是FP16或者FP32存的。FP16用2字节表示一个数,FP32用4字节。量化做的事情很直接——把32位浮点或者16位浮点压成8位整数、4位整数,甚至3位、2位。换算关系很直观:一个FP16权重占2字节,INT8占1字节,INT4只占0.5字节。7B模型按7×10⁹个参数算:
- FP16:7×10⁹ × 2B = 14GB
- INT8:7×10⁹ × 1B = 7GB
- INT4:7×10⁹ × 0.5B = 3.5GB
这还没算KV Cache和激活值。所以为什么大家都在做量化,最直接的原因就是模型装不装得下、跑不跑得动。
但量化不是简单地把数字截断。FP16的取值范围和精度都远大于INT8,直接截断等于把0.3存成0,模型输出直接乱掉。实际做法是找一个缩放系数scale和一个偏移zero_point,把这个张量里的数值范围映射到整数范围。反量化公式是:
r = (q - zero_point) × scale
其中q是量化后的整数,scale是浮点缩放因子,zero_point是整数偏移,r是还原后的浮点值。正向计算时,权重用低比特存,计算前反量化成浮点,或者使用特殊的融合核函数在整数域直接做矩阵乘,避免来回转换的开销。
1.2 对称量化与非对称量化的选择
按是否带偏移,量化分成对称和非对称两种。对称量化假设数据范围是[-a, a],zero_point固定为0,实现简单,INT8的硬件支持也最好。非对称量化允许正负范围不对等,多存一个zero_point,对激活值这种分布不均衡的数据更友好。
我实际测试下来,权重用对称量化就够了,因为权重分布一般接近高斯分布,正负对称。激活值建议非对称,否则负值区间会被浪费,量化精度损失加大。
这里有一个特别容易被忽略的点:量化粒度。同一个Tensor里不同通道的数值范围可能差好几倍,如果整层统一一个scale,小的通道会被压到几乎无法区分。所以现在主流做法是per-channel或者per-group量化。per-channel对每一行/每一列单独算scale,per-group更细,比如GPTQ里常见的group_size=128,就是每128个参数一组,每组一个scale和zero_point。粒度越细精度越高,但存储和计算的开销也更大,128是目前性价比比较好的平衡点。
1.3 动态量化与静态量化的区别
量化还可以按scale的确定时机分成动态和静态。
动态量化在推理时才计算激活值的scale,每一条数据不一样,精度最好,但统计开销大,且很多算子不支持。静态量化用一批校准数据提前统计出固定scale,推理时直接查表,速度更快,也更适合部署。
对大模型来说,主流方案基本都走静态量化路线。不管是GPTQ、AWQ还是llama.cpp里的GGUF,都是先拿一小批数据喂给模型,统计激活值分布,算出量化参数,然后才是真正的推理阶段。这里校准数据集的选择会直接影响量化质量,后面实操部分我会详细说。
2. 主流量化方案怎么选:PTQ、GPTQ、AWQ与GGUF
2.1 常见方案对比表
现在提到LLM量化,绕不开这几个名字:bitsandbytes的load_in_8bit和load_in_4bit、GPTQ、AWQ、GGUF,还有相对小众一点的SqueezeLLM、QLoRA里的NF4。它们都属于训练后量化,不需要重新训练大模型,区别主要体现在误差补偿策略上。
| 方案 | 基本思路 | 典型位宽 | 适合场景 |
|---|---|---|---|
| bitsandbytes | 分块量化,每块独立scale | INT8 / 4bit NF4 | 快速体验、单卡加载 |
| GPTQ | 基于Hessian矩阵的逐层误差补偿 | INT3 / INT4 / INT8 | 追求高压缩比和精度平衡 |
| AWQ | 根据激活值分布保护敏感通道 | INT4 / INT3 | 权重分布不均匀时 |
| GGUF (K-quants) | 按重要程度分配位宽,混合精度 | 2-8bit | CPU部署、边缘设备 |
2.2 GPTQ为什么能做到低比特高精度
GPTQ的核心思想不是简单把数值截短,而是“量化一层,修正一层”。它利用二阶信息(Hessian矩阵)算出哪些权重对模型输出的影响最大,然后逐个权重调整,让整层的输出尽量接近量化前的状态。
我最早跑GPTQ时用的是某开源社区提供的INT4模型,一个7B模型量化到4bit后,在中文问答场景下依然能保持基本流畅的对话能力,偶尔会出现轻微语句重复,但整体可用。这个方案的缺点是需要跑校准流程,耗时取决于模型大小和校准集长度,7B在普通消费级显卡上大约需要十几分钟到半小时。
实操中我建议优先使用别人已经量化好的公开权重,省时省力。如果需要自己量化,校准数据集长度控制在128到512条之间效果比较稳定,太少会过拟合,太多耗时翻倍但收益很小。
2.3 AWQ与感知缩放
AWQ的全称是Activation-aware Weight Quantization,核心洞察是:权重里不同的通道重要性不同,而通道的重要性可以通过对应激活值的幅度来判断。激活值大的通道,对应的权重误差会被放大,所以AWQ会为这些通道保留更高精度的表示,而不是对所有通道一视同仁。
和GPTQ相比,AWQ不依赖反向传播求Hessian,校准速度更快,而且有研究表明在部分任务上AWQ的低比特精度甚至优于GPTQ。我实测的感受是两者差距其实很小,更多是看部署框架的适配性。如果你用vLLM或者TensorRT-LLM,AWQ的支持度正在快速追上GPTQ,可以两个都试一下,用你自己的业务数据跑一遍看效果。
2.4 GGUF与llama.cpp的CPU部署
GGUF是llama.cpp项目定义的模型格式,它的一大特性是支持K-quants量化——也就是混合精度量化,不同层根据重要性分配不同位宽,比如非常敏感的层保留6bit,次敏感的用5bit,一般层用4bit甚至3bit。这种“好钢用在刀刃上”的思路,让GGUF在低比特下依然能保持不错的质量。
如果你要在CPU上跑模型,或者部署到树莓派这类资源非常有限的设备上,GGUF基本是最优选择。我试过把一个13B模型量化为Q4_K_M格式,在一颗普通桌面级CPU上,速度大概能到每秒4到6个token,用来做离线文本生成、知识库检索问答完全够用。需要提醒的是,GGUF文件的后缀参数(Q4_K_M、Q5_K_S、Q8_0等)不只是压缩率区别,还决定了质量下限,别为了省几百MB盲目选超低位宽。
3. 实操流程:从环境准备到推理加速的关键步骤
3.1 环境与依赖准备
不管你用哪种方案,第一步都是把环境跑通。我这边用的组合是:
- Python 3.10
- PyTorch 2.x
- Transformers 4.36+
- CUDA 12.1
依赖安装没有特别多的坑,但要注意CUDA版本和PyTorch的对应关系。如果不匹配,后面加载模型很容易报“找不到libcublas”之类的错误。一个判断经验:先跑一段最简单的模型加载代码,能过再继续,不要跳到量化步骤之后再回来查环境问题。
3.2 使用HuggingFace生态快速体验低比特加载
最简单的上手方式是用Transformers配合bitsandbytes,在加载模型时直接指定load_in_4bit=True:
from transformers import AutoModelForCausalLM, AutoTokenizer model_id = "某开源对话模型" tokenizer = AutoTokenizer.from_pretrained(model_id) model = AutoModelForCausalLM.from_pretrained( model_id, load_in_4bit=True, device_map="auto", torch_dtype="auto" )这里有个关键参数device_map="auto",它会在显存不够的时候自动把部分层放到CPU上。我自己的经验是,能不开就不开,因为CPU与GPU之间的传输会拖慢速度。如果显存确实紧张,优先考虑减小max_length或者使用梯度检查点之类的方式,而不是依赖跨设备调度。
3.3 用GPTQ自己做一次模型量化
如果手头有开源量化脚本或AutoGPTQ库,自己量化也很方便。流程非常简单:加载FP16模型、准备校准数据、逐层量化、保存权重。
我实际量化一个7B模型的完整流程大概是这样的:
# 使用AutoGPTQ进行量化 python quantize.py \ --model_path 某开源对话模型 \ --output_path ./output_model \ --bits 4 \ --group_size 128 \ --dataset c4 \ --sample_size 128运行时间在一个小时以内,具体看GPU性能。我有一次贪快把sample_size降到32,结果量化后的模型在长文本生成时明显变笨,开始重复轮回。后来把样本数恢复到128,问题基本消失。校准集宁可多不要少,128到256条是舒服区间。
量化完成后,加载方式会很顺滑:
from transformers import AutoModelForCausalLM model = AutoModelForCausalLM.from_pretrained( "./output_model", device_map="cuda:0" )3.4 显存占用与速度的实测对比
我自己用同一批测试数据整理过一个对比表格,方便直观感受量化带来的收益:
| 配置 | 权重显存 | 生成速度 | 首token延迟 |
|---|---|---|---|
| FP16 | 14GB | 42 tokens/s | 350ms |
| INT8 | 7GB | 46 tokens/s | 330ms |
| INT4 | 3.5GB | 51 tokens/s | 310ms |
这个趋势是典型的:模型越大、生成任务越长,量化带来的收益越明显。量化之后权重变小,访存压力降低,尤其对长上下文的推理,优势会被持续放大。
但要注意,显存占用不等于模型大小。一个常见误区是看到量化后权重3.5GB就觉得4GB显卡能跑,实际跑起来还要算KV Cache和激活值。一个7B模型在4096长度上下文的场景下,KV Cache大概要多占0.5到1GB,再加上CUDA context和其他开销,8GB显存比较稳,4GB会很勉强。
3.5 校准数据集的选择
量化方案的校准数据,本质是模拟模型在真实场景中的激活值分布。你用什么数据去校准,量化参数就会偏向什么数据。比如用英文通用语料校准的模型,在处理中文法律文本时精度损失就会相对偏大。
我的操作经验是:先拿业务场景里的真实数据,比如历史对话记录、线上日志,切成128到256段文本;再加上一部分通用语料混合,比例大概7:3。这样既能保证业务场景的适配性,又不容易过拟合。校准时的max_length建议设置成256到512之间,太长耗时增加,太短捕捉不到长距离依赖关系。
4. 量化参数选择:别只盯着位宽看
4.1 位宽与质量的权衡逻辑
很多人问“INT4和INT8选哪个”。答案取决于三件事:模型大小、目标硬件和精度要求。对7B模型,INT4可以把它压到一张8GB卡上,而INT8需要12GB左右;对70B模型,INT8需要70GB左右,INT4只需要35GB。硬件边界决定了下限,精度需求决定上限。
我现在的选择策略是:
- 消费级单卡、追求流畅交互:INT4
- 服务端多卡、追求稳定性:INT8
- 离线批量处理、不赶时间:保留FP16或BF16,不量化
4.2 group_size与batch_size的影响
group_size的选择对精度影响比很多人想象的大。group_size从128降到64,量化精度会更高,但存储开销也会变大。以INT4为例,group_size=128的情况下,每128个权重共享一组scale和zero_point;换成group_size=64,记录scale的密度翻倍,实际存储会超过理论0.5字节/参数。
我在一个代码生成类模型上测试过:
| group_size | 存储开销 | 代码补全准确率 |
|---|---|---|
| 32 | 较高 | 最高 |
| 64 | 中等 | 较高 |
| 128 | 较低 | 可接受 |
| 256 | 很低 | 明显下降 |
所以group_size不是越大越好。追求质量用64,追求容量用128,256只适用于对精度不太敏感的场景。
4.3 敏感层保留高精度:混合精度量化
有些模型关键信息集中在特定层上,比如某些注意力层或MLP的特定输出层。把这些层也一并量化为INT4,损失会很明显。解决方案是混合精度量化:大部分层用INT4,少数敏感层用INT8甚至FP16保留。
llama.cpp的K-quants就是这个思路的实践,它会对不同层自动分配不同位宽,比如attention层权重分配更高精度,FFN层用较低精度。实测下来这种处理方式在低比特下比统一位宽好不少。自己在做量化方案时也可以沿用这个思路,先用FP16跑一版,逐层替换成INT4,看替换到哪层输出质量开始严重下滑,那层就保留高精度。
5. 推理加速与部署落地:量化之后的下一步
5.1 选择合适的推理框架
模型量化完之后,真正的考验是能不能跑得快。Transformers库虽然方便,但逐层推理的效率和优化空间有限。实际部署时,我一般会根据硬件选择专用框架:
- GPU部署首选vLLM,支持PagedAttention,显存利用率和吞吐量优势明显
- CPU部署用llama.cpp,自带GGUF支持,对低资源环境非常友好
- 有TensorRT经验的可以选TensorRT-LLM,性能和延迟都能再压一截
vLLM里加载量化模型很简单:
from vllm import LLM llm = LLM(model="某模型路径", quantization="gptq")5.2 长上下文场景的KV Cache优化
量化权重只是内存优化的第一步。随着上下文长度增加,KV Cache会快速增长,这通常是大模型部署时真正的隐形杀手。
KV Cache同样可以量化。vLLM支持将KV Cache量化为INT8或INT4,通过参数指定缓存数据类型。我实测在长对话场景下,KV Cache量化后显存占用能够降低30%到50%,且推理质量影响很小,因为KV Cache中的数值分布在经过处理后相对稳定。
5.3 吞吐量与并发之间的取舍
很多人在并发调优时会发现,把模型量化到INT4后,单条请求确实变快了,但并发一高,吞吐量却上不去。原因很简单:INT4模型在GPU上需要额外的反量化操作,矩阵计算的并发效率反而比INT8差一些。
如果你的服务对并发吞吐有硬性要求,可以跑个对比测试:INT8权重 + 高并发batch,和INT4权重 + 低并发batch,看看哪个更贴近业务目标。我自己的经验是,高并发在线服务用INT8更稳,端侧或离线场景用INT4更值。
6. 常见问题与排查心得
6.1 量化后模型胡言乱语,怎么办
这个是遇到最多的反馈。先不要急着骂量化方案垃圾,按顺序排查:
- 校准数据集是否覆盖业务场景?代码模型用英文维基校准,效果不可能好
- group_size是否过大?256以上在很多场景下都不推荐
- 是否所有层都被量化了?有些敏感层需要保留高精度
- 模型原本的健壮性如何?我见过一个很脆的模型,FP16下回答都一般,量化后更是乱成一团
排查顺序按这个来,大部分情况都能定位到问题。
6.2 显存没降多少,量化是不是失效了
这种情况通常是KV Cache和激活值在主导显存。一个7B INT4模型,权重降到4GB,但如果上下文拉到8192,KV Cache能再占掉几个GB。你感觉显存“没少”,大概率是总显存被上下文长度拉平了。
技巧:用命令查看模型自身参数占用和KV Cache占用,先确认大头在哪,再决定要不要做KV Cache量化。
6.3 量化后速度反而变慢
低比特权重在CPU上不一定更快。原因有两个:一是反量化操作本身耗时,二是很多框架对INT4的算子优化还不到位。判断方式很简单:统一跑一个500 token的长生成任务,用生成的tokens/s和首个token延迟来评估,不要凭感觉。
6.4 量化模型在部分任务上精度损失明显
有些场景对模型输出的“细微语义”极其敏感,比如文本分类、实体抽取这类任务,INT4确实可能出现掉点。这类场景我一般只做INT8,甚至保留FP16,因为显存访问延迟不是首要矛盾,准确率才是。
7. 一些踩过坑之后想说的话
如果要我总结一句话,那就是:量化不是把精度扔掉,而是把模型里的冗余去掉。一个好的量化模型,应该在推理质量没有明显滑坡的前提下,让你在更便宜的硬件上跑起来。别一味追求最低位宽,也别一听说量化就害怕,关键是用数据说话。
我现在做量化项目时,会固定跑三个指标:困惑度、下游任务准确率、实际生成速度。三个指标一起看,哪个不达标就回到前面对应的环节调参。这套流程帮我避免了很多“凭感觉做量化”的坑。
最后再分享一个小技巧:拿到一个新的开源模型时,先别急着自己量化,先找社区有没有已经量化好并且口碑不错的版本,用业务数据测一遍。如果满足需求就省事很多,不满足再自己动手。毕竟量化本身不是目的,让模型在你预算范围内的硬件上跑起来、跑得好,这才是目的。