上一篇:海光 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
核心理由:
-
显存瓶颈全在 KV Cache。27B 模型 FP16 权重约 54GB,8 卡 TP=8 切分后每卡仅需 3.5~7GB 存放参数;但单个 256K 请求的 KV Cache 在 FP8 下也要消耗约 20~30GB 显存,需要分散到 8 张卡上。
-
必须开启 FP8 KV Cache :
--kv-cache-dtype fp8_e5m2(或fp8_e4m3)。若不开启,256K 的 KV Cache 显存占用翻倍,64GB 很容易 OOM。 -
不建议用 PP:单机 8 卡内部高带宽互联,TP 通信效率极高;PP 会引入 Bubble 等待,且 PP=2/TP=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。
理由:
-
TPOT 依赖算力与显存带宽。并发从 5 升到 70 时,Batch 增大导致显存带宽被瓜分,TPOT 会呈线性甚至指数级恶化(50~100ms+),直接击穿 20ms SLA。
-
256K 长上下文显存压力大。并发过大时系统会强制触发 Chunked Prefill 或 Request Eviction,延迟剧增甚至卡死。
-
用队列承接突发流量 。把内部
--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中; -
非流式输出时
content为null,且出现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 三维位置拆分)。但:
-
mrope_section是给 Qwen 多模态 / VL 模型用的三维位置拆分逻辑,纯文本模型没有对应结构,强行注入会导致 Attention 旋转矩阵算错; -
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 链式推测逻辑 |
海光平台注意:
-
开启 MTP 会额外分配 Draft Token 的 KV Cache 显存,若 OOM 可把
--mem-fraction-static从 0.90 降到 0.85; -
用 EvalScope 单并发测试观察
Spec. Accept Rate,若低于 10% 说明 MTP 反而成为瓶颈,建议关闭。 -
这里其实也讨了个巧,或者叫作弊了,我在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 性能突破的技术归因
-
TP=8 拆解显存带宽瓶颈:模型权重分散到 8 卡后,单卡 Decode 阶段需加载的显存数据量直接减半,解开了 K100AI DCU 的显存带宽限制;
-
Qwen3.8 原生 MTP + EAGLE 高度契合 :71.2% 的接受率 +
--speculative-num-steps 3,平均每次迭代产出 3.47 个 Token,相当于一次矩阵乘法干过去 3.5 次的活,是 TPOT 打进 18.35ms 的最大功臣; -
CUDA Graph 与队列精确匹配 :
--max-running-requests 5与--cuda-graph-max-bs 5一致,并发 5 全部命中预编译 CUDA Graph,规避了 Eager Mode 的 Kernel 启动开销; -
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_robin或shortest_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。