pathwaysutils 0.1.6__tar.gz → 0.1.8__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.6 → pathwaysutils-0.1.8}/PKG-INFO +2 -2
- {pathwaysutils-0.1.6 → pathwaysutils-0.1.8}/pathwaysutils/__init__.py +4 -3
- {pathwaysutils-0.1.6 → pathwaysutils-0.1.8}/pathwaysutils/collect_profile.py +3 -3
- {pathwaysutils-0.1.6 → pathwaysutils-0.1.8}/pathwaysutils/debug/watchdog.py +19 -6
- {pathwaysutils-0.1.6 → pathwaysutils-0.1.8}/pathwaysutils/elastic/elastic.py +46 -15
- pathwaysutils-0.1.8/pathwaysutils/elastic/manager.py +398 -0
- pathwaysutils-0.1.8/pathwaysutils/experimental/concatenate_by_mesh_axis.py +152 -0
- {pathwaysutils-0.1.6 → pathwaysutils-0.1.8}/pathwaysutils/experimental/profiling.py +3 -2
- {pathwaysutils-0.1.6 → pathwaysutils-0.1.8}/pathwaysutils/experimental/reshard.py +14 -12
- pathwaysutils-0.1.8/pathwaysutils/experimental/shared_pathways_service/deploy_pathways_service.py +261 -0
- {pathwaysutils-0.1.6 → pathwaysutils-0.1.8}/pathwaysutils/experimental/shared_pathways_service/gke_utils.py +29 -1
- {pathwaysutils-0.1.6 → pathwaysutils-0.1.8}/pathwaysutils/experimental/shared_pathways_service/isc_pathways.py +170 -12
- pathwaysutils-0.1.8/pathwaysutils/experimental/shared_pathways_service/metrics_collector.py +296 -0
- {pathwaysutils-0.1.6 → pathwaysutils-0.1.8}/pathwaysutils/experimental/shared_pathways_service/run_connect_example.py +6 -0
- pathwaysutils-0.1.8/pathwaysutils/experimental/shared_pathways_service/run_workload.py +180 -0
- pathwaysutils-0.1.8/pathwaysutils/experimental/shared_pathways_service/yamls/pw-service.yaml +152 -0
- {pathwaysutils-0.1.6 → pathwaysutils-0.1.8}/pathwaysutils/experimental/split_by_mesh_axis.py +4 -2
- {pathwaysutils-0.1.6 → pathwaysutils-0.1.8}/pathwaysutils/jax/__init__.py +20 -18
- {pathwaysutils-0.1.6 → pathwaysutils-0.1.8}/pathwaysutils/lru_cache.py +7 -3
- {pathwaysutils-0.1.6 → pathwaysutils-0.1.8}/pathwaysutils/persistence/helper.py +9 -8
- {pathwaysutils-0.1.6 → pathwaysutils-0.1.8}/pathwaysutils/persistence/orbax_handler.py +2 -2
- {pathwaysutils-0.1.6 → pathwaysutils-0.1.8}/pathwaysutils/plugin_executable.py +3 -4
- {pathwaysutils-0.1.6 → pathwaysutils-0.1.8}/pathwaysutils/profiling.py +130 -31
- {pathwaysutils-0.1.6 → pathwaysutils-0.1.8}/pathwaysutils/proxy_backend.py +4 -4
- {pathwaysutils-0.1.6 → pathwaysutils-0.1.8}/pathwaysutils/reshard.py +2 -1
- {pathwaysutils-0.1.6 → pathwaysutils-0.1.8}/pathwaysutils/test/debug/timing_test.py +20 -16
- pathwaysutils-0.1.8/pathwaysutils/test/debug/watchdog_test.py +185 -0
- {pathwaysutils-0.1.6 → pathwaysutils-0.1.8}/pathwaysutils/test/profiling_test.py +194 -39
- {pathwaysutils-0.1.6 → pathwaysutils-0.1.8}/pathwaysutils/test/proxy_backend_test.py +2 -2
- {pathwaysutils-0.1.6 → pathwaysutils-0.1.8}/pyproject.toml +1 -1
- pathwaysutils-0.1.6/pathwaysutils/elastic/manager.py +0 -171
- pathwaysutils-0.1.6/pathwaysutils/experimental/shared_pathways_service/yamls/pw-service-example.yaml +0 -18
- pathwaysutils-0.1.6/pathwaysutils/test/debug/watchdog_test.py +0 -94
- {pathwaysutils-0.1.6 → pathwaysutils-0.1.8}/LICENSE +0 -0
- {pathwaysutils-0.1.6 → pathwaysutils-0.1.8}/README.md +0 -0
- {pathwaysutils-0.1.6 → pathwaysutils-0.1.8}/pathwaysutils/_initialize.py +0 -0
- {pathwaysutils-0.1.6 → pathwaysutils-0.1.8}/pathwaysutils/debug/__init__.py +0 -0
- {pathwaysutils-0.1.6 → pathwaysutils-0.1.8}/pathwaysutils/debug/timing.py +0 -0
- {pathwaysutils-0.1.6 → pathwaysutils-0.1.8}/pathwaysutils/elastic/__init__.py +0 -0
- {pathwaysutils-0.1.6 → pathwaysutils-0.1.8}/pathwaysutils/experimental/__init__.py +0 -0
- {pathwaysutils-0.1.6 → pathwaysutils-0.1.8}/pathwaysutils/experimental/shared_pathways_service/README.md +0 -0
- {pathwaysutils-0.1.6 → pathwaysutils-0.1.8}/pathwaysutils/experimental/shared_pathways_service/__init__.py +0 -0
- {pathwaysutils-0.1.6 → pathwaysutils-0.1.8}/pathwaysutils/experimental/shared_pathways_service/validators.py +0 -0
- {pathwaysutils-0.1.6 → pathwaysutils-0.1.8}/pathwaysutils/experimental/shared_pathways_service/yamls/pw-proxy.yaml +0 -0
- {pathwaysutils-0.1.6 → pathwaysutils-0.1.8}/pathwaysutils/persistence/__init__.py +0 -0
- {pathwaysutils-0.1.6 → pathwaysutils-0.1.8}/pathwaysutils/test/__init__.py +0 -0
- {pathwaysutils-0.1.6 → pathwaysutils-0.1.8}/pathwaysutils/test/debug/__init__.py +0 -0
- {pathwaysutils-0.1.6 → pathwaysutils-0.1.8}/pathwaysutils/test/experimental/reshard_test.py +0 -0
- {pathwaysutils-0.1.6 → pathwaysutils-0.1.8}/pathwaysutils/test/initialize_test.py +0 -0
- {pathwaysutils-0.1.6 → pathwaysutils-0.1.8}/pathwaysutils/test/lru_cache_test.py +0 -0
- {pathwaysutils-0.1.6 → pathwaysutils-0.1.8}/pathwaysutils/test/persistence_test.py +0 -0
- {pathwaysutils-0.1.6 → pathwaysutils-0.1.8}/pathwaysutils/test/plugin_executable_test.py +0 -0
- {pathwaysutils-0.1.6 → pathwaysutils-0.1.8}/pathwaysutils/test/reshard_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.8
|
|
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.
|
|
13
|
+
Requires-Dist: jax>=0.8.0
|
|
14
14
|
Requires-Dist: orbax-checkpoint
|
|
15
15
|
Requires-Dist: uvicorn
|
|
16
16
|
Requires-Dist: requests
|
|
@@ -12,12 +12,13 @@
|
|
|
12
12
|
# See the License for the specific language governing permissions and
|
|
13
13
|
# limitations under the License.
|
|
14
14
|
"""Package of Pathways-on-Cloud utilities."""
|
|
15
|
+
from collections.abc import Callable
|
|
15
16
|
from pathwaysutils import _initialize
|
|
16
17
|
|
|
17
|
-
initialize = _initialize.initialize
|
|
18
|
-
is_pathways_backend_used = _initialize.is_pathways_backend_used
|
|
18
|
+
initialize: Callable[[], None] = _initialize.initialize
|
|
19
|
+
is_pathways_backend_used: Callable[[], bool] = _initialize.is_pathways_backend_used
|
|
19
20
|
|
|
20
21
|
del _initialize
|
|
21
22
|
|
|
22
23
|
# When changing this, also update the CHANGELOG.md.
|
|
23
|
-
__version__ = "v0.1.
|
|
24
|
+
__version__ = "v0.1.8"
|
|
@@ -26,7 +26,7 @@ _logger = logging.getLogger(__name__)
|
|
|
26
26
|
_logger.setLevel(logging.INFO)
|
|
27
27
|
|
|
28
28
|
|
|
29
|
-
_DESCRIPTION = """
|
|
29
|
+
_DESCRIPTION: str = """
|
|
30
30
|
To profile running JAX programs, you first need to start the profiler server
|
|
31
31
|
in the program of interest. You can do this via
|
|
32
32
|
`jax.profiler.start_server(<port>)`. Once the program is running and the
|
|
@@ -36,7 +36,7 @@ for a provided duration. The trace file will be dumped into a GCS bucket
|
|
|
36
36
|
"""
|
|
37
37
|
|
|
38
38
|
|
|
39
|
-
def _get_parser():
|
|
39
|
+
def _get_parser() -> argparse.ArgumentParser:
|
|
40
40
|
"""Returns an argument parser for the collect_profile script."""
|
|
41
41
|
parser = argparse.ArgumentParser(description=_DESCRIPTION)
|
|
42
42
|
parser.add_argument(
|
|
@@ -62,7 +62,7 @@ def _get_parser():
|
|
|
62
62
|
return parser
|
|
63
63
|
|
|
64
64
|
|
|
65
|
-
def main():
|
|
65
|
+
def main() -> None:
|
|
66
66
|
parser = _get_parser()
|
|
67
67
|
args = parser.parse_args()
|
|
68
68
|
|
|
@@ -43,12 +43,17 @@ def _log_thread_stack(thread: threading.Thread):
|
|
|
43
43
|
|
|
44
44
|
|
|
45
45
|
@contextlib.contextmanager
|
|
46
|
-
def watchdog(
|
|
46
|
+
def watchdog(
|
|
47
|
+
name: str,
|
|
48
|
+
timeout: float,
|
|
49
|
+
repeat: bool = True,
|
|
50
|
+
):
|
|
47
51
|
"""Watchdog context manager.
|
|
48
52
|
|
|
49
53
|
Prints the stack trace of all threads after `timeout` seconds.
|
|
50
54
|
|
|
51
55
|
Args:
|
|
56
|
+
name: The name of the watchdog, used in log messages.
|
|
52
57
|
timeout: The timeout in seconds. If the timeout is reached, the stack trace
|
|
53
58
|
of all threads will be printed.
|
|
54
59
|
repeat: Whether to repeat the watchdog after the timeout. If False, the
|
|
@@ -63,7 +68,10 @@ def watchdog(timeout: float, repeat: bool = True):
|
|
|
63
68
|
count = 0
|
|
64
69
|
while not event.wait(timeout):
|
|
65
70
|
_logger.debug(
|
|
66
|
-
"
|
|
71
|
+
"'%s' watchdog thread stack dump every %s seconds. Count: %s",
|
|
72
|
+
name,
|
|
73
|
+
timeout,
|
|
74
|
+
count,
|
|
67
75
|
)
|
|
68
76
|
try:
|
|
69
77
|
for thread in threading.enumerate():
|
|
@@ -73,17 +81,22 @@ def watchdog(timeout: float, repeat: bool = True):
|
|
|
73
81
|
_logger.debug("Error print traceback for thread: %s", thread.ident)
|
|
74
82
|
finally:
|
|
75
83
|
if not repeat:
|
|
76
|
-
_logger.critical("Timeout from watchdog
|
|
84
|
+
_logger.critical("Timeout from watchdog '%s'", name)
|
|
77
85
|
os.abort()
|
|
78
86
|
|
|
79
87
|
count += 1
|
|
80
88
|
|
|
81
|
-
_logger.debug(
|
|
82
|
-
|
|
89
|
+
_logger.debug(
|
|
90
|
+
"Registering '%s' watchdog with timeout %s seconds and repeat=%s",
|
|
91
|
+
name,
|
|
92
|
+
timeout,
|
|
93
|
+
repeat,
|
|
94
|
+
)
|
|
95
|
+
watchdog_thread = threading.Thread(target=handler, name=name)
|
|
83
96
|
watchdog_thread.start()
|
|
84
97
|
try:
|
|
85
98
|
yield
|
|
86
99
|
finally:
|
|
87
100
|
event.set()
|
|
88
101
|
watchdog_thread.join()
|
|
89
|
-
_logger.debug("Deregistering watchdog")
|
|
102
|
+
_logger.debug("Deregistering '%s' watchdog", name)
|
|
@@ -19,7 +19,7 @@ events. It also provides a utility for waiting for slices to become active.
|
|
|
19
19
|
"""
|
|
20
20
|
|
|
21
21
|
import collections
|
|
22
|
-
from collections.abc import Mapping, Sequence
|
|
22
|
+
from collections.abc import Mapping, Sequence, Set
|
|
23
23
|
import logging
|
|
24
24
|
import time
|
|
25
25
|
|
|
@@ -83,7 +83,7 @@ def _simple_execution(devices: Sequence[jax.Device]) -> jax.Array:
|
|
|
83
83
|
|
|
84
84
|
def get_slice_to_devices(
|
|
85
85
|
devices: Sequence[jax.Device],
|
|
86
|
-
) ->
|
|
86
|
+
) -> Mapping[int, Sequence[jax.Device]]:
|
|
87
87
|
"""Returns the mapping from slice index to devices."""
|
|
88
88
|
slice_to_devices = collections.defaultdict(list)
|
|
89
89
|
for d in devices:
|
|
@@ -94,7 +94,7 @@ def get_slice_to_devices(
|
|
|
94
94
|
@timing.timeit
|
|
95
95
|
def get_active_slice_indices(
|
|
96
96
|
slice_to_devices: Mapping[int, Sequence[jax.Device]] | None = None,
|
|
97
|
-
) ->
|
|
97
|
+
) -> Set[int]:
|
|
98
98
|
"""Returns the set of active slices indices.
|
|
99
99
|
|
|
100
100
|
Args:
|
|
@@ -106,8 +106,14 @@ def get_active_slice_indices(
|
|
|
106
106
|
A set of integers representing the indices of the active slices.
|
|
107
107
|
"""
|
|
108
108
|
if slice_to_devices is None:
|
|
109
|
+
_logger.debug("slice_to_devices is None. Getting from jax.devices().")
|
|
109
110
|
slice_to_devices = get_slice_to_devices(tuple(jax.devices()))
|
|
110
111
|
|
|
112
|
+
_logger.debug(
|
|
113
|
+
"Getting active slice indices for slices: %s",
|
|
114
|
+
sorted(list(slice_to_devices.keys())),
|
|
115
|
+
)
|
|
116
|
+
|
|
111
117
|
active_slice_indices = set()
|
|
112
118
|
|
|
113
119
|
results = {
|
|
@@ -116,17 +122,19 @@ def get_active_slice_indices(
|
|
|
116
122
|
}
|
|
117
123
|
|
|
118
124
|
for slice_index, x in results.items():
|
|
119
|
-
_logger.
|
|
125
|
+
_logger.debug("Checking slice_index=%s", slice_index)
|
|
120
126
|
expected = (
|
|
121
127
|
np.zeros(len(slice_to_devices[slice_index]), dtype=float)
|
|
122
128
|
+ _SIMPLE_EXECUTION_TEST_VALUE
|
|
123
129
|
)
|
|
124
130
|
try:
|
|
125
131
|
with timing.Timer(f"Checking {slice_index=}"):
|
|
132
|
+
_logger.debug("Blocking until ready for slice_index=%s", slice_index)
|
|
126
133
|
jax.block_until_ready(x)
|
|
134
|
+
_logger.debug("Execution finished for slice_index=%s", slice_index)
|
|
127
135
|
if np.allclose(x, expected):
|
|
128
136
|
active_slice_indices.add(slice_index)
|
|
129
|
-
_logger.
|
|
137
|
+
_logger.debug("slice_index=%s active", slice_index)
|
|
130
138
|
else:
|
|
131
139
|
_logger.error(
|
|
132
140
|
"Error with _simple_execution for slice_index=%s. "
|
|
@@ -139,11 +147,15 @@ def get_active_slice_indices(
|
|
|
139
147
|
f"Error with _simple_execution for slice_index={slice_index}."
|
|
140
148
|
)
|
|
141
149
|
except jax.errors.JaxRuntimeError as error:
|
|
150
|
+
_logger.debug(
|
|
151
|
+
"Caught JaxRuntimeError for slice_index=%s: %s", slice_index, error
|
|
152
|
+
)
|
|
142
153
|
if not is_error_due_to_slice_down(error):
|
|
154
|
+
_logger.info("Re-raising error for slice_index=%s", slice_index)
|
|
143
155
|
raise
|
|
144
|
-
_logger.
|
|
156
|
+
_logger.debug("slice_index=%s bad", slice_index)
|
|
145
157
|
|
|
146
|
-
_logger.
|
|
158
|
+
_logger.debug("active_slice_indices=%s", active_slice_indices)
|
|
147
159
|
|
|
148
160
|
return active_slice_indices
|
|
149
161
|
|
|
@@ -153,7 +165,7 @@ def wait_for_slices(
|
|
|
153
165
|
poll_interval: float | int = 10,
|
|
154
166
|
timeout: float | int | None = None,
|
|
155
167
|
slice_to_devices: Mapping[int, Sequence[jax.Device]] | None = None,
|
|
156
|
-
) ->
|
|
168
|
+
) -> Set[int]:
|
|
157
169
|
"""Waits until after at least `slice_count` slices become active.
|
|
158
170
|
|
|
159
171
|
Args:
|
|
@@ -174,22 +186,36 @@ def wait_for_slices(
|
|
|
174
186
|
active.
|
|
175
187
|
"""
|
|
176
188
|
if slice_to_devices is None:
|
|
189
|
+
_logger.debug("slice_to_devices is None. Getting from jax.devices().")
|
|
177
190
|
slice_to_devices = get_slice_to_devices(jax.devices())
|
|
178
191
|
|
|
192
|
+
_logger.info(
|
|
193
|
+
"Waiting for %s slices. Poll interval: %s, Timeout: %s",
|
|
194
|
+
slice_count,
|
|
195
|
+
poll_interval,
|
|
196
|
+
timeout,
|
|
197
|
+
)
|
|
179
198
|
start_time = time.time()
|
|
180
199
|
|
|
181
200
|
while True:
|
|
182
201
|
check_start_time = time.time()
|
|
183
202
|
|
|
203
|
+
_logger.debug("Checking active slices...")
|
|
184
204
|
active_slice_indices = get_active_slice_indices(slice_to_devices)
|
|
185
205
|
if len(active_slice_indices) >= slice_count:
|
|
186
|
-
_logger.info(
|
|
206
|
+
_logger.info(
|
|
207
|
+
"Sufficient slices active: %s >= %s. Active indices: %s",
|
|
208
|
+
len(active_slice_indices),
|
|
209
|
+
slice_count,
|
|
210
|
+
active_slice_indices,
|
|
211
|
+
)
|
|
187
212
|
return active_slice_indices
|
|
188
213
|
|
|
189
214
|
_logger.info(
|
|
190
|
-
"%s slices active. Wanting at least %s.",
|
|
215
|
+
"%s slices active. Wanting at least %s. Active indices: %s",
|
|
191
216
|
len(active_slice_indices),
|
|
192
217
|
slice_count,
|
|
218
|
+
active_slice_indices,
|
|
193
219
|
)
|
|
194
220
|
|
|
195
221
|
time_to_sleep = max(0, poll_interval - (time.time() - check_start_time))
|
|
@@ -206,7 +232,7 @@ def wait_for_slices(
|
|
|
206
232
|
)
|
|
207
233
|
|
|
208
234
|
if time_to_sleep > 0:
|
|
209
|
-
_logger.
|
|
235
|
+
_logger.debug("Sleeping for %.2f seconds.", time_to_sleep)
|
|
210
236
|
|
|
211
237
|
time.sleep(time_to_sleep)
|
|
212
238
|
|
|
@@ -228,10 +254,14 @@ def is_error_due_to_slice_down(error: Exception) -> bool:
|
|
|
228
254
|
traceback_logging_level = logging.DEBUG
|
|
229
255
|
|
|
230
256
|
if isinstance(error, jax.errors.JaxRuntimeError):
|
|
257
|
+
_logger.debug("Checking if JaxRuntimeError is due to slice down: %s", error)
|
|
231
258
|
if any(
|
|
232
259
|
error_type in str(error) for error_type in _ELASTIC_DOWN_ERROR_TYPES
|
|
233
260
|
):
|
|
234
|
-
_logger.
|
|
261
|
+
_logger.debug(
|
|
262
|
+
"Caught an error due to slice down (matched"
|
|
263
|
+
" _ELASTIC_DOWN_ERROR_TYPES)"
|
|
264
|
+
)
|
|
235
265
|
|
|
236
266
|
error_due_to_slice_down = True
|
|
237
267
|
|
|
@@ -240,15 +270,16 @@ def is_error_due_to_slice_down(error: Exception) -> bool:
|
|
|
240
270
|
for error_type in _ELASTIC_DOWN_ADDITIONAL_ERROR_TYPES
|
|
241
271
|
):
|
|
242
272
|
_logger.warning(
|
|
243
|
-
"Caught an error
|
|
244
|
-
" error will be treated
|
|
273
|
+
"Caught an error that may or may not be due to slice down (matched"
|
|
274
|
+
" _ELASTIC_DOWN_ADDITIONAL_ERROR_TYPES). This error will be treated"
|
|
275
|
+
" as due to slice down."
|
|
245
276
|
)
|
|
246
277
|
traceback_logging_level = logging.WARNING
|
|
247
278
|
|
|
248
279
|
error_due_to_slice_down = True
|
|
249
280
|
|
|
250
281
|
if not error_due_to_slice_down:
|
|
251
|
-
_logger.
|
|
282
|
+
_logger.debug("Caught an error not due to slice down")
|
|
252
283
|
|
|
253
284
|
_logger.log(traceback_logging_level, "Error details:", exc_info=True)
|
|
254
285
|
|
|
@@ -0,0 +1,398 @@
|
|
|
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
|
+
"""Elasticity manager.
|
|
15
|
+
|
|
16
|
+
This class provides a utility for elastic training. It provides a decorator that
|
|
17
|
+
retries a function in case of `jax.errors.JaxRuntimeError` caused by slice down
|
|
18
|
+
events. It also provides a utility for waiting for slices to become active.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from collections.abc import Callable, Mapping, Sequence, Set
|
|
22
|
+
import functools
|
|
23
|
+
import logging
|
|
24
|
+
import threading
|
|
25
|
+
from typing import Any, TypeVar
|
|
26
|
+
import warnings
|
|
27
|
+
|
|
28
|
+
import jax
|
|
29
|
+
from pathwaysutils.elastic import elastic
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
_logger = logging.getLogger(__name__)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class ElasticRuntimeError(RuntimeError):
|
|
36
|
+
"""Error raised when elasticity cannot continue."""
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class ScaleUpSignalError(Exception):
|
|
40
|
+
"""Signals that the workload is ready to scale up.
|
|
41
|
+
|
|
42
|
+
This exception should be raised by user code when it detects that new hardware
|
|
43
|
+
is available and it wants to restart computation to make use of it.
|
|
44
|
+
Raising this exception will interrupt the current computation and cause the
|
|
45
|
+
elasticity manager to retry it with an updated slice configuration that
|
|
46
|
+
includes the new hardware.
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
_F = TypeVar("_F", bound=Callable[..., Any])
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _elastic_event_cleanup() -> None:
|
|
54
|
+
"""Cleans up JAX profiles, caches, and live arrays."""
|
|
55
|
+
try:
|
|
56
|
+
_logger.info("Cleaning up any ongoing traces")
|
|
57
|
+
jax.profiler.stop_trace()
|
|
58
|
+
except (RuntimeError, ValueError):
|
|
59
|
+
_logger.info("No ongoing traces to clean up")
|
|
60
|
+
except Exception:
|
|
61
|
+
_logger.exception("Error cleaning up ongoing traces")
|
|
62
|
+
raise
|
|
63
|
+
|
|
64
|
+
jax.clear_caches()
|
|
65
|
+
for array in jax.live_arrays():
|
|
66
|
+
array.delete()
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class Manager:
|
|
70
|
+
"""Utility class for elastic training.
|
|
71
|
+
|
|
72
|
+
Attributes:
|
|
73
|
+
slice_to_devices: A mapping from slice index to a sequence of `jax.Device`
|
|
74
|
+
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.
|
|
79
|
+
"""
|
|
80
|
+
|
|
81
|
+
slice_to_devices: Mapping[int, Sequence[jax.Device]]
|
|
82
|
+
all_slice_indices: Set[int]
|
|
83
|
+
active_slice_indices: Set[int]
|
|
84
|
+
new_slice_event: threading.Event
|
|
85
|
+
|
|
86
|
+
def __init__(self, devices: Sequence[jax.Device] | None = None) -> None:
|
|
87
|
+
"""Initializes the manager.
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
devices: The devices to use. If None, jax.devices() is used.
|
|
91
|
+
"""
|
|
92
|
+
if devices is None:
|
|
93
|
+
devices = jax.devices()
|
|
94
|
+
self.slice_to_devices = elastic.get_slice_to_devices(devices)
|
|
95
|
+
|
|
96
|
+
self.all_slice_indices = set(self.slice_to_devices.keys())
|
|
97
|
+
|
|
98
|
+
self.active_slice_indices = elastic.get_active_slice_indices(
|
|
99
|
+
slice_to_devices=self.slice_to_devices
|
|
100
|
+
)
|
|
101
|
+
self.new_slice_event = threading.Event()
|
|
102
|
+
|
|
103
|
+
@functools.cached_property
|
|
104
|
+
def total_slice_count(self) -> int:
|
|
105
|
+
"""The total number of slices."""
|
|
106
|
+
return len(self.slice_to_devices)
|
|
107
|
+
|
|
108
|
+
@property
|
|
109
|
+
def default_device(self) -> jax.Device:
|
|
110
|
+
"""The device that should be set to the default device.
|
|
111
|
+
|
|
112
|
+
This will be from one of the slices in `active_slice_indices`.
|
|
113
|
+
"""
|
|
114
|
+
try:
|
|
115
|
+
return self.slice_to_devices[next(iter(self.active_slice_indices))][0]
|
|
116
|
+
except StopIteration as error:
|
|
117
|
+
raise ValueError("No active slices") from error
|
|
118
|
+
|
|
119
|
+
@property
|
|
120
|
+
def active_slice_count(self) -> int:
|
|
121
|
+
"""The number of active slices."""
|
|
122
|
+
return len(self.active_slice_indices)
|
|
123
|
+
|
|
124
|
+
@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
|
|
128
|
+
|
|
129
|
+
def scale_by_active_slices(self, x: int | float) -> int | float:
|
|
130
|
+
"""Scale x by the number of active slices."""
|
|
131
|
+
if isinstance(x, int):
|
|
132
|
+
quotient, remainder = divmod(
|
|
133
|
+
x * self.active_slice_count, self.total_slice_count
|
|
134
|
+
)
|
|
135
|
+
if remainder:
|
|
136
|
+
raise ValueError(
|
|
137
|
+
f"Cannot scale {x=} by active slices because it will result in a "
|
|
138
|
+
f"remainder of {remainder=}."
|
|
139
|
+
)
|
|
140
|
+
return quotient
|
|
141
|
+
elif isinstance(x, float):
|
|
142
|
+
return x * self.active_slice_count / self.total_slice_count
|
|
143
|
+
else:
|
|
144
|
+
raise ValueError(f"Unsupported type: {type(x)=}")
|
|
145
|
+
|
|
146
|
+
def _cleanup_on_retry(self):
|
|
147
|
+
"""Cleans up JAX caches and traces on retry."""
|
|
148
|
+
try:
|
|
149
|
+
_logger.debug("Cleaning up any ongoing traces")
|
|
150
|
+
jax.profiler.stop_trace()
|
|
151
|
+
except (RuntimeError, ValueError):
|
|
152
|
+
_logger.debug("No ongoing traces to clean up")
|
|
153
|
+
except Exception: # pylint: disable=broad-exception-caught
|
|
154
|
+
_logger.exception("Error cleaning up ongoing traces")
|
|
155
|
+
|
|
156
|
+
jax.clear_caches()
|
|
157
|
+
for array in jax.live_arrays():
|
|
158
|
+
array.delete()
|
|
159
|
+
|
|
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
|
|
169
|
+
|
|
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
|
+
)
|
|
179
|
+
|
|
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
|
|
187
|
+
|
|
188
|
+
_logger.debug("No new slices found.")
|
|
189
|
+
except Exception: # pylint: disable=broad-exception-caught
|
|
190
|
+
_logger.exception("Error in monitor thread")
|
|
191
|
+
|
|
192
|
+
def elastic_retry(
|
|
193
|
+
self,
|
|
194
|
+
max_retries: int,
|
|
195
|
+
minimum_slice_count: int | None = None,
|
|
196
|
+
poll_interval: float | int = 10,
|
|
197
|
+
timeout: float | None = None,
|
|
198
|
+
pre_callback: Callable[..., Any] | None = None,
|
|
199
|
+
on_elastic_event_callback: Callable[..., Any] | None = None,
|
|
200
|
+
) -> Callable[[_F], _F]:
|
|
201
|
+
"""Retries a function with elasticity fault tolerance.
|
|
202
|
+
|
|
203
|
+
This decorator wraps a function to automatically retry execution in case of
|
|
204
|
+
`jax.errors.JaxRuntimeError` caused by slice down events. It waits for
|
|
205
|
+
`minimum_slice_count` active slices before each attempt and cleans up JAX
|
|
206
|
+
caches on failure.
|
|
207
|
+
|
|
208
|
+
If `minimum_slice_count` is not met, the function will wait until at least
|
|
209
|
+
`minimum_slice_count` slices are active before execution. If
|
|
210
|
+
`minimum_slice_count` is None, it defaults to the total number of slices
|
|
211
|
+
(i.e., it waits for all slices to be active).
|
|
212
|
+
|
|
213
|
+
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.
|
|
218
|
+
|
|
219
|
+
Often, the function will dispatch JAX operations and wait for them to
|
|
220
|
+
complete while creating a log message. If using Python logging, it is
|
|
221
|
+
recommended to set `logging.raiseExceptions=True` to ensure that the
|
|
222
|
+
`jax.errors.JaxRuntimeError` is not silently ignored within the logging
|
|
223
|
+
call.
|
|
224
|
+
|
|
225
|
+
Args:
|
|
226
|
+
max_retries: The maximum number of times to retry the function.
|
|
227
|
+
minimum_slice_count: The minimum number of slices required to run the
|
|
228
|
+
function. If None, defaults to the total number of slices.
|
|
229
|
+
poll_interval: The number of seconds to wait between activity checks.
|
|
230
|
+
Defaults to 10 seconds.
|
|
231
|
+
timeout: The maximum number of seconds to wait for slices to become active
|
|
232
|
+
before each retry attempt. If None, there is no timeout.
|
|
233
|
+
pre_callback: A callback to call before the function is attempted.
|
|
234
|
+
on_elastic_event_callback: A callback to call after an elastic failure
|
|
235
|
+
occurs.
|
|
236
|
+
|
|
237
|
+
Returns:
|
|
238
|
+
A decorator that retries the wrapped function.
|
|
239
|
+
|
|
240
|
+
Raises:
|
|
241
|
+
ElasticRuntimeError: If all retry attempts fail.
|
|
242
|
+
Exception: Any other exception raised by the wrapped function that is not
|
|
243
|
+
due to a slice down event.
|
|
244
|
+
"""
|
|
245
|
+
target_slice_count = (
|
|
246
|
+
self.total_slice_count
|
|
247
|
+
if minimum_slice_count is None
|
|
248
|
+
else minimum_slice_count
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
if max_retries <= 0:
|
|
252
|
+
raise ValueError("max_retries must be positive.")
|
|
253
|
+
|
|
254
|
+
def decorator(func: _F) -> _F:
|
|
255
|
+
@functools.wraps(func)
|
|
256
|
+
def wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
257
|
+
|
|
258
|
+
def attempt_execution(retry_index: int) -> Any:
|
|
259
|
+
_logger.info(
|
|
260
|
+
"Elastic attempt %d out of %d", retry_index + 1, max_retries
|
|
261
|
+
)
|
|
262
|
+
self.active_slice_indices = elastic.wait_for_slices(
|
|
263
|
+
slice_count=target_slice_count,
|
|
264
|
+
slice_to_devices=self.slice_to_devices,
|
|
265
|
+
poll_interval=poll_interval,
|
|
266
|
+
timeout=timeout,
|
|
267
|
+
)
|
|
268
|
+
if pre_callback is not None:
|
|
269
|
+
pre_callback()
|
|
270
|
+
|
|
271
|
+
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
|
+
|
|
285
|
+
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
|
|
322
|
+
|
|
323
|
+
return decorator
|
|
324
|
+
|
|
325
|
+
def pause_resume(
|
|
326
|
+
self,
|
|
327
|
+
max_retries: int,
|
|
328
|
+
poll_interval: float | int = 10,
|
|
329
|
+
timeout: float | None = None,
|
|
330
|
+
pre_callback: Callable[..., Any] | None = None,
|
|
331
|
+
on_elastic_event_callback: Callable[..., Any] | None = None,
|
|
332
|
+
) -> Callable[[_F], _F]:
|
|
333
|
+
"""Retries a function with pause/resume fault tolerance.
|
|
334
|
+
|
|
335
|
+
DEPRECATED: Use `elastic_retry` instead.
|
|
336
|
+
|
|
337
|
+
Args:
|
|
338
|
+
max_retries: The maximum number of times to retry the function.
|
|
339
|
+
poll_interval: The number of seconds to wait between activity checks.
|
|
340
|
+
Defaults to 10 seconds.
|
|
341
|
+
timeout: The maximum number of seconds to wait for slices to become active
|
|
342
|
+
before each retry attempt. If None, there is no timeout.
|
|
343
|
+
pre_callback: A callback to call before the function is attempted.
|
|
344
|
+
on_elastic_event_callback: A callback to call after an elastic failure
|
|
345
|
+
occurs.
|
|
346
|
+
|
|
347
|
+
Returns:
|
|
348
|
+
A decorator that retries the wrapped function.
|
|
349
|
+
"""
|
|
350
|
+
warnings.warn(
|
|
351
|
+
"`pause_resume` is deprecated. Please use `elastic_retry` instead.",
|
|
352
|
+
DeprecationWarning,
|
|
353
|
+
stacklevel=2,
|
|
354
|
+
)
|
|
355
|
+
return self.elastic_retry(
|
|
356
|
+
max_retries=max_retries,
|
|
357
|
+
minimum_slice_count=None,
|
|
358
|
+
poll_interval=poll_interval,
|
|
359
|
+
timeout=timeout,
|
|
360
|
+
pre_callback=pre_callback,
|
|
361
|
+
on_elastic_event_callback=on_elastic_event_callback,
|
|
362
|
+
)
|
|
363
|
+
|
|
364
|
+
def replica_resize(
|
|
365
|
+
self,
|
|
366
|
+
max_resizes: int,
|
|
367
|
+
poll_interval: float = 10,
|
|
368
|
+
pre_callback: Callable[..., Any] | None = None,
|
|
369
|
+
on_elastic_event_callback: Callable[..., Any] | None = None,
|
|
370
|
+
) -> Callable[[_F], _F]:
|
|
371
|
+
"""Retries a function with replica/resize fault tolerance.
|
|
372
|
+
|
|
373
|
+
DEPRECATED: Use `elastic_retry` instead.
|
|
374
|
+
|
|
375
|
+
Args:
|
|
376
|
+
max_resizes: The maximum number of times to retry the function after
|
|
377
|
+
resizing the replica count.
|
|
378
|
+
poll_interval: The number of seconds to wait between active slice checks.
|
|
379
|
+
Defaults to 10 seconds.
|
|
380
|
+
pre_callback: A callback to call before the function is attempted.
|
|
381
|
+
on_elastic_event_callback: A callback to call after an elastic failure
|
|
382
|
+
occurs.
|
|
383
|
+
|
|
384
|
+
Returns:
|
|
385
|
+
A decorator that retries the wrapped function.
|
|
386
|
+
"""
|
|
387
|
+
warnings.warn(
|
|
388
|
+
"`replica_resize` is deprecated. Please use `elastic_retry` instead.",
|
|
389
|
+
DeprecationWarning,
|
|
390
|
+
stacklevel=2,
|
|
391
|
+
)
|
|
392
|
+
return self.elastic_retry(
|
|
393
|
+
max_retries=max_resizes,
|
|
394
|
+
minimum_slice_count=1,
|
|
395
|
+
poll_interval=poll_interval,
|
|
396
|
+
pre_callback=pre_callback,
|
|
397
|
+
on_elastic_event_callback=on_elastic_event_callback,
|
|
398
|
+
)
|