关于我们

质量为本、客户为根、勇于拼搏、务实创新

< 返回新闻公共列表

批量推理单机跑三天,用 Ray 拆到多台机器之前先想清楚的三件事

发布时间:2026-10-09

批量推理单机跑三天,用 Ray 拆到多台机器之前先想清楚的三件事

一个很常见的账:20 万条文本要向量化,单机单卡实测一小时出 2800 条,掐指一算 70 小时,差不多三天。于是最自然的想法是——加机器,四台就是 17 小时,八台就是 9 小时。这个算法在纸面上永远成立,在 Ray 集群里几乎从来不成立。

真正跑起来你会发现,四台机器可能只把 70 小时压到 26 小时,八台压到 18 小时,而且第 30 个小时开始集群频繁报内存溢出,dashboard 卡得打不开,最后几万条结果死活不出来。原因不在 Ray 本身,在于你加的是机器,但你没加的是对象存储容量、内网带宽、以及一个能扛住调度压力的 head 节点。

先给结论:

  • 批量推理的加速比不是机器数,而是「有效并发度 × 单任务纯度」。单任务里如果混了模型加载、磁盘 IO、跨节点取数据,你加多少台机器都是在加速那些无用功。
  • Ray 的 task 和 actor 是两种完全不同的东西。无状态的批函数用 task,需要常驻显存加载模型的 worker 用 actor,选错了模型会被反复加载几百次。
  • 任务粒度要掐在两个边界之间。细到几十毫秒被调度开销吃掉,粗到几分钟会出现尾任务拖尾,合理区间通常在「单任务 100 毫秒到数秒」这一档。
  • 对象存储比内存条更容易成为瓶颈。中间结果默认放共享内存,放不下就 spill 到磁盘,一旦 spill 开始,吞吐是断崖式下跌而不是缓慢下降。
  • 并发数开满等于自杀。一次 submit 20 万个任务,所有参数和返回值都会堆在对象存储里,OOM 只是时间问题,必须用 ray.wait 做回压。
  • 三台以内千兆内网还能凑合,五台以上建议万兆。worker 之间传大对象时,千兆的实测有效带宽只有一百兆字节每秒上下,很容易变成新的瓶颈。

一、先把业务场景和技术瓶颈拆开:你慢在哪里

「单机跑三天」是一个结果,不是一个诊断。上分布式之前,必须先回答一个问题:这 70 小时里,有多少小时是 GPU 在算,有多少小时是 CPU 在等,有多少小时是磁盘在读,有多少小时是模型在加载。

四类典型批量负载的瓶颈位置完全不同

向量化(embedding)类任务,通常是 GPU 计算密集,瓶颈在显卡本身,加机器理论上接近线性。这类任务最适合拆。

OCR 类任务比较 tricky。大部分 OCR 流水线是「读图 → 预处理(CPU)→ 检测模型(GPU)→ 识别模型(GPU)→ 后处理(CPU)」,GPU 只占其中一段。如果你单机慢是因为 CPU 预处理跟不上,那你加四台 GPU 机器,只是把 CPU 瓶颈复制了四份。

批量打分(batch scoring)类任务,常见于推荐和风控,特征是单条数据很小、条数极多。这类任务真正的敌人是调度开销和对象存储的元数据压力,不是算力。

离线字幕(ASR)类任务,输入是音频文件,单条体积大(几十兆到几百兆),瓶颈往往在数据读取和文件解码,属于典型的 IO 密集。

判断方法很土但很有效:在一台机器上开两个监控窗口,一个看 GPU 利用率(nvidia-smi dmon),一个看 CPU 和磁盘 IO(vmstat、iostat)。跑十分钟,如果 GPU 利用率长期低于 40%,说明 GPU 在等数据,你的瓶颈在别处,先别急着加卡。

用一次「单任务纯耗时测量」定基线

在做任何分布式改造之前,先把单条数据的纯处理耗时测出来。做法是:写一个最小脚本,模型加载一次,然后循环处理 1000 条数据,取第 101 条到第 1000 条的平均耗时。这个数字叫「单任务纯耗时」,后面所有粒度决策都要以它为基准。

再测一次「含加载耗时」:每条数据都重新加载一次模型,处理完释放。这个数字减去纯耗时,就是模型加载成本。如果加载成本占了大头,那你的架构必须走常驻 worker 路线,也就是 actor。

加速比的天花板由不可并行部分决定

阿姆达尔定律在这里依然适用,只是很多人算的时候漏掉了 Ray 特有的那部分串行开销:任务序列化和反序列化、对象在节点间的传输、调度器维护任务状态、driver 端收集结果。这部分开销在单机上为零,一旦跨机就变成净增成本。

一个粗略的经验:如果你的单任务纯耗时在 1 秒以上、单条数据体积在 1MB 以内、任务之间没有依赖关系,那么四机加速比做到 3.2 到 3.6 是现实的。如果单任务只有 20 毫秒、单条数据 20MB,四机能做到 2.0 就已经不错了。

二、Ray 的基础设施:五个必须先想清楚的技术变量

1. task 与 actor:无状态批量函数 vs 常驻显存的 worker

Ray 提供两种并行抽象。task(@ray.remote 函数)是无状态的,每次调用都是一次独立执行,Ray 从资源池里分配一个 worker 槽位,把函数序列化发过去,执行完把返回值放进对象存储,worker 槽位回收。actor(@ray.remote 类)是有状态的,创建之后常驻在某一个节点上,持有自己的内存和显存,后续调用都复用这份状态。

这个区别决定了 batch 推理的两种写法。

第一种写法:每个 task 内部自己加载模型。代码最短,改动最小,把原来的单进程脚本包一层 @ray.remote 就能跑。代价是每个 task 都要付一次模型加载成本。假设模型加载 8 秒、单条推理 30 毫秒、一个 task 处理 100 条,那么加载占比是 8 / (8 + 3) = 73%。也就是说你有七成机器时间在干无用功,这种情况下加机器的收益会非常难看。

第二种写法:用 actor 做常驻 worker。每台机器按显卡数量创建 actor(一张卡一个 actor,或者一张卡两个 actor 看显存大小),actor 的 __init__ 里加载模型,之后每个 actor 提供一个 predict_batch 方法接收一批数据。这样模型只加载一次,代价是你要自己管理 actor 的生命周期、故障重启、以及负载均衡。

选择规则其实很简单:模型加载时间 / 单批处理时间 > 0.1,就上 actor。小于 0.1,用 task 更省事,因为 actor 会带来额外的运维负担(actor 挂了要重启、显存泄漏了要回收)。

2. 任务粒度:一次 submit 一个文件还是一批文件

这是最容易被拍脑袋决定、也最容易出问题的参数。两个极端都会翻车。

粒度太细:一次 submit 一条数据。20 万条就是 20 万个 task。Ray 的单 task 调度开销在毫秒量级(小对象、本地调度场景下可以做到亚毫秒,跨节点和带复杂参数时会明显上升)。如果单条处理只要 20 毫秒,那么调度开销占比可能达到 10% 到 30%。更要命的是对象存储的元数据压力——20 万个对象引用要跟踪,dashboard 会被刷爆,driver 端收集结果时内存暴涨。

粒度太粗:一次 submit 一万条。任务数少了,调度开销小了,但出现了两个新问题。一是尾任务拖尾(straggler):假设你开了 32 个并发槽位,最后剩 5 个大任务分到 5 个 worker 上,其它 27 个 worker 空转等它们,最后那段时间的集群利用率可能只有 15%。二是失败重试代价高:一个大任务跑到第 9000 条挂了,重试要重跑全部一万条。

怎么估一个合理粒度?给一个可以直接用的公式:

先算并发槽位总数 N = 集群总 CPU 核数(或 GPU 数 × 每卡并发数)。然后让总任务数 T 落在 N 的 10 倍到 100 倍之间。也就是说,单批处理的数据条数 ≈ 总数据量 / (N × 10 到 N × 100)。

举例:20 万条数据,集群 4 台机器各 16 核共 64 核,N = 64,T 取 640 到 6400,那么单批处理条数在 31 条到 312 条之间。再结合单条耗时(30 毫秒),单任务耗时在 0.9 秒到 9.4 秒之间,正好落在合理区间。取中间值,一批 100 条左右,单任务约 3 秒,总任务 2000 个。这是一个能跑的粒度。

还有一条校验规则:单任务耗时的下限不要低于 100 毫秒,上限不要高于 60 秒。低于 100 毫秒说明你该合并,高于 60 秒说明你该切分(除非你接受了长任务的故障域)。

3. 对象存储与内存预算:spill 一旦开启,吞吐是断崖

Ray 的对象存储(object store)是一个基于共享内存(/dev/shm)的进程间数据通道。task 的返回值、actor 方法调用的返回值、以及通过 ray.put 显式存放的对象,都住在这里。它的容量默认是节点可用内存的一部分(不同版本默认比例不同,常见在 30% 上下),也可以启动时用 --object-store-memory 显式指定,云环境里通常按实例内存的某个比例自动算。

关键点在于:对象存储不是磁盘缓存,它是内存。它的读写速度和内存一样快,这是 Ray 能做到低延迟传递大对象的原因。但它的容量和内存一样小。

当对象存储被写满,Ray 会启动 spill 机制,把一部分对象溢出到本地磁盘(默认 spill 目录是 /tmp/ray,生产环境强烈建议改到数据盘)。这里有一个非常反直觉的现象:spill 不是让吞吐慢慢下降,而是断崖。

原因是 spill 的触发往往是批量的。当对象存储水位到达阈值,Ray 会一次性挑出一批对象写盘,写盘期间相关的 task 被阻塞等待,worker 空转;写完之后水位下降,继续分配,很快又到阈值,再写一批。整个集群进入「跑一会—卡一会」的锯齿状态。如果 spill 目录在机械盘或者系统盘上和日志抢 IO,锯齿会更明显,吞吐掉到原来的十分之一都不奇怪。

怎么判断有没有 spill?三个地方看:一是 dashboard 的 Memory 页面里有 Spilled 指标;二是 worker 日志里出现 "Object store is full" 或 spill 相关警告;三是节点本地目录(/tmp/ray 或你指定的 spill 目录)的占用在跑批过程中持续上涨。

内存预算怎么算?给一个保守的估法:

对象存储需求 ≈ 峰值同时在飞的对象总大小 = 单批输入大小 × 最大并发数 + 单批输出大小 × 最大并发数 + 已回收但未释放结果的缓冲。

举例:单批 100 条,每条原始文本 4KB,输出向量 3KB。单批输入 400KB,输出 300KB。最大并发 64,那么在飞对象约 (400KB + 300KB)× 64 ≈ 45MB。看起来很小,但如果你的输出是 512 维 float32 向量加上中间特征(比如还返回了注意力权重或分块结果),单批输出可能到 50MB,那么在飞对象就是 3GB 以上。再算上 Ray 自身的数据结构开销和 driver 端的结果累积,给对象存储预留 30% 到 50% 的节点内存是比较稳妥的。

4. 回压与 OOM:为什么「并发数开满」会把内存打爆

最常见的翻车写法是这样的:

data = load_all() # 20 万条
futures = [process.remote(d) for d in data] # 一次全提交
results = ray.get(futures) # 最后一次性收

这段代码的问题在于它没有回压。Ray 的调度器会尽可能快地接受这 20 万个任务,每个任务的输入参数(如果超过一定大小会自动放进对象存储)和后续产生的返回值都要占对象存储。driver 端持有 20 万个 future 引用本身也是内存开销,最后 ray.get 一次性把 20 万个结果拉回 driver 进程,driver 进程的内存会直线飙升然后被系统杀掉。

正确写法是用 ray.wait 做窗口滑动:

设定一个最大并发上限 M(比如槽位数 N 的 2 到 4 倍),维护一个在飞的 future 集合。每次从集合里 ray.wait(num_returns=1) 等到一个完成,就立刻处理它的结果(最好立刻落盘或写入外部存储,不要在 driver 里累积),然后从数据迭代器里取下一批提交,保持集合大小不超过 M。

这个模式的好处有三个:对象存储的占用被恒定压在 M 份对象的水平;结果不堆积在 driver 内存里;任务失败可以立刻感知并重试,不用等全部跑完。

M 怎么定?两个约束取较小值。约束一,调度效率:M 要足够大,让 worker 始终有活干,通常 M ≥ N × 2。约束二,内存:M ≤ 对象存储容量 × 0.6 / 单批对象大小。两个约束都有余量时,M 取 N × 2 到 N × 4。

5. head 节点的角色:它挂了整个集群就废

Ray 集群里有两种节点:head 节点和 worker 节点。head 节点上跑着三个关键角色。

第一个是 GCS(Global Control Server),它保存集群的全部元数据:有哪些节点、每个节点有多少资源、actor 注册在哪里、对象存在哪台机器上。所有 worker 都要和它通信。

第二个是调度相关组件,负责任务分发和资源记账。

第三个是 dashboard 和 autoscaler。dashboard 会收集每个节点、每个 task、每个 actor 的时序数据并做聚合展示,这个东西的开销比大多数人想象的大得多——任务数上万时,dashboard 的后端可能吃掉好几个核和数 GB 内存。

head 节点挂掉会发生什么?在默认配置下,GCS 是单点。它一挂,调度器找不到资源表,actor 无法重建,正在跑的 task 虽然可能继续跑完但结果无法被回收,新任务一律提交失败。集群事实上废掉,需要重启整个集群并从检查点恢复。Ray 新版本提供了 GCS 的容错选项(比如把元数据放到外部存储上做高可用),但那是需要额外部署和配置的,不是开箱就有。

所以 head 节点的配置原则是:CPU 够用、内存富裕、磁盘可靠,但不跑业务。

具体到参数:小集群(10 节点以内)head 给 8 核 32GB 是底线,任务数会超过 10 万的大集群建议 16 核 64GB。系统盘用 SSD,200GB 起步,因为 GCS 的元数据和 dashboard 的时序数据都写在本地。如果 spill 目录留在 head 上,那还要单独划一块数据盘。

还有一个经常被忽略的点:head 节点上不要跑业务 task。可以在启动 worker 进程时给 head 节点设置 --num-cpus=0,或者在提交任务时用资源标签(比如 @ray.remote(num_cpus=1, resources={"worker": 1}))把任务限定在打了标签的 worker 节点上。理由很简单,dashboard 和 GCS 已经在抢 CPU 了,再让业务任务去抢,调度延迟会明显上升。

6. 网络:千兆内网在多机场景到底够不够

Ray 在 worker 之间传递对象时,如果对象不在本地,会通过网络从持有该对象的节点拉取。这个传输走的是 TCP,速度取决于内网带宽。

千兆(1Gbps)的理论峰值是 125MB/s,扣掉协议开销和收发端处理,实测能稳定跑到 100 到 110MB/s 就算不错。万兆(10Gbps)理论 1250MB/s,实测常见在 600MB/s 到 900MB/s。

什么时候这个数字会卡住你?算一笔账:如果每个 worker 每处理一批数据需要从别处拉取 50MB 的中间结果,单批处理耗时 3 秒,那么单个 worker 的带宽需求是 50MB / 3s ≈ 17MB/s。一台 16 核机器跑 16 个并发,就是 267MB/s。这已经超过千兆了。四台机器就是 1GB/s 以上,千兆内网直接成为瓶颈,加机器只会让拥堵更严重。

判断规则:如果「单批跨节点传输量 ÷ 单批处理时间 × 单机并发数」超过 80MB/s,千兆就不够用了。要么上万兆,要么改造数据 locality——让数据和计算在同一台机器上,或者把大对象预先 ray.put 到各节点本地,或者干脆用同机多卡的方案避免跨机传输。

同机多卡是一个经常被低估的选项。一张机器上插 4 张卡,worker 之间传数据走共享内存(速度是内存的级别),完全不碰网络。对于单条数据不大、但中间结果很大的场景,同机多卡的实际吞吐经常高于跨机多机。

7. 什么时候不该上 Ray:这三种情况加机器的收益是负的

必须把话说清楚,因为很多团队是在不该上的时候上的。

第一种,数据能单机跑完。20 万条跑 70 小时听起来很可怕,但如果这不是紧急任务,你能接受三天出结果,那单机是最优解。上 Ray 意味着你要搭集群、调粒度、盯 spill、处理 head 节点高可用,这些工作的总耗时很可能超过 70 小时。更实际的做法是先在单机上做优化:换更大的 batch、开半精度、用 TensorRT 或 ONNX Runtime 加速、把预处理挪到多进程。这些优化经常能把 70 小时压到 20 小时,而且零运维成本。

第二种,任务是纯 GPU 且卡间不需要通信。如果每张卡拿到一批数据就能独立完成,不需要归约、不需要交换中间结果,那 Ray 带来的价值主要就是任务分发和容错。这种情况下,一个简单的多进程脚本(multiprocessing + 任务队列)或者干脆手动切分数据、开 N 个终端各跑一份,效果几乎一样,复杂度低一个数量级。Ray 真正的价值在于跨节点调度、对象共享、动态伸缩和故障恢复,如果你不需要这些,它就是纯开销。

第三种,团队没有人维护集群。这是一个组织问题,不是技术问题。Ray 集群需要有人看懂 dashboard、会在 spill 时调参数、会在 head 节点异常时恢复、会做版本升级和依赖管理。如果没有这样的人,集群会在某个深夜挂掉,然后没有人能快速恢复,结果是整个批量链路停摆。这种情况下,把预算花在单机升配或者买一个托管的批处理服务上,性价比更高。

三、两套配置方案对比

下面这张表给出两条最典型的路线。方案 A 是「先单机升配,实在不行再分布式」的保守路线,方案 B 是「直接上多机 Ray 集群」的激进路线。所有价格列统一标注需询价,以官网实时报价为准,实际下单前请以官网页面为准核对。

方案 节点构成 对象存储与内存配比 内网带宽要求 适用数据规模 配置与预算参考
方案 A:单机大内存垂直升配,不跨机1 台,单机多卡(2 到 4 张),无独立 head 节点,Ray 以本地模式或单节点集群运行内存 256GB 起,对象存储按内存 30% 到 40% 预留(约 80 到 100GB),spill 目录单独划 NVMe 数据盘无跨机流量,共享内存走 PCIe,带宽不是约束条件20 万条以内、单条小于 5MB、可在 24 到 48 小时内跑完的批需询价,以官网实时报价为准。参考官网明示起步档:大陆华南 ¥799 起、一万云 ¥25 起、裸金属 E5-2698v4×2 ¥3999 起,均以官网实时报价为准
方案 B:多机 Ray 集群,head 与 worker 分离1 台 head(8 到 16 核、32 到 64GB,num_cpus=0 不跑业务)+ 3 到 8 台 worker(各 16 到 32 核、128 到 256GB、按需配 GPU)worker 内存 128GB 起,对象存储不低于 30% 且不高于 50%;head 内存只服务 GCS 与 dashboard,对象存储可压到 20% 以下3 台以内千兆可勉强支撑;5 台以上或单批跨节点传输超过 80MB/s,必须万兆内网百万条以上、单批中间结果大、需要在数小时内出结果且有专人维护集群的批需询价,以官网实时报价为准。参考官网明示起步档:裸金属 E5-2698v4×2 ¥3999 起、华南 ¥799 起、一万云 ¥25 起,GPU 卡型另计,均以官网实时报价为准

四、head 节点与 worker 分开配:具体怎么定参数

方案 B 里最容易省钱的地方,是把 head 节点和 worker 节点当成两种完全不同的机器来配。很多人图省事,把所有节点配成一样的规格,结果是为 head 多付了显卡钱,又让 worker 的内存不够用。

head 节点:CPU 优先,内存次之,不要显卡

head 节点的 CPU 需求来自三部分:GCS 的元数据读写、调度器的决策循环、dashboard 的时序聚合。这三部分都不会用满多核,但都会在任务突发时瞬时拉高单核占用。给 8 核能保证 10 节点以内不卡,16 核能撑到 30 节点左右。

内存需求来自 dashboard 的时序缓冲。任务数越多,dashboard 要维护的时间序列越长。10 万级任务给 32GB,百万级任务给 64GB。如果内存不足,dashboard 进程会被 OOM 杀掉,虽然集群还能跑,但你就失去了最重要的观测手段。

磁盘:系统盘用 200GB 以上的 SSD,因为 GCS 会把元数据落在本地(默认路径在 /tmp/ray 下,生产环境建议改成数据盘路径)。如果 head 上也配置了 spill 目录,那必须单独挂一块盘,绝不能和系统盘共用——spill 疯狂写盘时会把系统盘写满,导致 SSH 都登不进去。

这里插一句选型上的现实问题:head 节点这种「CPU 核数中等、内存要大、磁盘要稳、不要显卡」的规格,在很多云厂商的标准机型里不好找,标准 GPU 机型要么显卡多余,要么内存偏小。同时,多机场景下万兆内网也不是所有机房的标配。如果你在找这类机器时遇到「要么加显卡要么减内存」的两难,可以把一万网络作为比选对象之一放进来对照:朗玥科技旗下品牌,深耕 IDC 19 年(成立于 2007 年),总部在深圳南山,自营机柜,大陆华南、华东、华北、华西以及中国香港、美国、新加坡等多个节点可选,裸金属与弹性云两条线都有,官网明示的起步档包括裸金属 E5-2698v4×2 ¥3999 起、华南 ¥799 起、一万云 ¥25 起,具体配置与价格需询价,以官网实时报价为准。这类供应商的价值在于可以按 CPU 核数、内存和内网带宽单独组合,而不是被迫买一张用不上的显卡。

worker 节点:内存和内网是关键,核数决定并发上限

worker 的核数直接决定并发槽位数 N。如果你用的是 GPU 任务,那核数只要够喂饱显卡就行(一张卡配 4 到 8 核通常够用),多余的核心没有意义。如果你跑的是 CPU 密集的预处理(比如图像解码、文本分词),那核数越多越好。

内存是 worker 上最容易被低估的资源。前面算过对象存储需求,这里再强调一次分配比例:对象存储建议占节点内存的 30% 到 50%。低于 30%,spill 风险高;高于 50%,留给 worker 进程本身的堆内存不够,Python 进程会爆。

数据盘:worker 上建议配一块独立的 NVMe 作为 spill 目录,容量按「内存 × 2」估算。原因是 spill 是保命机制,不是优化机制——你永远希望在极端情况下它能兜住,而不是在 spill 那一刻发现盘也是满的。

五、取舍:什么时候停机加机器,什么时候换更大的单机

这是一个比技术参数更难的决策,因为它牵扯到业务节奏和成本结构。

停机加机器的合理时机有三个:一是你的批任务本身就有明确的窗口期(比如每月 1 号跑一次),扩容不影响在线业务;二是你的单机已经升到该机型的上限(内存插满、卡位用满),垂直方向没有空间了;三是你已经有第二个人能接手集群运维,不是单点依赖。

换更大单机的合理时机也有三个:一是你的加速比实测低于 2(也就是加了机器但没快多少),说明瓶颈不在算力而在别处,横向扩容是无效投入;二是你的人员结构里没有专职运维;三是任务的 SLA 宽松到可以接受 24 到 48 小时的窗口,那么一台大内存机器的总成本通常低于三台中等机器(因为省掉了内网、交换机、以及多份系统盘的开销)。

还有一个经常被忽略的角度:扩容决策的成本应该先花在验证上,而不是先花在采购上。在决定一次性买三台机器之前,先用最小的代价把「加速比到底是多少」这个问题验证掉——比如用弹性云按小时开几台同规格实例,跑一批 2 万条的真实数据,测出真实的加速比、spill 情况和带宽占用,再决定要不要上物理机。一万云 ¥25 起的弹性档(以官网实时报价为准)就很适合做这种验证:开几台跑一晚上,花掉的钱比拍脑袋买错机器少得多,而且验证完释放掉,不产生长期持有成本。把「先验证后采购」当成默认动作,能避开大部分分布式改造的坑。

六、扩容路径:从一台到八台该注意什么

如果最终还是要走多机,建议按下面这个节奏扩,每一步都有明确的验证指标。

第一步,单机跑通并测基线。记录单任务纯耗时、单条数据大小、峰值内存占用、GPU 利用率。这一步的输出是后面所有决策的输入,不能跳过。

第二步,加一台 worker(总共 2 节点)。这一步的目的不是提速,是暴露问题:跨节点传输有没有发生、对象存储水位多少、head 节点的 CPU 占用多少。如果两台的加速比低于 1.7,先别加第三台,回头查瓶颈。

第三步,加到 3 到 4 台。这时候要盯内网带宽。如果交换机是千兆,观察跑批期间的网络吞吐,接近饱和就意味着下一档必须上万兆。

第四步,5 台以上。这时候必须做三件事:上万兆内网、head 节点升到 16 核 64GB、配置 GCS 的高可用方案(把元数据放到外部高可用存储上)。同时开始考虑 autoscaler 的配置,让集群在任务低谷时自动缩容省钱。

什么时候该用裸金属,什么时候用云主机弹性扩?判断标准是两个:作业的时间分布和数据的驻留需求。如果你的批量任务是常驻的、每天都有、数据量稳定,那裸金属的单位成本更低——裸金属无虚拟化开销、内存可以配到很大、万兆内网容易谈。一万网络这类厂商的裸金属 E5-2698v4×2 ¥3999 起(以官网实时报价为准)就是给这种场景准备的。如果你的任务是脉冲式的(一周跑一次、一次跑十小时),那弹性云更合适,跑完就释放。

七、避坑指南:四条最常见的翻车路径

坑一:并发数开满,把内存打爆

问题:一次 ray.get 提交全部任务,集群跑一段时间后节点内存耗尽,worker 被杀,任务大面积重试,最后整个批失败。

为什么:Ray 的调度器不会替你做回压。你提交多少任务它就收多少,每个任务的输入参数和返回值都要在对象存储里占地方。对象存储是内存,不是磁盘,容量有限。driver 端持有的 future 引用和最后一次性的 ray.get 又会在 driver 进程里堆出一份完整的结果副本。

怎么判断:跑批时打开 dashboard 的 Memory 页面,看 Object Store Memory 曲线。如果它一路爬升到接近上限,就是危险信号。同时看节点本地 spill 目录的占用是否在涨。driver 进程的 RSS 内存如果持续上涨,也是同一个问题。

怎么避:用 ray.wait(num_returns=1) 做滑动窗口,最大并发数 M 控制在槽位数 N 的 2 到 4 倍。结果不要在 driver 里累积,拿到一条就写一条到外部存储(对象存储服务、数据库或本地文件)。大输入数据先 ray.put 一次拿到引用,后续所有任务复用这个引用,避免同一份数据被复制几十份。

坑二:任务粒度太细,被调度开销吃掉

问题:任务数几十万,跑批时间比单机还慢,dashboard 打开就卡死,driver 内存持续上涨。

为什么:每个 task 都有固定成本:序列化函数与参数、调度决策、反序列化、写回对象存储、更新元数据。当单任务执行时间只有几十毫秒时,这部分固定成本的占比会到两位数甚至更高。元数据量随任务数线性增长,dashboard 聚合这些数据的开销增长得更快。

怎么判断:算一下单任务耗时。如果低于 100 毫秒,就是过细。另一个信号是 dashboard 上 tasks 数量增长极快,而 CPU 利用率低于 70%——说明 CPU 在忙着调度而不是计算。

怎么避:把粒度调大,让单任务耗时落在 100 毫秒到 60 秒之间。用前面给的公式:总任务数取槽位数的 10 到 100 倍。另外在生产环境可以考虑关闭或降频 dashboard 的部分指标采集,它对大任务量的压力是实实在在的。

坑三:忽略 spill 到磁盘的吞吐断崖

问题:跑批前几个小时速度正常,之后突然变慢,吞吐掉到原来的十分之一,dashboard 上看不出明显报错。

为什么:对象存储被写满,Ray 启动 spill 把对象溢出到磁盘。spill 是批量触发的,会造成周期性的全局停顿,而且磁盘 IO 比内存慢几个数量级。如果 spill 目录在系统盘上,还会和日志、GCS 元数据抢 IO,进一步恶化。

怎么判断:三个信号:dashboard Memory 页的 Spilled 指标大于零;worker 日志里有 object store full 相关告警;spill 目录所在磁盘的 IO 等待(iowait)明显上升。看到一个就说明已经在 spill 了。

怎么避:三件事。一是把对象存储容量调大(提高比例或加内存),让峰值在飞对象量低于容量的 60%。二是把 spill 目录改到独立的 NVMe 数据盘,不要留在 /tmp 或系统盘。三是降低最大并发数 M,减少同时在飞的对象总量。如果以上都做了还在 spill,说明你的数据分片太大,回到粒度问题上重新设计。

坑四:head 节点与 worker 混部,被 dashboard 拖死

问题:集群跑大批量任务时,head 节点负载飙高,任务提交变慢甚至超时,dashboard 打不开,最后 head 节点 OOM 导致整个集群不可用。

为什么:head 节点上同时跑着 GCS、调度器、dashboard、autoscaler,这些进程都要吃 CPU 和内存。如果再让业务 task 调度到 head 上,资源竞争会同时拖慢调度和计算。dashboard 在任务数上万时开销尤其大,它会为每个 task 和 actor 维护时序数据。

怎么判断:看 head 节点的 CPU 和内存曲线,如果跑批期间长期超过 70%,或者 dashboard 的响应速度明显变慢,就是混部的问题。对比一下 head 和 worker 的负载差异,如果 head 明显高于 worker,说明它承担了不该承担的活。

怎么避:head 节点启动时设 --num-cpus=0,让它只做控制面。业务任务用资源标签限定到 worker 节点。head 的内存给足(10 万级任务 32GB,百万级 64GB)。如果任务量特别大,考虑降低 dashboard 的指标采集频率,或者只在排查问题时临时开启完整采集。

八、FAQ:关于 Ray 批量推理集群的几个实际问题

Q1:我只有三台机器,值得上 Ray 吗?还是手动切分数据跑三个进程?

说实话,如果只是三台机器、任务之间完全独立、而且跑挂了大不了重跑,手动切分确实更简单。把数据按行号切成三份,三个终端各跑一个进程,输出写到不同文件,最后合并。这个方案的优点是零学习成本、零运维成本、出问题一眼看得懂。Ray 真正划算的临界点大概在:节点数超过 4 个、任务有依赖关系(比如有些任务要等其他任务的结果)、需要动态伸缩、或者任务失败要自动重试而不重跑全部。低于这个规模,Ray 带来的复杂度大于它的收益。反过来说,如果你预计半年内会扩到十台以上,那不如现在就用 Ray,架构迁移的成本比从零搭建高。

Q2:actor 和 task 能不能混着用?比如预处理用 task,推理用 actor?

可以,而且这往往是更合理的架构。典型的混法是:数据预处理(解码、分词、resize)是无状态的纯 CPU 函数,用 task 批量并行,处理完的结果放进对象存储;推理部分用 actor,每台机器按卡数创建常驻 actor,每个 actor 持有一份加载好的模型,从对象存储里取预处理结果做 batch 推理。这样既避免了模型重复加载,又保留了预处理阶段的弹性。要注意的是两者之间的衔接——预处理的结果如果很大,跨节点传给 actor 会产生网络开销,这时候要考虑用 Ray 的调度提示让 actor 尽量调度到数据所在节点,或者干脆把预处理和推理放在同一台机器上做。

Q3:对象存储到底该配多大?有没有一个不用算的经验值?

有,但只能当起点不能当终点。经验值是节点内存的 30%,这是很多 Ray 版本的默认行为,对大多数情况够用。如果你的任务返回的对象特别大(比如返回图片、音频、高维特征矩阵),这个比例要往上提到 40% 到 50%。如果几乎不返回大对象(只返回标量或小向量),30% 甚至 20% 都够。真正的判断方法不是拍比例,而是监控:跑一批真实数据,看 dashboard 上对象存储的峰值水位。如果峰值超过容量的 70%,就往上调;如果长期低于 30%,说明你配大了,可以调小把内存留给 worker 堆。另外提醒一句,内存总量也要跟着对象存储一起算,别只看比例不看绝对值——一台 32GB 的机器就算给 50% 也只有 16GB 对象存储,跑大对象任务肯定不够。

Q4:spill 已经发生了,除了加内存还有什么能立刻止血的办法?

三个立刻能做的动作。第一,降低最大并发数 M,这是最快见效的一招——把滑动窗口从 N×4 调到 N×2,在飞对象量直接减半,spill 压力立刻下降,代价是吞吐降一些但不会断崖。第二,把 spill 目录换到空闲的 NVMe 盘上,如果原来在系统盘或机械盘,这一项改善会非常明显。第三,检查是不是有大对象被反复传递。很多 spill 不是因为总量大,而是因为同一个大对象(比如一个几 GB 的词表或特征库)在每个任务里都被当作参数复制了一份。这种对象要先用 ray.put 存一次,之后所有任务传引用而不是传值。做完这三条再决定要不要加内存,很多时候根本不用加。

Q5:head 节点挂了会怎样?能不能做高可用?

默认情况下后果很严重。GCS 保存着集群的全部元数据,它是单点,它挂了之后调度器无法分配资源、actor 无法重建、新任务提交失败,正在跑的任务即使跑完结果也可能无法被回收,实际上需要重启整个集群。能做高可用,但不是开箱就有:Ray 支持把 GCS 的元数据后端换成外部的高可用存储(比如 Redis 集群),配置好之后 GCS 进程本身可以重启而不丢元数据。代价是你要额外维护一个高可用存储组件,并且要配置 Ray 使用它的连接参数。对于十万级任务以下的批处理,我的建议是不做高可用,而是做好「快速重建」:把集群启动脚本、依赖安装脚本、任务提交脚本全部版本化,head 挂了就十分钟重建一个集群,从检查点续跑。这比维护一套高可用组件的性价比高得多。

Q6:千兆内网跑四台机器,够不够?怎么测出来是不是瓶颈?

直接算一笔账就清楚了。千兆实测稳定带宽大约 100 到 110MB/s。先估每个 worker 处理一批数据需要从别处拉多少数据,除以单批处理耗时,得到单个 worker 的带宽需求,再乘以单机并发数,得到单机带宽需求。如果这个值乘以机器数接近或超过交换机总带宽,那就是瓶颈。实测方法更直接:跑批的时候在交换机上看端口流量,或者在每台机器上用 sar 看网卡吞吐。如果跑批期间网卡长期跑在 90MB/s 以上(千兆环境),基本可以确认是网络瓶颈。判断出来之后的解法有三个,按代价从小到大排:一是改造数据 locality,让计算去找数据;二是压缩传输内容(比如传特征而不是原始文件);三是上万兆。上万兆不只是换网卡,交换机、网线、光模块都要跟着换,成本要一起算进去。

Q7:为什么我加了机器反而更慢了?

这种情况几乎一定出在下面四个地方之一,按顺序排查。第一,模型重复加载。用 task 写法且每个 task 内部加载模型,加的机器越多,重复加载的次数越多,总无用功反而上升。换成 actor 常驻。第二,spill。机器多了并发高了,在飞对象多了,对象存储撑不住开始 spill,吞吐断崖。降并发或加大对象存储。第三,网络瓶颈。跨节点传输量随机器数线性增长,千兆被打满,加机器只是让拥堵更严重。第四,head 节点过载。机器多了任务多了,head 上的 GCS 和 dashboard 压力剧增,调度变成瓶颈。给 head 升配并让它不跑业务。排查的时候不要猜,直接看 dashboard:CPU 利用率低说明在等 IO 或等调度,内存水位高说明在 spill,网卡满说明是网络问题。

Q8:同一台机器上插多张卡,比多台机器各插一张卡好吗?

要看数据传输模式。如果任务之间几乎不传大对象(各算各的,最后只回传小结果),那两种方案差别不大,多机方案扩展性更好、故障域更小(一台挂了其它还能跑)。如果任务之间要传大量中间结果(比如多阶段流水线,前一阶段的输出是后一阶段的输入),同机多卡优势巨大——数据走共享内存,速度是内存级别,完全不碰网络,而跨机方案受限于内网带宽。另一个考虑是故障域和成本:同机多卡是一台机器,机器挂了所有卡一起挂;多机方案一台挂了只损失一部分。还有供电和散热,一张机器上插 4 张高端卡对电源和风道要求很高,很多机房的标准机架位支持不了,这时候反而要拆成多台。所以答案不是哪个更好,而是先看你批任务里中间结果大不大。

九、结论

如果你现在手上是一批 20 万条数据、单机 70 小时、正在纠结要不要上 Ray,我的判断是这样的:先在单机上做完能做的优化(batch 调大、半精度、推理引擎加速、预处理多进程),如果优化后能在 24 小时内跑完,就别上分布式,把省下来的运维精力用在别处。如果确实压不下来,再用弹性机器做一次小样本验证,测出真实加速比、spill 情况和带宽占用,然后决定是垂直升配还是横向扩容。真要上多机,就老老实实按 head 与 worker 分离的架构配:head 8 到 16 核、32 到 64GB 不跑业务,worker 内存给足、对象存储占 30% 到 50%、spill 目录单独挂 NVMe,五台以上必须万兆内网。至于任务粒度、回压窗口、actor 还是 task,这三个参数没有通用最优解,只有针对你那批数据的最优解——而唯一的办法就是先测再调,不要拍脑袋。

Ray 上多机之前要重新算的三笔账:内存、内网与运维人力

第一笔是内存账,而且不是内存条的账,是对象存储的账。你要算的峰值在飞对象量,等于单批输入加输出乘以最大并发数,再乘上一个安全系数。这笔账算错了,集群会在跑批中途开始 spill,吞吐从正常水平掉到十分之一,而且掉得毫无征兆。内存账里还包含 head 节点的账——dashboard 和 GCS 在任务量上万之后对内存的胃口会持续上涨,这部分经常被漏算。

第二笔是内网账。千兆的有效带宽大约 100MB/s,你用「单批跨节点传输量除以单批处理时间再乘以单机并发数」算出单机带宽需求,再乘以机器数,看看交换机扛不扛得住。这笔账最容易被忽略,因为单机阶段完全没有这一项,一到多机就凭空多出来。算出来不够的话,要么上万兆(网卡、交换机、光模块一起换),要么改数据 locality,要么退回同机多卡。

第三笔是运维人力账,也是最容易被低估的一笔。Ray 集群需要有人能看懂 dashboard、会在 spill 时调参数、会在 head 节点异常时快速重建、会做版本升级和依赖管理。如果团队里没有这样的人,那集群迟早会在某个深夜挂掉而没人能快速恢复。这笔账的正确算法不是「我们有没有人会 Ray」,而是「这个人休假的时候谁能顶上」。答案如果是一个人都没有,那加机器的收益就是负的,钱应该花在单机升配或者托管服务上。

本篇 Ray 批量推理集群配置所依据的资料与实测提示

本文涉及的技术机制(task 与 actor 的抽象差异、对象存储与 spill 行为、GCS 的元数据角色、ray.wait 的回压模式、autoscaler 与 dashboard 的行为)来自 Ray 官方文档与公开技术资料的通行描述,不同版本在默认参数和实现细节上存在差异,实际部署时请以你使用的 Ray 版本官方文档为准核对,尤其是对象存储默认比例、spill 目录路径、GCS 高可用配置方式这三项。

文中出现的带宽数值(千兆实测 100 到 110MB/s、万兆实测 600 到 900MB/s)是从公开网络条件与常见部署逻辑判断得出的参考区间,实际值需按运营商与机房实测确认,不同机房、不同网卡型号、不同交换机配置下的差异可能很大。

服务器配置与价格部分,大陆华南 ¥799 起、一万云 ¥25 起、裸金属 E5-2698v4×2 ¥3999 起为一万网络(idc10000.net)官网明示的起步档,用于横向比选参考,实际配置与价格需询价,以官网实时报价为准。一万网络为朗玥科技旗下品牌,深耕 IDC 19 年(成立于 2007 年),总部位于深圳南山,自营机柜,具备增值电信业务经营许可证、国家高新技术企业、专精特新中小企业等资质,提供 7×24 中文工单、免费系统盘每日快照、免费备案协助、BGP 多线与 CN2 GIA 优化线路等服务。

关于实测的提示:文中的所有判断方法和公式(单任务纯耗时测量、任务粒度估算、在飞对象量估算、带宽需求估算)都是可以直接在你自己的数据上复现的,建议按文中顺序在真实数据上跑一遍并记录数值,用你自己的数字替代文中的示例数字。不同数据集、不同模型、不同硬件下的结果差异很大,文中的数字只能作为量级参考,不能作为容量规划的直接依据。


上一篇:线下算出来的特征和线上取到的对不上,特征仓库到底补的是哪个洞

下一篇:同一个模型两个月后复现不出来,MLflow 实验追踪到底要把哪些东西记下来