Skip to content

Commit 2b11855

Browse files
committed
Add a new goodput elastic utils function record_slice_state()
1 parent 518ad8d commit 2b11855

3 files changed

Lines changed: 130 additions & 38 deletions

File tree

src/maxtext/common/goodput.py

Lines changed: 45 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -66,18 +66,31 @@ def maybe_monitor_goodput(config):
6666
enable_gcp_goodput_metrics=config.enable_gcp_goodput_metrics,
6767
enable_gcp_step_deviation_metrics=config.enable_gcp_step_deviation_metrics,
6868
)
69-
goodput_monitor = monitoring.GoodputMonitor(
70-
job_name=config.run_name,
71-
logger_name=f"goodput_{config.run_name}",
72-
tensorboard_dir=config.tensorboard_dir,
73-
upload_interval=config.goodput_upload_interval_seconds,
74-
monitoring_enabled=True,
75-
pathway_enabled=config.enable_pathways_goodput,
76-
include_badput_breakdown=True,
77-
include_step_deviation=config.monitor_step_time_deviation,
78-
step_deviation_interval_seconds=config.step_deviation_interval_seconds,
79-
gcp_options=gcp_options,
80-
)
69+
monitor_class = monitoring.GoodputMonitor
70+
71+
if config.elastic_enabled:
72+
try:
73+
from ml_goodput_measurement import monitoring_elastic # pylint: disable=import-outside-toplevel
74+
75+
monitor_class = monitoring_elastic.ElasticGoodputMonitor
76+
except ImportError:
77+
max_logging.log("Elastic monitor failed!")
78+
79+
kwargs = {
80+
"job_name": config.run_name,
81+
"logger_name": f"goodput_{config.run_name}",
82+
"tensorboard_dir": config.tensorboard_dir,
83+
"upload_interval": config.goodput_upload_interval_seconds,
84+
"monitoring_enabled": True,
85+
"include_badput_breakdown": True,
86+
"include_step_deviation": config.monitor_step_time_deviation,
87+
"step_deviation_interval_seconds": config.step_deviation_interval_seconds,
88+
"gcp_options": gcp_options,
89+
}
90+
if monitor_class == monitoring.GoodputMonitor:
91+
kwargs["pathway_enabled"] = config.enable_pathways_goodput
92+
93+
goodput_monitor = monitor_class(**kwargs)
8194
goodput_monitor.start_goodput_uploader()
8295
max_logging.log("Started Goodput upload to Tensorboard & GCM in the background!")
8396
yield
@@ -121,8 +134,23 @@ def create_goodput_recorder(config):
121134
if config.enable_goodput_recording and jax.process_index() == 0:
122135
max_logging.log("[GOODPUT NO-OP] recorder skipped (decoupled stub).")
123136
return None
124-
if config.enable_goodput_recording:
125-
logger_name = f"goodput_{config.run_name}"
126-
recorder = goodput.GoodputRecorder(config.run_name, logger_name, jax.process_index() == 0)
127-
return recorder
128-
return None
137+
138+
if not config.enable_goodput_recording:
139+
return None
140+
141+
logger_name = f"goodput_{config.run_name}"
142+
143+
# Detect if we should use the elastic-aware recorder
144+
if config.elastic_enabled:
145+
try:
146+
from ml_goodput_measurement import goodput_elastic # pylint: disable=import-outside-toplevel
147+
from maxtext.utils import elastic_utils # pylint: disable=import-outside-toplevel
148+
149+
recorder = goodput_elastic.ElasticGoodputRecorder(config.run_name, logger_name, jax.process_index() == 0)
150+
elastic_utils.record_slice_state(recorder)
151+
except ImportError as e:
152+
max_logging.log(f"Could not create elastic goodput recorder: {e}")
153+
else:
154+
return recorder
155+
156+
return goodput.GoodputRecorder(config.run_name, logger_name, jax.process_index() == 0)

src/maxtext/utils/elastic_utils.py

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,13 +28,37 @@
2828
pending_elastic_event_type = None
2929

3030

31+
def record_slice_state(recorder, active_slices_override: int | None = None) -> None:
32+
"""Records live slice counts and logs them to the GoodputRecorder."""
33+
if recorder is None or not hasattr(recorder, "record_elastic_slice_counts"):
34+
return
35+
36+
if not pathwaysutils.is_pathways_backend_used() or elastic_manager is None:
37+
available_slices = 1
38+
active_slices = 1 if active_slices_override is None else active_slices_override
39+
total_slices = 1
40+
else:
41+
available_slices = len(pathwaysutils.elastic.get_active_slice_indices())
42+
active_slices = (
43+
active_slices_override if active_slices_override is not None else len(elastic_manager.active_slice_indices)
44+
)
45+
total_slices = len(pathwaysutils.elastic.get_slice_to_devices(jax.devices()))
46+
47+
recorder.record_elastic_slice_counts(
48+
available_slices=available_slices,
49+
active_slices=active_slices,
50+
total_slices=total_slices,
51+
)
52+
53+
3154
def record_elastic_event_start(recorder, config) -> None:
3255
"""Records start of an elastic scale up event."""
3356
global pending_elastic_event_type
3457
event_type = "elastic_scale_up" if is_scale_up_event(config) else "elastic_slice_down"
3558
pending_elastic_event_type = event_type
3659
if recorder:
37-
recorder.record_custom_badput_event_start_time(custom_badput_event_type=event_type)
60+
recorder.record_elastic_wait_start_time(event_type=event_type)
61+
record_slice_state(recorder, active_slices_override=0)
3862

3963

4064
def record_elastic_wait_end_and_reinit_start(recorder) -> None:
@@ -45,16 +69,18 @@ def record_elastic_wait_end_and_reinit_start(recorder) -> None:
4569
event_type = pending_elastic_event_type
4670
pending_elastic_event_type = None
4771
if recorder:
48-
recorder.record_custom_badput_event_end_time(custom_badput_event_type=event_type)
49-
recorder.record_custom_badput_event_start_time(custom_badput_event_type="elastic_reinitialization")
72+
recorder.record_elastic_wait_end_time(event_type=event_type)
73+
recorder.record_elastic_reinit_start_time()
74+
record_slice_state(recorder)
5075
pending_reinit_recorder = recorder
5176

5277

5378
def record_elastic_reinit_end() -> None:
5479
"""Records end of elastic reinitialization event."""
5580
global pending_reinit_recorder
5681
if pending_reinit_recorder is not None:
57-
pending_reinit_recorder.record_custom_badput_event_end_time(custom_badput_event_type="elastic_reinitialization")
82+
pending_reinit_recorder.record_elastic_reinit_end_time()
83+
record_slice_state(pending_reinit_recorder)
5884
pending_reinit_recorder = None
5985

6086

@@ -175,6 +201,10 @@ def elastic_retry(config, callback_fn=None, pre_callback_fn=None):
175201
Returns:
176202
A decorator for elastic retry.
177203
"""
204+
if not pathwaysutils.is_pathways_backend_used() and config.elastic_enabled:
205+
max_logging.log("Elastic Retry disabled (simulated logging mode). Returning identity decorator.")
206+
return lambda f: f
207+
178208
if not elastic_enabled(config):
179209
msg = (
180210
"Elastic training requires the Pathways backend, and elastic_enabled"

tests/unit/elastic_utils_test.py

Lines changed: 51 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,12 @@ def setUp(self):
7070

7171
# Configure default behaviors if needed
7272
self.fake_pathwaysutils.is_pathways_backend_used.return_value = True
73+
self.fake_pathwaysutils.elastic = Mock()
74+
self.fake_pathwaysutils.elastic.get_active_slice_indices.return_value = [0, 1]
75+
self.fake_pathwaysutils.elastic.get_slice_to_devices.return_value = {
76+
0: [FakeDevice()],
77+
1: [FakeDevice()],
78+
}
7379
self.fake_jax.process_index.return_value = 0
7480

7581
# Inject fakes into elastic_utils namespace
@@ -97,9 +103,28 @@ def tearDown(self):
97103
elastic_utils.pending_elastic_event_type = None
98104
super().tearDown()
99105

106+
def test_record_slice_state(self):
107+
elastic_utils.elastic_manager = self.fake_manager
108+
self.fake_manager.active_slice_indices = {0}
109+
self.fake_manager.slice_to_devices = {0: [FakeDevice()], 1: [FakeDevice()]}
110+
self.fake_pathwaysutils.elastic.get_active_slice_indices.return_value = {0, 1}
111+
112+
fake_recorder = Mock()
113+
fake_recorder.record_elastic_slice_counts = Mock()
114+
115+
elastic_utils.record_slice_state(fake_recorder)
116+
117+
fake_recorder.record_elastic_slice_counts.assert_called_once_with(available_slices=2, active_slices=1, total_slices=2)
118+
119+
fake_recorder.record_elastic_slice_counts.reset_mock()
120+
elastic_utils.record_slice_state(fake_recorder, active_slices_override=0)
121+
fake_recorder.record_elastic_slice_counts.assert_called_once_with(available_slices=2, active_slices=0, total_slices=2)
122+
100123
def test_elastic_enabled(self):
101124
config = FakeConfig()
102125
self.fake_pathwaysutils.is_pathways_backend_used.return_value = True
126+
self.fake_pathwaysutils.elastic = Mock()
127+
self.fake_pathwaysutils.elastic.get_active_slice_indices.return_value = [0, 1]
103128
config.elastic_enabled = True
104129
self.assertTrue(elastic_utils.elastic_enabled(config))
105130

@@ -144,6 +169,8 @@ def test_live_devices_no_pathways(self):
144169
def test_live_devices_pathways(self):
145170
"""Tests live_devices when pathways is used."""
146171
self.fake_pathwaysutils.is_pathways_backend_used.return_value = True
172+
self.fake_pathwaysutils.elastic = Mock()
173+
self.fake_pathwaysutils.elastic.get_active_slice_indices.return_value = [0, 1]
147174
device0 = FakeDevice(slice_index=0)
148175
device1 = FakeDevice(slice_index=1)
149176
self.fake_jax.devices.return_value = [device0, device1]
@@ -168,6 +195,8 @@ def test_live_devices_disabled(self):
168195
def test_elastic_retry_disabled(self):
169196
"""Tests elastic_retry when disabled but pathways is used."""
170197
self.fake_pathwaysutils.is_pathways_backend_used.return_value = True
198+
self.fake_pathwaysutils.elastic = Mock()
199+
self.fake_pathwaysutils.elastic.get_active_slice_indices.return_value = [0, 1]
171200
config = FakeConfig()
172201
config.elastic_enabled = False
173202
msg = (
@@ -319,28 +348,28 @@ def test_record_elastic_event_start(self):
319348
"""Tests recording an elastic slice down start."""
320349
elastic_utils.elastic_manager = self.fake_manager
321350
self.fake_manager.new_slice_event.is_set.return_value = False
351+
self.fake_manager.slice_to_devices = {0: [FakeDevice()], 1: [FakeDevice()]}
322352
fake_recorder = Mock()
323353
config = FakeConfig()
324354

325355
elastic_utils.record_elastic_event_start(fake_recorder, config)
326356

327-
fake_recorder.record_custom_badput_event_start_time.assert_called_once_with(
328-
custom_badput_event_type="elastic_slice_down"
329-
)
357+
fake_recorder.record_elastic_wait_start_time.assert_called_once_with(event_type="elastic_slice_down")
358+
fake_recorder.record_elastic_slice_counts.assert_called_once()
330359
self.assertEqual(elastic_utils.pending_elastic_event_type, "elastic_slice_down")
331360

332361
def test_record_elastic_event_start_scale_up(self):
333362
"""Tests recording an elastic slice scale up start."""
334363
elastic_utils.elastic_manager = self.fake_manager
335364
self.fake_manager.new_slice_event.is_set.return_value = True
365+
self.fake_manager.slice_to_devices = {0: [FakeDevice()], 1: [FakeDevice()]}
336366
fake_recorder = Mock()
337367
config = FakeConfig()
338368

339369
elastic_utils.record_elastic_event_start(fake_recorder, config)
340370

341-
fake_recorder.record_custom_badput_event_start_time.assert_called_once_with(
342-
custom_badput_event_type="elastic_scale_up"
343-
)
371+
fake_recorder.record_elastic_wait_start_time.assert_called_once_with(event_type="elastic_scale_up")
372+
fake_recorder.record_elastic_slice_counts.assert_called_once()
344373

345374
def test_record_elastic_wait_end_and_reinit_start_noop_on_first_attempt(self):
346375
"""Tests recording elastic event end and elastic reinit start."""
@@ -349,36 +378,39 @@ def test_record_elastic_wait_end_and_reinit_start_noop_on_first_attempt(self):
349378

350379
elastic_utils.record_elastic_wait_end_and_reinit_start(fake_recorder)
351380

352-
fake_recorder.record_custom_badput_event_end_time.assert_not_called()
353-
fake_recorder.record_custom_badput_event_start_time.assert_not_called()
381+
fake_recorder.record_elastic_wait_end_time.assert_not_called()
382+
fake_recorder.record_elastic_reinit_start_time.assert_not_called()
354383
self.assertIsNone(elastic_utils.pending_reinit_recorder)
355384

356385
def test_record_elastic_wait_end_and_reinit_start(self):
357386
"""Test recording end of slice down and start of reinit."""
358387
elastic_utils.pending_elastic_event_type = "elastic_slice_down"
388+
elastic_utils.elastic_manager = self.fake_manager
389+
self.fake_manager.active_slice_indices = {0}
390+
self.fake_manager.slice_to_devices = {0: [FakeDevice()], 1: [FakeDevice()]}
391+
elastic_utils.pending_elastic_event_type = "elastic_slice_down"
359392
fake_recorder = Mock()
360393

361394
elastic_utils.record_elastic_wait_end_and_reinit_start(fake_recorder)
362395

363-
fake_recorder.record_custom_badput_event_end_time.assert_called_once_with(
364-
custom_badput_event_type="elastic_slice_down"
365-
)
366-
fake_recorder.record_custom_badput_event_start_time.assert_called_once_with(
367-
custom_badput_event_type="elastic_reinitialization"
368-
)
396+
fake_recorder.record_elastic_wait_end_time.assert_called_once_with(event_type="elastic_slice_down")
397+
fake_recorder.record_elastic_reinit_start_time.assert_called_once()
398+
fake_recorder.record_elastic_slice_counts.assert_called_once()
369399
self.assertIs(elastic_utils.pending_reinit_recorder, fake_recorder)
370400
self.assertIsNone(elastic_utils.pending_elastic_event_type)
371401

372402
def test_record_elastic_reinit_end(self):
373403
"""Tests recording end of elastic reinit."""
374404
fake_recorder = Mock()
375405
elastic_utils.pending_reinit_recorder = fake_recorder
406+
elastic_utils.elastic_manager = self.fake_manager
407+
self.fake_manager.active_slice_indices = {0}
408+
self.fake_manager.slice_to_devices = {0: [FakeDevice()], 1: [FakeDevice()]}
376409

377410
elastic_utils.record_elastic_reinit_end()
378411

379-
fake_recorder.record_custom_badput_event_end_time.assert_called_once_with(
380-
custom_badput_event_type="elastic_reinitialization"
381-
)
412+
fake_recorder.record_elastic_reinit_end_time.assert_called_once()
413+
fake_recorder.record_elastic_slice_counts.assert_called_once()
382414
self.assertIsNone(elastic_utils.pending_reinit_recorder)
383415

384416
def test_record_elastic_reinit_end_on_cold_start(self):
@@ -401,6 +433,8 @@ def __setattr__(self, name, value):
401433

402434
config = ReadOnlyConfig()
403435
self.fake_pathwaysutils.is_pathways_backend_used.return_value = True
436+
self.fake_pathwaysutils.elastic = Mock()
437+
self.fake_pathwaysutils.elastic.get_active_slice_indices.return_value = [0, 1]
404438

405439
# Should not raise ValueError
406440
elastic_utils.ensure_elastic_manager_initialized(config)

0 commit comments

Comments
 (0)