1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

LeaderWorkerSetとDisaggregatedSetの概要

1
Last updated at Posted at 2026-10-18

はじめに

以前、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をエージェント実行基盤のスタックの中でどう位置づけているかを、次の図で示しています。

Agent Substrateを中心とした、エージェント基盤のレイヤー構成図。最上位にAgent Runtime/Agent Harnesses/Custom Agents&Toolsからなる Agent Application Layer、その下にSandbox Isolation(gVisor/MicroVM)を含むAgent Substrateと、Deployments/StatefulSets/LWSなどのOther k8s workloads、最下層にKubernetesとIaaS/Bare metal、Blob storageが並ぶ。

(出典: 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です。

LWS Concept

LeaderWorkerSetの概念図。複数のreplica(leader + workerの組)が1つのLeaderWorkerSetにまとまる。
(参考: README.md#L31-L33)

1.2 なぜStatefulSetを土台に選んだか

デフォルトの Ordinal モードでは、LWS は StatefulSet を組み合わせて Pod を管理します。公式ドキュメントは、この選択の理由を5点挙げています。

  1. 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-1 and 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-topology defines 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 Workload and PodGroup objects and sets each managed pod's spec.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 Concept

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 role defined in a DisaggregatedSet spec maps to an independent LeaderWorkerSet, 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 ごとの可用性も含めた検証が必要で、専用の検証環境も欲しくなります。

参考

参考リンク

本文で登場する順に並べています。

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?