Skip to content

Commit e46e893

Browse files
lukebaumanncopybara-github
authored andcommitted
Remove head job sidecar mode from PathwaysJobSet.
PiperOrigin-RevId: 949622810
1 parent a04790b commit e46e893

5 files changed

Lines changed: 202 additions & 411 deletions

File tree

README.md

Lines changed: 0 additions & 21 deletions
This file was deleted.
Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
# PathwaysJobSet Builder
2+
3+
`PathwaysJobSet` is a client-side Python library designed to generate and
4+
deploy Kubernetes `JobSet` resources (`kind: JobSet`) for running Pathways
5+
workloads on GKE.
6+
7+
It serves as a direct replacement for the custom `PathwaysJob` CRD (`kind:
8+
PathwaysJob`), allowing you to deploy Pathways workloads using standard,
9+
community-supported `JobSet` resources without needing a custom controller
10+
installed on your GKE cluster.
11+
12+
---
13+
14+
## Key Features
15+
16+
- **Client-Side Generation:** Generates standard `JobSet` YAMLs that can be
17+
inspected, version-controlled, or applied manually.
18+
- **Standard Headless Execution:** Runs the Pathways Resource Manager (RM)
19+
and Proxy as standalone containers in the head job.
20+
- **GCSFuse Integration:** Easily mount GCS buckets to head and/or worker
21+
pods.
22+
- **Colocated Python Support:** Inject a colocated Python sidecar container
23+
into worker pods for multi-agent or hybrid workloads.
24+
- **Elasticity Support:** Configure elastic slices for dynamic scaling.
25+
26+
---
27+
28+
## Basic Usage
29+
30+
```python
31+
from pathwaysutils.experimental.gke import jobset
32+
33+
# 1. Initialize the builder
34+
pw_jobset = jobset.PathwaysJobSet(
35+
name="my-pathways-workload",
36+
namespace="default",
37+
pathways_dir="gs://my-bucket/pathways-scratch",
38+
tpu_type="v5e",
39+
topology="4x8",
40+
num_slices=1,
41+
)
42+
43+
# 2. Export to a standard JobSet YAML file
44+
pw_jobset.export_yaml("jobset.yaml")
45+
46+
# 3. Deploy directly to a GKE cluster (requires kubernetes configured)
47+
pw_jobset.apply(
48+
project_id="my-gcp-project",
49+
region="us-central1",
50+
cluster_id="my-gke-cluster",
51+
)
52+
```
53+
54+
---
55+
56+
## Examples
57+
58+
### 1. Single-Slice v5e-32 (Non-elastic)
59+
A standard single-slice workload on TPU v5e-32 (32 chips, `4x8` topology, 8
60+
VMs).
61+
62+
```python
63+
from pathwaysutils.experimental.gke import jobset
64+
65+
js = jobset.PathwaysJobSet(
66+
name="v5e-32-headless",
67+
namespace="default",
68+
pathways_dir="gs://my-bucket/scratch",
69+
tpu_type="v5e",
70+
topology="4x8",
71+
num_slices=1,
72+
)
73+
js.export_yaml("v5e_32_headless.yaml")
74+
```
75+
76+
### 2. Multislice v5p-4x4x4 (2 Slices)
77+
A multislice workload using 2 slices of TPU v5p-4x4x4 (64 chips per slice).
78+
79+
```python
80+
from pathwaysutils.experimental.gke import jobset
81+
82+
js = jobset.PathwaysJobSet(
83+
name="v5p-multislice",
84+
namespace="default",
85+
pathways_dir="gs://my-bucket/scratch",
86+
tpu_type="v5p",
87+
topology="4x4x4",
88+
num_slices=2, # 2 slices
89+
)
90+
js.export_yaml("v5p_multislice.yaml")
91+
```
92+
93+
### 3. Multislice v6e with Elasticity
94+
A multislice workload on TPU v6e-16 (topology `4x4`, 16 chips, 4 VMs per
95+
slice) with 2 active slices initially, and elasticity configured for up to 4
96+
slices.
97+
98+
```python
99+
from pathwaysutils.experimental.gke import jobset
100+
101+
js = jobset.PathwaysJobSet(
102+
name="v6e-elastic",
103+
namespace="default",
104+
pathways_dir="gs://my-bucket/scratch",
105+
tpu_type="v6e",
106+
topology="4x4",
107+
num_slices=2, # Initial/Active slices
108+
elastic_slices=4, # Enable elasticity (informs proxy of max slices)
109+
)
110+
js.export_yaml("v6e_elastic.yaml")
111+
```
112+
113+
### 4. Advanced Features: GCSFuse and Colocated Python
114+
This example demonstrates chaining advanced features:
115+
116+
- Mounting a GCS bucket to all containers (head and workers) using GCSFuse.
117+
- Adding a colocated Python sidecar to the worker pods.
118+
119+
```python
120+
from pathwaysutils.experimental.gke import jobset
121+
122+
js = (
123+
jobset.PathwaysJobSet(
124+
name="advanced-workload",
125+
namespace="default",
126+
pathways_dir="gs://my-bucket/scratch",
127+
tpu_type="v5e",
128+
topology="4x8",
129+
num_slices=1,
130+
)
131+
.add_colocated_python() # Injects colocated python sidecar to workers
132+
.add_gcsfuse(
133+
containers="all",
134+
mount_path="/data",
135+
bucket="my-data-bucket",
136+
read_only=True,
137+
)
138+
)
139+
js.export_yaml("advanced_workload.yaml")
140+
```
141+
142+
---
143+
144+
## API Reference: `PathwaysJobSet`
145+
146+
### Constructor `__init__`
147+
148+
```python
149+
def __init__(
150+
self,
151+
name: str,
152+
namespace: str,
153+
pathways_dir: str,
154+
tpu_type: str,
155+
topology: str,
156+
num_slices: int,
157+
max_restarts: int = 0,
158+
max_slice_restarts: int = 0,
159+
termination_grace_period_seconds: int | None = None,
160+
pathways_version: str = "latest",
161+
jobset_api_version: str = "v1alpha2",
162+
elastic_slices: int = 0,
163+
labels: Mapping[str, str] | None = None,
164+
annotations: Mapping[str, str] | None = None,
165+
shared_pathways_service: bool = False,
166+
)
167+
```
168+
169+
- `tpu_type`: Supported values include `"v5e"`, `"v5p"`, `"v6e"`, `"v4"`.
170+
- `elastic_slices`: If `> 0`, enables elasticity by passing
171+
`--num_elastic_slices` to the Pathways Proxy.
172+
- `shared_pathways_service`: If `True`, configures the head job to run only
173+
the Resource Manager (`pathways-rm`).
174+
175+
### Methods
176+
177+
- **`add_colocated_python()`**: Injects a colocated Python container and
178+
shared memory volume (`/tmp/shared-memory`) into worker pods.
179+
- **`add_gcsfuse(containers, mount_path, bucket, read_only=False)`**: Mounts
180+
a GCS bucket using GCSFuse CSI driver. `containers` can be `"head"`,
181+
`"worker"`, or `"all"`.
182+
- **`to_dict()`**: Returns the compiled K8s JobSet resource as a Python
183+
dictionary.
184+
- **`export_yaml(filepath)`**: Serializes and writes the JobSet to a YAML
185+
file.
186+
- **`apply(project_id, region, cluster_id)`**: Deploys the JobSet to the
187+
specified GKE cluster. Supports delete-and-recreate lifecycle if the JobSet
188+
already exists.

pathwaysutils/experimental/gke/jobset.py

Lines changed: 13 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -93,8 +93,6 @@ def __init__(
9393
tpu_type: str,
9494
topology: str,
9595
num_slices: int,
96-
user_pod_template: Mapping[str, Any] | None = None,
97-
main_container_name: str = "main",
9896
max_restarts: int = 0,
9997
max_slice_restarts: int = 0,
10098
termination_grace_period_seconds: int | None = None,
@@ -114,8 +112,6 @@ def __init__(
114112
tpu_type: TPU type (e.g., "v5e").
115113
topology: TPU topology (e.g., "2x2").
116114
num_slices: Number of slices.
117-
user_pod_template: Optional user pod template for the head job.
118-
main_container_name: Name of the main container in user_pod_template.
119115
max_restarts: Maximum number of restarts for the JobSet.
120116
max_slice_restarts: Maximum number of slice restarts.
121117
termination_grace_period_seconds: Optional termination grace period.
@@ -124,12 +120,8 @@ def __init__(
124120
elastic_slices: Number of elastic slices.
125121
labels: Optional labels for the JobSet.
126122
annotations: Optional annotations for the JobSet.
123+
shared_pathways_service: Whether to run only RM for Shared Pathways Service.
127124
"""
128-
if shared_pathways_service and user_pod_template:
129-
raise ValueError(
130-
"Cannot enable shared_pathways_service when user_pod_template is"
131-
" provided."
132-
)
133125
self._shared_pathways_service = shared_pathways_service
134126

135127
self._name = name
@@ -166,8 +158,6 @@ def __init__(
166158
num_slices=num_slices,
167159
instance_type=instance_type,
168160
image_tag=image_tag,
169-
user_pod_template=user_pod_template,
170-
main_container_name=main_container_name,
171161
elastic_slices=elastic_slices,
172162
shared_pathways_service=shared_pathways_service,
173163
)
@@ -185,7 +175,7 @@ def __init__(
185175
)
186176

187177
self._success_policy = None
188-
if user_pod_template or shared_pathways_service:
178+
if shared_pathways_service:
189179
self._success_policy = {
190180
"operator": "All",
191181
"targetReplicatedJobs": [PATHWAYS_HEAD_JOB_NAME],
@@ -205,8 +195,6 @@ def _build_head_job_template(
205195
num_slices: int,
206196
instance_type: str,
207197
image_tag: str,
208-
user_pod_template: Mapping[str, Any] | None,
209-
main_container_name: str,
210198
elastic_slices: int,
211199
shared_pathways_service: bool,
212200
) -> client.V1JobTemplateSpec:
@@ -217,9 +205,8 @@ def _build_head_job_template(
217205
num_slices: Number of slices.
218206
instance_type: TPU instance type (e.g., "tpuv5:2x2").
219207
image_tag: Version tag for Pathways images.
220-
user_pod_template: Optional user pod template for the head job.
221-
main_container_name: Name of the main container in user_pod_template.
222208
elastic_slices: Number of elastic slices.
209+
shared_pathways_service: Whether to run only RM for Shared Pathways Service.
223210
224211
Returns:
225212
The head job template.
@@ -318,74 +305,20 @@ def _build_head_job_template(
318305
),
319306
)
320307

321-
api_client = client.ApiClient()
322-
323-
if user_pod_template:
324-
user_template_obj = _deserialize_dict(
325-
api_client, user_pod_template, client.V1PodTemplateSpec
326-
)
327-
head_pod_spec = user_template_obj.spec
328-
head_pod_spec.host_network = True
329-
head_pod_spec.dns_policy = "ClusterFirstWithHostNet"
330-
331-
rm_container.restart_policy = "Always" # pyrefly: ignore[missing-attribute]
332-
proxy_container.restart_policy = "Always" # pyrefly: ignore[missing-attribute]
333-
334-
init_containers = head_pod_spec.init_containers or []
335-
init_containers.extend([rm_container, proxy_container])
336-
head_pod_spec.init_containers = init_containers
337-
338-
# Inject JAX env vars into main container.
339-
jax_env = [
340-
client.V1EnvVar(
341-
name="PATHWAYS_HEAD",
342-
value_from=client.V1EnvVarSource(
343-
field_ref=client.V1ObjectFieldSelector(
344-
field_path=(
345-
"metadata.labels['jobset.sigs.k8s.io/coordinator']"
346-
)
347-
)
348-
),
349-
),
350-
client.V1EnvVar(name="JAX_PLATFORMS", value="proxy"),
351-
client.V1EnvVar(name="XCLOUD_ENVIRONMENT", value="GCP"),
352-
client.V1EnvVar(
353-
name="JAX_BACKEND_TARGET",
354-
value=f"grpc://$(PATHWAYS_HEAD):{PATHWAYS_PROXY_PORT}",
355-
),
356-
]
357-
containers = head_pod_spec.containers or []
358-
for c in containers:
359-
if c.name == main_container_name:
360-
env = c.env or []
361-
env.extend(jax_env)
362-
c.env = env
363-
break
364-
head_pod_spec.containers = containers
365-
366-
annotations = user_pod_template.get("metadata", {}).get("annotations", {})
367-
labels = user_pod_template.get("metadata", {}).get("labels", {})
368-
else:
369-
# Headless mode.
370-
containers = [rm_container]
371-
if not shared_pathways_service:
372-
containers.append(proxy_container)
373-
head_pod_spec = client.V1PodSpec(
374-
host_network=True,
375-
dns_policy="ClusterFirstWithHostNet",
376-
containers=containers,
377-
)
378-
annotations = {}
379-
labels = {}
308+
containers = [rm_container]
309+
if not shared_pathways_service:
310+
containers.append(proxy_container)
380311

381-
if not head_pod_spec.restart_policy:
382-
head_pod_spec.restart_policy = "Never"
312+
head_pod_spec = client.V1PodSpec(
313+
host_network=True,
314+
dns_policy="ClusterFirstWithHostNet",
315+
containers=containers,
316+
restart_policy="Never",
317+
)
383318

384-
# Default annotations
385319
job_annotations = {
386320
"alpha.jobset.sigs.k8s.io/exclusive-topology": "kubernetes.io/hostname"
387321
}
388-
job_annotations.update(annotations)
389322

390323
head_job_template = client.V1JobTemplateSpec(
391324
metadata=client.V1ObjectMeta(annotations=job_annotations),
@@ -396,7 +329,7 @@ def _build_head_job_template(
396329
parallelism=1,
397330
template=client.V1PodTemplateSpec(
398331
metadata=client.V1ObjectMeta(
399-
annotations=job_annotations, labels=labels
332+
annotations=job_annotations, labels={}
400333
),
401334
spec=head_pod_spec,
402335
),

0 commit comments

Comments
 (0)