4bit量化最近几乎是每个搞大模型部署的人都会碰到的关键词。你不是第一个在 8GB 显卡上跑 7B 模型的人,但很可能你是那个满心欢喜用 4bit 加载模型、结果生成出来全是胡话的人。这时候你很容易得出一个结论:4bit 量化是坑,谁用谁翻车。但我更愿意把这个结论再往前推一步:4bit 本身不背锅,真正坑人的是量化方法、校准数据、group size、kernal 支持这些更细节的东西。把这几件事搞明白,4bit 完全可以在大多数消费级显卡和边缘设备上稳定跑起来。
这篇文章会围绕模型量化展开,重点讲清楚两件事:一是 4bit 量化到底为什么容易“翻车”,二是像 AWQ 这类激活感知量化方法、以及你在某些工具链里会看到的 oq4e 这类格式,究竟处在什么位置。读完你会有一个明确的判断框架:什么场景该用哪种量化、怎么验证效果、翻车之后从哪里排查。文章里给出的代码和命令,也会让你从“看热闹”快速切换到“能落地”。
1. 这篇文章真正要解决的问题
如果你只是想知道“怎么把 7B 模型塞进显卡里”,8bit 通常就够用了。但当你发现 8bit 还是塞不下,或者想在更小的显存、纯 CPU、甚至手机上跑模型时,你就必须面对 4bit。
问题在于,4bit 并不是一个统一概念。它可能是权重 4bit、激活 8bit 的方案,也可能是权重和激活都是 4bit 的方案;它可能是离线量化后的静态格式,也可能是运行时动态量化;它背后可能是 GPTQ 的误差重建,也可能是 AWQ 的激活感知缩放,还可能是 GGUF 里混合精度的 Q4_K_M。同样叫 4bit,底层格式可能完全不兼容,效果差异也能大到离谱。
这篇文章要解决的痛点,其实就三个:
- 理解 4bit 量化翻车的真正原因,而不是归咎于“量化垃圾”。
- 掌握 GPTQ、AWQ、NF4、HQQ、GGUF 这些主流方案的区别和适用场景。
- 拿到一套能直接复用的 4bit 部署流程:加载、量化、验证、排错。
如果你正在做大模型推理部署、QLoRA 微调,或者只是想在个人电脑上跑一个开源模型,这篇文章都适合你。如果你是搞算法研究、想从量化误差的数学原理深入,我也会在概念部分给你足够的入口。
2. 量化基础:4bit 到底改变了什么
要理解 4bit 为什么容易出问题,先要看清楚量化做了什么。
大模型默认用 FP16/BF16 保存权重,每个权重占 2 字节。量化到 4bit 后,每个权重只占 0.5 字节,理论显存占用能降到八分之一(因为从 16bit 变到 4bit,是 4 倍下降,但还要考虑 scale、zero point 等额外开销,实际达不到理论值)。
量化的基本公式是:
x_q = clamp(round(x / scale) + zero_point, qmin, qmax) x_deq = (x_q - zero_point) * scale其中scale是缩放因子,zero_point是零点偏移,qmin和qmax是量化后的整数范围。4bit 一般对应 16 个整数档位,比如[-8, 7]或者[0, 15]。
量化误差来自两步:第一步是除以scale后的四舍五入,第二步是clamp造成的大数值截断。在 8bit 时,256 个档位通常能覆盖权重分布;到 4bit 只剩 16 个档位,任何一端选错,误差都会被放大好几倍。
这里有个新手最容易误解的点:很多人以为 4bit 只是“用更少空间存权重”,但推理时如果你没有支持 4bit 矩阵乘法的硬件或 kernel,系统还是会把 4bit 权重解量化回 FP16 再计算。这样一来,显存确实降了,但速度可能反而更慢。这也是后面要反复强调的“存储位宽”和“计算位宽”的区别。
另一个关键概念是“量化粒度”:
- per-tensor:整层权重共用一个 scale,实现简单,但低比特下误差最大。
- per-channel:每个输出通道一个 scale,适合权重分布差异大的情况。
- per-group:把通道再分组,常见 group size 是 128 或 256,每组单独算 scale。这是当前主流 4bit 方案的基本单位。
group size 越小,量化越精细,但额外存储的 scale 也越多,推理时计算 overhead 也越大。通常 group size 128 是性价比最高的起点。
单纯做 4bit 舍入,也就是 round-to-nearest(RTN),在 8bit 下效果还行,但在 4bit 下基本等于把模型的表达能力削掉一大块。这就是很多人“随手一个 4bit 量化”会翻车的第一个原因。
3. 4bit 翻车的四类典型现场
先说结论:4bit 翻车不是一种现象,而是四类完全不同的问题。
第一类是输出胡言乱语。模型能加载,但生成的内容明显语无伦次,甚至中英文混杂、重复、断句错乱。这类问题多半出在量化方法上:你可能用了最朴素的 RTN,没有误差重建;或者校准集和你的使用场景严重不匹配。AWQ、GPTQ 这类方法之所以能稳住,并不是因为它们“魔法”,而是它们引入了误差补偿和激活感知保护。
第二类是精度下降不明显,但速度反而变慢。很多人以为量化后一定更快,其实不然。如果你的 GPU 不支持 INT4 矩阵乘,或者推理框架把 4bit 权重当作 FP16 去跑,那计算量并没有下降,只是显存小了。这时你会看到 GPU 利用率很高,但 tokens/s 还不如 FP16。遇到这种问题,要看 kernel 是否真的走了 int4 GEMM path。
第三类是同一个 4bit 文件,换一个推理框架就崩。这个问题在 GGUF、AWQ、GPTQ 之间特别常见。它们在底层打包格式上完全不同,不能交叉加载。你在 llama.cpp 里量化出的 Q4_K_M 文件,不能直接丢给 vLLM 用;你在 AutoAWQ 里量化出的模型,也不是所有框架都能认。更隐蔽的是,有些框架只支持特定 group size 或量化版本,加载时不会报错,但权重被错误解读,精度直接崩盘。
第四类是任务 A 正常,任务 B 崩。4bit 对不同任务敏感度差别很大。日常闲聊可能问题不大,但代码生成、数学推理、长文本检索这类任务对权重的信息密度要求更高。这是因为这些任务更依赖模型中的某些“关键通道”,而 4bit 量化最容易破坏的恰恰是这些通道。
以上四类问题,很多时候同时出现。要避免它们,不能只看“有没有量化成功”,而是要从原理上理解:量化过程中究竟牺牲了什么,用什么方案可以补回来。
4. 翻车的本质:离群值与量化粒度
为什么 LLM 的量化比传统 CNN 更麻烦?一个重要原因是 LLM 激活值里存在大量离群值(outlier)。
通俗解释:模型每一层的输出中,某些 channel 的值可能比其他 channel 大几十倍甚至上百倍。这些 channel 携带重要语义信息,比如注意力计算中的关键 token 特征。如果你做 per-tensor 量化,一个离群值就会把整个 scale 拉得很大,其他正常 channel 落在很少的量化档位里,精度自然崩盘。
可以做个小实验思路:假设某个 tensor 的绝大多数值在[0, 1]之间,但有一个值是100。如果按全范围[0, 100]定 scale,那么0.5这个值可能被量化成0,误差极大。如果忽略那个离群值,又会造成截断误差,但整体误差反而更可控。AWQ 的核心思想之一,就是识别这些关键通道,在量化前给它们一点额外的保护。
从数学上说,4bit 量化是下面这个优化问题:
argmin || WX - Q_4(W) X ||^2最简单的 RTN 是逐权重做舍入,完全不管整体误差。GPTQ 则用 Hessian 信息做逐层重建:量化某一列权重后,用它造成的误差去微调其他列,尽量让WX的输出误差最小。AWQ 的做法则是找激活分布中重要的 channel,在量化前把它们的 scale 调一下,相当于对权重做一次重排,再套 group-wise 量化。这样既不需要反向传播,也不需要太多校准数据。
所以,如果你只看表面,以为 4bit 就是把 float16 转成整数,那一定会翻车。真正的量化工程,是在做“用 16 个整数档位去近似一个 16bit 浮点数的分布,同时保住模型输出分布不变”。这件事的难度,远大于简单类型转换。
另外,关于标题里提到的oq4e,也可以放在这个语境里理解。公开资料里,oq4e并不是像 GPTQ、AWQ 这样有明确定义的通用算法名称,它更像是某个工具链或项目内部的私有格式标识。遇到这种名词时,你的第一反应不是去猜它好不好用,而是去查它对应的权重位宽、group size、scale 存储方式和 kernel 支持范围。基于这些信息,才能判断它和 AWQ 这类方案是否真的可以比较。
5. 主流 4bit 方案解析:GPTQ、AWQ、NF4、HQQ、GGUF
现在我们把视野拉高一点,看主流方案各自的定位。
5.1 GPTQ:训练后量化的老牌选手
GPTQ 是多年前提出的 PTQ(post-training quantization)方案,思路是对每一层做误差重建,利用二阶 Hessian 信息来补偿量化误差。量化过程需要提供校准文本,一般 128~256 条就够。量化完成后得到的是真正低比特权重,推理时不需要再重算太多东西。
优点:精度好、生态成熟、社区资料多。缺点:校准集如果和任务场景差距大,效果会打折扣;量化过程需要一定算力;不同实现之间的 kernel 差异也可能影响速度。
5.2 AWQ:激活感知量化,兼顾速度与精度
AWQ(Activation-aware Weight Quantization)的核心贡献是把“激活值分布”也纳入量化决策。它发现,LLM 中只有极少数 channel 是关键通道,这些通道的激活值特别大。与其对所有权重一视同仁地量化,不如给这些关键通道单独配一个缩放因子,让它们在量化时少受损伤。
AWQ 不需要反向传播,量化速度快,校准数据可以很少。目前它在 Hugging Face、TensorRT-LLM、vLLM、llama.cpp 等生态中都有实现,是部署侧最常见的选择之一。对大多数 7B、13B、70B 模型来说,AWQ 在 4bit 下都能做到“精度可接受 + 推理速度好”的平衡。
5.3 NF4:QLoRA 微调的好朋友,但不是通用推理最优解
NF4(Normal Float 4)是 QLoRA 里采用的 4bit 数据类型。它根据权重分布的分位数设计出 16 个量化档位,使量化误差在正态分布假设下更小,同时配合双重量化(double quantization)把 scale 也压到 8bit。
注意,NF4 最成熟的场景是参数高效微调,而不是纯推理部署。原因是它的打包格式和 kernel 支持在推理链路上不如 GPTQ/AWQ GGUF 那么普及。如果你是用load_in_4bit=True做 QLoRA,选 NF4 是对的;如果要部署到线上推理框架,优先考虑 AWQ 或 GPTQ。
5.4 HQQ:不需要校准集的快速方案
HQQ 走的是另一条路:用半二次优化直接求解使量化误差最小的 scale 和 zero point,不需要校准数据。优点是可以离线快速量化,适合那些拿不到高质量校准语料的场景。代价是精度通常要落后于 GPTQ/AWQ 一点点,具体差距看模型和任务。
5.5 GGUF 里的 Q4_K_M:打包格式和混合精度的选择
GGUF 是 llama.cpp 生态的模型打包格式,不是一种量化算法。里面Q4_0、Q4_K_S、Q4_K_M等后缀才是真正的量化类型。Q4_K_M代表“4bit 混合精度 K-quant”,对关键部分使用更高精度,整体效果比Q4_0更稳。如果你在 CPU 或苹果芯片上跑模型,GGUF 几乎是最省心的选择。
下面这张表可以帮助你做粗选:
| 方案 | 是否需要校准集 | 是否需要反向传播 | 典型场景 | 关键特点 |
|---|---|---|---|---|
| RTN | 否 | 否 | 快速试跑 | 简单,4bit 下容易翻车 |
| GPTQ | 是 | 否 | 离线量化 + 推理部署 | 二阶误差重建,精度好 |
| AWQ | 是(少量文本) | 否 | 离线量化 + 推理部署 | 激活感知,保护显著通道 |
| NF4 | 否 | 配合微调使用 | QLoRA 微调 | 分位数量化,不主打通用推理部署 |
| HQQ | 否 | 否 | 无校准集快速量化 | 迭代优化,速度快 |
| GGUF Q4_K_M | 否 | 否 | CPU/边缘设备部署 | 混合精度打包格式,跨平台友好 |
6. 实操:三种常见 4bit 落地路径
下面进入实际操作部分。我会提供三种路径:第一种是用bitsandbytes加载 4bit 模型,适合快速验证和 QLoRA 微调;第二种是用 AutoAWQ 做离线量化,适合部署到主流推理框架;第三种是用 llama.cpp 转成 GGUF,适合本地化、CPU 或苹果芯片设备。
6.1 路径一:bitsandbytes + transformers 加载 4bit
这种方式不需要手工量化,直接从 Hugging Face 加载时自动转成 4bit。它的缺点是推理 kernel 不一定最优,更适合快速试跑或微调。
import torch from transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig quantization_config = BitsAndBytesConfig( load_in_4bit=True, bnb_4bit_quant_type="nf4", # 可选 "fp4",nf4 通常更稳 bnb_4bit_compute_dtype=torch.bfloat16, bnb_4bit_use_double_quant=True, ) model_id = "meta-llama/Llama-2-7b-chat-hf" model = AutoModelForCausalLM.from_pretrained( model_id, quantization_config=quantization_config, device_map="auto", torch_dtype=torch.bfloat16, ) tokenizer = AutoTokenizer.from_pretrained(model_id) inputs = tokenizer("请用中文解释一下什么是量化。", return_tensors="pt").to("cuda") out = model.generate(**inputs, max_new_tokens=50) print(tokenizer.decode(out[0], skip_special_tokens=True))说明几点:
bnb_4bit_quant_type="nf4"是默认推荐,分位数设计对权重分布更友好。bnb_4bit_compute_dtype=torch.bfloat16建议和你平时训练/推理精度保持一致,不要用 fp32,否则可能引入额外开销和精度问题。- 如果你的 GPU 是老架构,
device_map="auto"可能会把部分层放 CPU,速度会非常慢。最好先确认显存足够。
这种加载方式的优点是省事,缺点是模型没有经过离线误差重建,表现不如 GPTQ/AWQ 那么稳定。如果你只是想在 8GB 显存上跑一个 demo,代码是够用的。
6.2 路径二:AutoAWQ 离线量化
AutoAWQ 是落地部署比较常用的量化工具。这里展示一个最小量化流程。注意 API 细节会随版本更新,下面的代码对应当前主流版本逻辑。
from awq import AutoAWQForCausalLM from transformers import AutoTokenizer model_dir = "meta-llama/Llama-2-7b-chat-hf" quant_dir = "llama2-7b-awq" quant_config = { "zero_point": True, "q_group_size": 128, "w_bit": 4, "version": "GEMM", } # 加载原始模型 model = AutoAWQForCausalLM.from_pretrained(model_dir, device_map="auto") tokenizer = AutoTokenizer.from_pretrained(model_dir, trust_remote_code=True) # 量化并保存 model.quantize(tokenizer, quant_config=quant_config) model.save_quantized(quant_dir) print(f"量化完成,模型保存在 {quant_dir}")量化完成后,推理时加载量化模型:
quant_model = AutoAWQForCausalLM.from_quantized( quant_dir, device_map="auto", max_memory={0: "7GiB"}, )AutoAWQ 的核心在于quant_config。q_group_size=128是推荐起点,如果显存极其紧张,可以试 256,但精度风险变大。zero_point保持True能保证非负权重也能被正确表示。
用 AutoAWQ 量化出的模型,在支持 AWQ kernel 的推理框架(如 vLLM、TensorRT-LLM、SGLang)里,速度和精度均有不错表现。
6.3 路径三:llama.cpp 转 GGUF,CPU 也能跑
如果你没有 NVIDIA 显卡,或者想在 Mac、树莓派、老电脑上跑,llama.cpp 是最成熟的方案。核心流程是先转 FP16 GGUF,再量化为 Q4_K_M。
# 1. 拉取 llama.cpp 代码并编译 git clone https://github.com/ggml-org/llama.cpp.git cd llama.cpp cmake -B build -DCMAKE_BUILD_TYPE=Release cmake --build build --config Release -j # 2. 把 HF 模型转成 FP16 GGUF python convert_hf_to_gguf.py /path/to/hf-model \ --outfile /path/to/model-f16.gguf \ --outtype f16 # 3. 量化为 Q4_K_M ./build/bin/llama-quantize \ /path/to/model-f16.gguf \ /path/to/model-q4_k_m.gguf \ q4_k_m转换完成后,可以用llama-cli直接测试:
./build/bin/llama-cli \ -m /path/to/model-q4_k_m.gguf \ -p "请用中文解释什么是4bit量化。" \ -n 50llama.cpp 的命令行参数在不同版本间变化不大,但具体子命令名可能不同。如果找不到convert_hf_to_gguf.py,先看仓库README.md。q4_k_m中的_k_表示 K-quants 混合精度,比q4_0更稳,是本地部署的默认推荐。
7. 运行结果与效果验证
量化做完,不能只看“模型能跑”就结束。你还需要回答三个问题:
- 量化后的模型回答质量是否还能接受?
- 显存和吞吐有没有达到预期?
- 特定任务上是否出现了不可接受的回退?
最简单的冒烟测试是让量化模型和 FP16 模型生成同一批 prompt,人工对比。更可靠的方式是计算验证集上的困惑度(perplexity)。困惑度越低,说明模型对文本分布的拟合越好。下面是一个用 transformers 计算简单困惑度的示例:
import torch from transformers import AutoModelForCausalLM, AutoTokenizer model_id = "/path/to/your/quantized-model" model = AutoModelForCausalLM.from_pretrained(model_id, device_map="auto") tokenizer = AutoTokenizer.from_pretrained(model_id) text = "这里放一段与你的使用场景接近的验证语料。" inputs = tokenizer(text, return_tensors="pt").to("cuda") with torch.no_grad(): outputs = model(**inputs, labels=inputs["input_ids"]) ppl = torch.exp(outputs.loss) print(f"Perplexity: {ppl:.4f}")注意,这个简单计算会包括 prompt 自身的 loss,所以数字仅供参考,不要当作严格学术指标。更专业的做法是使用lm-evaluation-harness等标准工具,在 HellaSwag、MMLU、HumanEval 等任务上对比量化前后效果。
如果你想看显存和吞吐,可以这样:
torch.cuda.reset_peak_memory_stats() # 执行若干次推理 for _ in range(10): out = model.generate(**inputs, max_new_tokens=50) peak_mem = torch.cuda.max_memory_allocated() / 1024**3 print(f"峰值显存约 {peak_mem:.2f} GB")显存下降但吞吐下降,通常要查 kernel 是否真正支持 int4 计算;显存没有下降但模型能跑,通常是因为上下文中的 KV cache 占了大量显存。
8. 常见问题与排查思路
下面的表格覆盖了最常见的问题。遇到问题时,不要急着换量化方案,先按“日志 -> 格式 -> 校准集 -> kernel”的顺序排查。
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| 输出胡言乱语,句子破碎 | 使用 RTN 或校准集不匹配 | 看量化日志和 loss;换校准集重量化 | 改用 GPTQ/AWQ;group size 设为 128 |
| 推理速度比 FP16 还慢 | GPU 不支持 INT4 矩阵乘,或 kernel 走了 dequant 路径 | 查nvidia-smi算力;确认框架的 int4 GEMM 支持 | 换 Ampere 及以上 GPU;换 llama.cpp/GGUF;退回 8bit |
| 显存下降不明显 | KV cache 占用过大,上下文太长 | 用torch.cuda.max_memory_allocated分段监控 | 量化 KV cache;减小max_length;换长上下文优化模型 |
| 换框架加载后精度崩 | AWQ/GPTQ/GGUF 格式不匹配 | 检查文件后缀和量化配置 | 在目标框架里重新转换,不要跨格式混用 |
| 量化后特定任务掉点严重 | 校准集与任务领域差异大 | 用任务相关语料做小样本对比 | 收集领域校准数据,重新量化 |
| QLoRA 微调 loss 爆炸 | compute dtype 或 scale 设置不对 | 检查训练日志和 dtype | bnb_4bit_compute_dtype用 bf16/fp16,不要用 fp32 |
| 模型加载时显存不够 | 加载时临时申请了 FP16 副本 | 设置low_cpu_mem_usage=True;确认device_map | 先meta初始化再加载权重 |
9. 最佳实践与工程建议
如果你的目标是上线一个模型服务,以下建议能帮你少走弯路。
第一,能用 8bit 就不要盲目上 4bit。8bit 的方案在精度和 kernel 支持上都更成熟,部署成本也更低。只有显存确实紧张时,才考虑 4bit,同时必须用 AWQ/GPTQ 这类误差补偿方案,而不是裸 RTN。
第二,校准集必须贴近真实任务。量化不是“随便找几段新闻就能校准”。代码生成模型最好用代码语料校准,中文医疗模型最好用中文医疗文本校准。校准集数量不一定要很大,但领域和风格一定要贴近。很多 4bit 翻车案例,翻在校准集和线上数据“两张皮”上。
第三,group size 从 128 开始。group size 越小,量化越精细,但 overhead 越大。128 是当前主流平衡点。如果显存还是不够,可以试 256,但要同时跑一轮评估,确认掉点在接受范围内。千万不要直接跳到 512。
第四,量化前保存 FP16 基线,量化后跑同一组 prompt 对比。这个基线不只是用来“看效果”,更是为了快速定位是量化精度问题,还是推理框架参数问题。没有基线的量化调试,基本靠猜。
第五,记录量化配置。建议在项目里写一个quant_config.json,至少包括量化方法、校准集来源、group size、量化类型、框架版本。模型量化是工程,不是一次性魔法。没有记录,三个月后你可能连自己当时怎么量化出来的都忘了。
第六,重视小模型和超大模型的特殊性。1B、3B 这样的小模型本身信息冗余低,4bit 量化后掉点会更明显。30B 以上大模型虽然冗余高,但量化耗时、校准集要求也更高。两者都需要充分验证,不能因为 7B 上效果好就推断其他尺寸也一定好。
第七,部署链路里,优先选择目标框架原生支持的量化格式。用 vLLM 就按 vLLM 的 AWQ/GPTQ 流程量化;用 llama.cpp 就转 GGUF。跨格式迁移看似方便,实际会遇到 kernel 不匹配、精度异常等问题,得不偿失。
10. 总结与后续学习方向
回到文章开头的问题:4bit 为什么会翻车?一句话回答:不是 4bit 不行,而是你把“4bit”当成了“一个东西”。实际上,4bit 背后有完全不同的量化策略、打包格式和 kernel 链路。RTN 是粗糙的简单舍入,GPTQ 用误差重建来补偿,AWQ 用激活感知来保护关键通道,NF4 为微调场景优化,GGUF 针对 CPU/Mac 做了工程化封装,而你看到的 oq4e 这类名字,更可能是某个工具链内部的私有格式,不能直接和这些通用算法画等号。
如果你想继续深入,下面几个方向值得投入:
- 研究量化误差的数学原理,尤其是 Hessian 引导的误差重建。
- 了解推理框架的 int4 GEMM kernel 差异,学会看
nvidia-smi和 profiling 输出。 - 尝试混合精度量化,在关键层保留 8bit,在非关键层用 4bit。
- 关注新硬件对低比特浮点格式的原生支持,这可能改变未来模型量化的入场门槛。
最后给你一个可操作的收尾建议:今天就从你自己的一个模型开始,用 AutoAWQ 做一次 4bit 量化,保留 FP16 基线,记录量化配置,跑一组与你业务相关的评测数据。这个流程走通之后,你已经比大多数停留在“4bit 翻车”印象里的人走得远了。