Skip to content

Commit d219c39

Browse files
authored
Merge pull request Borklet-Labs#11 from Borklet-Labs/cienet/replica_resize
Add: Elastic Training functionality for Pause & Resums and Replica Resize
2 parents 8ff1056 + 9e3fc76 commit d219c39

12 files changed

Lines changed: 300 additions & 54 deletions

File tree

axlearn/cloud/gcp/pathways_utils.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -556,6 +556,7 @@ def _build_pathways_head_sidecar_containers(self) -> list[Nested[Any]]:
556556
system = USER_FACING_NAME_TO_SYSTEM_CHARACTERISTICS[self._tpu_type]
557557
staging_location = f"{cfg.output_dir}/pathways-staging"
558558
pathways_tpu_version = get_pathways_tpu_version(system.gce_machine_type)
559+
num_elastic_slices = 1
559560

560561
# If multi-head, every pathways-head will only
561562
# be connected to one pathways instance (a pathways-worker replicated job).
@@ -565,6 +566,7 @@ def _build_pathways_head_sidecar_containers(self) -> list[Nested[Any]]:
565566
f"--resource_manager_address=localhost:{_PATHWAYS_RESOURCE_MANAGER_PORT}",
566567
f"--server_port={_PATHWAYS_PROXY_PORT}",
567568
f"--gcs_scratch_location={staging_location}",
569+
f"--num_elastic_slices={num_elastic_slices}",
568570
]
569571
if self._colocated_python.is_colocated_python_enabled:
570572
cmd_args.append("--sidecar_name=external")

axlearn/common/elastic_input.py

Lines changed: 53 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,8 @@
4949
from axlearn.common.config import REQUIRED, Required, config_class, maybe_set_config
5050
from axlearn.common.input_dispatch import BaseInputDispatcher, _validate_logical_feed_shapes
5151
from axlearn.common.module import Module
52-
from axlearn.common.utils import Nested, Tensor
52+
from axlearn.common.utils import Nested, Tensor, live_devices, live_slice_indices
53+
5354

5455

5556
class ElasticSpmdInputDispatcher(BaseInputDispatcher):
@@ -72,13 +73,20 @@ class Config(BaseInputDispatcher.Config):
7273

7374
@property
7475
def is_in_elastic_mode(self) -> bool:
76+
print("In is_in_elastic_mode by lkolluru")
7577
cfg = self.config
78+
print("cfg.num_max_slices by lkolluru: ", cfg.num_max_slices)
79+
print("slice_count by lkolluru: ", slice_count())
7680
if cfg.num_max_slices is None:
7781
return False
7882
else:
83+
print(
84+
f"Live_slice_count by lkolluru: {live_slice_indices()}, slices_cnt: {len(live_slice_indices())}",
85+
live_slice_indices(),len(live_slice_indices())
86+
)
7987
if slice_count() < cfg.num_max_slices:
8088
return True
81-
elif slice_count() == cfg.num_max_slices:
89+
elif slice_count() >= cfg.num_max_slices:
8290
return False
8391
else:
8492
# TODO (jtian22): consider supporting scaling up in the future.
@@ -92,6 +100,7 @@ def __init__(self, cfg: Config, *, parent: Optional[Module]):
92100
cfg: ElasticSpmdInputDispatcher.Config = self.config
93101

94102
mesh = thread_resources.env.physical_mesh
103+
print("physical mesh by lkolluru: ", mesh)
95104
if mesh.empty:
96105
raise ValueError("Expected to be initialized within the context of a mesh.")
97106

@@ -117,6 +126,7 @@ def __init__(self, cfg: Config, *, parent: Optional[Module]):
117126
)
118127
if self.is_in_elastic_mode:
119128
num_partitions = num_partitions // slice_count() * cfg.num_max_slices
129+
print(f"num_partitions by lkolluru: {num_partitions} and live_devices: {slice_count()} ")
120130

121131
if cfg.global_logical_batch_size % num_partitions != 0:
122132
raise ValueError(
@@ -125,6 +135,7 @@ def __init__(self, cfg: Config, *, parent: Optional[Module]):
125135
)
126136

127137
self._device_physical_batch_size = cfg.global_logical_batch_size // num_partitions
138+
print("device_physical_batch_size on init by lkolluru: ", self._device_physical_batch_size)
128139

129140
# Infer the physical feeds and feed index along dim=0.
130141
_, _, pid2fid = get_process_index_and_count_and_mapping(
@@ -156,14 +167,33 @@ def fid2pids(feed_id):
156167

157168
self.feed_count = len(set(pid2fid.values())) // slice_count() * cfg.num_max_slices
158169
self.feed_index = pid2fid[jax.process_index()]
170+
print("feed_count by lkolluru: ", self.feed_count)
171+
print("global_logical_batch_size by lkolluru: ", cfg.global_logical_batch_size)
172+
173+
# assert cfg.global_logical_batch_size % self.feed_count == 0
174+
if self.feed_count == 0:
175+
self._feed_logical_batch_size = cfg.global_logical_batch_size
176+
else:
177+
self._feed_logical_batch_size = cfg.global_logical_batch_size // self.feed_count
159178

160-
assert cfg.global_logical_batch_size % self.feed_count == 0
161-
self._feed_logical_batch_size = cfg.global_logical_batch_size // self.feed_count
179+
adjusted_device_physical_batch_size = math.ceil(
180+
self._device_physical_batch_size * (cfg.num_max_slices / slice_count())
181+
)
182+
print(
183+
"adjusted_device_physical_batch_size outside elastic by lkolluru: ",
184+
adjusted_device_physical_batch_size,
185+
)
162186

163187
if self.is_in_elastic_mode:
188+
print(" In elastic mode lkolluru")
189+
# I think this should be len(live_slice_indices())....
164190
adjusted_device_physical_batch_size = math.ceil(
165191
self._device_physical_batch_size * (cfg.num_max_slices / slice_count())
166192
)
193+
print(
194+
"adjusted_device_physical_batch_size inside elastic by lkolluru: ",
195+
adjusted_device_physical_batch_size,
196+
)
167197
padding_per_device = (
168198
adjusted_device_physical_batch_size - self._device_physical_batch_size
169199
)
@@ -402,7 +432,14 @@ def _padded_select(path, x, y):
402432

403433
def slice_count() -> int:
404434
"""Returns the number of slices."""
405-
return len(set(d.slice_index for d in jax.devices() if hasattr(d, "slice_index"))) or 1
435+
# slice_cnt_val=len(set(d.slice_index for d in jax.devices() if hasattr(d, "slice_index"))) or 1
436+
# print("slice_count by lkolluru: ", slice_cnt_val)
437+
# return len(set(d.slice_index for d in jax.devices() if hasattr(d, "slice_index"))) or 1
438+
slice_cnt_val = (
439+
len(set(d.slice_index for d in live_devices() if hasattr(d, "slice_index"))) or 1
440+
)
441+
print("slice_count by lkolluru: ", slice_cnt_val)
442+
return len(set(d.slice_index for d in live_devices() if hasattr(d, "slice_index"))) or 1
406443

407444

408445
def process_count_per_slice() -> int:
@@ -411,7 +448,8 @@ def process_count_per_slice() -> int:
411448
len(
412449
set(
413450
d.process_index
414-
for d in jax.devices()
451+
# for d in jax.devices()
452+
for d in live_devices()
415453
if hasattr(d, "slice_index") and d.slice_index == 0
416454
)
417455
)
@@ -502,6 +540,8 @@ def get_process_index_and_count_and_mapping(
502540
# compatible with any mesh with num_devices.
503541
device_map = tensor_sharding.devices_indices_map((tensor_sharding.num_devices,) * ndims)
504542

543+
print("device_map by lkolluru: ", device_map)
544+
505545
# Get the slices for 'dim' for all devices.
506546
global_slice = {k: v[dim] for k, v in device_map.items()}
507547

@@ -516,11 +556,15 @@ def get_process_index_and_count_and_mapping(
516556
process_to_slice[d.process_index].add(key)
517557
all_slices.add(key)
518558

559+
print("process_to_slice by lkolluru: ", process_to_slice)
560+
519561
# Get the set of slices for the current process which we will use to compute
520562
# the index of the current process.
521563
current_pid = next(iter(tensor_sharding.addressable_devices)).process_index
522564
addressable_slices = frozenset(process_to_slice[current_pid])
523565

566+
print("addressable_slices by lkolluru: ", addressable_slices)
567+
524568
# Verify that all processes have the same number of slices.
525569
slices_per_process = len(addressable_slices)
526570
if any(len(x) != slices_per_process for x in process_to_slice.values()):
@@ -529,6 +573,7 @@ def get_process_index_and_count_and_mapping(
529573
"different number of slices."
530574
)
531575
unique_processes = list({frozenset(x) for x in process_to_slice.values()})
576+
print("unique_processes by lkolluru: ", unique_processes)
532577

533578
# After removing duplicate processes all unique slices should
534579
# cover the dimension exactly once. If they don't it means that
@@ -540,6 +585,8 @@ def get_process_index_and_count_and_mapping(
540585
# !!! patch begin
541586
pid2fid = {}
542587
for pid, _ in process_to_slice.items():
588+
print("pid by lkolluru: ", pid)
543589
pid2fid[pid] = unique_processes.index(frozenset(process_to_slice[pid]))
544590
# !!! patch end
591+
print("pid2fid by lkolluru: ", pid2fid)
545592
return feed_index, feed_count, pid2fid

axlearn/common/flash_attention/neuron_attention.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,13 @@
1717
from axlearn.common.attention_bias import BaseAttentionBias, CausalAttentionBias, split
1818
from axlearn.common.flash_attention.common import BaseFlashAttention, repeat_kv_heads
1919
from axlearn.common.kv_cache.base_kv_cache import BaseKVCache
20-
from axlearn.common.utils import Nested
20+
from axlearn.common.utils import Nested, live_devices
2121

2222
# pytype: enable=import-error
2323

2424
Tensor = jax.Array
25-
lnc = 2 if jax.devices()[0].device_kind == "NC_v3d" else 1
25+
# lnc = 2 if jax.devices()[0].device_kind == "NC_v3d" else 1
26+
lnc = 2 if live_devices()[0].device_kind == "NC_v3d" else 1
2627

2728

2829
# TODO(apoorvtintin): Add segment IDs as an argument when the kernel supports it.

axlearn/common/launch.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@
4545
from absl import flags, logging
4646

4747
from axlearn.common.status_server import StatusHTTPServer
48-
from axlearn.common.utils import get_data_dir
48+
from axlearn.common.utils import get_data_dir, live_devices
4949
from axlearn.common.utils_spmd import setup as setup_spmd
5050

5151
# pylint: enable=wrong-import-position
@@ -142,7 +142,8 @@ def setup():
142142
status_server = StatusHTTPServer(FLAGS.status_port)
143143
status_server.start()
144144

145-
devices = jax.devices()
145+
# devices = jax.devices() ##### may be update here ####
146+
devices = live_devices()
146147
logging.info("Devices: %s", devices)
147148
local_devices = jax.local_devices()
148149
logging.info("Local Devices: %s", local_devices)

axlearn/common/launch_trainer.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
from axlearn.common import measurement
1414
from axlearn.common.config import TrainerConfigFn, get_named_trainer_config
1515
from axlearn.common.trainer import SpmdTrainer, select_mesh_config
16-
from axlearn.common.utils import MeshShape, get_data_dir, infer_mesh_shape
16+
from axlearn.common.utils import MeshShape, get_data_dir, infer_mesh_shape, live_devices
1717

1818
# Trainer-specific flags.
1919
flags.DEFINE_string(
@@ -145,10 +145,21 @@ def get_trainer_config(
145145
)
146146
trainer_config: SpmdTrainer.Config = trainer_config_fn()
147147
trainer_config.dir = trainer_config.dir or flag_values.trainer_dir
148+
149+
print(f"Trainer Config Dir: {trainer_config.dir} by Camilo")
148150
if flag_values.mesh_selector is not None:
149151
select_mesh_config(trainer_config, mesh_selector=flag_values.mesh_selector)
150152
trainer_config.mesh_axis_names = trainer_config.mesh_axis_names or ("data", "model")
151-
trainer_config.mesh_shape = trainer_config.mesh_shape or (len(jax.devices()), 1)
153+
# trainer_config.mesh_shape = trainer_config.mesh_shape or (len(jax.devices()), 1)
154+
155+
print("Live devices ", live_devices())
156+
if len(live_devices()) == 32:
157+
trainer_config.mesh_shape = trainer_config.mesh_shape or (len(live_devices()), 1)
158+
if len(live_devices()) == 16:
159+
trainer_config.mesh_shape = (1, 1, 1, len(live_devices()), 1, 1)
160+
161+
print("trainer_config.mesh_shape by camilo: ", trainer_config.mesh_shape)
162+
152163
if isinstance(trainer_config.mesh_shape, MeshShape):
153164
trainer_config.mesh_shape = infer_mesh_shape(trainer_config.mesh_shape)
154165
trainer_config.start_trace_steps = [int(el) for el in flag_values.trace_at_steps]

axlearn/common/launch_trainer_main.py

Lines changed: 70 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,85 @@
22

33
"""Main function for launching the trainer."""
44

5+
import pathwaysutils
6+
import functools
57
from absl import app, flags
8+
from pathwaysutils.elastic import elastic, manager
69

7-
from axlearn.common import launch, launch_trainer, measurement
10+
from axlearn.common import launch, launch_trainer, measurement, utils
811
from axlearn.common.config import config_for_function
912

13+
enable_elastic_training = True
14+
enable_pause_resume = False
15+
enable_replica_resize = True
16+
1017

1118
def main(_):
1219
measurement.initialize(flags.FLAGS)
1320
launch.setup()
1421
trainer_config = launch_trainer.get_trainer_config()
15-
trainer_config.set(recorder=config_for_function(lambda: measurement.global_recorder))
16-
measurement.start_monitoring()
17-
launch_trainer.run_trainer(trainer_config)
22+
# trainer_config.set(recorder=config_for_function(lambda: measurement.global_recorder))
23+
# measurement.start_monitoring()
24+
clean_up_checkpoints = functools.partial(utils.clean_up_checkpoints, checkpoint_dir=trainer_config.dir)
25+
if pathwaysutils.is_pathways_backend_used() and enable_elastic_training:
26+
27+
def train():
28+
# measurement.initialize(flags.FLAGS)
29+
# launch.setup()
30+
trainer_config = launch_trainer.get_trainer_config()
31+
trainer_config.set(recorder=config_for_function(lambda: measurement.global_recorder))
32+
measurement.start_monitoring()
33+
launch_trainer.run_trainer(trainer_config)
34+
35+
utils.elastic_manager = manager.Manager()
36+
37+
# utils.elastic_manager.live_devices = live_devices()
38+
39+
if enable_pause_resume:
40+
print("Pathways backend with pause resume being used")
41+
train = utils.elastic_manager.pause_resume(
42+
max_retries=10, # Handle up to 10 disruptions before restarting
43+
poll_interval=10, # While paused, checks every 10 seconds for health
44+
timeout=300, # Waits for slices to rejoin for 5 minutes
45+
# on_elastic_event_callback=clean_up_checkpoints,
46+
)(train)
47+
48+
if enable_replica_resize:
49+
50+
def pre_callback():
51+
# Wait up to 1 minute before starting if there are any inactive slices
52+
if utils.elastic_manager.inactive_slice_indices:
53+
try:
54+
utils.elastic_manager.active_slice_indices = elastic.wait_for_slices(
55+
slice_count=utils.elastic_manager.total_slice_count,
56+
slice_to_devices=utils.elastic_manager.slice_to_devices,
57+
poll_interval=10,
58+
timeout=60,
59+
)
60+
except TimeoutError:
61+
# If there are still inactive slices, we must update the active
62+
# slices with one final check and then proceed.
63+
utils.elastic_manager.active_slice_indices = (
64+
elastic.get_active_slice_indices(
65+
slice_to_devices=utils.elastic_manager.slice_to_devices,
66+
)
67+
)
68+
69+
train = utils.elastic_manager.replica_resize(
70+
max_resizes=10, # Handle up to 10 slice up or slice down transitions
71+
poll_interval=30, # Monitor thread checks inactive slice health every 30 seconds
72+
pre_callback=pre_callback,
73+
on_elastic_event_callback=clean_up_checkpoints,
74+
)(train)
75+
76+
train()
77+
else:
78+
measurement.initialize(flags.FLAGS)
79+
launch.setup()
80+
trainer_config = launch_trainer.get_trainer_config()
81+
trainer_config.set(recorder=config_for_function(lambda: measurement.global_recorder))
82+
measurement.start_monitoring()
83+
launch_trainer.run_trainer(trainer_config)
1884

1985

2086
if __name__ == "__main__":

axlearn/common/state_builder.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
flatten_items,
4848
get_data_dir,
4949
infer_mesh_shape,
50+
live_devices,
5051
set_data_dir,
5152
)
5253

@@ -479,7 +480,10 @@ def target_to_source(self, target: Builder.State) -> tuple[Builder.State, Any]:
479480
cfg.mesh_axis_names or trainer_cfg.mesh_axis_names or ("data", "model")
480481
)
481482
trainer_cfg.mesh_shape = infer_mesh_shape(
482-
cfg.mesh_shape or trainer_cfg.mesh_shape or (len(jax.devices()), 1)
483+
# cfg.mesh_shape or trainer_cfg.mesh_shape or (len(jax.devices()), 1)
484+
cfg.mesh_shape
485+
or trainer_cfg.mesh_shape
486+
or (len(live_devices()), 1)
483487
)
484488
# Reset datasets and evalers for the pretrained model config.
485489
# This input is not used. Set global_batch_size to 0 by default.

0 commit comments

Comments
 (0)