pathwaysutils 0.1.9__tar.gz → 0.1.10__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.9 → pathwaysutils-0.1.10}/PKG-INFO +1 -1
- {pathwaysutils-0.1.9 → pathwaysutils-0.1.10}/pathwaysutils/__init__.py +1 -1
- {pathwaysutils-0.1.9 → pathwaysutils-0.1.10}/pathwaysutils/elastic/manager.py +65 -20
- {pathwaysutils-0.1.9 → pathwaysutils-0.1.10}/pathwaysutils/experimental/gke/jobset.py +183 -18
- pathwaysutils-0.1.10/pathwaysutils/experimental/reshard.py +218 -0
- {pathwaysutils-0.1.9 → pathwaysutils-0.1.10}/pathwaysutils/experimental/shared_pathways_service/deploy_pathways_service.py +10 -0
- pathwaysutils-0.1.10/pathwaysutils/experimental/shared_pathways_service/dockerfiles/sample_sidecar.dockerfile +27 -0
- {pathwaysutils-0.1.9 → pathwaysutils-0.1.10}/pathwaysutils/experimental/shared_pathways_service/gke_utils.py +56 -0
- {pathwaysutils-0.1.9 → pathwaysutils-0.1.10}/pathwaysutils/experimental/shared_pathways_service/isc_pathways.py +38 -17
- {pathwaysutils-0.1.9 → pathwaysutils-0.1.10}/pathwaysutils/experimental/shared_pathways_service/metrics_collector.py +28 -6
- {pathwaysutils-0.1.9 → pathwaysutils-0.1.10}/pathwaysutils/experimental/shared_pathways_service/run_workload.py +2 -4
- {pathwaysutils-0.1.9 → pathwaysutils-0.1.10}/pathwaysutils/experimental/shared_pathways_service/validators.py +99 -0
- {pathwaysutils-0.1.9 → pathwaysutils-0.1.10}/pathwaysutils/experimental/shared_pathways_service/yamls/pw-service.yaml +45 -2
- {pathwaysutils-0.1.9 → pathwaysutils-0.1.10}/pathwaysutils/profiling.py +5 -2
- {pathwaysutils-0.1.9/pathwaysutils/experimental → pathwaysutils-0.1.10/pathwaysutils}/reshard.py +78 -158
- {pathwaysutils-0.1.9 → pathwaysutils-0.1.10}/pathwaysutils/test/experimental/gke/jobset_test.py +74 -1
- {pathwaysutils-0.1.9 → pathwaysutils-0.1.10}/pathwaysutils/test/profiling_test.py +24 -6
- pathwaysutils-0.1.9/pathwaysutils/reshard.py +0 -133
- {pathwaysutils-0.1.9 → pathwaysutils-0.1.10}/LICENSE +0 -0
- {pathwaysutils-0.1.9 → pathwaysutils-0.1.10}/README.md +0 -0
- {pathwaysutils-0.1.9 → pathwaysutils-0.1.10}/pathwaysutils/_initialize.py +0 -0
- {pathwaysutils-0.1.9 → pathwaysutils-0.1.10}/pathwaysutils/collect_profile.py +0 -0
- {pathwaysutils-0.1.9 → pathwaysutils-0.1.10}/pathwaysutils/debug/__init__.py +0 -0
- {pathwaysutils-0.1.9 → pathwaysutils-0.1.10}/pathwaysutils/debug/timing.py +0 -0
- {pathwaysutils-0.1.9 → pathwaysutils-0.1.10}/pathwaysutils/debug/watchdog.py +0 -0
- {pathwaysutils-0.1.9 → pathwaysutils-0.1.10}/pathwaysutils/elastic/__init__.py +0 -0
- {pathwaysutils-0.1.9 → pathwaysutils-0.1.10}/pathwaysutils/elastic/elastic.py +0 -0
- {pathwaysutils-0.1.9 → pathwaysutils-0.1.10}/pathwaysutils/experimental/__init__.py +0 -0
- {pathwaysutils-0.1.9 → pathwaysutils-0.1.10}/pathwaysutils/experimental/concatenate_by_mesh_axis.py +0 -0
- {pathwaysutils-0.1.9 → pathwaysutils-0.1.10}/pathwaysutils/experimental/gke/__init__.py +0 -0
- {pathwaysutils-0.1.9 → pathwaysutils-0.1.10}/pathwaysutils/experimental/profiling.py +0 -0
- {pathwaysutils-0.1.9 → pathwaysutils-0.1.10}/pathwaysutils/experimental/shared_pathways_service/README.md +0 -0
- {pathwaysutils-0.1.9 → pathwaysutils-0.1.10}/pathwaysutils/experimental/shared_pathways_service/__init__.py +0 -0
- {pathwaysutils-0.1.9 → pathwaysutils-0.1.10}/pathwaysutils/experimental/shared_pathways_service/run_connect_example.py +0 -0
- {pathwaysutils-0.1.9 → pathwaysutils-0.1.10}/pathwaysutils/experimental/shared_pathways_service/start_vscode_on_cpu_np.py +0 -0
- {pathwaysutils-0.1.9 → pathwaysutils-0.1.10}/pathwaysutils/experimental/shared_pathways_service/yamls/code-server.yaml +0 -0
- {pathwaysutils-0.1.9 → pathwaysutils-0.1.10}/pathwaysutils/experimental/shared_pathways_service/yamls/pw-proxy.yaml +0 -0
- {pathwaysutils-0.1.9 → pathwaysutils-0.1.10}/pathwaysutils/experimental/split_by_mesh_axis.py +0 -0
- {pathwaysutils-0.1.9 → pathwaysutils-0.1.10}/pathwaysutils/jax/__init__.py +0 -0
- {pathwaysutils-0.1.9 → pathwaysutils-0.1.10}/pathwaysutils/lru_cache.py +0 -0
- {pathwaysutils-0.1.9 → pathwaysutils-0.1.10}/pathwaysutils/persistence/__init__.py +0 -0
- {pathwaysutils-0.1.9 → pathwaysutils-0.1.10}/pathwaysutils/persistence/helper.py +0 -0
- {pathwaysutils-0.1.9 → pathwaysutils-0.1.10}/pathwaysutils/persistence/orbax_handler.py +0 -0
- {pathwaysutils-0.1.9 → pathwaysutils-0.1.10}/pathwaysutils/plugin_executable.py +0 -0
- {pathwaysutils-0.1.9 → pathwaysutils-0.1.10}/pathwaysutils/proxy_backend.py +0 -0
- {pathwaysutils-0.1.9 → pathwaysutils-0.1.10}/pathwaysutils/test/__init__.py +0 -0
- {pathwaysutils-0.1.9 → pathwaysutils-0.1.10}/pathwaysutils/test/debug/__init__.py +0 -0
- {pathwaysutils-0.1.9 → pathwaysutils-0.1.10}/pathwaysutils/test/debug/timing_test.py +0 -0
- {pathwaysutils-0.1.9 → pathwaysutils-0.1.10}/pathwaysutils/test/debug/watchdog_test.py +0 -0
- {pathwaysutils-0.1.9 → pathwaysutils-0.1.10}/pathwaysutils/test/experimental/reshard_test.py +0 -0
- {pathwaysutils-0.1.9 → pathwaysutils-0.1.10}/pathwaysutils/test/initialize_test.py +0 -0
- {pathwaysutils-0.1.9 → pathwaysutils-0.1.10}/pathwaysutils/test/lru_cache_test.py +0 -0
- {pathwaysutils-0.1.9 → pathwaysutils-0.1.10}/pathwaysutils/test/persistence_test.py +0 -0
- {pathwaysutils-0.1.9 → pathwaysutils-0.1.10}/pathwaysutils/test/plugin_executable_test.py +0 -0
- {pathwaysutils-0.1.9 → pathwaysutils-0.1.10}/pathwaysutils/test/proxy_backend_test.py +0 -0
- {pathwaysutils-0.1.9 → pathwaysutils-0.1.10}/pathwaysutils/test/reshard_test.py +0 -0
- {pathwaysutils-0.1.9 → pathwaysutils-0.1.10}/pyproject.toml +0 -0
|
@@ -22,7 +22,7 @@ from collections.abc import Callable, Mapping, Sequence, Set
|
|
|
22
22
|
import functools
|
|
23
23
|
import logging
|
|
24
24
|
import threading
|
|
25
|
-
from typing import Any, TypeVar
|
|
25
|
+
from typing import Any, TypeAlias, TypeVar
|
|
26
26
|
import warnings
|
|
27
27
|
|
|
28
28
|
import jax
|
|
@@ -48,6 +48,7 @@ class ScaleUpSignalError(Exception):
|
|
|
48
48
|
|
|
49
49
|
|
|
50
50
|
_F = TypeVar("_F", bound=Callable[..., Any])
|
|
51
|
+
RetryPolicy: TypeAlias = Callable[[int, Exception], bool]
|
|
51
52
|
|
|
52
53
|
|
|
53
54
|
def _elastic_event_cleanup() -> None:
|
|
@@ -66,6 +67,19 @@ def _elastic_event_cleanup() -> None:
|
|
|
66
67
|
array.delete()
|
|
67
68
|
|
|
68
69
|
|
|
70
|
+
class ElasticRetryLimit:
|
|
71
|
+
"""A retry callback that limits the number of attempts."""
|
|
72
|
+
|
|
73
|
+
def __init__(self, max_attempts: int):
|
|
74
|
+
if max_attempts <= 0:
|
|
75
|
+
raise ValueError("max_attempts must be positive.")
|
|
76
|
+
self.max_attempts = max_attempts
|
|
77
|
+
|
|
78
|
+
def __call__(self, attempt: int, error: Exception) -> bool:
|
|
79
|
+
del error # Unused
|
|
80
|
+
return attempt < self.max_attempts
|
|
81
|
+
|
|
82
|
+
|
|
69
83
|
class Manager:
|
|
70
84
|
"""Utility class for elastic training.
|
|
71
85
|
|
|
@@ -191,12 +205,14 @@ class Manager:
|
|
|
191
205
|
|
|
192
206
|
def elastic_retry(
|
|
193
207
|
self,
|
|
194
|
-
|
|
208
|
+
# TODO: b/523384843 - Remove max_retries parameter.
|
|
209
|
+
max_retries: int | None = None,
|
|
195
210
|
minimum_slice_count: int | None = None,
|
|
196
211
|
poll_interval: float | int = 10,
|
|
197
212
|
timeout: float | None = None,
|
|
198
213
|
pre_callback: Callable[..., Any] | None = None,
|
|
199
214
|
on_elastic_event_callback: Callable[..., Any] | None = None,
|
|
215
|
+
retry_policy: RetryPolicy | None = None,
|
|
200
216
|
) -> Callable[[_F], _F]:
|
|
201
217
|
"""Retries a function with elasticity fault tolerance.
|
|
202
218
|
|
|
@@ -224,6 +240,8 @@ class Manager:
|
|
|
224
240
|
|
|
225
241
|
Args:
|
|
226
242
|
max_retries: The maximum number of times to retry the function.
|
|
243
|
+
Deprecated: Use `retry_policy` instead.
|
|
244
|
+
TODO: b/523384843 - Remove max_retries parameter.
|
|
227
245
|
minimum_slice_count: The minimum number of slices required to run the
|
|
228
246
|
function. If None, defaults to the total number of slices.
|
|
229
247
|
poll_interval: The number of seconds to wait between activity checks.
|
|
@@ -233,6 +251,11 @@ class Manager:
|
|
|
233
251
|
pre_callback: A callback to call before the function is attempted.
|
|
234
252
|
on_elastic_event_callback: A callback to call after an elastic failure
|
|
235
253
|
occurs.
|
|
254
|
+
retry_policy: A policy (callable) to determine if a retry should be
|
|
255
|
+
attempted. It accepts the attempt number (1-indexed) and the exception
|
|
256
|
+
that triggered the retry. If it returns False, no more retries are
|
|
257
|
+
attempted. If neither `retry_policy` nor `max_retries` is specified, it
|
|
258
|
+
defaults to unlimited retries.
|
|
236
259
|
|
|
237
260
|
Returns:
|
|
238
261
|
A decorator that retries the wrapped function.
|
|
@@ -248,17 +271,24 @@ class Manager:
|
|
|
248
271
|
else minimum_slice_count
|
|
249
272
|
)
|
|
250
273
|
|
|
251
|
-
if max_retries
|
|
252
|
-
raise ValueError("max_retries
|
|
274
|
+
if max_retries is not None and retry_policy is not None:
|
|
275
|
+
raise ValueError("Cannot specify both max_retries and retry_policy.")
|
|
276
|
+
|
|
277
|
+
if retry_policy is None:
|
|
278
|
+
if max_retries is None:
|
|
279
|
+
# Default to unlimited retries if neither parameter is supplied.
|
|
280
|
+
retry_policy = lambda attempt, error: True
|
|
281
|
+
else:
|
|
282
|
+
if max_retries <= 0:
|
|
283
|
+
raise ValueError("max_retries must be positive.")
|
|
284
|
+
retry_policy = ElasticRetryLimit(max_retries)
|
|
253
285
|
|
|
254
286
|
def decorator(func: _F) -> _F:
|
|
255
287
|
@functools.wraps(func)
|
|
256
288
|
def wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
257
289
|
|
|
258
|
-
def attempt_execution(
|
|
259
|
-
_logger.info(
|
|
260
|
-
"Elastic attempt %d out of %d", retry_index + 1, max_retries
|
|
261
|
-
)
|
|
290
|
+
def attempt_execution(attempt: int) -> Any:
|
|
291
|
+
_logger.info("Elastic attempt %d", attempt)
|
|
262
292
|
self.active_slice_indices = elastic.wait_for_slices(
|
|
263
293
|
slice_count=target_slice_count,
|
|
264
294
|
slice_to_devices=self.slice_to_devices,
|
|
@@ -289,34 +319,49 @@ class Manager:
|
|
|
289
319
|
if monitor_thread is not None:
|
|
290
320
|
monitor_thread.join()
|
|
291
321
|
|
|
292
|
-
|
|
322
|
+
attempt = 1
|
|
323
|
+
while True:
|
|
293
324
|
try:
|
|
294
|
-
return attempt_execution(
|
|
295
|
-
except ScaleUpSignalError:
|
|
296
|
-
_logger.info("Scale up requested.
|
|
325
|
+
return attempt_execution(attempt)
|
|
326
|
+
except ScaleUpSignalError as error:
|
|
327
|
+
_logger.info("Scale up requested.")
|
|
297
328
|
_elastic_event_cleanup()
|
|
298
329
|
|
|
299
330
|
if on_elastic_event_callback is not None:
|
|
300
331
|
on_elastic_event_callback()
|
|
332
|
+
|
|
333
|
+
if not retry_policy(attempt, error):
|
|
334
|
+
_logger.info(
|
|
335
|
+
"Retry policy rejected retry after ScaleUpSignalError."
|
|
336
|
+
)
|
|
337
|
+
raise ElasticRuntimeError(
|
|
338
|
+
f"Elastic attempt {attempt} failed."
|
|
339
|
+
) from error
|
|
340
|
+
|
|
341
|
+
_logger.info("Retrying.")
|
|
301
342
|
except jax.errors.JaxRuntimeError as error:
|
|
302
343
|
if not elastic.is_error_due_to_slice_down(error):
|
|
303
344
|
raise
|
|
304
345
|
|
|
305
346
|
if self.new_slice_event.is_set():
|
|
306
|
-
_logger.info(
|
|
307
|
-
"Slice down event and new slice available detected. Retrying."
|
|
308
|
-
)
|
|
347
|
+
_logger.info("Slice down event and new slice available detected.")
|
|
309
348
|
else:
|
|
310
|
-
_logger.info("Slice down event detected.
|
|
349
|
+
_logger.info("Slice down event detected.")
|
|
311
350
|
|
|
312
351
|
_elastic_event_cleanup()
|
|
313
352
|
|
|
314
353
|
if on_elastic_event_callback is not None:
|
|
315
354
|
on_elastic_event_callback()
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
355
|
+
|
|
356
|
+
if not retry_policy(attempt, error):
|
|
357
|
+
_logger.info("Retry policy rejected retry after JaxRuntimeError.")
|
|
358
|
+
raise ElasticRuntimeError(
|
|
359
|
+
f"Elastic attempt {attempt} failed."
|
|
360
|
+
) from error
|
|
361
|
+
|
|
362
|
+
_logger.info("Retrying.")
|
|
363
|
+
|
|
364
|
+
attempt += 1
|
|
320
365
|
|
|
321
366
|
return wrapper
|
|
322
367
|
|
|
@@ -9,10 +9,11 @@
|
|
|
9
9
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
10
10
|
# See the License for the specific language governing permissions and
|
|
11
11
|
# limitations under the License.
|
|
12
|
-
"""Pathways JobSet generator and builder (
|
|
12
|
+
"""Pathways JobSet generator and builder (with Worker Job Config)."""
|
|
13
13
|
|
|
14
14
|
import json
|
|
15
15
|
import logging
|
|
16
|
+
import math
|
|
16
17
|
from typing import Any, Mapping
|
|
17
18
|
from kubernetes import client
|
|
18
19
|
|
|
@@ -33,6 +34,7 @@ DEFAULT_PATHWAYS_PROXY_IMAGE = (
|
|
|
33
34
|
|
|
34
35
|
PATHWAYS_PROXY_PORT = 29000
|
|
35
36
|
PATHWAYS_RM_PORT = 29001
|
|
37
|
+
PATHWAYS_WORKER_PORT = 29005
|
|
36
38
|
|
|
37
39
|
MACHINE_TYPE_TO_TPU_VERSION_MAP = {
|
|
38
40
|
"tpu7x-standard-4t": "tpu7x",
|
|
@@ -77,7 +79,7 @@ def _deserialize_dict(
|
|
|
77
79
|
|
|
78
80
|
|
|
79
81
|
class PathwaysJobSet:
|
|
80
|
-
"""
|
|
82
|
+
"""JobSet configuration generator for Pathways."""
|
|
81
83
|
|
|
82
84
|
def __init__(
|
|
83
85
|
self,
|
|
@@ -90,6 +92,8 @@ class PathwaysJobSet:
|
|
|
90
92
|
user_pod_template: Mapping[str, Any] | None = None,
|
|
91
93
|
main_container_name: str = "main",
|
|
92
94
|
max_restarts: int = 0,
|
|
95
|
+
max_slice_restarts: int = 0,
|
|
96
|
+
termination_grace_period_seconds: int | None = None,
|
|
93
97
|
pathways_version: str = "latest",
|
|
94
98
|
jobset_api_version: str = "v1alpha2",
|
|
95
99
|
elastic_slices: int = 0,
|
|
@@ -108,6 +112,8 @@ class PathwaysJobSet:
|
|
|
108
112
|
user_pod_template: Optional user pod template for the head job.
|
|
109
113
|
main_container_name: Name of the main container in user_pod_template.
|
|
110
114
|
max_restarts: Maximum number of restarts for the JobSet.
|
|
115
|
+
max_slice_restarts: Maximum number of slice restarts.
|
|
116
|
+
termination_grace_period_seconds: Optional termination grace period.
|
|
111
117
|
pathways_version: Version tag for Pathways images.
|
|
112
118
|
jobset_api_version: API version of JobSet.
|
|
113
119
|
elastic_slices: Number of elastic slices.
|
|
@@ -126,6 +132,19 @@ class PathwaysJobSet:
|
|
|
126
132
|
if not tpu_version:
|
|
127
133
|
raise ValueError(f"Unsupported TPU type: {tpu_type}")
|
|
128
134
|
|
|
135
|
+
gke_accel_type = MACHINE_TYPE_TO_GKE_ACCELERATOR_TYPE_MAP.get(
|
|
136
|
+
tpu_type.lower()
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
# Calculate VMs.
|
|
140
|
+
dims = [int(x) for x in topology.split("x")]
|
|
141
|
+
total_chips = math.prod(dims)
|
|
142
|
+
chips_per_vm = 8 if tpu_type.lower().endswith("8t") else 4
|
|
143
|
+
if total_chips < chips_per_vm:
|
|
144
|
+
num_vms = 1
|
|
145
|
+
else:
|
|
146
|
+
num_vms = total_chips // chips_per_vm
|
|
147
|
+
|
|
129
148
|
instance_type = f"{tpu_version}:{topology}"
|
|
130
149
|
image_tag = pathways_version
|
|
131
150
|
|
|
@@ -140,8 +159,17 @@ class PathwaysJobSet:
|
|
|
140
159
|
elastic_slices=elastic_slices,
|
|
141
160
|
)
|
|
142
161
|
|
|
143
|
-
# Build
|
|
144
|
-
self._worker_job_template = self.
|
|
162
|
+
# Build worker template.
|
|
163
|
+
self._worker_job_template = self._build_worker_job_template(
|
|
164
|
+
pathways_dir=pathways_dir,
|
|
165
|
+
num_vms=num_vms,
|
|
166
|
+
chips_per_vm=chips_per_vm,
|
|
167
|
+
gke_accel_type=gke_accel_type,
|
|
168
|
+
topology=topology,
|
|
169
|
+
image_tag=image_tag,
|
|
170
|
+
max_slice_restarts=max_slice_restarts,
|
|
171
|
+
termination_grace_period_seconds=termination_grace_period_seconds,
|
|
172
|
+
)
|
|
145
173
|
|
|
146
174
|
self._success_policy = None
|
|
147
175
|
if user_pod_template:
|
|
@@ -150,20 +178,6 @@ class PathwaysJobSet:
|
|
|
150
178
|
"targetReplicatedJobs": [PATHWAYS_HEAD_JOB_NAME],
|
|
151
179
|
}
|
|
152
180
|
|
|
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
181
|
def _build_head_job_template(
|
|
168
182
|
self,
|
|
169
183
|
pathways_dir: str,
|
|
@@ -365,6 +379,157 @@ class PathwaysJobSet:
|
|
|
365
379
|
)
|
|
366
380
|
return head_job_template
|
|
367
381
|
|
|
382
|
+
def _build_worker_job_template(
|
|
383
|
+
self,
|
|
384
|
+
pathways_dir: str,
|
|
385
|
+
num_vms: int,
|
|
386
|
+
chips_per_vm: int,
|
|
387
|
+
gke_accel_type: str,
|
|
388
|
+
topology: str,
|
|
389
|
+
image_tag: str,
|
|
390
|
+
max_slice_restarts: int,
|
|
391
|
+
termination_grace_period_seconds: int | None,
|
|
392
|
+
) -> client.V1JobTemplateSpec:
|
|
393
|
+
worker_image = f"{DEFAULT_PATHWAYS_RM_AND_WORKER_IMAGE}:{image_tag}"
|
|
394
|
+
|
|
395
|
+
args = [
|
|
396
|
+
f"--resource_manager_address=$(PATHWAYS_HEAD):{PATHWAYS_RM_PORT}",
|
|
397
|
+
f"--server_port={PATHWAYS_WORKER_PORT}",
|
|
398
|
+
f"--gcs_scratch_location={pathways_dir}",
|
|
399
|
+
]
|
|
400
|
+
worker_env = [
|
|
401
|
+
client.V1EnvVar(name="TPU_MIN_LOG_LEVEL", value="0"),
|
|
402
|
+
client.V1EnvVar(name="TF_CPP_MIN_LOG_LEVEL", value="0"),
|
|
403
|
+
client.V1EnvVar(name="XCLOUD_ENVIRONMENT", value="GCP"),
|
|
404
|
+
client.V1EnvVar(name="MEGASCALE_GRPC_ENABLE_XOR_TRACER", value="false"),
|
|
405
|
+
client.V1EnvVar(
|
|
406
|
+
name="MEGASCALE_NUM_SLICES",
|
|
407
|
+
value_from=client.V1EnvVarSource(
|
|
408
|
+
field_ref=client.V1ObjectFieldSelector(
|
|
409
|
+
field_path="metadata.labels['jobset.sigs.k8s.io/replicatedjob-replicas']"
|
|
410
|
+
)
|
|
411
|
+
),
|
|
412
|
+
),
|
|
413
|
+
client.V1EnvVar(
|
|
414
|
+
name="JOBSET_NAME",
|
|
415
|
+
value_from=client.V1EnvVarSource(
|
|
416
|
+
field_ref=client.V1ObjectFieldSelector(
|
|
417
|
+
field_path=(
|
|
418
|
+
"metadata.annotations['jobset.sigs.k8s.io/jobset-name']"
|
|
419
|
+
)
|
|
420
|
+
)
|
|
421
|
+
),
|
|
422
|
+
),
|
|
423
|
+
client.V1EnvVar(
|
|
424
|
+
name="REPLICATED_JOB_NAME",
|
|
425
|
+
value_from=client.V1EnvVarSource(
|
|
426
|
+
field_ref=client.V1ObjectFieldSelector(
|
|
427
|
+
field_path="metadata.annotations['jobset.sigs.k8s.io/replicatedjob-name']"
|
|
428
|
+
)
|
|
429
|
+
),
|
|
430
|
+
),
|
|
431
|
+
client.V1EnvVar(
|
|
432
|
+
name="MEGASCALE_SLICE_ID",
|
|
433
|
+
value_from=client.V1EnvVarSource(
|
|
434
|
+
field_ref=client.V1ObjectFieldSelector(
|
|
435
|
+
field_path="metadata.labels['jobset.sigs.k8s.io/job-index']"
|
|
436
|
+
)
|
|
437
|
+
),
|
|
438
|
+
),
|
|
439
|
+
client.V1EnvVar(
|
|
440
|
+
name="PATHWAYS_HEAD",
|
|
441
|
+
value_from=client.V1EnvVarSource(
|
|
442
|
+
field_ref=client.V1ObjectFieldSelector(
|
|
443
|
+
field_path=(
|
|
444
|
+
"metadata.labels['jobset.sigs.k8s.io/coordinator']"
|
|
445
|
+
)
|
|
446
|
+
)
|
|
447
|
+
),
|
|
448
|
+
),
|
|
449
|
+
client.V1EnvVar(
|
|
450
|
+
name="MEGASCALE_COORDINATOR_ADDRESS",
|
|
451
|
+
value_from=client.V1EnvVarSource(
|
|
452
|
+
field_ref=client.V1ObjectFieldSelector(
|
|
453
|
+
field_path=(
|
|
454
|
+
"metadata.labels['jobset.sigs.k8s.io/coordinator']"
|
|
455
|
+
)
|
|
456
|
+
)
|
|
457
|
+
),
|
|
458
|
+
),
|
|
459
|
+
]
|
|
460
|
+
|
|
461
|
+
worker_container = client.V1Container(
|
|
462
|
+
name="pathways-worker",
|
|
463
|
+
image=worker_image,
|
|
464
|
+
image_pull_policy="Always",
|
|
465
|
+
args=args,
|
|
466
|
+
env=worker_env,
|
|
467
|
+
ports=[
|
|
468
|
+
client.V1ContainerPort(
|
|
469
|
+
container_port=PATHWAYS_WORKER_PORT, protocol="TCP"
|
|
470
|
+
),
|
|
471
|
+
client.V1ContainerPort(container_port=29006, protocol="TCP"),
|
|
472
|
+
client.V1ContainerPort(container_port=8471, protocol="TCP"),
|
|
473
|
+
client.V1ContainerPort(container_port=8080, protocol="TCP"),
|
|
474
|
+
],
|
|
475
|
+
volume_mounts=[
|
|
476
|
+
client.V1VolumeMount(name="shared-tmp", mount_path="/tmp")
|
|
477
|
+
],
|
|
478
|
+
resources=client.V1ResourceRequirements(
|
|
479
|
+
limits={"google.com/tpu": str(chips_per_vm)}
|
|
480
|
+
),
|
|
481
|
+
)
|
|
482
|
+
|
|
483
|
+
node_selector = {
|
|
484
|
+
"cloud.google.com/gke-tpu-accelerator": gke_accel_type,
|
|
485
|
+
"cloud.google.com/gke-tpu-topology": topology,
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
backoff_limit = num_vms * 4
|
|
489
|
+
if max_slice_restarts > 0:
|
|
490
|
+
backoff_limit = num_vms * max_slice_restarts
|
|
491
|
+
|
|
492
|
+
worker_pod_spec = client.V1PodSpec(
|
|
493
|
+
containers=[worker_container],
|
|
494
|
+
node_selector=node_selector,
|
|
495
|
+
volumes=[
|
|
496
|
+
client.V1Volume(
|
|
497
|
+
name="shared-tmp",
|
|
498
|
+
host_path=client.V1HostPathVolumeSource(
|
|
499
|
+
path="/tmp", type="DirectoryOrCreate"
|
|
500
|
+
),
|
|
501
|
+
)
|
|
502
|
+
],
|
|
503
|
+
host_network=True,
|
|
504
|
+
dns_policy="ClusterFirstWithHostNet",
|
|
505
|
+
restart_policy="OnFailure",
|
|
506
|
+
)
|
|
507
|
+
if termination_grace_period_seconds is not None:
|
|
508
|
+
worker_pod_spec.termination_grace_period_seconds = (
|
|
509
|
+
termination_grace_period_seconds
|
|
510
|
+
)
|
|
511
|
+
|
|
512
|
+
worker_job_template = client.V1JobTemplateSpec(
|
|
513
|
+
metadata=client.V1ObjectMeta(),
|
|
514
|
+
spec=client.V1JobSpec(
|
|
515
|
+
backoff_limit=backoff_limit,
|
|
516
|
+
completion_mode="Indexed",
|
|
517
|
+
completions=num_vms,
|
|
518
|
+
parallelism=num_vms,
|
|
519
|
+
template=client.V1PodTemplateSpec(
|
|
520
|
+
metadata=client.V1ObjectMeta(
|
|
521
|
+
annotations={
|
|
522
|
+
"alpha.jobset.sigs.k8s.io/exclusive-topology": (
|
|
523
|
+
"cloud.google.com/gke-nodepool"
|
|
524
|
+
)
|
|
525
|
+
}
|
|
526
|
+
),
|
|
527
|
+
spec=worker_pod_spec,
|
|
528
|
+
),
|
|
529
|
+
),
|
|
530
|
+
)
|
|
531
|
+
return worker_job_template
|
|
532
|
+
|
|
368
533
|
def _compile_config(self) -> dict[str, Any]:
|
|
369
534
|
"""Compiles the JobSet configuration into a dictionary."""
|
|
370
535
|
with client.ApiClient() as api_client:
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
# Copyright 2025 Google LLC
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# https://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
"""Experimental resharding API for elastic device sets."""
|
|
15
|
+
|
|
16
|
+
import base64
|
|
17
|
+
from collections.abc import Callable, Mapping, Sequence
|
|
18
|
+
import json
|
|
19
|
+
import logging
|
|
20
|
+
from typing import Any
|
|
21
|
+
|
|
22
|
+
import jax
|
|
23
|
+
from pathwaysutils import lru_cache
|
|
24
|
+
from pathwaysutils import plugin_executable
|
|
25
|
+
from pathwaysutils import reshard as pw_reshard
|
|
26
|
+
|
|
27
|
+
# Redirects for generic intermediate sharding helpers and exceptions
|
|
28
|
+
NoIntermediateShardingError = pw_reshard.NoIntermediateShardingError
|
|
29
|
+
NoIntermediateShardingNeededError = pw_reshard.NoIntermediateShardingNeededError
|
|
30
|
+
find_intermediate_sharding = pw_reshard.find_intermediate_sharding
|
|
31
|
+
reshard_with_intermediate_sharding_generic = (
|
|
32
|
+
pw_reshard.reshard_with_intermediate_sharding_generic
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
_logger = logging.getLogger(__name__)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _identity(x: Any) -> Any:
|
|
39
|
+
"""A helper function that returns its input."""
|
|
40
|
+
return x
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class ReshardingPlanWrapper:
|
|
44
|
+
"""Wrapper around PluginProgram(reshard_request)."""
|
|
45
|
+
|
|
46
|
+
_plugin_executable: plugin_executable.PluginExecutable
|
|
47
|
+
_avals: Sequence[jax.core.ShapedArray]
|
|
48
|
+
_out_shardings: Sequence[jax.sharding.Sharding]
|
|
49
|
+
|
|
50
|
+
def __init__(
|
|
51
|
+
self,
|
|
52
|
+
avals: Sequence[jax.core.ShapedArray],
|
|
53
|
+
source_shardings: Sequence[jax.sharding.Sharding],
|
|
54
|
+
destination_shardings: Sequence[jax.sharding.Sharding],
|
|
55
|
+
donate: bool,
|
|
56
|
+
):
|
|
57
|
+
|
|
58
|
+
def ifrt_hlo_sharding(
|
|
59
|
+
aval: jax.core.ShapedArray, sharding: jax.sharding.Sharding
|
|
60
|
+
) -> Mapping[str, Any]:
|
|
61
|
+
result = {
|
|
62
|
+
"devices": {
|
|
63
|
+
"device_ids": [
|
|
64
|
+
device.id for device in sharding._addressable_device_assignment # pylint: disable=protected-access
|
|
65
|
+
]
|
|
66
|
+
},
|
|
67
|
+
"xla_hlo_sharding": (
|
|
68
|
+
base64.b64encode(
|
|
69
|
+
sharding._to_xla_hlo_sharding(aval.ndim) # pylint: disable=protected-access
|
|
70
|
+
.to_proto()
|
|
71
|
+
.SerializeToString()
|
|
72
|
+
).decode("utf-8")
|
|
73
|
+
),
|
|
74
|
+
}
|
|
75
|
+
if sharding.memory_kind is not None:
|
|
76
|
+
result["memory_kind"] = sharding.memory_kind
|
|
77
|
+
return result
|
|
78
|
+
|
|
79
|
+
request = {
|
|
80
|
+
"reshardRequest": {
|
|
81
|
+
"donateInput": donate,
|
|
82
|
+
"inSharding": [
|
|
83
|
+
ifrt_hlo_sharding(aval, old_sharding)
|
|
84
|
+
for aval, old_sharding in zip(avals, source_shardings)
|
|
85
|
+
],
|
|
86
|
+
"outSharding": [
|
|
87
|
+
ifrt_hlo_sharding(aval, new_sharding)
|
|
88
|
+
for aval, new_sharding in zip(avals, destination_shardings)
|
|
89
|
+
],
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
self._plugin_executable = plugin_executable.PluginExecutable(
|
|
94
|
+
json.dumps(request)
|
|
95
|
+
)
|
|
96
|
+
self._avals = avals
|
|
97
|
+
self._out_shardings = destination_shardings
|
|
98
|
+
|
|
99
|
+
def execute(self, inp_arrays: tuple[jax.Array, ...]) -> Sequence[jax.Array]:
|
|
100
|
+
out_arrays, fut = self._plugin_executable.call(
|
|
101
|
+
inp_arrays, self._out_shardings, self._avals
|
|
102
|
+
)
|
|
103
|
+
fut.result()
|
|
104
|
+
return out_arrays
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _get_resharding_plan(
|
|
108
|
+
avals: tuple[jax.core.ShapedArray, ...],
|
|
109
|
+
old_shardings: tuple[jax.sharding.Sharding, ...],
|
|
110
|
+
new_shardings: tuple[jax.sharding.Sharding, ...],
|
|
111
|
+
donate: bool,
|
|
112
|
+
) -> ReshardingPlanWrapper:
|
|
113
|
+
"""Returns a resharding plan for the given sharding task."""
|
|
114
|
+
return ReshardingPlanWrapper(avals, old_shardings, new_shardings, donate)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
_get_resharding_plan_cached = lru_cache.lru_cache()(_get_resharding_plan)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _sidechannel_jax_array_reshard(
|
|
121
|
+
array_info: Mapping[str, Any], *, donate: bool, cache_resharding_plans: bool
|
|
122
|
+
) -> Sequence[jax.Array]:
|
|
123
|
+
get_resharding_plan_func = (
|
|
124
|
+
_get_resharding_plan_cached
|
|
125
|
+
if cache_resharding_plans
|
|
126
|
+
else _get_resharding_plan
|
|
127
|
+
)
|
|
128
|
+
return get_resharding_plan_func(
|
|
129
|
+
tuple(arr.aval for arr in array_info["arrays"]),
|
|
130
|
+
tuple(arr.sharding for arr in array_info["arrays"]),
|
|
131
|
+
tuple(array_info["dst_shardings"]),
|
|
132
|
+
donate,
|
|
133
|
+
).execute(tuple(array_info["arrays"]))
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def reshard(
|
|
137
|
+
x: Any,
|
|
138
|
+
sharding: jax.sharding.Sharding | Any,
|
|
139
|
+
*,
|
|
140
|
+
donate: bool = False,
|
|
141
|
+
may_alias: bool | None = None,
|
|
142
|
+
cache_resharding_plans: bool = False,
|
|
143
|
+
) -> Any:
|
|
144
|
+
"""Reshards `x` to `sharding`.
|
|
145
|
+
|
|
146
|
+
This function is an alternative to `pathwaysutils.reshard` (which is the IFRT
|
|
147
|
+
reshard) that uses the sidechannel MPMD resharding API for the final reshard.
|
|
148
|
+
|
|
149
|
+
Args:
|
|
150
|
+
x: An array, scalar, or (nested) standard Python container thereof.
|
|
151
|
+
sharding: A `Sharding` or a (nested) `Sharding` in standard Python container
|
|
152
|
+
(must be a tree prefix of `x`), representing the device(s) and sharding to
|
|
153
|
+
which `x` should be sharded to. The result will be committed to the
|
|
154
|
+
device(s) of the sharding.
|
|
155
|
+
donate: If `True`, donate all input arrays, which may reduce the amount of
|
|
156
|
+
memory needed for resharding. Buffers donated to resharding should not be
|
|
157
|
+
reused.
|
|
158
|
+
may_alias: If `True`, may alias the input array with the output array. May
|
|
159
|
+
reduce the amount of memory needed for resharding. Not used at the moment.
|
|
160
|
+
cache_resharding_plans: If `True`, uses a resharding plan cache to avoid
|
|
161
|
+
recreating plans for the same resharding operation.
|
|
162
|
+
|
|
163
|
+
Returns:
|
|
164
|
+
A copy of `x` whose sharding is `sharding`.
|
|
165
|
+
"""
|
|
166
|
+
return pw_reshard.reshard_generic(
|
|
167
|
+
x,
|
|
168
|
+
sharding,
|
|
169
|
+
donate=donate,
|
|
170
|
+
may_alias=may_alias,
|
|
171
|
+
jax_array_reshard_fn=_sidechannel_jax_array_reshard,
|
|
172
|
+
cache_resharding_plans=cache_resharding_plans,
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def sidechannel_reshard_with_intermediate_sharding(
|
|
177
|
+
x: Any,
|
|
178
|
+
in_sharding: jax.sharding.Sharding,
|
|
179
|
+
out_sharding: jax.sharding.Sharding,
|
|
180
|
+
*,
|
|
181
|
+
donate: bool = False,
|
|
182
|
+
may_alias: bool | None = None,
|
|
183
|
+
cache_resharding_plans: bool = False,
|
|
184
|
+
) -> Any:
|
|
185
|
+
"""Reshards `x` to `out_sharding`, using an intermediate sharding if possible.
|
|
186
|
+
|
|
187
|
+
This function is an alternative to `reshard` that may be faster and sometimes
|
|
188
|
+
essential for certain sharding combinations by using an intermediate sharding
|
|
189
|
+
to avoid expensive all-gathers. If no beneficial intermediate sharding is
|
|
190
|
+
found, it falls back to standard resharding. See `find_intermediate_sharding`
|
|
191
|
+
for more details on when an intermediate sharding is used.
|
|
192
|
+
|
|
193
|
+
Uses the sidechannel resharding API for the final reshard.
|
|
194
|
+
|
|
195
|
+
Args:
|
|
196
|
+
x: An array, scalar, or (nested) standard Python container thereof.
|
|
197
|
+
in_sharding: The source sharding of `x`.
|
|
198
|
+
out_sharding: The target sharding for `x`.
|
|
199
|
+
donate: If `True`, donate all input arrays, which may reduce the amount of
|
|
200
|
+
memory needed for resharding. Buffers donated to resharding should not be
|
|
201
|
+
reused.
|
|
202
|
+
may_alias: If `True`, may alias the input array with the output array. May
|
|
203
|
+
reduce the amount of memory needed for resharding. Not used at the moment.
|
|
204
|
+
cache_resharding_plans: If `True`, uses a resharding plan cache to avoid
|
|
205
|
+
recreating plans for the same resharding operation.
|
|
206
|
+
|
|
207
|
+
Returns:
|
|
208
|
+
A copy of `x` whose sharding is `out_sharding`.
|
|
209
|
+
"""
|
|
210
|
+
return reshard_with_intermediate_sharding_generic(
|
|
211
|
+
x,
|
|
212
|
+
in_sharding,
|
|
213
|
+
out_sharding,
|
|
214
|
+
donate=donate,
|
|
215
|
+
may_alias=may_alias,
|
|
216
|
+
jax_array_reshard_fn=_sidechannel_jax_array_reshard,
|
|
217
|
+
cache_resharding_plans=cache_resharding_plans,
|
|
218
|
+
)
|