把模型权重压进 4 bit
Day 31-34 讨论了 serving 系统如何管理 KV Cache、调度请求和复用前缀。 Day 35 切到另一条推理优化主线:权重量化。 目标不是让模型“变聪明”,而是把模型权重从 FP16/BF16 压到 INT8/INT4, 让单卡能放下更大的模型,让内存带宽压力下降,并在有合适 kernel 时提升吞吐。 今天重点理解 INT8 / INT4、GPTQ、AWQ, 最后用 AutoGPTQ 或 llama.cpp 完成一次可复现的量化与评估。
思维导图
为什么先做权重量化
推理时,模型权重通常是最大的一块静态显存。 对一个 7B 模型,FP16 权重大约 14GB;如果权重压到 4 bit,理论权重主体约 3.5GB, 再加上 scale、zero point、packing metadata 和运行时 buffer。 这会直接影响能否单卡加载、能否留下更多显存给 KV Cache,以及 decode 阶段读权重时的带宽压力。
7B 权重主体约 14GB,13B 约 26GB,70B 约 140GB。实际还要算 tokenizer、buffer 和框架开销。
4 bit 不是“精确四分之一总显存”,因为每组权重还需要 scale / zero point 等元数据。
量化文件变小不等于一定更快。若 kernel 需要频繁反量化或没有 Tensor Core / SIMD 优化,收益会被吃掉。
更大模型放进同一张卡
权重量化最直接的收益是降低模型常驻显存,让 7B/13B/34B 在更小设备上运行。
decode 更少搬权重
decode 每步 batch 小、读权重频繁,低 bit 权重可降低内存带宽压力,前提是 kernel 真的高效。
低 bit 会引入误差
量化会改变线性层输出。校准集、group size、保留哪些层高精度,都会影响 perplexity 和下游任务。
INT8 / INT4 基础
量化的基本动作是把浮点权重映射到整数格点。 反推理时,整数权重需要和 scale 一起恢复为近似浮点值,或在 mixed-precision kernel 中边乘边反量化。 LLM 权重量化常见的是 weight-only quantization:权重低 bit 存储,激活仍用 FP16/BF16。
非对称量化带 zero point。适合范围不以 0 为中心的分布,但硬件实现可能更复杂。
反量化后的权重近似原权重。量化误差会通过线性层传到 logits。
group size 越小,scale 越细,质量通常更好;但 metadata 更多,packing / kernel 也更复杂。
toy symmetric quantization
import torch def quantize_symmetric(x, bits=4, group_size=128): qmax = 2 ** (bits - 1) - 1 qmin = -2 ** (bits - 1) flat = x.flatten() qs, scales = [], [] for start in range(0, flat.numel(), group_size): g = flat[start:start + group_size] scale = g.abs().max().clamp_min(1e-8) / qmax q = torch.round(g / scale).clamp(qmin, qmax).to(torch.int8) qs.append(q) scales.append(scale) return torch.cat(qs), torch.stack(scales) def dequantize_symmetric(q, scales, shape, group_size=128): chunks = [] for i, scale in enumerate(scales): chunks.append(q[i * group_size:(i + 1) * group_size].float() * scale) return torch.cat(chunks).reshape(shape)
| 方式 | 特点 | 优点 | 风险 |
|---|---|---|---|
| INT8 | 8 bit 权重,通常质量更稳。 | 误差小,部署风险低。 | 压缩率不如 INT4,速度收益依赖 kernel。 |
| INT4 | 4 bit 权重,LLM 本地推理很常见。 | 显存下降明显,适合单卡/边缘设备。 | 质量更依赖校准、group size 和方法。 |
| per-channel | 每个输出通道有独立 scale。 | 比 per-tensor 更能适配不同通道范围。 | metadata 增加,实现复杂一点。 |
| group-wise | 每组若干权重共享 scale,如 group size 128。 | 质量和体积折中较好。 | group size 选错会影响精度或吞吐。 |
GPTQ 与 AWQ
直接 round-to-nearest 通常不够好。 GPTQ 和 AWQ 都是 post-training quantization,不需要重新训练完整模型, 但会用少量校准数据判断哪些误差更重要。 粗略说,GPTQ 试图逐层最小化量化后的重构误差;AWQ 则用激活分布识别重要通道, 通过缩放保护 salient weights,尽量让 4 bit 权重保留指令模型的质量。
用近似二阶信息补偿误差
GPTQ 是 one-shot weight quantization。它用校准样本估计层输入分布,逐层量化权重,并用近似 Hessian 信息补偿已量化权重带来的输出误差。
用激活识别重要权重通道
AWQ 认为权重本身大小不是唯一信号,应该看真实激活会放大哪些通道。它保护少量 salient channels,常用于 4-bit weight-only 部署。
| 方法 | 关键输入 | 典型配置 | 适合场景 |
|---|---|---|---|
| RTN | 只看权重范围。 | round(w / scale) | 教学、快速基线,质量通常不是最好。 |
| GPTQ | 校准样本的层输入分布。 | bits=4, group_size=128, act_order/desc_act | GPU 侧 4-bit GPTQ 格式,常用于 Transformers / ExLlama / Marlin 路线。 |
| AWQ | 激活统计与重要通道。 | w_bit=4, q_group_size=128 | 指令模型、多模态模型、对泛化稳定性要求较高的部署。 |
| GGUF k-quants | llama.cpp 的 tensor packing 规则,可选 imatrix。 | Q4_K_M, Q5_K_M, Q8_0 | CPU / Metal / Vulkan / 边缘设备上的 llama.cpp 生态。 |
动手一:AutoGPTQ 路线
AutoGPTQ 是理解 GPTQ 流程的好入口:准备校准样本,配置 bits/group_size,
加载 FP16 模型,执行 model.quantize(examples),保存量化权重。
真实项目里要注意维护状态、模型支持表、CUDA/PyTorch 版本和推理 kernel 是否匹配。
from transformers import AutoTokenizer from auto_gptq import AutoGPTQForCausalLM, BaseQuantizeConfig pretrained_model_dir = "facebook/opt-125m" quantized_model_dir = "opt-125m-gptq-4bit" tokenizer = AutoTokenizer.from_pretrained(pretrained_model_dir, use_fast=True) examples = [ tokenizer("Quantization compresses LLM weights for inference."), tokenizer("Calibration samples should look like serving prompts."), ] quantize_config = BaseQuantizeConfig( bits=4, group_size=128, desc_act=False, # True 可能更准,但推理/兼容性需验证 ) model = AutoGPTQForCausalLM.from_pretrained( pretrained_model_dir, quantize_config, device="cuda:0", ) model.quantize(examples) model.save_quantized(quantized_model_dir, use_safetensors=True) tokenizer.save_pretrained(quantized_model_dir)
选校准集
尽量接近线上 prompt 分布。通用模型可用 C4/WikiText 子集,业务模型用真实脱敏样本。
先量小模型
用 125M/1B 级模型跑通流程,再量 7B/13B,避免一次量化卡在环境和显存问题上。
记录配置
保存 bits、group_size、desc_act、seqlen、校准样本数、CUDA/PyTorch/AutoGPTQ 版本。
重新评估
量化后必须跑 perplexity、任务集、人工样例和 latency,不能只看模型能加载。
动手二:llama.cpp / GGUF 路线
llama.cpp 的常见流程是两步:
先把 Hugging Face 模型转换为高精度 GGUF,再用 llama-quantize
生成 Q4/Q5/Q8 等量化 GGUF。这个路线特别适合本地推理、CPU/Metal/Vulkan 或边缘设备部署。
# 1. 安装 Python 依赖 python3 -m pip install -r requirements.txt # 2. 从 Hugging Face repo 转成高精度 GGUF python convert_hf_to_gguf.py \ --outfile model-bf16.gguf \ --outtype bf16 \ --remote meta-llama/Llama-3.2-1B-Instruct # 3. 量化成常用 Q4_K_M ./build/bin/llama-quantize \ model-bf16.gguf \ model-Q4_K_M.gguf \ Q4_K_M # 4. 运行量化模型 ./build/bin/llama-cli \ -m model-Q4_K_M.gguf \ -p "Explain weight quantization in one paragraph." \ -n 128
| GGUF 类型 | 大致定位 | 建议 |
|---|---|---|
| Q8_0 | 接近高质量低风险量化。 | 作为质量对照或空间还够时使用。 |
| Q5_K_M | 质量和体积折中偏质量。 | 对回答稳定性要求较高时优先试。 |
| Q4_K_M | 本地推理常见 4-bit 折中。 | Day35 推荐先用它跑完整流程。 |
| Q2 / Q3 | 极限压缩。 | 除非设备非常受限,否则要谨慎评估质量下降。 |
量化后怎么评估
量化评估要同时看三类指标: 质量有没有明显下降,性能有没有真实提升,资源占用是否达成目标。 只看模型文件变小会漏掉最关键的问题:低 bit 权重可能造成困惑度上升、输出格式不稳, 也可能因为 kernel 不匹配而没有速度收益。
| 维度 | 指标 | 怎么测 | 通过标准 |
|---|---|---|---|
| 质量 | perplexity、MMLU/GSM8K、业务 golden set、人工样例。 | 同一 tokenizer、同一 prompt、同一 decoding 参数对比 FP16。 | 重要任务下降在可接受范围内,格式和拒答行为无明显异常。 |
| 性能 | TTFT、TPOT、tokens/s、并发吞吐。 | 固定 batch、context、输出长度,分别测 prefill 和 decode。 | 速度收益来自真实 serving workload,不是单条样例偶然变快。 |
| 资源 | 模型文件大小、加载后显存、KV cache 可用空间。 | 记录 nvidia-smi / engine memory report / llama.cpp 日志。 | 权重节省转化为更大 batch、更长 context 或更低硬件成本。 |
报告模板
model: Llama-3.2-1B-Instruct baseline: bf16 quantized: Q4_K_M / GPTQ-4bit-g128 calibration: samples: 128 max_seq_len: 2048 source: mixed chat prompts quality: ppl_fp16: 8.42 ppl_quant: 8.83 golden_pass_rate_fp16: 94.0% golden_pass_rate_quant: 92.5% performance: peak_vram_fp16: 3.1 GB peak_vram_quant: 1.4 GB tpot_fp16: 28.4 ms tpot_quant: 17.9 ms decision: accept: yes reason: quality drop acceptable, memory target met, decode faster
常见误区
| 误区 | 为什么不准确 | 正确做法 |
|---|---|---|
| 4-bit 一定快 4 倍 | 权重读写减少,但反量化、packing、kernel 和 batch shape 都会影响速度。 | 分别测 prefill、decode、并发吞吐和显存。 |
| 文件能加载就算成功 | 加载成功只说明格式和 kernel 兼容,不说明质量可用。 | 跑 perplexity、golden set 和真实 prompt。 |
| 校准集随便选 | GPTQ/AWQ 都依赖校准样本反映激活或层输入分布。 | 校准集要尽量接近线上场景,并记录采样方式。 |
| 权重量化解决所有显存问题 | 长上下文 serving 的 KV Cache 仍可能是大头。 | Day36 继续看 KV Cache 与激活量化,结合 PagedAttention 管理。 |
常见疑问
Q1 权重量化和 KV Cache 量化有什么区别? +
权重量化压的是模型参数,通常是离线完成,模型加载后一直使用。KV Cache 量化压的是推理过程中动态产生的 K/V,会影响长上下文和高并发时的显存。Day35 只讲权重,Day36 会讲 KV 与激活。
Q2 GPTQ 和 AWQ 谁更好? +
没有固定答案。GPTQ 路线成熟,生态里有很多 kernel 和模型格式;AWQ 对 instruction-tuned、多模态和泛化场景常有不错表现。最终取决于模型、校准集、目标硬件和推理引擎支持。
Q3 为什么 group size 常见 128? +
它是质量、元数据开销和 kernel 实现之间的工程折中。更小 group 通常误差更小,但 scale 更多;更大 group 元数据少,但不同权重分布被迫共享同一 scale,误差可能更大。
Q4 什么时候不应该量化到 INT4? +
如果任务对细微数值很敏感、模型很小、线上要求严格可复现,或者目标引擎没有高效 INT4 kernel,INT8、FP8 或 BF16 可能更稳。量化是部署选择,不是越低越高级。
复盘问题
- 为什么 weight-only quantization 能降低 decode 阶段的内存带宽压力?
- scale、zero point、group size 分别控制什么?group size 变小会带来什么代价?
- GPTQ 为什么需要校准样本?它试图最小化什么误差?
- AWQ 为什么要看 activation 而不只看 weight magnitude?
- 量化模型上线前,你会用哪些质量、性能和资源指标做验收?
今日检查清单
- 能解释 INT8 / INT4 权重量化的 scale、zero point 和反量化公式。
- 能区分 per-tensor、per-channel、group-wise quantization。
- 能说明 GPTQ 的逐层重构误差思路和 AWQ 的激活感知通道保护思路。
- 能用 AutoGPTQ 或 llama.cpp 跑通一次小模型量化。
- 能记录 bits、group_size、校准集、版本、目标硬件和推理 kernel。
- 能输出一张量化前后质量、延迟、吞吐、显存的对比表。
推荐阅读
GPTQ: Accurate Post-Training Quantization for Generative Pre-trained Transformers
GPTQ 原论文。重点读 one-shot、近似二阶信息和 3/4-bit 权重量化实验。
AWQ: Activation-aware Weight Quantization for LLM Compression and Acceleration
AWQ 原论文。重点读 salient weights、activation-aware scaling 和 4-bit weight-only 部署动机。
Day 36 预告
KV Cache 与激活量化
下一天会从静态权重走向动态数据: FP8 KV Cache、SmoothQuant、per-channel vs per-token activation quantization, 以及为什么长上下文 serving 里 KV Cache 量化和权重量化同样重要。