量化不是简单地把数字变短,而是在误差、吞吐、显存和可用性之间做精细折中

量化首先解决的不是速度,而是内存账

只要把模型部署过几次,就会发现一个朴素事实。

很多时候系统跑不起来,并不是算力先不够。

而是权重根本装不进显存。

一个 7B 级模型如果按 FP16 存储,粗略看就需要十几 GB。

再加上 KV Cache、激活、工作区,很快就会逼近常见消费级显卡的上限。

所以量化首先是在改写这本内存账。

它把原本以浮点形式存储的参数,压缩成更低位宽的整数或特殊低比特格式。

这样做最直接的收益是显存下降。

其次才可能带来带宽和吞吐上的收益。

量化quantizationquantizationQuantization 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:把“哪种误差最伤模型”算得更细

GPTQGPTQGPTQGPTQ 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:优先保住显著权重

AWQAWQAWQAWQ 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",
)

这段代码能直接跑的前提,是环境里已经安装了相应版本的 transformersacceleratebitsandbytes

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 部署为什么在工程上依然非常重要。

参考资料

版权声明: 如无特别声明,本文版权归 sshipanoo 所有,转载请注明本文链接。

(采用 CC BY-NC-SA 4.0 许可协议进行授权)

本文标题:模型量化INT8与INT4

本文链接:https://www.sshipanoo.com/blog/ai/inference-opt/02-模型量化INT8与INT4/

本文最后一次更新为 天前,文章中的某些内容可能已过时!