@@ -36,14 +36,21 @@ class ElasticRuntimeError(RuntimeError):
3636 """Error raised when elasticity cannot continue."""
3737
3838
39- class NewSliceAvailableError (RuntimeError ):
40- """Error raised when a new slice is available."""
39+ class ScaleUpError (RuntimeError ):
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+ """
4148
4249
4350_F = TypeVar ("_F" , bound = Callable [..., Any ])
4451
4552
46- def _elastic_event_cleanup ():
53+ def _elastic_event_cleanup () -> None :
4754 """Cleans up JAX profiles, caches, and live arrays."""
4855 try :
4956 _logger .info ("Cleaning up any ongoing traces" )
@@ -60,10 +67,19 @@ def _elastic_event_cleanup():
6067
6168
6269class Manager :
63- """Utility class for elastic training."""
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+ """
6480
65- _total_slice_count : int | None = None
6681 slice_to_devices : Mapping [int , Sequence [jax .Device ]]
82+ all_slice_indices : set [int ]
6783 active_slice_indices : set [int ]
6884 new_slice_event : threading .Event
6985
@@ -84,16 +100,14 @@ def __init__(self, devices: Sequence[jax.Device] | None = None) -> None:
84100 )
85101 self .new_slice_event = threading .Event ()
86102
87- @property
103+ @functools . cached_property
88104 def total_slice_count (self ) -> int :
89- """Returns the total number of slices."""
90- if self ._total_slice_count is None :
91- self ._total_slice_count = len (self .slice_to_devices )
92- return self ._total_slice_count
105+ """The total number of slices."""
106+ return len (self .slice_to_devices )
93107
94108 @property
95109 def default_device (self ) -> jax .Device :
96- """Returns the device that should be set to the default device.
110+ """The device that should be set to the default device.
97111
98112 This will be from one of the slices in `active_slice_indices`.
99113 """
@@ -104,12 +118,12 @@ def default_device(self) -> jax.Device:
104118
105119 @property
106120 def active_slice_count (self ) -> int :
107- """Returns the number of slices."""
121+ """The number of active slices."""
108122 return len (self .active_slice_indices )
109123
110124 @property
111125 def inactive_slice_indices (self ) -> set [int ]:
112- """Returns the set of inactive slice indices."""
126+ """The set of inactive slice indices."""
113127 return self .all_slice_indices - self .active_slice_indices
114128
115129 def scale_by_active_slices (self , x : int | float ) -> int | float :
@@ -149,11 +163,24 @@ def _elasticity_retry_decorator(
149163 pre_callback : Callable [..., Any ] | None = None ,
150164 on_elastic_event_callback : Callable [..., Any ] | None = None ,
151165 ) -> Callable [[_F ], _F ]:
152- """Retries a function with elasticity fault tolerance."""
166+ """Retries a function with elasticity fault tolerance.
153167
154- def decorator (func ):
168+ Args:
169+ max_retries: The maximum number of times to retry the function.
170+ pre_callback: A callback to call before each attempt of the wrapped
171+ function.
172+ on_elastic_event_callback: A callback to call after an elastic failure
173+ occurs.
174+
175+ Returns:
176+ A function decorator.
177+ """
178+
179+ if max_retries <= 0 :
180+ raise ValueError ("max_retries must be positive." )
181+ def decorator (func : _F ) -> _F :
155182 @functools .wraps (func )
156- def wrapper (* args , ** kwargs ) :
183+ def wrapper (* args : Any , ** kwargs : Any ) -> Any :
157184 for retry_index in range (max_retries ):
158185 try :
159186 _logger .info (
@@ -164,8 +191,8 @@ def wrapper(*args, **kwargs):
164191
165192 with jax .default_device (self .default_device ):
166193 return func (* args , ** kwargs )
167- except NewSliceAvailableError :
168- _logger .info ("New slice available . Retrying." )
194+ except ScaleUpError :
195+ _logger .info ("Scale up requested . Retrying." )
169196 _elastic_event_cleanup ()
170197
171198 if on_elastic_event_callback is not None :
@@ -185,9 +212,10 @@ def wrapper(*args, **kwargs):
185212
186213 if on_elastic_event_callback is not None :
187214 on_elastic_event_callback ()
188- raise ElasticRuntimeError (
189- f"Elastic attempt { max_retries } out of { max_retries } failed."
190- )
215+ else :
216+ raise ElasticRuntimeError (
217+ f"Elastic attempt { max_retries } out of { max_retries } failed."
218+ )
191219
192220 return wrapper
193221
@@ -226,7 +254,7 @@ def pause_resume(
226254 occurs.
227255
228256 Returns:
229- The result of the wrapped function.
257+ A decorator that retries the wrapped function.
230258
231259 Raises:
232260 ElasticRuntimeError: If all retry attempts fail.
@@ -300,7 +328,7 @@ def replica_resize(
300328 occurs.
301329
302330 Returns:
303- The result of the wrapped function.
331+ A decorator that retries the wrapped function.
304332
305333 Raises:
306334 ElasticRuntimeError: If all retry attempts fail.
0 commit comments