量化不是简单地把数字变短,而是在误差、吞吐、显存和可用性之间做精细折中
量化首先解决的不是速度,而是内存账
只要把模型部署过几次,就会发现一个朴素事实。
很多时候系统跑不起来,并不是算力先不够。
而是权重根本装不进显存。
一个 7B 级模型如果按 FP16 存储,粗略看就需要十几 GB。
再加上 KV Cache、激活、工作区,很快就会逼近常见消费级显卡的上限。
所以量化首先是在改写这本内存账。
它把原本以浮点形式存储的参数,压缩成更低位宽的整数或特殊低比特格式。
这样做最直接的收益是显存下降。
其次才可能带来带宽和吞吐上的收益。
量化quantizationQuantization maps higher-precision floating-point values to a lower-bit representation so models use less memory and often less memory bandwidth during inference.但量化从来不是白拿好处。
你压得越狠,原始权重的数值信息损失就越大。
因此量化的核心问题不是“能不能压”。
而是“压到什么程度、用什么方法压、在什么硬件和引擎里压才划算”。
从浮点到整数:scale 和 zero-point 在做什么
最基础的量化思路并不复杂。
先把一组浮点值映射到某个较小的整数区间。
比如 INT8 的 [-128, 127]。
然后在需要计算时,再通过反量化近似恢复。
最常见的两个参数是 scale 和 zero-point。
scale 决定“一个整数步长对应多少真实值”。
zero-point 决定“哪个整数代表真实值 0”。
这可以用一个很常见的近似式来表示。
q = round(x / scale) + zero_point
x_approx = (q - zero_point) * scale
如果使用对称量化,zero-point 往往取 0。
如果使用非对称量化,zero-point 可以帮助更好覆盖偏移分布。
zero-pointzero-pointzero-point 让整数空间中的某个离散值与实数 0 对齐,这对分布不以 0 为中心的张量尤其重要。问题也由此出现。
一旦你把连续浮点压成离散整数,就一定会有舍入误差。
如果某层权重分布很平滑,误差可能不大。
如果某层有极端值、长尾分布或特别敏感的通道,误差就可能被放大。
所以真正有效的量化算法,都不是“对每个矩阵一刀切地压缩”。
而是在想办法识别哪些地方更敏感。
为什么 INT8 普遍稳,INT4 更激进
INT8 通常是很多团队的第一站。
原因很现实。
它的压缩比足够明显。
精度损失又往往还在可接受范围。
很多工程栈也对 INT8 支持成熟。
因此它经常被用作推理降显存的默认起点。
INT4 则更激进。
理论上权重体积又能再砍一半。
但 4 比特带来的表达空间很小。
如果算法、校准数据或者内核实现不够好,模型退化会更明显。
所以今天常见的 INT4 路线,几乎都会附带更复杂的误差控制策略。
GPTQ、AWQ、NF4、group-wise quantization,本质上都在处理这个问题。
GPTQ:把“哪种误差最伤模型”算得更细
GPTQGPTQGPTQ is a post-training weight quantization method that quantizes weights row by row while minimizing reconstruction error with second-order information from calibration data.GPTQ 是部署里非常常见的一条后训练量化路线。
它的关键点不在于“4bit”本身。
而在于它不是盲目把每个权重都压成整数。
而是借助校准数据,估计一部分二阶信息,按行逐步量化权重矩阵,让量化后的输出误差尽量小。
直觉上可以把它理解成这样。
同样是量化误差,落在某些方向上可能没那么致命。
落在另一些方向上却会严重破坏模型输出。
GPTQ 试图优先保护那些“更重要”的方向。
它适合离线量化。
也就是先花一段时间做量化,再把量化后的模型长期用于推理。
这也是为什么很多社区模型会直接发布 GPTQ checkpoint。
因为不是每个部署者都想自己重新跑校准。
AWQ:优先保住显著权重
AWQAWQAWQ stands for Activation-aware Weight Quantization. It preserves a small subset of salient weights so 4-bit compression causes less quality loss.AWQ 的思路和 GPTQ 有相通之处,但切口不同。
它强调的是 activation-aware。
也就是从激活响应出发,找出对结果更敏感的一小部分权重,让这些“显著权重”尽量得到更好的保留。
这件事的工程价值非常明确。
并不是所有参数都一样重要。
如果能识别出真正影响输出的一小撮值,就没必要对全体参数一视同仁。
这样就能在 4bit 压缩下,把退化控制在相对温和的范围。
因此 AWQ 常见于已经预量化好的发布模型。
很多服务框架直接支持加载 AWQ 权重。
部署时重点更多在“选对版本”,而不是自己发明量化流程。
bitsandbytes:最容易上手,也最像工程工具箱
相比 GPTQ 和 AWQ 偏“离线制品”的使用方式,bitsandbytes 更像工程里最容易直接上手的选项。
它和 Hugging Face transformers 集成得很深。
部署者通常不必先离线导出一个新 checkpoint。
而是直接在 from_pretrained() 时指定量化配置。
官方文档明确把它定位为最容易使用的 8bit 和 4bit 量化方案。
其中 8bit 的 LLM.int8 路线,会把异常值保留在更高精度路径里处理。
4bit 则常和 QLoRA 微调一起出现。
动态量化路径动态量化路径bitsandbytes 的一个工程优势是,它并不要求你先产出新的独立模型文件,而是能在加载阶段接管线性层的低比特表示与计算路径。这意味着如果你的目标是尽快在单机上把模型跑起来,bitsandbytes 往往是最短路径。
它的缺点也同样现实。
不同硬件、不同算子和不同框架组合下,极致性能未必总是最优。
所以它更像快速部署与开发便利性的优先选项。
三者的差别,最好用“生产流程位置”来理解
很多比较文章喜欢问一句。
GPTQ、AWQ、bitsandbytes 谁更好。
这个问法不够工程化。
更好的问法是,它们分别处在部署流程的哪个位置。
- GPTQ 更像离线后训练量化算法,强调通过校准最小化权重误差。
- AWQ 更像面向 4bit 落地的精度保护策略,强调保住显著权重。
- bitsandbytes 更像开发者友好的运行时集成方案,强调易用性与快速加载。
也就是说,它们不是严格互斥的“品牌替代”关系。
而是面向不同落地路径的工具。
当你需要一个已经量化好的 checkpoint,可能会选 GPTQ 或 AWQ 版本。
当你要在 transformers 里最快把模型降显存跑起来,通常先想到 bitsandbytes。
为什么很多量化比较会吵起来
bitsandbytes 的真实加载方式
下面先看最实用的例子。
直接用 bitsandbytes 在 transformers 里加载 8bit 和 4bit 模型。
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig
model_id = "Qwen/Qwen2.5-1.5B-Instruct"
tokenizer = AutoTokenizer.from_pretrained(model_id)
int8_config = BitsAndBytesConfig(load_in_8bit=True)
model_int8 = AutoModelForCausalLM.from_pretrained(
model_id,
quantization_config=int8_config,
device_map="auto",
)
nf4_config = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_quant_type="nf4",
bnb_4bit_compute_dtype=torch.bfloat16,
)
model_int4 = AutoModelForCausalLM.from_pretrained(
model_id,
quantization_config=nf4_config,
device_map="auto",
)
这段代码能直接跑的前提,是环境里已经安装了相应版本的 transformers、accelerate 和 bitsandbytes。
pip install --upgrade transformers accelerate bitsandbytes
如果你只是想验证“显存是否降下来”,这已经足够。
但如果你要上线,还要继续看延迟、吞吐和结果质量。
GPTQ 的离线量化示例
Hugging Face 目前仍保留了 GPTQ 的典型使用方式。
也就是先构造 GPTQConfig,再基于校准数据离线量化。
from transformers import AutoModelForCausalLM, AutoTokenizer, GPTQConfig
model_id = "facebook/opt-125m"
tokenizer = AutoTokenizer.from_pretrained(model_id)
gptq_config = GPTQConfig(
bits=4,
dataset="c4",
tokenizer=tokenizer,
)
quantized_model = AutoModelForCausalLM.from_pretrained(
model_id,
device_map="auto",
quantization_config=gptq_config,
)
quantized_model.save_pretrained("./opt-125m-gptq")
tokenizer.save_pretrained("./opt-125m-gptq")
这里要注意两点。
第一,GPTQ 量化本身就可能很耗时。
第二,校准数据不是随便塞一点文本就完事。
你喂给它的数据分布,会影响量化后的效果。
因此生产里更常见的做法,是直接使用已经验证过的 GPTQ checkpoint,而不是每次自己重新量化。
AWQ 更常见的是加载现成模型
对于 AWQ,很多团队实际工作流不是“自己从头量化”。
而是直接拉取已经 AWQ 化的模型版本。
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer
model_id = "TheBloke/Mistral-7B-Instruct-v0.1-AWQ"
tokenizer = AutoTokenizer.from_pretrained(model_id)
model = AutoModelForCausalLM.from_pretrained(
model_id,
device_map="cuda:0",
torch_dtype=torch.float16,
)
prompt = "Explain AWQ in one paragraph."
inputs = tokenizer(prompt, return_tensors="pt").to("cuda:0")
outputs = model.generate(**inputs, max_new_tokens=128)
print(tokenizer.decode(outputs[0], skip_special_tokens=True))
如果你的推理引擎原生支持 AWQ,这类模型通常会比临时运行时量化更稳定。
但它要求你选择与引擎、架构、内核都匹配的 checkpoint。
精度损失主要从哪里来
量化退化很少是一个抽象的“有点不准”。
它通常来自几类更具体的来源。
- 舍入误差,把连续值压到离散桶里。
- 分组量化误差,同一组参数共享缩放因子后,局部细节被抹平。
- 异常值处理不足,少量大权重或大激活拉坏整体映射。
- 不同层敏感度不同,统一策略可能让关键层退化更明显。
- 量化格式与推理内核不匹配,导致理论精度和真实运行结果出现偏差。
这也是为什么只看 bit 数并不够。
同样是 4bit,AWQ 和一个粗糙的逐张量量化结果可能差很多。
PPL 为什么常用来做量化评估
部署里最实用的一类量化评估,是先看困惑度。
这里常说的困惑度,也就是 perplexity,衡量的是模型面对参考文本时“有多意外”。
困惑度不是万能指标。
它不能替代指令跟随、多轮对话或特定业务评测。
但它非常适合做第一层筛查。
原因很简单。
量化如果严重破坏了基础语言建模能力,PPL 往往会先明显变坏。
下面给一个最小可跑的 PPL 评估脚本。
import math
import torch
from datasets import load_dataset
from transformers import AutoModelForCausalLM, AutoTokenizer
model_id = "Qwen/Qwen2.5-1.5B-Instruct"
tokenizer = AutoTokenizer.from_pretrained(model_id)
model = AutoModelForCausalLM.from_pretrained(
model_id,
torch_dtype="auto",
device_map="auto",
)
model.eval()
dataset = load_dataset("wikitext", "wikitext-2-raw-v1", split="test[:1%]")
text = "\n\n".join(x["text"] for x in dataset if x["text"].strip())
enc = tokenizer(text, return_tensors="pt")
input_ids = enc["input_ids"].to(model.device)
stride = 512
max_len = 1024
nlls = []
for begin in range(0, input_ids.size(1), stride):
end = min(begin + max_len, input_ids.size(1))
trg_len = end - begin
chunk = input_ids[:, begin:end]
labels = chunk.clone()
labels[:, :-trg_len] = -100
with torch.no_grad():
outputs = model(chunk, labels=labels)
nlls.append(outputs.loss * trg_len)
if end == input_ids.size(1):
break
ppl = torch.exp(torch.stack(nlls).sum() / end)
print("perplexity =", float(ppl))
更严谨的做法,是把原始 FP16/BF16 模型与不同量化版本放在同一评测集上比较。
如果 PPL 升高不多,再继续做业务评测。
如果 PPL 已经显著恶化,就没必要急着上线上压测。
量化不是越低越好,而是越合适越好
很多量化讨论最后都会陷入一个单维排序。
谁更小,谁就更先进。
这不是生产视角。
如果你的目标是单卡上线一个 7B 模型,INT8 也许已经足够。
如果你的目标是把 70B 级模型塞进更有限的硬件,INT4 才会变成必要手段。
如果你还要继续做低显存微调,bitsandbytes 的 4bit 路线和后面的 QLoRA 会更自然衔接。
真正成熟的做法通常是先设约束,再选量化方案。
显存上限是多少。
可接受的 PPL 退化是多少。
使用的推理引擎支持哪些格式。
是否还需要继续微调。
把这些问题答清楚,再谈 GPTQ、AWQ 或 bitsandbytes,讨论才有意义。
本篇要点
- 量化首先是在压缩权重与显存占用,其次才可能带来吞吐收益。
- scale 和 zero-point 是浮点到整数映射的基础参数,误差来自离散化和分布失真。
- GPTQ 偏离线后训练量化,AWQ 强调保护显著权重,bitsandbytes 更强调运行时易用性。
- INT8 通常更稳,INT4 更激进,但配合合适算法后能显著降低部署门槛。
- PPL 是量化评估的第一层筛查指标,但最终仍要回到具体任务效果和线上指标。
下一篇
上一篇解释了推理为何会受显存牵制,这一篇讨论了如何把权重压缩下来。下一篇会继续沿着“如何把模型放进更普通的机器”往下走,进入 GGUF 与 llama.cpp,看看 CPU 部署为什么在工程上依然非常重要。
参考资料
- Hugging Face bitsandbytes Quantization
- Hugging Face GPTQ
- Hugging Face AWQ
- bitsandbytes Documentation
版权声明: 如无特别声明,本文版权归 sshipanoo 所有,转载请注明本文链接。
(采用 CC BY-NC-SA 4.0 许可协议进行授权)
本文标题:模型量化INT8与INT4
本文链接:https://www.sshipanoo.com/blog/ai/inference-opt/02-模型量化INT8与INT4/
本文最后一次更新为 天前,文章中的某些内容可能已过时!