海光 K100AI DCU(8 卡)部署 Qwen3.8-27B 实践指南(SGLang 篇)

上一篇:海光 K100AI DCU (8卡) 部署 Qwen3.8-27B 实践指南(VLLM篇)


一、背景与目标

本文产生的背景是使用VLLM完成了Qwen3.8 27B模型推理,结果客户有一个硬指标,希望5个并发的时候TPOT(Time Per Output Token,单 Token 输出延迟)在20ms以内,也就是每个Token的产生时间要在20ms以内,虽然海光的社区给了SGLang推理Qwen3.8-27B的的参考,但是非流式输出中文乱码,TPOT达不到20ms,这篇文章是基于真实部署与压测过程的排坑记录,从容器化、NUMA 拓扑绑定、Router 负载均衡、思考模式参数配置、开启投机采样加速到最终性能压测,完整覆盖了在海光 K100AI DCU 国产算力平台上用 SGLang 服务化部署 Qwen3.8-27B 的全过程。

最终结论速览

在 8 卡 K100AI(每卡 64GB 显存)上,采用TP=8 单实例 + EAGLE/MTP 投机采样 ,在并发 5 的长文本场景下,TPOT p50 低至18.35ms 、单用户解码速度51.2 tok/s 、系统吞吐215.79 tok/s,成功达成 "TPOT ≤ 20ms" 的目标。

本文涉及的硬件环境为:

  • 加速卡:海光 K100AI DCU × 8,每卡 64GB 显存;

  • CPU 架构 :双路 CPU,系统识别出 8 个 NUMA 节点(Node 0~7),卡与 CPU 之间依赖 PCIe 总线交换数据;

  • 推理框架:SGLang 0.5.12(海光定制镜像);

  • 官方部署手册点此访问

  • 模型:Qwen3.8-27B。

整个调优过程经历了 "启动参数docker-compose化 → CPU/GPU拓扑绑定 → 双实例 Router → 输出乱码排障 → 投机采样加速 → TP=8 单实例" 等多个阶段,文章最后提供了8卡单实例、SGLang Router路由2 * 4卡两个完成的docker-compose.yaml,复制即可用。


二、环境与软件栈

2.1 镜像与版本

海光 DCU 平台必须使用适配 DTK(海光开发工具套件)的 SGLang 定制镜像,本文使用的镜像为:

复制代码
harbor.sourcefind.cn:5443/dcu/admin/base/custom:sglang0.5.12-ubuntu22.04-dtk26.04-py3.10-20260620

2.2 关键环境变量(ROCm/DCU 算子开关)

启动脚本中需要设置以下环境变量,用于开启海光 DCU 上对应的融合算子与优化,这些参数从官方手册中复制:

环境变量 含义
SGLANG_ALLOW_OVERWRITE_LONGER_CONTEXT_LEN 1 允许覆盖更长的上下文长度
SGLANG_USE_LIGHTOP 1 启用 LightOp 轻量算子
SGLANG_USE_MARLIN_W16A16_MOE 1 MoE 权重 W16A16 Marlin GEMM
SGLANG_USE_FUSED_TOPK_SOFTMAX 1 融合 TopK + Softmax
SGLANG_ROCM_USE_AITER_MOE False 关闭 AITER MoE 实现
SGLANG_USE_CAUSAL_CONV1D 1 启用 Causal Conv1D
SGLANG_USE_CUDA_IPC_TRANSPORT 1 启用 IPC 传输,配合 ipc: host
SGLANG_USE_AITER_LINEAR_ATTN 1 启用 AITER Linear Attention

三、容器化部署:从 docker run 启动命令转换到 Docker Compose

3.1 DCU 容器必须的设备与权限映射

与 NVIDIA GPU 只需 --gpus all 不同,海光 DCU / ROCm 环境在容器内需要映射设备节点和权限:

配置项 docker run 参数 docker-compose.yaml
设备映射 --device=/dev/kfd --device=/dev/dri --device=/dev/mkfd devices: - /dev/kfd:/dev/kfd - /dev/dri:/dev/dri - /dev/mkfd:/dev/mkfd
特权模式 --privileged privileged: true
共享内存 --shm-size 256g shm_size: 256g(双实例建议根据总内存大小调整,我总内存512G,单实例设置 196g)
附加权限 --cap-add=SYS_PTRACE cap_add: [SYS_PTRACE]
用户组 --group-add video group_add: [video]
seccomp --security-opt seccomp=unconfined security_opt: [seccomp:unconfined]
驱动目录 -v /opt/hyhal/:/opt/hyhal/:ro volumes: - /opt/hyhal/:/opt/hyhal/:ro

要点/opt/hyhal/ 是海光驱动(HAL)目录,必须挂载进容器并在 environment 节点配置 LD_LIBRARY_PATH=/opt/hyhal/lib:/opt/rocm/lib:${LD_LIBRARY_PATH},否则 torch 导入时会因为找不到 librocm_smi64.so`` ``.2 而直接崩溃(见第 7 节的排障)。

3.2 模型挂载路径

--model-path 需要指向容器内实际可访问的路径。本文的挂载方式为:

yaml 复制代码
volumes:
 - /opt/models/Qwen3.8-27B/model/:/model/

即宿主机模型目录挂载到容器内 /model/,启动命令中使用 --model-path /model/SGLang Router也需要挂在模型目录,和模型推理的挂载必须相同


四、TP/PP 与上下文长度的选择

4.1 256K 超长上下文:TP=8 + PP=1

当需要支撑 256K 超长上下文时,推荐的并行配置为:

yaml 复制代码
 --tp-size 8
 --pp-size 1

核心理由

  1. 显存瓶颈全在 KV Cache。27B 模型 FP16 权重约 54GB,8 卡 TP=8 切分后每卡仅需 3.5~7GB 存放参数;但单个 256K 请求的 KV Cache 在 FP8 下也要消耗约 20~30GB 显存,需要分散到 8 张卡上。

  2. 必须开启 FP8 KV Cache--kv-cache-dtype fp8_e5m2(或 fp8_e4m3)。若不开启,256K 的 KV Cache 显存占用翻倍,64GB 很容易 OOM。

  3. 不建议用 PP:单机 8 卡内部高带宽互联,TP 通信效率极高;PP 会引入 Bubble 等待,且 PP=2/TP=4 容易造成显存与计算不均。

  4. 长上下文场景建议调低静态显存占比,为动态 KV Cache 预留空间:--mem-fraction-static 0.80

4.2 128K 上下文:不需要任何 RoPE 覆写

官方手册给了激进的长上下文 RoPE 覆写参数 --json-model-override-args,如果实际业务只需要 128K 上下文不要 配置 --json-model-override-args 那套激进的 RoPE 覆写参数,只需要把上下文长度设置为 --context-length 131072(128 × 1024)。

原因见第 8 节 ------ 激进的 YaRN 外推参数反而会在纯文本模型上引发严重的输出乱码。


五、围绕 TPOT 目标的并发与 CUDA Graph 调优

5.1 目标并发 5、压测峰值 70,--max-running-requests 该设多少?

结论:设为 5 ~ 8,绝不要因为压测峰值到过 70 就设成 70。

理由:

  1. TPOT 依赖算力与显存带宽。并发从 5 升到 70 时,Batch 增大导致显存带宽被瓜分,TPOT 会呈线性甚至指数级恶化(50~100ms+),直接击穿 20ms SLA。

  2. 256K 长上下文显存压力大。并发过大时系统会强制触发 Chunked Prefill 或 Request Eviction,延迟剧增甚至卡死。

  3. 用队列承接突发流量 。把内部 --max-running-requests 限制在 5~8,外部冲进 70 个请求时,多出的请求在 HTTP 队列等待,牺牲 TTFT、保住 TPOT。

参数建议

模式 --max-running-requests 说明
严格 SLA 5 全力保证 TPOT ≤ 20ms
带缓冲 8 若 5 并发下 TPOT 仅 10~12ms,可放宽到 8 提升吞吐

5.2 CUDA Graph 尺寸必须与运行请求数匹配

复制代码
--cuda-graph-max-bs 5   # 与 --max-running-requests 保持一致

把 CUDA Graph 的最大 Batch Size 设为与 --max-running-requests 一致,能保证 Decode 阶段所有请求都命中预编译的 CUDA Graph 路径,显著降低 Kernel 调度开销 ------ 这是降低 TPOT 时间的关键。实测证明:并发从 2 升到 5,TPOT 仅从 44.42ms 升到 46.32ms(约 +4%),正是 CUDA Graph + 限流机制发挥了作用;如果不限流,TPOT 可能飙到 100ms+。

5.3 为什么并发越高 TPOT 反而越差?

LLM Decode 是显存带宽受限 的:每生成一个 Token,都要把模型全部权重从 HBM 搬到计算核心。并发 2 时读一次权重算 2 个序列,并发 5 时算 5 个序列,KV Cache 读取量与 Attention 计算量翻倍,单请求 Decode 速度自然下降。并发增加牺牲单用户延迟、换取系统总吞吐,这是正常的物理规律,需要靠上面的限流机制来约束。


六、NUMA 拓扑与卡绑定(核心踩坑点)

6.1 先确认拓扑:numactl -H + hy-smi --showtoponuma

在双路 CPU + 多卡环境下,两个 CPU Socket 之间靠 UPI 互联,跨 Socket 访问内存的距离代价很高(距离矩阵中 Node 0→Node 3 的距离为 16,而跨 Socket 可到 28),所以必须先使用以下命令核对 CPU NUMA 节点与 DCU 卡的物理映射关系,然后正确绑定卡关系:

shell 复制代码
numactl -H                 # 查看 NUMA 节点、CPU、内存

hy-smi --showtoponuma      # 查看每张卡(HCU)的 NUMA 亲和节点

本次环境实际输出:

shell 复制代码
# numactl -H
available: 8 nodes (0-7)
node 0 cpus: 0 1 2 3 4 5 6 7 64 65 66 67 68 69 70 71
node 0 size: 64320 MB
node 0 free: 2117 MB
node 1 cpus: 8 9 10 11 12 13 14 15 72 73 74 75 76 77 78 79
node 1 size: 64507 MB
node 1 free: 58749 MB
node 2 cpus: 16 17 18 19 20 21 22 23 80 81 82 83 84 85 86 87
node 2 size: 64507 MB
node 2 free: 62867 MB
node 3 cpus: 24 25 26 27 28 29 30 31 88 89 90 91 92 93 94 95
node 3 size: 64507 MB
node 3 free: 5906 MB
node 4 cpus: 32 33 34 35 36 37 38 39 96 97 98 99 100 101 102 103
node 4 size: 64507 MB
node 4 free: 62405 MB
node 5 cpus: 40 41 42 43 44 45 46 47 104 105 106 107 108 109 110 111
node 5 size: 64507 MB
node 5 free: 63878 MB
node 6 cpus: 48 49 50 51 52 53 54 55 112 113 114 115 116 117 118 119
node 6 size: 64507 MB
node 6 free: 62224 MB
node 7 cpus: 56 57 58 59 60 61 62 63 120 121 122 123 124 125 126 127
node 7 size: 64468 MB
node 7 free: 62736 MB
node distances:
node   0   1   2   3   4   5   6   7 
  0:  10  16  16  16  28  28  22  28 
  1:  16  10  16  16  28  28  28  22 
  2:  16  16  10  16  22  28  28  28 
  3:  16  16  16  10  28  22  28  28 
  4:  28  28  22  28  10  16  16  16 
  5:  28  28  28  22  16  10  16  16 
  6:  22  28  28  28  16  16  10  16 
  7:  28  22  28  28  16  16  16  10 

# hy-smi --showtoponuma
================================= System Management Interface ==================================
================================================================================================
HCU[0]          : (Topology) Numa Node 0
HCU[0]          : (Topology) Numa Affinity 0
HCU[1]          : (Topology) Numa Node 0
HCU[1]          : (Topology) Numa Affinity 0
HCU[2]          : (Topology) Numa Node 0
HCU[2]          : (Topology) Numa Affinity 0
HCU[3]          : (Topology) Numa Node 0
HCU[3]          : (Topology) Numa Affinity 0
HCU[4]          : (Topology) Numa Node 3
HCU[4]          : (Topology) Numa Affinity 3
HCU[5]          : (Topology) Numa Node 3
HCU[5]          : (Topology) Numa Affinity 3
HCU[6]          : (Topology) Numa Node 3
HCU[6]          : (Topology) Numa Affinity 3
HCU[7]          : (Topology) Numa Node 3
HCU[7]          : (Topology) Numa Affinity 3
================================================================================================
======================================== End of SMI Log ========================================

查询结果显示 HCU 0-3 亲和 NUMA Node 0,HCU 4-7 亲和 NUMA Node 3 。上面说过两个 CPU Socket 之间靠 UPI 互联,跨 Socket 访问内存的距离代价很高(距离矩阵中 Node 0→Node 3 的距离为 16,而跨 Socket 可到 28),所以绑定必须与物理拓扑一致

6.2 --numa-node 参数的注意事项

注意事项一:参数个数必须与实例使用的卡数一致(否则直接 IndexError)

在 TP=4 的实例里,只写 --numa-node 0 会被 SGLang 解析成长度为 1 的列表 [0],当进程为第 2 张卡(gpu_id=1)找 NUMA 节点时,访问 numa_node[1] 触发越界:

复制代码
in configure_subprocessnuma_node = get_numa_node_if_available(server_args, gpu_id) File "/usr/local/lib/python3.10/dist-packages/sglang/srt/utils/numa_utils.py", line 104, in get_numa_node_if_available return server_args.numa_node[gpu_id] IndexError:
list index out of range 

正确写法(两种选一):

  • 方式一(推荐) :直接删除 --numa-node,让 SGLang 结合 HIP_VISIBLE_DEVICES 自动绑定;

  • 方式二(显式绑定) :按卡数填满节点值,如 TP=4 时写 --numa-node 0 0 0 0

注意事项二:TP=8 单实例跨两个 NUMA 节点时,不能只绑一个节点

8 张卡分布在 Node 0 和 Node 3,如果单实例 TP=8 硬绑 --numa-node 0,控制 Node 3 显卡的线程和内存分配会被迫跨节点调用,产生严重的远端内存访问延迟。此时要么去掉参数让系统自动绑定,要么显式写满 8 个值:

yaml 复制代码
--numa-node 0 0 0 0 3 3 3 3

注意事项三:绑定的 NUMA 节点内存不足会触发 OOM Killer

本文环境中 Node 1 只剩 2117MB 只剩~10GB 空闲内存,若直接把容器绑到内存耗尽的节点上,SGLang 在分配 Host 端内存(内存池、IPC Buffer)时会被 OOM Killer 杀掉。启动前建议:

shell 复制代码
sudo sync && echo 3 | sudo tee /proc/sys/vm/drop_caches   # 清理缓存释放内存

同时检查是否有残留的 python/vllm/sglang 进程占用节点内存。

6.3 两种架构下的绑定策略总结

架构 绑定策略
TP=8 单实例 删除 --numa-node,或显式 --numa-node 0 0 0 0 3 3 3 3 ,我的方案里用了后者
TP=4 × 2 双实例 实例 1:HIP_VISIBLE_DEVICES=0,1,2,3 + --numa-node 0 0 0 0;实例 2:HIP_VISIBLE_DEVICES=4,5,6,7 + --numa-node 3 3 3 3

七、双实例 + Router 架构:统一入口与负载均衡

7.1 为什么需要统一入口

把 8 卡拆成 Node 0 (TP4) + Node 3 (TP4) 两个独立实例后,它们在网络层面是两个独立 endpoint,上层业务无法无感调用。必须在前端挂一层代理,提供统一入口、负载均衡、健康检查与排队限流。

7.2 方案选型:SGLang 原生 Router vs Nginx

方案一:SGLang 原生 Router(推荐)

SGLang 官方自带的分布式 Router 基于 Rust 开发,自带 Radix Cache 前缀缓存感知分发,能最大化 Prompt 复用率、降低 TTFT,且无需额外引入组件:

yaml 复制代码
  qwen3.8-router:
    image: harbor.sourcefind.cn:5443/dcu/admin/base/custom:sglang0.5.12-ubuntu22.04-dtk26.04-py3.10-20260620
    container_name: qwen3.8-router
    network_mode: host
    restart: always
    depends_on:
      - qwen3.8-30001
      - qwen3.8-30002
    user: root
    privileged: true
    security_opt: ["seccomp:unconfined"]
    cap_add: ["SYS_PTRACE"]
    group_add: ["video"]
    devices:
      - /dev/kfd:/dev/kfd
      - /dev/dri:/dev/dri
      - /dev/mkfd:/dev/mkfd
    volumes:
      - /opt/hyhal/:/opt/hyhal/:ro
      - /opt/models/Qwen3.8-27B/model/:/model/
    environment:
      - LD_LIBRARY_PATH=/opt/hyhal/lib:/opt/rocm/lib:${LD_LIBRARY_PATH}
      - SGLANG_ALLOW_OVERWRITE_LONGER_CONTEXT_LEN=1
      - SGLANG_USE_LIGHTOP=1
      - SGLANG_USE_MARLIN_W16A16_MOE=1
      - SGLANG_USE_FUSED_TOPK_SOFTMAX=1
      - SGLANG_ROCM_USE_AITER_MOE=False
      - SGLANG_USE_CAUSAL_CONV1D=1
      - SGLANG_USE_CUDA_IPC_TRANSPORT=1
      - SGLANG_USE_AITER_LINEAR_ATTN=1
    command: >
      python3 -m sglang_router.launch_router
      --host 0.0.0.0
      --port 9027
      --api-key <YOUR_API_KEY>
      --worker-urls http://127.0.0.1:30001 http://127.0.0.1:30002
      --policy cache_aware

⚠️

注意模块名是下划线

海光定制镜像的 Router 是独立 Rust 组件,其 Python 包导入名是 sglang_router,不是点号 sglang.router,写错会报ModuleNotFoundError: No module named 'sglang.router'

方案二:Nginx 轮询

适合纯无状态并发压测,配置简单,但需要自行处理 SSE 流式(关闭 proxy_buffering)、长连接与超时(proxy_read_timeout 600s)。

7.3 Router 容器必须挂载海光驱动目录

Router 容器虽然不直接做推理,但启动时导入 torch 会初始化 ROCm/DTK 运行时。如果 Router 容器没有挂载 /opt/hyhal/,会报以下错误:

shell 复制代码
ImportError: librocm_smi64.so.2: cannot open shared object file: No such file or directory

Router 还需要挂载模型目录。Rust Router 要读取分词器计算 Token 与缓存状态,如果容器内没有 /model/,它会把 Worker 节点上报的路径当成 HuggingFace Repo ID 去联网下载,报 "Failed to download tokenizer from HuggingFace"。

以下是上述两点的配置示例:

yaml 复制代码
volumes:
  - /opt/hyhal/:/opt/hyhal/:ro
  - /opt/models/Qwen3.8-27B/model/:/model/   # Router 需要读 tokenizer

environment:
  - LD_LIBRARY_PATH=/opt/hyhal/lib:/opt/rocm/lib:${LD_LIBRARY_PATH}

7.4 在 Router 上配置 API Key

在 Router 启动命令中加 --api-key <YOUR_API_KEY> 即可实现统一鉴权,客户端请求携带 Authorization: Bearer <YOUR_API_KEY>

yaml 复制代码
command: >
  python3 -m sglang_router.launch_router
  --host 0.0.0.0
  --port 9027
  --api-key <YOUR_API_KEY>
  --worker-urls http://127.0.0.1:30001 http://127.0.0.1:30002
  --policy cache_aware

如需后端 Worker 被绕过 Router 时也强制校验,可在 Worker 的 launch_server 命令里加同样的 --api-key

7.5 路由策略:cache_aware 的低并发倾斜问题

使用 --policy cache_aware 时,Router 会根据请求与各 Worker 已有 Radix Cache 的重合度做贪婪匹配。在低并发(如 2~3 个并发)且无 Cache 可复用的场景下,Router 倾向于把所有请求都挤在已建立 Cache 的那个 Worker 上,导致另一个实例长期空闲。

解决方案:切换到轮询或最短队列策略,并开启健康检查:

yaml 复制代码
--policy round_robin
--check-health
策略 适用场景
cache_aware 高并发 + 高 Prompt Cache 复用(多轮对话、固定 System Prompt 智能体)
round_robin 要求流量严格均摊、消除单机倾斜(推荐,低并发场景)
shortest_queue 限制 --max-running-requests 的场景,选择队列最短的 Worker

八、非流式输出时中文输出乱码

8.1 现象

  • 流式输出时中文内容正常显示在 reasoning_content 中;

  • 非流式输出时 contentnull,且出现 ací 5 5 5...Sûûûû... 等乱码与死循环重复,直到耗尽 max_tokens

8.2 可能原因(时间关系未再做验证):激进的 RoPE 覆写参数(--json-model-override-args

启动脚本里带的这串参数:

yaml 复制代码
{"rope_parameters":{"mrope_interleaved":true,"mrope_section":[11,11,10],"rope_type":"yarn","rope_theta":10000000,"partial_rotary_factor":0.25,"factor":4.0,"original_max_position_embeddings":262144}}

它的作用是把模型硬推到 256K 超长上下文外推(YaRN 4.0 倍外推 + 1000 万 rope_theta + MRoPE 三维位置拆分)。但:

  1. mrope_section 是给 Qwen 多模态 / VL 模型用的三维位置拆分逻辑,纯文本模型没有对应结构,强行注入会导致 Attention 旋转矩阵算错;

  2. YaRN 4.0 + 1000 万 Theta 是 "牺牲短文本精度换超长文本不崩" 的激进权衡,短文本下位置频率被拉稀疏,Q/K 点积失效,Decode 阶段不断重复或吐乱码。

所以 我取消了上面的参数,先把模型上下文长度设置成了 128K, 使用模型官方 config.json 已内置 128K 的 RoPE 配置,SGLang 自动加载。

8.3 修改 tokenizer_config.json 开启思考,解决非流式输出中文乱码问题。

之前问大模型,建议我关闭思考和工具调用,但是我的业务确实需要思考与工具调用,所以最后找到了这条路:改模型 tokenizer_config.json,在末尾追加默认思考参数:

json 复制代码
{
 "chat_template": "...",
 "default_chat_template_kwargs": {
   "enable_thinking": true,
   "preserve_thinking": true,
   "reasoning_effort": "medium"
 }
}

其中:

  • enable_thinking: true 让模板在 add_generation_prompt 阶段自动在 Prompt 结尾注入 thinking\n,引导模型正确进入思考流程(这正是解决乱码的关键,避免模型找不到思考起始符);

  • reasoning_effort 控制思考深度(low / medium / xhigh);

  • preserve_thinking: true 在对话历史中保留思考过程。

按照上面这么改的原因是 --default-chat-template-kwargs不是 SGLang 命令行参数 ,SGLang 不识别该参数,必须通过客户端请求体 chat_template_kwargs 透传或改 tokenizer_config.json 实现,我不想让用户传这个参数,或者因为用户忘记传这个参数,导致中文输出不正常,所以我是通过修改模型的 tokenizer_config.json 文件实现。

此时 Worker 启动命令继续保留:

复制代码
--reasoning-parser qwen3
--tool-call-parser qwen3_coder

写到这里,我也想起来我在A100上用VLLM推理Qwen3.8-27B-FP8的时候,没有指定 reasoning_effort在 FastGPT 智能体平台调用模型的时候,也出现了指令遵循性不够的情况,虽然说默认思考是xhigh,但实际用的时候没有思考输出,模型也显得很笨,不知道是不是vllm版本兼容性的问题,但是在SGLang里碰到了,我怀疑是模型权重文件本身的问题了。


九、开启投机采样(MTP/EAGLE)

Qwen3.8-27B 原生带 MTP(Multi-Token Prediction)模块。vLLM 可以用 --speculative-config '{"method":"mtp","num_speculative_tokens":3}'直接开启,而 SGLang 采用平铺参数开启:

复制代码
 --speculative-algorithm EAGLE
 --speculative-num-steps 3
 --speculative-eagle-topk 1

对应关系:

vLLM 配置 SGLang 参数 说明
"method":"mtp" --speculative-algorithm EAGLE EAGLE 能自动识别模型自带 MTP/Speculative 预测头
"num_speculative_tokens":3 --speculative-num-steps 3 每次预测 3 个 Token
--- --speculative-eagle-topk 1 匹配传统 MTP 的 1-D 链式推测逻辑

海光平台注意

  1. 开启 MTP 会额外分配 Draft Token 的 KV Cache 显存,若 OOM 可把 --mem-fraction-static 从 0.90 降到 0.85;

  2. 用 EvalScope 单并发测试观察 Spec. Accept Rate,若低于 10% 说明 MTP 反而成为瓶颈,建议关闭。

  3. 这里其实也讨了个巧,或者叫作弊了,我在VLLM日志里就发现了EvalScope测试的时候MTP的命中率基本都是0.9或者1,所以SGLang我也打开,作弊一个,让TPOT更好看,真实业务做不到0.9,我们有另一个AI辅助编码场景,看日志实际上是在0.7~0.8X之间。


十、性能压测(EvalScope)与结果对比

10.1 EvalScope 压测命令

shell 复制代码
evalscope perf \
  --url "http://localhost:9027/v1/chat/completions" \
  --parallel 5 \
  --number 50 \
  --model Qwen3.8-27B \
  --api openai \
  --api-key "<YOUR_API_KEY>" \
  --dataset random \
  --min-prompt-length 128 \
  --max-prompt-length 1024 \
  --min-tokens 1024 \
  --max-tokens 4096 \
  --tokenizer-path "/opt/models/Qwen3.8-27B/model" \
  --stream 2>&1 | tee "evalscope_Qwen3.8-27B_$(date +%Y%m%d_%H%M%S).log"

只测单一并发 时,把 --parallel--number 都只留一个值(如 --parallel 10 --number 50)。

**友情提示:**配了并发 10 但最开始只发了一个请求的原因:

Dataset 生成慢**:--dataset random 需要本地用 Tokenizer 实时生成随机 Token,生成长度过大且生成过慢时,看起来像只发了 1 个请求;最开始Router + 2个实例的时候单实例只有22Token/s,生成4096个Token,太慢了。

10.2 SGLang的三种实测数据以及与VLLM的对比

关键指标 SGLang,4 卡双实例(无投机) SGLang,TP=8 单实例 + MTP SGLang,Router + 双 Worker (TP=4×2) + MTP VLLM,TP=4,DP=2 + MTP
TPOT p50 44.43 ms 18.35 ms 18.31 ms 32.43ms
TPOT Avg 46.32 ms 19.53 ms 23.87 ms 32.37ms
TPOT p99/Max --- 24.81 ms 69.59 ms 43.1 ms
Decode 速度 19.19 tok/s 51.2 tok/s 41.89 tok/s 40.49 tok/s
系统吞吐 (Gen/s) 64.16 tok/s 215.79 tok/s 219.08 tok/s 139.28 tok/s
投机接受率 5.6%(未生效) 71.2% 73.4% 68.4
Decoded Tok/Iter 1.06 3.47 3.76 3.16

10.3 性能突破的技术归因

  1. TP=8 拆解显存带宽瓶颈:模型权重分散到 8 卡后,单卡 Decode 阶段需加载的显存数据量直接减半,解开了 K100AI DCU 的显存带宽限制;

  2. Qwen3.8 原生 MTP + EAGLE 高度契合 :71.2% 的接受率 + --speculative-num-steps 3,平均每次迭代产出 3.47 个 Token,相当于一次矩阵乘法干过去 3.5 次的活,是 TPOT 打进 18.35ms 的最大功臣;

  3. CUDA Graph 与队列精确匹配--max-running-requests 5--cuda-graph-max-bs 5 一致,并发 5 全部命中预编译 CUDA Graph,规避了 Eager Mode 的 Kernel 启动开销;

  4. NUMA 物理绑定抹平跨卡延迟--numa-node 0 0 0 0 3 3 3 3 的硬件级亲和绑定,配合高接受率的投机加速,把跨 Socket PCIe 通信开销完全掩盖。


十一、两种部署架构的选型建议

对比维度 TP=8 单实例 Router + 双 Worker (TP=4×2)
TPOT p50 18.35 ms 18.31 ms(一致)
TPOT Avg / p99 19.53 / 24.81 ms 23.87 / 69.59 ms(长尾抖动明显)
单请求解码速度 51.2 tok/s 41.89 tok/s(约 -18%)
系统吞吐 215.79 tok/s 219.08 tok/s(基本持平,略升 1.5%)
投机接受率 71.2% 73.4%
架构复杂度 简单,无代理层 多一层 Router,需处理负载均衡

结论

  • 追求极致低延迟(推荐) :选择 TP=8 单实例。没有 Router 代理层、没有负载不均问题,p99 极佳,单用户 51+ tok/s;

  • 多租户隔离 / 高并发防单点故障 / 高 Cache 复用 :选择 Router + 双 Worker 。但若压测数据无 Cache 可复用,建议把路由策略从 cache_aware 调整为 round_robinshortest_queue,以平摊流量、消除长尾抖动。


十二、最终完整配置参考

12.1 推荐方案:TP=8 单实例(达成 TPOT ≤ 20ms)

yaml 复制代码
version: '3.8'

services:

  Qwen3.8-27B:
    image: harbor.sourcefind.cn:5443/dcu/admin/base/custom:sglang0.5.12-ubuntu22.04-dtk26.04-py3.10-20260620
    container_name: Qwen3.8-27B
    user: root
    privileged: true
    network_mode: host
    shm_size: 256g
    security_opt: ["seccomp:unconfined"]
    cap_add: ["SYS_PTRACE"]
    group_add: ["video"]
    devices:
      - /dev/kfd:/dev/kfd
      - /dev/dri:/dev/dri
      - /dev/mkfd:/dev/mkfd
    volumes:
      - /opt/hyhal/:/opt/hyhal/:ro
      - /opt/models/Qwen3.8-27B/model/:/model/
    environment:
      - HIP_VISIBLE_DEVICES=0,1,2,3,4,5,6,7
      - SGLANG_ALLOW_OVERWRITE_LONGER_CONTEXT_LEN=1
      - SGLANG_USE_LIGHTOP=1
      - SGLANG_USE_MARLIN_W16A16_MOE=1
      - SGLANG_USE_FUSED_TOPK_SOFTMAX=1
      - SGLANG_ROCM_USE_AITER_MOE=False
      - SGLANG_USE_CAUSAL_CONV1D=1
      - SGLANG_USE_CUDA_IPC_TRANSPORT=1
      - SGLANG_USE_AITER_LINEAR_ATTN=1
    command: >
      python3 -u -m sglang.launch_server
      --numa-node 0 0 0 0 3 3 3 3
      --mamba-scheduler-strategy extra_buffer
      --max-running-requests 5
      --cuda-graph-max-bs 5
      --kv-cache-dtype fp8_e5m2
      --model-path /model/
      --host 0.0.0.0
      --port 9027
      --api-key <YOUR-API-KEY>
      --served-model-name Qwen3.8-27B
      --mm-attention-backend fa3
      --attention-backend fa3
      --enable-piecewise-cuda-graph
      --tp-size 8
      --pp-size 1
      --context-length 131072
      --page-size 64
      --mem-fraction-static 0.90
      --keep-mm-feature-on-device
      --tool-call-parser qwen3_coder
      --reasoning-parser qwen3
      --speculative-algorithm EAGLE
      --speculative-num-steps 3
      --speculative-eagle-topk 1

说明:该配置依赖 tokenizer_config.json 已按第 8.4 节加入default_chat_template_kwargs 才能正确支持思考与中文输出;若不需要思考,tokenizer_config.json文件中的思考模式改false即可

12.2 备选方案:Router + 双 Worker(多租户 / 高可用)

架构如下:
#mermaid-svg-XVcia0BvYmAwq8Iz{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-XVcia0BvYmAwq8Iz .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-XVcia0BvYmAwq8Iz .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-XVcia0BvYmAwq8Iz .error-icon{fill:#552222;}#mermaid-svg-XVcia0BvYmAwq8Iz .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-XVcia0BvYmAwq8Iz .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-XVcia0BvYmAwq8Iz .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-XVcia0BvYmAwq8Iz .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-XVcia0BvYmAwq8Iz .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-XVcia0BvYmAwq8Iz .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-XVcia0BvYmAwq8Iz .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-XVcia0BvYmAwq8Iz .marker{fill:#333333;stroke:#333333;}#mermaid-svg-XVcia0BvYmAwq8Iz .marker.cross{stroke:#333333;}#mermaid-svg-XVcia0BvYmAwq8Iz svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-XVcia0BvYmAwq8Iz p{margin:0;}#mermaid-svg-XVcia0BvYmAwq8Iz .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-XVcia0BvYmAwq8Iz .cluster-label text{fill:#333;}#mermaid-svg-XVcia0BvYmAwq8Iz .cluster-label span{color:#333;}#mermaid-svg-XVcia0BvYmAwq8Iz .cluster-label span p{background-color:transparent;}#mermaid-svg-XVcia0BvYmAwq8Iz .label text,#mermaid-svg-XVcia0BvYmAwq8Iz span{fill:#333;color:#333;}#mermaid-svg-XVcia0BvYmAwq8Iz .node rect,#mermaid-svg-XVcia0BvYmAwq8Iz .node circle,#mermaid-svg-XVcia0BvYmAwq8Iz .node ellipse,#mermaid-svg-XVcia0BvYmAwq8Iz .node polygon,#mermaid-svg-XVcia0BvYmAwq8Iz .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-XVcia0BvYmAwq8Iz .rough-node .label text,#mermaid-svg-XVcia0BvYmAwq8Iz .node .label text,#mermaid-svg-XVcia0BvYmAwq8Iz .image-shape .label,#mermaid-svg-XVcia0BvYmAwq8Iz .icon-shape .label{text-anchor:middle;}#mermaid-svg-XVcia0BvYmAwq8Iz .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-XVcia0BvYmAwq8Iz .rough-node .label,#mermaid-svg-XVcia0BvYmAwq8Iz .node .label,#mermaid-svg-XVcia0BvYmAwq8Iz .image-shape .label,#mermaid-svg-XVcia0BvYmAwq8Iz .icon-shape .label{text-align:center;}#mermaid-svg-XVcia0BvYmAwq8Iz .node.clickable{cursor:pointer;}#mermaid-svg-XVcia0BvYmAwq8Iz .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-XVcia0BvYmAwq8Iz .arrowheadPath{fill:#333333;}#mermaid-svg-XVcia0BvYmAwq8Iz .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-XVcia0BvYmAwq8Iz .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-XVcia0BvYmAwq8Iz .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-XVcia0BvYmAwq8Iz .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-XVcia0BvYmAwq8Iz .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-XVcia0BvYmAwq8Iz .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-XVcia0BvYmAwq8Iz .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-XVcia0BvYmAwq8Iz .cluster text{fill:#333;}#mermaid-svg-XVcia0BvYmAwq8Iz .cluster span{color:#333;}#mermaid-svg-XVcia0BvYmAwq8Iz div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-XVcia0BvYmAwq8Iz .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-XVcia0BvYmAwq8Iz rect.text{fill:none;stroke-width:0;}#mermaid-svg-XVcia0BvYmAwq8Iz .icon-shape,#mermaid-svg-XVcia0BvYmAwq8Iz .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-XVcia0BvYmAwq8Iz .icon-shape p,#mermaid-svg-XVcia0BvYmAwq8Iz .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-XVcia0BvYmAwq8Iz .icon-shape .label rect,#mermaid-svg-XVcia0BvYmAwq8Iz .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-XVcia0BvYmAwq8Iz .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-XVcia0BvYmAwq8Iz .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-XVcia0BvYmAwq8Iz :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} Bearer API Key
轮询分发
轮询分发
客户端
Router :9027

round_robin + check-health
Worker1 :30001

NUMA Node0 / HCU 0-3

TP=4 + MTP
Worker2 :30002

NUMA Node3 / HCU 4-7

TP=4 + MTP

  • Worker 1:HIP_VISIBLE_DEVICES=0,1,2,3 + --numa-node 0 0 0 0 + --port 30001

  • Worker 2:HIP_VISIBLE_DEVICES=4,5,6,7 + --numa-node 3 3 3 3 + --port 30002

  • Router:python3 -m sglang_router.launch_router --port 9027 --worker-urls http://127.0.0.1:30001 http://127.0.0.1:30002 --policy round_robin --check-health --api-key <YOUR_API_KEY>,同时必须挂载 /opt/hyhal/与模型目录 并设置 LD_LIBRARY_PATH

yaml 复制代码
version: '3.8'

services:
  Qwen3.8-Router:
    image: harbor.sourcefind.cn:5443/dcu/admin/base/custom:sglang0.5.12-ubuntu22.04-dtk26.04-py3.10-20260620
    container_name: Qwen3.8-Router
    network_mode: host
    restart: always
    depends_on:
      - Qwen3.8-30001
      - Qwen3.8-30002
    user: root
    privileged: true
    security_opt: ["seccomp:unconfined"]
    cap_add: ["SYS_PTRACE"]
    group_add: ["video"]
    devices:
      - /dev/kfd:/dev/kfd
      - /dev/dri:/dev/dri
      - /dev/mkfd:/dev/mkfd
    volumes:
      - /opt/hyhal/:/opt/hyhal/:ro
      - /opt/models/Qwen3.8-27B/model/:/model/
    environment:
      - LD_LIBRARY_PATH=/opt/hyhal/lib:/opt/rocm/lib:${LD_LIBRARY_PATH}
      - SGLANG_ALLOW_OVERWRITE_LONGER_CONTEXT_LEN=1
      - SGLANG_USE_LIGHTOP=1
      - SGLANG_USE_MARLIN_W16A16_MOE=1
      - SGLANG_USE_FUSED_TOPK_SOFTMAX=1
      - SGLANG_ROCM_USE_AITER_MOE=False
      - SGLANG_USE_CAUSAL_CONV1D=1
      - SGLANG_USE_CUDA_IPC_TRANSPORT=1
      - SGLANG_USE_AITER_LINEAR_ATTN=1
    command: >
      python3 -m sglang_router.launch_router
      --host 0.0.0.0
      --port 9027
      --api-key <YOUR-API-KEY>
      --worker-urls http://127.0.0.1:30001 http://127.0.0.1:30002
      --policy cache_aware

  Qwen3.8-30001:
    image: harbor.sourcefind.cn:5443/dcu/admin/base/custom:sglang0.5.12-ubuntu22.04-dtk26.04-py3.10-20260620
    container_name: Qwen3.8-30001
    user: root
    privileged: true
    network_mode: host
    shm_size: 196g
    security_opt: ["seccomp:unconfined"]
    cap_add: ["SYS_PTRACE"]
    group_add: ["video"]
    devices:
      - /dev/kfd:/dev/kfd
      - /dev/dri:/dev/dri
      - /dev/mkfd:/dev/mkfd
    volumes:
      - /opt/hyhal/:/opt/hyhal/:ro
      - /opt/models/Qwen3.8-27B/model/:/model/
    environment:
      - HIP_VISIBLE_DEVICES=0,1,2,3  # 绑定 HCU 0,1,2,3
      - SGLANG_ALLOW_OVERWRITE_LONGER_CONTEXT_LEN=1
      - SGLANG_USE_LIGHTOP=1
      - SGLANG_USE_MARLIN_W16A16_MOE=1
      - SGLANG_USE_FUSED_TOPK_SOFTMAX=1
      - SGLANG_ROCM_USE_AITER_MOE=False
      - SGLANG_USE_CAUSAL_CONV1D=1
      - SGLANG_USE_CUDA_IPC_TRANSPORT=1
      - SGLANG_USE_AITER_LINEAR_ATTN=1
    command: >
      python3 -u -m sglang.launch_server
      --numa-node 0 0 0 0
      --mamba-scheduler-strategy extra_buffer
      --max-running-requests 5
      --cuda-graph-max-bs 5
      --kv-cache-dtype fp8_e5m2
      --model-path /model/
      --host 0.0.0.0
      --port 30001
      --served-model-name Qwen3.8-27B
      --mm-attention-backend fa3
      --attention-backend fa3
      --enable-piecewise-cuda-graph
      --tp-size 4
      --pp-size 1
      --context-length 131072
      --page-size 64
      --mem-fraction-static 0.90
      --keep-mm-feature-on-device
      --tool-call-parser qwen3_coder
      --reasoning-parser qwen3
      --speculative-algorithm EAGLE
      --speculative-num-steps 3
      --speculative-eagle-topk 1

  Qwen3.8-30002:
    image: harbor.sourcefind.cn:5443/dcu/admin/base/custom:sglang0.5.12-ubuntu22.04-dtk26.04-py3.10-20260620
    container_name: Qwen3.8-30002
    user: root
    privileged: true
    network_mode: host
    shm_size: 196g
    security_opt: ["seccomp:unconfined"]
    cap_add: ["SYS_PTRACE"]
    group_add: ["video"]
    devices:
      - /dev/kfd:/dev/kfd
      - /dev/dri:/dev/dri
      - /dev/mkfd:/dev/mkfd
    volumes:
      - /opt/hyhal/:/opt/hyhal/:ro
      - /opt/models/Qwen3.8-27B/model/:/model/
    environment:
      - HIP_VISIBLE_DEVICES=4,5,6,7  # 绑定 HCU 4,5,6,7
      - SGLANG_ALLOW_OVERWRITE_LONGER_CONTEXT_LEN=1
      - SGLANG_USE_LIGHTOP=1
      - SGLANG_USE_MARLIN_W16A16_MOE=1
      - SGLANG_USE_FUSED_TOPK_SOFTMAX=1
      - SGLANG_ROCM_USE_AITER_MOE=False
      - SGLANG_USE_CAUSAL_CONV1D=1
      - SGLANG_USE_CUDA_IPC_TRANSPORT=1
      - SGLANG_USE_AITER_LINEAR_ATTN=1
    command: >
      python3 -u -m sglang.launch_server
      --numa-node 3 3 3 3
      --mamba-scheduler-strategy extra_buffer
      --max-running-requests 5
      --cuda-graph-max-bs 5
      --kv-cache-dtype fp8_e5m2
      --model-path /model/
      --host 0.0.0.0
      --port 30002
      --served-model-name Qwen3.8-27B
      --mm-attention-backend fa3
      --attention-backend fa3
      --enable-piecewise-cuda-graph
      --tp-size 4
      --pp-size 1
      --context-length 131072
      --page-size 64
      --mem-fraction-static 0.90
      --keep-mm-feature-on-device
      --tool-call-parser qwen3_coder
      --reasoning-parser qwen3
      --speculative-algorithm EAGLE
      --speculative-num-steps 3
      --speculative-eagle-topk 1

十三、常见报错速查表

报错 / 现象 根因 解决方案
IndexError: list index out of range(numa_utils.py) --numa-node 数量小于实例卡数 删除该参数,或按卡数填满,如 --numa-node 0 0 0 0
ImportError: librocm_smi64.so.2 Router/Worker 未挂载海光驱动 挂载 /opt/hyhal/ + 设 LD_LIBRARY_PATH
ModuleNotFoundError: No module named 'sglang.router' 模块名写错 改为 sglang_router(下划线)
Failed to load tokenizer ... from HuggingFace Router 容器未挂载模型目录 Router 挂载 /model/
输出乱码 / 死循环重复 / content 为 null RoPE 覆写参数或解析器不匹配 去掉 --json-model-override-args--enable-strict-thinking;parser 设 none 或改 tokenizer_config.json
AssertionError: unrecognized format of chat template file --chat-template 传了目录 删掉该参数,或传 .json/.jinja 文件
配置 TP=4 + PP=2 启动崩溃 TP×PP=8 与 HIP_VISIBLE_DEVICES=4 卡矛盾 PP 改 1(DP2 架构下每个 Worker 只用 4 卡)
--mem-fraction-static 0.96 长文本 OOM 静态显存占比过高,KV Cache 空间不足 回调到 0.85~0.90
低并发下请求全部挤在一个 Worker cache_aware 贪婪匹配 --policy round_robin + --check-health
EvalScope 并发 10 只发 1 个请求 URL 带完整路径 /dataset 生成慢 / 连接池阻塞 --url 用基础地址 /v1;调小 token 长度;查后端日志
开了 MTP 后 Tok/Iter 仍 ≈1.0 投机未真正生效 确认参数拼写与模型是否自带 MTP;观察 Accept Rate

结语

本文完整记录了在海光 K100AI DCU(8 卡)平台上用 SGLang 部署 Qwen3.8-27B 的全过程。总结成一句话的经验:

NUMA 亲和绑定是命脉,CUDA Graph 与并发限流是延迟保障,思考模板适配是中文输出正确性的前提,TP=8 + MTP/EAGLE 则是把 K100AI 性能榨干的关键一锤。

最终这套方案在并发 5 的长文本场景下,把 TPOT p50 从最初的 44ms 级压到 18.35ms,单用户解码速度 51.2 tok/s,系统吞吐 215.79 tok/s,在国产算力平台上实现了 "极速打字机" 级别的体验。希望这份实战记录能为同样在国产 DCU 平台上做 LLM 服务化的同学提供一份可复用的参考。

注:文中所有性能数据均来自真实 EvalScope 压测日志,测试数据集为

random

(无 Prompt Cache 复用),上下文输出长度 1024~4096 Token。

相关推荐
谢白羽3 天前
SGLang的AWQ量化笔记
笔记·python·sglang
auto_go7 天前
大模型实战指南(11)——推理框架选型实战:vLLM × SGLang × TensorRT-LLM 深度对比与部署指南
vllm·sglang
谢白羽8 天前
SGLang模型加载过程笔记
笔记·llm·论文·agent·vllm·大模型部署·sglang
一次旅行13 天前
2026‑08‑22 AI产业深度解读|Anthropic自研芯片布局、SGLang权重缓存守护进程、Agent任务作弊审计、AI原生SDLC
人工智能·缓存·sglang
Jay Kay14 天前
DFlash 与 DSpark:从原理到 SGLang 实现
sglang
天涯明月199314 天前
SGLang深度研究报告
人工智能·后端·推理·sglang
海天一色y22 天前
Sglang本地部署Qwen3.5-9B模型
人工智能·llm·sglang
咖啡星人k23 天前
本地跑起 MiniMax H3:SGLang/vLLM/diffusers/ComfyUI 四种部署路线实测对比
vllm·sglang
Luchang-Li1 个月前
SGLang Qwen3/kimi-k3 Hybrid Linear Mamba KV cache管理
sglang