pathwaysutils 0.1.10__tar.gz → 0.1.12__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.10 → pathwaysutils-0.1.12}/PKG-INFO +18 -3
- {pathwaysutils-0.1.10 → pathwaysutils-0.1.12}/pathwaysutils/__init__.py +1 -1
- {pathwaysutils-0.1.10 → pathwaysutils-0.1.12}/pathwaysutils/_initialize.py +9 -5
- {pathwaysutils-0.1.10 → pathwaysutils-0.1.12}/pathwaysutils/collect_profile.py +4 -1
- {pathwaysutils-0.1.10 → pathwaysutils-0.1.12}/pathwaysutils/debug/watchdog.py +1 -1
- {pathwaysutils-0.1.10 → pathwaysutils-0.1.12}/pathwaysutils/elastic/elastic.py +15 -9
- {pathwaysutils-0.1.10 → pathwaysutils-0.1.12}/pathwaysutils/elastic/manager.py +182 -109
- pathwaysutils-0.1.12/pathwaysutils/experimental/gke/jobset.py +921 -0
- {pathwaysutils-0.1.10 → pathwaysutils-0.1.12}/pathwaysutils/experimental/shared_pathways_service/README.md +43 -14
- {pathwaysutils-0.1.10 → pathwaysutils-0.1.12}/pathwaysutils/experimental/shared_pathways_service/deploy_pathways_service.py +123 -52
- {pathwaysutils-0.1.10 → pathwaysutils-0.1.12}/pathwaysutils/experimental/shared_pathways_service/gke_utils.py +376 -56
- {pathwaysutils-0.1.10 → pathwaysutils-0.1.12}/pathwaysutils/experimental/shared_pathways_service/isc_pathways.py +269 -97
- {pathwaysutils-0.1.10 → pathwaysutils-0.1.12}/pathwaysutils/experimental/shared_pathways_service/metrics_collector.py +10 -1
- {pathwaysutils-0.1.10 → pathwaysutils-0.1.12}/pathwaysutils/experimental/shared_pathways_service/run_connect_example.py +35 -7
- {pathwaysutils-0.1.10 → pathwaysutils-0.1.12}/pathwaysutils/experimental/shared_pathways_service/run_workload.py +28 -7
- pathwaysutils-0.1.12/pathwaysutils/experimental/shared_pathways_service/tpu_specs.py +109 -0
- {pathwaysutils-0.1.10 → pathwaysutils-0.1.12}/pathwaysutils/experimental/shared_pathways_service/validators.py +0 -16
- {pathwaysutils-0.1.10 → pathwaysutils-0.1.12}/pathwaysutils/experimental/split_by_mesh_axis.py +1 -1
- {pathwaysutils-0.1.10 → pathwaysutils-0.1.12}/pathwaysutils/lru_cache.py +3 -3
- {pathwaysutils-0.1.10 → pathwaysutils-0.1.12}/pathwaysutils/persistence/helper.py +5 -18
- {pathwaysutils-0.1.10 → pathwaysutils-0.1.12}/pathwaysutils/plugin_executable.py +2 -2
- {pathwaysutils-0.1.10 → pathwaysutils-0.1.12}/pathwaysutils/profiling.py +141 -38
- {pathwaysutils-0.1.10 → pathwaysutils-0.1.12}/pathwaysutils/proxy_backend.py +14 -4
- {pathwaysutils-0.1.10 → pathwaysutils-0.1.12}/pathwaysutils/reshard.py +1 -1
- pathwaysutils-0.1.12/pathwaysutils/test/experimental/gke/jobset_test.py +850 -0
- pathwaysutils-0.1.10/pathwaysutils/experimental/shared_pathways_service/yamls/pw-service.yaml → pathwaysutils-0.1.12/pathwaysutils/test/experimental/gke/testdata/model_lite_tpuv5e_4x8.yaml +104 -75
- pathwaysutils-0.1.12/pathwaysutils/test/experimental/shared_pathways_service/deploy_pathways_service_test.py +313 -0
- pathwaysutils-0.1.12/pathwaysutils/test/experimental/shared_pathways_service/gke_utils_test.py +1599 -0
- pathwaysutils-0.1.12/pathwaysutils/test/experimental/shared_pathways_service/isc_pathways_test.py +1507 -0
- pathwaysutils-0.1.12/pathwaysutils/test/experimental/shared_pathways_service/metrics_collector_test.py +247 -0
- pathwaysutils-0.1.12/pathwaysutils/test/experimental/shared_pathways_service/run_workload_test.py +240 -0
- pathwaysutils-0.1.12/pathwaysutils/test/experimental/shared_pathways_service/start_vscode_on_cpu_np_test.py +324 -0
- pathwaysutils-0.1.12/pathwaysutils/test/experimental/shared_pathways_service/tpu_specs_test.py +77 -0
- pathwaysutils-0.1.12/pathwaysutils/test/experimental/shared_pathways_service/validators_test.py +314 -0
- {pathwaysutils-0.1.10 → pathwaysutils-0.1.12}/pathwaysutils/test/initialize_test.py +15 -0
- pathwaysutils-0.1.12/pathwaysutils/test/mock_fault_injection.py +293 -0
- pathwaysutils-0.1.12/pathwaysutils/test/mock_fault_injection_test.py +201 -0
- {pathwaysutils-0.1.10 → pathwaysutils-0.1.12}/pathwaysutils/test/profiling_test.py +335 -19
- {pathwaysutils-0.1.10 → pathwaysutils-0.1.12}/pathwaysutils/test/proxy_backend_test.py +41 -0
- {pathwaysutils-0.1.10 → pathwaysutils-0.1.12}/pyproject.toml +21 -2
- pathwaysutils-0.1.10/pathwaysutils/experimental/gke/jobset.py +0 -579
- pathwaysutils-0.1.10/pathwaysutils/persistence/orbax_handler.py +0 -292
- pathwaysutils-0.1.10/pathwaysutils/test/experimental/gke/jobset_test.py +0 -210
- {pathwaysutils-0.1.10 → pathwaysutils-0.1.12}/LICENSE +0 -0
- {pathwaysutils-0.1.10 → pathwaysutils-0.1.12}/README.md +0 -0
- {pathwaysutils-0.1.10 → pathwaysutils-0.1.12}/pathwaysutils/debug/__init__.py +0 -0
- {pathwaysutils-0.1.10 → pathwaysutils-0.1.12}/pathwaysutils/debug/timing.py +0 -0
- {pathwaysutils-0.1.10 → pathwaysutils-0.1.12}/pathwaysutils/elastic/__init__.py +0 -0
- {pathwaysutils-0.1.10 → pathwaysutils-0.1.12}/pathwaysutils/experimental/__init__.py +0 -0
- {pathwaysutils-0.1.10 → pathwaysutils-0.1.12}/pathwaysutils/experimental/concatenate_by_mesh_axis.py +0 -0
- {pathwaysutils-0.1.10 → pathwaysutils-0.1.12}/pathwaysutils/experimental/gke/__init__.py +0 -0
- {pathwaysutils-0.1.10 → pathwaysutils-0.1.12}/pathwaysutils/experimental/profiling.py +0 -0
- {pathwaysutils-0.1.10 → pathwaysutils-0.1.12}/pathwaysutils/experimental/reshard.py +0 -0
- {pathwaysutils-0.1.10 → pathwaysutils-0.1.12}/pathwaysutils/experimental/shared_pathways_service/__init__.py +0 -0
- {pathwaysutils-0.1.10 → pathwaysutils-0.1.12}/pathwaysutils/experimental/shared_pathways_service/dockerfiles/sample_sidecar.dockerfile +0 -0
- {pathwaysutils-0.1.10 → pathwaysutils-0.1.12}/pathwaysutils/experimental/shared_pathways_service/start_vscode_on_cpu_np.py +0 -0
- {pathwaysutils-0.1.10 → pathwaysutils-0.1.12}/pathwaysutils/experimental/shared_pathways_service/yamls/code-server.yaml +0 -0
- {pathwaysutils-0.1.10 → pathwaysutils-0.1.12}/pathwaysutils/experimental/shared_pathways_service/yamls/pw-proxy.yaml +0 -0
- {pathwaysutils-0.1.10 → pathwaysutils-0.1.12}/pathwaysutils/jax/__init__.py +0 -0
- {pathwaysutils-0.1.10 → pathwaysutils-0.1.12}/pathwaysutils/persistence/__init__.py +0 -0
- {pathwaysutils-0.1.10 → pathwaysutils-0.1.12}/pathwaysutils/test/__init__.py +0 -0
- {pathwaysutils-0.1.10 → pathwaysutils-0.1.12}/pathwaysutils/test/debug/__init__.py +0 -0
- {pathwaysutils-0.1.10 → pathwaysutils-0.1.12}/pathwaysutils/test/debug/timing_test.py +0 -0
- {pathwaysutils-0.1.10 → pathwaysutils-0.1.12}/pathwaysutils/test/debug/watchdog_test.py +0 -0
- {pathwaysutils-0.1.10 → pathwaysutils-0.1.12}/pathwaysutils/test/experimental/reshard_test.py +0 -0
- {pathwaysutils-0.1.10 → pathwaysutils-0.1.12}/pathwaysutils/test/lru_cache_test.py +0 -0
- {pathwaysutils-0.1.10 → pathwaysutils-0.1.12}/pathwaysutils/test/persistence_test.py +0 -0
- {pathwaysutils-0.1.10 → pathwaysutils-0.1.12}/pathwaysutils/test/plugin_executable_test.py +0 -0
- {pathwaysutils-0.1.10 → pathwaysutils-0.1.12}/pathwaysutils/test/reshard_test.py +0 -0
|
@@ -1,26 +1,41 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pathwaysutils
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.12
|
|
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
|
|
11
11
|
Requires-Dist: absl-py
|
|
12
12
|
Requires-Dist: fastapi
|
|
13
13
|
Requires-Dist: jax>=0.8.3
|
|
14
|
-
Requires-Dist: orbax-checkpoint
|
|
14
|
+
Requires-Dist: orbax-checkpoint>=0.12.3
|
|
15
15
|
Requires-Dist: uvicorn
|
|
16
16
|
Requires-Dist: requests
|
|
17
17
|
Requires-Dist: packaging
|
|
18
18
|
Requires-Dist: pylint>=2.6.0 ; extra == "dev"
|
|
19
19
|
Requires-Dist: pyink ; extra == "dev"
|
|
20
|
+
Requires-Dist: kubernetes ; extra == "gke"
|
|
21
|
+
Requires-Dist: pyyaml ; extra == "gke"
|
|
22
|
+
Requires-Dist: portpicker ; extra == "sps"
|
|
23
|
+
Requires-Dist: google-cloud-monitoring ; extra == "sps"
|
|
24
|
+
Requires-Dist: portpicker ; extra == "sps-without-monitoring"
|
|
25
|
+
Requires-Dist: google-cloud-monitoring ; extra == "test"
|
|
26
|
+
Requires-Dist: httpx ; extra == "test"
|
|
27
|
+
Requires-Dist: kubernetes ; extra == "test"
|
|
28
|
+
Requires-Dist: portpicker ; extra == "test"
|
|
29
|
+
Requires-Dist: pyyaml ; extra == "test"
|
|
30
|
+
Requires-Dist: pytest ; extra == "test"
|
|
20
31
|
Project-URL: changelog, https://github.com/AI-Hypercomputer/pathways-utils/blob/main/CHANGELOG.md
|
|
21
32
|
Project-URL: homepage, https://github.com/AI-Hypercomputer/pathways-utils
|
|
22
33
|
Project-URL: repository, https://github.com/AI-Hypercomputer/pathways-utils
|
|
23
34
|
Provides-Extra: dev
|
|
35
|
+
Provides-Extra: gke
|
|
36
|
+
Provides-Extra: sps
|
|
37
|
+
Provides-Extra: sps-without-monitoring
|
|
38
|
+
Provides-Extra: test
|
|
24
39
|
|
|
25
40
|
# Package of Pathways-on-Cloud utilities
|
|
26
41
|
|
|
@@ -18,10 +18,11 @@ import logging
|
|
|
18
18
|
import os
|
|
19
19
|
|
|
20
20
|
import jax
|
|
21
|
+
from orbax.checkpoint import type_handlers
|
|
21
22
|
from orbax.checkpoint._src.metadata import array_metadata_store as array_metadata_store_lib
|
|
23
|
+
from orbax.checkpoint._src.serialization import cloud_pathways_array_handler
|
|
22
24
|
from pathwaysutils import profiling
|
|
23
25
|
from pathwaysutils import proxy_backend
|
|
24
|
-
from pathwaysutils.persistence import orbax_handler
|
|
25
26
|
|
|
26
27
|
|
|
27
28
|
_logger = logging.getLogger(__name__)
|
|
@@ -91,11 +92,14 @@ def initialize() -> None:
|
|
|
91
92
|
_logger.debug("Detected Pathways-on-Cloud backend. Applying changes.")
|
|
92
93
|
proxy_backend.register_backend_factory()
|
|
93
94
|
profiling.monkey_patch_jax()
|
|
94
|
-
# TODO: b/365549911 - Remove when OCDBT-compatible
|
|
95
95
|
if _is_persistence_enabled():
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
96
|
+
type_handlers.register_type_handler(
|
|
97
|
+
jax.Array,
|
|
98
|
+
cloud_pathways_array_handler.CloudPathwaysArrayHandler(
|
|
99
|
+
timeout=datetime.timedelta(hours=1),
|
|
100
|
+
array_metadata_store=array_metadata_store_lib.Store(),
|
|
101
|
+
),
|
|
102
|
+
override=True,
|
|
99
103
|
)
|
|
100
104
|
|
|
101
105
|
# Turn off JAX compilation cache because Pathways handles its own
|
|
@@ -67,7 +67,10 @@ def main() -> None:
|
|
|
67
67
|
args = parser.parse_args()
|
|
68
68
|
|
|
69
69
|
if profiling.collect_profile(
|
|
70
|
-
args.port,
|
|
70
|
+
args.port,
|
|
71
|
+
args.duration_ms,
|
|
72
|
+
args.host,
|
|
73
|
+
args.log_dir,
|
|
71
74
|
):
|
|
72
75
|
_logger.info("Dumped profiling information in: %s", args.log_dir)
|
|
73
76
|
else:
|
|
@@ -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
|
|
|
@@ -171,7 +171,7 @@ def get_active_slice_indices(
|
|
|
171
171
|
|
|
172
172
|
|
|
173
173
|
def wait_for_slices(
|
|
174
|
-
slice_count: int,
|
|
174
|
+
slice_count: int | None = None,
|
|
175
175
|
poll_interval: float | int = 10,
|
|
176
176
|
timeout: float | int | None = None,
|
|
177
177
|
slice_to_devices: Mapping[int, Sequence[jax.Device]] | None = None,
|
|
@@ -180,7 +180,8 @@ def wait_for_slices(
|
|
|
180
180
|
"""Waits until after at least `slice_count` slices become active.
|
|
181
181
|
|
|
182
182
|
Args:
|
|
183
|
-
slice_count: The number of slices to wait for.
|
|
183
|
+
slice_count: The number of slices to wait for. If None, defaults to the
|
|
184
|
+
total number of slices.
|
|
184
185
|
poll_interval: The minimum number of seconds to wait between availability
|
|
185
186
|
checks. If the check takes longer than this, the next check will start
|
|
186
187
|
immediately after the current check completes. Defaults to 10 seconds.
|
|
@@ -201,6 +202,10 @@ def wait_for_slices(
|
|
|
201
202
|
_logger.debug("slice_to_devices is None. Getting from jax.devices().")
|
|
202
203
|
slice_to_devices = get_slice_to_devices(jax.devices())
|
|
203
204
|
|
|
205
|
+
if slice_count is None:
|
|
206
|
+
_logger.debug("slice_count is None. Using len(slice_to_devices).")
|
|
207
|
+
slice_count = len(slice_to_devices)
|
|
208
|
+
|
|
204
209
|
_logger.info(
|
|
205
210
|
"Waiting for %s slices. Poll interval: %s, Timeout: %s",
|
|
206
211
|
slice_count,
|
|
@@ -249,7 +254,9 @@ def wait_for_slices(
|
|
|
249
254
|
time.sleep(time_to_sleep)
|
|
250
255
|
|
|
251
256
|
|
|
252
|
-
def is_error_due_to_slice_down(
|
|
257
|
+
def is_error_due_to_slice_down(
|
|
258
|
+
error: Exception, log_traceback: bool = True
|
|
259
|
+
) -> bool:
|
|
253
260
|
"""Returns True if the error is due to slice down.
|
|
254
261
|
|
|
255
262
|
The error types that are considered due to slice down are
|
|
@@ -261,6 +268,7 @@ def is_error_due_to_slice_down(error: Exception) -> bool:
|
|
|
261
268
|
|
|
262
269
|
Args:
|
|
263
270
|
error: The error to check.
|
|
271
|
+
log_traceback: If True, log the traceback of the error.
|
|
264
272
|
"""
|
|
265
273
|
error_due_to_slice_down = False
|
|
266
274
|
traceback_logging_level = logging.DEBUG
|
|
@@ -290,9 +298,7 @@ def is_error_due_to_slice_down(error: Exception) -> bool:
|
|
|
290
298
|
|
|
291
299
|
error_due_to_slice_down = True
|
|
292
300
|
|
|
293
|
-
if not error_due_to_slice_down:
|
|
294
|
-
_logger.
|
|
295
|
-
|
|
296
|
-
_logger.log(traceback_logging_level, "Error details:", exc_info=True)
|
|
301
|
+
if not error_due_to_slice_down and log_traceback:
|
|
302
|
+
_logger.log(traceback_logging_level, "Error details:", exc_info=True)
|
|
297
303
|
|
|
298
304
|
return error_due_to_slice_down
|
|
@@ -86,17 +86,24 @@ class Manager:
|
|
|
86
86
|
Attributes:
|
|
87
87
|
slice_to_devices: A mapping from slice index to a sequence of `jax.Device`
|
|
88
88
|
objects for that slice.
|
|
89
|
-
all_slice_indices: A set of all possible slice indices.
|
|
90
|
-
active_slice_indices: A set of indices of the currently
|
|
91
|
-
|
|
92
|
-
|
|
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.
|
|
93
98
|
"""
|
|
94
99
|
|
|
95
100
|
slice_to_devices: Mapping[int, Sequence[jax.Device]]
|
|
96
101
|
all_slice_indices: Set[int]
|
|
97
102
|
active_slice_indices: Set[int]
|
|
98
|
-
|
|
99
|
-
|
|
103
|
+
inactive_slice_indices: Set[int]
|
|
104
|
+
available_inactive_slices: Set[int]
|
|
105
|
+
_stop_event: threading.Event | None
|
|
106
|
+
_monitor_thread: threading.Thread | None
|
|
100
107
|
def __init__(self, devices: Sequence[jax.Device] | None = None) -> None:
|
|
101
108
|
"""Initializes the manager.
|
|
102
109
|
|
|
@@ -107,12 +114,56 @@ class Manager:
|
|
|
107
114
|
devices = jax.devices()
|
|
108
115
|
self.slice_to_devices = elastic.get_slice_to_devices(devices)
|
|
109
116
|
|
|
110
|
-
self.all_slice_indices =
|
|
117
|
+
self.all_slice_indices = frozenset(self.slice_to_devices.keys())
|
|
118
|
+
|
|
119
|
+
self._active_slice_indices = frozenset()
|
|
120
|
+
self.available_inactive_slices = frozenset()
|
|
111
121
|
|
|
112
122
|
self.active_slice_indices = elastic.get_active_slice_indices(
|
|
113
123
|
slice_to_devices=self.slice_to_devices
|
|
114
124
|
)
|
|
115
|
-
|
|
125
|
+
|
|
126
|
+
self._stop_event = None
|
|
127
|
+
self._monitor_thread = None
|
|
128
|
+
|
|
129
|
+
def start_monitoring(self, poll_interval: float | int = 10) -> None:
|
|
130
|
+
"""Starts the background monitor thread.
|
|
131
|
+
|
|
132
|
+
Args:
|
|
133
|
+
poll_interval: The number of seconds to wait between activity checks.
|
|
134
|
+
"""
|
|
135
|
+
if self._monitor_thread is not None and self._monitor_thread.is_alive():
|
|
136
|
+
_logger.warning("Monitor thread is already running.")
|
|
137
|
+
return
|
|
138
|
+
|
|
139
|
+
self._stop_event = threading.Event()
|
|
140
|
+
self._monitor_thread = threading.Thread(
|
|
141
|
+
target=self._monitor_new_slices,
|
|
142
|
+
args=(self._stop_event, poll_interval),
|
|
143
|
+
daemon=True,
|
|
144
|
+
)
|
|
145
|
+
self._monitor_thread.start()
|
|
146
|
+
_logger.info("Elastic monitor thread started with interval %s.", poll_interval)
|
|
147
|
+
|
|
148
|
+
def close(self) -> None:
|
|
149
|
+
"""Stops the background monitor thread."""
|
|
150
|
+
if self._stop_event is not None:
|
|
151
|
+
self._stop_event.set()
|
|
152
|
+
if self._monitor_thread is not None:
|
|
153
|
+
_logger.info("Closing manager, waiting for monitor thread to stop...")
|
|
154
|
+
try:
|
|
155
|
+
self._monitor_thread.join(timeout=5)
|
|
156
|
+
if self._monitor_thread.is_alive():
|
|
157
|
+
_logger.warning(
|
|
158
|
+
"Elastic monitor thread failed to stop within 5s timeout."
|
|
159
|
+
)
|
|
160
|
+
except RuntimeError as e:
|
|
161
|
+
if "cannot join thread" in str(e):
|
|
162
|
+
pass
|
|
163
|
+
else:
|
|
164
|
+
raise
|
|
165
|
+
self._monitor_thread = None
|
|
166
|
+
self._stop_event = None
|
|
116
167
|
|
|
117
168
|
@functools.cached_property
|
|
118
169
|
def total_slice_count(self) -> int:
|
|
@@ -130,16 +181,26 @@ class Manager:
|
|
|
130
181
|
except StopIteration as error:
|
|
131
182
|
raise ValueError("No active slices") from error
|
|
132
183
|
|
|
184
|
+
@property
|
|
185
|
+
def active_slice_indices(self) -> frozenset[int]:
|
|
186
|
+
"""The indices of active slices."""
|
|
187
|
+
return self._active_slice_indices
|
|
188
|
+
|
|
189
|
+
@active_slice_indices.setter
|
|
190
|
+
def active_slice_indices(self, value: Set[int]) -> None:
|
|
191
|
+
self._active_slice_indices = frozenset(value)
|
|
192
|
+
self.inactive_slice_indices = (
|
|
193
|
+
self.all_slice_indices - self._active_slice_indices
|
|
194
|
+
)
|
|
195
|
+
self.available_inactive_slices = frozenset(
|
|
196
|
+
self.available_inactive_slices - self._active_slice_indices
|
|
197
|
+
)
|
|
198
|
+
|
|
133
199
|
@property
|
|
134
200
|
def active_slice_count(self) -> int:
|
|
135
201
|
"""The number of active slices."""
|
|
136
202
|
return len(self.active_slice_indices)
|
|
137
203
|
|
|
138
|
-
@property
|
|
139
|
-
def inactive_slice_indices(self) -> set[int]:
|
|
140
|
-
"""The set of inactive slice indices."""
|
|
141
|
-
return self.all_slice_indices - self.active_slice_indices
|
|
142
|
-
|
|
143
204
|
def scale_by_active_slices(self, x: int | float) -> int | float:
|
|
144
205
|
"""Scale x by the number of active slices."""
|
|
145
206
|
if isinstance(x, int):
|
|
@@ -171,37 +232,57 @@ class Manager:
|
|
|
171
232
|
for array in jax.live_arrays():
|
|
172
233
|
array.delete()
|
|
173
234
|
|
|
174
|
-
def
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
_logger.debug("No inactive slices to check.")
|
|
182
|
-
continue
|
|
235
|
+
def _check_inactive_slices(self) -> None:
|
|
236
|
+
"""Checks inactive slices and updates available_inactive_slices."""
|
|
237
|
+
if not self.inactive_slice_indices:
|
|
238
|
+
_logger.debug("No inactive slices to check.")
|
|
239
|
+
if self.available_inactive_slices:
|
|
240
|
+
self.available_inactive_slices = frozenset()
|
|
241
|
+
return
|
|
183
242
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
243
|
+
_logger.debug(
|
|
244
|
+
"Now checking inactive slices %s", self.inactive_slice_indices
|
|
245
|
+
)
|
|
246
|
+
inactive_slice_to_devices = {
|
|
247
|
+
i: self.slice_to_devices[i] for i in self.inactive_slice_indices
|
|
248
|
+
}
|
|
249
|
+
found_slices = elastic.get_active_slice_indices(
|
|
250
|
+
inactive_slice_to_devices
|
|
251
|
+
)
|
|
193
252
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
newly_active_indices,
|
|
198
|
-
)
|
|
199
|
-
self.new_slice_event.set()
|
|
200
|
-
return
|
|
253
|
+
_logger.debug(
|
|
254
|
+
"Found available and inactive slices %s", found_slices
|
|
255
|
+
)
|
|
201
256
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
257
|
+
# Filter against active_slice_indices in case the main thread initiated scale-up
|
|
258
|
+
# and claimed these slices while this background health check was running.
|
|
259
|
+
found_slices = found_slices - self.active_slice_indices
|
|
260
|
+
|
|
261
|
+
if found_slices != self.available_inactive_slices:
|
|
262
|
+
_logger.info(
|
|
263
|
+
"Newly available but inactive slices %s", found_slices
|
|
264
|
+
)
|
|
265
|
+
self.available_inactive_slices = frozenset(found_slices)
|
|
266
|
+
|
|
267
|
+
def _monitor_new_slices(
|
|
268
|
+
self, stop_event: threading.Event, poll_interval: float | int
|
|
269
|
+
) -> None:
|
|
270
|
+
"""Monitors for new slices and updates available_inactive_slices."""
|
|
271
|
+
_logger.info("Elastic monitor thread started.")
|
|
272
|
+
try:
|
|
273
|
+
while not stop_event.wait(poll_interval):
|
|
274
|
+
try:
|
|
275
|
+
self._check_inactive_slices()
|
|
276
|
+
except Exception: # pylint: disable=broad-exception-caught
|
|
277
|
+
_logger.exception("Error in monitor thread loop")
|
|
278
|
+
except BaseException as e:
|
|
279
|
+
_logger.critical(
|
|
280
|
+
"Catastrophic error in monitor thread, thread is dying!",
|
|
281
|
+
exc_info=True,
|
|
282
|
+
)
|
|
283
|
+
raise
|
|
284
|
+
finally:
|
|
285
|
+
_logger.info("Elastic monitor thread stopped.")
|
|
205
286
|
|
|
206
287
|
def elastic_retry(
|
|
207
288
|
self,
|
|
@@ -227,10 +308,10 @@ class Manager:
|
|
|
227
308
|
(i.e., it waits for all slices to be active).
|
|
228
309
|
|
|
229
310
|
When `minimum_slice_count` is less than the total number of slices, a
|
|
230
|
-
background thread will monitor for
|
|
231
|
-
`self.
|
|
232
|
-
a `ScaleUpSignalError` to gracefully interrupt the
|
|
233
|
-
trigger a retry.
|
|
311
|
+
background thread will monitor for newly joined inactive slices and populate
|
|
312
|
+
`self.available_inactive_slices`. User code can check this set (e.g. at step
|
|
313
|
+
boundaries) and raise a `ScaleUpSignalError` to gracefully interrupt the
|
|
314
|
+
current execution and trigger a retry with the expanded hardware.
|
|
234
315
|
|
|
235
316
|
Often, the function will dispatch JAX operations and wait for them to
|
|
236
317
|
complete while creating a log message. If using Python logging, it is
|
|
@@ -286,6 +367,7 @@ class Manager:
|
|
|
286
367
|
def decorator(func: _F) -> _F:
|
|
287
368
|
@functools.wraps(func)
|
|
288
369
|
def wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
370
|
+
self.start_monitoring(poll_interval)
|
|
289
371
|
|
|
290
372
|
def attempt_execution(attempt: int) -> Any:
|
|
291
373
|
_logger.info("Elastic attempt %d", attempt)
|
|
@@ -295,75 +377,66 @@ class Manager:
|
|
|
295
377
|
poll_interval=poll_interval,
|
|
296
378
|
timeout=timeout,
|
|
297
379
|
)
|
|
380
|
+
# Reset available_inactive_slices at attempt start since
|
|
381
|
+
# active_slice_indices has just been updated by wait_for_slices.
|
|
382
|
+
self.available_inactive_slices = frozenset()
|
|
298
383
|
if pre_callback is not None:
|
|
299
384
|
pre_callback()
|
|
300
385
|
|
|
301
386
|
with jax.default_device(self.default_device):
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
387
|
+
return func(*args, **kwargs)
|
|
388
|
+
|
|
389
|
+
def handle_scale_up_error(attempt: int, error: ScaleUpSignalError) -> None:
|
|
390
|
+
_logger.info("Scale up requested.")
|
|
391
|
+
_elastic_event_cleanup()
|
|
392
|
+
# Reset available_inactive_slices before retry callback and next attempt.
|
|
393
|
+
self.available_inactive_slices = frozenset()
|
|
394
|
+
|
|
395
|
+
if on_elastic_event_callback is not None:
|
|
396
|
+
on_elastic_event_callback()
|
|
397
|
+
|
|
398
|
+
if not retry_policy(attempt, error):
|
|
399
|
+
_logger.info("Retry policy rejected retry after ScaleUpSignalError.")
|
|
400
|
+
raise ElasticRuntimeError(
|
|
401
|
+
f"Elastic attempt {attempt} failed."
|
|
402
|
+
) from error
|
|
403
|
+
|
|
404
|
+
def handle_slice_down_error(
|
|
405
|
+
attempt: int, error: jax.errors.JaxRuntimeError
|
|
406
|
+
) -> None:
|
|
407
|
+
if not elastic.is_error_due_to_slice_down(error):
|
|
408
|
+
raise
|
|
409
|
+
|
|
410
|
+
_logger.exception("Elastic event detected")
|
|
411
|
+
_elastic_event_cleanup()
|
|
412
|
+
# Reset available_inactive_slices on slice-down failure before retry.
|
|
413
|
+
self.available_inactive_slices = frozenset()
|
|
414
|
+
|
|
415
|
+
if on_elastic_event_callback is not None:
|
|
416
|
+
on_elastic_event_callback()
|
|
417
|
+
|
|
418
|
+
if not retry_policy(attempt, error):
|
|
419
|
+
_logger.info(
|
|
420
|
+
"Retry policy rejected retry after jax.errors.JaxRuntimeError."
|
|
421
|
+
)
|
|
422
|
+
raise ElasticRuntimeError(
|
|
423
|
+
f"Elastic attempt {attempt} failed."
|
|
424
|
+
) from error
|
|
425
|
+
|
|
426
|
+
try:
|
|
427
|
+
attempt = 1
|
|
428
|
+
while True:
|
|
315
429
|
try:
|
|
316
|
-
return
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
except ScaleUpSignalError as error:
|
|
327
|
-
_logger.info("Scale up requested.")
|
|
328
|
-
_elastic_event_cleanup()
|
|
329
|
-
|
|
330
|
-
if on_elastic_event_callback is not None:
|
|
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.")
|
|
342
|
-
except jax.errors.JaxRuntimeError as error:
|
|
343
|
-
if not elastic.is_error_due_to_slice_down(error):
|
|
344
|
-
raise
|
|
345
|
-
|
|
346
|
-
if self.new_slice_event.is_set():
|
|
347
|
-
_logger.info("Slice down event and new slice available detected.")
|
|
348
|
-
else:
|
|
349
|
-
_logger.info("Slice down event detected.")
|
|
350
|
-
|
|
351
|
-
_elastic_event_cleanup()
|
|
352
|
-
|
|
353
|
-
if on_elastic_event_callback is not None:
|
|
354
|
-
on_elastic_event_callback()
|
|
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
|
|
365
|
-
|
|
366
|
-
return wrapper
|
|
430
|
+
return attempt_execution(attempt)
|
|
431
|
+
except ScaleUpSignalError as error:
|
|
432
|
+
handle_scale_up_error(attempt, error)
|
|
433
|
+
except jax.errors.JaxRuntimeError as error:
|
|
434
|
+
handle_slice_down_error(attempt, error)
|
|
435
|
+
attempt += 1
|
|
436
|
+
finally:
|
|
437
|
+
self.close()
|
|
438
|
+
|
|
439
|
+
return wrapper # pyrefly: ignore[bad-return]
|
|
367
440
|
|
|
368
441
|
return decorator
|
|
369
442
|
|