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

资源碎片在大对象内存分配和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