⚠️ Alpha内测版本警告:此为早期内部构建版本,尚不完整且可能存在错误,欢迎大家提Issue反馈问题或建议。
Skip to content

第19章 小模型 LLM 解码 + Agent 自动优化

本章导读

本章把 Agent 应用到 LLM 解码场景。受书基线 16GB 显存限制,选型 MoE 小模型 LFM2.5-8B-A1B(A1B:每 token 只激活 1 个专家)的 GGUF 量化版本(Q4_K_M,4.79 GiB),用 llama.cpp ROCm 后端实测。decode 基线、RPB/nwarps 扫描与 Q4 vs Q8 精度对比均来自真实实验(RX 7900 XTX,数字以标注平台为准)。PagedAttention、多卡 TP 等留给 hello-mlsys。

19.1 显存约束下的模型选型

16GB 显存是第一约束。一个 8B 参数的稠密模型,fp16 权重约 17 GB——放不下。所以选型逻辑分两步:先定模型结构,再定量化格式。

为什么是 MoE(A1B)而不是更小的稠密模型:Qwen 0.5B/1.8B 这类小模型能全驻留,但稠密模型的 decode 吞吐完全由「全量权重 ÷ 带宽」决定,没有优化空间可讲。MoE A1B(如 LFM2.5-8B-A1B:8.47B 总参数、每 token 只激活 1 个专家)让「每 token 读取量」远小于全量权重——这是第 19.4 节吞吐主线的关键变量,也是 Agent 真正的优化空间所在。

为什么走 llama.cpp + GGUF,而不是 vLLM 量化:在 AMD 消费卡上,vLLM 的量化路径几乎全灭。7900 XTX 上的实测(2026-07):

量化路线状态原因
AWQ (4-bit)可用Triton 后端自动启用
GPTQ可能可用纯 PyTorch fallback,未经测试
FP8不可用三层锁:AITER gfx9 gate、CK gfx1100 ISA 不兼容(15 个月未修)、torch._scaled_mm 只认 MI300+
Marlin不可用CUDA only,ROCm 明确拒绝
MXFP4不可用gfx1100 缺少 MXFP4 微码

根因是 RDNA3 的 WMMA 与 NVIDIA Tensor Core 不同:FP16/INT8/INT4 共用同一条 16×16×16 流水线(吞吐相同),没有 FP8 指令、没有硬件 scale 融合、没有 INT8/INT4 翻倍。消费卡上的量化精度选择空间,实际上只有 GGUF(Q4/Q8 档)和 AWQ

llama.cpp + Q4_K_M 的收益是实打实的:同一批测试里(7900 XTX,pp512+tg128),llama.cpp Q4_K_M 的端到端耗时约为 vLLM FP16 基线的 1/2.4~1/5.7——注意 MoE 行的对比对象是不同模型(vLLM 侧只有 Qwen2.5-7B FP16 基线),量级对比仅供参考:LFM2.5-8B-A1B 为 667ms vs 3250ms,约 4.9 倍。LFM2.5-8B-A1B 的 Q4_K_M 为 4.79 GiB,16GB 卡上可以全量驻留(-ngl 99)。这就是本章的模型。

19.2 LLM 推理流程拆解

一次生成式推理分成两个阶段,瓶颈性质完全不同:

阶段计算形态算术强度瓶颈
prefill整段 prompt 的矩阵乘(GEMM)算力
decode每生成一个 token 的矩阵×向量(GEMV)带宽

decode 为什么是「算子密集型」?因为每生成一个 token,都要把模型权重读一遍——本章的 LFM2.5-8B-A1B Q4_K_M 有 4.79 GiB 权重(按全量读口径,每 token 就是 4.79 GiB 流量;MoE 稀疏激活会减少实际读取量,见第 19.4 节)。而计算量只有一次矩阵×向量:M=1 的 GEMM,即 GEMV(General Matrix-Vector multiplication)。

对比算术强度(第 4.4.2 节的定义):prefill 的 GEMM 每次读取可以复用多次(大 M 分块),算术强度高;decode 的 GEMV 每个权重元素只被一个 token 用一次,读出来用完即弃。所以 decode 的吞吐几乎完全由「每 token 读权重的字节数 ÷ 显存带宽」决定——这是理解 decode 性能的唯一主线

19.3 建立 decode baseline

decode 的测量口径:固定 prompt、固定生成长度,测每 token 时间(TPOT),通常报告为 tokens/s。测量时要固定生成长度并重复多次——和 Part 1 的检查清单一样,单次结果不可信。

下面是一次真实的 MoE 模型 decode 调参实验(llama.cpp,ROCm 后端,-p 0 -n 64 -r 5 即 64 token 生成、5 次重复)。模型是 8.47B 参数、MoE A1B(每 token 只激活 1 个专家)的量化版本(Q4_K_M,4.79 GiB):

配置t/s(mean ± std)
默认(RPB=1, NW=8)214.60 ± 0.45

这是基线。注意 std 只有 0.45——测量本身很稳,接下来任何配置差异都可以归因于配置而不是噪声。

19.4 decode 的算子视角

把 decode 的每步拆成算子:attention(读取 KV cache,第 17 章 FA Decode 的 M=1 场景)、MoE 专家 GEMV(投影)、输出投影。当模型是 MoE 时,GEMV 是流量主力——decode 的带宽大部分花在读权重,就为了一次矩阵×向量。

decode 的理论吞吐上限由带宽和「每 token 读多少字节」决定:

=显存带宽每 token 读取量

先看一个错误示范:把整个模型权重(4.79 GiB)都算进「每 token 读取量」,用 7900 XTX 的显存带宽 960 GB/s:

4.79 GiB5.1 GB960 GB/s5.4 ms/token190 t/s

但实测是 214.6 t/s——比这个「上限」还快。矛盾说明假设错了:MoE A1B 每层只激活 1 个专家,每 token 读的是共享权重 + 激活专家,远小于 4.79 GiB 总量。反推:如果 214.6 t/s 真是全量读取,所需带宽是 214.6 × 5.1 GB ≈ 1.1 TB/s,超过 960 GB/s 的物理上限——所以稀疏激活让每 token 的实际读取量明显小于全量权重。这给优化留下了两个方向:压「每 token 读取量」(更强量化、更少激活参数),以及让 GEMV 访存更饱和(下表)。真实实验对 block 配置(每块处理的行数 RPB、每块的 warp 数)做了单变量扫描:

RPB(每块处理行数)扫描,固定 num_warps=8:

RPBt/s相对默认
1(默认)214.60 ± 0.451.00x
2276.90 ± 5.731.29x
4293.82 ± 1.821.37x
8285.53 ± 0.491.33x

num_warps 扫描,固定 RPB=1:

num_warpst/s相对默认
8(默认)214.52 ± 0.331.00x
4254.58 ± 0.751.19x
2308.85 ± 2.081.44x
1290.17 ± 1.071.35x

两个维度的结论一致:block 配置对 MoE decode 吞吐有 ~1.4x 的影响,且最优不在默认值。RPB=4 或 num_warps=2 都能把 214 t/s 提到 290-310 t/s,成本为零(只改配置不改算法)。注意两张扫描是两次独立测量,各自带默认配置行,两次默认值(214.60 与 214.52)的微小差异在噪声范围内。

这组数据给 Agent 优化(第 19.5-19.6 节)提供了第一个抓手:decode 性能不是「算法没选对」,而是「访存没吃饱」——优化方向是让 GEMV 的加载更饱和,而不是换更好的 GEMM。这也呼应第 11.12.5 节:decode 形状算术强度低(I < 10),属于访存受限类,tile/num_warps 的选择要以占用率和访存吞吐为先。

19.5 Agent 优化 KV cache 访问

decode 的第二个算子是 attention:每生成一个 token,query 要和 KV cache 里的全部历史 token 做相似度计算。它的访问模式是 M=1 的读取——读多写一、写完不回读。KV cache 随 context 线性增长:context 越长,KV 读取在 decode 流量里的占比越高。

KV cache 优化的完整轨迹在第 17 章(FA Decode:naive → split-KV,4.17x/5.13x/5.26x;Paged Attention 3.75x),本章不重复,只提炼两个对 Agent 有用的结论:

  1. KV cache 优化的杠杆是结构改变,不是配置搜索。FA Decode 轨迹里 1.83x → 3.4x 来自 block/warp 配置,4.35x → 5.26x 来自 split-KV(把 N 维切成 4 份、grid 翻倍、加一个轻量归约 kernel)。结构改变的前提是 profiling 信号——「grid=32 只填了 GPU 一半的 SM」这个观察才是方向来源。
  2. 优化手段的有效性取决于题目形状。Paged Attention 在 B=64 时 3.75x,但小 N(N=2048)只有 1.46x——split-KV 的多次 dispatch 开销在小题上盖过了并行度收益。Agent 做 KV cache 优化时,第一件事是确认题目大小在哪个区间。

为什么 KV cache 值得单独拆出来?decode 的时间预算里,attention、norm、launch、sync 这类与格式无关的固定开销约占 30%(时间分解推导见第 19.6.1 节)。KV cache 读取属于其中;当 context 变长,它从固定开销变成增长项——这是 decode 吞吐主线(权重带宽,第 19.4 节)之外的第二个变量。

19.6 Agent 优化精度选择

decode 的吞吐主线是「每 token 读取量 ÷ 带宽」(第 19.4 节),那是不是量化越低越好?下面这个真实实验会推翻这个直觉——精度选择和调度配置是组合实验,单独看任何一个都会得出错误结论

实验:LFM2.5-8B-A1B 的 Q4_K_M(4.79 GiB)vs Q8_0(8.4 GiB),llama.cpp ROCm 后端,RX 7900 XTX,512 prompt / 128 gen,5 次重复:

格式nwarpsdecode t/svs 该格式默认
Q4_K_M默认250.8 ± 0.7ref
Q4_K_M1257.3 ± 1.0+2.6%
Q4_K_M2251.2 ± 1.5+0.2%
Q8_0默认225.4 ± 0.5ref
Q8_01345.5 ± 1.2+53.3%
Q8_02333.4 ± 1.8+47.9%
Q8_04284.1 ± 1.1+26.0%

两个反直觉结论:

  1. 默认配置下 Q4 只比 Q8 快 11%(250.8 vs 225.4)——权重小了 1.75 倍,速度只快 11%。原因是 decode 不是纯带宽受限:时间预算里权重 DRAM 读取约占 57%,Q4 的反量化(格式税)约 13%,attention/norm/launch 等固定开销约 30%(推导见 19.6.1)。权重减半 ≠ 时间减半
  2. 统一 nw=1 后 Q8 反超 Q4(345.5 vs 257.3,Q4/Q8 = 0.745x)——Q8 反而快 34%,与 NVIDIA 完全相反(3080: 1.148x、5060 Ti: 1.232x,都是 Q4 更快)。

原因不在格式本身,而在默认调度:llama.cpp #23528 之后,Q4_K_M 进了 RDNA3 的 nw=1 白名单(默认就单 warp),Q8_0 不在白名单(默认 nw=8)——所以 Q8 切 nw=1 释放了 +53.3%,Q4 只有 +2.6%。把两者都拉到 nw=1 的公平起点,AMD 上 Q8 的简单反量化路径(8-bit 直接转 fp16、无 scale/min 位域解包)反而更吃流水线:

指标30805060 Ti7900 XTX
Q4/Q8 @ nw=11.148x1.232x0.745x
Q8 nw=1 增益+84.5%+141.4%+53.3%

19.6.1 Q4 反量化贵在哪:vecdot 指令级实测

E2E 表格只能看到「结果」。Q4 反量化到底消耗多少计算量,可以直接采样:用 HIP 微基准对 Q4_K 和 Q8_0 的 vec_dot 路径做 2048 个 block、每轮 16.4 亿次调用的实测(RX 7900 XTX,gfx1100):

路径ps/block相对
Q8_0 full vec_dot5.11.00x
Q4_K full vec_dot9.11.77x

单次调用慢 77%。看每条路径的指令构成(源码静态分析,每 block):

操作Q8_0Q4_K
dp4a(整数点积)28
浮点乘加110
4-bit nibble 解包04
scale 位域解包04 条位操作
每次调用的数据读取~16 B~34 B

Q4_K 的指令数是 Q8_0 的 10 倍(K-quant 的双 dot product、逐 sub-block 的 scale/min 修正),但实测只慢 1.77 倍——原因:dp4a 在 RDNA3 上只需 1 个周期,浮点操作 4 周期延迟但可被编译器与 dp4a 交错隐藏,真正的差异大头是内存:Q4_K 每次调用读 ~34B vs Q8_0 的 ~16B(2.1 倍流量),且 144B 的 block 跨 2 条 128B 的 L1 cache line(34B 只需 1 条)。

但在 MMVQ kernel 的每一行(4096 元素)里,这个差距又被调用次数抵消:

Q8_0Q4_K
vec_dot 调用/线程/行168
每行 vec_dot 时间/线程16 × 5.1 ps = 82 ps8 × 9.1 ps = 73 ps

vec_dot 层面 Q4_K 每行反而略快(调用次数减半抵消了单次慢 1.77x)。所以 Q4 的「格式税」不在 vec_dot 内部,而在 kernel 外层:更大的 block 跨 cache line、MMVQ 主循环里的索引与 scale 解包开销、以及共享内存归约相对占比的变化——这些都只能在 E2E 层观察到,微基准测不到。

时间占比:decode 的时间不是「读权重」一项。从 3080 上的实测反推(Q8_0 为 baseline,nw=1):

  • 权重 DRAM 读取约 57%
  • Q4 反量化(格式税)约 13%
  • attention / norm / launch / sync 等固定开销约 30%

由此可以算出 Q4 的理论上限:如果反量化开销为零,加速上限是 1 / (0.43 + 0.57/1.75) = 1.32x;加上 13% 的反量化税,实际只有 1 / (0.43 + 0.57/1.75 + 0.13) = 1.13x——与实测的 1.12x 吻合。权重减半带来的带宽红利,大部分被 30% 的固定开销和 13% 的格式税吃掉了。 这也解释了为什么 nw=1 会缩小 Q4/Q8 的差距(3080 上从默认的 1.37x 缩到 1.12x):wave/occupancy 开销越少,Q4 的反量化占比越大,带宽优势越被稀释。

prefill 的结论也不同:Q4 7022 t/s vs Q8 3088 t/s——Q4 在 prefill 上快 2.3 倍(Q4 走更简单的量化路径)。所以「哪个精度更好」在 decode 和 prefill 是两个答案。

还有一个测量方法论的教训:热缓存微基准会系统性高估小权重的量化收益。7900 XTX 上同一批权重的冷/热缓存对照显示,量化收益的交叉点从热缓存下的 ~105MB 左移到冷缓存下的 ~50MB;而真实 decode 的权重以 GB 计,远大于 96MB Infinity Cache,是纯 DRAM 流式访问。Agent 评估精度选择时,必须用真实 e2e 的测量,不能用微基准的数字外推

给 Agent 的落地结论:

  • 精度 × 调度(nwarps/RPB)是组合实验:只换格式不换调度,会得到「Q4 更好」的错误结论;
  • 工具封装(第 15 章)时,benchmark 工具要支持「格式 + 调度参数」同时作为输入,输出按组合标注;
  • AMD 消费卡的精度选择空间有限(FP8/INT4 张量核路径不可用,见第 19.1 节),GGUF Q4/Q8 与 AWQ 之外没有更细的档位。

19.7 单卡边界与下一步

本章在单卡上能做的就到这里,三重边界:

  1. 显存边界:Q4_K_M 4.79 GiB 全量驻留是前提;batch 或 context 增长时,KV cache 与权重争 16GB 总量。fp16 8B(约 17 GB)在单卡上不可行——量化不是可选项。
  2. 生态边界:消费卡(RDNA3)没有 FP8 指令、没有 INT4 翻倍吞吐(第 19.1 节),精度选择止步于 GGUF/AWQ;PagedAttention、多卡 TP、并发调度均留给 hello-mlsys(第 19.5 节的 Paged Attention 只是教学 kernel,不是生产实现)。
  3. Agent 边界:本章 Agent 能做的是受控实验(换配置、换格式、跑对比),不能发明新算子结构——第 15-17 章已经实证了这个边界(纯 Agent 自动优化贡献接近 0%,关键洞察全部来自人)。

本章小结

  • decode 与 prefill 瓶颈不同:prefill 是算力受限的 GEMM,decode 是访存受限的 GEMV。
  • decode 的吞吐主线是「每 token 读权重的字节数 ÷ 显存带宽」,MoE 量化模型尤其明显(A1B 稀疏激活让每 token 读取量远小于全量权重)。
  • block 配置(RPB / num_warps)对 MoE decode 有 ~1.4x 影响,且默认值不是最优——这是零成本的调优空间。
  • 精度选择必须与调度配置一起看:默认配置下 Q4 只比 Q8 快 11%,统一 nw=1 后 Q8 反超 34%(AMD 与 NVIDIA 结论相反);prefill 上 Q4 快 2.3 倍。
  • decode 不是纯带宽受限:权重读取 ~57%、格式税 ~13%、固定开销 ~30%——权重减半 ≠ 时间减半。
  • 本章实验数据在 RX 7900 XTX(gfx1100)+ llama.cpp 上实测,书基线 RX 9070 XT 上可用同样流程复测;热缓存微基准不可外推。

延伸阅读

  • llama.cpp ROCm 构建与 GGUF 量化格式(官方 README 的 ROCm 构建说明与 GGUF 文档)
  • 第 17 章 多轮优化实战 — FA Decode 的 M=1 注意力优化轨迹,decode 的另一半算子
  • 第 11.12 节 tile 形状怎么选 — 访存受限 shape 的配置选择规则
  • llama.cpp llama-bench 工具与 LLAMA_MMVQ_NWARPS / LLAMA_MMVQ_RPB 环境变量——本章 RPB/nwarps 扫描所用的实验协议
  • 第 15 章 工具封装 — 把本章的受控实验协议封装成 Agent 可调用的工具