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.
Files changed (57) hide show
  1. {pathwaysutils-0.1.9 → pathwaysutils-0.1.10}/PKG-INFO +1 -1
  2. {pathwaysutils-0.1.9 → pathwaysutils-0.1.10}/pathwaysutils/__init__.py +1 -1
  3. {pathwaysutils-0.1.9 → pathwaysutils-0.1.10}/pathwaysutils/elastic/manager.py +65 -20
  4. {pathwaysutils-0.1.9 → pathwaysutils-0.1.10}/pathwaysutils/experimental/gke/jobset.py +183 -18
  5. pathwaysutils-0.1.10/pathwaysutils/experimental/reshard.py +218 -0
  6. {pathwaysutils-0.1.9 → pathwaysutils-0.1.10}/pathwaysutils/experimental/shared_pathways_service/deploy_pathways_service.py +10 -0
  7. pathwaysutils-0.1.10/pathwaysutils/experimental/shared_pathways_service/dockerfiles/sample_sidecar.dockerfile +27 -0
  8. {pathwaysutils-0.1.9 → pathwaysutils-0.1.10}/pathwaysutils/experimental/shared_pathways_service/gke_utils.py +56 -0
  9. {pathwaysutils-0.1.9 → pathwaysutils-0.1.10}/pathwaysutils/experimental/shared_pathways_service/isc_pathways.py +38 -17
  10. {pathwaysutils-0.1.9 → pathwaysutils-0.1.10}/pathwaysutils/experimental/shared_pathways_service/metrics_collector.py +28 -6
  11. {pathwaysutils-0.1.9 → pathwaysutils-0.1.10}/pathwaysutils/experimental/shared_pathways_service/run_workload.py +2 -4
  12. {pathwaysutils-0.1.9 → pathwaysutils-0.1.10}/pathwaysutils/experimental/shared_pathways_service/validators.py +99 -0
  13. {pathwaysutils-0.1.9 → pathwaysutils-0.1.10}/pathwaysutils/experimental/shared_pathways_service/yamls/pw-service.yaml +45 -2
  14. {pathwaysutils-0.1.9 → pathwaysutils-0.1.10}/pathwaysutils/profiling.py +5 -2
  15. {pathwaysutils-0.1.9/pathwaysutils/experimental → pathwaysutils-0.1.10/pathwaysutils}/reshard.py +78 -158
  16. {pathwaysutils-0.1.9 → pathwaysutils-0.1.10}/pathwaysutils/test/experimental/gke/jobset_test.py +74 -1
  17. {pathwaysutils-0.1.9 → pathwaysutils-0.1.10}/pathwaysutils/test/profiling_test.py +24 -6
  18. pathwaysutils-0.1.9/pathwaysutils/reshard.py +0 -133
  19. {pathwaysutils-0.1.9 → pathwaysutils-0.1.10}/LICENSE +0 -0
  20. {pathwaysutils-0.1.9 → pathwaysutils-0.1.10}/README.md +0 -0
  21. {pathwaysutils-0.1.9 → pathwaysutils-0.1.10}/pathwaysutils/_initialize.py +0 -0
  22. {pathwaysutils-0.1.9 → pathwaysutils-0.1.10}/pathwaysutils/collect_profile.py +0 -0
  23. {pathwaysutils-0.1.9 → pathwaysutils-0.1.10}/pathwaysutils/debug/__init__.py +0 -0
  24. {pathwaysutils-0.1.9 → pathwaysutils-0.1.10}/pathwaysutils/debug/timing.py +0 -0
  25. {pathwaysutils-0.1.9 → pathwaysutils-0.1.10}/pathwaysutils/debug/watchdog.py +0 -0
  26. {pathwaysutils-0.1.9 → pathwaysutils-0.1.10}/pathwaysutils/elastic/__init__.py +0 -0
  27. {pathwaysutils-0.1.9 → pathwaysutils-0.1.10}/pathwaysutils/elastic/elastic.py +0 -0
  28. {pathwaysutils-0.1.9 → pathwaysutils-0.1.10}/pathwaysutils/experimental/__init__.py +0 -0
  29. {pathwaysutils-0.1.9 → pathwaysutils-0.1.10}/pathwaysutils/experimental/concatenate_by_mesh_axis.py +0 -0
  30. {pathwaysutils-0.1.9 → pathwaysutils-0.1.10}/pathwaysutils/experimental/gke/__init__.py +0 -0
  31. {pathwaysutils-0.1.9 → pathwaysutils-0.1.10}/pathwaysutils/experimental/profiling.py +0 -0
  32. {pathwaysutils-0.1.9 → pathwaysutils-0.1.10}/pathwaysutils/experimental/shared_pathways_service/README.md +0 -0
  33. {pathwaysutils-0.1.9 → pathwaysutils-0.1.10}/pathwaysutils/experimental/shared_pathways_service/__init__.py +0 -0
  34. {pathwaysutils-0.1.9 → pathwaysutils-0.1.10}/pathwaysutils/experimental/shared_pathways_service/run_connect_example.py +0 -0
  35. {pathwaysutils-0.1.9 → pathwaysutils-0.1.10}/pathwaysutils/experimental/shared_pathways_service/start_vscode_on_cpu_np.py +0 -0
  36. {pathwaysutils-0.1.9 → pathwaysutils-0.1.10}/pathwaysutils/experimental/shared_pathways_service/yamls/code-server.yaml +0 -0
  37. {pathwaysutils-0.1.9 → pathwaysutils-0.1.10}/pathwaysutils/experimental/shared_pathways_service/yamls/pw-proxy.yaml +0 -0
  38. {pathwaysutils-0.1.9 → pathwaysutils-0.1.10}/pathwaysutils/experimental/split_by_mesh_axis.py +0 -0
  39. {pathwaysutils-0.1.9 → pathwaysutils-0.1.10}/pathwaysutils/jax/__init__.py +0 -0
  40. {pathwaysutils-0.1.9 → pathwaysutils-0.1.10}/pathwaysutils/lru_cache.py +0 -0
  41. {pathwaysutils-0.1.9 → pathwaysutils-0.1.10}/pathwaysutils/persistence/__init__.py +0 -0
  42. {pathwaysutils-0.1.9 → pathwaysutils-0.1.10}/pathwaysutils/persistence/helper.py +0 -0
  43. {pathwaysutils-0.1.9 → pathwaysutils-0.1.10}/pathwaysutils/persistence/orbax_handler.py +0 -0
  44. {pathwaysutils-0.1.9 → pathwaysutils-0.1.10}/pathwaysutils/plugin_executable.py +0 -0
  45. {pathwaysutils-0.1.9 → pathwaysutils-0.1.10}/pathwaysutils/proxy_backend.py +0 -0
  46. {pathwaysutils-0.1.9 → pathwaysutils-0.1.10}/pathwaysutils/test/__init__.py +0 -0
  47. {pathwaysutils-0.1.9 → pathwaysutils-0.1.10}/pathwaysutils/test/debug/__init__.py +0 -0
  48. {pathwaysutils-0.1.9 → pathwaysutils-0.1.10}/pathwaysutils/test/debug/timing_test.py +0 -0
  49. {pathwaysutils-0.1.9 → pathwaysutils-0.1.10}/pathwaysutils/test/debug/watchdog_test.py +0 -0
  50. {pathwaysutils-0.1.9 → pathwaysutils-0.1.10}/pathwaysutils/test/experimental/reshard_test.py +0 -0
  51. {pathwaysutils-0.1.9 → pathwaysutils-0.1.10}/pathwaysutils/test/initialize_test.py +0 -0
  52. {pathwaysutils-0.1.9 → pathwaysutils-0.1.10}/pathwaysutils/test/lru_cache_test.py +0 -0
  53. {pathwaysutils-0.1.9 → pathwaysutils-0.1.10}/pathwaysutils/test/persistence_test.py +0 -0
  54. {pathwaysutils-0.1.9 → pathwaysutils-0.1.10}/pathwaysutils/test/plugin_executable_test.py +0 -0
  55. {pathwaysutils-0.1.9 → pathwaysutils-0.1.10}/pathwaysutils/test/proxy_backend_test.py +0 -0
  56. {pathwaysutils-0.1.9 → pathwaysutils-0.1.10}/pathwaysutils/test/reshard_test.py +0 -0
  57. {pathwaysutils-0.1.9 → pathwaysutils-0.1.10}/pyproject.toml +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pathwaysutils
3
- Version: 0.1.9
3
+ Version: 0.1.10
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>
@@ -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.9"
24
+ __version__ = "v0.1.10"
@@ -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
- max_retries: int,
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 <= 0:
252
- raise ValueError("max_retries must be positive.")
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(retry_index: int) -> Any:
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
- for retry_index in range(max_retries):
322
+ attempt = 1
323
+ while True:
293
324
  try:
294
- return attempt_execution(retry_index)
295
- except ScaleUpSignalError:
296
- _logger.info("Scale up requested. Retrying.")
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. Retrying.")
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
- else:
317
- raise ElasticRuntimeError(
318
- f"Elastic attempt {max_retries} out of {max_retries} failed."
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 (Head Job Config)."""
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
- """Generates JobSet configuration for Pathways (with Head Job Config)."""
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 minimal worker template (placeholder)
144
- self._worker_job_template = self._build_minimal_job_template("worker")
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
+ )