Hey,各位折腾大模型部署的朋友们,好久不见。这次继续聊模型量化,而且是专门针对大语言模型(LLM)的那套玩法。
如果说量化基础篇讲的是通用方法论,那到了LLM这里,画风就完全不一样了。你有没发现,用PyTorch自带的torch.quantization去量化一个70亿参数的模型,结果往往惨不忍睹?要么推理速度没提升多少,要么回答质量断崖式下跌。原因很简单:LLM的输出分布和CNN完全不是一个套路。卷积网络的激活值相对规整,而LLM的激活值里藏着大量极端离群点,用经典的minmax或percentile策略一刀切,误差全砸在这些关键点上。
今天这篇“下篇”,咱们就把LLM圈子里那五个最出名的专用方案全部扒一遍:LLM.int8、GPTQ、AWQ、SmoothQuant、NF4。我会把它当成一次选型复盘来写,每个方案的核心思路、数学原理(尽量用人话讲)、实操参数、踩坑记录,一股脑倒出来。你可以直接对照自己的业务场景抄作业。
1. 为什么通用量化在LLM上失灵:离群点才是罪魁祸首
得先把这个根子上的问题说清楚。刚才提到离群点,具体长什么样?你在HuggingFace上随便拉一个7B模型跑一下前向,把某一层的激活值打印出来,会发现99.9%的数值都趴在-1到1这个区间里,但总有那么几个值能飙到20、50甚至上百。这串数字看起来有点疯狂,但确实是LLM的常态。
这种分布对量化是致命的。因为INT8的范围只有[-128, 127],你要把FP16的激活值映射过去,缩放因子(scale)就得根据绝对值最大的那个离群值来定。假设最大激活值是80,那scale就是80/127≈0.63,结果那些占比99.9%的正常小值被压缩到只有几个量化步长去表示,信息几乎全部丢失。这就是为什么通用量化方案在LLM上“翻车”频率特别高。
更麻烦的是,离群点不是随机分布的,它高度集中在特定的特征通道(channel)和特定的token位置上。有研究者发现,这些离群值往往出现在句子开头、标点符号附近,而且某些维度上几乎每一个token都会激活。这就没法靠简单的“裁剪”来清理,你剪掉它,模型能力直接受损;你不剪它,量化误差大到离谱。
所以LLM专用量化方案的核心命题就变成了:如何在保留离群点信息的前提下,尽可能把大部分计算用低位宽完成。围绕这个命题,演化出了几条完全不同的技术路线:有把离群点单独拎出来用高精度算的(LLM.int8)、有靠二阶信息逐层补偿误差的(GPTQ)、有主动把量化难度从激活转移到权重上的(SmoothQuant)、还有从数据格式层面做文章的(NF4)。下面一个一个说。
2. LLM.int8():离群点特殊对待的混合精度分解
这个方案是HuggingFace和论文《LLM.int8(): 8-bit Matrix Multiplication for Transformers at Scale》带火的,也是最早一批让人真正敢在超大模型上跑量化的方法。它的核心思路特别直白:既然离群点是少数,那就把它们单独提出来,用FP16算,剩下的绝大多数正常值用INT8矩阵乘法算,最后把两部分结果拼回去。
2.1 核心原理拆解:向量级量化与混合精度分解
LLM.int8()其实包含了两层设计。
第一层叫向量级量化(Vector-wise Quantization)。普通量化是给整张权重矩阵算一个scale,LLM.int8()改成给矩阵的每一列单独算一个scale。为什么要这么做?因为权重矩阵的每一列对应一个输出特征,列之间的数值范围差异极大,统一用一个scale会委屈那些数值范围小的列。逐列独立缩放以后,每列的信噪比都能保证,这是它对精度友好的第一步。
第二层才是真正的重点,叫混合精度分解(Mixed-precision Decomposition)。流程是这样的:前向计算时,先看当前这个样本(或者这个token)的激活值,找出哪些特征维度上存在离群点(阈值通常设为6.0,也就是绝对值超过6就认为离群)。然后把这部分离群列对应的权重子矩阵和激活子矩阵分别抠出来,用FP16精度做矩阵乘;剩余那些没有离群点的正常列,就用INT8精度做矩阵乘。两个结果最后在FP16域里相加,得到完整的输出。
这块的实现细节有个坑:离群特征虽然只占很小比例,但你不能简单把它丢掉或者粗暴量化,必须保持它在FP16域的精确计算,否则模型会直接开始胡言乱语。
2.2 八位量化实测与避坑指南
实操中用LLM.int8()非常简单,在Transformers库的from_pretrained里加一个load_in_8bit=True就行。但我强烈建议你配合device_map="auto"使用,因为8bit加载模型时bitsandbytes库会自动做设备分配。
我来说说实测数据。拿一个13B模型,FP16大概要26GB显存,8bit加载后能压到13GB左右,显存占用减半。推理速度方面,在A100 80G上,13B模型用8bit(LLM.int8)的生成速度大约是FP16的1.05到1.1倍。注意,这个加速幅度比理想中低多了,原因就是混合精度分解本身有开销:每次矩阵乘都要判断离群列、做矩阵切片、把结果拼接回去,这些操作的额外耗时抵消了一部分INT8矩阵乘的加速收益。
而且越是小模型,越体现不出优势。7B以下模型,LLM.int8()的速度甚至可能比FP16还慢,因为模型本身计算量不大,切片拼接的CPU/GPU通信开销反而占了主导。所以我的建议是:用这个方案的人,要么模型特别大(重点解决显存放不下的问题),要么机器显存刚好卡在临界点上,否则性价比不高。
避坑提示几个:
- 不要和
torch.compile一起用。torch.compile的图优化和bitsandbytes库的8bit线性层有一定兼容性问题,实测经常会报奇怪的算子错误,或者直接变慢。 - 切换模型到GPU后再用8bit。如果模型原本在CPU上,bitsandbytes会强制要求重新加载到GPU,而且可能触发精度对齐问题。
- 最新版bitsandbytes对多卡环境友好多了,但还是建议你先用单卡跑通,再上多卡,排查问题时能节省大量时间。
2.3 这个方案适合谁用
总结一下:LLM.int8()适合“你有一块24G卡,想跑一个原本26G的模型,又不愿意做GPTQ量化重新生成4bit checkpoint”的过渡场景。好处是无感、无需校准数据集、开箱即用;坏处是加速有限、显存节省没到极致。
3. GPTQ:基于二阶信息的逐层量化补偿
如果说LLM.int8()是在“运行时”做文章,那GPTQ就是在“离线”阶段就把量化误差压到最低。它属于**训练后量化(PTQ)**路线,核心思路来源于一篇更早的论文——OBS(Optimal Brain Surgeon,最优脑外科手术)。那篇论文本来讲的是剪枝,它的思想是:当你移除神经网络里某个权重时,其他权重会产生连锁误差影响,如果知道损失函数的二阶导信息(Hessian矩阵),就能在移除一个权重的同时对其他权重做补偿,把总误差降到最低。
GPTQ把同样的思想移植到了量化上:量化一个权重不是孤立操作,我可以根据Hessian矩阵算出这个量化操作对其他权重的影响,然后同步更新剩余权重,让整体误差最小化。
3.1 量化补偿的数学逻辑
具体来说,GPTQ是对模型逐层(layer-by-layer)量化的。每一层内部,它按列处理权重矩阵:先量化某一列,然后计算这个量化误差对后面所有未量化列的影响,并给这些未量化列加上一个修正量。
这里涉及两个关键概念:
- Hessian矩阵:衡量权重变化对输出误差影响的二阶信息,用校准集(calibration set)里几百条样本的激活值算出来,通常是个几百乘几百的矩阵。它能精确告诉我们“如果把某个权重动一下,其他权重该往哪个方向补”。
- 逐层重建:GPTQ不需要反向传播,也不需要梯度更新,它就是在做“最小化当前层输出误差”的优化。这个优化不带标签、不涉及训练,所以速度极快,一个13B模型在单张A100上半小时到一个小时就能完成全部量化。
实际操作中还有一个提升稳定性的技巧,叫阻尼(dampening)。计算Hessian矩阵逆的时候,为了防止列不满秩导致数值爆炸,GPTQ给Hessian对角线加了一个很小的常数percdamp,常见取0.01。这个参数看着不起眼,但它对量化后模型最终困惑度的影响非常显著,不建议直接改成0。
3.2 实操参数与量化流程记录
用AutoGPTQ库做GPTQ量化的完整流程我跑过很多次,和你分享一下关键步骤。
首先准备环境:
pip install auto-gptq optimum然后加载模型和校准数据。校准数据的质量是整个量化成败的关键,千万不要随便找一段文本就去校准。我常用的做法是从模型的原始训练数据分布里采样,或者至少用和下游任务同领域的、几百条覆盖多样性的文本。
接下来是几组关键参数:
- bits:4是主流,也有用3的(但要接受明显的精度下降),2基本不建议尝试,崩得很快。
- group_size:指权重矩阵分组量化的粒度。常见选择128和32。组越小,量化越精细,精度越好,但显存占用和计算开销也随之上升。实测下来,group_size=128是效果和性能之间最稳的平衡点,绝大多数开源量化模型(比如那些跑的很多的4bit量化版模型)都用这个值。
- desc_act(激活顺序):字面意思是“激活顺序”,实际指是否在量化时考虑激活值的真实数值范围来决定量化顺序。开启后精度确实更高,但代价是推理时需要先对权重矩阵做重排,加载和首次推理明显变慢。我的经验是,如果你只是日常聊天测试,开着问题不大;如果要做高并发推理,
desc_act=False或者选一个不增加额外IO开销的配置可能更合适。
跑量化的时候用类似下面这段代码:
from transformers import AutoTokenizer from auto_gptq import AutoGPTQForCausalLM, BaseQuantizeConfig tokenizer = AutoTokenizer.from_pretrained("你的基础模型路径") calibration_loader = ... # 准备好校准数据 quantize_config = BaseQuantizeConfig( bits=4, group_size=128, desc_act=True, damp_percent=0.01, ) model = AutoGPTQForCausalLM.from_pretrained( "你的基础模型路径", quantize_config=quantize_config, ) model.quantize(calibration_loader) model.save_quantized("量化后的输出目录", use_safetensors=True)3.3 两个容易踩的坑
第一个坑是关于首次加载慢。量化后的模型文件确实小了,但加载到GPU上做from_pretrained的时候,需要把4bit的权重反量化(dequantize)到FP16再进行计算。这个反量化过程如果没用预编译的优化内核,可能非常慢,第一次加载十几二十分钟都正常。这个问题在AutoGPTQ和ExLlama的实现里都有一定程度的优化,但如果你用得很早或者在某些框架里,一定要有这个心理预期。
第二个坑是关于校准集偏差。GPTQ的校准集如果和上线后的实际输入分布差异过大,量化误差会被成倍放大。具体症状就是模型平时看着正常,一遇到特定格式的输入就开始胡言乱语、答非所问。解决办法是准备一份足够贴近真实业务场景的校准集,不要偷懒从网上随便拉几百段新闻混过去。
4. AWQ:激活感知的权重量化
AWQ的全称是Activation-aware Weight Quantization,意思是“激活感知的权重量化”。它和GPTQ在哲学上有一个重要差异:GPTQ觉得量化误差是均匀分布在所有权重上的,我要做的是在量化过程中动态补偿;AWQ则认为,不同权重对最终输出的重要性是不一样的,有些权重通道天然更重要,量化时得“偏心”对待它们。
4.1 如何识别并保护重要权重通道
AWQ最聪明的地方在于,它找到了一个指标来识别哪些权重通道更重要:激活值的大小。如果某个输入特征通道上的激活值普遍很大,那对应的权重通道对输出影响就更大,量化误差在这个通道上会被放大。这个思路直觉上很成立,而且实现成本极低——你只需要跑一遍校准集,统计每个通道激活值的平均值就够了,不需要任何反向传播和微调。
那怎么“偏心”呢?光靠直觉说“这个通道少量化几个bit”是不行的,AWQ的实际做法是给重要通道的权重乘一个缩放系数,同时给激活值除以同一个系数,数学上整个矩阵乘的结果完全不变,但量化误差的分布被改变了。
具体过程是这样:假设原始计算是WX,我找到缩放向量s,把它变成(W·s)(X/s)。直观理解就是:重要通道的权重被放大以后,量化时相对误差占比变小,精度自然更高;虽然激活被缩小了会略微损失一点点激活精度,但只要s选得合理,收益远大于损失。
这里s的选择不是手调的,AWQ用了一个简单的网格搜索或者梯度搜索,目标函数是量化前后输出的均方误差最小。整个搜索过程非常快,13B模型几分钟就能完成。
4.2 与GPTQ的横向对比与选型建议
AWQ和GPTQ是目前最主流的两个4bit W4A16量化方案,我实际对比过很多次,直接说结论:
- 推理速度:两者在成熟推理框架下(如vLLM、TensorRT-LLM、llama.cpp)差距不大,但AWQ在部分硬件上有更高效的kernel支持(因为它的权重量化不需要反向传播和动态补偿矩阵),生成速度有时能比GPTQ快5%到8%。
- 量化速度和成本:AWQ只需要校准集的“前向统计+缩放搜索”,不需要GPTQ那样逐层做Hessian矩阵求逆和补偿更新,所以量化过程快得多,对算力紧张的团队更友好。
- 精度表现:整体在同一水平线,但AWQ对校准数据集的依赖略低一些,因为它用的是“统计值”而不是“全局二阶优化”,理论上更鲁棒。不过在实际体验中,两者在复杂推理任务上差距很小,都属于4bit quantization的一线水平。
我的选型建议很简单:如果你的部署框架已经明确支持某种格式,就优先用那个框架对应的量化方案。举例来说,TensorRT-LLM对AWQ的支持非常完善,那你在NVIDIA显卡上部署就用AWQ;如果团队已经用AutoGPTQ跑通了一套流水线,也没必要为了那点速度差去折腾新格式。
4.3 AWQ实操中的几个细节
用AutoAWQ库做量化的流程大概是:
from awq import AutoAWQForCausalLM model = AutoAWQForCausalLM.from_pretrained("你的基础模型路径") tokenizer = AutoTokenizer.from_pretrained("你的基础模型路径") quant_config = {"zero_point": True, "q_group_size": 128, "w_bit": 4, "version": "GEMM"} model.quantize(tokenizer, quant_config=quant_config) model.save_quantized("量化后的输出目录", safetensors=True)这里的q_group_size同样是128,zero_point=True表示使用对称量化加零点偏移,version选GEMM还是GEMV取决于你的推理场景。
GEMM适合批量推理、吞吐优先的场景(比如服务端的连续多请求)。GEMV适合单请求、低延迟场景(比如在线实时聊天)。
如果你搞混了这个配置,最常见的问题是:模型在并发请求下吞吐上不去,或者单次响应延迟莫名其妙高。排查了半天才发现是量化版本的kernel选错了。
AWQ还有一个好处是,模型量化后不用像GPTQ那样每次加载都做全局反量化,很多推理框架可以直接操作4bit权重矩阵,显存占用和加载速度都更好。
5. SmoothQuant:把量化难题从激活转移给权重
刚才几个方案都是W4A16(权重4bit、激活16bit),核心原因是激活值太“刺头”,离群点太多,很难压到8bit以下。但问题来了:如果我只量化权重、不量化激活,矩阵乘法时权重是低精度、激活是FP16,那计算时还是要把权重反量化回FP16再做FP16乘加,根本发挥不出低精度矩阵乘法的硬件加速能力。
想要真正利用INT8的Tensor Core加速,必须做到W8A8:权重8bit、激活8bit。而卡住W8A8的瓶颈,就在激活值本身。SmoothQuant就是为了解决这个卡点而生的。
5.1 数学迁移的核心思想
SmoothQuant的核心思想,用一句话概括就是:把激活值上的量化难度,通过数学上等价的变换,迁移一部分到权重侧。
具体推导非常漂亮:
设激活为x,权重为W。我们可以构造一个对角缩放矩阵diag(s),然后把计算写成:
Wx = (W · diag(s)^{-1}) · (diag(s) · x)这个式子两边严格相等,不引入任何近似。也就是说,我对激活做的任何缩放,都可以在权重上做相反的缩放来完全抵消。既然激活里有几个通道的离群值特别大,那我就把这些通道的激活缩小;对应地,把权重中相同通道放大。这样激活的整体数值范围被压平了,INT8量化变得可行;而权重虽然数值范围变大了,但权重量化本身比激活量化宽容得多,损失可控。
这里有一个超参数alpha,控制缩放迁移的力度(migration strength)。alpha=0表示完全不迁移,等于没做;alpha=1表示把激活的数值范围完全压到和权重一致。最佳值通常在0.5左右,太大太小都会导致一侧的量化误差失控。
SmoothQuant厉害在它是一个数学等价的迁移,不需要重新训练,只需要校准集统计迁移强度,而且迁移后的权重计算不需要任何额外运行时开销——因为所有缩放都可以预先融合进权重里,推理时正常做W8A8矩阵乘即可。
5.2 实际收益与部署经验
SmoothQuant的收益最直观地体现在吞吐上:真正跑INT8矩阵乘比FP16矩阵乘能快1.5到2倍甚至更多,在TensorRT-LLM、vLLM这些框架里都有现成的支持。
部署时我建议你用Text Generation Inference(TGI)或者TensorRT-LLM,它们对SmoothQuant的集成最完善,基本只需指定quantize=smoothquant相关的参数。如果你用自己写的推理代码,那得注意几个核心步骤:
- 准备校准集,统计每个激活通道的绝对值最大值,算出迁移强度
alpha。 - 用
alpha生成平滑缩放因子s,对模型的权重矩阵和激活分别做缩放。 - 将缩放后的权重直接转成INT8格式保存,推理时加载INT8权重,并按INT8 GEMM的方式计算。
实测下来,SmoothQuant在13B以上模型上的精度损失是非常低的,困惑度(perplexity)通常只增加零点几。它适合的场景是:你需要低延迟高吞吐的在线服务,并且GPU支持高效的INT8计算。如果你的推理框架不支持INT8 GEMM优化,SmoothQuant的收益会大打折扣。
5.3 SmoothQuant的三个判断条件
这里说点实在的,SmoothQuant不是谁想用就能用好的,你得先满足三个条件:
- 你的推理后端必须真正使用INT8 GEMM,而不是INT8权重+FP16计算。
- 你手头有足够的校准数据来统计激活分布,数据质量直接影响平滑效果。
- 你对上线的精度要求足够宽(能接受微小的质量波动)。如果你做的任务对输出质量极其敏感(比如医疗问答、代码生成),W8A8方案的精度损失还是要比4bit方案小得多,但依然要谨慎测试。
6. NF4:从数据格式层面逼近理论最优
最后这个方案和前面四个画风不太一样。前面的方案都是“量化算法”,NF4更多是一种“数据格式”——它定义了一种4bit的数值表示方法,专门用来提升LLM量化后的信息保留能力。它最出名的应用场景就是QLoRA。
6.1 分位数格式的设计逻辑
为什么需要一种新的4bit格式?传统FP4(浮点4bit)只有3个有效数字位,数值范围不小但精度分布不均匀;INT4虽然步长均匀,但对LLM这种长尾分布来说,大量小值会被粗暴归零。NF4(NormalFloat4)的思路是去拟合LLM权重真实分布——近似正态分布。
NF4的设计原理是分位数量化(Quantile Quantization):既然权重大致服从标准正态分布,那我就在[0, 1]区间上均匀地取16个分位点,然后映射到正态分布对应的数值上。这样可以保证:数值落在每个分位区间内的概率相等,量化误差被最小化。
这和我们前面讲过的minmax策略完全不同:minmax关心的是“绝对值范围”,NF4关心的是“概率密度匹配”。这正好符合LLM权重的分布特性——绝大多数权重挤在0附近,极小部分拖在尾部。NF4用自己的分位点把0附近的小值分隔得特别细,精度极高,而尾部大值虽然有粗粒度,但影响相对小。
6.2 QLoRA训练中的实际应用
QLoRA(Quantized LLM with Low-Rank Adaptation)就是建立在NF4之上的。它的核心组合是:
- NF4量化底模:把预训练模型(比如13B甚至70B)用NF4格式加载到GPU上,冻结权重,显存占用直接缩小4倍。70B模型原本要140GB显存,NF4后只需要35GB左右,一张48G的卡(或两张24G卡)就能跑起来。
- Double Quantization:对量化过程中产生的缩放因子(scale)再做一次量化,省掉那部分显存开销。这个操作看着微不足道,但实际能省出几百MB到1GB,对极限显存场景很关键。
- 分页优化器(Paged Optimizer):借鉴操作系统虚拟内存的分页思想,当显存不足时把优化器状态暂时挪到CPU内存,需要时再自动拉回GPU,避免训练中途OOM崩溃。
实操中用QLoRA微调大模型,只要改几行参数:
from transformers import BitsAndBytesConfig bnb_config = BitsAndBytesConfig( load_in_4bit=True, bnb_4bit_quant_type="nf4", # nf4 or fp4 bnb_4bit_use_double_quant=True, # 开启二次量化 bnb_4bit_compute_dtype=torch.bfloat16, # 计算时反量化到bf16 ) model = AutoModelForCausalLM.from_pretrained( "你的基础模型路径", quantization_config=bnb_config, device_map="auto", )这里有几个坑我必须强调:
bnb_4bit_compute_dtype建议用bfloat16而不是float16。实测在推理和训练中,bf16的动态范围更大,避免了fp16在某些数值区间上溢出导致loss变成NaN。load_in_4bit=True后不要直接对底模做任何参数更新,要做微调必须挂LoRA适配器(PeftModel),训练时只有适配器参数参与梯度更新,底模权重始终是冻结的NF4状态。- 4bit加载并不意味着所有算子都变成INT4矩阵乘,NF4推理时通常还是要反量化到
compute_dtype(比如bf16)再做矩阵乘。它真正带来的是显存的大幅度节省,而非计算加速。想明白这一点,你就不会对QLoRA的推理速度抱有不切实际的期望。
6.3 NF4和FP4的实际选择
bitsandbytes库提供了NF4和FP4两种4bit格式,实测下来NF4的精度更好,这也是为什么QLoRA论文把NF4作为默认选择。FP4的优势在于实现简单、和硬件对齐更直接(有些GPU硬件原生支持FP4),但精度损失略大。如果你拿不定主意,无脑选NF4就好。
有一个细节值得留意:NF4的量化是**分块(block-wise)**的,默认块大小是64,也就是说每64个权重共享一个缩放因子。块大小越小,精度越高但显存开销越大;如果你追求极致显存,可以把bnb_4bit_block_size调大,但不要超过默认太多,否则精度损失会迅速累积。
7. 五个方案对比与选型决策参考
花了大篇幅把五种方案逐个讲透,现在该回归到最现实的问题:我到底该用哪一个?我把它们放在一张表里快速对比:
| 方案 | 位宽 | 激活策略 | 校准数据 | 是否需要微调 | 核心优势 | 最适合场景 |
|---|---|---|---|---|---|---|
| LLM.int8 | W8A8 | 混合精度分解 | 不需要 | 否 | 开箱即用 | 临时跑大模型、显存卡点 |
| GPTQ | W4A16 | FP16激活 | 需要几百条 | 否 | 离线精度补偿 | 4bit部署、离线量化生态成熟 |
| AWQ | W4A16 | FP16激活 | 需要几百条 | 否 | 量化速度快、kernel支持好 | 高并发在线服务 |
| SmoothQuant | W8A8 | 平滑后INT8 | 需要几百条 | 否 | 真正INT8加速、精度损失小 | 高吞吐在线服务、低延迟场景 |
| NF4 | W4A16 | FP16激活 | 不需要 | 配合QLoRA | 显存极致压缩 | 单卡训练/微调大模型 |
从大方向上看,各方案的适用边界非常清晰:
- 追求极致推理速度 + GPU支持INT8加速:选SmoothQuant,W8A8才是硬件加速的主力形态。
- 追求显存省到极致 + 推理速度次要:选AWQ或GPTQ,它们都能实现4bit权重加载,而且部署生态相对成熟。
- 既要省显存又要做微调:选NF4+QLoRA,这是目前唯一能把训练级显存占用降下来的方案。
- 不想动量化流程、追求无感使用:选LLM.int8(),虽然加速有限但胜在省心。
如果你觉得方案太多不知道怎么选,有一个更粗暴的经验法则:部署场景选AWQ,没有明确部署框架就选GPTQ,训练微调选NF4,对延迟有强要求且GPU型号较新选SmoothQuant。
8. 高频问题排查清单
最后这部分,全是实际操作中碰过的坑,按方案分类记录下来,省得你到时候一个个去搜问题。
LLM.int8 相关:
模型加载时报CUDA out of memory:先确认device_map="auto"是否生效,再看有没有在模型加载前不小心把其他tensor占用了显存。如果显存确实小,建议直接转4bit方案。
load_in_8bit=True以后模型推理还是占用很多显存:检查一下加载后是否触发了模型的反量化(比如调用了.half()或.float()),有些操作会不经意间把8bit权重解包回FP16,显存瞬间爆炸。
GPTQ 相关:
模型量化后输出明显变差:检查group_size是不是被设成了32甚至更小,检查校准数据是否足够多样,必要时把damp_percent从0.01往0.05方向微调。
加载4bit模型速度极慢:这是反量化开销导致的,优先考虑用ExLlamaV2或者AutoGPTQ的最新kernel,能优化不少。如果首次加载实在不能忍,试试转成AWQ格式,通常加载会快一些。
AWQ 相关:
量化时提示显存不足:AWQ量化过程需要跑校准集的前向传播,建议把校准集分批次加载,避免一次性把几千条样本全塞进显存。
量化后的模型在CPU上无法推理:AWQ的kernel依赖GPU,CPU上跑需要额外的反量化支持,很多推理框架没做这个适配。如果你有CPU推理需求,用GGUF或者GPTQ的CPU方案更现实。
SmoothQuant 相关:
推理速度和FP16相比没有明显提升:确认你的推理后端真的在做INT8 GEMM,很多框架虽然支持SmoothQuant加载,但默认计算路径还是FP16。查一下kernel有没有落到INT8分支。
alpha调高到0.7后精度急剧下降:这通常说明当前模型对权重范围很敏感,迁移过度了。回退到0.5,或者把校准集换得更好一些再试。
NF4 相关:
QLoRA训练时loss震荡或出现NaN:优先检查bnb_4bit_compute_dtype是否为bfloat16,再检查学习率是否偏高。QLoRA微调一般用2e-4左右,比全参数微调低一个量级。
4bit加载后生成结果糟糕:不是所有模型都适合直接4bit加载,有些模型在权重量化后误差传播特别明显。这种时候先试bnb_4bit_quant_type="fp4",如果还不行就退回8bit或换NF4分块大小更大的配置。
通用问题:
模型输出有时胡言乱语、有时正常:大概率是量化误差在某些输入上集中爆发,这通常与校准集和真实输入的分布偏差有关,核心解决思路是重新校准或换量化方案,不存在一步到位的银弹。
不管用哪个方案,记得重复一遍:量化不是免费午餐,每一步都是精度与资源的权衡。上线之前一定找一批有代表性的测试用例,对比量化前后的输出质量,而不是只看显存占用和跑分数据。
结尾
这套五个方案用下来,我自己的体会是:方案本身没有绝对优劣,最重要的是想清楚你的瓶颈在哪。如果模型跑不动是因为显存不够,NF4和4bit方案就是解药;如果是因为推理速度不够,SmoothQuant的W8A8才是真正该投入的方向;如果你只是想快速验证一个想法,LLM.int8()最省事。
最后再分享一个小技巧:不管用哪个方案,都把量化前后的模型输出日志留一份,跑线上之前用几十条真实业务case做一次盲测对比,把质量损失控制在一个可接受范围以后再上线。我见过太多因为量化之后没做质量回归测试,上线就被用户反馈“变笨了”的翻车案例。量化这个领域水很深,但把它当成一套工具箱而不是一道单向门,你会用得更从容。