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.
- {pathwaysutils-0.1.8 → pathwaysutils-0.1.9}/PKG-INFO +2 -2
- {pathwaysutils-0.1.8 → pathwaysutils-0.1.9}/pathwaysutils/__init__.py +1 -1
- {pathwaysutils-0.1.8 → pathwaysutils-0.1.9}/pathwaysutils/elastic/elastic.py +84 -72
- pathwaysutils-0.1.9/pathwaysutils/experimental/gke/__init__.py +11 -0
- pathwaysutils-0.1.9/pathwaysutils/experimental/gke/jobset.py +414 -0
- {pathwaysutils-0.1.8 → pathwaysutils-0.1.9}/pathwaysutils/experimental/shared_pathways_service/deploy_pathways_service.py +2 -2
- {pathwaysutils-0.1.8 → pathwaysutils-0.1.9}/pathwaysutils/experimental/shared_pathways_service/gke_utils.py +165 -39
- {pathwaysutils-0.1.8 → pathwaysutils-0.1.9}/pathwaysutils/experimental/shared_pathways_service/isc_pathways.py +35 -5
- {pathwaysutils-0.1.8 → pathwaysutils-0.1.9}/pathwaysutils/experimental/shared_pathways_service/run_connect_example.py +4 -4
- {pathwaysutils-0.1.8 → pathwaysutils-0.1.9}/pathwaysutils/experimental/shared_pathways_service/run_workload.py +4 -16
- pathwaysutils-0.1.9/pathwaysutils/experimental/shared_pathways_service/start_vscode_on_cpu_np.py +162 -0
- {pathwaysutils-0.1.8 → pathwaysutils-0.1.9}/pathwaysutils/experimental/shared_pathways_service/validators.py +30 -2
- pathwaysutils-0.1.9/pathwaysutils/experimental/shared_pathways_service/yamls/code-server.yaml +58 -0
- {pathwaysutils-0.1.8 → pathwaysutils-0.1.9}/pathwaysutils/experimental/shared_pathways_service/yamls/pw-proxy.yaml +1 -1
- {pathwaysutils-0.1.8 → pathwaysutils-0.1.9}/pathwaysutils/jax/__init__.py +1 -1
- {pathwaysutils-0.1.8 → pathwaysutils-0.1.9}/pathwaysutils/persistence/helper.py +0 -28
- {pathwaysutils-0.1.8 → pathwaysutils-0.1.9}/pathwaysutils/persistence/orbax_handler.py +3 -3
- {pathwaysutils-0.1.8 → pathwaysutils-0.1.9}/pathwaysutils/profiling.py +101 -41
- pathwaysutils-0.1.9/pathwaysutils/test/experimental/gke/jobset_test.py +137 -0
- {pathwaysutils-0.1.8 → pathwaysutils-0.1.9}/pathwaysutils/test/experimental/reshard_test.py +15 -3
- {pathwaysutils-0.1.8 → pathwaysutils-0.1.9}/pathwaysutils/test/profiling_test.py +128 -40
- {pathwaysutils-0.1.8 → pathwaysutils-0.1.9}/pathwaysutils/test/reshard_test.py +10 -2
- {pathwaysutils-0.1.8 → pathwaysutils-0.1.9}/pyproject.toml +1 -1
- {pathwaysutils-0.1.8 → pathwaysutils-0.1.9}/LICENSE +0 -0
- {pathwaysutils-0.1.8 → pathwaysutils-0.1.9}/README.md +0 -0
- {pathwaysutils-0.1.8 → pathwaysutils-0.1.9}/pathwaysutils/_initialize.py +0 -0
- {pathwaysutils-0.1.8 → pathwaysutils-0.1.9}/pathwaysutils/collect_profile.py +0 -0
- {pathwaysutils-0.1.8 → pathwaysutils-0.1.9}/pathwaysutils/debug/__init__.py +0 -0
- {pathwaysutils-0.1.8 → pathwaysutils-0.1.9}/pathwaysutils/debug/timing.py +0 -0
- {pathwaysutils-0.1.8 → pathwaysutils-0.1.9}/pathwaysutils/debug/watchdog.py +0 -0
- {pathwaysutils-0.1.8 → pathwaysutils-0.1.9}/pathwaysutils/elastic/__init__.py +0 -0
- {pathwaysutils-0.1.8 → pathwaysutils-0.1.9}/pathwaysutils/elastic/manager.py +0 -0
- {pathwaysutils-0.1.8 → pathwaysutils-0.1.9}/pathwaysutils/experimental/__init__.py +0 -0
- {pathwaysutils-0.1.8 → pathwaysutils-0.1.9}/pathwaysutils/experimental/concatenate_by_mesh_axis.py +0 -0
- {pathwaysutils-0.1.8 → pathwaysutils-0.1.9}/pathwaysutils/experimental/profiling.py +0 -0
- {pathwaysutils-0.1.8 → pathwaysutils-0.1.9}/pathwaysutils/experimental/reshard.py +0 -0
- {pathwaysutils-0.1.8 → pathwaysutils-0.1.9}/pathwaysutils/experimental/shared_pathways_service/README.md +0 -0
- {pathwaysutils-0.1.8 → pathwaysutils-0.1.9}/pathwaysutils/experimental/shared_pathways_service/__init__.py +0 -0
- {pathwaysutils-0.1.8 → pathwaysutils-0.1.9}/pathwaysutils/experimental/shared_pathways_service/metrics_collector.py +0 -0
- {pathwaysutils-0.1.8 → pathwaysutils-0.1.9}/pathwaysutils/experimental/shared_pathways_service/yamls/pw-service.yaml +0 -0
- {pathwaysutils-0.1.8 → pathwaysutils-0.1.9}/pathwaysutils/experimental/split_by_mesh_axis.py +0 -0
- {pathwaysutils-0.1.8 → pathwaysutils-0.1.9}/pathwaysutils/lru_cache.py +0 -0
- {pathwaysutils-0.1.8 → pathwaysutils-0.1.9}/pathwaysutils/persistence/__init__.py +0 -0
- {pathwaysutils-0.1.8 → pathwaysutils-0.1.9}/pathwaysutils/plugin_executable.py +0 -0
- {pathwaysutils-0.1.8 → pathwaysutils-0.1.9}/pathwaysutils/proxy_backend.py +0 -0
- {pathwaysutils-0.1.8 → pathwaysutils-0.1.9}/pathwaysutils/reshard.py +0 -0
- {pathwaysutils-0.1.8 → pathwaysutils-0.1.9}/pathwaysutils/test/__init__.py +0 -0
- {pathwaysutils-0.1.8 → pathwaysutils-0.1.9}/pathwaysutils/test/debug/__init__.py +0 -0
- {pathwaysutils-0.1.8 → pathwaysutils-0.1.9}/pathwaysutils/test/debug/timing_test.py +0 -0
- {pathwaysutils-0.1.8 → pathwaysutils-0.1.9}/pathwaysutils/test/debug/watchdog_test.py +0 -0
- {pathwaysutils-0.1.8 → pathwaysutils-0.1.9}/pathwaysutils/test/initialize_test.py +0 -0
- {pathwaysutils-0.1.8 → pathwaysutils-0.1.9}/pathwaysutils/test/lru_cache_test.py +0 -0
- {pathwaysutils-0.1.8 → pathwaysutils-0.1.9}/pathwaysutils/test/persistence_test.py +0 -0
- {pathwaysutils-0.1.8 → pathwaysutils-0.1.9}/pathwaysutils/test/plugin_executable_test.py +0 -0
- {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.
|
|
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.
|
|
13
|
+
Requires-Dist: jax>=0.8.3
|
|
14
14
|
Requires-Dist: orbax-checkpoint
|
|
15
15
|
Requires-Dist: uvicorn
|
|
16
16
|
Requires-Dist: requests
|
|
@@ -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
|
-
|
|
45
|
-
"""
|
|
47
|
+
class SliceHealthChecker(ABC):
|
|
48
|
+
"""Base class for slice health checkers.
|
|
46
49
|
|
|
47
|
-
|
|
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
|
-
|
|
50
|
-
|
|
55
|
+
def __init__(self, slice_to_devices: Mapping[int, Sequence[jax.Device]]):
|
|
56
|
+
self.slice_to_devices = slice_to_devices
|
|
51
57
|
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
|
|
59
|
-
|
|
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
|
-
|
|
66
|
-
|
|
71
|
+
class DefaultSliceHealthChecker(SliceHealthChecker):
|
|
72
|
+
"""Default implementation that checks each slice individually.
|
|
67
73
|
|
|
68
|
-
|
|
69
|
-
|
|
74
|
+
It executes a simple computation on each slice independently to verify
|
|
75
|
+
that the devices are active and responsive.
|
|
70
76
|
|
|
71
|
-
|
|
72
|
-
|
|
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
|
-
|
|
78
|
-
|
|
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
|
-
|
|
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="
|
|
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="
|
|
116
|
+
accelerator_label="tpu7x",
|
|
117
117
|
instance_prefix="tpu7x",
|
|
118
118
|
),
|
|
119
119
|
}
|