Skip to content
Published on

Ray 2.56 的标签局部性调度 — 放置组开始看见 NVLink 机架,而不是节点

分享
Authors

引言 — 节点边界不再是性能边界

分布式调度器里有一条由来已久的隐含假设:「快速互连止于节点之内」。在 8-GPU 的 HGX 服务器时代,这条假设大体为真 — NVLink 在节点内部把 GPU 绑在一起,节点之间则落到 InfiniBand 或以太网。所以只要调度器懂得「往同一个节点上堆」,快速通信就自动跟着来了。

NVIDIA 的机架级系统打破了这条假设。按 NVIDIA 的规格(厂商自测数值),GB300 NVL72 在一个机架里把 72 颗 Blackwell Ultra GPU 和 36 颗 Grace CPU 绑成一个 NVLink 域,并给出 130 TB/s 的 NVLink 带宽。也就是说,「快速互连」的边界如今是机架,而不是节点。这是一条只按节点思考的调度器看不见的边界。

2026 年 6 月 29 日发布的 Ray 2.56.0,把 Ray 核心对这一地形变化的第一个回应放进了发布亮点 — 能感知 GPU 域的放置组,官方名称是标签局部性调度(label locality scheduling)。本文根据发布说明、文档和源码里能确认到的内容,梳理这项功能解决了什么、究竟怎么工作、以及为什么贴着 alpha 的标签。

放置组复习 — 四种策略全都是节点粒度的

Ray 的放置组(placement group)是一种把一组资源 bundle 原子性预留下来的 gang 调度原语。它表达的是「4 个 GPU、2 个 CPU 的 bundle 一共 18 个,要么全都拿到,要么一个都别占」这样的诉求。对于多节点训练、分布式推理这类只起来一部分 worker 就毫无意义的工作负载,它是基本工具,用 Ray Serve 做模型服务分布式训练栈底下跑的也是它。

放置策略有四种 — PACK(倾向堆到一个节点)、STRICT_PACK(强制一个节点)、SPREAD(倾向按节点分散)、STRICT_SPREAD(强制按节点分散)。这里值得注意的词不是策略名,而是「节点」。四种策略里,放置的单位都是节点。比节点更大的拓扑 — 机架、NVLink 域 — 在这套词汇里并不存在。

问题 — 在 NVL 机架上,STRICT_PACK 不可能,PACK 又漠不关心

Ray 2.56.0 文档举的例子原样搬过来是这样。在一个 2 个机架、每机架 18 个节点、每节点 4 个 GPU 和 2 个 CPU 的集群里,你想把 18 个 bundle 放进同一个机架。

from ray.util.placement_group import placement_group

# 尝试 1: STRICT_PACK -> 想把 18 个 bundle 塞进「一个节点」。
# 节点上只有 4 个 GPU,所以这个放置组会永远 pending。
pg = placement_group([{"GPU": 4, "CPU": 2}] * 18, strategy="STRICT_PACK")

# 尝试 2: PACK -> 会分摊到多个节点,但没有「机架」这个概念,
# 所以哪怕 bundle 横跨两个机架,调度器也毫无怨言。
pg = placement_group([{"GPU": 4, "CPU": 2}] * 18, strategy="PACK")

横跨两个机架会发生什么呢 — 跨过去那一处的通信会掉到 NVLink 域之外,也就是掉到机架间网络上。等于说,你为了待在域内才买的硬件,被调度器废掉了一半。

当然,绕过的办法是有的。给每个节点打上自定义标签,再用静态标签选择器钉死 — 比如要求所有 bundle 都带 rack-1 标签。文档明示了这个绕法的两点限制。第一,选择器是静态的,应付不了故障 — rack-1 的节点全死了,放置组没法自动搬到别的机架。第二,你真正想要的其实是「随便哪个机架,一个就行」,却不得不用手指定某个特定机架,机架一多,这份管理本身就成了负担。

Ray 2.56 的答案 — 在节点策略之上叠一层域级层

标签局部性调度不替换既有的节点粒度策略,而是在它们之上叠一层域级调度层。按文档的说法,动作分三步。

  1. 把候选节点按 ray.io/gpu-domain 标签的值分组 — 值相同就是同一个域。
  2. 挑出一个能容纳全部 bundle 的域。
  3. 在那个域里,原样套用用户指定的节点级策略。

域级只支持 STRICT_PACK — 要么整个放置组进得了同一个域,要么就不被调度。触发条件是,放置组的所有 bundle 都在 bundle_label_selector 里要求 GB200 或 GB300 加速器类型。

import ray
from ray.util.placement_group import placement_group

bundles = [{"GPU": 4, "CPU": 2}] * 18
label_selector = [{"ray.io/accelerator-type": "GB300"}] * 18

pg = placement_group(
    bundles=bundles,
    bundle_label_selector=label_selector,
)

ray.get(pg.ready())  # 18 个 bundle 全部落在同一个 ray.io/gpu-domain 里

触发条件有多窄,在 2.56.0 标签的源码里能原样确认 — 触发器字面上就是 GB200 和 GB300 这两个值的硬编码集合,而且哪怕只有一个 bundle 带了这个选择器、其余不是同一个值,就会抛 ValueError。不管是 H100 还是 B200,只要写别的加速器类型,就只会当作没有域层的普通静态标签选择器来工作。

这里有一处运维人员务必知道的不对称。这两个标签的出处不同。

  • ray.io/accelerator-type 是 Ray 自动打的 — 按标签文档,默认节点标签是 ray.io/node-id 和 ray.io/accelerator-type 这两个,后者由 NVML 报告的 GPU 名称推导而来(CPU 节点上不会打)。
  • ray.io/gpu-domain 则不由 Ray 自动打。这是文档明确写下来的地方 — 节点属于哪个机架,得由运维人员来填。
# 节点所属的 NVLink 域(机架)必须自己用标签指定
ray start --labels="ray.io/gpu-domain=rack-1"

也就是说,「域感知」有一半是配置流水线的活儿。如果没有在节点启动时拿到机架标识并写进这个标签的自动化,这项功能连起步都做不到。顺带一提,自动扩缩容那边是准备好的 — 把域约束和标签选择器传给自动扩缩容协议的改动(#61614#62487)进了同一个版本,所以扩容判断也会收到域信息。

故障语义 — 文档诚实写下来的部分

这项功能存在的理由有一半是应对故障(毕竟静态选择器在机架挂掉时就到此为止了)。文档明示的语义分两支。

部分故障 — 域内一部分节点死掉时,Ray 会把丢失的 bundle 重新调度到同一个域里还活着的节点上。剩下的 bundle 上的 actor 和 task 继续跑。不过,如果同一个域里资源不够,那些 bundle 就会以 infeasible 状态留在队列里,一直等到同一个域腾出资源为止。想搬到别的域的话,办法只有一个 — 用 remove_placement_group 删掉再新建。而这个调用会把用着这个放置组 bundle 的 actor 和 task 全部强制终止,并且不会帮你重启。恢复的自动化是用户自己的事。

整体故障 — 域里的节点全死掉时,Ray 会清掉域分配,把整个放置组重新调度到别的域。静态标签选择器做不到的,正是这一点。

落在哪儿是可以确认的。仪表盘的放置组表格里多了 Label Domain 列(#62533),状态 API 里也看得到。

ray list placement-groups --detail
# 部分输出 (对 2.56.0 文档示例的摘录)
- placement_group_id: 237f47c3235ac1a96ad423c3f74501000000
  name: gpu-domain-pg
  state: CREATED
  label_domain_key: ray.io/gpu-domain
  label_domain_assignments:
    ray.io/gpu-domain: rack-2

不使用标签局部性的放置组,label_domain_key 是空字符串。

alpha 就是 alpha — 限制清单

这是一项文档自己就警告说处于 alpha 的功能,限制相当明确。这里只列出已确认的。

  • 触发器是 GB200、GB300 专用。是源码里硬编码的两个值,文档也明示只支持这两种加速器类型。作为这项功能起点的 PR #61442(2026 年 4 月 3 日合入)也不掩饰目的 — 它是为 NVML 域(抽象成 GPU 域)而做的,并写明首先瞄准 GB300 机架。
  • 域级策略只有 STRICT_PACK 一种。文档提到了日后增加域级策略、以及支持任意标签域的计划,但眼下只是计划。
  • ray.io/gpu-domain 标签是手动的。把机架拓扑誊写成标签的自动化得自己造,而这个标签一旦写错,调度器就会相信错误的拓扑。
  • 部分故障时「在同一个域等待」的语义是双刃的。如果目标是域内恢复,这是正确的默认值;但对于「哪个域都行,快点重新起来」更重要的工作负载,在 infeasible 队列里无限期等待反而是亏,而唯一的出口是甘冒 actor 全灭风险的重建。
  • 这是刚出炉的代码路径。就在同一个发布周期里,已经进了一个修复 — 标签域放置组被卡在 infeasible 队列里的 bug(#62483,4 月 25 日合入)。alpha 这个标签不是装饰。
  • 没有公开的性能数字。发布说明里没有,文档里没有,PR 里也没有这项功能的基准测试。它本来就不是速度优化,而是放置语义的功能,收益的大小由「域内 NVLink 带宽与机架间网络带宽之差 × 你的工作负载的节点间通信量」决定。那个数字,得在你自己的集群上测。

这项功能所处的位置 — 向标签收敛的 Ray 调度

标签局部性不是一项孤立的功能,而是 Ray 调度在标签选择器之上重组这条脉络的最新一步。能确认到的时间线是这样 — 标签调度本身从 2.49(2025 年 8 月)起作为 beta 功能进来(文档介绍它是「用于从 KubeRay 控制调度」,但 ray start --labels 这类通用路径也一并有文档),从 2.50 起,只要是开了自动扩缩容 v2 的动态集群,就懂得为满足标签要求而在指定的 worker 组里增加节点,2.53 则给 Ray Train 的 ScalingConfig 加上了 label_selector。而 2.56,在把 Ray 内部各库用的 NodeAffinitySchedulingStrategy(soft=False) 换成 ray.io/node-id 标签选择器的工作(#54940)之外,还载上了这次这项在标签之上叠加域概念的功能。方向就是把一个个特殊用途的调度策略逐步吸收进标签词汇里。TPU 那边一直是用 SlicePlacementGroup 这条单独的路径在解同一个问题,而这次的版本给它也加上了显式的 bundle_label_selector 支持(#63171)。

这个方向本身并非 Ray 独有。Kubernetes 也正用 v1.36 的 Workload、PodGroup API 把 gang 调度和拓扑感知拉进核心调度器。机架级硬件在给各家调度器出同一道作业题,而大家都开始写下同一个答案 —「比节点更大的单位」。

什么时候用,什么时候可以无视

有理由用它的场景,条件很清楚 — 你有 GB200、GB300 的 NVL 机架,集群里的域不止一个,并且要跑一个放置组会横跨多个节点的工作负载(多节点训练、跨节点的张量/专家并行推理)。这时这项功能给你的是两样东西。防止跨域,以及不必用手指定域(包括整体故障时的自动搬家)。

反过来,下面这些情况可以无视。

  • 工作负载装得进一个节点 — STRICT_PACK 已经是答案了。
  • 没有多节点的 NVLink 域 — 在 8-GPU 服务器的集群里,节点边界就是域边界,既有策略已经够用,而且本来不是 GB200、GB300 的话触发器也不会发动。
  • 整个集群就是一个域 — 可选的域只有一个,域选择层就没什么可做的。
  • 故障时「随便在哪儿快点重启」比「在同一个域等待」更重要 — 这项功能的部分故障语义反倒可能拖你后腿。要么备好重建的自动化再用,要么现在还是先保持距离为好。

结语

归纳起来是这样。Ray 的放置组至今只有节点粒度的词汇,而 NVL72 这类机架级系统造出了那套词汇表达不了的边界 — 多节点的 NVLink 域。Ray 2.56.0 的标签局部性调度,把带有相同 ray.io/gpu-domain 标签的一组节点定义为域,并在节点策略之上叠一层,把整个放置组 STRICT_PACK 进同一个域。与静态标签选择器不同,整个域故障时它会自动搬到别的域去。

同时,这项功能忠实于 alpha 的定义。GB200、GB300 的硬编码触发器、域级 STRICT_PACK 这一种策略、手动的 gpu-domain 标签、破坏性的域强制迁移路径,以及没有公开的性能数字。如果你真的在运维 NVL 机架,现在就值得开始验证;如果不是 — 这项功能对你产生意义的那一天,硬件采购订单会先告诉你。

参考资料