从 24 到 51 tok/s:用 GPUStack 调优 4 台 DGX Spark 上的 GLM 推理

GPU 价格持续上涨,促使我们重新审视一些过去觉得"不太划算"的设备,DGX Spark 就是其中之一。此前,它的性能和价格定位让我们有些犹豫;如今,在新的采购成本背景下,用它搭建小规模推理集群,开始成为一个值得认真验证的选择。

于是,我们拿出 4 台 DGX Spark,用 GPUStack 统一管理,并通过 vLLM 运行 GLM-5.3-Flash-NVFP4。经过两天的网络调优、MTP 投机解码适配和多节点排障,我们得到了下面这组结果。

最终,在本次测试条件下,单流解码速度从 24.3 tok/s 提升到 51.3 tok/s ,约为基线的 2.11 倍 ;4 路并发的聚合输出速度从 72.0 tok/s 提升到 104.1 tok/s。

这篇文章记录三个问题:网络优化带来了什么、MTP 为什么一开始跑不起来,以及 GPUStack 多节点部署中有哪些值得提前避开的坑。

本文数据来自这套 4 节点集群的测试记录。性能结论对应文中的模型、配置和测试用例;网卡名称、路径及参数需要结合实际环境调整。

一、测试环境:4 台 Spark,跨节点 TP=4

硬件与网络

项目 配置
计算节点 4 × NVIDIA DGX Spark,搭载 GB10 Grace Blackwell 超级芯片
统一内存 每台 128 GB
网络适配器 ConnectX-7;本次使用的逻辑接口见文末配置
交换机 MikroTik CRS804-4DDQ,4 个 200G 接入口,RouterOS 7.19.6
rail 1 192.168.100.0/24
rail 2 192.168.101.0/24

这里的"双 rail"需要先说明:按本次部署记录,每个节点的两个逻辑接口共享一条 200G 物理链路,并非两条独立的 200G 链路。4 个节点分别连接交换机的一个物理端口。

本次方案也有一个约束:交换机侧未提供可用的 DCB/PFC/ECN 配置能力,相关调优集中在网卡侧。因此,后文记录的是这套拓扑下的配置和测试结果,不将其表述为已经建立了端到端无损网络。

软件与模型

项目 配置
编排平台 GPUStack,Server 部署在 node01,4 台 Worker 注册到同一集群
推理引擎 vLLM,使用 B12X 注意力后端
容器镜像 eugr/spark-vllm-b12x:latest
模型 GLM-5.3-Flash-NVFP4
量化与缓存 NVFP4 权重量化,FP8 KV cache
并行方式 TP=4,跨 4 个节点;PP=1

GPUStack 负责模型配置、Worker 调度和实例管理,底层推理与通信优化则围绕 vLLM、MTP 和 NCCL 展开。

二、先打通网络:让 NCCL 用好 RoCE

跨 4 节点做张量并行,节点之间的通信是绕不开的一环。我们先从 MTU、网卡 QoS 和 NCCL 接口选择入手。

1. 统一 MTU

主机侧将 MTU 设为 9000,并写入 netplan 持久化;交换机侧按本次平台的配置方式设置 mtu=9216,记录的实际生效值为 9000。

这一步的重点是检查整条路径的生效配置和连通性,不能只修改主机侧就结束。

2. 配置网卡侧 QoS

以下以本次使用的一个接口为例:

bash 复制代码
mlnx_qos -i enp1s0f0np0 --trust dscp
mlnx_qos -i enp1s0f0np0 -f 0,0,0,1,0,0,0,0

本次配置使用 DSCP 信任模式,并开启优先级 3 的 PFC。

这里踩过一个很容易忽略的坑:mlnx_qos -p 用于优先级映射 ,-f 才是 PFC 开关。早期记录混淆过这两个参数,执行后必须回读配置,确认结果符合预期。

3. 让 NCCL 使用两个逻辑 RDMA 接口

text 复制代码
NCCL_IB_HCA=rocep1s0f0,roceP2p1s0f0
NCCL_IB_TC=104
NCCL_IB_QPS_PER_CONNECTION=4
NCCL_IB_SPLIT_DATA_ON_QPS=1

其中,NCCL_IB_TC=104 对应 DSCP 26 的设置;是否映射到预期的优先级和队列,需要结合网卡 QoS 配置核对。

网络优化带来了什么?

在记录的 4 节点、256 MB AllReduce 测试中,带宽数值从 TCP 回退状态下的 4.0 GB/s 提升至 22.9 GB/s ,约为原来的 5.7 倍 。单 rail 对照记录为 11.4 GB/s ,双 rail 约为其 2.0 倍。

这里引用的是 NCCL 测试记录中的带宽数值,不直接将其等同于物理端口线速。

业务测试中,长输入用例的首 token 延迟(TTFT)也出现明显改善,最终对比结果见第六节。不过,网络优化并没有让所有指标同步翻倍:本次记录中,纯解码速度的直接增益约为 2%~5%。

网络优化解决了通信瓶颈,而单流解码速度的大幅提升,主要发生在打通 MTP 之后。

三、打通 MTP:单流解码速度翻倍的关键

GLM-5.3-Flash 带有 MTP(Multi-Token Prediction,多 token 预测)模块。我们尝试通过投机解码提高输出速度,但在这套环境中,先后遇到了权重加载和 KV cache 布局问题。

第一关:MTP 权重被按错误的量化格式加载

本次 checkpoint 中,MTP 层对应第 45 层,名称中没有直接包含 mtp。该层的专家权重实际为 BF16,但量化配置的 ignore 列表遗漏了它,导致草稿侧按 NVFP4 分配,触发 256/512 的尺寸不匹配错误。

处理方式是在 4 个节点的模型配置中补充量化豁免规则。

先备份配置文件:

bash 复制代码
cd /models/GLM-5.3-Flash-NVFP4
cp -p config.json config.json.bak-ignore45

然后在以下两处 ignore 数组中,分别追加这 4 个字符串:

  • quantization_config.ignore
  • text_config.quantization_config.ignore
json 复制代码
[
  "model.language_model.layers.45",
  "model.language_model.layers.45*",
  "model.layers.45",
  "model.layers.45*"
]

上面展示的是待追加条目 ,不要用它们覆盖原有 ignore 数组。第 45 层也是本次 checkpoint 的具体情况,其他权重版本需要重新核对。

4 个节点修改完成后,在 GPUStack 中删除对应模型实例,触发重建和权重重新加载。这个操作会中断该实例的服务,应安排在可停机时段执行。

修改后,MTP 草稿侧按 BF16 加载该层,尺寸不匹配问题消失。需要回滚时,恢复各节点的备份文件,再重建实例。

第二关:KV cache 布局不兼容

权重加载通过后,新的问题出现在 FlashInfer 主后端与 MTP 草稿的 KV cache 布局组合上。

当时的布局约束相互冲突:部分布局无法将 2,560 个 token 的管理块切分成 256 个 token 的内核块,而能够切分的布局又未被解析器接受,初始化因此受阻。

我们曾构建本地补丁镜像,尝试放宽布局声明。最终跑通的方案更直接:将主模型和草稿侧的注意力后端都切换为 B12X。在本次配置中,管理块与内核块无需再做上述切分。

text 复制代码
--attention-backend=B12X
--speculative-config={"method":"mtp","num_speculative_tokens":3,"moe_backend":"humming","attention_backend":"B12X"}

随后,我们使用未加入本地布局补丁的基底镜像重新验证,同样配置能够运行,性能记录未见明显差异,因此最终移除了这项本地补丁。

这次排障留下的经验是:遇到后端兼容性问题,先核对已有后端的支持范围,再决定是否维护自己的补丁。

第三关:投机深度选 3,还是 5?

MTP 跑起来后,还需要确定一次预测多少个 token。

我们参考的配方使用 num_speculative_tokens=5。针对本次 4 节点 TP=4 环境,我们比较了深度 3 和 5,每组运行 3 轮,取中位数:

测试用例 深度 3 深度 5 深度 5 相对变化
单流长输出解码 51.9 tok/s 48.7 tok/s −6.2%
4 路并发聚合输出 110.0 tok/s 96.7 tok/s −12.1%

结果很明确:在这组测试里,深度 3 更快。

我们的判断是,更深投机带来的额外计算没有被接受 token 数的增长抵消。但这一解释仍需要结合逐层接受率和耗时统计进一步验证,不能仅凭吞吐结果确定原因。

因此,最终配置选择深度 3。参考配方提供了起点,实际工作负载下的 A/B 测试决定取舍。

开启 MTP 后的性能对比

下表来自同一套基准测试用例,比较无 MTP 基线与 MTP × 3、双 rail 的组合配置:

测试用例 无 MTP 基线 MTP × 3 + 双 rail 最终/基线
单流短输出解码 24.4 tok/s 50.0 tok/s 2.05×
单流长输出解码 24.3 tok/s 51.3 tok/s 2.11×
4 路并发聚合输出 72.0 tok/s 104.1 tok/s 1.45×
长输入场景解码 24.4 tok/s 51.9 tok/s 2.13×

这组结果体现的是组合配置的收益,不能把全部增益都归因于单个参数。它与前面的投机深度 A/B 属于不同轮次测试,因此绝对数值略有差异。

功能回归方面,17 道测试题全部通过,包括字母计数等用例。这提供了基础功能检查结果,但不等同于完整的输出分布一致性验证。

四、一次"无报错卡住":Gloo 选到了环回地址

调优过程中,曾出现一次很难从面板直接判断的故障:4 个 vLLM rank 进程全部停在初始化阶段,GPU 利用率为 0%,日志中没有明确报错。

我们用 py-spy 抓取调用栈,发现进程停在:

text 复制代码
gloo::transport::tcp::Pair::waitUntilConnected

继续检查监听地址后,我们发现相关 Worker 进程只监听了 127.0.0.1。

本次排查发现,实例容器内的 /etc/hosts 将主机名映射到了环回地址。Gloo 根据主机名选择地址时,将该地址用于跨节点连接,导致初始化无法完成。

解决方式是显式指定 Gloo 使用的网络接口:

text 复制代码
GLOO_SOCKET_IFNAME=enp1s0f0np0

本次故障中,仅设置 NCCL_SOCKET_IFNAME 没有解决问题,还需要单独设置 GLOO_SOCKET_IFNAME。

此后,这个变量被纳入了本套集群的固定配置。遇到类似的初始化卡住问题,值得同时检查调用栈 、监听地址 和容器内主机名解析。

五、GPUStack 多节点运维:我们留下的 6 条经验

GPUStack 将 4 个节点纳入同一套配置与实例管理流程,方便我们反复验证不同组合。两天调优下来,下面这些操作经验也值得保留。

本节描述的是本次部署中的观察。API 路径、日志位置和容器行为应以实际安装版本为准。

1. 修改配置后,做一次读回验证

本次使用的 API 操作主要有三类:

text 复制代码
GET    /v2/models/<model_id>
PUT    /v2/models/<model_id>
DELETE /v2/model-instances/<instance_id>

分别用于读取模型配置、更新配置,以及删除实例以触发重建。它们是操作路径示意,调用时还需要对应的认证信息和请求体。

一个实用习惯是:PUT 后再 GET 一次,确认关键字段已经按预期保存。

2. 单独检查自定义镜像是否生效

本次部署中,我们曾多次观察到 PUT 后 image_name 变为 None,随后从节点使用了非预期镜像。

我们的处理方式是读回配置,发现异常后补齐字段重新提交,并继续核对各节点实际运行的镜像。这个现象作为本次环境的排障记录保留,不据此判断所有版本都有相同行为。

3. 从节点进度,要结合本地日志判断

本次部署中,Web 面板提供的日志主要反映主实例状态。从节点停留在"Preparing model files"状态时,我们会进入对应的 Worker 容器查看日志:

bash 复制代码
docker exec gpustack-worker cat /var/lib/gpustack/log/serve/INSTANCE_ID.0.log

日志路径和文件后缀需要按实际实例确认。我们曾通过重启对应 Worker 恢复异常,但这会影响该 Worker 上的服务,应先保留日志并确认影响范围。

4. 清理残留容器,必须精确限定对象

在本次测试中,删除实例后,部分从节点曾残留旧容器,影响下一轮从属实例的健康检查。与此同时,新实例可能很快开始创建,过宽的清理条件可能误删新容器。

因此,我们将操作拆成两步:先列出候选容器,核对旧实例身份,再按明确的容器 ID 删除。

bash 复制代码
# 先查看候选项;名称过滤只用于定位,还需核对实例身份
docker ps -a --filter 'name=<旧实例唯一后缀>' \
  --format 'table {{.ID}}\t{{.Names}}\t{{.Status}}'

# 确认属于旧实例后,将 CONTAINER_ID 替换为实际容器 ID
docker rm -f CONTAINER_ID

5. 检查 RDMA 所需的容器运行条件

本次检查的实例容器采用 privileged 模式、host 网络,并具有 /dev/infiniband 设备访问权限,为容器内使用 RoCE 提供了所需条件。

这些条件不能单独作为通信验证结果。实际使用时,我们仍结合容器配置、NCCL 日志和通信测试判断运行状态。

6. 自定义镜像,让后端选择更灵活

本次在 GPUStack 的"推理后端"中指定:

text 复制代码
镜像:eugr/spark-vllm-b12x:latest
入口命令:vllm serve

由此接入所需的 B12X vLLM 环境,再通过模型参数和环境变量管理不同测试组合。配置完成后,同样需要核对实际实例是否使用了预期镜像和启动参数。

六、最终配置与测试成绩

经过上述调整,我们将本次配置固定为:

  • 4 节点 TP=4,PP=1。
  • 主模型与 MTP 草稿侧均使用 B12X 注意力后端。
  • MTP 投机深度为 3。
  • NCCL 使用两个逻辑 RDMA 接口,显式指定 Gloo 接口。
  • gpu-memory-utilization=0.85,max-model-len=500000。

本次没有沿用参考配置中的 kv_cache_memory_bytes=8G,也没有将 gpu-memory-utilization 提高到 0.87。我们的取舍是保留更稳妥的运行余量,而不是继续追求更高的内存占用。

性能结果

最终业务性能取自 bench.py 三次运行结果的中位数;NCCL 带宽单独列出,避免与业务输出速度混为一谈。

业务指标 基线 最终结果
单流长输出解码 24.3 tok/s 51.3 tok/s ,2.11×
4 路并发聚合输出 72.0 tok/s 104.1 tok/s ,1.45×
长输入用例 TTFT 1.73 s 0.12 s ,降低约 93%

TTFT 数值对应本次测试用例。由于正文未展开输入长度、缓存命中和预热条件,不宜将它直接外推为任意长输入请求的首 token 延迟,也不单独归因于某一项网络参数。

通信测试指标 基线 调优后
NCCL AllReduce 带宽记录 4.0 GB/s 约 19~23 GB/s

另外,max-model-len 从原先记录的 32K token 调整到 500,000 token。这只是配置上限的变化,不等同于已经完成 500K token 长请求或 4 路 500K token 并发的完整验收。

重启稳定性

脚本化重启连续测试 3 轮,均在首次尝试中成功。就绪耗时约为 12.5~13.5 分钟,其中权重加载约 5 分钟,B12X 内核调优及相关初始化约 7 分钟,其余时间用于其他启动步骤。

这 3 轮结果为本次配置定版提供了依据;更长时间的运行稳定性仍需要持续观察。

写在最后:让每个关键选择都有对照数据

回看这两天,最值得保留的经验有两条。

第一,先选对可用的后端组合。B12X 解决了本次 MTP 的布局兼容性问题,也让我们得以移除排障期间的本地补丁。

第二,用对照实验决定参数。单 rail 与双 rail、投机深度 3 与 5、无 MTP 与最终组合配置,分别回答了不同的问题。把这些问题拆开测试,比一次加入所有参数更容易判断收益来自哪里。

GPUStack 为这次实验提供了统一的多节点管理入口,而最终性能来自网络、推理后端与模型配置的共同配合。对于同样在探索 Spark 集群的同学,希望这份实录能提供一条可参考的排查路径。

欢迎在评论区交流你的硬件组合、测试方法,以及 GPUStack 多节点部署中遇到的问题。


附录:本次部署的配置参考

以下内容保留本次环境的具体参数。使用前,请按实际版本、模型权重、接口名称和可用内存核对。

模型基本配置

配置项 本次设置
模型来源 local_path
本地路径 /models/GLM-5.3-Flash-NVFP4
分布式推理 开启跨 Worker 分布式推理
后端配置标识 vLLM(0.31.0-spark-custom,保留原部署记录中的标识)
镜像 eugr/spark-vllm-b12x:latest
副本数 1

backend_parameters

下面每行对应一个后端参数。--speculative-config 的 JSON 值应作为一个完整参数传入。

text 复制代码
--distributed-executor-backend=mp
--tensor-parallel-size=4
--trust-remote-code
--max-model-len=500000
--gpu-memory-utilization=0.85
--max-num-seqs=4
--max-num-batched-tokens=8192
--kv-cache-dtype=fp8
--block-size=256
--mamba-cache-mode=align
--no-enable-flashinfer-autotune
--enable-auto-tool-choice
--tool-call-parser=glm47
--reasoning-parser=glm47
--attention-backend=B12X
--speculative-config={"method":"mtp","num_speculative_tokens":3,"moe_backend":"humming","attention_backend":"B12X"}

环境变量

在 GPUStack 中按键值分别填写:

text 复制代码
VLLM_USE_AOT_COMPILE=1
VLLM_USE_MEGA_AOT_ARTIFACT=1
VLLM_USE_V2_MODEL_RUNNER=1
CUTE_DSL_ARCH=sm_121a
VLLM_MEMORY_PROFILER_ESTIMATE_CUDAGRAPHS=0
VLLM_SSM_CONV_STATE_LAYOUT=DS
NCCL_IB_HCA=rocep1s0f0,roceP2p1s0f0
NCCL_IB_TC=104
NCCL_IB_QPS_PER_CONNECTION=4
NCCL_IB_SPLIT_DATA_ON_QPS=1
NCCL_SOCKET_IFNAME=enp1s0f0np0
GLOO_SOCKET_IFNAME=enp1s0f0np0

排障期间额外开启以下日志变量:

text 复制代码
NCCL_DEBUG=INFO
NCCL_DEBUG_SUBSYS=INIT,NET

启动前核对

  1. 4 个节点均已完成本次 checkpoint 所需的第 45 层量化豁免配置,并保留原始配置备份。
  2. MTU、DSCP 信任、优先级映射及网卡侧 PFC 配置已回读确认,持久化配置与运行状态一致。
  3. RDMA 接口名称和 Gloo 接口名称与各节点实际设备一致。
  4. 本次配置未固定 NCCL_IB_GID_INDEX,由 NCCL 自动选择;启动后结合日志核对实际选择结果。
  5. 重建实例后,确认各节点使用预期镜像与参数,并完成通信、健康检查及真实推理验证。

加入 GPUStack 社区

GPUStack 社区聚焦 AI 基础设施与大模型实践。

社区中持续分享真实环境下的部署经验、问题排查案例,以及推理引擎、算力管理和系统架构相关讨论。

欢迎扫码加入 GPUStack 社区,与更多关注 AI Infra 与大模型推理实践的伙伴一起交流、学习与分享。

若群聊已满或二维码失效,请访问 GPUStack 社区二维码 查看最新信息。

About GPUStack

GPUStack 是由 GPUStack.ai 推出的开源项目。GPUStack.ai 成立于 2022 年,是 AI Infra 解决方案提供商,目前已完成两轮、共计 1.03 亿元融资,包括 5,300 万元种子轮融资及 5,000 万元 Pre-A 轮融资。创始团队成员均来自业界应用广泛的 Kubernetes 管理平台 Rancher 核心团队。其中,联合创始人兼 CTO 梁胜博士曾任 SUSE 全球工程及创新总裁;加入 SUSE 之前,他于 2014 年 9 月创立 Rancher Labs 并担任 CEO。

我们的愿景是赋能企业在任意环境中使用人工智能,实现业务的卓越运营。GPUStack 是实现这一目标的重要一步。

使用 GPUStack,迅速搭建你的专属 MaaS 平台!从本地快速构建 GPU 集群开始,运行和使用各类 AI 模型,为 AI 应用提供支持。

【开源地址】:github.com/gpustack/gpustack

相关推荐
leizhen1984080934 分钟前
操作系统正在被为智能体重做_上集(漫画)
人工智能·安全
BD_Marathon34 分钟前
【RAG开发】LangChain调用大语言模型
人工智能·语言模型·langchain
大鹏的NLP博客36 分钟前
工业视觉目标检测阈值选择与生产决策评估报告
人工智能·目标检测·图像检测
xiaoyunxuexibiji37 分钟前
供应商认证管理怎么做?从资质审核到现场认证、样品认证
运维·服务器·人工智能
嘉行文39 分钟前
传统机器学习与深度学习的特点与选择
人工智能·深度学习·机器学习
咕泡科技39 分钟前
深度学习碾压机器学习?经典机器学习是否真的无用了?从LLM底层逻辑到工业落地全景解析
人工智能·深度学习·机器学习·llm·nlp
腾讯云大数据1 小时前
腾讯云AI Native数据平台功能发布合集【9月】
人工智能·腾讯云
谢客诚科技1 小时前
镀膜脆性薄片,不能接触镀膜面,脆性薄片搬运选伯努利还是边缘承靠式末端模组?
人工智能·机器人