pathwaysutils 0.1.8__tar.gz → 0.1.9__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. {pathwaysutils-0.1.8 → pathwaysutils-0.1.9}/PKG-INFO +2 -2
  2. {pathwaysutils-0.1.8 → pathwaysutils-0.1.9}/pathwaysutils/__init__.py +1 -1
  3. {pathwaysutils-0.1.8 → pathwaysutils-0.1.9}/pathwaysutils/elastic/elastic.py +84 -72
  4. pathwaysutils-0.1.9/pathwaysutils/experimental/gke/__init__.py +11 -0
  5. pathwaysutils-0.1.9/pathwaysutils/experimental/gke/jobset.py +414 -0
  6. {pathwaysutils-0.1.8 → pathwaysutils-0.1.9}/pathwaysutils/experimental/shared_pathways_service/deploy_pathways_service.py +2 -2
  7. {pathwaysutils-0.1.8 → pathwaysutils-0.1.9}/pathwaysutils/experimental/shared_pathways_service/gke_utils.py +165 -39
  8. {pathwaysutils-0.1.8 → pathwaysutils-0.1.9}/pathwaysutils/experimental/shared_pathways_service/isc_pathways.py +35 -5
  9. {pathwaysutils-0.1.8 → pathwaysutils-0.1.9}/pathwaysutils/experimental/shared_pathways_service/run_connect_example.py +4 -4
  10. {pathwaysutils-0.1.8 → pathwaysutils-0.1.9}/pathwaysutils/experimental/shared_pathways_service/run_workload.py +4 -16
  11. pathwaysutils-0.1.9/pathwaysutils/experimental/shared_pathways_service/start_vscode_on_cpu_np.py +162 -0
  12. {pathwaysutils-0.1.8 → pathwaysutils-0.1.9}/pathwaysutils/experimental/shared_pathways_service/validators.py +30 -2
  13. pathwaysutils-0.1.9/pathwaysutils/experimental/shared_pathways_service/yamls/code-server.yaml +58 -0
  14. {pathwaysutils-0.1.8 → pathwaysutils-0.1.9}/pathwaysutils/experimental/shared_pathways_service/yamls/pw-proxy.yaml +1 -1
  15. {pathwaysutils-0.1.8 → pathwaysutils-0.1.9}/pathwaysutils/jax/__init__.py +1 -1
  16. {pathwaysutils-0.1.8 → pathwaysutils-0.1.9}/pathwaysutils/persistence/helper.py +0 -28
  17. {pathwaysutils-0.1.8 → pathwaysutils-0.1.9}/pathwaysutils/persistence/orbax_handler.py +3 -3
  18. {pathwaysutils-0.1.8 → pathwaysutils-0.1.9}/pathwaysutils/profiling.py +101 -41
  19. pathwaysutils-0.1.9/pathwaysutils/test/experimental/gke/jobset_test.py +137 -0
  20. {pathwaysutils-0.1.8 → pathwaysutils-0.1.9}/pathwaysutils/test/experimental/reshard_test.py +15 -3
  21. {pathwaysutils-0.1.8 → pathwaysutils-0.1.9}/pathwaysutils/test/profiling_test.py +128 -40
  22. {pathwaysutils-0.1.8 → pathwaysutils-0.1.9}/pathwaysutils/test/reshard_test.py +10 -2
  23. {pathwaysutils-0.1.8 → pathwaysutils-0.1.9}/pyproject.toml +1 -1
  24. {pathwaysutils-0.1.8 → pathwaysutils-0.1.9}/LICENSE +0 -0
  25. {pathwaysutils-0.1.8 → pathwaysutils-0.1.9}/README.md +0 -0
  26. {pathwaysutils-0.1.8 → pathwaysutils-0.1.9}/pathwaysutils/_initialize.py +0 -0
  27. {pathwaysutils-0.1.8 → pathwaysutils-0.1.9}/pathwaysutils/collect_profile.py +0 -0
  28. {pathwaysutils-0.1.8 → pathwaysutils-0.1.9}/pathwaysutils/debug/__init__.py +0 -0
  29. {pathwaysutils-0.1.8 → pathwaysutils-0.1.9}/pathwaysutils/debug/timing.py +0 -0
  30. {pathwaysutils-0.1.8 → pathwaysutils-0.1.9}/pathwaysutils/debug/watchdog.py +0 -0
  31. {pathwaysutils-0.1.8 → pathwaysutils-0.1.9}/pathwaysutils/elastic/__init__.py +0 -0
  32. {pathwaysutils-0.1.8 → pathwaysutils-0.1.9}/pathwaysutils/elastic/manager.py +0 -0
  33. {pathwaysutils-0.1.8 → pathwaysutils-0.1.9}/pathwaysutils/experimental/__init__.py +0 -0
  34. {pathwaysutils-0.1.8 → pathwaysutils-0.1.9}/pathwaysutils/experimental/concatenate_by_mesh_axis.py +0 -0
  35. {pathwaysutils-0.1.8 → pathwaysutils-0.1.9}/pathwaysutils/experimental/profiling.py +0 -0
  36. {pathwaysutils-0.1.8 → pathwaysutils-0.1.9}/pathwaysutils/experimental/reshard.py +0 -0
  37. {pathwaysutils-0.1.8 → pathwaysutils-0.1.9}/pathwaysutils/experimental/shared_pathways_service/README.md +0 -0
  38. {pathwaysutils-0.1.8 → pathwaysutils-0.1.9}/pathwaysutils/experimental/shared_pathways_service/__init__.py +0 -0
  39. {pathwaysutils-0.1.8 → pathwaysutils-0.1.9}/pathwaysutils/experimental/shared_pathways_service/metrics_collector.py +0 -0
  40. {pathwaysutils-0.1.8 → pathwaysutils-0.1.9}/pathwaysutils/experimental/shared_pathways_service/yamls/pw-service.yaml +0 -0
  41. {pathwaysutils-0.1.8 → pathwaysutils-0.1.9}/pathwaysutils/experimental/split_by_mesh_axis.py +0 -0
  42. {pathwaysutils-0.1.8 → pathwaysutils-0.1.9}/pathwaysutils/lru_cache.py +0 -0
  43. {pathwaysutils-0.1.8 → pathwaysutils-0.1.9}/pathwaysutils/persistence/__init__.py +0 -0
  44. {pathwaysutils-0.1.8 → pathwaysutils-0.1.9}/pathwaysutils/plugin_executable.py +0 -0
  45. {pathwaysutils-0.1.8 → pathwaysutils-0.1.9}/pathwaysutils/proxy_backend.py +0 -0
  46. {pathwaysutils-0.1.8 → pathwaysutils-0.1.9}/pathwaysutils/reshard.py +0 -0
  47. {pathwaysutils-0.1.8 → pathwaysutils-0.1.9}/pathwaysutils/test/__init__.py +0 -0
  48. {pathwaysutils-0.1.8 → pathwaysutils-0.1.9}/pathwaysutils/test/debug/__init__.py +0 -0
  49. {pathwaysutils-0.1.8 → pathwaysutils-0.1.9}/pathwaysutils/test/debug/timing_test.py +0 -0
  50. {pathwaysutils-0.1.8 → pathwaysutils-0.1.9}/pathwaysutils/test/debug/watchdog_test.py +0 -0
  51. {pathwaysutils-0.1.8 → pathwaysutils-0.1.9}/pathwaysutils/test/initialize_test.py +0 -0
  52. {pathwaysutils-0.1.8 → pathwaysutils-0.1.9}/pathwaysutils/test/lru_cache_test.py +0 -0
  53. {pathwaysutils-0.1.8 → pathwaysutils-0.1.9}/pathwaysutils/test/persistence_test.py +0 -0
  54. {pathwaysutils-0.1.8 → pathwaysutils-0.1.9}/pathwaysutils/test/plugin_executable_test.py +0 -0
  55. {pathwaysutils-0.1.8 → pathwaysutils-0.1.9}/pathwaysutils/test/proxy_backend_test.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pathwaysutils
3
- Version: 0.1.8
3
+ Version: 0.1.9
4
4
  Summary: Package of Pathways-on-Cloud utilities.
5
5
  Keywords:
6
6
  Author-email: Pathways-on-Cloud Utilities Developers <pathwaysutils-dev@google.com>
@@ -10,7 +10,7 @@ Classifier: License :: OSI Approved :: Apache Software License
10
10
  License-File: LICENSE
11
11
  Requires-Dist: absl-py
12
12
  Requires-Dist: fastapi
13
- Requires-Dist: jax>=0.8.0
13
+ Requires-Dist: jax>=0.8.3
14
14
  Requires-Dist: orbax-checkpoint
15
15
  Requires-Dist: uvicorn
16
16
  Requires-Dist: requests
@@ -21,4 +21,4 @@ is_pathways_backend_used: Callable[[], bool] = _initialize.is_pathways_backend_u
21
21
  del _initialize
22
22
 
23
23
  # When changing this, also update the CHANGELOG.md.
24
- __version__ = "v0.1.8"
24
+ __version__ = "v0.1.9"
@@ -18,10 +18,13 @@ retries a function in case of `jax.errors.JaxRuntimeError` caused by slice down
18
18
  events. It also provides a utility for waiting for slices to become active.
19
19
  """
20
20
 
21
+ from abc import ABC, abstractmethod
21
22
  import collections
22
- from collections.abc import Mapping, Sequence, Set
23
+ from collections.abc import Callable, Mapping, Sequence, Set
24
+ import itertools
23
25
  import logging
24
26
  import time
27
+ from typing import Any
25
28
 
26
29
  import jax
27
30
  import numpy as np
@@ -41,44 +44,87 @@ _ELASTIC_DOWN_ADDITIONAL_ERROR_TYPES = frozenset([
41
44
  ])
42
45
 
43
46
 
44
- def _plus_one(x: jax.Array) -> jax.Array:
45
- """Adds one to each element in the array.
47
+ class SliceHealthChecker(ABC):
48
+ """Base class for slice health checkers.
46
49
 
47
- Used to test if a slice is active.
50
+ Implementations must provide `dispatch` and `validate` methods.
51
+ `dispatch` starts the health check operations, and `validate` waits for them
52
+ and returns the active slice indices.
53
+ """
48
54
 
49
- Args:
50
- x: The array to add one to.
55
+ def __init__(self, slice_to_devices: Mapping[int, Sequence[jax.Device]]):
56
+ self.slice_to_devices = slice_to_devices
51
57
 
52
- Returns:
53
- The array with one added to each element.
54
- """
55
- return x + 1
58
+ @abstractmethod
59
+ def dispatch(self) -> None:
60
+ """Dispatches the JAX operations in the background."""
56
61
 
62
+ @abstractmethod
63
+ def validate(self) -> Set[int]:
64
+ """Blocks on results and returns the set of active slice indices.
57
65
 
58
- def _simple_execution(devices: Sequence[jax.Device]) -> jax.Array:
59
- """Simple execution to test if a slice is active.
66
+ Returns:
67
+ Set of active slice indices.
68
+ """
60
69
 
61
- This function is used to test if a slice is active. It executes a simple
62
- computation on the devices and returns the result. If any of the devices are
63
- not active, the returned array will fail with a JaxRuntimeError used.
64
70
 
65
- Simply executing this function is not enough to determine if the slice is
66
- active. We also need to check the value of the returned array.
71
+ class DefaultSliceHealthChecker(SliceHealthChecker):
72
+ """Default implementation that checks each slice individually.
67
73
 
68
- Args:
69
- devices: The devices to execute on.
74
+ It executes a simple computation on each slice independently to verify
75
+ that the devices are active and responsive.
70
76
 
71
- Returns:
72
- The result of the execution.
77
+ The computation involves creating a zero array of the same size as the
78
+ number of devices in the slice, adding (_SIMPLE_EXECUTION_TEST_VALUE - 1)
79
+ to it, and then running a pmap to add 1 to each element. This verifies
80
+ that the devices are active and can perform computations.
73
81
  """
74
- if not devices:
75
- raise ValueError("No devices")
76
82
 
77
- test_input = np.zeros(len(devices), dtype=float) + (
78
- _SIMPLE_EXECUTION_TEST_VALUE - 1
79
- )
83
+ def _plus_one(self, x: jax.Array) -> jax.Array:
84
+ return x + 1
85
+
86
+ def _simple_execution(self, devices: Sequence[jax.Device]) -> jax.Array:
87
+ if not devices:
88
+ raise ValueError("No devices")
89
+
90
+ test_input = np.zeros(len(devices), dtype=float) + (
91
+ _SIMPLE_EXECUTION_TEST_VALUE - 1
92
+ )
93
+
94
+ return jax.pmap(self._plus_one, devices=devices)(test_input)
95
+
96
+ def dispatch(self) -> None:
97
+ self.results = {
98
+ slice_index: self._simple_execution(devices)
99
+ for slice_index, devices in self.slice_to_devices.items()
100
+ }
101
+
102
+ def validate(self) -> Set[int]:
103
+ active_slice_indices = set()
104
+ for slice_index, x in self.results.items():
105
+ expected = (
106
+ np.zeros(len(self.slice_to_devices[slice_index]), dtype=float)
107
+ + _SIMPLE_EXECUTION_TEST_VALUE
108
+ )
109
+ try:
110
+ jax.block_until_ready(x)
111
+ if np.allclose(x, expected):
112
+ active_slice_indices.add(slice_index)
113
+ else:
114
+ msg = (
115
+ f"Error with _simple_execution for slice_index={slice_index}. "
116
+ f"Expected: {expected!r}, Actual: {x!r}"
117
+ )
118
+ _logger.error(msg)
119
+ raise ValueError(msg)
120
+ except jax.errors.JaxRuntimeError as error:
121
+ _logger.debug(
122
+ "Caught JaxRuntimeError for slice_index=%s: %s", slice_index, error
123
+ )
124
+ if not is_error_due_to_slice_down(error):
125
+ raise
126
+ return active_slice_indices
80
127
 
81
- return jax.pmap(_plus_one, devices=devices)(test_input)
82
128
 
83
129
 
84
130
  def get_slice_to_devices(
@@ -94,6 +140,7 @@ def get_slice_to_devices(
94
140
  @timing.timeit
95
141
  def get_active_slice_indices(
96
142
  slice_to_devices: Mapping[int, Sequence[jax.Device]] | None = None,
143
+ checker: SliceHealthChecker | None = None,
97
144
  ) -> Set[int]:
98
145
  """Returns the set of active slices indices.
99
146
 
@@ -101,6 +148,8 @@ def get_active_slice_indices(
101
148
  slice_to_devices: A mapping from slice index to devices. If None,
102
149
  `get_slice_to_devices(jax.devices())` is used to gather all available
103
150
  devices and group them by slice.
151
+ checker: A SliceHealthChecker instance. If None, DefaultSliceHealthChecker
152
+ is used.
104
153
 
105
154
  Returns:
106
155
  A set of integers representing the indices of the active slices.
@@ -109,55 +158,16 @@ def get_active_slice_indices(
109
158
  _logger.debug("slice_to_devices is None. Getting from jax.devices().")
110
159
  slice_to_devices = get_slice_to_devices(tuple(jax.devices()))
111
160
 
161
+ if checker is None:
162
+ checker = DefaultSliceHealthChecker(slice_to_devices)
163
+
112
164
  _logger.debug(
113
165
  "Getting active slice indices for slices: %s",
114
166
  sorted(list(slice_to_devices.keys())),
115
167
  )
116
168
 
117
- active_slice_indices = set()
118
-
119
- results = {
120
- slice_index: _simple_execution(devices)
121
- for slice_index, devices in slice_to_devices.items()
122
- }
123
-
124
- for slice_index, x in results.items():
125
- _logger.debug("Checking slice_index=%s", slice_index)
126
- expected = (
127
- np.zeros(len(slice_to_devices[slice_index]), dtype=float)
128
- + _SIMPLE_EXECUTION_TEST_VALUE
129
- )
130
- try:
131
- with timing.Timer(f"Checking {slice_index=}"):
132
- _logger.debug("Blocking until ready for slice_index=%s", slice_index)
133
- jax.block_until_ready(x)
134
- _logger.debug("Execution finished for slice_index=%s", slice_index)
135
- if np.allclose(x, expected):
136
- active_slice_indices.add(slice_index)
137
- _logger.debug("slice_index=%s active", slice_index)
138
- else:
139
- _logger.error(
140
- "Error with _simple_execution for slice_index=%s. "
141
- "This should never happen. Expected: %r, Actual: %r",
142
- slice_index,
143
- expected,
144
- x,
145
- )
146
- raise ValueError(
147
- f"Error with _simple_execution for slice_index={slice_index}."
148
- )
149
- except jax.errors.JaxRuntimeError as error:
150
- _logger.debug(
151
- "Caught JaxRuntimeError for slice_index=%s: %s", slice_index, error
152
- )
153
- if not is_error_due_to_slice_down(error):
154
- _logger.info("Re-raising error for slice_index=%s", slice_index)
155
- raise
156
- _logger.debug("slice_index=%s bad", slice_index)
157
-
158
- _logger.debug("active_slice_indices=%s", active_slice_indices)
159
-
160
- return active_slice_indices
169
+ checker.dispatch()
170
+ return checker.validate()
161
171
 
162
172
 
163
173
  def wait_for_slices(
@@ -165,6 +175,7 @@ def wait_for_slices(
165
175
  poll_interval: float | int = 10,
166
176
  timeout: float | int | None = None,
167
177
  slice_to_devices: Mapping[int, Sequence[jax.Device]] | None = None,
178
+ checker: SliceHealthChecker | None = None,
168
179
  ) -> Set[int]:
169
180
  """Waits until after at least `slice_count` slices become active.
170
181
 
@@ -177,6 +188,7 @@ def wait_for_slices(
177
188
  timeout.
178
189
  slice_to_devices: A mapping from slice index to devices. If None,
179
190
  `get_slice_to_devices(jax.devices())` is used.
191
+ checker: A SliceHealthChecker instance to use for checking health.
180
192
 
181
193
  Returns:
182
194
  The active slice indices
@@ -201,7 +213,7 @@ def wait_for_slices(
201
213
  check_start_time = time.time()
202
214
 
203
215
  _logger.debug("Checking active slices...")
204
- active_slice_indices = get_active_slice_indices(slice_to_devices)
216
+ active_slice_indices = get_active_slice_indices(slice_to_devices, checker)
205
217
  if len(active_slice_indices) >= slice_count:
206
218
  _logger.info(
207
219
  "Sufficient slices active: %s >= %s. Active indices: %s",
@@ -0,0 +1,11 @@
1
+ # Licensed under the Apache License, Version 2.0 (the "License");
2
+ # you may not use this file except in compliance with the License.
3
+ # You may obtain a copy of the License at
4
+ #
5
+ # https://www.apache.org/licenses/LICENSE-2.0
6
+ #
7
+ # Unless required by applicable law or agreed to in writing, software
8
+ # distributed under the License is distributed on an "AS IS" BASIS,
9
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10
+ # See the License for the specific language governing permissions and
11
+ # limitations under the License.
@@ -0,0 +1,414 @@
1
+ # Licensed under the Apache License, Version 2.0 (the "License");
2
+ # you may not use this file except in compliance with the License.
3
+ # You may obtain a copy of the License at
4
+ #
5
+ # https://www.apache.org/licenses/LICENSE-2.0
6
+ #
7
+ # Unless required by applicable law or agreed to in writing, software
8
+ # distributed under the License is distributed on an "AS IS" BASIS,
9
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10
+ # See the License for the specific language governing permissions and
11
+ # limitations under the License.
12
+ """Pathways JobSet generator and builder (Head Job Config)."""
13
+
14
+ import json
15
+ import logging
16
+ from typing import Any, Mapping
17
+ from kubernetes import client
18
+
19
+ # GKE sidecar containers restartPolicy compatibility placeholder.
20
+
21
+ _logger = logging.getLogger(__name__)
22
+
23
+ # Core constants.
24
+ PATHWAYS_HEAD_JOB_NAME = "pathways-head"
25
+ PATHWAYS_WORKER_JOB_NAME = "pathways-worker"
26
+
27
+ DEFAULT_PATHWAYS_RM_AND_WORKER_IMAGE = (
28
+ "us-docker.pkg.dev/cloud-tpu-v2-images/pathways/server"
29
+ )
30
+ DEFAULT_PATHWAYS_PROXY_IMAGE = (
31
+ "us-docker.pkg.dev/cloud-tpu-v2-images/pathways/proxy_server"
32
+ )
33
+
34
+ PATHWAYS_PROXY_PORT = 29000
35
+ PATHWAYS_RM_PORT = 29001
36
+
37
+ MACHINE_TYPE_TO_TPU_VERSION_MAP = {
38
+ "tpu7x-standard-4t": "tpu7x",
39
+ "tpu7x": "tpu7x",
40
+ "ct6e-standard-4t": "tpuv6e",
41
+ "v6e": "tpuv6e",
42
+ "ct6e-standard-8t": "tpuv6e1t",
43
+ "ct5p-hightpu-4t": "tpuv5",
44
+ "v5p": "tpuv5",
45
+ "ct5lp-hightpu-4t": "tpuv5e",
46
+ "v5e": "tpuv5e",
47
+ "ct5lp-hightpu-8t": "tpuv5e1t",
48
+ "ct4p-hightpu-4t": "tpuv4",
49
+ "v4": "tpuv4",
50
+ }
51
+
52
+ MACHINE_TYPE_TO_GKE_ACCELERATOR_TYPE_MAP = {
53
+ "tpu7x-standard-4t": "tpu7x",
54
+ "tpu7x": "tpu7x",
55
+ "ct6e-standard-4t": "tpu-v6e-slice",
56
+ "v6e": "tpu-v6e-slice",
57
+ "ct6e-standard-8t": "tpu-v6e-slice",
58
+ "ct5p-hightpu-4t": "tpu-v5p-slice",
59
+ "v5p": "tpu-v5p-slice",
60
+ "ct5lp-hightpu-4t": "tpu-v5-lite-podslice",
61
+ "v5e": "tpu-v5-lite-podslice",
62
+ "ct5lp-hightpu-8t": "tpu-v5-lite-podslice",
63
+ "ct4p-hightpu-4t": "tpu-v4-podslice",
64
+ "v4": "tpu-v4-podslice",
65
+ }
66
+
67
+
68
+ def _deserialize_dict(
69
+ api_client: client.ApiClient, data_dict: Mapping[str, Any], klass: Any
70
+ ) -> Any:
71
+ class FakeResponse:
72
+
73
+ def __init__(self, data):
74
+ self.data = data
75
+
76
+ return api_client.deserialize(FakeResponse(json.dumps(data_dict)), klass)
77
+
78
+
79
+ class PathwaysJobSet:
80
+ """Generates JobSet configuration for Pathways (with Head Job Config)."""
81
+
82
+ def __init__(
83
+ self,
84
+ name: str,
85
+ namespace: str,
86
+ pathways_dir: str,
87
+ tpu_type: str,
88
+ topology: str,
89
+ num_slices: int,
90
+ user_pod_template: Mapping[str, Any] | None = None,
91
+ main_container_name: str = "main",
92
+ max_restarts: int = 0,
93
+ pathways_version: str = "latest",
94
+ jobset_api_version: str = "v1alpha2",
95
+ elastic_slices: int = 0,
96
+ labels: Mapping[str, str] | None = None,
97
+ annotations: Mapping[str, str] | None = None,
98
+ ):
99
+ """Initializes the instance.
100
+
101
+ Args:
102
+ name: Name of the JobSet.
103
+ namespace: Namespace of the JobSet.
104
+ pathways_dir: GCS path for Pathways scratch space.
105
+ tpu_type: TPU type (e.g., "v5e").
106
+ topology: TPU topology (e.g., "2x2").
107
+ num_slices: Number of slices.
108
+ user_pod_template: Optional user pod template for the head job.
109
+ main_container_name: Name of the main container in user_pod_template.
110
+ max_restarts: Maximum number of restarts for the JobSet.
111
+ pathways_version: Version tag for Pathways images.
112
+ jobset_api_version: API version of JobSet.
113
+ elastic_slices: Number of elastic slices.
114
+ labels: Optional labels for the JobSet.
115
+ annotations: Optional annotations for the JobSet.
116
+ """
117
+ self._name = name
118
+ self._namespace = namespace
119
+ self._jobset_api_version = jobset_api_version
120
+ self._max_restarts = max_restarts
121
+ self._worker_replicas = num_slices
122
+ self._labels = dict(labels) if labels else {}
123
+ self._annotations = dict(annotations) if annotations else {}
124
+
125
+ tpu_version = MACHINE_TYPE_TO_TPU_VERSION_MAP.get(tpu_type.lower())
126
+ if not tpu_version:
127
+ raise ValueError(f"Unsupported TPU type: {tpu_type}")
128
+
129
+ instance_type = f"{tpu_version}:{topology}"
130
+ image_tag = pathways_version
131
+
132
+ # Build head template.
133
+ self._head_job_template = self._build_head_job_template(
134
+ pathways_dir=pathways_dir,
135
+ num_slices=num_slices,
136
+ instance_type=instance_type,
137
+ image_tag=image_tag,
138
+ user_pod_template=user_pod_template,
139
+ main_container_name=main_container_name,
140
+ elastic_slices=elastic_slices,
141
+ )
142
+
143
+ # Build minimal worker template (placeholder)
144
+ self._worker_job_template = self._build_minimal_job_template("worker")
145
+
146
+ self._success_policy = None
147
+ if user_pod_template:
148
+ self._success_policy = {
149
+ "operator": "All",
150
+ "targetReplicatedJobs": [PATHWAYS_HEAD_JOB_NAME],
151
+ }
152
+
153
+ def _build_minimal_job_template(self, role: str) -> client.V1JobTemplateSpec:
154
+ """Builds a minimal job template for a given role."""
155
+ pod_spec = client.V1PodSpec(
156
+ containers=[
157
+ client.V1Container(name=f"placeholder-{role}", image="ubuntu")
158
+ ]
159
+ )
160
+ job_spec = client.V1JobSpec(
161
+ template=client.V1PodTemplateSpec(
162
+ metadata=client.V1ObjectMeta(labels={"role": role}), spec=pod_spec
163
+ )
164
+ )
165
+ return client.V1JobTemplateSpec(spec=job_spec)
166
+
167
+ def _build_head_job_template(
168
+ self,
169
+ pathways_dir: str,
170
+ num_slices: int,
171
+ instance_type: str,
172
+ image_tag: str,
173
+ user_pod_template: Mapping[str, Any] | None,
174
+ main_container_name: str,
175
+ elastic_slices: int,
176
+ ) -> client.V1JobTemplateSpec:
177
+ """Builds the head job template for the JobSet.
178
+
179
+ Args:
180
+ pathways_dir: GCS path for Pathways scratch space.
181
+ num_slices: Number of slices.
182
+ instance_type: TPU instance type (e.g., "tpuv5:2x2").
183
+ image_tag: Version tag for Pathways images.
184
+ user_pod_template: Optional user pod template for the head job.
185
+ main_container_name: Name of the main container in user_pod_template.
186
+ elastic_slices: Number of elastic slices.
187
+
188
+ Returns:
189
+ The head job template.
190
+ """
191
+ rm_image = f"{DEFAULT_PATHWAYS_RM_AND_WORKER_IMAGE}:{image_tag}"
192
+ proxy_image = f"{DEFAULT_PATHWAYS_PROXY_IMAGE}:{image_tag}"
193
+
194
+ rm_args = [
195
+ f"--server_port={PATHWAYS_RM_PORT}",
196
+ f"--gcs_scratch_location={pathways_dir}",
197
+ "--node_type=resource_manager",
198
+ f"--instance_count={num_slices}",
199
+ f"--instance_type={instance_type}",
200
+ ]
201
+ rm_env = [
202
+ client.V1EnvVar(
203
+ name="REPLICATED_JOB_NAME",
204
+ value_from=client.V1EnvVarSource(
205
+ field_ref=client.V1ObjectFieldSelector(
206
+ field_path="metadata.annotations['jobset.sigs.k8s.io/replicatedjob-name']"
207
+ )
208
+ ),
209
+ ),
210
+ client.V1EnvVar(
211
+ name="JOBSET_NAME",
212
+ value_from=client.V1EnvVarSource(
213
+ field_ref=client.V1ObjectFieldSelector(
214
+ field_path=(
215
+ "metadata.annotations['jobset.sigs.k8s.io/jobset-name']"
216
+ )
217
+ )
218
+ ),
219
+ ),
220
+ client.V1EnvVar(
221
+ name="HOST_ADDRESS",
222
+ value_from=client.V1EnvVarSource(
223
+ field_ref=client.V1ObjectFieldSelector(
224
+ field_path=(
225
+ "metadata.labels['jobset.sigs.k8s.io/coordinator']"
226
+ )
227
+ )
228
+ ),
229
+ ),
230
+ client.V1EnvVar(name="TPU_SKIP_MDS_QUERY", value="true"),
231
+ ]
232
+ rm_container = client.V1Container(
233
+ name="pathways-rm",
234
+ image=rm_image,
235
+ image_pull_policy="Always",
236
+ args=rm_args,
237
+ env=rm_env,
238
+ ports=[
239
+ client.V1ContainerPort(
240
+ container_port=PATHWAYS_RM_PORT, protocol="TCP"
241
+ ),
242
+ client.V1ContainerPort(container_port=29002, protocol="TCP"),
243
+ ],
244
+ resources=client.V1ResourceRequirements(
245
+ limits={"cpu": "8", "memory": "32G"}
246
+ ),
247
+ )
248
+
249
+ proxy_args = [
250
+ f"--server_port={PATHWAYS_PROXY_PORT}",
251
+ f"--resource_manager_address=$(PATHWAYS_HEAD):{PATHWAYS_RM_PORT}",
252
+ f"--gcs_scratch_location={pathways_dir}",
253
+ ]
254
+ if elastic_slices > 0:
255
+ proxy_args.append(f"--num_elastic_slices={elastic_slices}")
256
+
257
+ proxy_env = [
258
+ client.V1EnvVar(
259
+ name="PATHWAYS_HEAD",
260
+ value_from=client.V1EnvVarSource(
261
+ field_ref=client.V1ObjectFieldSelector(
262
+ field_path=(
263
+ "metadata.labels['jobset.sigs.k8s.io/coordinator']"
264
+ )
265
+ )
266
+ ),
267
+ )
268
+ ]
269
+ proxy_container = client.V1Container(
270
+ name="pathways-proxy",
271
+ image=proxy_image,
272
+ image_pull_policy="Always",
273
+ args=proxy_args,
274
+ env=proxy_env,
275
+ ports=[
276
+ client.V1ContainerPort(
277
+ container_port=PATHWAYS_PROXY_PORT, protocol="TCP"
278
+ )
279
+ ],
280
+ resources=client.V1ResourceRequirements(
281
+ limits={"cpu": "16", "memory": "100G"}
282
+ ),
283
+ )
284
+
285
+ api_client = client.ApiClient()
286
+
287
+ if user_pod_template:
288
+ user_template_obj = _deserialize_dict(
289
+ api_client, user_pod_template, client.V1PodTemplateSpec
290
+ )
291
+ head_pod_spec = user_template_obj.spec
292
+ head_pod_spec.host_network = True
293
+ head_pod_spec.dns_policy = "ClusterFirstWithHostNet"
294
+
295
+ rm_container.restart_policy = "Always"
296
+ proxy_container.restart_policy = "Always"
297
+
298
+ init_containers = head_pod_spec.init_containers or []
299
+ init_containers.extend([rm_container, proxy_container])
300
+ head_pod_spec.init_containers = init_containers
301
+
302
+ # Inject JAX env vars into main container.
303
+ jax_env = [
304
+ client.V1EnvVar(
305
+ name="PATHWAYS_HEAD",
306
+ value_from=client.V1EnvVarSource(
307
+ field_ref=client.V1ObjectFieldSelector(
308
+ field_path=(
309
+ "metadata.labels['jobset.sigs.k8s.io/coordinator']"
310
+ )
311
+ )
312
+ ),
313
+ ),
314
+ client.V1EnvVar(name="JAX_PLATFORMS", value="proxy"),
315
+ client.V1EnvVar(name="XCLOUD_ENVIRONMENT", value="GCP"),
316
+ client.V1EnvVar(
317
+ name="JAX_BACKEND_TARGET",
318
+ value=f"grpc://$(PATHWAYS_HEAD):{PATHWAYS_PROXY_PORT}",
319
+ ),
320
+ ]
321
+ containers = head_pod_spec.containers or []
322
+ for c in containers:
323
+ if c.name == main_container_name:
324
+ env = c.env or []
325
+ env.extend(jax_env)
326
+ c.env = env
327
+ break
328
+ head_pod_spec.containers = containers
329
+
330
+ annotations = user_pod_template.get("metadata", {}).get("annotations", {})
331
+ labels = user_pod_template.get("metadata", {}).get("labels", {})
332
+ else:
333
+ # Headless mode.
334
+ head_pod_spec = client.V1PodSpec(
335
+ host_network=True,
336
+ dns_policy="ClusterFirstWithHostNet",
337
+ containers=[rm_container, proxy_container],
338
+ )
339
+ annotations = {}
340
+ labels = {}
341
+
342
+ if not head_pod_spec.restart_policy:
343
+ head_pod_spec.restart_policy = "Never"
344
+
345
+ # Default annotations
346
+ job_annotations = {
347
+ "alpha.jobset.sigs.k8s.io/exclusive-topology": "kubernetes.io/hostname"
348
+ }
349
+ job_annotations.update(annotations)
350
+
351
+ head_job_template = client.V1JobTemplateSpec(
352
+ metadata=client.V1ObjectMeta(annotations=job_annotations),
353
+ spec=client.V1JobSpec(
354
+ backoff_limit=0,
355
+ completion_mode="Indexed",
356
+ completions=1,
357
+ parallelism=1,
358
+ template=client.V1PodTemplateSpec(
359
+ metadata=client.V1ObjectMeta(
360
+ annotations=job_annotations, labels=labels
361
+ ),
362
+ spec=head_pod_spec,
363
+ ),
364
+ ),
365
+ )
366
+ return head_job_template
367
+
368
+ def _compile_config(self) -> dict[str, Any]:
369
+ """Compiles the JobSet configuration into a dictionary."""
370
+ with client.ApiClient() as api_client:
371
+ serialized_head = api_client.sanitize_for_serialization(
372
+ self._head_job_template
373
+ )
374
+ serialized_worker = api_client.sanitize_for_serialization(
375
+ self._worker_job_template
376
+ )
377
+
378
+ replicated_jobs = [
379
+ {
380
+ "name": PATHWAYS_HEAD_JOB_NAME,
381
+ "replicas": 1,
382
+ "template": serialized_head,
383
+ },
384
+ {
385
+ "name": PATHWAYS_WORKER_JOB_NAME,
386
+ "replicas": self._worker_replicas,
387
+ "template": serialized_worker,
388
+ },
389
+ ]
390
+
391
+ jobset_config = {
392
+ "apiVersion": f"jobset.sigs.k8s.io/{self._jobset_api_version}",
393
+ "kind": "JobSet",
394
+ "metadata": {
395
+ "name": self._name,
396
+ "namespace": self._namespace,
397
+ },
398
+ "spec": {
399
+ "failurePolicy": {"maxRestarts": self._max_restarts},
400
+ "replicatedJobs": replicated_jobs,
401
+ },
402
+ }
403
+ if self._labels:
404
+ jobset_config["metadata"]["labels"] = self._labels
405
+ if self._annotations:
406
+ jobset_config["metadata"]["annotations"] = self._annotations
407
+ if self._success_policy:
408
+ jobset_config["spec"]["successPolicy"] = self._success_policy
409
+
410
+ return jobset_config
411
+
412
+ def to_dict(self) -> dict[str, Any]:
413
+ """Returns the JobSet configuration as a dictionary."""
414
+ return self._compile_config()
@@ -102,7 +102,7 @@ def get_tpu_config(tpu_type: str) -> TPUConfig:
102
102
  machine_type="ct5p-hightpu-4t",
103
103
  chips_per_vm=4,
104
104
  accelerator_label="tpu-v5p-slice",
105
- instance_prefix="tpuv5p",
105
+ instance_prefix="tpuv5",
106
106
  ),
107
107
  "v6e": TPUConfig(
108
108
  machine_type="ct6e-standard-4t",
@@ -113,7 +113,7 @@ def get_tpu_config(tpu_type: str) -> TPUConfig:
113
113
  "tpu7x": TPUConfig(
114
114
  machine_type="tpu7x-standard-4t",
115
115
  chips_per_vm=4,
116
- accelerator_label="tpu-v7-slice",
116
+ accelerator_label="tpu7x",
117
117
  instance_prefix="tpu7x",
118
118
  ),
119
119
  }