こんにちは。LINEヤフーでAIプラットフォーム向けのKubernetesクラスタの設計や構築、運用を担当している大村です。
LINEヤフーでは、100を超えるサービス向けにAI/機械学習を活用したサービスのライフサイクル全般(MLOps)をサポートするAIプラットフォームを内製しています。
最近、AIプラットフォーム向けのKubernetesクラスタであるAI Cloud Platform(ACP)において、H100 GPUクラスタの導入が行われ、本格的に分散深層学習ワークロードを受け入れる基盤が整って きました。それを契機として、ACPにおけるバッチスケジューラの選定を行いました。その結果、kube-scheduler(Scheduling Frameworkによる拡張)+Kueueという組み合わせを導入することにしました。
このブログでは、その検討の軌跡を紹介することで
- ACPにおけるスケジューリングの現状その課題
- AI向けKubernetesにおけるバッチスケジューラに求められる典型的な要件
- 現状における主な選択肢と評価基準
- なぜACPではkube-scheduler+Kueueという選択をしたのか
を共有させていただきたいと思います。AI/バッチ処理系のKubernetesクラスタに関わる方々にとって参考になれば幸いです。
LINEヤフーのAI Cloud Platform(ACP)の概要
LINEヤフーでは、100を超えるサービス向けにAI/機械学習を活用したサービスのライフサイクル全般(MLOps)をサポートするAIプラットフォームを内製しています。このプラットフォームでは、MLOpsを実践するためのライフサイクル全般をサポートしています。
AIプラットフォームの中で、機械学習、大規模データ処理の計算処理の中心的な役割を担っているのがAI Cloud Platform(ACP)というKubernetesクラスタです。このクラスタは、CPUサーバだけでなく、複数のGPUモデルのサーバ群からなる、いわゆるMultitenant Heterogeneous GPUクラスタとなっています。
また最近、Rethinking AI Infrastructure Blogシリーズでも紹介させていただいている通り、データセンターレベル、物理GPUネットワークレイヤ、GPUサーバレイヤ/Kubernetesレイヤに至るまで、RDMAを活用した大規模分散深層学習に最適化された構成になっています。
これまでのACPにおけるスケジューリングの課題
ACPは、AIプラットフォームのKubernetesクラスタとして、これまで6年以上新規のフレームワークのサポート、社内システム連携のサポート、新規GPUのサポートなどの多角的な面で改善を行いながら多くのユーザを収容しつづけています。
ただし、スケジューリングについては、下記のような標準的なチューニングにとどまっていました。
- 重要なワークロードとそうでないワークロードを分けるためにPriority Classを活用
- kube-schedulerのNodeResourceFit/NodeResourcesBalancedAllocation Pluginをチューニングを導入しBinPackingを実現
- ResourceQuotaの仕組みを使ってテナント(Kubernetes Namespace)ごとに利用できるリソース上限を設定
かつ、これには下記のような課題があることも認識されていました。
- All-Reduce型の分散深層学習ジョブが多いと学習ジョブ同士がデッドロックしてし まう(i.e. Gang Schedulingがない)
- 大規模な分散深層学習ジョブを実行すると他のテナントのPodが大量に止められてしまう
- Quotaの上限値しか管理しておらず、テナントごとの利用可能リソース下限値の設定が難しかった
- ResourceQuotaのScope SelectorによるPriority ClassごとのQuota制御は利用可能ですが利用はしていませんでした
- Quotaの制限を超えるとPod作成エラーとなりジョブを投げることもできない
などです。
ACPにおけるバッチスケジューラの選定
そこで、今一度ACPにおけるバッチスケジューラの選定を行いました。選定においては、
- バッチスケジューラに求められる機能要件・運用要件を再整理
- 現時点での現実的な選択肢の選定
- それぞれの選択肢の比較
- ACPとしてのバッチスケジューラの決定
のステップを踏んで行いました。
次節から、これらの検討の軌跡を紹介していきます。
ACPにおけるバッチスケジューラに求められる要件
この節では、ACPのバッチスケジューラに求められる要件についてまとめました。大きく機能要件・運用時要件に分けて検討を行いました。
機能要件
Kubernetesにおける標準的な要件
Kubernetesの最もPrimitiveなワークロードの単位であるPodに備わっている、もしくは標準のkube-schedulerに備わっているスケジューリング機能においてよく使われるであろう要件をまとめました。サードパーティのカスタムスケジューラを選択する際、これらの機能はサードパーティが独自に実装する必要があります。
- Priority and Preemption
- Node selector, Node-affinity/anti-affinity
- Inter-pod affinity, anti-affinity
- Taint/Toleration
- Image Locality
- Scheduling Readiness
- Pod Overhead
- Sidecar Containers
大規模機械学習・バッチ処理特有の要件
Kubernetes上で大規模に機械学習・バッチ処理を動作させる際に必要となると思われる要件をまとめました。これらの要求は独特なものが多いので要件ごとに概要を説明していきます。
Gang Scheduling (Co-Scheduling)
All-Reduce型の分散深層学習では、すべてのワークロー ドが動作していないと計算が始められないという特性を持っています。1 Podずつスケジュール判断を行うスケジューラの場合、下記のように簡単にデッドロックが起こってしまいます。
- 2 Nodeあると仮定します。簡単のためそれぞれのNodeには2 Podまで配置できるとしましょう
- この状態で、Job A(3 Pod), Job B(3 Pod)が要求されたとして
- Job A, Job BのPodが交互にスケジュールされると、各Nodeに2 Podずつ配置された状態でNodeが満杯になってしまい、
- Job A, Job Bともに1 Podずつが未スケジュールのまま永遠に待たされることになってしまいます
- もちろんJob側でtimeout等のヒューリスティックを用いてデッドロックを解決することはできますが、Kubernetesクラスタ管理者としてはこのようなJob側での対応を行わなければならない状況は避けたい
なので、
デッドロックが起きないようにJob Aの3 Pod, もしくは Job Bの3 Podを同時にNodeに配置したい
という機能要求が発生します。
Bin Packing
これは計算リソースをできるだけ効率的に活用したいという要求です。クラスタ全体でフラグメンテーション(Podが配置できないような小さな隙間)を少なくなるようにしたほうが、よりたくさんのジョブを同時に処理できるというわけです。
Network Topology Aware Scheduling
分散深層学習においては、Pod同士が大量のデータ(重みやニューラルネットワーク上を流れるデータなど)をやり取りします。つまり学習に参加するPod同士の通信遅延が学習時間に大きく影響することになります。ACPのH100 GPUクラスタは800 Gbpsという広帯域を持っていますが、それでもPod同士が近いに越したことはありません。
下図はRethinking AI Infrastructure Blogシリーズ Part2で紹介されたACPのH100 GPUクラスタのネットワーク構成図です。この図でいえば、SU(Scalable Unit) 1, SU 16にジョブを分散して配置するよりも、SU 1に閉じ込めて配置したほうが、Super Spineスイッチを経由する必要がないため低遅延で計算ができるのです。
出典:GPUクラスタネットワークとその設計思想(Rethinking AI Infrastructure Part 2)
Oracle CloudのFirst Principles: Superclusters with RDMA—Ultra-high performance at massive scaleではNetwork Topology Awareなスケジューリングを実践しているとのことです。一概には比較できませんが、Oracle Cloudのデータセンタでは、データセンタ内の一番遠いGPU同士のラウンドトリップ遅延が20マイクロ秒、一番近いGPU同士のラウンドトリップ遅延が6.5マイクロ秒とのことです。大雑把に計算すると、ジョブ全体を閉じ込めたほうが実行時間が2倍以上速くなるということです(※帯 域についてはフルバイセクションが確保されていると仮定します)。
Intra-Node Topology Aware Scheduling
同じようなことがNode内でも発生します。Node内にもさまざまなトポロジが存在します。典型的には、NUMA(Non-Uniform Memory Access)トポロジ、PCIeトポロジがあります。NUMAトポロジは計算が実行されるCPUからメモリを読み込むスピードに影響しますし、PCIeトポロジはGPUとNICのデータ転送スピードに影響します。つまりPodをNode内で実行する際に、これらのトポロジを考慮してより近くに配置したいという要求です。
出典: Kubernetes Topology Manager Moves to Beta - Align Up!
Min-Max Quota管理
課題の節でも述べた通り、Kubernetes標準のResource Quotaでは上限値しか指定できません。マルチテナントなクラスタでは、各テナントにおいて、「このくらいは最低限使いたい」という要求は一般的です。また、クラスタ管理者としては、各テナントに割り当てる計算リソースの上限値を割り当て、乱用を防いだり、空き計算リソースをテナント間で融通しあえるように管理したいという場合も多いです。このように、本要件は、割当計算 リソースの上下限を指定してQuota管理を行いたいという要求です。一般的にはElastic Quota, Capacity Schedulingと呼ばれることが多いと思います。この要件の拡張として、Quotaの管理を階層的に管理したいという要求もあります。
Resource Fairness
マルチテナントなクラスタでは、クラスタ内の計算リソースは複数のテナントが共有して利用しています。前節のようなMin-Max Quota管理を行っている場合、最低利用リソース量(Quotaの下限値)として払い出されている分はテナント固有のリソースと捉えられますが、払い出されていない分の計算リソースについては共有リソースとして捉えられます。この場合、共有資源をどのテナントがどのくらい利用できるか?という点で公平性を確保したいというのも一般的な要求です。共有リソースが空いていたからといって、特定のテナントがクラスタ全体を長時間独占/寡占してしまうことは一般的には良くないと考えられます。ただし、プロジェクトやテナントには企業としての重要度も存在するため、現実的には重みをつけた上での公平性が求められます。
Job Queueing
これは想像しやすいと思います。いわゆる「キュー」にジョブを詰めて順番に実行したいという要求です。Kubernetes標準のResource Quotaでは上限値を超えるとAPIエラーとなりPodを作成することができなくなってしまいます。管理されたQuotaの範囲内で、空きができ次第順番にジョブを実行してほしいという要求です。
運用系の要件
機能要件だけでなく、長く安定して運用していくためにも気になることはあります。今回の検討では下記を要件として挙げました。
- Observabilityを確保するためのメトリクスはあるか?
- リリースサイクルは安定しているか?
- ドキュメントは適切に整備されているか?
- アップグレード作業にあたって適切な情報が提供されているか?
拡張性
機能要件を満たさない場合、独自に拡張することが求められます。できるだけForkを行わない形で独自拡張部分だけ内部でメンテナンスできると望ましいです。
検討を行った選択肢
選択肢を選ぶ上で、OSSであること、OSSプロジェクトの成熟度を重要視して選定を行いました。今回の検討では下記を対象としました。バージョンは検討を行った時点での最新バージョンとなっています。
- kube-scheduler(Scheduling Frameworkによる拡張) + Kueue
- バージョン:
- プロジェクト成熟度:
- Kubernetes自体はCNCF Graduated Project
- KueueはSIG Schedulingのサブプロジェクトとして開発が行われている
- Volcano
- バージョン: v1.10.0
- プロジェクト成熟度: CNCF Incubation Project
- もともとSIG Schedulingのサブプロジェクトとして開発されていたkube-batchが前身となっている。
- Apache Yunikorn
- バージョン: v1.6.0
- プロジェクト成熟度: Apache Graduated Project
- Apache Hadoop YARNを強く意識して開発されている。特徴としてKubernetesに依存しない作りになっており、Kubernetes依存部分はyunikorn-k8sshimという独立したコンポーネントに切り出されている
koordinatorも初期検討では候補に上がったのですが、プロジェクト成熟度がCNCF Sandbox Projectであることを考慮し、選択肢としては採用しませんでした。
選択肢ごとの比較
本節では、選択肢ごとの対応概況を説明していきます。各機能の詳細説明は本ブログでは割愛します。それぞれのWebサイトを参照ください。
機能要件
それぞれのサポート状況を大まかに判定するために下記のマークを用いて記載していきます。
- ⭕: サポートし ている
- 🔺: サポートが限定的(or 拡張をsupportしている)
- ❌: サポートしていない(要fork)
Kubernetesにおける標準的な要件
Priority and Preemption
kube-scheduler+Kueue | Volcano | Yunikorn |
---|---|---|
⭕ | ⭕ | ⭕ |
- kube-scheduler+Kueue
- PodとKueueの2レベルで優先度・Preemptionが利用可能
- Priority and Preemption
- Kueue - Workload Priority Class
- Kueue - Preemption
- Volcano
- Volcano - Priority
- 公式のPriorityClassを参照可能
- ただしPreemptionロジックは独自(Fair Share等導入するPluginによって異なる)
- Yunikorn
- Yunikornでは優先度が柔軟に設定できる。
- 公式のPriorityClassの値をベースに、queueごとにoffset, fencingが設定でき、それらを基に計算されるEffective Priorityが使われる
- Yunikorn - Queue Priority - offset
- queueにpriority offsetが設定を指定して優先度が調整可能
- Yunikorn - Priority Fencing
- fencingを利用すると、queue内で見たときと別のqueueから見たときで優先度が変えられる
- 外から評価したときはqueueのoffsetが優先度として使われる
- この図がわかりやすい
- Preemptionロジックは独自実装
- ただしintra-queue(queue内) preemptionは未実装
Node selector, Node-affinity/anti-affinity
kube-scheduler+Kueue | Volcano | Yunikorn |
---|---|---|
⭕ | ⭕ | 🔺 |
- kube-scheduler+Kueue
- サポートしている
- Volcano
- サポートしている
- Yunikorn
- preferredDuringSchedulingIgnoredDuringExecutionが未サポート(PodSpecに定義しても無視される)
Inter-pod affinity, anti-affinity
kube-scheduler+Kueue | Volcano | Yunikorn |
---|---|---|
⭕ | ⭕ |