在 Hugging Face 找一个模型,经常会撞见这样的名字:
Qwen-...-GPTQ-Int4
Qwen-...-AWQ
点进去,权重文件却都叫 model-00001-of-00004.safetensors。启动时又有人用:
vllm serve <model>
也有人用:
python -m sglang.launch_server --model-path <model>
五个名字挤在一条链路里,很容易被误认为五种互相竞争的“模型格式”。其实它们分属三个楼层:
- GPTQ、AWQ 是量化算法:决定“哪些小数变成哪些低比特整数,怎样把误差压小”;
- safetensors 是张量容器:决定“张量如何安全、快速地装进文件”;
- vLLM、SGLang 是推理引擎:决定“请求怎么排队、KV Cache 怎么放、调用哪个 GPU 内核”。
一句话先把全文串起来:
原始 BF16 权重经过 GPTQ 或 AWQ 离线量化,作为若干张打包后的张量存进 safetensors;vLLM 或 SGLang 读取配置与张量,选中匹配的量化内核,再把很多用户的请求一起送上 GPU。
下面从这条流水线最底层的问题讲起:为什么要把 16 bit 压成 4 bit?
一、量化首先是在救显存带宽
1.1 7B 模型的第一本账
一个 7B 模型若把全部参数存成 BF16,每个参数 2 字节,光权重理论上就要:
70 亿 × 2 字节 ≈ 14 GB
若主体权重压成 4 bit:
70 亿 × 0.5 字节 ≈ 3.5 GB
实际文件会再多出 scale、zero-point、索引和未量化层,所以不会刚好等于 3.5 GB;但“接近四分之一”已经足以让原本塞不进显存的模型跑起来。
更微妙的是速度。自回归解码每生成一个 token,模型都要把大量权重从显存搬到计算单元。batch 较小时,GPU 往往不是算不动,而是权重搬得不够快。权重缩到四分之一,同样的显存带宽便能喂更多次矩阵乘法。
1.2 W4A16:仓库里住 4 bit,计算时仍用 16 bit
常见 GPTQ / AWQ checkpoint 属于 weight-only quantization(仅权重量化),通常简写成 W4A16:
W4:权重以 4 bit 整数码打包存储;A16:激活仍是 FP16 或 BF16;- 乘法前或乘法过程中,用 scale 把 4 bit 码解释回较高精度。
它并不是先把整个权重矩阵还原成 FP16,再额外存一份。高性能内核会把“读取压缩权重 → 解包 / 反量化 → 矩阵乘法”融合起来,让还原值尽量只活在寄存器或片上缓存里。
这也解释了为什么 4 bit 不保证更快:
- 小 batch 解码常受显存带宽限制,少搬权重通常很赚;
- 大 batch 或长 prompt 的 prefill 更可能受计算限制,反量化开销可能抵消收益;
- GPU 是否有匹配的 Marlin、ExLlama、Triton 或厂商内核,往往比后缀名本身更重要;
- group size、zero-point、act-order、模型形状和硬件代际都可能改变内核选择。
所以量化的确定收益是“权重更小”;速度收益则必须在目标硬件和真实流量上测。
1.3 从“还原权重”换成“保住输出”
一层线性变换可以写成:
y = W x
最朴素的量化只关心每个权重自身的误差:
让 Ŵ 尽量接近 W
但模型真正使用的是输出 Wx。某个权重误差看起来很大,若对应输入几乎总是 0,实际影响可能很小;另一个误差虽小,若总被巨大激活放大,反而很危险。
GPTQ 与 AWQ 的共同进步,就是把校准数据里的激活 x 拉进视野:
让 Ŵx 尽量接近 Wx
两者的路线却完全不同。GPTQ 像一位会“挪账”的会计:量化一个位置后,把它造成的输出误差补偿到尚未量化的位置;AWQ 像一位提前调增益的录音师:先找出经常承受大信号的通道,重新缩放后再量化。
二、GPTQ:量化一列,补偿后面的列
GPTQ 全名是 GPT Quantization。它来自 2022 年的论文 GPTQ: Accurate Post-Training Quantization for Generative Pre-trained Transformers,是一种 PTQ(post-training quantization,训练后量化):不重新训练模型,只用一小批校准样本观察各层输入。
2.1 校准数据到底在做什么
让若干段文本跑过原模型,把某一层看到的输入激活收集成矩阵 X。这层量化的目标可直观写成:
最小化 || W X - Ŵ X ||²
注意它不是要求量化权重 Ŵ 在几何上最像 W,而是要求二者喂入典型输入 X 后,输出尽量相同。
输入中哪些方向经常出现、幅度多大、彼此怎样相关,可以由近似二阶信息描述。GPTQ 延续 Optimal Brain Quantization 的思路,用由 XXᵀ 得到的 Hessian 近似及其逆,估计“改动一个权重后,剩余权重怎样调整最划算”。
不必被 Hessian 吓到。把一行权重想成四位合奏者:
原输出 = w₁x₁ + w₂x₂ + w₃x₃ + w₄x₄
把 w₁ 四舍五入成 4 bit 后,第一位跑调了。若 x₁ 与 x₂ 在校准数据里经常一起变化,就可以稍微调整尚未量化的 w₂,让整段合奏的输出重新靠近原版。GPTQ 做的就是一套高效、逐步的“跑调补偿”。
2.2 为什么要逐列处理
对一层矩阵,GPTQ 大致重复:
- 选择当前要量化的一列(实现中会分 block 处理);
- 把浮点权重舍入到有限的量化刻度;
- 计算这次舍入造成的误差;
- 根据逆 Hessian 的相关项,更新后面尚未量化的权重;
- 继续下一列。
这比“所有权重各自就近取整”更慢,却仍是一次性的离线工作。量化结果保存后,线上推理不再重复 Hessian 计算。
常见配置里的几个黑话也有了落点:
| 字段 | 直观含义 | 常见取舍 |
|---|---|---|
bits | 主体整数码位宽 | 4 bit 最常见 |
group_size | 多少个权重共用一组 scale / zero-point | 128 常见;更小通常更准但元数据更多 |
sym | 是否采用对称量化 | 会影响误差与内核兼容性 |
desc_act / act-order | 是否优先处理激活较大的列 | 质量可能更稳,但布局与内核支持要核对 |
damp_percent | 给 Hessian 对角线加多少阻尼 | 防止数值不稳定 |
2.3 GPTQ 的强项与代价
GPTQ 的优点是目标直接:每层都在努力保住真实校准输入下的输出,并用二阶信息系统地补偿误差。代价也来自同一处:
- 量化过程比简单取整更重;
- 结果可能对校准数据和配置敏感;
- checkpoint 的具体打包格式、对称性和 act-order 会影响推理内核兼容性;
- “GPTQ”只说明算法家族,不保证两个仓库生成的文件能被同一套旧内核无差别读取。
工具生态也在变化。当前 Hugging Face Transformers 文档已经把 GPTQModel 列为活跃后端,并明确说明 AutoGPTQ 不再受支持。这里退场的是某个实现,不是 GPTQ 这个算法。
三、AWQ:先保护大激活经过的通道
AWQ 全名是 Activation-aware Weight Quantization,来自 2023 年论文 AWQ: Activation-aware Weight Quantization for LLM Compression and Acceleration。
它的出发点是一条观察:权重的重要性不能只看权重绝对值,还要看输入激活。论文发现,只需重点保护大约 1% 的显著权重,就能明显降低量化误差;而找它们时,看激活分布比只看权重更可靠。
3.1 大激活为什么会放大量化误差
仍看一项乘法:
yⱼ = wⱼ xⱼ
若量化把 wⱼ 变成 wⱼ + Δwⱼ,输出误差就是:
Δyⱼ = Δwⱼ xⱼ
同样是 0.02 的权重误差:
xⱼ = 0.1 → 输出误差 0.002
xⱼ = 100 → 输出误差 2
第二条通道显然更值得保护。AWQ 先跑少量校准样本,统计各输入通道的激活幅度,再搜索一组合适的 per-channel scale。
3.2 不用混合精度,也能“偏心”重要通道
最直觉的保护方式,是把重要权重留成 FP16、其余权重压成 INT4。但这种零散混合精度会破坏规则矩阵布局,GPU 很难高效执行。
AWQ 的妙处是做一个数学等价的缩放。若用行向量写线性层:
y = x W = (x / s) · (s W)
这里 s 按输入通道缩放:把显著通道对应的权重先放大,再量化;同时把输入通道反向缩小,所以未量化时的函数完全不变。放大后的重要权重占据更多量化刻度,相对舍入误差更小。
关键是:AWQ 论文里的“保护 1% 显著权重”不等于最终文件里真的散落着 1% FP16 权重。它通过通道缩放实现保护,最终仍可保持规则的低比特矩阵和硬件友好的内核。
AWQ 不做反向传播,也不逐层重建输出;它搜索缩放因子后再进行 weight-only 量化。和 GPTQ 相比,思路更轻、更贴近硬件布局,但“更轻”不等于任何模型上都必然更准。
3.3 GPTQ 与 AWQ,到底选谁
| GPTQ | AWQ | |
|---|---|---|
| 看什么 | 层输入的相关性与近似二阶信息 | 各输入通道的激活显著性 |
| 核心动作 | 量化当前列,再补偿未量化列 | 等价缩放重要通道,再统一量化 |
| 是否训练 | 否,训练后量化 | 否,训练后量化 |
| 是否需要校准数据 | 需要 | 需要 |
| 常见部署形态 | W4A16 / W3A16 等 | 以 W4A16 最常见 |
| 工程风险 | act-order、对称性、打包格式与内核要匹配 | GEMM / GEMV 版本、group size 与内核要匹配 |
实际选择顺序不该是“谁的论文更新”,而应是:
- 目标模型有没有可信的现成量化版本;
- 目标 GPU 和引擎有没有成熟内核;
- 你的 batch、prompt 长度与并发量下谁更快;
- 你的真实任务上谁的质量损失更小。
还有一条容易踩坑的现状:AutoAWQ 仓库已经归档,但 AWQ 算法没有消失。 原仓库说明其能力已由 vLLM 项目的 llm-compressor 等后续工具承接。下载旧教程时,要分清“算法名”和“某个曾经流行的 Python 包名”。
四、safetensors:它只负责装,不负责量
现在回答开头的问题:为什么 GPTQ 与 AWQ 模型的文件都可以叫 .safetensors?
因为 safetensors 根本不规定张量是“怎么量化出来的”。它只规定张量的名字、dtype、shape、偏移和原始字节怎样安全地写进文件。
4.1 一个 safetensors 文件只有三段
官方格式非常克制:
- 前 8 字节:一个小端序无符号整数
N,表示 header 长度; - 接下来
N字节:UTF-8 JSON header; - 剩余部分:连续的张量字节缓冲区。
header 为每个张量记录:
{
"model.layers.0.self_attn.q_proj.weight": {
"dtype": "BF16",
"shape": [4096, 4096],
"data_offsets": [0, 33554432]
}
}
它比 pickle 安全,是因为文件是数据而不是可执行的 Python 对象图,加载权重时不会顺手执行任意 pickle 代码。连续数据与明确偏移也便于 mmap、按需读取与分布式切片。
但“safe”不能无限外推:
- safetensors 只保护这个权重容器的反序列化路径;
config.json、tokenizer、聊天模板仍在外部;- 若启动时开启
trust_remote_code=True,远程模型代码仍是另一条安全边界; - 文件安全不等于模型输出安全,也不等于仓库里的所有文件都可信。
4.2 4 bit 到底怎样塞进 safetensors
传统张量 API 对半字节并不总有自然表示。常见 GPTQ / AWQ checkpoint 会把若干个 4 bit 码打包进 INT32 容器张量,同时另存:
qweight 打包后的低比特权重
qzeros 打包后的 zero-point(若使用)
scales 每组缩放因子
g_idx 分组 / act-order 相关索引(部分 GPTQ 格式)
safetensors 忠实保存这些张量,却不知道 qweight 应该怎样解包。真正的说明书通常在 config.json:
{
"quantization_config": {
"quant_method": "awq",
"bits": 4,
"group_size": 128,
"zero_point": true,
"version": "gemm"
}
}
因此一个可部署模型仓库不是“几个 safetensors 就够了”,而是一套契约:
权重字节(*.safetensors)
+ 张量分片索引(*.safetensors.index.json)
+ 模型结构(config.json)
+ 量化解释(quantization_config)
+ tokenizer / chat template
+ 推理引擎中匹配的解包与 GEMM 内核
大模型分成多个 safetensors 只是为了下载、上传和并行加载方便。model.safetensors.index.json 会告诉加载器“哪个张量在哪个分片”,并不改变量化算法。
4.3 它与 GGUF 的边界
上一篇 GGUF 讲过,GGUF 更像一只自带模型元数据与 tokenizer 的单文件行李箱,围绕 llama.cpp 的端侧运行方式设计;safetensors 更像通用张量货柜,通常与 Hugging Face 的多个 JSON、tokenizer 文件一起出现。
二者都能装低精度权重,也都避免 pickle 式可执行反序列化,但“都安全、都能装权重”不代表布局和运行生态相同。一个为 CPU / 端侧块量化优化的 GGUF,不能仅靠改扩展名变成 GPU 友好的 AWQ checkpoint。
五、引擎真正加载了什么
到这里,离线工作已经结束。线上启动 vLLM 或 SGLang 时,大致会发生:
- 读取
config.json,识别 Llama、Qwen、DeepSeek 等架构; - 读取
quantization_config,识别 GPTQ / AWQ、位宽、group size、zero-point 与格式版本; - 根据 tensor parallel 切分规则,只把当前 GPU 负责的张量切片读进来;
- 检查当前 GPU、dtype、模型层形状是否有兼容内核;
- 必要时把 checkpoint 布局重排成 Marlin 等内核偏好的布局;
- 预留 KV Cache 与工作区,启动调度器;
- 收到请求后做 continuous batching,让新请求能插入、结束请求能及时退出。
这里最重要的工程事实是:
“引擎声称支持 AWQ”只是第一关;模型架构、量化变体、硬件、并行方式与内核的交集,才是真正的支持范围。
vLLM 当前稳定文档的兼容表就把 AWQ、GPTQ、Marlin 分成不同条目,并按 Volta、Turing、Ampere、Ada、Hopper、Intel GPU 与 CPU 分列。SGLang 当前文档也分别列出 NVIDIA、AMD、Ascend 上的实现路径。版本更新很快,部署前应看你安装版本的文档和启动日志,而不是记住一张永不过期的截图。
六、vLLM 与 SGLang:分页放置和前缀复用不是二选一
两者都是面向 GPU 高吞吐服务的推理引擎,都支持 OpenAI 风格 API、连续批处理、tensor parallel、量化内核、前缀缓存和大量现代模型。今天再把它们概括成“vLLM 只会分页、SGLang 只会前缀缓存”,已经不准确。
但理解它们各自出名的核心抽象,仍然很有用。
6.1 vLLM 的 PagedAttention:先解决 KV Cache 放不下、放不齐
每个请求的 token 数不同,生成还会随时继续。若为每个请求预留一整块“最大长度”的连续 KV Cache,会产生巨大的内部碎片;若频繁申请不同大小的连续显存,又会产生外部碎片。
PagedAttention 借用了操作系统虚拟内存的思路:
- 把 KV Cache 切成固定 token 数的 block;
- 每个序列维护逻辑 block 到物理 block 的映射;
- 物理块不必连续;
- 序列增长时再分配新块,结束时归还。
这解决的是放置与碎片问题:KV Cache 应该住在哪些物理显存块里。
6.2 SGLang 的 RadixAttention:再解决同一前缀算了又算
很多真实请求共享前缀:
- 多轮聊天共享旧对话;
- few-shot 任务共享示例;
- RAG 请求共享系统提示;
- agent 的多条分支共享搜索历史;
- self-consistency 采样共享同一道题。
RadixAttention 把 token 序列作为 key,把对应 KV Cache 作为 value,组织进 radix tree。新请求到来时先做最长前缀匹配,命中的 KV 不必重新 prefill;空间不足时再按缓存策略淘汰叶子。
这解决的是内容复用问题:哪些请求其实已经算过同一段前缀。
两者可以共存。SGLang 的 radix tree 对应的 KV 也放在 paged memory pool 中;vLLM 也提供 Automatic Prefix Caching。如今更合适的理解是:
| 关注点 | vLLM 的传统强项 | SGLang 的传统强项 |
|---|---|---|
| 核心出发点 | 通用、高吞吐、显存高利用率的 LLM 服务 | 复杂 LLM 程序与跨请求前缀复用 |
| 代表性抽象 | PagedAttention | RadixAttention |
| 适合重点验证 | 广泛模型 / 硬件 / 部署生态,常规 API 流量 | 多轮、few-shot、agent、分支采样等高前缀复用流量 |
| 今天的现实 | 也有自动前缀缓存、结构化输出等 | 也有分页、连续批处理和广泛模型支持 |
不要拿不同版本、不同内核、不同 batch 配置的一张吞吐图宣布永久冠军。更换一个模型架构、CUDA 版本、prompt 长度分布或输出长度,结论就可能翻转。
七、权重省下的显存,可能被 KV Cache 吃回去
量化主要压缩静态权重;服务运行时还有 KV Cache、激活、CUDA Graph、通信 buffer 和临时 workspace。
对标准多头 / 分组查询注意力,一条序列的 KV Cache 可粗略估算为:
KV 字节数
= 2 # K 和 V
× 层数
× KV heads
× head_dim
× token 数
× 每元素字节数
× 并发序列数
假设一个模型有 32 层、8 个 KV heads、head_dim = 128,KV 用 BF16,每条序列长 32k:
2 × 32 × 8 × 128 × 32768 × 2 字节
= 4 GiB / 序列
这已经比 7B 模型的理想 4 bit 权重 3.5 GB 还大。并发再乘上去,KV 很快成为主角。
因此:
- 模型装不下:权重量化最有效;
- 长上下文装不下:看 GQA / MLA、KV Cache dtype、分页与上下文长度;
- 并发上不去:看 KV Cache 容量、调度、prefix cache 命中率;
- 吞吐不高:还要区分 prefill 与 decode、memory-bound 与 compute-bound。
这也是为什么只比较模型文件大小,无法预测服务能扛多少并发。
八、从下载到上线:一条不容易踩坑的路径
8.1 下载前先读配置,不只看仓库名
至少确认:
architectures
quant_method
bits
group_size
zero_point / sym
version / format
desc_act(GPTQ 常见)
再去目标引擎当前版本的兼容表核对:
- 模型架构是否支持;
- GPU compute capability 是否支持;
- tensor parallel 下是否支持;
- 量化格式能否走优化内核,还是只能走慢速 fallback;
- 多模态、LoRA、speculative decoding 等附加特性能否同时开启。
8.2 预量化 checkpoint 通常让引擎自动识别
以官方文档的常规路径为例,仓库中已有正确 quantization_config 时,可以直接启动。vLLM:
vllm serve <org>/<awq-or-gptq-model> \
--host 0.0.0.0 \
--port 8000
SGLang:
python -m sglang.launch_server \
--model-path <org>/<awq-or-gptq-model> \
--host 0.0.0.0 \
--port 30000
不要机械照抄旧教程再加一个 --quantization awq。SGLang 当前文档明确区分:
- offline quantization:加载已经量化的权重,从仓库配置识别;
- online quantization:加载高精度权重,启动时动态计算 scale,需要显式参数。
两者不是同一件事。vLLM 也会根据 checkpoint 的量化配置选择加载路径;若手动覆盖,必须知道自己覆盖了什么。
8.3 启动成功不等于部署正确
上线前至少做四类验证:
- 正确性:固定一组 prompt,对比 BF16 与量化版的任务指标或人工结果;
- 容量:测最长 prompt、最长输出和目标并发下是否 OOM;
- 延迟:分别看 TTFT(首 token 延迟)与 TPOT / ITL(后续 token 间隔);
- 吞吐:在真实到达率和长度分布下测 output tokens/s,而不是只跑 batch=1。
建议把测试分成四格:
| 短 prompt | 长 prompt | |
|---|---|---|
| 低并发 | 观察单请求 decode 与量化内核 | 观察 prefill、峰值显存 |
| 高并发 | 观察连续批处理与调度 | 观察 KV Cache、prefix reuse、排队延迟 |
若业务有稳定的 system prompt、多轮会话或 agent 分支,再单独记录 prefix cache hit rate。它可能比 GPTQ 与 AWQ 的差别更影响总体成本。
九、六个常见误解
误解一:.safetensors 就代表 FP16 / BF16
不对。它可以装浮点张量,也可以装打包后的 GPTQ / AWQ 张量。算法写在配置与张量语义里,不写在扩展名里。
误解二:AWQ 比 GPTQ 新,所以一定更好
不对。质量、速度和兼容性都依赖模型、校准数据、配置、内核和硬件。工程上应测目标 workload。
误解三:4 bit 模型的所有数都是 4 bit
不对。常见 W4A16 只压主体权重;激活、部分层、scale、KV Cache 往往仍是 16 bit 或其他精度。
误解四:权重缩四倍,显存一定缩四倍
不对。运行时还有 KV Cache、激活与工作区。上下文足够长时,KV 可以比权重更大。
误解五:量化越小,吞吐一定越高
不对。memory-bound 场景通常受益;compute-bound 场景可能被反量化和不理想内核拖慢。
误解六:PagedAttention 与 RadixAttention 是竞品
不对。一个主要解决物理 KV 块的放置与碎片,一个主要解决共同前缀的识别与复用;现代引擎会同时吸收两类思想。
十、写在最后
现在再看开头那条命令,背后的分工应该清楚了:
GPTQ / AWQ
决定怎样把高精度权重近似成低比特权重
safetensors + config.json
决定这些张量怎样存,以及加载器怎样理解它们
vLLM / SGLang + quant kernel
决定怎样把权重放上 GPU,怎样管理 KV Cache,
又怎样把许多请求高效地跑完
真正的部署选型从来不是“五选一”,而是每层各选一块彼此咬合的齿轮。
如果只记住一条经验,就记住这句:
先用量化解决“模型能否装下”,再用引擎解决“流量能否跑满”;最后用自己的模型、硬件和请求分布,把质量、延迟、吞吐一起测出来。
参考
- GPTQ: Accurate Post-Training Quantization for Generative Pre-trained Transformers
- AWQ: Activation-aware Weight Quantization for LLM Compression and Acceleration
- Hugging Face Transformers:GPTQ与 AWQ
- safetensors 官方格式说明
- vLLM 量化兼容表、PagedAttention 设计文档与 Automatic Prefix Caching
- SGLang 量化文档
- SGLang 论文与 RadixAttention 官方介绍
- AutoAWQ 归档说明