欢迎光临
我们一直在努力

单张 R9700 跑 Qwen3.8 27B:把“能跑“熬成“好用“的调优实录

/* 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。

手头有两个量化版本:

格式文件大小单流解码可用于 KV 的显存
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 的桌面机,退一步就是全线陪葬。

教训:

  • fp8 在 32 GiB 卡上余量只有 5.5 GiB,headroom 只能给 512 MiB(给 2048 会把 KV 压到不可用),并发和上下文都必须收紧;
  • 老脚本必须逐项核对,不能因为"以前能跑"就信它;
  • 顺便一提:出问题时 systemd-run –scope 也挂了(Failed to start transient scope unit: Connection timed out),意味着 MemorySwapMax=0 这道 swap 保护失效——这是我后来发现宿主机更容易被拖垮的间接原因。

  • 七、最终五套配置

    脚本量化并发上下文headroom定位
    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 单流解码

    配置TTFT解码
    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(冷)

    配置promptTTFTprefill解码
    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 前缀缓存(编程场景的关键)

    配置冷启动热命中复用 token加速
    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 并发聚合

    配置聚合吞吐各序列 TTFT
    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 路并发,足够支撑一个真正能用的本地编程助手。

    单卡大模型调优的核心心法可以浓缩成三句话:

  • 先算显存账(权重 → KV → 激活区 → headroom),再谈配置;
  • 量化精度选低一档(mxfp4 > fp8),访存受限场景下省字节就是提速;
  • 缓存是最大的杠杆(前缀缓存 + checkpoint),重复前缀多的场景收益最高。

  • 测试环境:单张 AMD Radeon AI PRO R9700,radiance + HIP 7.16,Ubuntu 24.04.5,kernel 7.0.0-42。所有数字均为本机实测,欢迎在评论区交流指正。

    赞(0)
    未经允许不得转载:171主机测评 » 单张 R9700 跑 Qwen3.8 27B:把“能跑“熬成“好用“的调优实录
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址