为什么讲这个案例:它把「模型即组件 → 资源约束 → 压缩权衡 → 度量证伪」串成一条完整链路,且不需要写训练代码就能跑起来做实证。量化常被当作"调一个开关",但工程真相是:每一次压缩都在【显存 / 速度 / 质量 / 工具可靠性】四维之间做取舍,而每一步取舍都必须能被一张度量表证伪。
阅读路径:§1 立命题,§2 讲原理(量化在动什么),§3 摆三条路线,§4 讲 MoE 卸载,§5 讲 KV cache,§6 交付度量四元组表(本篇核心),§7 记录规范,§8 结论模板,§9 动手实验,§10 与前后篇接口。
1. 为什么要压缩:显存是硬约束
软件工程里"需求"必须是可验证的;资源约束同理。把一句"这模型能不能跑起来"翻译成可判定命题,是本篇的第一步:
可判定命题 P:在给定硬件(例如"16GB 显存")、给定运行时、给定量化配置下,模型能否加载并完成目标上下文长度的推理,且四项指标(显存峰值 / tokens/s / 基准保留率 / 工具调用成功率)落在事先约定的验收线以内。
命题 P 之所以有用,是因为它把"能不能跑"拆成可测量、可失败、可归因的子句。显存不达标 → P 假;能跑但慢到不可用 → P 在速度维度假;跑得快但基准掉一大截 → P 在质量维度假。压缩的全部工程意义,就是在这四个维度上把 P 逼近为真,并诚实记录代价。
| 术语 | 它在预算哪一块 | 压缩能碰它吗 |
|---|---|---|
| 权重显存 | 模型参数本身常驻占用(FP16 下约 2 bytes × 参数量) | 能——权重量化 |
| KV cache | 注意力历史键值缓存,随上下文长度线性增长 | 能——KV cache 量化(§5) |
| 激活显存 | 前向中间张量,训练/长序列时占比高 | 能,但推理部署较少单独压 |
| 运行时常量 | CUDA 上下文、计算图、显存碎片预留 | 压不动,属于固定开销 |
一句话:决定"16GB 够不够"的,往往是权重 + KV cache 两块。短上下文时权重大头;长上下文时 KV cache 反超——这两块要分开度量(§5、§6)。
能不能装下——P 命题的第一道硬门。
装下了够不够快——跨引擎只比趋势。
快了还准不准——相对 FP16 基线掉了多少。
准了还能不能听话出结构化结果。
2. 量化到底在动什么
"量化"这个词被用得太宽。工程上必须先分清:你压的是权重、激活,还是KV cache——三者动的是不同的显存块,误差来源也不同。
2.1 权重量化:把连续张量映射到 b 比特格点
最朴素的对称均匀量化,把一个权重张量 W 映射到 b 比特能表达的有限格点上。设标度 Δ 由最大值决定:
量化误差是"落点到最近格点"的距离,其均方误差近似随比特数指数下降:
这就是"少一比特、误差平方级上升"的直觉来源。但真实量化远不止朴素均匀格点:分组 scale 非均匀码本 重要性感知 是三条主流改进——K-quants(Q4_K_M)用分组 + 混合精度码本,IQ(IQ3_XXS)用信息论设计的非均匀码本,AWQ 则按激活重要性保护关键通道。它们都在同一命题(省显存)下,用不同的方式对抗上式右侧的误差。
诚实提示:格点误差
∝ 2^{-2b}是对单张量、均匀分布的粗模型;真实模型里权重分布长尾、层间敏感不同,所以"3-bit 一定崩"并不成立——分组与码本设计能把可用比特位继续往下推。基准保留率必须实测(§6)。
2.2 权重 / 激活 / KV:三个不同的显存块
- 权重量化:压常驻参数,省的是"加载即占用"的固定块。部署最广(GGUF 的
Q4_K_M、vLLM/TensorRT 的AWQ/GPTQ)。 - 激活量化:压前向中间值(如 W8A8),主要省算力/带宽,对显存收益依赖实现,端侧部署才常见。
- KV cache 量化:压注意力历史(如
q4_0),省的是随上下文增长的那块——长文本场景的决定性变量(§5)。
把三者混为一谈是常见错误:"我量化了"到底量化了谁? 度量表里必须分列标注,否则两个都叫"4-bit"的配置可能一块压了权重、一块压了 KV,数字根本无法横比。
3. 三条路线对照
压缩工具生态庞杂,本篇刻意选三条可比性各不相同的路线来教"怎么比、什么不能比"。
3.1 llama.cpp 家族内部:Q4_K_M ↔ IQ3_XXS(可横比)
同一个引擎(llama.cpp)、同一份权重来源、只是量化配方不同——这是最干净的对照。Q4_K_M 约 4.x 比特,质量-显存的稳健默认;IQ3_XXS 约 3.x 比特,用非均匀码本把显存再往下压,代价通常体现在基准保留率与部分工具调用的稳定性。同引擎横向对比,四个维度可直接入表。
3.2 GPU 原生 AWQ:单独走一轨
AWQ(Activation-aware Weight Quantization)是 4-bit、以激活分布指导"哪些通道更重要"的权重量化,常与 vLLM / TensorRT-LLM 搭配,在 GPU 上吃满 kernel 融合与批量吞吐。它与 GGUF 数字不可直接横比:不同的运行时、不同的调度与批处理假设,会让同样的"4-bit"给出完全不同的 tokens/s。
3.3 三值 / 1-bit:显存换质量的下界样本
三值(ternary,权重 ∈ {-1, 0, +1})与 1-bit 路线是"极端压缩"参照系——它未必实用,但作为度量表的下界锚点极有价值:它把"压到极致时质量与工具可靠性如何塌方"变成可观测的失败样例,帮读者理解权衡曲线的右端。
| 路线 | 比特量级 | 典型运行时 | 可比性(诚实标注) |
|---|---|---|---|
| Q4_K_M | ≈ 4.x bit | llama.cpp / GGUF | 与 IQ3_XXS 可横比(同引擎) |
| IQ3_XXS | ≈ 3.x bit | llama.cpp / GGUF | 与 Q4_K_M 可横比(同引擎) |
| AWQ | 4-bit 权重 | vLLM / TensorRT-LLM | 跨引擎,速度/保留率不可与 GGUF 直接对 |
| 三值 / 1-bit | 1 – 1.58 bit | 研究实现 / 定制 kernel | 作为下界锚点,勿与生产配置混比 |
本篇最重要的方法论一句话:跨引擎的绝对数字不能直接对比;能做的是"同引擎内横比配方"或"同配方跨引擎看趋势",而度量表里必须写明"运行时 + 版本",让读者知道哪些列可跨行比较、哪些不可。
4. MoE 卸载:总参数大 ≠ 每 token 都贵
以 Qwen3.6-35B-A3B 这类稀疏专家(MoE)结构为例:标称 35B 是"专家池总参数",而 A3B(active 3B)说明每个 token 实际只激活约 3B 参数——路由器(gating network)为每个 token 挑 top-k 个专家。
工程含义:"总参数决定权重显存的下限,激活参数决定每次前向的计算量"。正因为每 token 只碰一小撮专家,才给"分层加载 / CPU-GPU 卸载专家"留出空间:冷专家留在内存/磁盘,热专家常驻显存。
度量陷阱:MoE 卸载省的是显存,赔的是速度(PCIe 换入换出)。所以在四元组表里,"MoE + 高卸载比例"往往呈现显存好看、tokens/s 难看的组合——把它和"稠密量化"放同一张表按四维并列,才看得见真实权衡。
5. KV cache 量化:长上下文的显存真正去向
短上下文时权重是大头;一旦上下文拉长,KV cache 会反超权重成为主要占用。它随"层数 × 头维度 × 序列长度 × 精度字节"线性增长:
其中 2 来自 K 与 V,L 层数、d_kv KV 头总维、S 序列长度、b_prec 每元素字节、B 批量。把 b_prec 从 2(FP16)降到 0.5(q4_0 四比特)近似让这一块腰斩再腰斩,于是同样的显存能塞下更长的上下文。
把注意力历史压到约 4-bit;长上下文/大 batch 时收益最明显,短上下文收益有限。
权重降 1 bit 与 KV 降 1 bit 动的是不同张量,度量表必须分别记列,否则无法归因。
务必分开度量:"我开了 q4_0 KV cache"和"我用 Q4_K_M 权重"是两件独立的事。基准保留率的下降,可能来自权重、也可能来自 KV——本篇要求两条各留一列,让失败可归因(§6 schema)。
6. 度量四元组表(本篇核心交付)
如果只交付一样东西,那就是这张表。每一步压缩都要能被它证伪,四个维度缺一不可:
| 维度 | 指标 | 怎么读 | 典型失败信号 |
|---|---|---|---|
| ① 显存峰值 | 推理期 nvidia-smi / 运行时上报的峰值占用 (GB) | 越低越好;判 P 命题第一关 | 加载即 OOM / 长上下文崩 |
| ② tokens/s | 稳态解码吞吐(预热后取均值) | 越高越好;跨引擎只比趋势 | MoE 高卸载 / KV 换入换出拖速 |
| ③ 基准保留率 | 量化后得分 / FP16 基线得分 (%) | 越接近 100% 越好,须标注跑的是哪个 bench | 数学/代码子集先塌 |
| ④ 工具调用成功率 | 结构化工具/函数调用格式合法率 + 端到端完成率 (%) | 压缩常最先伤"格式服从",专列一项 | JSON 参数畸变 / 幻觉工具名 |
6.1 统一 traces.csv schema:预留 quant_bits 列
本系列共用一份 traces.csv 轨迹文件。本篇负责写入压缩侧字段,其中量化位宽 quant_bits 是刻意保留的关键列——它与续篇 pr12 的 spec_decode(投机解码开关)两列正交合成,供 pr14 的 2×2 因子设计实证使用:
run_id,model_commit,runtime,runtime_version,engine,weight_quant,kv_quant,quant_bits,ctx_len,batch,spec_decode,peak_vram_gb,tps,bench_retention_pct,tool_call_ok_pct,fail_code,notes
| 列 | 本篇职责 | 为 pr12 / pr14 预留 |
|---|---|---|
weight_quant / kv_quant | 分开记录两处压缩(§5) | — |
quant_bits | 综合量化位宽(因子 A 水平) | 与 spec_decode 合成 2×2 |
spec_decode | 本篇留空 / 填 off 占位 | 由 pr12 填 on/off(因子 B) |
peak_vram_gb / tps / bench_retention_pct / tool_call_ok_pct | 四维指标(§6 表) | pr14 作为因变量分析 |
runtime_version / model_commit | 可复现锚点(§7) | pr08 仪表盘聚合键 |
为什么留两列做因子:压缩(省显存)与提速(投机解码)看起来各管各的,工程上会互相牵制(投机解码要额外预算,量化会改变接受率)。把
quant_bits × spec_decode摆成 2×2,pr14 才能实证"两者是否交互",而不是各自拍脑袋调参。
7. 可复现记录规范
压缩实验最容易被"数字来源不明"腐蚀。记录规范本身是交付物的一部分:
- ☑ 硬件型号:GPU 具体型号、显存容量、是否并发其它进程(外部效度声明)。
- ☑ 运行时与版本:llama.cpp / vLLM 的 build 版本或 commit;GGUF 量化档位(Q4_K_M / IQ3_XXS / q4_0)写全。
- ☑ 模型 commit / hash:权重仓库的 commit 或文件哈希——量化配方随版本演进,只写模型名不足以复现。
- ☑ 数字来源标注:每个数字必须注明"社区实测"还是"本机自测";社区数字不得当作承诺复述。
速度类数字不作承诺:tokens/s 强依赖 CPU、内存带宽、批量、温度墙。报告里写速度必须连同"预热轮数 + 重复次数 + 取均值/CI"一起写;单独一个"能跑 60 tok/s"是广告不是度量。
8. 结论模板:给出证据链,也给出失败样例
"16GB 目标是否达成"必须写成带证据链的判定 + 反例,而不是"能跑/不能跑"的口号。模板:
命题 P:在 <硬件> + <运行时版本> + <量化配置> 下,16GB 是否达成?
- ① 显存峰值:____ GB(nvidia-smi,预热后 5 次均值[95%CI])→ 达标? Y/N
- ② tokens/s:____(自测,标注 CPU/批量/上下文)→ 可用? Y/N
- ③ 基准保留率:____%(bench=____,基线=FP16)→ 达标? Y/N
- ④ 工具调用成功率:____%(任务集=____)→ 达标? Y/N
证据链:traces.csv#run_id=____ 复现:见 §9 脚本 + model_commit=____
失败样例:(如 IQ3_XXS 在长数学题上 JSON 参数畸变率 > __%,附 run_id)
降级方案:若实验室无 24GB+ 显存 → 退到三值/小模型(如 MiniMind),
结论可比性变弱,须在报告显式注明硬件受限导致的外部效度损失。
降级不是失败,藏起降级才是。当硬件不足,退到三值小模型 / MiniMind 也能跑通同一张四元组表——只是"数字量级"不再对标消费级大模型。诚实写明"因硬件受限,本报告只验证方法链、不外推绝对性能",就是合格的工程结论。
9. 动手实验(可运行)
目标:加载 Q4_K_M 与 IQ3_XXS 两档,采集显存与 tokens/s,连同量化位宽写入 traces.csv。所有命令行参数以官方仓库为准,下面是示例配置。
9.1 llama.cpp:Q4_K_M ↔ IQ3_XXS 对照采集
第一步,用 llama.cpp 的 server 模式分别加载两档量化(GGUF 文件名与参数以官方仓库为准):
# 启服务:llama-server(OpenAI 兼容 API,默认端口 8080)
# 权重与量化档位以官方仓库为准;ctx 控制上下文长度,n-gpu-layers 控制卸载层数
llama-server \
--model model-Q4_K_M.gguf \
--ctx-size 4096 \
--n-gpu-layers 99 \
--host 127.0.0.1 --port 8080
# KV cache 量化对照(§5):额外加 --cache-type-k/--cache-type-v q4_0
# llama-server --model model-Q4_K_M.gguf --ctx-size 4096 \
# --cache-type-k q4_0 --cache-type-v q4_0 --n-gpu-layers 99 \
# --host 127.0.0.1 --port 8080
第二步,Python 侧:向本地 API 发一段固定 prompt,边生成边轮询 nvidia-smi 采峰值显存与吞吐,逐档写入 traces.csv。
import json, time, csv, threading, subprocess, requests
SERVER = "http://127.0.0.1:8080/v1/chat/completions"
PROMPT = "解释一下量化,并用 JSON 返回 {\"bits\": , \"scheme\": }。"
def nvidia_peak(stop, box):
peak = 0
while not stop.is_set:
out = subprocess.check_output(
["nvidia-smi", "--query-gpu=memory.used",
"--format=csv,noheader,nounits"]).decode
peak = max(peak, int(out.strip.splitlines[0]))
time.sleep(0.2)
box["peak_mb"] = peak
def run_case(model_commit, quant_name, quant_bits, ctx_len):
payload = {"model": quant_name,
"messages": [{"role": "user", "content": PROMPT}],
"max_tokens": 256, "temperature": 0}
stop, box = threading.Event, {}
t = threading.Thread(target=nvidia_peak, args=(stop, box)); t.start
t0 = time.time
r = requests.post(SERVER, data=json.dumps(payload), timeout=180)
secs = time.time - t0
stop.set; t.join
data = r.json
# 工具/格式合法率:这里粗判回复里能否解析出合法 JSON(§6 第④维)
text = data["choices"][0]["message"]["content"]
tool_ok = int(_json_like(text)) # 0/1 占位,实际可跑更大任务集
usage = data.get("usage", {})
toks = usage.get("completion_tokens") or 0
tps = toks / secs if secs > 0 else 0.0
return dict(model_commit=model_commit, engine="llama.cpp",
weight_quant=quant_name, kv_quant="f16",
quant_bits=quant_bits, ctx_len=ctx_len, batch=1,
spec_decode="", # 留给 pr12 填
peak_vram_gb=round(box["peak_mb"] / 1024, 2),
tps=round(tps, 1),
bench_retention_pct="", # 需跑 bench 才填(§6)
tool_call_ok_pct=tool_ok * 100,
fail_code="", notes="示例配置,以官方仓库为准")
def _json_like(s):
# 粗验:能否在文本里找到花括号且解析成功(演示用途)
i, j = s.find("{"), s.rfind("}")
if i < 0 or j < 0: return False
try: json.loads(s[i:j + 1]); return True
except Exception: return False
CASES = [ # (commit, 量化名, 位宽, ctx) —— 同引擎横比 Q4_K_M vs IQ3_XXS
("<commit>", "Q4_K_M", 4.5, 4096),
("<commit>", "IQ3_XXS", 3.5, 4096),
]
FIELDS = ["model_commit","engine","weight_quant","kv_quant","quant_bits",
"ctx_len","batch","spec_decode","peak_vram_gb","tps",
"bench_retention_pct","tool_call_ok_pct","fail_code","notes"]
rows = [run_case(*c) for c in CASES]
with open("traces.csv", "w", newline="", encoding="utf-8") as f:
w = csv.DictWriter(f, fieldnames=FIELDS)
w.writeheader; w.writerows(rows)
for r in rows:
print(f"{r['weight_quant']:>8}: {r['peak_vram_gb']} GB, {r['tps']} tok/s, "
f"quant_bits={r['quant_bits']}")
print("已写出 traces.csv —— spec_decode 列留空给 pr12 合成 2×2")
9.2 vLLM:AWQ serve 最小片段
AWQ 走 GPU 原生轨,用 vLLM 起服务。参数以 vLLM 官方仓库为准;注意这里得到的 tokens/s 与 GGUF 不可直接横比(§3.2):
# vLLM OpenAI 兼容服务(示例,quantization 名称与参数以官方仓库为准)
python -m vllm.entrypoints.openai.api_server \
--model <AWQ_MODEL_REPO_OR_PATH> \
--quantization awq \
--max-model-len 4096 \
--dtype half \
--host 127.0.0.1 --port 8000
# 采集时把上表 engine 字段填 vllm、weight_quant 填 awq,
# 并在 notes 标注"跨引擎数字仅看趋势,不与 GGUF 绝对值对拍"。
诚实提示:上面所有命令行参数、量化档位命名、MoE/KV 选项都随版本演进,以各自官方仓库文档为准;代码注释只描述功能与参数含义,不绑定任何具体机器。
10. 与前后续篇的接口
本篇是"压缩"上篇,产出的四元组表与 traces.csv 是后续各篇的公共契约:
pr10 教你"把模型当版本化组件"——本篇的 model_commit / runtime_version 正是那套版本化思想落到压缩的体现。
| 篇目 | 如何使用本篇 |
|---|---|
| pr10 · 模型即组件 | 本篇把"量化配方 + commit"当组件版本管理,是 pr10 版本化视图的下游落地 |
| pr12 · 提速(中) | 沿用同一 traces.csv;pr12 填 spec_decode 列,与本篇 quant_bits 正交 |
| pr14 · 实证方法 | 用 quant_bits × spec_decode 合成 2×2 因子设计,检验压缩与提速的交互效应 |
| pr08 · 度量仪表盘 | 四维指标以 runtime_version / model_commit 为键聚合进仪表盘 |
一页纸总结:压缩不是开关,是权衡——量化动的是【权重 / 激活 / KV cache】三块不同显存;三条路线(Q4_K_M↔IQ3_XXS 可横比、AWQ 跨引擎只比趋势、三值作下界)诚实标注可比性;MoE 卸载与 KV cache 量化各解决一类瓶颈;一切最终收敛到一张四元组度量表(显存 / tokens/s / 基准保留率 / 工具调用成功率),并预留 quant_bits 列等 pr12 合成 2×2。每一步压缩都要能被这张表证伪。