@@ -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