Skip to content

Commit 763b4f6

Browse files
lukebaumannSharon Yu
authored andcommitted
Add a replica_resize decorator for fault tolerance in elastic Pathways.
PiperOrigin-RevId: 857246882
1 parent c055be1 commit 763b4f6

1 file changed

Lines changed: 51 additions & 23 deletions

File tree

pathwaysutils/elastic/manager.py

Lines changed: 51 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -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

6269
class 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

Comments
 (0)