はじめに
以前、Agent Substrate の概要を執筆した流れで、もう1つ気になっていた LeaderWorkerSet (LWS) を確認してみました。LWSはSIG Appsが管理するKubernetesの公式サブプロジェクト(kubernetes-sigs)で、vLLM/SGLangのような分散LLM推論を「leader 1台 + worker N台」のPodグループ単位でデプロイ・スケール・ローリングアップデートするためのAPIです。
関連する仕組みについては、以下の記事も執筆しています。エージェントの Task を管理する Agent Executor、その実行基盤となる Agent Substrate、単一 Pod のサンドボックスを管理する Agent Sandbox について整理しています。
Google Cloudは、Agent Substrateをエージェント実行基盤のスタックの中でどう位置づけているかを、次の図で示しています。
(出典: Bringing you Agent Sandbox on GKE and Agent Substrate | Google Cloud Blog, 2026-05-20)
LWSは、この図の右側のOther k8s workloads (Deployments, StatefulSets, LWS)に入っています。緑色のAgent Substrate(エージェントの実行基盤)の内側ではなく、DeploymentやStatefulSetと並ぶ「その他のKubernetesワークロード」の1つという位置づけです。
この記事では、LeaderWorkerSetがどういう課題を解決するためのAPIなのか、内部でどう実装されているか、そしてその上に作られた新しいAPIであるDisaggregatedSetが何を追加しているのかを整理します。
対象コンポーネントとバージョン
| コンポーネント | バージョン | リポジトリ | 本記事での役割 |
|---|---|---|---|
| LeaderWorkerSet (LWS) | v1.0.0 | kubernetes-sigs/lws | 本記事全体のソースコード調査対象(LeaderWorkerSet/DisaggregatedSet両APIを含む) |
| kubernetes/kubernetes | v1.37.1 | kubernetes/kubernetes | LWSのgo.modが参照するk8s.io/api v0.37.1に対応するリリース |
| kind | v0.33.0 | kubernetes-sigs/kind | 検証環境。ノードイメージはkindest/node:v1.37.0
|
本記事の実装説明は LWS v1.0.0 に固定します。LeaderWorkerSet と DisaggregatedSet は、このプロジェクトが CRD とコントローラーで提供する API です。Kubernetes 本体に組み込まれた API とは区別します。
(参考: Distributed Inference APIs | LWS Overview)
TL;DR
- LWS は Pod グループをレプリケーションの単位にします。leader と worker をまとめてスケール・更新します。
- デフォルトの Ordinal モードは2階層の StatefulSet で構成されます。Hash モードでは leader の管理に Deployment を使います。
- デフォルトでは、Pod の削除やコンテナの再起動を検知するとグループを再作成します。他のグループは再作成の対象になりません。
- 並列作成と gang scheduling は別の機能です。全 Pod をまとめてスケジューリングするには、追加の設定と対応するスケジューラーが必要です。
- DisaggregatedSet は複数 role の LWS をまとめて管理します。role 間の更新進捗を調整しますが、更新中の稼働数の比率が常に一定になるわけではありません。
第1章: LeaderWorkerSetが解決する課題
この章では、分散LLM推論のようなワークロードが、なぜDeploymentやStatefulSetだけでは扱いにくいのかを整理します。
1.1 Podグループを1つの単位として扱う必要性
vLLM や SGLang のようなフレームワークで、tensor 並列・pipeline 並列を複数ホストにまたがって構成する場合、1つの推論インスタンスを複数の Pod に分けて動かすことがあります。各プロセスは、フレームワークに応じた rank や並列数の設定で協調動作します。READMEはLWSの目的を次のように説明しています。
LeaderWorkerSet (LWS): An API for deploying a group of pods as a unit of replication. It aims to address common deployment patterns of AI/ML inference workloads, especially multi-host inference workloads where the LLM will be sharded and run across multiple devices on multiple nodes.
(参考: README.md#L9)
StatefulSetは個々のPodに安定した名前とストレージを与えますが、「leaderとworkerで別々のPodテンプレートを使い、それらをまとめて1つのレプリカとしてスケール・更新する」という発想を持っていません。LWSはこのギャップを埋めるためのAPIです。
LeaderWorkerSetの概念図。複数のreplica(leader + workerの組)が1つのLeaderWorkerSetにまとまる。
(参考: README.md#L31-L33)
1.2 なぜStatefulSetを土台に選んだか
デフォルトの Ordinal モードでは、LWS は StatefulSet を組み合わせて Pod を管理します。公式ドキュメントは、この選択の理由を5点挙げています。
- Deterministic Ordinal Identity and Predictable Networking ... 2. Native Dynamic Storage (
volumeClaimTemplates) ... 3. Parallel Pod Management ... 4. Partitioned Rolling Updates ... 5. Kubernetes API Composition Best Practices
(参考: Architecture and Relationship with StatefulSet | LWS Concepts)
特に3番目の「並列 Pod 管理」は、1つの Pod が Ready になるまで次の Pod の作成を待つと、NCCL/MPI などの通信相手がそろわず起動が進まないケースに対応します。StatefulSet の Parallel は、前の Pod の Ready を待たずに Pod を作成できる設定です。全 Pod の同時起動や、一括でのリソース確保を保証するものではありません。
また、startupPolicy のデフォルトは LeaderCreated で、leader Pod の作成後に worker 用 StatefulSet を作ります。LeaderReady を選ぶと、leader が Ready になるまで worker の作成を待ちます。
(参考: Startup Policy | LWS Concepts、leaderworkerset_types.go#L650-L656)
1.3 vLLM の並列実行と LWS のグループ管理
LWS が Pod をまとめ、vLLM がモデルを分割して実行します。Pod の数を指定するだけで、モデルの分割方法まで決まるわけではありません。
Google Cloud の2024年11月8日の記事では、Llama 3.1 405B FP16 を、各8基の H100 を持つ2台の A3 ノードで動かしています。ノード内は tensor parallelism (TP) で分割し、ノード間は層を分担する pipeline parallelism (PP) を使う、TP=8・PP=2 の例です。
(参考: How to deploy and serve multi-host gen AI large open models over GKE | Google Cloud Blog)
このように1 Pod が1ノードの8 GPU を使う構成を LWS で表すと、設定の対応は次のようになります。
| 設定 | 値 | 意味 |
|---|---|---|
LWS spec.replicas
|
1 | 推論インスタンスを1組動かす |
LWS leaderWorkerTemplate.size
|
2 | leader を含む2 Pod で1組を構成する |
| 各 Pod の GPU 要求 | 8 | 1組あたり合計16 GPU を使う |
vLLM --tensor-parallel-size
|
8 | tensor を8分割して実行する |
vLLM --pipeline-parallel-size
|
2 | モデルの層を2段に分けて実行する |
ここで replicas を2にすると、同じ構成の推論インスタンスが2組、定常状態で4 Pod・32 GPU になります。1つのモデルの分割数を増やす操作とは異なります。また、「TP はノード内、PP はノード間」はこの例の構成です。GPU 間の接続やモデルに応じて別の分割も選べます。
(参考: Parallelism and Scaling | vLLM)
Google の記事は Ray を使っています。Ray は、Python の処理を複数の CPU・GPU やノードに分散して実行するためのオープンソースのフレームワークです。この構成では、vLLM の分散実行を担います。
2026年10月21日に確認した vLLM の LWS 手順には multiprocessing と Ray の両方の例があります。Ray は LWS 自体の必須要件ではありません。multiprocessing の例では、LWS が設定する環境変数を vLLM の起動引数に渡します。
| LWS の環境変数 | vLLM の引数 | 渡す情報 |
|---|---|---|
LWS_GROUP_SIZE |
--nnodes |
グループ内の Pod 数。この例ではノード数に対応 |
LWS_WORKER_INDEX |
--node-rank |
グループ内のインデックス。leader は0 |
LWS_LEADER_ADDRESS |
--master-addr |
分散実行の接続先となる leader のアドレス |
同じ手順では、HTTP Service が leader のラベルだけを選択し、worker は分散実行に参加します。外部リクエストの受付先と、計算に参加する Pod を分けている点も、2種類のテンプレートを使う理由です。
(参考: LWS | vLLM、leaderworkerset_types.go#L70-L80)
vLLM の stable ドキュメントは更新されます。ここでは設定の対応を説明しており、この GPU 構成で推論を実行したわけではありません。実際に試す際は、vLLM のイメージと対応する手順を固定し、GPU メモリ、モデルの配布方法、ノード間通信も確認してください。手順にある「2ノード・各8 GPU」はそのデプロイ例の条件で、LWS API 全体の最低要件ではありません。
第2章: Ordinal モードの2階層 StatefulSet 構成
この章では、LeaderWorkerSetが実際にどのKubernetesリソースを作ってグループを実現しているかを、ソースコードから確認します。
2.1 LeaderWorkerSetSpecの主要フィールド
LeaderWorkerSetSpecは、グループ数(replicas)とグループの中身(leaderWorkerTemplate)を分けて持っています。
type LeaderWorkerSetSpec struct {
// replicas is the number of leader-workers groups. ...
// On scale down, the leader pod as well as the workers statefulset will be deleted.
// Default to 1.
Replicas *int32 `json:"replicas,omitempty"`
// leaderWorkerTemplate defines the template for leader/worker pods
LeaderWorkerTemplate LeaderWorkerTemplate `json:"leaderWorkerTemplate"`
...
}
(参考: leaderworkerset_types.go#L175-L191)
LeaderWorkerTemplateの方は、leader用とworker用で別々のPodTemplateを持てるようになっています。
type LeaderWorkerTemplate struct {
// leaderTemplate defines the pod template for leader pods.
LeaderTemplate *corev1.PodTemplateSpec `json:"leaderTemplate,omitempty"`
// workerTemplate defines the pod template for worker pods.
WorkerTemplate corev1.PodTemplateSpec `json:"workerTemplate"`
// size is the number of pods to create. It is the total number of pods in each group.
// The minimum is 1 which represent the leader. ...
Size *int32 `json:"size,omitempty"`
...
}
(参考: leaderworkerset_types.go#L383-L403)
sizeが1グループあたりのPod総数(leader含む)で、replicasがグループの数です。更新や再作成が完了した定常状態では、Pod の総数は replicas * size になります。更新・終了処理の途中では、この数と一致しないことがあります。leaderTemplate を省略すると、leader にも workerTemplate が使われます。
2.2 Leader用StatefulSetとWorker用StatefulSetの構築
ここでは、デフォルトの groupIdentity: Ordinal を説明します。LWS は、LeaderWorkerSet そのものを管理するLeaderWorkerSetReconcilerと、個々のleader Podを監視するPodReconcilerという2つのコントローラーで構成されています。Leader用のStatefulSetはLeaderWorkerSetReconciler側、Worker用のStatefulSetはPodReconciler側で構築されます。
Leader用StatefulSetの構築では、replicas個のleader PodをParallelなPod管理方式で作ります。
statefulSetConfig := appsapplyv1.StatefulSet(lws.Name, lws.Namespace).
WithSpec(appsapplyv1.StatefulSetSpec().
WithServiceName(lws.Name).
WithReplicas(replicas).
WithPodManagementPolicy(appsv1.ParallelPodManagement).
WithTemplate(podTemplateApplyConfiguration).
...
(参考: leaderworkerset_controller.go#L1189-L1226)
デフォルトの startupPolicy: LeaderCreated では、leader Pod が1つ作られるたびに、PodReconcilerがそのleaderに対応するWorker用StatefulSetを<lws名>-<replicaのインデックス>という名前で作ります。Pod数はsize - 1(leaderを除いた数)で、オーディナルは0ではなく1から始まります。
statefulSetConfig := appsapplyv1.StatefulSet(workerStatefulSetName(&leaderPod), leaderPod.Namespace).
WithSpec(appsapplyv1.StatefulSetSpec().
WithServiceName(serviceName).
WithReplicas(*currentLws.Spec.LeaderWorkerTemplate.Size - 1).
WithPodManagementPolicy(appsv1.ParallelPodManagement).
WithTemplate(&podTemplateApplyConfiguration).
WithOrdinals(appsapplyv1.StatefulSetOrdinals().WithStart(1)).
...
(参考: pod_controller.go#L1586-L1649)
つまり、replicas=2, size=3のLeaderWorkerSetを1つ作ると、実際には次のKubernetesリソースが生成されます。
LeaderWorkerSet (replicas=2, size=3)
│
└── Leader StatefulSet "<lws名>" (replicas=2)
│
├── Leader Pod "<lws名>-0" + Worker StatefulSet "<lws名>-0" (replicas=2, startOrdinal=1)
│ → Worker Pod "<lws名>-0-1", "<lws名>-0-2"
│
└── Leader Pod "<lws名>-1" + Worker StatefulSet "<lws名>-1" (replicas=2, startOrdinal=1)
→ Worker Pod "<lws名>-1-1", "<lws名>-1-2"
(参考: Architecture and Relationship with StatefulSet | LWS Concepts)
5.2節で、この構造を実際にkindクラスタ上で確認します。
第3章: グループ単位の振る舞いを支える主な仕組み
この章では、LeaderWorkerSetが「グループを1つの単位として扱う」ために追加している、StatefulSet単体には無い仕組みを見ていきます。
3.1 グループ単位の再作成: RestartPolicy
分散推論では、1つのworker Podが落ちて集団通信が崩れても、そのPodだけを再起動すれば済むとは限りません。LWSはデフォルトで、グループ内のどれか1つのPodが失敗・再起動した場合、グループ全体(leader + 全worker)を作り直すRecreateGroupOnPodRestartというポリシーを使います。
実装では、PodReconciler が Pod の削除やコンテナの再起動を検知し、グループの leader Pod を削除することで再作成を進めます。
// the leader pod will be deleted if the worker pod is deleted or any container was restarted
if !isRecreatePending && !podutils.ContainerRestarted(pod) && !podutils.PodDeleted(pod) {
return false, ctrl.Result{}, nil
}
(参考: pod_controller.go#L562-L567)
worker 用 StatefulSet の owner reference は leader Pod を指しています。leader の削除に伴って、ガベージコレクションが worker 用 StatefulSet とその Pod を削除します。その後、leader 用 StatefulSet と PodReconciler が新しい leader と worker 群を作ります。削除・終了・再作成は非同期に進むため、全 Pod が同時に停止・起動するという意味ではありません。
(参考: pod_controller.go#L384-L393、pod_controller.go#L684-L699)
v1.0.0 では、再作成回数の上限を設ける maxGroupRestarts と、再作成の間隔を延ばす restartBackoff も設定できます。どちらもデフォルトでは未設定で、回数制限やバックオフは有効になりません。5.3節では、これらを設定せずに worker Pod を1つ削除した場合を確認します。
(参考: leaderworkerset_types.go#L413-L439)
3.2 グループ識別方式: OrdinalとHash
LeaderWorkerSetSpecのgroupIdentityフィールドは、グループの名前付けとスケールダウン方式を切り替えます。
groupIdentity determines how group identities are assigned.
Ordinal (default) manages leaders through a StatefulSet: groups are named<lws>-0..<lws>-N-1and scale down always removes the highest ordinal.
Hash manages leaders through a Deployment: group names are hash-suffixed, scale down prefers unscheduled and not-ready groups over healthy ones, and rollouts are paced by a group readiness gate on the leader pods.
(参考: leaderworkerset_types.go#L215-L225)
Hashモードの実装コメントは、その仕組みをさらに具体的に説明しています。
reconcileHash reconciles a LeaderWorkerSet with GroupIdentity=Hash. Leaders are managed through a Deployment instead of a StatefulSet: the ReplicaSet picks scale-down victims (unscheduled and not-ready groups before healthy ones) and the Deployment paces rollouts, throttled by the group-ready readiness gate that the pod controller maintains on leader pods.
(参考: leaderworkerset_hash.go#L45-L49)
| 項目 | Ordinal(デフォルト) | Hash |
|---|---|---|
| leaderの実体 | StatefulSet | Deployment |
| グループ名 |
<lws>-0, <lws>-1, ... の連番 |
Deployment が生成するハッシュ・接尾辞を含む名前 |
| スケールダウン対象 | 常に最大オーディナルのグループ | 未スケジュール・未Readyなグループを優先 |
| ロールアウトの調整方法 | StatefulSetのpartition | leader Podへのreadiness gate |
Ordinal は安定したグループ名が必要な構成に適しています。Hash は、連番に依存せず、未スケジュール・未 Ready のグループを優先して減らしたい場合の選択肢です。ただし、アプリケーションの処理内容を見て、任意のグループを確実に残す機能ではありません。groupIdentity は作成後に変更できません。
(参考: Group Identity | LWS Concepts)
3.3 トポロジー配置とgang scheduling
分散推論はノード間の通信帯域がボトルネックになりやすいため、LWS には1つのグループを同じトポロジードメイン(ラック、ブロックなど)に配置するための設定があります。利用する場合は、次のアノテーションにノードのトポロジーラベルのキーを指定します。デフォルトでこの配置制約が付くわけではありません。
The annotation
leaderworkerset.sigs.k8s.io/exclusive-topologydefines a 1:1 mapping between an LWS replica and a topology domain.
(参考: Exclusive Topology Placement | LWS Concepts)
これとは別に、Alpha機能として「グループ内の全Podをall-or-nothingでスケジューリングする」gang schedulingも用意されています。
Native gang scheduling uses the Kubernetes Workload-Aware Scheduling APIs to admit related pods as a unit. LWS owns the
WorkloadandPodGroupobjects and sets each managed pod'sspec.schedulingGroup; users express the intent only on the LeaderWorkerSet.
(参考: Gang Scheduling | LWS Concepts)
v1.0.0 の Kubernetes ネイティブな gang scheduling には、次の設定が必要です。5章の通常のインストールでは有効にしていません。
| 対象 | 必要な設定 |
|---|---|
| Kubernetes | v1.37 の Workload / PodGroup API。kube-apiserver・kube-controller-manager・kube-scheduler で GenericWorkload=true を有効化し、kube-apiserver の --runtime-config に scheduling.k8s.io/v1beta1=true を指定する |
| LWS コントローラー |
featureGates.WorkloadAwareScheduling: true と gangSchedulingManagement.schedulerProvider: kubernetes
|
| LeaderWorkerSet |
spec.scheduling を設定する。scheduling: {} ではグループごとの gang scheduling を使う |
(参考: Native Gang Scheduling | LWS Installation、Gang Scheduling | LWS Concepts)
gang scheduling は Alpha 機能です。Kubernetes ネイティブの API を使う構成のほか、Volcano 向けの実装もあります。ネイティブ構成のグループ単位の gang scheduling は startupPolicy: LeaderReady と併用できず、exclusive-topology アノテーションとの併用にも制約があります。並列作成だけを設定した通常の LWS は、Pod ごとにスケジューリングされます。
3.4 HPA が増減するのもグループ数
LWS は HPA 向けの scale サブリソースを持ち、spec.replicas を通じてグループ数を増減できます。HPA が変更するのはグループ数で、size や vLLM の TP・PP 数ではありません。1.3節の構成なら、1レプリカ追加するたびに2 Pod・16 GPU 分の配置先が必要です。
v1.0.0 の status.hpaPodSelector は leader Pod だけを選択します。Pod の CPU 使用率を使う HPA でも、worker を含むグループ全体の GPU 負荷を自動的に評価するわけではありません。推論負荷に合わせるには、受付待ちリクエスト数など、利用するサービング基盤で取得できる指標とスケール方針を検討します。
(参考: leaderworkerset_types.go#L607-L611、scale サブリソースの定義、Autoscaling | LWS Examples)
第4章: DisaggregatedSet — 複数のLWSをrole単位でまとめる
この章では、LWSの上に作られた新しいAPIであるDisaggregatedSetが何を追加しているかを整理します。
4.1 Disaggregated推論という課題
LLM推論は、入力プロンプトからKVキャッシュを作るprefillフェーズと、トークンを1つずつ生成するdecodeフェーズに分けられます。一般に prefill は計算量、decode はメモリ帯域の影響を受けやすいため、それぞれ別のリソース量・別のスケール方針を持つPod群として分離(disaggregate)したい、という要求があります。
DisaggregatedSet (DS): An API to support advanced multi-node inference. LWS forms the core API for multi-node while DisaggregatedSet builds on it to add advanced disaggregated workload deployment with support for autoscaling, rollouts and failure handling.
(参考: README.md#L11)
DisaggregatedSetの概念図。複数のroleがそれぞれ独立したLeaderWorkerSetとして管理される。
(参考: README.md#L42-L44)
4.2 DisaggregatedSetは子LeaderWorkerSetを作るAPI
DisaggregatedSetはLWSを置き換えるAPIではなく、LWSの上に乗るAPIです。
DisaggregatedSet does not replace LeaderWorkerSet — it orchestrates multiple LeaderWorkerSets.
Each
roledefined in aDisaggregatedSetspec maps to an independentLeaderWorkerSet, deployed in the same namespace.
(参考: DisaggregatedSet | LWS Concepts)
DisaggregatedSetSpec は、role(prefill/decode 等)のリストと、スライス数(同じ role トポロジーをまるごと複製する単位)を持ちます。slices のデフォルトは1です。以下では subrole を使わない基本構成を説明します。
type DisaggregatedSetSpec struct {
// Roles defines the list of roles (at least 1 required).
Roles []DisaggregatedRoleSpec `json:"roles"`
// Slices is the number of independent copies of the whole role topology.
Slices *int32 `json:"slices,omitempty"`
...
}
(参考: disaggregatedset_types.go#L217-L247)
DisaggregatedRoleSpecはsigs.k8s.io/lwsのLeaderWorkerSetTemplateSpecをそのまま埋め込んでおり、各roleの実体は中身がLWSのSpecそのものです。
type DisaggregatedRoleSpec struct {
// Name is the unique identifier for this role.
Name string `json:"name"`
...
// LeaderWorkerSetTemplateSpec defines the LWS template for this role.
// Note: Spec.RolloutStrategy.Type must be RollingUpdate (or empty) and
// Spec.RolloutStrategy.RollingUpdateConfiguration.Partition must not be set.
// DisaggregatedSet handles rollouts across roles.
leaderworkerset.LeaderWorkerSetTemplateSpec `json:",inline"`
}
(参考: disaggregatedset_types.go#L184-L216)
定常状態では、各スライスの role ごとに子 LWS が作られます。更新中は旧・新リビジョンの子 LWS が共存するため、常に「role 数と同じ数の LWS」になるわけではありません。子 LWS の名前は、どのDisaggregatedSetのどのスライス・どのリビジョン・どのroleに属するかが分かるように、次の規則で生成されます。
func GenerateName(baseName string, slice int, revision, role string) string {
return fmt.Sprintf("%s-%d-%s-%s", baseName, slice, revision, role)
}
(参考: utils.go#L96-L98)
生成された子LWSには、disaggregatedset.x-k8s.io/name・disaggregatedset.x-k8s.io/role・disaggregatedset.x-k8s.io/sliceといったラベルが付き、ハードコードした名前ではなくこれらのラベルで子リソースを特定することが推奨されています(参考: DisaggregatedSet | LWS Concepts)。5.4節で、実際にDisaggregatedSetを作って子LWSの名前・ラベルを確認します。
4.3 role間で協調するロールアウト
DisaggregatedSet は、prefill と decode のような複数 role の更新進捗を調整します。各 role が独立に更新を完了するのではなく、role ごとのレプリカ数に対する更新割合を考慮して、旧・新リビジョンへの移行を進めます。
各 role の LWS Spec は RollingUpdate を使い、partition の値は DisaggregatedSet が管理します。一方、追加を許すグループ数の maxSurge と、利用できなくなることを許すグループ数の maxUnavailable は、role ごとに設定できます。
v1.0.0 のデフォルトは maxSurge: 0・maxUnavailable: 1 です。1グループしかない role は、更新中に全グループが利用できなくなることがあります。たとえば prefill が1、decode が2の構成でも、更新途中の稼働数が常に 1:2 に保たれる保証はありません。停止を許さない role では maxUnavailable: 0 と正の maxSurge、追加分を動かせるリソースを用意する必要があります。
(参考: DisaggregatedSet | LWS Concepts、disaggregatedset_types.go#L184-L216)
DisaggregatedSet が管理するのは role の配置・スケール・更新です。prefill から decode への KV キャッシュ転送や、推論リクエストの振り分けには、利用する推論フレームワーク側の設定も必要です。
第5章: kindクラスタで実際に動かす
この章では、2026年10月21日に kind v0.33.0・Kubernetes v1.37.0・LWS v1.0.0 で実行した手順と結果を整理します。nginx で API とリソース構成を確認するもので、LLM 推論の性能や GPU 通信、gang scheduling は検証していません。
5.1 インストール
Docker・kind・kubectl を用意し、検証専用のクラスタを作ります。lws-demo は既存環境と重複しない名前にしてください。通常の LWS / DisaggregatedSet を使うには、公式のリリースマニフェストを適用します。
kind create cluster --name lws-demo --image kindest/node:v1.37.0
VERSION=v1.0.0
kubectl apply --server-side -f https://github.com/kubernetes-sigs/lws/releases/download/$VERSION/manifests.yaml
kubectl wait deploy/lws-controller-manager -n lws-system --for=condition=available --timeout=5m
(参考: Install a released version | LWS Installation)
このマニフェストにはLeaderWorkerSetとDisaggregatedSet両方のCRDが含まれており、DisaggregatedSetを使うための追加のインストール手順は必要ありません。
Starting from v0.9.0, DisaggregatedSet is bundled with the LWS controller manager.
For kubectl and Kustomize installs, the standard v0.9.0+ manifests include the DisaggregatedSet CRD, controller permissions, and validating webhook. No separate DisaggregatedSet installation step is required.
(参考: DisaggregatedSet | LWS Installation)
5.2 LeaderWorkerSetを作って2階層のStatefulSetを確認する
GPU を使わずに確認できるよう、公式の nginx 例から LWS 部分を使いました。以下を lws-sample.yaml として保存します。HPA は使わないため、この確認に Metrics Server は不要です。
apiVersion: leaderworkerset.x-k8s.io/v1
kind: LeaderWorkerSet
metadata:
name: leaderworkerset-sample
spec:
replicas: 2
leaderWorkerTemplate:
size: 3
leaderTemplate:
spec:
containers:
- name: leader
image: nginx:1.27
resources:
requests: {cpu: 100m, memory: 128Mi}
limits: {cpu: 500m, memory: 256Mi}
workerTemplate:
spec:
containers:
- name: worker
image: nginx:1.27
resources:
requests: {cpu: 100m, memory: 128Mi}
limits: {cpu: 500m, memory: 256Mi}
(参考: nginx.yaml#L5-L38、HPA部分は本記事では使用していません)
kubectl apply -f lws-sample.yaml
kubectl wait pod -l leaderworkerset.sigs.k8s.io/name=leaderworkerset-sample --for=condition=Ready --timeout=5m
replicas=2, size=3 で作成し、全6 Pod が Ready になるまで確認すると、2.2節で説明した通りのStatefulSet構造がそのまま現れました。
以下は今回の実行結果です。
$ kubectl get leaderworkerset,statefulset,pods
NAME READY DESIRED UP-TO-DATE AGE
leaderworkerset.leaderworkerset.x-k8s.io/leaderworkerset-sample 2 2 2 36s
NAME READY AGE
statefulset.apps/leaderworkerset-sample 2/2 35s
statefulset.apps/leaderworkerset-sample-0 2/2 35s
statefulset.apps/leaderworkerset-sample-1 2/2 35s
NAME READY STATUS RESTARTS AGE
pod/leaderworkerset-sample-0 1/1 Running 0 35s
pod/leaderworkerset-sample-0-1 1/1 Running 0 35s
pod/leaderworkerset-sample-0-2 1/1 Running 0 35s
pod/leaderworkerset-sample-1 1/1 Running 0 35s
pod/leaderworkerset-sample-1-1 1/1 Running 0 35s
pod/leaderworkerset-sample-1-2 1/1 Running 0 35s
leader用StatefulSetが1つ(leaderworkerset-sample)、replicaごとのworker用StatefulSetが2つ(leaderworkerset-sample-0、leaderworkerset-sample-1)作られ、worker Podのオーディナルが0ではなく1から始まっていることも確認できました。あわせて、グループ内のPod間でDNS解決するためのheadless Serviceも作られています。
$ kubectl get svc leaderworkerset-sample -o wide
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE SELECTOR
leaderworkerset-sample ClusterIP None <none> <none> 35s leaderworkerset.sigs.k8s.io/name=leaderworkerset-sample
5.3 Pod を1つ削除してグループの再作成を確認する
3.1節で見たRecreateGroupOnPodRestart(デフォルト)の挙動を、replica 0のworker Pod 1つを明示的に削除して確認しました。
削除前後で、次のコマンドから Pod の UID と作成時刻を取得して比較できます。同じ名前の Pod でも、UID が変われば別の Pod です。
kubectl get pods -l leaderworkerset.sigs.k8s.io/name=leaderworkerset-sample \
-o custom-columns=NAME:.metadata.name,UID:.metadata.uid,CREATED:.metadata.creationTimestamp
kubectl delete pod leaderworkerset-sample-0-1
# 再作成は非同期に進むため、グループ0の置き換えと全6 Pod の Ready を確認する
kubectl get pods -l leaderworkerset.sigs.k8s.io/name=leaderworkerset-sample -w
一覧の監視は Ctrl-C で終了し、最初の UID・作成時刻の取得コマンドを再実行します。
削除前後でPodのUIDと作成時刻を比較すると、削除したPodだけでなく、同じreplica(グループ0)のleaderと残りのworkerもすべて新しいUID・新しい作成時刻を持つPodに置き換わっていました。
UID は先頭8文字を抜粋しています。
| Pod | 削除前の UID | 削除後の UID | 結果 |
|---|---|---|---|
leaderworkerset-sample-0 |
6196dd0c |
f8e535fc |
作り直された |
leaderworkerset-sample-0-1 |
e41eae20 |
82eebca7 |
作り直された |
leaderworkerset-sample-0-2 |
6985a793 |
539304af |
作り直された |
leaderworkerset-sample-1 |
561f7a2d |
561f7a2d |
変化なし |
leaderworkerset-sample-1-1 |
4353aebd |
4353aebd |
変化なし |
leaderworkerset-sample-1-2 |
464c526f |
464c526f |
変化なし |
グループ1(無関係なreplica)のPodは一切再作成されず、UIDも作成時刻も変化しませんでした。worker Podを1つ削除しただけで、そのPodが属するグループ全体(leader含む)だけが作り直され、他のグループには影響しないことを確認できました。
5.4 DisaggregatedSetを作って子LWSの生成を確認する
4.2節で見た子 LWS の生成を、1スライス・prefill/decode の2 role で確認しました。実際の推論処理はせず、nginx で API の挙動だけを見ています。以下を disaggregated-sample.yaml として保存します。
apiVersion: disaggregatedset.x-k8s.io/v1
kind: DisaggregatedSet
metadata:
name: nginx-disagg
spec:
roles:
- name: prefill
spec:
replicas: 1
leaderWorkerTemplate:
size: 1
workerTemplate:
spec:
containers:
- {name: nginx, image: nginx:1.27, resources: {requests: {cpu: 50m, memory: 64Mi}}}
- name: decode
spec:
replicas: 2
leaderWorkerTemplate:
size: 1
workerTemplate:
spec:
containers:
- {name: nginx, image: nginx:1.27, resources: {requests: {cpu: 50m, memory: 64Mi}}}
(参考: disaggregatedset_types.go#L184-L247で確認したスキーマに沿って作成)
kubectl apply -f disaggregated-sample.yaml
kubectl get leaderworkerset -l disaggregatedset.x-k8s.io/name=nginx-disagg -w
各 role が必要数の Ready グループを持つことを確認し、Ctrl-C で監視を終了します。以下は今回の実行結果で、リビジョンハッシュは設定内容によって変わります。
適用すると、4.2節で説明した命名規則の通り、<ds名>-<スライス番号>-<リビジョンハッシュ>-<role名>という名前の子LWSが2つ作られました。
$ kubectl get disaggregatedset,leaderworkerset
NAME AGE
disaggregatedset.disaggregatedset.x-k8s.io/nginx-disagg 3s
NAME READY DESIRED UP-TO-DATE AGE
leaderworkerset.leaderworkerset.x-k8s.io/leaderworkerset-sample 2 2 2 43s
leaderworkerset.leaderworkerset.x-k8s.io/nginx-disagg-0-69150551-decode 2 2 2 3s
leaderworkerset.leaderworkerset.x-k8s.io/nginx-disagg-0-69150551-prefill 1 1 1 3s
nginx-disagg-0-69150551-prefillのラベルとowner referenceを見ると、4.2節で引用したdisaggregatedset.x-k8s.io/*ラベルと、親のDisaggregatedSetへのowner referenceが実際に付与されていました。
$ kubectl get leaderworkerset nginx-disagg-0-69150551-prefill --show-labels
LABELS: disaggregatedset.x-k8s.io/name=nginx-disagg,disaggregatedset.x-k8s.io/revision=69150551,disaggregatedset.x-k8s.io/role=prefill,disaggregatedset.x-k8s.io/slice=0,...
実際に確認する際は、ハッシュを含む名前を固定せず、ラベルから子 LWS を取得できます。owner reference は次のコマンドで確認できます。
kubectl get leaderworkerset \
-l disaggregatedset.x-k8s.io/name=nginx-disagg,disaggregatedset.x-k8s.io/role=prefill \
-o 'jsonpath={range .items[*]}{.metadata.name}{"\n"}{.metadata.ownerReferences}{"\n"}{end}'
この例は size: 1 で、各グループには leader だけが存在します。leaderTemplate を省略しているため、leader は workerTemplate の nginx で動きます。今回の size: 1 の構成では worker 用 StatefulSet は作られず、各 role の leader 用 StatefulSet だけが作られました。
また、DisaggregatedSetの状態(status.roleStatuses)には各roleのreplicas/readyReplicas/updatedReplicasがroleごとに記録されていました。
status:
roleStatuses:
- {name: prefill, desiredReplicas: 1, replicas: 1, readyReplicas: 1, updatedReplicas: 1}
- {name: decode, desiredReplicas: 2, replicas: 2, readyReplicas: 2, updatedReplicas: 2}
DisaggregatedSetという1つのリソースを操作するだけで、prefillとdecodeという性質の異なる2つのPod群が、それぞれ独立したLWSとして一貫した命名・ラベルの下に管理されることを確認できました。
検証が終わったら、作成したクラスタを削除します。
kind delete cluster --name lws-demo
おわりに
Agent Substrate が Actor 用の control plane を Kubernetes の外に持つ一方で、LWS は StatefulSet や Deployment を組み合わせて Pod グループを管理します。同じ AI 関連の基盤でも、扱う単位と Kubernetes に任せる範囲が違う点が面白いと感じました。
今回は nginx でリソース構成とグループの再作成を確認しました。実際の分散推論では、GPU の確保や通信、role ごとの可用性も含めた検証が必要で、専用の検証環境も欲しくなります。
参考
参考リンク
本文で登場する順に並べています。
- Agent Executorの概要
- Agent Substrate の概要
- Agent Sandboxの概要
- Bringing you Agent Sandbox on GKE and Agent Substrate | Google Cloud Blog
- kubernetes-sigs/lws
- Distributed Inference APIs | LWS Overview
- Architecture and Relationship with StatefulSet | LWS Concepts
- Startup Policy | LWS Concepts
- How to deploy and serve multi-host gen AI large open models over GKE | Google Cloud Blog
- Parallelism and Scaling | vLLM
- LWS | vLLM
- Failure Handling and Restart Policies | LWS Concepts
- Group Identity | LWS Concepts
- Exclusive Topology Placement | LWS Concepts
- Gang Scheduling | LWS Concepts
- Autoscaling | LWS Examples
- DisaggregatedSet | LWS Concepts
- Installation | LWS Docs
