我的双 3090 工作站,为何最终全投向了 SGLang

我的双 3090 工作站,为何最终全投向了 SGLang

硬件:双 RTX 3090 24GB · AMD Ryzen 9 5950X · 64GB RAM
模型:Qwen3.8-27B (AutoRound W4A16 / IQ4_XS GGUF)
引擎:llama.cpp + SGLang v0.5.17
日期:2026-08-17
⚠️ 本文所有数据均为本机实测,结论只来自验证过的实验,不含理论推测。

一、从一个”秒失败”开始

深夜,我对着 AI 编程助手(deepseek harness)下达了一个再平常不过的指令:

“请评估这份公司业务发展报告。”
然后它秒失败了。
日志显示:500 Internal Server Error,重试后 Connection error。输入输出 token 全是 0 —— 模型根本没开始推理就崩了。
这不是普通的报错。我开始了长达一天的排查,最终竟然把整个 GPU0 的推理引擎都换了。

二、第一次抓到的”真凶”:GDN 架构的 prefill 困境

2.1 现象:长上下文回填极慢

排查日志时,我发现了一个反常现象。同样是处理 prompt,速度差异巨大:

Prompt 大小 llama.cpp prefill 速度
972 tokens 208 t/s
2,000 tokens 112 t/s
6,144 tokens 25~40 t/s
21,000 tokens <25 t/s,300 秒未完成

而我的 dsh 请求带了 14000+ token 的报告全文 —— 光”读进去”就要 8~10 分钟,远超客户端的 300 秒超时。

2.2 根因:Gated Delta Net 是顺序扫描

翻开 llama.cpp 源码,找到了一行扎眼的注释:

//TODO: Add chunked kernel for even faster pre-fill
for (int t = 0; t < n_tokens; t++) {   // 逐 token 顺序循环

Qwen3.8-27B 用了 64 层混合架构,其中 48 层是 Gated Delta Net(SSM,状态空间模型)。SSM 的本质是”每个 token 的状态依赖前一个 token”——无法像 Transformer 那样并行 prefill
翻译成人话:别人的模型是”一次读进一整页书”,而 GDN 是”逐字逐句读”。上下文越长,回填越慢。

2.3 决定性对照:SGLang 快 20 倍

同样的 20000 token prompt:

引擎 prefill 耗时
llama.cpp >300 秒(未完成)
SGLang 2.0 秒

SGLang 的 triton chunked SSM 实现解决了顺序扫描问题。

这个差距彻底颠覆了我的架构认知。

三、192K 上下文?看起来很美,其实没用

我原来的 GPU0 用 llama.cpp 跑 192K 上下文。看起来参数很豪华,但算一笔账:

llama.cpp:填满 192K 需要 2.1 小时(192000 / 25 t/s)

SGLang:填满 100K 只要 77 秒

> 就像一辆标称续航 600 公里、但充满要 2 小时的车,和一辆标称 500 公里、15 分钟充满的车——后者才是真正能用的。

结论很残酷:192K 上下文因为回填太慢而实际不可用。SGLang 的 100K 才是真能用的上下文。

于是我做了一个大胆的决定:把 GPU0 也从 llama.cpp 切换到 SGLang。

四、双 SGLang 之路:三次 OOM 踩坑实录

切换过程远没有想象中顺利。我经历了三次显存 OOM 崩溃,每次都是同样的报错:

torch.OutOfMemoryError: CUDA out of memory.
Tried to allocate 24.00 MiB.
GPU 0 has a total capacity of 23.54 GiB of which 25.12 MiB is free.

4.1 坑一:mem-fraction 调太高

mem-fraction 控制 SGLang 的显存池。我试了 0.93、0.92,推理时都崩溃。
排查发现:GPU0 接了显示器,桌面系统(Xorg/GNOME/Firefox)要占 ~0.7GB 显存。这挤压了 SGLang 的推理缓冲,导致 sampler 需要分配 512MB 临时内存时失败。
教训:GPU0 必须给桌面留足余量。 最终 mem-fraction 定在 0.91,可用余量 1.49GB,稳定。

4.2 坑二:KV 池每次启动都不一样

我注意到一个诡异现象:每次启动,KV 池大小都不一样(70045 / 73898)。
翻源码找到了答案:

budget_bytes = int(max(0.0, free_gb - headroom_gb) * (1 << 30)) + ...

KV 池 = (启动时实际空闲显存 – 预留 headroom) / 每 token 占用。每次启动时桌面进程占用不同 → KV 池不同。

4.3 坑三:并发大请求直接干爆显存(真正的秒失败元凶)

回到最初的”秒失败”。终于定位到了:
dsh 发送请求时没设 max_tokens
SGLang 按 SGLANG_MAX_NEW_TOKENS_LIMIT=32000 给每个请求预留生成空间
3 个并发 × (19000 prompt + 32000 生成) = 153000 tokens,远超 KV 池 67375
第 3 个请求进来 → 显存耗尽 → scheduler 崩溃 → watchdog 重启 → 500
修复:把 limit 从 32000 降到 16384。3 并发最大预留 49152 < 池 67375,压力测试通过。

五、最终架构:双卡 SGLang 各司其职

GPU 引擎 上下文 mem-fraction 输出上限 定位
GPU0 SGLang 69,632 0.91 16,384 日常对话/代码/视觉(接显示器,预留桌面)
GPU1 SGLang 102,400 0.93 32,000 opencode 主/子代理并发 + 长上下文

稳定性验证(全部实测)

GPU0:3 并发 × 14000 token 大请求 → 全通过,RestartCount=0

GPU1:3 并发 × 14000 token × 真正长生成 15181 tokens → 全通过,无 OOM

dsh 报告评审任务:流畅运行

六、几点经验总结

一、回填速度决定上下文是否可用。参数上的 192K 不如实际能用的 100K。

二、显存余量是 SGLang 的生命线。接显示器的 GPU 必须给桌面留余量(mem-fraction 0.91 vs 0.93)。

三、客户端必须设置 max_tokens。不设的话,SGLang 会按服务端 limit 给每个请求预留全量生成空间,并发时极易 OOM。

四、压力测试要逼近真实场景。短请求测试看不出问题,必须用”并发 × 大 prompt × 真长生成”来验证。

五、SSM 架构的 prefill 是物理瓶颈。GDN/SSM 层顺序扫描是架构特性,llama.cpp 官方也承认 chunked kernel 未实现——选引擎时要认清这一点。

本文数据全部来自 2026-08-17 本机实测,配置细节见 docs/review/c1skill-gdn-prefill-slow-rootcause-2026-08-17.md 与 docs/review/c1skill-sglang-concurrent-oom-2026-08-17.md。


皖ICP备2025105865号-2|皖公网安备34010402704739号