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.
- {pathwaysutils-0.1.9 → pathwaysutils-0.1.11}/PKG-INFO +2 -2
- {pathwaysutils-0.1.9 → pathwaysutils-0.1.11}/pathwaysutils/__init__.py +1 -1
- {pathwaysutils-0.1.9 → pathwaysutils-0.1.11}/pathwaysutils/debug/watchdog.py +1 -1
- {pathwaysutils-0.1.9 → pathwaysutils-0.1.11}/pathwaysutils/elastic/elastic.py +8 -7
- {pathwaysutils-0.1.9 → pathwaysutils-0.1.11}/pathwaysutils/elastic/manager.py +216 -100
- pathwaysutils-0.1.11/pathwaysutils/experimental/gke/jobset.py +923 -0
- pathwaysutils-0.1.11/pathwaysutils/experimental/reshard.py +218 -0
- {pathwaysutils-0.1.9 → pathwaysutils-0.1.11}/pathwaysutils/experimental/shared_pathways_service/deploy_pathways_service.py +59 -48
- pathwaysutils-0.1.11/pathwaysutils/experimental/shared_pathways_service/dockerfiles/sample_sidecar.dockerfile +27 -0
- {pathwaysutils-0.1.9 → pathwaysutils-0.1.11}/pathwaysutils/experimental/shared_pathways_service/gke_utils.py +56 -0
- {pathwaysutils-0.1.9 → pathwaysutils-0.1.11}/pathwaysutils/experimental/shared_pathways_service/isc_pathways.py +38 -17
- {pathwaysutils-0.1.9 → pathwaysutils-0.1.11}/pathwaysutils/experimental/shared_pathways_service/metrics_collector.py +29 -7
- {pathwaysutils-0.1.9 → pathwaysutils-0.1.11}/pathwaysutils/experimental/shared_pathways_service/run_workload.py +2 -4
- pathwaysutils-0.1.11/pathwaysutils/experimental/shared_pathways_service/tpu_specs.py +109 -0
- {pathwaysutils-0.1.9 → pathwaysutils-0.1.11}/pathwaysutils/experimental/shared_pathways_service/validators.py +99 -0
- {pathwaysutils-0.1.9 → pathwaysutils-0.1.11}/pathwaysutils/experimental/split_by_mesh_axis.py +1 -1
- {pathwaysutils-0.1.9 → pathwaysutils-0.1.11}/pathwaysutils/lru_cache.py +3 -3
- {pathwaysutils-0.1.9 → pathwaysutils-0.1.11}/pathwaysutils/persistence/helper.py +6 -6
- {pathwaysutils-0.1.9 → pathwaysutils-0.1.11}/pathwaysutils/persistence/orbax_handler.py +5 -5
- {pathwaysutils-0.1.9 → pathwaysutils-0.1.11}/pathwaysutils/plugin_executable.py +2 -2
- {pathwaysutils-0.1.9 → pathwaysutils-0.1.11}/pathwaysutils/profiling.py +14 -9
- {pathwaysutils-0.1.9/pathwaysutils/experimental → pathwaysutils-0.1.11/pathwaysutils}/reshard.py +79 -159
- pathwaysutils-0.1.11/pathwaysutils/test/experimental/gke/jobset_test.py +977 -0
- 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
- {pathwaysutils-0.1.9 → pathwaysutils-0.1.11}/pathwaysutils/test/profiling_test.py +25 -7
- {pathwaysutils-0.1.9 → pathwaysutils-0.1.11}/pyproject.toml +1 -1
- pathwaysutils-0.1.9/pathwaysutils/experimental/gke/jobset.py +0 -414
- pathwaysutils-0.1.9/pathwaysutils/reshard.py +0 -133
- pathwaysutils-0.1.9/pathwaysutils/test/experimental/gke/jobset_test.py +0 -137
- {pathwaysutils-0.1.9 → pathwaysutils-0.1.11}/LICENSE +0 -0
- {pathwaysutils-0.1.9 → pathwaysutils-0.1.11}/README.md +0 -0
- {pathwaysutils-0.1.9 → pathwaysutils-0.1.11}/pathwaysutils/_initialize.py +0 -0
- {pathwaysutils-0.1.9 → pathwaysutils-0.1.11}/pathwaysutils/collect_profile.py +0 -0
- {pathwaysutils-0.1.9 → pathwaysutils-0.1.11}/pathwaysutils/debug/__init__.py +0 -0
- {pathwaysutils-0.1.9 → pathwaysutils-0.1.11}/pathwaysutils/debug/timing.py +0 -0
- {pathwaysutils-0.1.9 → pathwaysutils-0.1.11}/pathwaysutils/elastic/__init__.py +0 -0
- {pathwaysutils-0.1.9 → pathwaysutils-0.1.11}/pathwaysutils/experimental/__init__.py +0 -0
- {pathwaysutils-0.1.9 → pathwaysutils-0.1.11}/pathwaysutils/experimental/concatenate_by_mesh_axis.py +0 -0
- {pathwaysutils-0.1.9 → pathwaysutils-0.1.11}/pathwaysutils/experimental/gke/__init__.py +0 -0
- {pathwaysutils-0.1.9 → pathwaysutils-0.1.11}/pathwaysutils/experimental/profiling.py +0 -0
- {pathwaysutils-0.1.9 → pathwaysutils-0.1.11}/pathwaysutils/experimental/shared_pathways_service/README.md +0 -0
- {pathwaysutils-0.1.9 → pathwaysutils-0.1.11}/pathwaysutils/experimental/shared_pathways_service/__init__.py +0 -0
- {pathwaysutils-0.1.9 → pathwaysutils-0.1.11}/pathwaysutils/experimental/shared_pathways_service/run_connect_example.py +0 -0
- {pathwaysutils-0.1.9 → pathwaysutils-0.1.11}/pathwaysutils/experimental/shared_pathways_service/start_vscode_on_cpu_np.py +0 -0
- {pathwaysutils-0.1.9 → pathwaysutils-0.1.11}/pathwaysutils/experimental/shared_pathways_service/yamls/code-server.yaml +0 -0
- {pathwaysutils-0.1.9 → pathwaysutils-0.1.11}/pathwaysutils/experimental/shared_pathways_service/yamls/pw-proxy.yaml +0 -0
- {pathwaysutils-0.1.9 → pathwaysutils-0.1.11}/pathwaysutils/jax/__init__.py +0 -0
- {pathwaysutils-0.1.9 → pathwaysutils-0.1.11}/pathwaysutils/persistence/__init__.py +0 -0
- {pathwaysutils-0.1.9 → pathwaysutils-0.1.11}/pathwaysutils/proxy_backend.py +0 -0
- {pathwaysutils-0.1.9 → pathwaysutils-0.1.11}/pathwaysutils/test/__init__.py +0 -0
- {pathwaysutils-0.1.9 → pathwaysutils-0.1.11}/pathwaysutils/test/debug/__init__.py +0 -0
- {pathwaysutils-0.1.9 → pathwaysutils-0.1.11}/pathwaysutils/test/debug/timing_test.py +0 -0
- {pathwaysutils-0.1.9 → pathwaysutils-0.1.11}/pathwaysutils/test/debug/watchdog_test.py +0 -0
- {pathwaysutils-0.1.9 → pathwaysutils-0.1.11}/pathwaysutils/test/experimental/reshard_test.py +0 -0
- {pathwaysutils-0.1.9 → pathwaysutils-0.1.11}/pathwaysutils/test/initialize_test.py +0 -0
- {pathwaysutils-0.1.9 → pathwaysutils-0.1.11}/pathwaysutils/test/lru_cache_test.py +0 -0
- {pathwaysutils-0.1.9 → pathwaysutils-0.1.11}/pathwaysutils/test/persistence_test.py +0 -0
- {pathwaysutils-0.1.9 → pathwaysutils-0.1.11}/pathwaysutils/test/plugin_executable_test.py +0 -0
- {pathwaysutils-0.1.9 → pathwaysutils-0.1.11}/pathwaysutils/test/proxy_backend_test.py +0 -0
- {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.
|
|
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.
|
|
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
|
|
@@ -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(
|
|
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.
|
|
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
|
|
77
|
-
|
|
78
|
-
|
|
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
|
-
|
|
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 =
|
|
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.
|
|
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
|
|
126
|
-
"""
|
|
127
|
-
|
|
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
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
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
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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
|
-
|
|
181
|
-
|
|
182
|
-
|
|
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
|
-
|
|
189
|
-
|
|
190
|
-
|
|
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
|
-
|
|
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
|
|
215
|
-
`self.
|
|
216
|
-
a `ScaleUpSignalError` to gracefully interrupt the
|
|
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
|
|
252
|
-
raise ValueError("max_retries
|
|
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(
|
|
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
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
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
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
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
|
|