单卡3090部署35B MoE模型:SGLang完整指南

单卡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.jsonlayer_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_reasonstoptool_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模型,遇到过什么奇怪的错误?欢迎留言分享你的踩坑经历。


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