从 FP32 到 INT4:模型量化实战笔记

从 FP32 到 INT4:模型量化实战笔记
从 FP32 到 INT4模型量化实战笔记一、为什么必须量化显存墙与算力瓶颈大模型落地的第一道门槛永远是硬件成本。一个 70B 参数的模型FP16 精度下需要 140GB 显存单张 A100 80G 根本装不下。就算用张量并行塞进两张卡KV Cache 的显存占用也会限制并发——8K 上下文长度下每个请求的 KV Cache 约占 1.6GB单卡撑死不到 20 个并发。量化能直接压缩存储和计算开销。INT8 让模型体积减半INT4 压缩到原始的 1/4。更重要的是NVIDIA GPU 的 INT8 Tensor Core 吞吐量是 FP16 的 2 倍INT4 甚至能达到 4 倍。这意味着量化不仅省显存还能直接提升算力利用率。但代价也很明确精度损失。一个粗暴的 INT4 量化可能让推理能力下降 20% 以上数学推理、代码生成这些对精度敏感的任务首当其冲。如何在压缩率和精度之间找平衡是量化工程的核心问题。二、量化原理从对称量化到 GPTQ 的误差补偿量化的本质是把连续浮点值映射到离散整数区间。理解映射机制才能看懂精度损失的根源。flowchart TB subgraph 量化流程[模型量化全流程] A[FP32/FP16 原始权重] -- B{选择量化策略} B --|对称量化| C[计算绝对值最大值 amax] B --|非对称量化| D[计算最小值/最大值 min/max] C -- E[缩放因子 S amax / 127] D -- F[缩放因子 S max-min / 255br零点 Z round(-min/S)] E -- G[量化: q round(w / S)] F -- H[量化: q round(w / S) Z] G -- I[INT8 权重 Scale] H -- J[INT8 权重 Scale ZeroPoint] end subgraph 校准流程[校准数据驱动优化] K[校准数据集] -- L[前向传播收集激活分布] L -- M{选择校准算法} M --|MinMax| N[取激活值极值] M --|Percentile| O[取激活值百分位截断] M --|Entropy| P[最小化 KL 散度] M --|GPTQ| Q[逐层 Hessian 最优补偿] end I -- K J -- K对称量化假设权重分布关于零点对称只需要一个缩放因子 Scale。计算公式是q round(w / S)其中S amax / 127INT8或S amax / 7INT4。对称量化的优势是硬件友好——INT8 Tensor Core 的乘加运算不需要零点偏移计算路径最短。非对称量化允许权重分布不对称引入零点 ZeroPoint。计算公式是q round(w / S) Z。非对称量化对偏斜分布比如 ReLU 后的激活值全部为正更友好但反量化时多了加法开销。权重量化相对简单因为权重在推理时是固定的可以离线完成。激活量化就麻烦得多——激活值随输入变化必须在运行时动态量化。这就需要校准Calibration用代表性数据集跑一遍前向传播统计激活值的分布范围据此确定量化参数。GPTQ 是目前最主流的权重量化方案。核心思想是逐层量化权重每量化一个权重列后立即调整同层未量化的权重补偿量化误差。补偿基于 Hessian 矩阵——Hessian 矩阵的逆矩阵编码了每个权重对损失函数的敏感度敏感度高的权重应分配更多量化比特。GPTQ 的数学保证是在给定比特宽度下逐层量化误差最小。AWQActivation-aware Weight Quantization则从另一个角度切入不是所有权重同等重要与激活值幅度大的通道对应的权重更重要。AWQ 通过观察激活分布识别出重要通道对这些通道的权重进行缩放后再量化有效保护关键权重不被截断。三、工程落地GPTQ 与 AWQ 的实践3.1 GPTQ 量化流程from auto_gptq import AutoGPTQForCausalLM, BaseQuantizeConfig from transformers import AutoTokenizer from datasets import load_dataset import torch # 量化配置——4-bit 量化分组大小 128 quantize_config BaseQuantizeConfig( bits4, # 量化比特数 group_size128, # 分组量化每 128 个权重共享一组 Scale desc_actTrue, # 启用激活值排序按重要性排序后量化 damp_percent0.01, # Hessian 对角阻尼防止数值不稳定 symTrue, # 对称量化硬件友好 ) # 加载原始 FP16 模型 model_path /models/Qwen2-72B-Instruct tokenizer AutoTokenizer.from_pretrained(model_path) model AutoGPTQForCausalLM.from_pretrained( model_path, quantize_configquantize_config, torch_dtypetorch.float16, ) # 准备校准数据——512 条代表性文本 dataset load_dataset(wikitext, wikitext-2-raw-v1, splittrain) calibration_data [] for i, example in enumerate(dataset): if i 512: break tokens tokenizer(example[text], return_tensorspt, max_length2048, truncationTrue) calibration_data.append(tokens.input_ids) # 执行量化——耗时约 2-4 小时70B 模型单卡 A100 model.quantize(calibration_data) # 保存量化模型 output_dir /models/Qwen2-72B-Instruct-GPTQ-Int4 model.save_quantized(output_dir) tokenizer.save_pretrained(output_dir)group_size128是关键参数。分组量化将权重向量切分为 128 个一组每组独立计算 Scale。这比全局共享一个 Scale 精度更高因为不同权重区间的分布差异大。但 group_size 越小Scale 参数越多存储开销越大。128 是精度与存储的经验最优平衡点。desc_actTrue启用激活值排序。GPTQ 论文发现按激活值幅度对权重列排序后先量化不重要的列再量化重要的列精度损失更小。代价是量化速度降低约 30%因为需要额外的排序计算。3.2 AWQ 量化流程from awq import AutoAWQForCausalLM from transformers import AutoTokenizer model_path /models/Qwen2-72B-Instruct tokenizer AutoTokenizer.from_pretrained(model_path) # AWQ 量化配置 quant_config { zero_point: True, # 非对称量化适合激活值偏斜分布 q_group_size: 128, # 分组大小 w_bit: 4, # 权重量化比特数 version: GEMM, # 使用 GEMM 内核推理速度更快 } model AutoAWQForCausalLM.from_pretrained(model_path) model.quantize(tokenizer, quant_configquant_config) # 保存 output_dir /models/Qwen2-72B-Instruct-AWQ-Int4 model.save_quantized(output_dir) tokenizer.save_pretrained(output_dir)AWQ 的核心优势是量化速度比 GPTQ 快 2-3 倍因为不需要计算 Hessian 矩阵。AWQ 只需观察激活分布确定重要通道然后对权重做缩放变换计算量远小于 GPTQ 的逐列补偿。3.3 量化精度验证from transformers import AutoModelForCausalLM, AutoTokenizer import torch def evaluate_perplexity(model_path, dataset_namewikitext): 评估量化模型的困惑度——量化精度最核心的指标 tokenizer AutoTokenizer.from_pretrained(model_path) model AutoModelForCausalLM.from_pretrained( model_path, torch_dtypetorch.float16, device_mapauto ) from datasets import load_dataset dataset load_dataset(dataset_name, wikitext-2-raw-v1, splittest) encodings tokenizer(\n\n.join(dataset[text]), return_tensorspt) # 滑动窗口计算困惑度 max_length 2048 stride 512 seq_len encodings.input_ids.size(1) nlls [] for i in range(0, seq_len, stride): begin max(i stride - max_length, 0) end i stride input_ids encodings.input_ids[:, begin:end].to(model.device) target_ids input_ids.clone() with torch.no_grad(): outputs model(input_ids, labelstarget_ids) nlls.append(outputs.loss.item()) import math ppl math.exp(sum(nlls) / len(nlls)) return ppl # 对比 FP16 与 INT4 的困惑度差异 fp16_ppl evaluate_perplexity(/models/Qwen2-72B-Instruct) gptq_ppl evaluate_perplexity(/models/Qwen2-72B-Instruct-GPTQ-Int4) awq_ppl evaluate_perplexity(/models/Qwen2-72B-Instruct-AWQ-Int4) print(fFP16 PPL: {fp16_ppl:.2f}) print(fGPTQ-Int4 PPL: {gptq_ppl:.2f} (退化 {gptq_ppl - fp16_ppl:.2f})) print(fAWQ-Int4 PPL: {awq_ppl:.2f} (退化 {awq_ppl - fp16_ppl:.2f}))困惑度Perplexity是量化精度最核心的评估指标。经验阈值INT4 量化的 PPL 退化应控制在 0.5 以内超过 1.0 则说明量化策略需要调整。四、量化的代价边界精度退化与硬件依赖的隐性成本量化不是免费的午餐每降低一个比特都有对应的代价。INT4 权重量化的精度退化是不可避免的。在数学推理GSM8K和代码生成HumanEval任务上INT4 相比 FP16 的准确率下降通常在 2%-5%。这个退化在小模型 7B上更严重因为小模型的权重冗余度低量化容错空间小。对于 7B 以下的模型INT8 量化通常是更安全的选择。分组量化的 Scale 参数增加了存储和访存开销。group_size128 时每 128 个权重额外存储 1 个 Scale 值开销约 0.8%。但更关键的是反量化时需要从显存读取 Scale 值增加了访存次数。在 Decode 阶段访存密集型这个开销可能抵消部分算力增益。GPTQ 的desc_actTrue虽然提升了量化精度但量化后的权重顺序被打乱推理时需要额外的索引操作。某些推理引擎如早期的 TensorRT-LLM 版本不支持 desc_act 排序导致必须关闭此选项精度退化约 0.1-0.3 PPL。AWQ 的缩放变换在保护重要权重的同时可能放大不重要权重的量化噪声。在权重分布极端偏斜的层如 LayerNorm 附近的投影层AWQ 的精度可能不如 GPTQ。量化方案显存节省推理加速PPL 退化量化耗时适用模型规模INT8 对称50%1.5-2x 0.1 1h所有规模GPTQ-Int475%2-3x0.2-0.52-4h 7BAWQ-Int475%2-3x0.3-0.61-2h 7BGPTQ-Int4 (desc_act)75%2-3x0.1-0.33-5h 13B五、总结模型量化是大模型从实验室走向生产的关键工程手段。核心决策不是要不要量化而是量化到什么程度、用什么方案。落地路线建议第一步对 7B 以下模型使用 INT8 量化精度退化可控工程复杂度最低。第二步对 7B 以上模型使用 GPTQ-Int4开启 desc_act在精度与压缩率之间取得最优平衡。第三步如果量化速度是瓶颈如需要频繁量化新模型切换到 AWQ-Int4牺牲约 0.1 PPL 换取 2-3 倍的量化加速。每一步量化都必须用困惑度和下游任务准确率双重验证。仅看 PPL 不够——PPL 退化 0.3 可能对应数学推理准确率下降 5%而 PPL 退化 0.5 可能对应代码生成几乎不受影响。量化验证必须面向目标场景而非通用指标。