Skip to content

Commit c009e7c

Browse files
lukebaumanncopybara-github
authored andcommitted
Implement checkpoint-based elasticity using set-based slice tracking in MaxText.
PiperOrigin-RevId: 936972164
1 parent 0cdef0c commit c009e7c

2 files changed

Lines changed: 145 additions & 92 deletions

File tree

pathwaysutils/elastic/elastic.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ def validate(self) -> Set[int]:
121121
_logger.debug(
122122
"Caught JaxRuntimeError for slice_index=%s: %s", slice_index, error
123123
)
124-
if not is_error_due_to_slice_down(error):
124+
if not is_error_due_to_slice_down(error, log_traceback=False):
125125
raise
126126
return active_slice_indices
127127

@@ -249,7 +249,9 @@ def wait_for_slices(
249249
time.sleep(time_to_sleep)
250250

251251

252-
def is_error_due_to_slice_down(error: Exception) -> bool:
252+
def is_error_due_to_slice_down(
253+
error: Exception, log_traceback: bool = True
254+
) -> bool:
253255
"""Returns True if the error is due to slice down.
254256
255257
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:
261263
262264
Args:
263265
error: The error to check.
266+
log_traceback: If True, log the traceback of the error.
264267
"""
265268
error_due_to_slice_down = False
266269
traceback_logging_level = logging.DEBUG
@@ -293,6 +296,7 @@ def is_error_due_to_slice_down(error: Exception) -> bool:
293296
if not error_due_to_slice_down:
294297
_logger.debug("Caught an error not due to slice down")
295298

296-
_logger.log(traceback_logging_level, "Error details:", exc_info=True)
299+
if log_traceback:
300+
_logger.log(traceback_logging_level, "Error details:", exc_info=True)
297301

298302
return error_due_to_slice_down

pathwaysutils/elastic/manager.py

Lines changed: 138 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,9 @@ class Manager:
9696
all_slice_indices: Set[int]
9797
active_slice_indices: Set[int]
9898
new_slice_event: threading.Event
99-
99+
available_inactive_slices: Set[int]
100+
_stop_event: threading.Event | None
101+
_monitor_thread: threading.Thread | None
100102
def __init__(self, devices: Sequence[jax.Device] | None = None) -> None:
101103
"""Initializes the manager.
102104
@@ -113,6 +115,45 @@ def __init__(self, devices: Sequence[jax.Device] | None = None) -> None:
113115
slice_to_devices=self.slice_to_devices
114116
)
115117
self.new_slice_event = threading.Event()
118+
self.available_inactive_slices = set()
119+
120+
self._stop_event = None
121+
self._monitor_thread = None
122+
123+
def start_monitoring(self, poll_interval: float | int = 10) -> None:
124+
"""Starts the background monitor thread.
125+
126+
Args:
127+
poll_interval: The number of seconds to wait between activity checks.
128+
"""
129+
if self._monitor_thread is not None and self._monitor_thread.is_alive():
130+
_logger.warning("Monitor thread is already running.")
131+
return
132+
133+
self._stop_event = threading.Event()
134+
self._monitor_thread = threading.Thread(
135+
target=self._monitor_new_slices,
136+
args=(self._stop_event, poll_interval),
137+
daemon=True,
138+
)
139+
self._monitor_thread.start()
140+
_logger.info("Elastic monitor thread started with interval %s.", poll_interval)
141+
142+
def close(self) -> None:
143+
"""Stops the background monitor thread."""
144+
if self._stop_event is not None:
145+
self._stop_event.set()
146+
if self._monitor_thread is not None:
147+
_logger.info("Closing manager, waiting for monitor thread to stop...")
148+
try:
149+
self._monitor_thread.join(timeout=5)
150+
except RuntimeError as e:
151+
if "cannot join thread" in str(e):
152+
pass
153+
else:
154+
raise
155+
self._monitor_thread = None
156+
self._stop_event = None
116157

117158
@functools.cached_property
118159
def total_slice_count(self) -> int:
@@ -171,37 +212,58 @@ def _cleanup_on_retry(self):
171212
for array in jax.live_arrays():
172213
array.delete()
173214

174-
def _monitor_new_slices(
175-
self, stop_event: threading.Event, poll_interval: float | int
176-
) -> None:
177-
"""Monitors for new slices and sets the `new_slice_event` if found."""
178-
while not stop_event.wait(poll_interval):
179-
try:
180-
if not self.inactive_slice_indices:
181-
_logger.debug("No inactive slices to check.")
182-
continue
215+
def _check_inactive_slices(self) -> None:
216+
"""Checks inactive slices and updates available_inactive_slices."""
217+
if not self.inactive_slice_indices:
218+
_logger.debug("No inactive slices to check.")
219+
if self.available_inactive_slices:
220+
self.available_inactive_slices.clear()
221+
self.new_slice_event.clear()
222+
return
223+
224+
_logger.debug(
225+
"Now checking inactive slices %s", self.inactive_slice_indices
226+
)
227+
inactive_slice_to_devices = {
228+
i: self.slice_to_devices[i] for i in self.inactive_slice_indices
229+
}
230+
found_slices = elastic.get_active_slice_indices(
231+
inactive_slice_to_devices
232+
)
183233

184-
_logger.debug(
185-
"Checking inactive slices: %s", self.inactive_slice_indices
186-
)
187-
inactive_slice_to_devices = {
188-
i: self.slice_to_devices[i] for i in self.inactive_slice_indices
189-
}
190-
newly_active_indices = elastic.get_active_slice_indices(
191-
inactive_slice_to_devices
192-
)
234+
_logger.debug(
235+
"Found available and inactive slices %s", found_slices
236+
)
193237

194-
if newly_active_indices:
195-
_logger.info(
196-
"New slices found: %s. Setting new slice event.",
197-
newly_active_indices,
198-
)
199-
self.new_slice_event.set()
200-
return
238+
if found_slices != self.available_inactive_slices:
239+
_logger.info(
240+
"Newly available but inactive slices %s", found_slices
241+
)
242+
self.available_inactive_slices = found_slices
243+
if self.available_inactive_slices:
244+
self.new_slice_event.set()
245+
else:
246+
self.new_slice_event.clear()
201247

202-
_logger.debug("No new slices found.")
203-
except Exception: # pylint: disable=broad-exception-caught
204-
_logger.exception("Error in monitor thread")
248+
def _monitor_new_slices(
249+
self, stop_event: threading.Event, poll_interval: float | int
250+
) -> None:
251+
"""Monitors for new slices and updates available_inactive_slices."""
252+
_logger.info("Elastic monitor thread started.")
253+
try:
254+
while not stop_event.wait(poll_interval):
255+
try:
256+
self._check_inactive_slices()
257+
except Exception: # pylint: disable=broad-exception-caught
258+
_logger.exception("Error in monitor thread loop")
259+
except BaseException as e:
260+
_logger.critical(
261+
"Catastrophic error in monitor thread, thread is dying!",
262+
exc_info=True,
263+
)
264+
raise
265+
finally:
266+
_logger.info("Elastic monitor thread stopped.")
205267

206268
def elastic_retry(
207269
self,
@@ -286,6 +348,7 @@ def elastic_retry(
286348
def decorator(func: _F) -> _F:
287349
@functools.wraps(func)
288350
def wrapper(*args: Any, **kwargs: Any) -> Any:
351+
self.start_monitoring(poll_interval)
289352

290353
def attempt_execution(attempt: int) -> Any:
291354
_logger.info("Elastic attempt %d", attempt)
@@ -295,73 +358,59 @@ def attempt_execution(attempt: int) -> Any:
295358
poll_interval=poll_interval,
296359
timeout=timeout,
297360
)
361+
self.available_inactive_slices.clear()
298362
if pre_callback is not None:
299363
pre_callback()
300364

301365
with jax.default_device(self.default_device):
302366
self.new_slice_event.clear()
303-
stop_event = threading.Event()
304-
305-
if target_slice_count < self.total_slice_count:
306-
monitor_thread = threading.Thread(
307-
target=self._monitor_new_slices,
308-
args=(stop_event, poll_interval),
309-
daemon=True,
310-
)
311-
monitor_thread.start()
312-
else:
313-
monitor_thread = None
367+
return func(*args, **kwargs)
368+
369+
def handle_scale_up_error(attempt: int, error: ScaleUpSignalError) -> None:
370+
_logger.info("Scale up requested.")
371+
_elastic_event_cleanup()
372+
373+
if on_elastic_event_callback is not None:
374+
on_elastic_event_callback()
375+
376+
if not retry_policy(attempt, error):
377+
_logger.info("Retry policy rejected retry after ScaleUpSignalError.")
378+
raise ElasticRuntimeError(f"Elastic attempt {attempt} failed.") from error
379+
380+
_logger.info("Retrying.")
381+
382+
def handle_slice_down_error(attempt: int, error: jax.errors.JaxRuntimeError) -> None:
383+
if not elastic.is_error_due_to_slice_down(error):
384+
raise
385+
386+
if self.new_slice_event.is_set():
387+
_logger.info("Slice down event and new slice available detected.")
388+
else:
389+
_logger.info("Slice down event detected.")
390+
391+
_elastic_event_cleanup()
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 JaxRuntimeError.")
398+
raise ElasticRuntimeError(f"Elastic attempt {attempt} failed.") from error
399+
400+
_logger.info("Retrying.")
314401

402+
try:
403+
attempt = 1
404+
while True:
315405
try:
316-
return func(*args, **kwargs)
317-
finally:
318-
stop_event.set()
319-
if monitor_thread is not None:
320-
monitor_thread.join()
321-
322-
attempt = 1
323-
while True:
324-
try:
325-
return attempt_execution(attempt)
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
406+
return attempt_execution(attempt)
407+
except ScaleUpSignalError as error:
408+
handle_scale_up_error(attempt, error)
409+
except jax.errors.JaxRuntimeError as error:
410+
handle_slice_down_error(attempt, error)
411+
attempt += 1
412+
finally:
413+
self.close()
365414

366415
return wrapper
367416

0 commit comments

Comments
 (0)