pathwaysutils 0.1.8__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.8 → pathwaysutils-0.1.10}/PKG-INFO +2 -2
- {pathwaysutils-0.1.8 → pathwaysutils-0.1.10}/pathwaysutils/__init__.py +1 -1
- {pathwaysutils-0.1.8 → pathwaysutils-0.1.10}/pathwaysutils/elastic/elastic.py +84 -72
- {pathwaysutils-0.1.8 → pathwaysutils-0.1.10}/pathwaysutils/elastic/manager.py +65 -20
- pathwaysutils-0.1.10/pathwaysutils/experimental/gke/__init__.py +11 -0
- pathwaysutils-0.1.10/pathwaysutils/experimental/gke/jobset.py +579 -0
- pathwaysutils-0.1.10/pathwaysutils/experimental/reshard.py +218 -0
- {pathwaysutils-0.1.8 → pathwaysutils-0.1.10}/pathwaysutils/experimental/shared_pathways_service/deploy_pathways_service.py +12 -2
- pathwaysutils-0.1.10/pathwaysutils/experimental/shared_pathways_service/dockerfiles/sample_sidecar.dockerfile +27 -0
- {pathwaysutils-0.1.8 → pathwaysutils-0.1.10}/pathwaysutils/experimental/shared_pathways_service/gke_utils.py +216 -34
- {pathwaysutils-0.1.8 → pathwaysutils-0.1.10}/pathwaysutils/experimental/shared_pathways_service/isc_pathways.py +70 -19
- {pathwaysutils-0.1.8 → pathwaysutils-0.1.10}/pathwaysutils/experimental/shared_pathways_service/metrics_collector.py +28 -6
- {pathwaysutils-0.1.8 → pathwaysutils-0.1.10}/pathwaysutils/experimental/shared_pathways_service/run_connect_example.py +4 -4
- {pathwaysutils-0.1.8 → pathwaysutils-0.1.10}/pathwaysutils/experimental/shared_pathways_service/run_workload.py +5 -19
- pathwaysutils-0.1.10/pathwaysutils/experimental/shared_pathways_service/start_vscode_on_cpu_np.py +162 -0
- pathwaysutils-0.1.10/pathwaysutils/experimental/shared_pathways_service/validators.py +234 -0
- pathwaysutils-0.1.10/pathwaysutils/experimental/shared_pathways_service/yamls/code-server.yaml +58 -0
- {pathwaysutils-0.1.8 → pathwaysutils-0.1.10}/pathwaysutils/experimental/shared_pathways_service/yamls/pw-proxy.yaml +1 -1
- {pathwaysutils-0.1.8 → pathwaysutils-0.1.10}/pathwaysutils/experimental/shared_pathways_service/yamls/pw-service.yaml +45 -2
- {pathwaysutils-0.1.8 → pathwaysutils-0.1.10}/pathwaysutils/jax/__init__.py +1 -1
- {pathwaysutils-0.1.8 → pathwaysutils-0.1.10}/pathwaysutils/persistence/helper.py +0 -28
- {pathwaysutils-0.1.8 → pathwaysutils-0.1.10}/pathwaysutils/persistence/orbax_handler.py +3 -3
- {pathwaysutils-0.1.8 → pathwaysutils-0.1.10}/pathwaysutils/profiling.py +105 -42
- {pathwaysutils-0.1.8/pathwaysutils/experimental → pathwaysutils-0.1.10/pathwaysutils}/reshard.py +78 -158
- pathwaysutils-0.1.10/pathwaysutils/test/experimental/gke/jobset_test.py +210 -0
- {pathwaysutils-0.1.8 → pathwaysutils-0.1.10}/pathwaysutils/test/experimental/reshard_test.py +15 -3
- {pathwaysutils-0.1.8 → pathwaysutils-0.1.10}/pathwaysutils/test/profiling_test.py +152 -46
- {pathwaysutils-0.1.8 → pathwaysutils-0.1.10}/pathwaysutils/test/reshard_test.py +10 -2
- {pathwaysutils-0.1.8 → pathwaysutils-0.1.10}/pyproject.toml +1 -1
- pathwaysutils-0.1.8/pathwaysutils/experimental/shared_pathways_service/validators.py +0 -107
- pathwaysutils-0.1.8/pathwaysutils/reshard.py +0 -133
- {pathwaysutils-0.1.8 → pathwaysutils-0.1.10}/LICENSE +0 -0
- {pathwaysutils-0.1.8 → pathwaysutils-0.1.10}/README.md +0 -0
- {pathwaysutils-0.1.8 → pathwaysutils-0.1.10}/pathwaysutils/_initialize.py +0 -0
- {pathwaysutils-0.1.8 → pathwaysutils-0.1.10}/pathwaysutils/collect_profile.py +0 -0
- {pathwaysutils-0.1.8 → pathwaysutils-0.1.10}/pathwaysutils/debug/__init__.py +0 -0
- {pathwaysutils-0.1.8 → pathwaysutils-0.1.10}/pathwaysutils/debug/timing.py +0 -0
- {pathwaysutils-0.1.8 → pathwaysutils-0.1.10}/pathwaysutils/debug/watchdog.py +0 -0
- {pathwaysutils-0.1.8 → pathwaysutils-0.1.10}/pathwaysutils/elastic/__init__.py +0 -0
- {pathwaysutils-0.1.8 → pathwaysutils-0.1.10}/pathwaysutils/experimental/__init__.py +0 -0
- {pathwaysutils-0.1.8 → pathwaysutils-0.1.10}/pathwaysutils/experimental/concatenate_by_mesh_axis.py +0 -0
- {pathwaysutils-0.1.8 → pathwaysutils-0.1.10}/pathwaysutils/experimental/profiling.py +0 -0
- {pathwaysutils-0.1.8 → pathwaysutils-0.1.10}/pathwaysutils/experimental/shared_pathways_service/README.md +0 -0
- {pathwaysutils-0.1.8 → pathwaysutils-0.1.10}/pathwaysutils/experimental/shared_pathways_service/__init__.py +0 -0
- {pathwaysutils-0.1.8 → pathwaysutils-0.1.10}/pathwaysutils/experimental/split_by_mesh_axis.py +0 -0
- {pathwaysutils-0.1.8 → pathwaysutils-0.1.10}/pathwaysutils/lru_cache.py +0 -0
- {pathwaysutils-0.1.8 → pathwaysutils-0.1.10}/pathwaysutils/persistence/__init__.py +0 -0
- {pathwaysutils-0.1.8 → pathwaysutils-0.1.10}/pathwaysutils/plugin_executable.py +0 -0
- {pathwaysutils-0.1.8 → pathwaysutils-0.1.10}/pathwaysutils/proxy_backend.py +0 -0
- {pathwaysutils-0.1.8 → pathwaysutils-0.1.10}/pathwaysutils/test/__init__.py +0 -0
- {pathwaysutils-0.1.8 → pathwaysutils-0.1.10}/pathwaysutils/test/debug/__init__.py +0 -0
- {pathwaysutils-0.1.8 → pathwaysutils-0.1.10}/pathwaysutils/test/debug/timing_test.py +0 -0
- {pathwaysutils-0.1.8 → pathwaysutils-0.1.10}/pathwaysutils/test/debug/watchdog_test.py +0 -0
- {pathwaysutils-0.1.8 → pathwaysutils-0.1.10}/pathwaysutils/test/initialize_test.py +0 -0
- {pathwaysutils-0.1.8 → pathwaysutils-0.1.10}/pathwaysutils/test/lru_cache_test.py +0 -0
- {pathwaysutils-0.1.8 → pathwaysutils-0.1.10}/pathwaysutils/test/persistence_test.py +0 -0
- {pathwaysutils-0.1.8 → pathwaysutils-0.1.10}/pathwaysutils/test/plugin_executable_test.py +0 -0
- {pathwaysutils-0.1.8 → pathwaysutils-0.1.10}/pathwaysutils/test/proxy_backend_test.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pathwaysutils
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.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>
|
|
@@ -10,7 +10,7 @@ Classifier: License :: OSI Approved :: Apache Software License
|
|
|
10
10
|
License-File: LICENSE
|
|
11
11
|
Requires-Dist: absl-py
|
|
12
12
|
Requires-Dist: fastapi
|
|
13
|
-
Requires-Dist: jax>=0.8.
|
|
13
|
+
Requires-Dist: jax>=0.8.3
|
|
14
14
|
Requires-Dist: orbax-checkpoint
|
|
15
15
|
Requires-Dist: uvicorn
|
|
16
16
|
Requires-Dist: requests
|
|
@@ -18,10 +18,13 @@ retries a function in case of `jax.errors.JaxRuntimeError` caused by slice down
|
|
|
18
18
|
events. It also provides a utility for waiting for slices to become active.
|
|
19
19
|
"""
|
|
20
20
|
|
|
21
|
+
from abc import ABC, abstractmethod
|
|
21
22
|
import collections
|
|
22
|
-
from collections.abc import Mapping, Sequence, Set
|
|
23
|
+
from collections.abc import Callable, Mapping, Sequence, Set
|
|
24
|
+
import itertools
|
|
23
25
|
import logging
|
|
24
26
|
import time
|
|
27
|
+
from typing import Any
|
|
25
28
|
|
|
26
29
|
import jax
|
|
27
30
|
import numpy as np
|
|
@@ -41,44 +44,87 @@ _ELASTIC_DOWN_ADDITIONAL_ERROR_TYPES = frozenset([
|
|
|
41
44
|
])
|
|
42
45
|
|
|
43
46
|
|
|
44
|
-
|
|
45
|
-
"""
|
|
47
|
+
class SliceHealthChecker(ABC):
|
|
48
|
+
"""Base class for slice health checkers.
|
|
46
49
|
|
|
47
|
-
|
|
50
|
+
Implementations must provide `dispatch` and `validate` methods.
|
|
51
|
+
`dispatch` starts the health check operations, and `validate` waits for them
|
|
52
|
+
and returns the active slice indices.
|
|
53
|
+
"""
|
|
48
54
|
|
|
49
|
-
|
|
50
|
-
|
|
55
|
+
def __init__(self, slice_to_devices: Mapping[int, Sequence[jax.Device]]):
|
|
56
|
+
self.slice_to_devices = slice_to_devices
|
|
51
57
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
return x + 1
|
|
58
|
+
@abstractmethod
|
|
59
|
+
def dispatch(self) -> None:
|
|
60
|
+
"""Dispatches the JAX operations in the background."""
|
|
56
61
|
|
|
62
|
+
@abstractmethod
|
|
63
|
+
def validate(self) -> Set[int]:
|
|
64
|
+
"""Blocks on results and returns the set of active slice indices.
|
|
57
65
|
|
|
58
|
-
|
|
59
|
-
|
|
66
|
+
Returns:
|
|
67
|
+
Set of active slice indices.
|
|
68
|
+
"""
|
|
60
69
|
|
|
61
|
-
This function is used to test if a slice is active. It executes a simple
|
|
62
|
-
computation on the devices and returns the result. If any of the devices are
|
|
63
|
-
not active, the returned array will fail with a JaxRuntimeError used.
|
|
64
70
|
|
|
65
|
-
|
|
66
|
-
|
|
71
|
+
class DefaultSliceHealthChecker(SliceHealthChecker):
|
|
72
|
+
"""Default implementation that checks each slice individually.
|
|
67
73
|
|
|
68
|
-
|
|
69
|
-
|
|
74
|
+
It executes a simple computation on each slice independently to verify
|
|
75
|
+
that the devices are active and responsive.
|
|
70
76
|
|
|
71
|
-
|
|
72
|
-
|
|
77
|
+
The computation involves creating a zero array of the same size as the
|
|
78
|
+
number of devices in the slice, adding (_SIMPLE_EXECUTION_TEST_VALUE - 1)
|
|
79
|
+
to it, and then running a pmap to add 1 to each element. This verifies
|
|
80
|
+
that the devices are active and can perform computations.
|
|
73
81
|
"""
|
|
74
|
-
if not devices:
|
|
75
|
-
raise ValueError("No devices")
|
|
76
82
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
83
|
+
def _plus_one(self, x: jax.Array) -> jax.Array:
|
|
84
|
+
return x + 1
|
|
85
|
+
|
|
86
|
+
def _simple_execution(self, devices: Sequence[jax.Device]) -> jax.Array:
|
|
87
|
+
if not devices:
|
|
88
|
+
raise ValueError("No devices")
|
|
89
|
+
|
|
90
|
+
test_input = np.zeros(len(devices), dtype=float) + (
|
|
91
|
+
_SIMPLE_EXECUTION_TEST_VALUE - 1
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
return jax.pmap(self._plus_one, devices=devices)(test_input)
|
|
95
|
+
|
|
96
|
+
def dispatch(self) -> None:
|
|
97
|
+
self.results = {
|
|
98
|
+
slice_index: self._simple_execution(devices)
|
|
99
|
+
for slice_index, devices in self.slice_to_devices.items()
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
def validate(self) -> Set[int]:
|
|
103
|
+
active_slice_indices = set()
|
|
104
|
+
for slice_index, x in self.results.items():
|
|
105
|
+
expected = (
|
|
106
|
+
np.zeros(len(self.slice_to_devices[slice_index]), dtype=float)
|
|
107
|
+
+ _SIMPLE_EXECUTION_TEST_VALUE
|
|
108
|
+
)
|
|
109
|
+
try:
|
|
110
|
+
jax.block_until_ready(x)
|
|
111
|
+
if np.allclose(x, expected):
|
|
112
|
+
active_slice_indices.add(slice_index)
|
|
113
|
+
else:
|
|
114
|
+
msg = (
|
|
115
|
+
f"Error with _simple_execution for slice_index={slice_index}. "
|
|
116
|
+
f"Expected: {expected!r}, Actual: {x!r}"
|
|
117
|
+
)
|
|
118
|
+
_logger.error(msg)
|
|
119
|
+
raise ValueError(msg)
|
|
120
|
+
except jax.errors.JaxRuntimeError as error:
|
|
121
|
+
_logger.debug(
|
|
122
|
+
"Caught JaxRuntimeError for slice_index=%s: %s", slice_index, error
|
|
123
|
+
)
|
|
124
|
+
if not is_error_due_to_slice_down(error):
|
|
125
|
+
raise
|
|
126
|
+
return active_slice_indices
|
|
80
127
|
|
|
81
|
-
return jax.pmap(_plus_one, devices=devices)(test_input)
|
|
82
128
|
|
|
83
129
|
|
|
84
130
|
def get_slice_to_devices(
|
|
@@ -94,6 +140,7 @@ def get_slice_to_devices(
|
|
|
94
140
|
@timing.timeit
|
|
95
141
|
def get_active_slice_indices(
|
|
96
142
|
slice_to_devices: Mapping[int, Sequence[jax.Device]] | None = None,
|
|
143
|
+
checker: SliceHealthChecker | None = None,
|
|
97
144
|
) -> Set[int]:
|
|
98
145
|
"""Returns the set of active slices indices.
|
|
99
146
|
|
|
@@ -101,6 +148,8 @@ def get_active_slice_indices(
|
|
|
101
148
|
slice_to_devices: A mapping from slice index to devices. If None,
|
|
102
149
|
`get_slice_to_devices(jax.devices())` is used to gather all available
|
|
103
150
|
devices and group them by slice.
|
|
151
|
+
checker: A SliceHealthChecker instance. If None, DefaultSliceHealthChecker
|
|
152
|
+
is used.
|
|
104
153
|
|
|
105
154
|
Returns:
|
|
106
155
|
A set of integers representing the indices of the active slices.
|
|
@@ -109,55 +158,16 @@ def get_active_slice_indices(
|
|
|
109
158
|
_logger.debug("slice_to_devices is None. Getting from jax.devices().")
|
|
110
159
|
slice_to_devices = get_slice_to_devices(tuple(jax.devices()))
|
|
111
160
|
|
|
161
|
+
if checker is None:
|
|
162
|
+
checker = DefaultSliceHealthChecker(slice_to_devices)
|
|
163
|
+
|
|
112
164
|
_logger.debug(
|
|
113
165
|
"Getting active slice indices for slices: %s",
|
|
114
166
|
sorted(list(slice_to_devices.keys())),
|
|
115
167
|
)
|
|
116
168
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
results = {
|
|
120
|
-
slice_index: _simple_execution(devices)
|
|
121
|
-
for slice_index, devices in slice_to_devices.items()
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
for slice_index, x in results.items():
|
|
125
|
-
_logger.debug("Checking slice_index=%s", slice_index)
|
|
126
|
-
expected = (
|
|
127
|
-
np.zeros(len(slice_to_devices[slice_index]), dtype=float)
|
|
128
|
-
+ _SIMPLE_EXECUTION_TEST_VALUE
|
|
129
|
-
)
|
|
130
|
-
try:
|
|
131
|
-
with timing.Timer(f"Checking {slice_index=}"):
|
|
132
|
-
_logger.debug("Blocking until ready for slice_index=%s", slice_index)
|
|
133
|
-
jax.block_until_ready(x)
|
|
134
|
-
_logger.debug("Execution finished for slice_index=%s", slice_index)
|
|
135
|
-
if np.allclose(x, expected):
|
|
136
|
-
active_slice_indices.add(slice_index)
|
|
137
|
-
_logger.debug("slice_index=%s active", slice_index)
|
|
138
|
-
else:
|
|
139
|
-
_logger.error(
|
|
140
|
-
"Error with _simple_execution for slice_index=%s. "
|
|
141
|
-
"This should never happen. Expected: %r, Actual: %r",
|
|
142
|
-
slice_index,
|
|
143
|
-
expected,
|
|
144
|
-
x,
|
|
145
|
-
)
|
|
146
|
-
raise ValueError(
|
|
147
|
-
f"Error with _simple_execution for slice_index={slice_index}."
|
|
148
|
-
)
|
|
149
|
-
except jax.errors.JaxRuntimeError as error:
|
|
150
|
-
_logger.debug(
|
|
151
|
-
"Caught JaxRuntimeError for slice_index=%s: %s", slice_index, error
|
|
152
|
-
)
|
|
153
|
-
if not is_error_due_to_slice_down(error):
|
|
154
|
-
_logger.info("Re-raising error for slice_index=%s", slice_index)
|
|
155
|
-
raise
|
|
156
|
-
_logger.debug("slice_index=%s bad", slice_index)
|
|
157
|
-
|
|
158
|
-
_logger.debug("active_slice_indices=%s", active_slice_indices)
|
|
159
|
-
|
|
160
|
-
return active_slice_indices
|
|
169
|
+
checker.dispatch()
|
|
170
|
+
return checker.validate()
|
|
161
171
|
|
|
162
172
|
|
|
163
173
|
def wait_for_slices(
|
|
@@ -165,6 +175,7 @@ def wait_for_slices(
|
|
|
165
175
|
poll_interval: float | int = 10,
|
|
166
176
|
timeout: float | int | None = None,
|
|
167
177
|
slice_to_devices: Mapping[int, Sequence[jax.Device]] | None = None,
|
|
178
|
+
checker: SliceHealthChecker | None = None,
|
|
168
179
|
) -> Set[int]:
|
|
169
180
|
"""Waits until after at least `slice_count` slices become active.
|
|
170
181
|
|
|
@@ -177,6 +188,7 @@ def wait_for_slices(
|
|
|
177
188
|
timeout.
|
|
178
189
|
slice_to_devices: A mapping from slice index to devices. If None,
|
|
179
190
|
`get_slice_to_devices(jax.devices())` is used.
|
|
191
|
+
checker: A SliceHealthChecker instance to use for checking health.
|
|
180
192
|
|
|
181
193
|
Returns:
|
|
182
194
|
The active slice indices
|
|
@@ -201,7 +213,7 @@ def wait_for_slices(
|
|
|
201
213
|
check_start_time = time.time()
|
|
202
214
|
|
|
203
215
|
_logger.debug("Checking active slices...")
|
|
204
|
-
active_slice_indices = get_active_slice_indices(slice_to_devices)
|
|
216
|
+
active_slice_indices = get_active_slice_indices(slice_to_devices, checker)
|
|
205
217
|
if len(active_slice_indices) >= slice_count:
|
|
206
218
|
_logger.info(
|
|
207
219
|
"Sufficient slices active: %s >= %s. Active indices: %s",
|
|
@@ -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
|
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
2
|
+
# you may not use this file except in compliance with the License.
|
|
3
|
+
# You may obtain a copy of the License at
|
|
4
|
+
#
|
|
5
|
+
# https://www.apache.org/licenses/LICENSE-2.0
|
|
6
|
+
#
|
|
7
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
8
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
9
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
10
|
+
# See the License for the specific language governing permissions and
|
|
11
|
+
# limitations under the License.
|