pathwaysutils 0.1.9__tar.gz → 0.1.11__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 (60) hide show
  1. {pathwaysutils-0.1.9 → pathwaysutils-0.1.11}/PKG-INFO +2 -2
  2. {pathwaysutils-0.1.9 → pathwaysutils-0.1.11}/pathwaysutils/__init__.py +1 -1
  3. {pathwaysutils-0.1.9 → pathwaysutils-0.1.11}/pathwaysutils/debug/watchdog.py +1 -1
  4. {pathwaysutils-0.1.9 → pathwaysutils-0.1.11}/pathwaysutils/elastic/elastic.py +8 -7
  5. {pathwaysutils-0.1.9 → pathwaysutils-0.1.11}/pathwaysutils/elastic/manager.py +216 -100
  6. pathwaysutils-0.1.11/pathwaysutils/experimental/gke/jobset.py +923 -0
  7. pathwaysutils-0.1.11/pathwaysutils/experimental/reshard.py +218 -0
  8. {pathwaysutils-0.1.9 → pathwaysutils-0.1.11}/pathwaysutils/experimental/shared_pathways_service/deploy_pathways_service.py +59 -48
  9. pathwaysutils-0.1.11/pathwaysutils/experimental/shared_pathways_service/dockerfiles/sample_sidecar.dockerfile +27 -0
  10. {pathwaysutils-0.1.9 → pathwaysutils-0.1.11}/pathwaysutils/experimental/shared_pathways_service/gke_utils.py +56 -0
  11. {pathwaysutils-0.1.9 → pathwaysutils-0.1.11}/pathwaysutils/experimental/shared_pathways_service/isc_pathways.py +38 -17
  12. {pathwaysutils-0.1.9 → pathwaysutils-0.1.11}/pathwaysutils/experimental/shared_pathways_service/metrics_collector.py +29 -7
  13. {pathwaysutils-0.1.9 → pathwaysutils-0.1.11}/pathwaysutils/experimental/shared_pathways_service/run_workload.py +2 -4
  14. pathwaysutils-0.1.11/pathwaysutils/experimental/shared_pathways_service/tpu_specs.py +109 -0
  15. {pathwaysutils-0.1.9 → pathwaysutils-0.1.11}/pathwaysutils/experimental/shared_pathways_service/validators.py +99 -0
  16. {pathwaysutils-0.1.9 → pathwaysutils-0.1.11}/pathwaysutils/experimental/split_by_mesh_axis.py +1 -1
  17. {pathwaysutils-0.1.9 → pathwaysutils-0.1.11}/pathwaysutils/lru_cache.py +3 -3
  18. {pathwaysutils-0.1.9 → pathwaysutils-0.1.11}/pathwaysutils/persistence/helper.py +6 -6
  19. {pathwaysutils-0.1.9 → pathwaysutils-0.1.11}/pathwaysutils/persistence/orbax_handler.py +5 -5
  20. {pathwaysutils-0.1.9 → pathwaysutils-0.1.11}/pathwaysutils/plugin_executable.py +2 -2
  21. {pathwaysutils-0.1.9 → pathwaysutils-0.1.11}/pathwaysutils/profiling.py +14 -9
  22. {pathwaysutils-0.1.9/pathwaysutils/experimental → pathwaysutils-0.1.11/pathwaysutils}/reshard.py +79 -159
  23. pathwaysutils-0.1.11/pathwaysutils/test/experimental/gke/jobset_test.py +977 -0
  24. pathwaysutils-0.1.9/pathwaysutils/experimental/shared_pathways_service/yamls/pw-service.yaml → pathwaysutils-0.1.11/pathwaysutils/test/experimental/gke/testdata/model_lite_tpuv5e_4x8.yaml +104 -28
  25. {pathwaysutils-0.1.9 → pathwaysutils-0.1.11}/pathwaysutils/test/profiling_test.py +25 -7
  26. {pathwaysutils-0.1.9 → pathwaysutils-0.1.11}/pyproject.toml +1 -1
  27. pathwaysutils-0.1.9/pathwaysutils/experimental/gke/jobset.py +0 -414
  28. pathwaysutils-0.1.9/pathwaysutils/reshard.py +0 -133
  29. pathwaysutils-0.1.9/pathwaysutils/test/experimental/gke/jobset_test.py +0 -137
  30. {pathwaysutils-0.1.9 → pathwaysutils-0.1.11}/LICENSE +0 -0
  31. {pathwaysutils-0.1.9 → pathwaysutils-0.1.11}/README.md +0 -0
  32. {pathwaysutils-0.1.9 → pathwaysutils-0.1.11}/pathwaysutils/_initialize.py +0 -0
  33. {pathwaysutils-0.1.9 → pathwaysutils-0.1.11}/pathwaysutils/collect_profile.py +0 -0
  34. {pathwaysutils-0.1.9 → pathwaysutils-0.1.11}/pathwaysutils/debug/__init__.py +0 -0
  35. {pathwaysutils-0.1.9 → pathwaysutils-0.1.11}/pathwaysutils/debug/timing.py +0 -0
  36. {pathwaysutils-0.1.9 → pathwaysutils-0.1.11}/pathwaysutils/elastic/__init__.py +0 -0
  37. {pathwaysutils-0.1.9 → pathwaysutils-0.1.11}/pathwaysutils/experimental/__init__.py +0 -0
  38. {pathwaysutils-0.1.9 → pathwaysutils-0.1.11}/pathwaysutils/experimental/concatenate_by_mesh_axis.py +0 -0
  39. {pathwaysutils-0.1.9 → pathwaysutils-0.1.11}/pathwaysutils/experimental/gke/__init__.py +0 -0
  40. {pathwaysutils-0.1.9 → pathwaysutils-0.1.11}/pathwaysutils/experimental/profiling.py +0 -0
  41. {pathwaysutils-0.1.9 → pathwaysutils-0.1.11}/pathwaysutils/experimental/shared_pathways_service/README.md +0 -0
  42. {pathwaysutils-0.1.9 → pathwaysutils-0.1.11}/pathwaysutils/experimental/shared_pathways_service/__init__.py +0 -0
  43. {pathwaysutils-0.1.9 → pathwaysutils-0.1.11}/pathwaysutils/experimental/shared_pathways_service/run_connect_example.py +0 -0
  44. {pathwaysutils-0.1.9 → pathwaysutils-0.1.11}/pathwaysutils/experimental/shared_pathways_service/start_vscode_on_cpu_np.py +0 -0
  45. {pathwaysutils-0.1.9 → pathwaysutils-0.1.11}/pathwaysutils/experimental/shared_pathways_service/yamls/code-server.yaml +0 -0
  46. {pathwaysutils-0.1.9 → pathwaysutils-0.1.11}/pathwaysutils/experimental/shared_pathways_service/yamls/pw-proxy.yaml +0 -0
  47. {pathwaysutils-0.1.9 → pathwaysutils-0.1.11}/pathwaysutils/jax/__init__.py +0 -0
  48. {pathwaysutils-0.1.9 → pathwaysutils-0.1.11}/pathwaysutils/persistence/__init__.py +0 -0
  49. {pathwaysutils-0.1.9 → pathwaysutils-0.1.11}/pathwaysutils/proxy_backend.py +0 -0
  50. {pathwaysutils-0.1.9 → pathwaysutils-0.1.11}/pathwaysutils/test/__init__.py +0 -0
  51. {pathwaysutils-0.1.9 → pathwaysutils-0.1.11}/pathwaysutils/test/debug/__init__.py +0 -0
  52. {pathwaysutils-0.1.9 → pathwaysutils-0.1.11}/pathwaysutils/test/debug/timing_test.py +0 -0
  53. {pathwaysutils-0.1.9 → pathwaysutils-0.1.11}/pathwaysutils/test/debug/watchdog_test.py +0 -0
  54. {pathwaysutils-0.1.9 → pathwaysutils-0.1.11}/pathwaysutils/test/experimental/reshard_test.py +0 -0
  55. {pathwaysutils-0.1.9 → pathwaysutils-0.1.11}/pathwaysutils/test/initialize_test.py +0 -0
  56. {pathwaysutils-0.1.9 → pathwaysutils-0.1.11}/pathwaysutils/test/lru_cache_test.py +0 -0
  57. {pathwaysutils-0.1.9 → pathwaysutils-0.1.11}/pathwaysutils/test/persistence_test.py +0 -0
  58. {pathwaysutils-0.1.9 → pathwaysutils-0.1.11}/pathwaysutils/test/plugin_executable_test.py +0 -0
  59. {pathwaysutils-0.1.9 → pathwaysutils-0.1.11}/pathwaysutils/test/proxy_backend_test.py +0 -0
  60. {pathwaysutils-0.1.9 → pathwaysutils-0.1.11}/pathwaysutils/test/reshard_test.py +0 -0
@@ -1,10 +1,10 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pathwaysutils
3
- Version: 0.1.9
3
+ Version: 0.1.11
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>
7
- Requires-Python: >=3.10
7
+ Requires-Python: >=3.11
8
8
  Description-Content-Type: text/markdown
9
9
  Classifier: License :: OSI Approved :: Apache Software License
10
10
  License-File: LICENSE
@@ -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.11"
@@ -34,7 +34,7 @@ def _log_thread_stack(thread: threading.Thread):
34
34
  _logger.debug(
35
35
  "".join(
36
36
  traceback.format_stack(
37
- sys._current_frames().get( # pylint: disable=protected-access
37
+ sys._current_frames().get( # pylint: disable=protected-access # pyrefly: ignore[no-matching-overload]
38
38
  thread.ident, []
39
39
  )
40
40
  )
@@ -121,9 +121,9 @@ class DefaultSliceHealthChecker(SliceHealthChecker):
121
121
  _logger.debug(
122
122
  "Caught JaxRuntimeError for slice_index=%s: %s", slice_index, error
123
123
  )
124
- if not is_error_due_to_slice_down(error):
124
+ if not is_error_due_to_slice_down(error, log_traceback=False):
125
125
  raise
126
- return active_slice_indices
126
+ return frozenset(active_slice_indices)
127
127
 
128
128
 
129
129
 
@@ -249,7 +249,9 @@ def wait_for_slices(
249
249
  time.sleep(time_to_sleep)
250
250
 
251
251
 
252
- def is_error_due_to_slice_down(error: Exception) -> bool:
252
+ def is_error_due_to_slice_down(
253
+ error: Exception, log_traceback: bool = True
254
+ ) -> bool:
253
255
  """Returns True if the error is due to slice down.
254
256
 
255
257
  The error types that are considered due to slice down are
@@ -261,6 +263,7 @@ def is_error_due_to_slice_down(error: Exception) -> bool:
261
263
 
262
264
  Args:
263
265
  error: The error to check.
266
+ log_traceback: If True, log the traceback of the error.
264
267
  """
265
268
  error_due_to_slice_down = False
266
269
  traceback_logging_level = logging.DEBUG
@@ -290,9 +293,7 @@ def is_error_due_to_slice_down(error: Exception) -> bool:
290
293
 
291
294
  error_due_to_slice_down = True
292
295
 
293
- if not error_due_to_slice_down:
294
- _logger.debug("Caught an error not due to slice down")
295
-
296
- _logger.log(traceback_logging_level, "Error details:", exc_info=True)
296
+ if not error_due_to_slice_down and log_traceback:
297
+ _logger.log(traceback_logging_level, "Error details:", exc_info=True)
297
298
 
298
299
  return error_due_to_slice_down
@@ -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,23 +67,43 @@ 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
 
72
86
  Attributes:
73
87
  slice_to_devices: A mapping from slice index to a sequence of `jax.Device`
74
88
  objects for that slice.
75
- all_slice_indices: A set of all possible slice indices.
76
- active_slice_indices: A set of indices of the currently active slices.
77
- new_slice_event: A `threading.Event` that is set when new slices become
78
- available during replica/resize mode.
89
+ all_slice_indices: A set of all possible slice indices in the allocation.
90
+ active_slice_indices: A set of indices of the slices currently participating
91
+ in the JAX computation mesh.
92
+ inactive_slice_indices: A set of indices of the slices in the allocation
93
+ that are not currently participating in the JAX computation mesh (e.g.
94
+ because they are down or have not joined yet).
95
+ available_inactive_slices: A set of indices of the inactive slices that
96
+ have been detected as up and healthy by the background monitor thread,
97
+ but have not yet been joined to the active JAX computation mesh.
79
98
  """
80
99
 
81
100
  slice_to_devices: Mapping[int, Sequence[jax.Device]]
82
101
  all_slice_indices: Set[int]
83
102
  active_slice_indices: Set[int]
84
- new_slice_event: threading.Event
85
-
103
+ inactive_slice_indices: Set[int]
104
+ available_inactive_slices: Set[int]
105
+ _stop_event: threading.Event | None
106
+ _monitor_thread: threading.Thread | None
86
107
  def __init__(self, devices: Sequence[jax.Device] | None = None) -> None:
87
108
  """Initializes the manager.
88
109
 
@@ -93,12 +114,55 @@ class Manager:
93
114
  devices = jax.devices()
94
115
  self.slice_to_devices = elastic.get_slice_to_devices(devices)
95
116
 
96
- self.all_slice_indices = set(self.slice_to_devices.keys())
117
+ self.all_slice_indices = frozenset(self.slice_to_devices.keys())
97
118
 
98
119
  self.active_slice_indices = elastic.get_active_slice_indices(
99
120
  slice_to_devices=self.slice_to_devices
100
121
  )
101
- self.new_slice_event = threading.Event()
122
+ self.inactive_slice_indices = self.all_slice_indices - self.active_slice_indices
123
+ self.available_inactive_slices = frozenset()
124
+
125
+ self._stop_event = None
126
+ self._monitor_thread = None
127
+
128
+ def start_monitoring(self, poll_interval: float | int = 10) -> None:
129
+ """Starts the background monitor thread.
130
+
131
+ Args:
132
+ poll_interval: The number of seconds to wait between activity checks.
133
+ """
134
+ if self._monitor_thread is not None and self._monitor_thread.is_alive():
135
+ _logger.warning("Monitor thread is already running.")
136
+ return
137
+
138
+ self._stop_event = threading.Event()
139
+ self._monitor_thread = threading.Thread(
140
+ target=self._monitor_new_slices,
141
+ args=(self._stop_event, poll_interval),
142
+ daemon=True,
143
+ )
144
+ self._monitor_thread.start()
145
+ _logger.info("Elastic monitor thread started with interval %s.", poll_interval)
146
+
147
+ def close(self) -> None:
148
+ """Stops the background monitor thread."""
149
+ if self._stop_event is not None:
150
+ self._stop_event.set()
151
+ if self._monitor_thread is not None:
152
+ _logger.info("Closing manager, waiting for monitor thread to stop...")
153
+ try:
154
+ self._monitor_thread.join(timeout=5)
155
+ if self._monitor_thread.is_alive():
156
+ _logger.warning(
157
+ "Elastic monitor thread failed to stop within 5s timeout."
158
+ )
159
+ except RuntimeError as e:
160
+ if "cannot join thread" in str(e):
161
+ pass
162
+ else:
163
+ raise
164
+ self._monitor_thread = None
165
+ self._stop_event = None
102
166
 
103
167
  @functools.cached_property
104
168
  def total_slice_count(self) -> int:
@@ -122,9 +186,15 @@ class Manager:
122
186
  return len(self.active_slice_indices)
123
187
 
124
188
  @property
125
- def inactive_slice_indices(self) -> set[int]:
126
- """The set of inactive slice indices."""
127
- return self.all_slice_indices - self.active_slice_indices
189
+ def new_slice_event(self) -> threading.Event:
190
+ """Deprecated compatibility property for un-updated MaxText code.
191
+
192
+ TODO: b/527183831 - Remove this property once MaxText CL 2 is submitted.
193
+ """
194
+ event = threading.Event()
195
+ if self.available_inactive_slices:
196
+ event.set()
197
+ return event
128
198
 
129
199
  def scale_by_active_slices(self, x: int | float) -> int | float:
130
200
  """Scale x by the number of active slices."""
@@ -157,46 +227,68 @@ class Manager:
157
227
  for array in jax.live_arrays():
158
228
  array.delete()
159
229
 
160
- def _monitor_new_slices(
161
- self, stop_event: threading.Event, poll_interval: float | int
162
- ) -> None:
163
- """Monitors for new slices and sets the `new_slice_event` if found."""
164
- while not stop_event.wait(poll_interval):
165
- try:
166
- if not self.inactive_slice_indices:
167
- _logger.debug("No inactive slices to check.")
168
- continue
230
+ def _check_inactive_slices(self) -> None:
231
+ """Checks inactive slices and updates available_inactive_slices."""
232
+ if not self.inactive_slice_indices:
233
+ _logger.debug("No inactive slices to check.")
234
+ if self.available_inactive_slices:
235
+ self.available_inactive_slices = frozenset()
236
+ return
169
237
 
170
- _logger.debug(
171
- "Checking inactive slices: %s", self.inactive_slice_indices
172
- )
173
- inactive_slice_to_devices = {
174
- i: self.slice_to_devices[i] for i in self.inactive_slice_indices
175
- }
176
- newly_active_indices = elastic.get_active_slice_indices(
177
- inactive_slice_to_devices
178
- )
238
+ _logger.debug(
239
+ "Now checking inactive slices %s", self.inactive_slice_indices
240
+ )
241
+ inactive_slice_to_devices = {
242
+ i: self.slice_to_devices[i] for i in self.inactive_slice_indices
243
+ }
244
+ found_slices = elastic.get_active_slice_indices(
245
+ inactive_slice_to_devices
246
+ )
179
247
 
180
- if newly_active_indices:
181
- _logger.info(
182
- "New slices found: %s. Setting new slice event.",
183
- newly_active_indices,
184
- )
185
- self.new_slice_event.set()
186
- return
248
+ _logger.debug(
249
+ "Found available and inactive slices %s", found_slices
250
+ )
187
251
 
188
- _logger.debug("No new slices found.")
189
- except Exception: # pylint: disable=broad-exception-caught
190
- _logger.exception("Error in monitor thread")
252
+ # Filter against active_slice_indices in case the main thread initiated scale-up
253
+ # and claimed these slices while this background health check was running.
254
+ found_slices = found_slices - self.active_slice_indices
255
+
256
+ if found_slices != self.available_inactive_slices:
257
+ _logger.info(
258
+ "Newly available but inactive slices %s", found_slices
259
+ )
260
+ self.available_inactive_slices = frozenset(found_slices)
261
+
262
+ def _monitor_new_slices(
263
+ self, stop_event: threading.Event, poll_interval: float | int
264
+ ) -> None:
265
+ """Monitors for new slices and updates available_inactive_slices."""
266
+ _logger.info("Elastic monitor thread started.")
267
+ try:
268
+ while not stop_event.wait(poll_interval):
269
+ try:
270
+ self._check_inactive_slices()
271
+ except Exception: # pylint: disable=broad-exception-caught
272
+ _logger.exception("Error in monitor thread loop")
273
+ except BaseException as e:
274
+ _logger.critical(
275
+ "Catastrophic error in monitor thread, thread is dying!",
276
+ exc_info=True,
277
+ )
278
+ raise
279
+ finally:
280
+ _logger.info("Elastic monitor thread stopped.")
191
281
 
192
282
  def elastic_retry(
193
283
  self,
194
- max_retries: int,
284
+ # TODO: b/523384843 - Remove max_retries parameter.
285
+ max_retries: int | None = None,
195
286
  minimum_slice_count: int | None = None,
196
287
  poll_interval: float | int = 10,
197
288
  timeout: float | None = None,
198
289
  pre_callback: Callable[..., Any] | None = None,
199
290
  on_elastic_event_callback: Callable[..., Any] | None = None,
291
+ retry_policy: RetryPolicy | None = None,
200
292
  ) -> Callable[[_F], _F]:
201
293
  """Retries a function with elasticity fault tolerance.
202
294
 
@@ -211,10 +303,10 @@ class Manager:
211
303
  (i.e., it waits for all slices to be active).
212
304
 
213
305
  When `minimum_slice_count` is less than the total number of slices, a
214
- background thread will monitor for new slices becoming available and set
215
- `self.new_slice_event`. The user code can then poll this event and raise
216
- a `ScaleUpSignalError` to gracefully interrupt the current execution and
217
- trigger a retry.
306
+ background thread will monitor for newly joined inactive slices and populate
307
+ `self.available_inactive_slices`. User code can check this set (e.g. at step
308
+ boundaries) and raise a `ScaleUpSignalError` to gracefully interrupt the
309
+ current execution and trigger a retry with the expanded hardware.
218
310
 
219
311
  Often, the function will dispatch JAX operations and wait for them to
220
312
  complete while creating a log message. If using Python logging, it is
@@ -224,6 +316,8 @@ class Manager:
224
316
 
225
317
  Args:
226
318
  max_retries: The maximum number of times to retry the function.
319
+ Deprecated: Use `retry_policy` instead.
320
+ TODO: b/523384843 - Remove max_retries parameter.
227
321
  minimum_slice_count: The minimum number of slices required to run the
228
322
  function. If None, defaults to the total number of slices.
229
323
  poll_interval: The number of seconds to wait between activity checks.
@@ -233,6 +327,11 @@ class Manager:
233
327
  pre_callback: A callback to call before the function is attempted.
234
328
  on_elastic_event_callback: A callback to call after an elastic failure
235
329
  occurs.
330
+ retry_policy: A policy (callable) to determine if a retry should be
331
+ attempted. It accepts the attempt number (1-indexed) and the exception
332
+ that triggered the retry. If it returns False, no more retries are
333
+ attempted. If neither `retry_policy` nor `max_retries` is specified, it
334
+ defaults to unlimited retries.
236
335
 
237
336
  Returns:
238
337
  A decorator that retries the wrapped function.
@@ -248,77 +347,94 @@ class Manager:
248
347
  else minimum_slice_count
249
348
  )
250
349
 
251
- if max_retries <= 0:
252
- raise ValueError("max_retries must be positive.")
350
+ if max_retries is not None and retry_policy is not None:
351
+ raise ValueError("Cannot specify both max_retries and retry_policy.")
352
+
353
+ if retry_policy is None:
354
+ if max_retries is None:
355
+ # Default to unlimited retries if neither parameter is supplied.
356
+ retry_policy = lambda attempt, error: True
357
+ else:
358
+ if max_retries <= 0:
359
+ raise ValueError("max_retries must be positive.")
360
+ retry_policy = ElasticRetryLimit(max_retries)
253
361
 
254
362
  def decorator(func: _F) -> _F:
255
363
  @functools.wraps(func)
256
364
  def wrapper(*args: Any, **kwargs: Any) -> Any:
365
+ self.start_monitoring(poll_interval)
257
366
 
258
- def attempt_execution(retry_index: int) -> Any:
259
- _logger.info(
260
- "Elastic attempt %d out of %d", retry_index + 1, max_retries
261
- )
367
+ def attempt_execution(attempt: int) -> Any:
368
+ _logger.info("Elastic attempt %d", attempt)
262
369
  self.active_slice_indices = elastic.wait_for_slices(
263
370
  slice_count=target_slice_count,
264
371
  slice_to_devices=self.slice_to_devices,
265
372
  poll_interval=poll_interval,
266
373
  timeout=timeout,
267
374
  )
375
+ self.inactive_slice_indices = (
376
+ self.all_slice_indices - self.active_slice_indices
377
+ )
378
+ # Reset available_inactive_slices at attempt start since
379
+ # active_slice_indices has just been updated by wait_for_slices.
380
+ self.available_inactive_slices = frozenset()
268
381
  if pre_callback is not None:
269
382
  pre_callback()
270
383
 
271
384
  with jax.default_device(self.default_device):
272
- self.new_slice_event.clear()
273
- stop_event = threading.Event()
274
-
275
- if target_slice_count < self.total_slice_count:
276
- monitor_thread = threading.Thread(
277
- target=self._monitor_new_slices,
278
- args=(stop_event, poll_interval),
279
- daemon=True,
280
- )
281
- monitor_thread.start()
282
- else:
283
- monitor_thread = None
284
-
385
+ return func(*args, **kwargs)
386
+
387
+ def handle_scale_up_error(attempt: int, error: ScaleUpSignalError) -> None:
388
+ _logger.info("Scale up requested.")
389
+ _elastic_event_cleanup()
390
+ # Reset available_inactive_slices before retry callback and next attempt.
391
+ self.available_inactive_slices = frozenset()
392
+
393
+ if on_elastic_event_callback is not None:
394
+ on_elastic_event_callback()
395
+
396
+ if not retry_policy(attempt, error):
397
+ _logger.info("Retry policy rejected retry after ScaleUpSignalError.")
398
+ raise ElasticRuntimeError(
399
+ f"Elastic attempt {attempt} failed."
400
+ ) from error
401
+
402
+ def handle_slice_down_error(
403
+ attempt: int, error: jax.errors.JaxRuntimeError
404
+ ) -> None:
405
+ if not elastic.is_error_due_to_slice_down(error):
406
+ raise
407
+
408
+ _logger.exception("Elastic event detected")
409
+ _elastic_event_cleanup()
410
+ # Reset available_inactive_slices on slice-down failure before retry.
411
+ self.available_inactive_slices = frozenset()
412
+
413
+ if on_elastic_event_callback is not None:
414
+ on_elastic_event_callback()
415
+
416
+ if not retry_policy(attempt, error):
417
+ _logger.info(
418
+ "Retry policy rejected retry after jax.errors.JaxRuntimeError."
419
+ )
420
+ raise ElasticRuntimeError(
421
+ f"Elastic attempt {attempt} failed."
422
+ ) from error
423
+
424
+ try:
425
+ attempt = 1
426
+ while True:
285
427
  try:
286
- return func(*args, **kwargs)
287
- finally:
288
- stop_event.set()
289
- if monitor_thread is not None:
290
- monitor_thread.join()
291
-
292
- for retry_index in range(max_retries):
293
- try:
294
- return attempt_execution(retry_index)
295
- except ScaleUpSignalError:
296
- _logger.info("Scale up requested. Retrying.")
297
- _elastic_event_cleanup()
298
-
299
- if on_elastic_event_callback is not None:
300
- on_elastic_event_callback()
301
- except jax.errors.JaxRuntimeError as error:
302
- if not elastic.is_error_due_to_slice_down(error):
303
- raise
304
-
305
- if self.new_slice_event.is_set():
306
- _logger.info(
307
- "Slice down event and new slice available detected. Retrying."
308
- )
309
- else:
310
- _logger.info("Slice down event detected. Retrying.")
311
-
312
- _elastic_event_cleanup()
313
-
314
- if on_elastic_event_callback is not None:
315
- on_elastic_event_callback()
316
- else:
317
- raise ElasticRuntimeError(
318
- f"Elastic attempt {max_retries} out of {max_retries} failed."
319
- )
320
-
321
- return wrapper
428
+ return attempt_execution(attempt)
429
+ except ScaleUpSignalError as error:
430
+ handle_scale_up_error(attempt, error)
431
+ except jax.errors.JaxRuntimeError as error:
432
+ handle_slice_down_error(attempt, error)
433
+ attempt += 1
434
+ finally:
435
+ self.close()
436
+
437
+ return wrapper # pyrefly: ignore[bad-return]
322
438
 
323
439
  return decorator
324
440