为什么讲这个案例:它把「模型即组件 → 资源约束 → 压缩权衡 → 度量证伪」串成一条完整链路,且不需要写训练代码就能跑起来做实证。量化常被当作"调一个开关",但工程真相是:每一次压缩都在【显存 / 速度 / 质量 / 工具可靠性】四维之间做取舍,而每一步取舍都必须能被一张度量表证伪。

阅读路径:§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 命题的第一道硬门。

②
tokens/s

装下了够不够快——跨引擎只比趋势。

③
基准保留率

快了还准不准——相对 FP16 基线掉了多少。

④
工具调用成功率

准了还能不能听话出结构化结果。


2. 量化到底在动什么

"量化"这个词被用得太宽。工程上必须先分清:你压的是权重、激活,还是KV cache——三者动的是不同的显存块,误差来源也不同。

2.1 权重量化:把连续张量映射到 b 比特格点

最朴素的对称均匀量化,把一个权重张量 W 映射到 b 比特能表达的有限格点上。设标度 Δ 由最大值决定:

$$\hat{w}=\Delta\cdot \mathrm{round}\!\left(\frac{w}{\Delta}\right),\qquad \Delta=\frac{\max|W|}{2^{\,b-1}-1}$$

量化误差是"落点到最近格点"的距离,其均方误差近似随比特数指数下降:

$$\mathbb{E}\big[(W-\hat{W})^2\big]\approx \frac{\Delta^2}{12}\ \propto\ 2^{-2b}$$

这就是"少一比特、误差平方级上升"的直觉来源。但真实量化远不止朴素均匀格点:分组 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 bitllama.cpp / GGUF与 IQ3_XXS 可横比(同引擎)
IQ3_XXS≈ 3.x bitllama.cpp / GGUF与 Q4_K_M 可横比(同引擎)
AWQ4-bit 权重vLLM / TensorRT-LLM跨引擎,速度/保留率不可与 GGUF 直接对
三值 / 1-bit1 – 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 卸载专家"留出空间:冷专家留在内存/磁盘,热专家常驻显存。

每个 token:路由 → 选 top-k 专家 → 分层驻留 Token隐状态 Router / Gate挑 top-k 专家 专家池 Expert Pool总参 35B / 激活 ~3BE1 E2 E3 ... En 分层驻留:热专家上 GPU,冷专家落 CPU / 磁盘 GPU 显存层常驻 top 命中专家 + 注意力 CPU 内存 / 磁盘层冷专家按需换入 收益:权重显存"下限"被激活参数压低;代价:换入换出引入延迟,卸载比例要实测(§6)。 注意:MoE 的 tokens/s 与稠密模型不同源,跨结构对比只谈趋势,不谈绝对值。
图 4-1 · MoE 路由与专家分层卸载:路由器决定每个 token 激活哪些专家,冷/热分层使显存换吞吐成为可能

度量陷阱:MoE 卸载省的是显存,赔的是速度(PCIe 换入换出)。所以在四元组表里,"MoE + 高卸载比例"往往呈现显存好看、tokens/s 难看的组合——把它和"稠密量化"放同一张表按四维并列,才看得见真实权衡。


5. KV cache 量化:长上下文的显存真正去向

短上下文时权重是大头;一旦上下文拉长,KV cache 会反超权重成为主要占用。它随"层数 × 头维度 × 序列长度 × 精度字节"线性增长:

$$\mathrm{KV}_{\text{bytes}}=2\cdot L\cdot d_{\text{kv}}\cdot S\cdot b_{\text{prec}}\cdot B$$

其中 2 来自 K 与 V,L 层数、d_kv KV 头总维、S 序列长度、b_prec 每元素字节、B 批量。把 b_prec 从 2(FP16)降到 0.5(q4_0 四比特)近似让这一块腰斩再腰斩,于是同样的显存能塞下更长的上下文。

q4_0
KV cache 量化档位

把注意力历史压到约 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 因子设计实证使用:

CSV header
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 目标是否达成"必须写成带证据链的判定 + 反例,而不是"能跑/不能跑"的口号。模板:

Markdown 结论模板
命题 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 文件名与参数以官方仓库为准):

Bash
# 启服务: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。

Python
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):

Bash
# 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 选项都随版本演进,以各自官方仓库文档为准;代码注释只描述功能与参数含义,不绑定任何具体机器。


本篇是"压缩"上篇,产出的四元组表与 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。每一步压缩都要能被这张表证伪。