单卡3090部署35B MoE模型:SGLang完整指南
一台24GB的RTX 3090,你想跑一个35B参数的MoE模型——听起来像做梦。但Qwen-3.6-35B-A3B MoE在AWQ 4-bit量化后正好能塞进去,推理引擎SGLang能把它跑得快起来。问题是参数怎么配、补丁怎么打、工具调用怎么开?这篇指南带你一步步走出所有坑,复制粘贴就能用。
为什么是SGLang而不是vLLM或llama.cpp
MoE(混合专家)模型的特点是每次只激活部分参数(这里约3.5B活跃),但整体显存占用取决于总参数。SGLang的RadixAttention、线性前缀缓存和Mamba后端对MoE和长上下文特别友好。实测32K上下文下,SGLang的prefill比llama.cpp快7倍——这对coding agent频繁读取长历史至关重要。
你需要的东西
- 硬件:NVIDIA RTX 3090 24GB(单卡)
- 软件:Docker、NVIDIA Container Toolkit(CUDA 12+)
- 模型文件:selode-ai/Qwen-3.6-35B-A3B-VRAP-4-bit-AWQ-21.2GB(已量化AWQ,约21.2GB)
- 容器镜像:
lmsysorg/sglang:latest
我的机器上还有另一张3090跑着27B的llama.cpp做视觉任务,但这不影响——只要让两个容器分别绑定不同GPU。
第一步:准备启动脚本 launch_selode.py
这是最关键的一步。SGLang需要调用一个Python脚本来启动,我们不能直接用python3 -m sglang.launch_server——因为需要打两个补丁。
创建一个文件/models/launch_selode.py,内容包含两个补丁和启动逻辑:
import sys
import os
# ========== 补丁1:Layer Types验证跳过 ==========
import transformers.configuration_utils as cu
_orig_validate = cu.PretrainedConfig.validate_layer_type
def _patched_validate(self):
mt = getattr(self, 'model_type', '')
if mt and 'qwen3_5_moe' in str(mt):
return
tc = getattr(self, 'text_config', None)
if tc:
tmt = getattr(tc, 'model_type', '')
if tmt and 'qwen3_5_moe' in str(tmt):
return
return _orig_validate(self)
cu.PretrainedConfig.validate_layer_type = _patched_validate
# ========== 补丁2:ASR模型注册冲突 ==========
from transformers import AutoConfig as _AC
_orig_register = _AC.register
def _patched_register(model_type, config, exist_ok=False):
if 'qwen3_asr' in str(model_type):
exist_ok = True
return _orig_register(model_type, config, exist_ok=exist_ok)
_AC.register = _patched_register
# ========== 启动SGLang服务器 ==========
from sglang.srt.server import launch_server_cli
launch_server_cli()
这两个补丁解决什么?
– 补丁1:Qwen3.5 MoE的config.json中layer_types字段只有16个元素但num_hidden_layers=40,老版本transformers会报错。这个补丁跳过验证。
– 补丁2:新的transformers已经原生注册了qwen3_asr模型类型,SGLang再注册一次会冲突。设置exist_ok=True跳过重复注册。
保存这个文件到你的模型目录(例如/models/launch_selode.py)。
第二步:编写Docker Compose配置
创建docker-compose.yaml,注意关键参数:
services:
sgl-selode:
image: lmsysorg/sglang:latest
container_name: sgl-selode
restart: unless-stopped
runtime: nvidia
ulimits:
memlock:
soft: -1
hard: -1
shm_size: 32g
ports:
- "15433:30000"
volumes:
- ./models:/models:ro
environment:
- CUDA_VISIBLE_DEVICES=1 # 绑定GPU1
ipc: host
command: >
python3 /launch_selode.py
--model-path /models/huggingface/selode-ai/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.90
--mamba-full-memory-ratio 0.7
--mamba-radix-cache-strategy extra_buffer
--cuda-graph-max-bs-decode 1
--enable-mixed-chunk
--trust-remote-code
--disable-custom-all-reduce
--tool-call-parser qwen3_coder
--reasoning-parser qwen3-thinking
下面是每个参数的作用——你不必全记住,只要知道哪些不能少即可。
| 参数 | 为何必须 | 如果不加会怎样 |
|---|---|---|
--dtype bfloat16 |
模型内部conv_states是bf16,输入默认是fp16,类型不匹配直接崩溃 | RuntimeError: Index put requires source and destination dtypes match |
--context-length 65536 |
24GB显存下只能到64K上下文,再大会OOM | 服务启动后可能运行中报显存不足 |
--mem-fraction-static 0.90 |
给KV Cache和Mamba状态留空间,0.95也可以但余量只剩~0.6GB | 空闲显存过小,万一有波动就崩 |
--tool-call-parser qwen3_coder |
开启函数调用结构化解析 | 工具调用返回的是纯XML文本而不是JSON对象 |
--reasoning-parser qwen3-thinking |
自动剥离<think>推理块 |
返回内容包含推理过程,客户端难处理 |
--enable-mixed-chunk |
混合分块prefill,提升首次token延迟 | 长上下文时first token稍慢 |
--cuda-graph-max-bs-decode 1 |
单用户场景,bs=1足够 | 不需要改,默认也是1 |
如果你遇到qwen3_asr相关错误,说明补丁2未生效。检查launch_selode.py是否被正确挂载到/launch_selode.py。
第三步:启动容器并等待
执行:
cd /opt/ai-workspace # 你的docker-compose.yaml所在目录
docker compose up -d sgl-selode
docker logs -f sgl-selode
你会看到类似输出:
Loading model…
Patch applied: layer_types validation skipped.
Patch applied: qwen3_asr registration conflict resolved.
…
CUDA Graph capturing… (this may take ~170s)
Uvicorn running on http://0.0.0.0:30000
冷启动时间约200秒(模型加载20s + CUDA Graph捕获170s)。别急,这是正常的。捕获完成后后续推理速度稳定,不会像llama.cpp那样多轮掉速。
第四步:验证服务
启动成功后,执行健康检查和工具调用测试:
# 检查模型列表
curl http://localhost:15433/v1/models
# 测试普通对话
curl http://localhost:15433/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{"model":"default","messages":[{"role":"user","content":"写一段Python冒泡排序"}],"max_tokens":200}'
# 测试工具调用
curl http://localhost:15433/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{
"model":"default",
"messages":[{"role":"user","content":"北京天气?"}],
"tools":[{"type":"function","function":{"name":"get_weather","description":"获取天气","parameters":{"type":"object","properties":{"city":{"type":"string"}},"required":["city"]}}}],
"tool_choice":"auto","max_tokens":100
}'
工具调用返回应该类似:
{
"choices": [{
"finish_reason": "tool_calls",
"message": {
"tool_calls": [{
"id": "call_eec41f94dcc141e78a4becf1",
"type": "function",
"function": {
"name": "get_weather",
"arguments": "{\"city\": \"北京\"}"
}
}]
}
}]
}
如果finish_reason是stop且tool_calls为null,说明--tool-call-parser未生效——检查启动命令是否完整。
性能基准(验证部署效果)
我用同一份文本做了4次独立测试(随机化内容避免前缀缓存干扰),结果如下:
| 上下文长度 | Prompt Tokens | 生成Token数 | 总耗时 | 含prefill速度 |
|---|---|---|---|---|
| 短 | 12 | 200 | 2.3s | 88.9 t/s |
| 1K | ~1050 | 50 | 0.6s | 57.8 t/s |
| 8K | ~7800 | 50 | 1.3s | 31.3 t/s |
| 32K | ~33600 | 100 | 6.2s | 16.1 t/s |
Prefill单独看更惊人:8K上下文中prefill速度达7645 tok/s,32K时8338 tok/s——这得益于MoE稀疏激活和SGLang的chunked prefill。
作为对比,同一台机器上的27B Dense模型(llama.cpp Q4_K_XL)在32K上下文耗时28.8s,而35B MoE只用4.0s,快了7倍。这差距来自MoE活跃参数少、RadixAttention前缀缓存、以及FlashInfer Mamba后端。
常见问题
Q1: 启动后卡在“CUDA Graph capturing…”
正常。第一次捕获需~170s,之后就不再重录。如果你等超过5分钟,检查日志是否有错误。
Q2: curl测试返回502
服务还在启动中。用docker logs -f sgl-selode看进度。最后一行出现“Uvicorn running”才就绪。
Q3: 报错“ValueError: layer_types has 40 elements but num_hidden_layers is 16”
说明补丁1未生效。检查launch_selode.py是否在加载任何transformers组件之前执行了补丁代码。
Q4: 运行一段对话后显存OOM
降低--context-length到32768,或--mem-fraction-static到0.85。24GB显存下64K上下文+0.9是上限,空闲只剩~1.7GB。
Q5: 工具调用返回纯文本XML
确认启动命令包含--tool-call-parser qwen3_coder --reasoning-parser qwen3-thinking。
最终确认的一键启动命令
如果你不想自己写脚本,这里是一个完整可用的docker-compose命令块(假设模型路径为/models/huggingface/selode-ai/Qwen-3.6-35B-A3B-VRAP-4-bit-AWQ-21.2GB,启动脚本在/models/launch_selode.py):
cd /opt/ai-workspace
cat > launch_selode.py << 'EOF'
import sys, os, transformers.configuration_utils as cu
_orig = cu.PretrainedConfig.validate_layer_type
def _p(self):
mt = getattr(self,'model_type','')
if mt and 'qwen3_5_moe' in str(mt): return
tc = getattr(self,'text_config',None)
if tc:
tmt = getattr(tc,'model_type','')
if tmt and 'qwen3_5_moe' in str(tmt): return
return _orig(self)
cu.PretrainedConfig.validate_layer_type = _p
from transformers import AutoConfig as _AC
_orig_reg = _AC.register
def _p_reg(mt, cf, eo=False):
if 'qwen3_asr' in str(mt): eo=True
return _orig_reg(mt,cf,exist_ok=eo)
_AC.register = _p_reg
from sglang.srt.server import launch_server_cli; launch_server_cli()
EOF
docker run -d --gpus '"device=1"' --shm-size=32g \
--name sgl-selode -p 15433:30000 \
-v $(pwd)/launch_selode.py:/launch_selode.py:ro \
-v $(pwd)/models:/models:ro lmsysorg/sglang:latest \
python3 /launch_selode.py \
--model-path /models/huggingface/selode-ai/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.90 \
--mamba-full-memory-ratio 0.7 \
--mamba-radix-cache-strategy extra_buffer \
--cuda-graph-max-bs-decode 1 \
--enable-mixed-chunk --trust-remote-code \
--disable-custom-all-reduce \
--tool-call-parser qwen3_coder \
--reasoning-parser qwen3-thinking
等200秒后,用上述curl测试即可。
总结
单卡3090跑35B MoE不再是纸上谈兵。核心三个要点:必须加--dtype bfloat16避免类型崩溃,两个transformers补丁缺一不可,--tool-call-parser qwen3_coder和--reasoning-parser qwen3-thinking一起开启才能正常使用工具调用。性能上,长上下文场景比27B Dense模型快5-7倍,尤其适合coding agent。如果你也在24GB卡上部署过MoE模型,遇到过什么奇怪的错误?欢迎留言分享你的踩坑经历。
