导读:本期聚焦于Ada创作的《如何用Ray Placement Groups解决集群资源碎片与调度亲和性问题?》,敬请观看详情。集群资源明明有剩余,Ray任务却持续PENDING,这通常不是总量不足,而是资源碎片在作怪。调度器要求整块CPU或内存资源,节点上被切碎的空闲片段无法满足分配请求,导致作业长时间排队。Placement Groups提供了显式预留与分组调度能力,支持STRICT_PACK、PACK、SPREAD等策略,既可以把任务集中到同一节点减少数据搬运,也能按拓扑打散提升容错性。本文从碎片产生的原因讲起,演示如何创建Placement Group并绑定任务,再结合自定义资源标签实现节点亲和性约束,最后给出GPU训练场景下的部署建议,帮助集群在混合负载下获得更稳定的资源分配效果。

在Ray集群里,任务调度依赖每个节点的可用资源视图。节点启动时会向GCS上报总资源,例如16核CPU、32GB内存、1张GPU。随着任务不断创建和销毁,资源被反复分配与释放,节点上会出现大量不连续的空闲片段。比如一个节点总共有8核,当前空闲6核,但分别位于两个NUMA节点,单个任务要求4核整块分配时就无法满足。这就是资源碎片。调度器看到的资源总量是够的,但无法找到满足请求的整块资源,任务只能停留在PENDING状态。

如何用Ray Placement Groups解决集群资源碎片与调度亲和性问题?

资源碎片在大对象内存分配和GPU调度中尤其明显。Ray的对象存储使用共享内存,分配器需要连续地址空间。如果节点内存被切成许多小块,即使剩余内存总和超过对象大小,也会出现OutOfMemory错误。GPU任务同样如此,假设每个节点有8张卡,已经被占用了1、3、5、7号卡,剩下4张卡分散在不同拓扑分组中,而训练作业要求4卡NVLINK互联,就会调度失败。要解决这类问题,不能只盯着资源总量,还需要显式控制任务放置方式。

一、Placement Groups如何反向预留资源

Placement Groups的核心思路是允许应用先声明一组资源,再由Ray统一找节点并预留。它改变的不仅是过滤条件,而是资源分配的顺序:普通任务由调度器逐个寻找节点,而Placement Groups先把多个资源单元捆绑成一个组,再整体放置。这样可以避免前面的任务把节点切碎后,后面的大请求无家可归。创建时通过bundle描述每个资源单元,例如一个bundle需要2核CPU和4GB内存,一组Placement Group可以包含多个bundle。

Ray提供四种策略。STRICT_PACK要求所有bundle放到同一个节点,适合需要共享内存的分布式任务;PACK尽量放在同一个节点,但节点资源不足时允许跨节点,灵活性更高;SPREAD要求不同bundle尽量分布在不同的节点,适合提高容错性;STRICT_SPREAD严格要求每个bundle占用不同节点,如果节点数不够就直接失败。下面的代码创建一个包含两个bundle的Placement Group,每个bundle申请4核CPU和8GB内存,并采用STRICT_PACK策略。

import ray
from ray.util.placement_group import placement_group
from ray.util.scheduling_strategies import PlacementGroupSchedulingStrategy

# 初始化Ray集群
ray.init(address="auto")

# 创建Placement Group:两个bundle,每个4核CPU、8GB内存
pg = placement_group(
    bundles=[{"CPU": 4, "memory": 8 * 1024 * 1024 * 1024}, {"CPU": 4, "memory": 8 * 1024 * 1024 * 1024}],
    strategy="STRICT_PACK"
)

# 等待Placement Group就绪
ray.get(pg.ready())
print(pg.bundle_specs)

代码中的内存单位是字节,因此8GB写成8 * 1024 * 1024 * 1024。bundle_specs可以查看每个bundle实际落入的节点ID。创建完成后,任务需要显式绑定Placement Group,否则不会自动使用预留资源。绑定方式是在@ray.remote装饰器中传入scheduling_strategy参数。

@ray.remote(num_cpus=4, memory=8 * 1024 * 1024 * 1024)
def train_model():
    return "training started"

# 将任务调度到Placement Group的第一个bundle
strategy = PlacementGroupSchedulingStrategy(
    placement_group=pg,
    placement_group_bundle_index=0
)

future = train_model.options(scheduling_strategy=strategy).remote()
print(ray.get(future))

通过placement_group_bundle_index可以精确指定任务使用哪个bundle。这样多个任务就能共享同一组预留资源,避免与集群中的碎片资源竞争。需要注意,Placement Group的生命周期独立于任务,任务结束后资源不会自动释放,必须显式调用ray.util.remove_placement_group(pg)来清理,否则会出现资源泄漏。

二、亲和性调度:让数据与算力靠得更近

Placement Groups解决的是整块资源预留问题,亲和性则进一步约束资源应该去哪些节点。Ray的亲和性可以通过自定义节点标签实现。启动worker节点时,给节点打上标签,例如分区、机架、网络区域或硬件型号。调度时在@ray.remote中通过resources参数要求任务只落在包含该标签的节点上。这样的好处是训练数据已经缓存在特定节点本地磁盘,或者GPU与特定网络拓扑绑定,可以显著减少数据加载和通信开销。

例如某个团队有A、B两个GPU资源池,A池是A100卡,B池是V100卡,训练脚本只兼容A100。可以在启动节点时设置资源标签:

# 启动节点时指定资源标签
ray start --address="主节点地址" --resources='{"gpu_type": 1, "zone": "east"}'

注意命令行中的JSON字符串需要使用单引号包裹,避免shell解析花括号。任务声明资源需求时,除了常规的num_gpus,还声明gpu_type资源为1:

@ray.remote(num_gpus=1, resources={"gpu_type": 1})
def inference():
    return "using A100"

result = ray.get(inference.remote())

如果只想让任务运行在特定节点,还可以把自定义资源设为一种计数型资源。节点有多少张对应类型的卡,就把该资源设为多少;任务申请1则消耗1。这样既实现了亲和性,又保持了资源计量的准确性。Placement Group同样支持自定义资源,bundle里可以写{"CPU": 8, "GPU": 1, "gpu_type": 1},从而同时控制资源数量和节点类型。

另一种亲和性思路是使用节点IP或主机名作为软约束。Ray虽然没有直接的node affinity API,但可以通过Placement Group的bundle索引加自定义资源变相实现。如果确实需要绕过调度器直接固定节点,可以在节点上创建只有该节点具备的唯一资源,例如{"node_rack": 1},任务强制申请该资源即可。这种方法简单直接,但会降低集群弹性,节点故障后任务无法自动迁移,只适合对数据局部性要求极高的场景。

三、GPU训练场景的完整规划与调优建议

以一个8节点、每节点8卡A100的集群为例,训练任务需要2个节点共16卡进行数据并行。如果直接提交16个GPU需求,调度器可能把它们分散到16个不同节点,导致跨节点通信成为瓶颈。更合理的做法是先创建STRICT_SPREAD或PACK的Placement Group,每个bundle申请8卡并带gpu_type标签,再让16个worker任务均匀绑定到两个bundle。这样既能保证卡的数量,也能保证拓扑相对集中。

代码可以这样组织:先创建两个bundle,每个bundle申请8张GPU。策略选择PACK,允许节点不足时跨节点,但优先满足同一节点。然后为每个bundle创建8个remote任务,通过placement_group_bundle_index指定归属。主训练进程等待所有worker准备就绪后再建立通信组。如果使用Ray Train,可以直接传入Placement Group配置,由框架自动分配。

from ray.util.placement_group import placement_group

pg = placement_group(
    bundles=[
        {"GPU": 8, "gpu_type": 1},
        {"GPU": 8, "gpu_type": 1}
    ],
    strategy="PACK"
)
ray.get(pg.ready())

@ray.remote(num_gpus=1, resources={"gpu_type": 1})
def worker(index):
    return f"worker {index} started"

# 每个bundle启动8个worker
for bundle_index in range(2):
    for i in range(8):
        strategy = PlacementGroupSchedulingStrategy(
            placement_group=pg,
            placement_group_bundle_index=bundle_index
        )
        worker.options(scheduling_strategy=strategy).remote(i)

上例中bundle申请了8张GPU,但每个worker只申请1张,这样同一个bundle内可以容纳8个worker。如果bundle的GPU数量与worker数量不匹配,Placement Group会预留过多资源,造成新的浪费。因此规划时要先明确每个bundle承载的任务数量,再反推每个bundle的资源规格。一般建议bundle的粒度与单个worker或单个进程的资源需求保持一致,便于复用和清理。

资源碎片还会影响动态扩缩容。Ray Autoscaler在缩容节点时,会优先选择空闲资源最多的节点,但如果这些节点上仍有Placement Group预留,可能无法正常释放。调优时需要定期清理不再使用的Placement Group,并设置较短的生命周期。可以使用try-finally保证异常情况下也能调用remove_placement_group。此外,监控GCS的调度延迟和PENDING任务数量,能够及早发现资源碎片是否已经影响整体吞吐。

RayPlacement Groups资源碎片修改时间:2026-10-04 00:51:53

免责声明:已尽一切努力确保本网站所含信息的准确性。网站作品多为原创整理与精心创作,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们进行处理Email:chomcom@qq.com。
引用或转载本作品时,请注明当前出处:https://www.ipipp.com/html/1004/65320.html,基于非商业用途的前提下,欢迎转载或二创本作品。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。