/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */
#content_views .toc,
/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */
#content_views.markdown_views > p:empty:has(+ .toc),
#content_views.markdown_views > .toc + p:empty,
/* 富文本旧版目录标记 */
#content_views.htmledit_views #main-toc,
#content_views.htmledit_views #hr-toc,
#content_views.htmledit_views p[id*=\”-toc\”] {
display: none !important;
}
/* 目录去掉后,紧跟的首个标题不再多出一块上边距 */
#content_views.markdown_views > .toc + h1,
#content_views.markdown_views > .toc + h2,
#content_views.markdown_views > .toc + h3,
#content_views.markdown_views > .toc + h4,
#content_views.markdown_views > .toc + p:empty + h1,
#content_views.markdown_views > .toc + p:empty + h2,
#content_views.markdown_views > .toc + p:empty + h3,
#content_views.markdown_views > .toc + p:empty + h4 {
margin-top: 0 !important;
}
单张 R9700 跑 Qwen3.8 27B:把"能跑"熬成"好用"的调优实录
摘要:手里只有一张 AMD Radeon AI PRO R9700(31.86 GiB 显存),想拿它当本地的编程助手后端。折腾了几天,中间把整台机器搞宕机过无数次,最后沉淀出 5 套配置,覆盖从"最快响应"到"262k 全文喂入"的全部场景。本文记录全部过程、踩过的坑和最终实测数据,希望帮到同样在单卡上折腾大模型的朋友。
本文关键词:AMD R9700 / 单卡大模型 / mxfp4 量化 / KV Cache 调优 / 前缀缓存 / 本地编程助手
一、硬件与基线
GPU AMD Radeon AI PRO R9700, 31.86 GiB VRAM (gfx1201)
CPU Intel i5-11500, 6C/12T
RAM 30.91 GiB + 8 GiB swap
OS Ubuntu 24.04.5, kernel 7.0.0-42
栈 radiance (HIP 7.16)
模型 Qwen3.8 27B
模型是 dense 的 27B(64 层,hidden 5120,GQA 24/4 head,词表 248320),原生训练上下文 262144。
手头有两个量化版本:
| mxfp4 | 19.4 GB | 215 tok/s | ~13.7 GiB |
| fp8 | 30.8 GB | 144 tok/s | ~4.5 GiB |
第一个反直觉的结论:mxfp4 不只是省显存,它直接快 50%。 27B 这个尺寸在单卡上仍是访存受限(memory-bound),权重精度减半意味着每步要搬的字节减半,解码速度自然上去。省下的 11 GiB 又直接变成了上下文容量。
经验:在 fp8 和 mxfp4 之间犹豫的话,除非有明确的精度要求,无脑选 mxfp4。
二、显存账本:先搞清楚钱花在哪
radiance 启动时会打一份 VRAM 预算报告,这个非常有用,一定要看:
card total 31.86 GiB
already held 608.62 MiB 驱动上下文、code object
driver drift 16.00 MiB
activation arena 1.47 GiB 由 buffer plan 算出
step staging 133.55 MiB
hardware queues 79.72 MiB
–gpu-headroom-mib 96.00 MiB
——————————
claimable 29.48 GiB
static weights 15.70 GiB 扣掉这层才轮到 KV
——————————
elastic 13.78 GiB
kv cache 8.53 GiB
unusable 5.25 GiB 两边都到顶了
几个要点:
- 权重是硬扣的:先扣 15.70 GiB,剩下才是 KV 的地盘;
- state pool 按序列算:f16 下每路约 1.2 GiB,所以 –max-num-seqs 不能乱开(引擎默认 256 需要 74 GiB,纯属自杀);
- unusable 那 5.25 GiB 不是浪费:是 KV 分配器寻址粒度的上限。
三、坑一:VRAM planner 会把 KV 吃到 100%
一开始想配 8 路 × 128k,直接被将死:
E arena: device allocation of 1.49 GiB failed:
rad_dev_alloc: out of memory (hipErrorOutOfMemory)
E startup stopped: out of memory
I kv cache 13.67 GiB 100% of it — 39% backing of the 8 x 131072 worst case
KV cache 份额是 100%,而 1.49 GiB 的 activation arena 是在那之后才申请的,于是分配失败。降并发到 6 也一样——不管几路,planner 都会把 elastic 全部给 KV。
解药是 –gpu-headroom-mib,预先扣下一块不属于 KV 的地:
–gpu-headroom-mib 2048
生效后:
kv cache 11.97 GiB 100% of it — 46% backing of the 6 x 131072 worst case
= 357,424 tokens, 2.7 sessions at 131072
这是本次调优最值钱的一条经验:遇到 arena … out of memory 而不是权重 OOM,八成是 KV 吃太满。别降并发,去加 headroom。
四、坑二:前缀缓存默认是"哑"的
编程场景有个特点——每次对话的前缀(system prompt、仓库上下文、文件内容)高度重复。前缀缓存本该是最大的杠杆,结果我测出来是这样:
32k prompt 冷启动: ttft = 11.1 s
32k prompt 再问一遍: ttft = 11.1 s ← 一点没省
cached_tokens = 0
查日志才发现根因是我自己种的:
–checkpoint-slots 0 # 关掉了快照
–prefix-resume exact 需要恢复快照才能跳过 prefill,而 –checkpoint-slots 0 把快照全关了,于是"命中"了也没东西可恢复。删掉走 auto 之后:
PREFIX 32000 cold=11.37s warm=1.40s cached=30720 → 8.1 倍
编程助手最该打开的两个参数就是 –prefix-cache-mib 和保持 checkpoint 开启。
五、坑三:–max-num-batched-tokens 1000 卡住并发
默认批预算是 1000,单流没感觉,多路并发立刻现原形。改成 4096 后,4 路并发聚合吞吐从 43 tok/s 提到 197 tok/s。
单卡上 4096 是个安全的甜点值——激活区按 batch 算,再往上要小心显存。
六、坑四(最惨):整台机器被 OOM 杀掉
这是最值得写出来的一条,因为它不是配置报错,而是直接把桌面环境打死了。
当时在测一个老的 fp8 脚本,机器突然无响应。事后翻 journal:
14:20:10 gnome-shell invoked oom-killer
14:20:27 Out of memory: Killed process (awesun)
clash / gnome-shell / tracker-miner / xdg-portal 全线被 oom-kill
14:43:57 bluetoothd invoked oom-killer ← 二次触发,随后重启
这是宿主内存 OOM,不是 GPU OOM。
根因是那个脚本从最老的版本继承下来,四项关键配置全是缺失的:
缺 –placement all_vram → 权重摊到 host pool,吃宿主内存
缺 –host-pool-mib 0 → 同上
缺 –kv-cache-dtype fp8 → KV 按 f16 存,显存需求翻倍
缺 –gpu-headroom-mib → KV 吃满,arena 抢不到
fp8 权重已经占了 26.32 GiB(共 31.86 GiB),KV 再按 f16 存,显存必然爆,分配就往宿主内存退——30 GiB 的桌面机,退一步就是全线陪葬。
教训:
七、最终五套配置
| radiance-fast.sh | mxfp4 | 4 | 64k | 2048 | 单请求最快 |
| radiance-code.sh | mxfp4 | 6 | 128k | 2048 | 编程日常推荐 |
| radiance-long.sh | mxfp4 | 1 | 262k | 2048 | 整个仓库一次喂 |
| radiance-fp8-fast.sh | fp8 | 4 | 32k | 512 | fp8 短线 |
| radiance-fp8-long.sh | fp8 | 1 | 64k | 512 | fp8 长线 |
mxfp4 通用骨架:
–served-model-name "Qwen3.8-27B"
–max-num-batched-tokens 4096 # 原 1000,并发救命
–kv-cache-dtype fp8
–prefix-cache-mib 8192 # 编程场景的灵魂
–gpu-headroom-mib 2048 # 防止 KV 吃满挤爆 arena
–placement all_vram
–host-pool-mib 0
–host 0.0.0.0 –port 8000
单卡绝不开 –tp(张量并行)。
八、实测数据(全部冷启动,均已标定)
测法:temperature=0,非流式,计时取服务端返回的 timings。每组 prefill 都加了随机前缀以强制走冷路径。
8.1 单流解码
| fast 4×64k (mxfp4) | 59 ms | 215.7 tok/s |
| code 6×128k (mxfp4) | 39 ms | 218.4 tok/s |
| long 1×262k (mxfp4) | 170 ms | 218.5 tok/s |
| fp8-fast 4×32k | 75 ms | 143.7 tok/s |
| fp8-long 1×64k | 72 ms | 143.9 tok/s |
mxfp4 跨三种并发配置几乎不掉速(215–218),说明序列数对解码影响很小。
8.2 长上下文 prefill(冷)
| fast | 16k | 4.98 s | 3218 tok/s | 180 tok/s |
| fast | 35k | 7.81 s | 2674 tok/s | 163 tok/s |
| code | 33k | — | 2426 tok/s | 183 tok/s |
| code | 72k | — | 1810 tok/s | 168 tok/s |
| long | 68k | 26.94 s | 2432 tok/s | 169 tok/s |
| long | 146k | 54.33 s | 1485 tok/s | 123 tok/s |
| fp8-fast | 20k | 4.00 s | 2853 tok/s | 118 tok/s |
| fp8-long | 38k | 8.62 s | 2515 tok/s | 116 tok/s |
注意 146k 那行:长上下文下解码掉到 123 tok/s。这是长上下文注意力的必然代价(KV 读取 + 注意力平方项),不是配置问题。能拆短就拆短,长上下文是"能用"不是"好用"。
8.3 前缀缓存(编程场景的关键)
| long (32k) | 11.37 s | 1.40 s | 30720 | 8.1× |
| fast (32k) | 1.40 s | 1.41 s | 28672 | 1.0×* |
| code (32k) | 1.37 s | 1.38 s | 28672 | 1.0×* |
| fp8-long (32k) | 2.24 s | 1.43 s | 30720 | 1.6× |
| fp8-fast (17.6k) | 1.23 s | 1.21 s | 16384 | 1.0×* |
* 这三行的"冷启动"其实已经是热缓存了——32k 小于其可用 KV,上一次测试的残留在缓存里。这恰恰说明前缀缓存生效了,只是从外部看不出跳变。long 那行因为 32k 相对 262k 显得突出,冷热差异才显式可见。
8.4 并发聚合
| fast 4 路 | 197.1 tok/s | 2.0–3.5 s |
| code 6 路 | 156.0 tok/s | 2.0–7.8 s |
| fp8-fast 4 路 | 111.1 tok/s | 3.0–6.7 s |
6 路不是 4 路的 1.5 倍(197 → 156),因为 6 路要排队更多 prefill。并发不是越多越好。
九、几条血泪教训
改配置后一定要 curl /v1/models 验证。我至少两次以为新配置生效了,实际跑的是旧进程——pkill 后新实例被端口占用挡住,日志里躺着一个 E 我根本没看。
–served-model-name 不设,模型 ID 就是容器名(qwen35),不同脚本之间不一致,客户端很容易 400。
脚本的头部注释会腐烂。我有两个脚本头部写着 MAX_LEN 200000 / MAX_SEQS 8,实际跑的是 32000 / 4。注释要跟变量一起改。
日志看过期了会骗人。日志里混着好几次启动记录,tail 看到的可能是老进程的 report。要盯时间戳。
测 prefill 一定要加随机前缀。否则第二次测的就全是缓存命中,TTFT 从 26s 变成 0.85s,你会以为自己调优成功了。
PCIe 降速值得留意:
WARN link-below-max: 0001:00.0 runs 16.0 GT/s x16 of its 32.0 GT/s x16
纯算力影响有限,但走 host memory 的东西(流式专家、all-reduce、嵌入原地读)都会受制。
–checkpoint-slots 0 是优化陷阱。看着像省显存,实际把前缀缓存价值清零。
十、给单卡玩家的建议
如果你也在一张 32 GiB 卡上跑 27B 级别的模型,建议:
- 优先选 mxfp4 而不是 fp8:快 50%、省 11 GiB、装得下更多上下文;
- 认真读启动时的 VRAM 报告,特别是 claimable / static weights / elastic 三段;
- KV 吃满 100% 导致启动失败时,用 –gpu-headroom-mib,别急着降并发;
- 编程场景把 –prefix-cache-mib 和 checkpoint 都打开,投入产出比最高;
- –max-num-batched-tokens 从 1000 提到 4096,多路体验质变;
- 上下文不是越大越好:262k 能跑,但 146k 时解码只有 123 tok/s,按需分配;
- fp8 在 32 GiB 卡上是危险动作:余量 5.5 GiB,一个参数没配好就可能把宿主拖垮。
总结
这套东西的定位很清楚:不是替代云 API,而是让"代码不出本地"变得可行——215 tok/s 的单流速度、10 万级 token 的上下文、6 路并发,足够支撑一个真正能用的本地编程助手。
单卡大模型调优的核心心法可以浓缩成三句话:
测试环境:单张 AMD Radeon AI PRO R9700,radiance + HIP 7.16,Ubuntu 24.04.5,kernel 7.0.0-42。所有数字均为本机实测,欢迎在评论区交流指正。


