2026/9/20 4:49:18

用资源约束限制 Ray 并发任务数量:基于 memory 与 num_cpus 的 OOM 防护实践

用资源约束限制 Ray 并发任务数量:基于 memory 与 num_cpus 的 OOM 防护实践 用资源约束限制 Ray 并发任务数量基于 memory 与 num_cpus 的 OOM 防护实践【免费下载链接】rayRay is an AI compute engine. Ray consists of a core distributed runtime and a set of AI Libraries for accelerating ML workloads.项目地址: https://gitcode.com/gh_mirrors/ra/ray导读在 Ray 中任务的并行度由调度器根据资源可用性自动决定。但当任务需要加载大量数据到堆内存时默认的并发策略可能让节点内存被迅速耗尽并触发 OOM。本文介绍 Ray 官方核心模式「使用资源Resources限制并发运行任务数」通过调整任务的memory、num_cpus等资源需求主动控制单节点上同时运行的任务与 Actor 数量从根本上预防内存过载。读完本文你将掌握资源需求与调度准入的底层机制、无限制与有限制两种写法的对比以及 Actor、逻辑资源、自定义资源等边界场景的处理方法。模式概述为什么要限制并发运行的任务数在 Ray 中任务Task和Actor的并发度并非无约束的调度器会依据每个节点可用的资源数量来决定能同时运行多少个任务。默认情况下Ray 的每个任务默认请求1 个 CPU资源Ray 的每个 Actor 默认请求0 个 CPU资源调度时 1 个、运行时 0 个详见后文。因此调度器会把任务的并发数限制在可用 CPU 数量以内而 Actor 的并发数则几乎是无限的。对于单个任务使用超过 1 个 CPU例如通过多线程的场景并发任务之间可能因竞争而出现性能下降但一般不会引发严重问题。真正危险的是内存如果任务或 Actor 使用的内存超过了它应有的份额多个并发实例叠加起来就可能让节点过载引发 OOM内存耗尽。针对这种情况本模式给出的解决方案非常直接——提高每个任务或 Actor 请求的资源量从而减少每个节点上并发运行的任务或 Actor 数量。这一做法之所以有效是因为 Ray 保证同一节点上所有并发运行的任务与 Actor 的资源需求之和不会超过该节点的总资源。这是 Ray 调度器在准入控制admission control环节强制保证的不变量也是本模式的原理基石。注意Actor 任务对于 Actor 上执行的任务actor tasks并发运行的 Actor 数量会直接限制可同时运行的 actor task 数量。也就是说想限制 actor task 的并发度先要限制 Actor 本身的并发度。典型使用场景数据处理负载的 OOM 防护本模式最典型的应用场景如下你有一个数据处理工作负载它使用 Ray 的远程函数remote functions 独立地处理每一个输入文件。由于每个任务都需要把输入数据加载进堆内存并完成处理同时运行的任务过多时很容易导致 OOM。在这个场景中每个任务都需要大量堆内存而 CPU 用量可能并不高。此时就可以用memory资源来限制并发运行的任务数使用num_cpus等其他资源也能达到同样的目的。需要特别强调的是与num_cpus类似memory资源需求是逻辑的。Ray 不会强制监控每个任务的实际物理内存占用也不会在任务实际内存超过声明值时干预或杀死任务。也就是说资源需求只影响调度准入真正的内存自律需要由任务自身保证——这是一个在使用本模式前必须理解的前提。代码示例无限制 vs 有限制完整的可运行示例位于仓库中的 limit_running_tasks.py下面对照展开。无限制版本16 个并发任务压垮 16G 内存import ray # 假设该 Ray 节点有 16 个 CPU 和 16G 内存。 ray.init() ray.remote def process(file): # 实际工作为读取文件并处理数据。 # 假设每个任务需要占用 2G 内存。 pass NUM_FILES 1000 result_refs [] for i in range(NUM_FILES): # 默认情况下process 任务使用 1 个 CPU 资源不请求其他资源。 # 这意味着 16 个任务可以并发运行 # 而 16 个任务总共需要 32G 内存远超节点 16G必然 OOM。 result_refs.append(process.remote(f{i}.csv)) ray.get(result_refs)在没有限制的版本中process任务只声明了默认的 1 CPU 资源没有声明任何内存需求。节点有 16 个 CPU所以调度器允许 16 个任务并发运行而每个任务实际需要 2G 内存16 个并发任务共需要 32G远超节点 16G 的内存容量最终会触发 OOM。有限制版本通过 memory 资源把并发数压到 8result_refs [] for i in range(NUM_FILES): # 现在每个任务声明使用 2G 内存资源 # 并发运行的任务数被限制为 8 个16G / 2G。 # 在这种场景下把 num_cpus 设为 2 也能达到同样的效果。 result_refs.append( process.options(memory2 * 1024 * 1024 * 1024).remote(f{i}.csv) ) ray.get(result_refs)关键改动只有一处通过process.options(memory2 * 1024 * 1024 * 1024)为每个任务显式声明2G 内存资源需求注意单位是字节2 * 1024 * 1024 * 1024即 2GiB。节点总内存为 16G因此调度器最多允许16G / 2G 8个任务并发运行8 个任务总共最多消耗 16G 内存恰好不会超过节点容量OOM 问题随之消除。代码注释中还给出了一个等价写法把num_cpus设为 2 也能达到同样的并发限制效果16 个 CPU / 2 CPU 8 个并发任务因为调度器只关心资源需求总和不超过节点资源这一约束而不在乎具体使用哪种资源。原理纵深Ray 资源需求与调度准入机制要深入理解本模式需要掌握 Ray 资源系统的基本模型详见文档 Resources。资源是逻辑的而非物理的Ray 中的资源是一个键值对键是资源名称值是浮点数数量。CPU、GPU、内存是 Ray 原生支持的预定义资源除此之外还支持自定义资源。关键的语义是Ray 资源是逻辑资源不必与物理资源一一对应。例如可以通过ray start --head --num-cpus0在物理上拥有 8 个 CPU 的机器上启动一个拥有 0 个逻辑 CPU 的 head 节点这样做主要是让调度器不在 head 节点上调度任务把资源留给 Ray 系统进程。逻辑资源的语义带来了几个重要推论资源需求不约束实际物理资源使用Ray 不会阻止一个num_cpus1的任务启动多线程并使用多个物理 CPU同样声明memory2G的任务若实际吃掉 4G 内存Ray 也不会干预。确保任务实际用量不超过声明值是开发者自己的责任。Ray 不做 CPU 隔离不会为某个任务预留物理 CPU 或做亲和性绑定线程调度交给操作系统必要时可用sched_setaffinity等系统 API 自行绑定。Ray 提供 GPU 隔离通过自动设置CUDA_VISIBLE_DEVICES环境变量以可见设备的形式隔离 GPU详见 Accelerators。此外若为任务/Actor 设置了num_cpusRay 会为其设置OMP_NUM_THREADSnum_cpus环境变量未指定时则设为 1以避免多 worker 场景下 OpenMP 线程争抢。这一点在使用多线程线性代数库numpy、PyTorch、TensorFlow时值得留意。节点资源的默认配置规则在不做任何显式配置时Ray 会按以下规则自动确定每个节点的逻辑资源量逻辑 CPU 数num_cpus等于机器/容器的 CPU 数逻辑 GPU 数num_gpus等于机器/容器的 GPU 数内存memory等于 Ray 运行时启动时可用内存的70%对象存储内存object_store_memory等于可用内存的 30%注意对象存储内存不是逻辑资源不能用于调度。因此本文示例中节点有 16G 内存对应的实际逻辑memory资源量约为 11.2G16G × 70%读者在自己环境中复现时应结合实际节点内存与配置计算并发上限。注意 Ray不允许在节点启动后动态更新资源容量。调度保证并发资源需求之和不超过节点总量文档 Resources 中明确给出了任务与 Actor 资源需求对调度并发度的影响同一节点上所有并发执行的任务与 Actor 的逻辑资源需求之和不能超过该节点的总逻辑资源。这正是本模式的全部依据把每个任务的内存需求声明得越大能被同时调度到该节点上的任务就越少。文档还专门把利用这一性质限制并发运行任务、规避 OOM的做法链接到了本文讲解的模式即core-patterns-limit-running-tasks说明这是官方推荐的标准实践。用 options() 与 ray.remote() 声明资源需求除了示例中的process.options(memory...)还有多种声明资源需求的方式在装饰器中声明ray.remote(num_cpus2, memory2 * 1024 * 1024 * 1024)在调用时声明process.options(num_cpus2, memory...)使用自定义资源process.options(resources{special_hardware: 1})。Java 与 C 也支持等价写法例如 C 中为ray::Task(MyFunction).SetResource(CPU, 1.0)、ray::Actor(CreateCounter).SetResource(GPU, 1.0)。分数资源需求Ray 支持分数资源需求。例如任务/ Actor 是 IO 密集型、CPU 占用很低时可以指定num_cpus0.5甚至num_cpus0。分数资源需求的精度为 0.0001应避免使用超出该精度的浮点数。另需注意GPU、TPU、neuron_cores 资源需求若大于 1必须为整数例如num_gpus1.5是非法值。Actor 场景与默认值陷阱文档 Actors 与 Resources 中明确指出默认情况下Ray 任务使用 1 个逻辑 CPU 资源Actor 在调度时使用 1 个逻辑 CPU在运行时使用 0 个逻辑 CPU。这意味着默认情况下 Actor 无法被调度到 0 CPU 节点上但在任何非 0 CPU 节点上都可以无限量地运行这一默认行为是历史原因造成的。因此想限制 Actor 的并发数必须显式设置num_cpus例如ray.remote(num_cpus1)否则并发 Actor 数量不受 CPU 约束也就无法借助本模式控流若显式声明了资源需求这些资源在调度时和运行时都会被要求满足对于 actor task限制 Actor 本身的并发数即是限制其任务的并发数本模式开头 note 的内容。与其他限流模式的区分limit-pending-tasksRay 的核心模式索引中还有一个容易混淆的模式「使用 ray.wait 限制 pending 任务数」。两者的分工如下本文模式limit running tasks通过调整每个任务的资源需求控制有多少任务可以同时运行属于调度层面的资源准入控制是本模式推荐的修改并发运行数的标准方法limit-pending-tasks 模式通过ray.wait()施加背压backpressure控制有多少任务处于在途in-flight/ 排队pending状态防止无限提交任务导致 pending 队列无限增长。它主要用于限制同时飞行中的任务数虽然也可用于限制并发运行数但不推荐因为会影响调度性能。两者的适用场景也不同如果一次性提交有限数量的任务每个任务在队列中仅占用少量内存做簿记通常不会出问题真正危险的是源源不断提交任务的无限流。当面对无限任务流时优先考虑ray.wait()背压而当目标是精确控制并发运行数时则应回归本文的资源需求方案。小结与最佳实践清单总结本模式的实践要点默认并发任务默认 1 CPUActor 默认运行时 0 CPUCPU 限制了任务并发但 Actor 并发近乎无限。OOM 根因任务/Actor 实际内存用量超过其资源份额多实例叠加导致节点内存耗尽。核心手段通过task.options(memory...)、num_cpus或自定义资源提高单任务资源声明利用节点上并发资源需求之和 ≤ 节点总资源的调度不变量压降并发数。逻辑资源语义memory与num_cpus都是逻辑资源Ray 不强制监控实际物理用量任务自身需自律。节点资源默认值内存逻辑资源默认是可用内存的 70%复现示例时需按实际节点配置计算并发上限。Actor 需显式设num_cpus否则并发不受控actor task 的并发由 Actor 并发决定。区分两种限流控制运行中任务数用本模式资源需求控制排队中任务数用 ray.wait 背压模式。通过为任务声明合理的内存或 CPU资源需求你可以在不改动任何业务逻辑的前提下用一行options()调用为数据密集型负载建立可靠的 OOM 防线。【免费下载链接】rayRay is an AI compute engine. Ray consists of a core distributed runtime and a set of AI Libraries for accelerating ML workloads.项目地址: https://gitcode.com/gh_mirrors/ra/ray创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考