双 RTX 3090 部署 35B MoE 模型:从 llama.cpp 到 SGLang 的选型与调优实录
产品经理丢过来一个需求:在本地工作站上同时跑两个大模型服务——一个 27B 稠密模型,一个 35B MoE 模型,互不干扰。预算不允许买 H100,只有两台 RTX 3090(每张 24GB 显存)。llama.cpp 简单但面对 MoE 架构优化有限,SGLang 高效但配置复杂得像在走钢丝。这篇文章记录了从框架选型、环境搭建,到最终稳定运行的完整过程,以及三个“坑”的解法——如果你也在用消费级显卡跑大模型,这些经验可能让你少走半天弯路。
为什么选 SGLang 而不是 llama.cpp
先说说硬件底牌:AMD Ryzen 9 5950X(16核32线程)、64GB 内存、两张 RTX 3090。一张跑 Qwen3.6-27B(用 llama.cpp),另一张跑 Qwen3.6-35B-A3B MoE(准备用 SGLang)。
llama.cpp 的好处是上手快、社区生态成熟、CPU 和 GPU 混合推理也稳定。但它对 MoE 架构的优化比较浅——它把 MoE 当作普通稠密模型一样逐层推理,没有利用专家并行(Expert Parallelism)和内核融合(Kernel Fusion)来降低通信开销。对于 35B 但只激活 3B 参数的 MoE 模型,llama.cpp 的吞吐只有 80-90 t/s,显存利用率也不高。
SGLang 的优势恰恰是专门针对 MoE 和长上下文场景做了深度优化:Indexer Prologue Fusion 把 12 个 decode kernel 合并为 4 个,Breakable CUDA Graph 降低每步启动开销,Spec V2 让 draft 生成与目标模型流水线化。这些技术写在论文里很高大上,实际效果如何?先跑个测试。
部署的第一个坑:版本兼容性与运行时补丁
拉取 SGLang v0.5.15 镜像(2026年7月10日发布),配置 docker-compose,启动容器——然后撞上第一个错误:
ValueError: num_hidden_layers (40) must be equal to the number of layer_types (16)
检查模型配置文件 config.json,发现 layer_types 只有 16 条(因为 MoE 用循环 pattern 省略了重复条目),但 transformers 5.8.1+ 的 validate_layer_type() 函数要求两者严格相等。这是模型作者 selode-ai 的魔改版 Qwen3.5 MoE,兼容性需要 runtime patch。
修复方法:写一个 launch 脚本,在 transformers 加载配置前 monkey-patch 这个校验函数,遇到 qwen3_5_moe 模型类型时跳过验证。代码不复杂:
from transformers import Qwen3Config
def patched_validate(layer_types, num_hidden_layers):
if hasattr(Qwen3Config, 'model_type') and 'qwen3_5_moe' in str(Qwen3Config):
return
# 否则正常校验
pass
第二个补丁更简单:SGLang 启动时注册 qwen3_asr 模型类型,但新版 transformers 原生也注册了同一个类型,冲突报错。给 AutoConfig.register 加上 exist_ok=True 即可。我已经把这个补丁提交给了 SGLang 项目(PR #30935),两行代码,防止以后别人踩坑。
经验:新框架 + 魔改模型 = 至少预留两小时处理兼容性问题。
灵魂拷问:Breakable CUDA Graph 到底能不能用
官方文档说 v0.5.15 默认启用 Breakable CUDA Graph(BCG),能降低 kernel 启动开销,带来约 8% 的 decode 加速。我满怀期待地启动——然后 crash:
TypeError: Unsupported BCG output type: LogitsProcessorOutput
BCG 和 Qwen3.5 MoE 的 LogitsProcessor 输出类型不兼容。退回到默认的 full 后端。但 full 后端也不是省油的灯:24GB 显存跑 35B MoE 实在太紧了。
模型权重加载后占用 18.68 GB,Mamba state 缓存 1.10 GB,KV 缓存 1.60 GB,CUDA Graph 捕获时只剩约 1.48 GB。关键参数 --mem-fraction-static 设成 0.90 时捕获不稳定,调到 0.92 才成功——但调成 0.92 意味着显存分配非常激进,多一分则溢,少一分则崩。
另一个救命参数是 --mamba-backend。SGLang 默认用 triton 后端,但切到 flashinfer 后 Mamba state 缓存更紧凑,给 CUDA Graph 留下了关键的一点点余量。最终 CUDA Graph 捕获耗时 190 秒——因为 200 个专家的 MoE 需要 Triton JIT 编译,这个等待是正常的,不是卡死。如果你的 watchdog 默认 300 秒,别急着关。
最终参数配置:一把钥匙开一把锁
经过多轮调优(每改一个参数就要等 3 分钟的 CUDA Graph 捕获,然后压测),最终启动命令如下:
python3 /launch_selode.py \
--model-path /models/selode-35b/Qwen-3.6-35B-A3B-VRAP-4-bit-AWQ-21.2GB \
--port 30000 --host 0.0.0.0 \
--dtype bfloat16 \
--context-length 65536 \
--mem-fraction-static 0.92 \
--mamba-backend flashinfer \
--mamba-full-memory-ratio 0.7 \
--mamba-radix-cache-strategy extra_buffer \
--enable-mixed-chunk \
--enable-dynamic-chunking \
--trust-remote-code \
--disable-custom-all-reduce \
--tool-call-parser qwen3_coder \
--reasoning-parser qwen3-thinking
几个关键参数的取舍,整理成表格:
| 参数 | 值 | 原因 |
|---|---|---|
| `–mem-fraction-static` | 0.92 | 24GB 卡上的极限值。>0.92 导致 CUDA Graph 捕获失败;<0.90 导致 Mamba cache 分配不足(KV 池从 83K tokens 缩到 54K) |
| `–mamba-backend` | flashinfer | **必须设**。triton 后端在 CUDA Graph 捕获时显存不够 |
| `–cuda-graph-backend-decode` | 不设(默认 full) | breakable 与 Qwen3.5 MoE 不兼容 |
| `–kv-cache-dtype` | 不设(默认 bf16) | RTX 3090(Ampere)无原生 fp8 Tensor Core,fp8 会破坏 CUDA Graph 兼容性 |
| `–context-length` | 65536 | 128K 在 24GB 卡上不可达,65K 是安全上限 |
一个教训:我之前想“多留点显存”,把 mem-fraction-static 从 0.92 调到 0.88,结果 KV 池缩到 54K tokens,差点跑不满 64K 上下文。回头翻文档,发现官方早写了这个结论——读文档比瞎调重要。
性能结果:v0.5.15 的优化真的有提升
用实际 API 请求压测:
短文本生成(200 tokens 输出)
第一次请求(含 JIT 预热):109.3 t/s
稳定态:130.1 t/s
相比之前文档记录的 121 t/s,提升了约 7%。这主要归功于 v0.5.15 的 Indexer Prologue Fusion(12 个 kernel 合并为 4 个)和 Breakable CUDA Graph 默认开启(虽然我们没用 breakable 后端,但 full 也能吃到部分优化)。
长上下文压力测试
| Prompt 长度 | 总耗时 | 状态 |
|---|---|---|
| 12,016 tokens | 2.62s | ✅ |
| 31,209 tokens | 4.55s | ✅ |
| 40,865 tokens | 6.49s | ✅ |
| 65,009 tokens | 7.77s | ✅ 满负载通过 |
65K 上下文时 KV 池使用约 65K/83.6K = 78%,还有约 18K tokens 余量。如果业务需要更长的上下文,必须换显存更大的卡或者用更激进的量化。
这个方案适合什么场景,不适合什么
适合:
– 已有双 RTX 3090 或 RTX 4090 的工作站/服务器,预算有限但需要同时运行两个模型服务
– 35B 左右的 MoE 模型作为主推理模型,并需要支持长上下文(如对话、代码生成)
– 愿意花半天时间做配置调试,追求高吞吐和低延迟
不适合:
– 只有单卡 24GB 显存:跑 35B MoE 太勉强,建议改用 27B 稠密模型
– 需要 128K 以上长上下文:必须换 A100 或 H100
– 希望“开箱即用”:SGLang 的配置复杂度比 llama.cpp 高一个量级,不是拿来就能跑
几个能带走的经验
- CUDA Graph 捕获需要 3 分钟——别急,等着。 200 专家 MoE 的 Triton JIT 编译就是这个速度。如果 5 分钟还没完成,检查显存余量。
- 每改一个参数都要重新跑压测,因为参数之间有强耦合。我一开始堆了 8 个冗余参数,后来逐个清理,只保留必要的配置,性能反而提升了。
- fp8 KV Cache 在 RTX 3090 上没用——Ampere 架构没有原生 fp8 Tensor Core,计算时软件反量化到 bf16,还会破坏 CUDA Graph。保持 bf16 默认即可。
你可能会遇到类似的场景:手头只有消费级显卡,却要跑超大规模 MoE 模型。希望这份踩坑记录能帮你节省一个周末——至少你知道 Breakable CUDA Graph 在 Qwen3.5 MoE 上不能用,也知道mamba-backend flashinfer是救命稻草。你遇到过其他的“显卡不够”但“非跑不可”的尴尬吗?欢迎留言聊聊,也许下一个方案就在你的问题里。
