Anyscale、AI ワークロード向け Ray の 1 万ノードクラスターへのスケーリングを報告
本文の状態
日本語全文を表示中
詳細モードで約28分の本文を読めます。
同じ出来事の情報源
この情報源を基点に整理
Anyscale Engineering
Anyscale Engineering は、Ray の大規模 AI ワークロード向けにスケーラビリティを大幅に向上させ、10,000 ノードクラスターでの起動時間を劇的に短縮し、40,000 アクターの運用を可能にしたと発表した。
AI深層分析を開く2026年8月26日 02:02
AI深層分析
キーポイント
大規模クラスターでのパフォーマンス劇的改善
Anyscale Engineering は、10,000 ノード環境における配置グループ準備時間を 303 倍、2,000 ノード環境でのアクター起動を 6.5 倍高速化し、1 年前には不可能だった大規模運用を実現した。
ボトルネックの特定と技術的解決
スレッド間のロック競合、非同期作業を処理する単一スレッドの過負荷、ステールなリソースビューに基づくスケジューリングという 3 つの主要なボトルネックを特定し、Ray Core の改善で解消した。
バッチ推論とデータパイプラインの高速化
500 ノード環境でのバッチ推論エンドツーエンド処理が 23% 高速化され、Ray Data のシャッフル処理も 24% 向上し、ドライバーがボトルネックとなる問題を解決した。
大規模 RL と学習ワークロードへの対応
2,000 から 10,000 ノードのクラスターで実行される大規模強化学習や前処理・後処理トレーニングにおいて、ネットワークトポロジ制約下での起動時間と障害発生後の再起動時間が最適化された。
ドライバのボトルネック発生原因
Ray Dataのバッチ推論やシャッフルパイプラインで、ray.waitなどのコアAPI自体は高速にもかかわらず、スケジューリングループが低下しスループットとエンドツーエンド時間が悪化する問題が発生した。
重要な引用
Scales to 40,000 actors on a 10,000 node cluster, a scale it could not support a year ago.
303× faster to get placement groups ready at 10,000 nodes.
The bottlenecks we found kept falling into three kinds: Lock contention between threads. A single overwhelmed thread handling async work. Scheduling on stale resource views.
Batch inference and shuffle pipelines were losing time inside Ray Core calls, and their scheduling loop slowed down with them, hurting their sustained task throughput and end to end time.
編集コメントを表示
編集コメント
Anyscale Engineering は、Ray の大規模化における根本的なボトルネックを特定し、実証データに基づいた具体的な改善を示した。この成果は、数千ノード規模の AI ワークロードを実用化する上で重要なマイルストーンとなる。
Source Article
元記事を日本語で読む
本文に関係しない購読案内、埋め込み通知、サイト内プロモーションは除いています。
本稿の作成に貢献し、ベンチマークやレビューを提供いただいた以下の皆様に感謝いたします。Edward Oakes 氏、Mengjin 氏、Kartica Modi 氏、Dhyey Shah 氏、Josh Lee 氏、Zac Policzer 氏、Steve Alexander 氏、Andrew Sy Kim 氏(Google)、Mao Yancan 氏(ByteDance)。
Ray がますます多くの新興 AI ワークロードで利用されるようになり、ユーザーが Ray Core を二つの方向から限界まで押し上げていることを観察しました。
- バッチ推論、シャッフル、その他のデータパイプライン: ドライバーが数百万個のオブジェクトのライフサイクルを管理しながら、クリティカルパス上でスケジューリングループを実行するワークロードです。これにより、持続的なタスクスループットに上限が生じます。
- 大規模な強化学習(RL)および事前/事後トレーニング: これらは 2,000 ノードから 10,000 ノード規模のクラスター上で実行され、ネットワークトポロジーの制約下で数万人のアクターがスケジューリングされます。これらのユーザーにとって、起動時間や障害発生後の再起動時間は極めて重要です。
私たちが特定したボトルネックは、主に以下の 3 つに分類されました。
- スレッド間のロック競合
- 非同期作業を処理する単一の過負荷スレッド
- stale なリソースビューに基づくスケジューリング
Ray Core の改善の一連の取り組みを経て、昨年のこれらのワークロードと比較した現状は以下の通りです。
500 ノードのバッチ推論においてエンドツーエンドのパフォーマンスが23%向上。
Ray Data のシャッフル処理でも、同様に24%高速化を実現しました。
トポロジー制約のある大規模なトレーニングクラスターで、より多くのアクターを起動可能に。特に配置グループの準備時間が劇的に短縮され、2,000 ノードでは62 倍、10,000 ノードでは驚異的な303 倍の高速化を達成しました。
また、2,000 ノードクラスターにおけるアクターの起動自体も6.5 倍速くなり、10,000 ノードのクラスターで最大 40,000 のアクターを処理可能になりました。これは、一年前には不可能だったスケールです。
本稿では、これらのボトルネックをどのように特定し、解決したのかを順を追って解説します。まずはドライバー内部のデータパイプラインから始め、その後 10,000 ノード規模のトレーニングクラスターへと視点を移しますが、前述の 3 つのボトルネックは両方のストーリーにおいて共通して現れる課題です。
ドライバーがボトルネックとなる時
この問題のきっかけは Ray Data からでした。バッチ推論とシャッフルパイプラインで、Ray Core の呼び出し処理に時間がかかりすぎていることが判明しました。その結果、スケジューリングループ全体が遅延し、持続的なタスクスループットやエンドツーエンドの処理時間が悪化していました。
しかし、ray.wait やその他の Ray Core API 単体で測定すると、それらは高速に動作しているのです。では、いったいどこで時間をロスしていたのでしょうか?
その答えを知るために、ある一つのオブジェクトがたどるライフサイクルを追ってみましょう。数百万ものオブジェクトが同じ旅路を歩んでいます。

図 1: ある一つのオブジェクトのライフサイクル
図 1: オブジェクトのライフサイクル
図 1 に示すように、この話に関わる Ray ドライバープロセスには 2 つのスレッドが存在します。Python スレッドでは ray.wait や独自のスケジューリングロジックなど、ユーザーが記述したコードを実行します。一方、Ray Core のバックグラウンドスレッドである C スレッドは、すべての非同期処理を担当します。
図 1 の Python スレッド部分を見てみましょう。タスクを提出するには f.remote() を呼び出し、オブジェクト参照(ref)を受け取ります。その後、オブジェクトが準備されるまで待機します(実際の Ray Data のスケジューリングループではタイムアウトを設定し、継続的にチェックを行います)。そして、この ref を下流の他のタスクに渡します。最後に、オブジェクトの使用が完了して不要になった時点で、ref を破棄します。
裏側で何が起こっているかを見てみましょう。ray.wait(Ray Core の API)を呼び出すと、そのスレッドはオブジェクトの準備状態を待つために一時停止されます。タスクはワーカーノード A で実行され、出力オブジェクトがそこに保存されます。理想的な待機時間は、まさにタスクの実行時間そのものであるべきです。タスク完了後に通知を受け取った C スレッドは、オブジェクトの場所を記録するなどの事務処理を行った後、Python スレッドを起動します。
次に、この ref を下流のタスク g に渡します。タスク g はワーカーノード B でスケジューリングされますが、そのためにはオブジェクトの場所を知る必要があります。そこで、オブジェクトの場所を購読します。C スレッドはすでにその場所を記録しており、今度はそれを公開して返します。ノード B はノード A からオブジェクトを取得し、タスク g が実行されます。 (原文の技術表記: ray.wait,)
裏側でオブジェクトの参照カウントがゼロに減ると、クリーンアップ処理がトリガーされます。これには、そのオブジェクトの場所を待っていた他のプロセスに対して「失敗した」というメッセージを送信して待ちを解除し、クラスタ内のすべてのコピーを解放する作業も含まれます。
上記のような背景を踏まえて、ドライバーで見つかったボトルネックとその解決策を見ていきましょう。
2 つのスレッド、1 つのロック

図 2: 同じパブリックロックを巡って争う 2 つのスレッド
*Figure 2: Two threads fighting over the same publish lock*
上記のオブジェクトライフサイクルからわかるように、両方のスレッドがオブジェクトの更新情報を公開しています。これらすべての公開操作を保護する「パブリックロック」が存在します。
図 2 に示す通り、最後の参照カウントを解放する際、Python スレッドはロックを取得する必要があります。同時に、C スレッドも同じロックを必要としており、オブジェクトの場所情報を公開しようとしています。
2 つのスレッドが 1 つのロックを共有しているため、スケールした環境で深刻な競合が発生します。バッチ推論の性質上、オブジェクトは毎秒プルされるため、C スレッドはオブジェクト位置の公開のために publish ロックを常に保持し続けます。同時に、使用済みオブジェクトが次々と破棄される際にも、Python スレッドは失敗メッセージの公開のために同じ publish ロックを取得する必要があり、これがボトルネックとなります。
500 ノード規模のバッチ推論ワークロードでは、publish ロックの競合だけでスケジューリングループ全体の 17.4% の時間を消費していました。
このようなロック競合を解消するには、関連するすべての作業を単一のスレッドに集約する必要があります。公開処理(失敗メッセージを含む)をすべて C スレッドへ移した結果、Python スレッドが publish ロックを取得する必要がなくなり、プロファイル上でロック競合が完全に消滅しました。
LinkOne のスレッドオーバーフロー

Figure 3: C スレッドがオーバーフローした際の追加のブロック時間
*Figure 3: C スレッドがオーバーフローした際の追加のブロック時間*
そして、オブジェクトライフサイクルの中間に隠された 2 つ目のボトルネックが ray.wait そのものです。図 3 に示す通り、ray.wait で費やす時間は、オブジェクトが生成される速度だけでなく、C スレッドの混雑状況にも左右されます。待機時間の青色部分はタスク実行に要する時間であり、赤色部分が追加的なブロック時間です。
オブジェクトが準備されても、C スレッドは他の多数のオブジェクトの位置更新処理などを行っており、それらの処理が C スレッドの操作キューでウェイクアップ処理よりも先に並んでいる可能性があります。このキュー待ち時間が遅延の主要な要因であることが判明しました。そこで原因を追求したところ、2 つの問題が見つかりました。
1 つ目の問題は、C スレッドへの微小なコールバックの洪水です。個々のコールバックは軽量ですが、数百万回も投稿すると、その投稿自体がパフォーマンスを損ないます。あるシャッフル実行では、コールバック数が 100 万を超え、C スレッドの処理時間の約 17% を占めました。各コールバックは待ち時間 5.7 秒後に 0.04 ms で実行されていました。
これらのコールバックの大半が引数の確認処理でした。タスクを提出する前に、クラスター内のどこかで各引数が生成されたことを確認する必要があり、引数ごとに 1 つずつコールバックが発生します。シャッフルの reduce タスクは膨大な数の引数を扱いますが、reduce はすべての map タスクが完了した後に開始されるため、それらの引数はすでに生成済みのはずです。
問題点は、タスク作成時に引数がすでに準備されていても、潜在的なデッドロックを避けるためにドライバがコールバックを投稿していたことです。今回の修正ではすべてのロック処理を安全にし、タスク作成時点で引数がまだ準備されていない場合のみ非同期チェックを投稿するようにしました。これだけで、シャッフル処理の全体速度が 9.2% 向上しました。
2 つ目の問題はプロファイルから即座に浮かび上がりました。C スレッドの時間の約 3 分の 2 が、他のノードへオブジェクト位置情報を公開する作業に費やされていました。調査の結果、各ノード上のすべてのタスクが自身の引数の位置情報を購読していることが判明しました。しかし、オブジェクトがすでにローカルノード上に存在する場合、タスクがその位置情報を購読する必要はありません。Ray はデータを近くに配置してタスクをスケジューリングするため、同じノード上にオブジェクトが存在する可能性は非常に高いのです。この改善により、ローカルにあるオブジェクトへの要求をスキップし、ドライバの公開トラフィックを半分まで削減できました。
これら以外にも多数の修正が行われましたが、これらの取り組みによって持続的なタスクスループットは最大 1.6 倍に向上しました。その結果、500 ノード環境におけるバッチ推論パイプラインの全体処理時間が 23% 短縮され、シャッフル処理も 24% 高速化されました。
長期的には、Ray Core のさらなる高速化を目指し、C スレッドを複数のスレッドに分割する取り組みを進めています。
大規模トレーニング実行のための LinkScaling
上記の Ray Data ワークロードに加え、多くのユーザーから「アクターの起動時間と再起動時間が、大規模な強化学習(RL)や事前学習・事後学習ワークロードのボトルネックになっている」という声をいただいています。GPU のアイドル時間は莫大なコストの浪費であり、ユーザーはそのコストを何度も支払っています。一度は起動時、もう一度は故障時の再起動時です。NCCL コミュニケーターが「すべてかゼロか」の挙動を示すため、1 ノードでも死滅すれば、すべてのアクターを再スタートさせる必要があるからです。
また、データ転送速度がトレーニング実行のパフォーマンスに大きく影響するため、ネットワークトポロジーの制約にも関心を持っています。これを表現する標準的な方法は、Ray のトポロジ対応配置グループスケジューリングです。例えば、NVIDIA GB200 や GB300 の NVL72 ラックを使用する場合、一般的な構成は「1 ラック(18 ノード)ごとに 1 つの配置グループ」を設定し、「各ノードに 1 つのバンドル」、「各バンドル内に 4 つのアクター」とします。これはラック内の GPU にそれぞれ 1 つずつ対応させるものであり、18×4=72 で NVL72 の総 GPU 数と一致します。
したがって、現在の規模および将来の規模で RL や事前学習・事後学習ワークロードをサポートするためには、Ray はまず 10,000 ノードレベルへのスケーリングを実現し、その上で配置グループスケジューリングとアクタースケジューリングを十分に高速化する必要があります。
10,000 ノード規模での GCS の安定維持
GCS(Global Control Service)は Ray のグローバル制御サービスであり、制御プレーンの中心に位置しています。以下のような重要な役割を担っています。
- ノード管理
- アクターのライフサイクル管理
- 内部キーバリューストアの提供
- オートスケーラーへの需要収集
- 配置グループスケジューリング
これまで、これらの処理はすべて単一のメインスレッドで共有されており、ノード数が増えるにつれて負荷も増大していました。これが再びボトルネックとなり、スレッドが過負荷状態に陥る問題が発生しました。GCS(Global Control Store)のメインスレッドは、ユーザーワークロードが到着する前からすでに繁忙を極めていました。そのため、アクターや配置グループのワークロードが到来すると、すぐに GCS がボトルネックとなり、容易に機能不全に陥っていたのです。
そこで私たちは GCS をマルチスレッド化し、クラスター規模に合わせて拡張可能にし、アクターの負荷も処理できるようにしました。ノードステータスの更新ブロードキャスト、内部キーバリューストアのサービス提供、自動スケールラーのための保留中の需要収集といった処理をそれぞれ独立したスレッドに移行させたのです。
また、GCS に残る負荷も軽減しました。ノード変更の購読は O(workers) から O(nodes) へ改善され、配置グループのバンドル情報はアクター提出時に GCS から照会するのではなくキャッシュされるようになりました。さらに、タスクイベントを GCS から完全に外し、直接ダッシュボードへ流れるようにしたことで、GCS の負担を大幅に減らしています。
現在のアイドル状態での負荷は以下の通りです。昨年の Ray (2.51) では、ジョブを 0 件実行している 10,000 ノードのクラスターでも GCS メインスレッドの稼働率は約 61% に達していました。しかし、現在の nightly ビルドでは、同じアイドル状態のクラスターで稼働率は約 38% に低下しています。
その結果、GCS は 10,000 ノード規模でも安定して動作するようになりました。ただし、これだけでスケジューリングが自動的に高速化されるわけではありません。それは別の課題として残されています。
LinkRay の分散スケジューリング
Ray のスケジューリングは分散型です。すべてのノードがスケジューリングを実行できるため、各ノードはクラスター全体のリソース状況を把握する必要があります。
図 4 に示すように、各ノードは独自のローカルなリソースビューを保持しており、これが唯一の信頼できる情報源(ソース・オブ・トゥルース)となります。何らかの変更が発生すると、そのノードは自らのリソースビューのスナップショットを取得し、それを GCS(Global Control Store)へ送信します。
GCS はすべての情報を収集した後、変更内容を各ノードに配信(ファンアウト)します。これにより、すべてのノードが他者のリソースビューも保持する状態になります。Ray 内部では、このデータ配送サービスは「シンカー(syncer)」と呼ばれています。

図 4: リソースビューの移動経路
では、簡単なタスクの提出プロセスを通じて、Ray のスケジューリングがどのように行われるかを見ていきましょう。
まず、提出者はリクエストをあるノードに送信します。原則として、これは任意のノードで構いません。実際には、通常は提出者が接続しているノードか、あるいはタスクに必要なデータの大部分を保持しているノードになります。ここではそのノードを「ノード A」と呼びましょう。
ノード A はまず、このタスクをローカルで実行できるかどうかを確認します。もしローカルでの実行が不可能な場合、クラスタ内のリソース状況を基に適切なノードを選択し、今回は「ノード B」を選定します。その後、ノード A はそのリソース見積もりからタスクに必要なリソースを見積もって差し引き(楽観的な減算)を行い、この決定を提出者に返却します。
提出者はその後、このリクエストをノード B に送信し、同様のプロセスが繰り返されます。今回はノード B が受け入れ、実際にリソースを割り当ててタスクを実行します。

Figure 5: How Ray schedules a task
*Figure 5: How Ray schedules a task*
この設計が何に依存しているか、注意深く見てみましょう。ノード A は自らのリソース見積もりに基づいて判断を下しますが、その見積もりは古くなっている可能性があります。そのため、判断が誤るケースも生じます。
そのような場合、提出者はリクエストをノード B に送りますが、ノード B がこれを拒否します。すると再スケジューリングが行われます。私たちは最新の資源ビューを届けるシンク(同期)機能に依存し、ノード A の楽観的な見積もりの誤りを修正させることにも同様に依存しています。
この「依存」こそが、第 3 のボトルネックが存在する場所です。シンクが遅れると、上記のすべてのスケジューリング処理が古くなったリソースビューに基づいて行われることになります。
LinkPlacement グループ:数時間から数秒へ
大規模環境における配置グループのスケジューリングは、エンドツーエンドで非常に遅い問題がありました。その原因の一つが、前述の設計にありました。
「楽観的な推定」と、それが誤った場合に修正を行う「シンク(同期)プロセス」を思い出してください。しかし、このシンクではカバーしきれない欠陥が存在します。図 6 は、その問題が発生する様子を一貫して追跡しています。
ノード A がリクエストを提出者に返した瞬間から、ノード A はそのリクエストを追跡しなくなります。一方で、ノード B のリソースはすでにノード A の内部ビュー上で楽観的に差し引かれてしまっています。誤った推定を修正できる唯一の方法は、シンクによる更新です。
したがって、提出者がリクエストをドロップした場合や、提出者自身が停止してしまった場合、ノード B 側では何の変化も起こりません。その結果、シンクがトリガーされることもなく、ノード A によるリソースの差し引きは恒久的に漏洩したままとなります。

図 6: リクエストが到着しない場合、推定値の漏洩が発生する
Ray の問題を解決するため、従来は 3 秒ごとに全ノードがローカルで更新されたビューを最後に受信したスナップショットへリセットしていました。同様に、配置グループ(Placement Groups)を自らのリソースビューに基づいて割り当てる GCS も同じ処理を行っていました。
通常はこの仕組みに問題はありません。同期プロセスは 3 秒よりも遥かに高速であり、新しいスナップショットが絶えず届いているからです。しかし、大規模クラスターでは同期が遅延し、この「保険」となるリセット自体がボトルネックとなりました。配置グループは通常クラスターの起動直後に作成されるため、各ノードが報告する初期のスナップショットではクラスターが空であるという状態になります。3 秒ごとのリセットにより、既に使用済みのリソースが再び利用可能に見えるようになり、スケジューラーは実際には何も割り当てていませんでした。単に配置グループを既に満杯のノードへ送り続けるだけであり、同期プロセスが追いつくまでこの状態が続きます。
しかし、配置グループに対して本当にこのようなリセットが必要なのでしょうか?実は、配置グループのスケジューリングは Ray の他の部分とは異なります。図 7 に示す通り、配置グループは 2 フェーズコミットを経て処理され、GCS によってのみ割り当てることができます。GCS はまずリソース確保を試み、すべてのノードからの予約が成功した場合にのみコミットします。アクターやタスクのリクエストと異なり、配置グループのリクエストは他者を介して転送されることはなく、2 フェーズコミットの仕組みにより GCS は常にその成否を把握できます。つまり、GCS にはこの「保険」は不要だったのです。
GCS のリセットコードを単に削除しただけで、10,000 ノード規模のクラスターにおける配置グループのスケジューリングが数秒以内に完了するようになりました。
図 7:配置グループのコミットは 2 フェーズで行われる

図 7:配置グループのコミットは 2 フェーズで行われる
遅延の別の要因は、配置グループ(Placement Group)の準備状態を確認する際に発生するオーバーヘッドです。pg.ready() は、ユーザーが配置グループの準備完了をチェックするための API です。一見すると単純なチェックに見えますが、内部では実際にはダミータスクを配置グループに送信しています。このタスクがグループにスケジューリングされれば、グループは準備完了とみなされます。また、タスクは自然に参照(ref)を返すため、pg.ready() は自動的に非同期 API として機能します。
しかし、この簡略化されたアプローチには実害がありました。ダミータスクはジョブのランタイム環境を引き継ぐため、単純なリソースチェックでも Torch のインストール完了を待たされる事態が発生しました。さらに、この API は配置グループごとに参照を一つ返す仕様のため、結果としてシリアルループの実行を誘発します。実際、多くのユーザーが以下のようなコードを実装していました。
for pg in pgs:
ray.get(pg.ready())しかし、各 ray.get は独自のカスタムタスクでブロックされていたため、チェックは順次実行され、前のタスクが完了するまで次のカスタムタスクの送信が行われませんでした。1,000 グループあれば、1,000 回の逐次的な往復通信が必要でした。一方、pg.ready() は現在は GCS への直接非同期クエリとなっています。依然として同じ ObjectRef を返しますが、背後にタスクが存在しないため、オーバーヘッドは完全に排除されています。
その結果、昨年の Ray と最新の nightly ビルドの両方で、200 ノードから 10,000 ノードまで配置グループを作成する際の性能がどうなったかを示します。構成は、ラックあたり 18 ノードごとに 1 つの配置グループを設け、各ノードに 1 つのバンドルを割り当てるものです。

Figure 8: クラスター規模別の配置グループ準備時間
*Figure 8: クラスター規模別の配置グループ準備時間*
LinkActor のスケジューリング:現実との乖離が数分に及ぶ
配置グループの作成時間が秒単位まで短縮されたにもかかわらず、トポロジ対応型配置グループ内でのアクターの起動は、10,000 ノード規模でも依然として遅いままです。この課題には 2 つの根本的な問題があり、それぞれに対して並行して解決策を推進しています。
まず一つ目の問題は、同期処理(syncer)自体が遅いことです。Ray はすべてのノードがスケジューリング判断を行えることを前提としているため、図 9 に示すように、各ノードからの変更が GCS(Global Control Store)に集中し、そこから再び全 10,000 ノードへ拡散されます。この構造により同期処理がボトルネックとなり、ノードは古いリソース情報を保持したまま不適切な判断を下してしまうのです。

Figure 9: 10,000 ノード規模では同期処理が追いつかない
*Figure 9: 10,000 ノード規模では同期処理が追いつかない*
ステートビューの陳腐化は、特に配置グループ(Placement Groups)においてアクターに二重の悪影響を及ぼします。通常、アクターのスケジューリングはヘッドノードが担当しますが、トポロジ対応型の配置グループが配置された後、アクターをそれらに割り当てるためには、まず配置グループのリソース情報を取得する必要があります。
当初、配置グループのリソース情報は、それを保持するノード上にのみ存在し、シンクャー(syncer)を通じてヘッドノードへ伝播される仕組みです。しかし、このシンクャーの速度は遅く、10,000 ノード規模のクラスターではピーク時に情報の伝播に約 200 秒を要することが確認されています。その結果、配置グループの情報が到着するまで、ヘッドノードは一切スケジューリングを実行できません。
さらに、情報が到着したとしても、シンクャーが常に高速で情報を届けるわけではなく、3 秒のリセット処理が情報の鮮度をさらに低下させるため、ヘッドノードは依然として陳腐化したビューに基づいてスケジューリングを続けてしまいます。この二つの要因により、ヘッドノードは誤った判断を下し、すでに満杯のノードへアクターを送り込んでしまうことになります。これが拒否と再スケジューリングの連鎖です。クラスターの使用率が上がるにつれて状況は悪化し、最終的には再スケジューリングの頻度がさらに増加します。このように、長い遅延(ロングテール)が実際に観測されています。
この課題を解決するため、私たちは現在、より集約型のスケジューリング機構の実現に向けて取り組んでいます。Ray のワークロードの多くは、スケジューリング判断のために数ノードあれば十分です。そのため、「すべてのノードがスケジューリングを行う」という設計の前提を見直すことにしました。
意思決定を集中させることで、クラスター全体のリソース状況を把握する必要があるのは一部のノードだけで済みます。その結果、同期処理の負荷が減り、情報の鮮度が向上します。現在、古くなったビューによって発生している拒否応答の多くが解消されるでしょう。
2 つ目の問題は、現在のアクターには「二つの脳」が存在することです。GCS(Global Control Store)は作成・再起動・破棄のタイミングを決定しますが、所有者(通常はドライバー)は参照を持ち、誰がまだ使用中かを把握しています。アクターの起動時には、この 2 つの間で通信が行われるためコストがかかります。図 10 はそのプロセスを示しており、少なくとも 4 回のメッセージ交換が必要です。具体的には、アクターの登録から、参照カウントに関する永続的な購読を経て、アドレスの取得までを含みます。
再起動時にも同様の影響が出ます。トレーニングワークロードでは、1 つのアクターが死滅するとすべてのアクターが死滅し、再起動を余儀なくされます。その際、数万に及ぶ永続的な購読が同時に GCS に通知を送り、再起動プロセスはアクターごとに最初から通信をやり直すことになります。これがすべて同時に行われるため、大きな負荷となります。

図 10:Driver と GCS の間のやり取り
上記の問題を解決するため、現在取り組んでいるのは、Actor のライフサイクル管理を GCS からオーナー側へ移行することです。そもそも Actor のライフサイクルが GCS に存在する理由は、デタッチされた Actor をサポートするためです。これらの Actor は Driver とは独立したライフサイクルを持つため、Driver が終了しても存続する必要があります。しかし、非デタッチ型の Actor については、ライフサイクルをオーナーに委ねることで、会話の遅延そのものを解消できます。
以下の改善効果は、前述の GCS および配置グループ(Placement Group)の取り組みに加え、上記 2 つの経路に関する先行研究の結果を統合したものです。これにはコミュニティからの貢献も含まれており、シンクャーが同期メッセージをバッチ処理で送信する仕組みが実装されました。その結果として得られるのは、トポロジを考慮した配置グループの作成と、それらへの Actor のスケジューリングです。これは昨年の Ray と、現在の nightly ビルドを比較したものです。

図 11:クラスター規模別のエンドツーエンド Actor 起動時間
LinkWhere This Leaves Us
本記事で解説したすべての修正は、すでに Ray の nightly ビルドに反映されています。引き続き、アクターとドライバーの拡張性向上に向けた投資を続けており、今回紹介した 2 つの改善もその一環です。
Ray をこれらの極限まで押し上げている方々には、どこが限界に達しているかぜひお聞かせください。GitHub や Ray Slack で私たちとつながることができます。
関連記事
今日のまとめ
AIデイリーブリーフで今日の重要ニュースをまとめ読み