我的双 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。
