Skip to content

Commit 4676159

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

3 files changed

Lines changed: 118 additions & 26 deletions

File tree

src/maxtext/common/goodput.py

Lines changed: 35 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import contextlib
2323
import jax
2424
from enum import Enum
25+
import pathwaysutils
2526
from maxtext.utils import max_logging
2627
from maxtext.common.gcloud_stub import goodput_modules
2728

@@ -66,18 +67,31 @@ def maybe_monitor_goodput(config):
6667
enable_gcp_goodput_metrics=config.enable_gcp_goodput_metrics,
6768
enable_gcp_step_deviation_metrics=config.enable_gcp_step_deviation_metrics,
6869
)
69-
goodput_monitor = monitoring.GoodputMonitor(
70+
monitor_class = monitoring.GoodputMonitor
71+
72+
if config.elastic_enabled:
73+
try:
74+
from ml_goodput_measurement import monitoring_elastic
75+
monitor_class = monitoring_elastic.ElasticGoodputMonitor
76+
except ImportError:
77+
max_logging.log("Elastic monitor failed!")
78+
pass
79+
80+
kwargs = dict(
7081
job_name=config.run_name,
7182
logger_name=f"goodput_{config.run_name}",
7283
tensorboard_dir=config.tensorboard_dir,
7384
upload_interval=config.goodput_upload_interval_seconds,
7485
monitoring_enabled=True,
75-
pathway_enabled=config.enable_pathways_goodput,
7686
include_badput_breakdown=True,
7787
include_step_deviation=config.monitor_step_time_deviation,
7888
step_deviation_interval_seconds=config.step_deviation_interval_seconds,
7989
gcp_options=gcp_options,
8090
)
91+
if monitor_class == monitoring.GoodputMonitor:
92+
kwargs["pathway_enabled"] = config.enable_pathways_goodput
93+
94+
goodput_monitor = monitor_class(**kwargs)
8195
goodput_monitor.start_goodput_uploader()
8296
max_logging.log("Started Goodput upload to Tensorboard & GCM in the background!")
8397
yield
@@ -121,8 +135,22 @@ def create_goodput_recorder(config):
121135
if config.enable_goodput_recording and jax.process_index() == 0:
122136
max_logging.log("[GOODPUT NO-OP] recorder skipped (decoupled stub).")
123137
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
138+
139+
if not config.enable_goodput_recording:
140+
return None
141+
142+
logger_name = f"goodput_{config.run_name}"
143+
144+
# Detect if we should use the elastic-aware recorder
145+
if config.elastic_enabled:
146+
try:
147+
from ml_goodput_measurement import goodput_elastic
148+
from maxtext.utils import elastic_utils
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: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,13 +28,29 @@
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') or not pathwaysutils.is_pathways_backend_used() or elastic_manager is None:
34+
return
35+
36+
available_slices = len(pathwaysutils.elastic.get_active_slice_indices())
37+
active_slices = active_slices_override if active_slices_override is not None else len(elastic_manager.active_slice_indices)
38+
total_slices = len(pathwaysutils.elastic.get_slice_to_devices(jax.devices()))
39+
40+
recorder.record_elastic_slice_counts(
41+
available_slices=available_slices,
42+
active_slices=active_slices,
43+
total_slices=total_slices
44+
)
45+
3146
def record_elastic_event_start(recorder, config) -> None:
3247
"""Records start of an elastic scale up event."""
3348
global pending_elastic_event_type
3449
event_type = "elastic_scale_up" if is_scale_up_event(config) else "elastic_slice_down"
3550
pending_elastic_event_type = event_type
3651
if recorder:
37-
recorder.record_custom_badput_event_start_time(custom_badput_event_type=event_type)
52+
recorder.record_elastic_wait_start_time(event_type=event_type)
53+
record_slice_state(recorder, active_slices_override=0)
3854

3955

4056
def record_elastic_wait_end_and_reinit_start(recorder) -> None:
@@ -45,16 +61,18 @@ def record_elastic_wait_end_and_reinit_start(recorder) -> None:
4561
event_type = pending_elastic_event_type
4662
pending_elastic_event_type = None
4763
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")
64+
recorder.record_elastic_wait_end_time(event_type=event_type)
65+
recorder.record_elastic_reinit_start_time()
66+
record_slice_state(recorder)
5067
pending_reinit_recorder = recorder
5168

5269

5370
def record_elastic_reinit_end() -> None:
5471
"""Records end of elastic reinitialization event."""
5572
global pending_reinit_recorder
5673
if pending_reinit_recorder is not None:
57-
pending_reinit_recorder.record_custom_badput_event_end_time(custom_badput_event_type="elastic_reinitialization")
74+
pending_reinit_recorder.record_elastic_reinit_end_time()
75+
record_slice_state(pending_reinit_recorder)
5876
pending_reinit_recorder = None
5977

6078

tests/unit/elastic_utils_test.py

Lines changed: 61 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,9 @@ 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 = {0: [FakeDevice()], 1: [FakeDevice()]}
7376
self.fake_jax.process_index.return_value = 0
7477

7578
# Inject fakes into elastic_utils namespace
@@ -97,9 +100,37 @@ def tearDown(self):
97100
elastic_utils.pending_elastic_event_type = None
98101
super().tearDown()
99102

103+
104+
def test_record_slice_state(self):
105+
elastic_utils.elastic_manager = self.fake_manager
106+
self.fake_manager.active_slice_indices = {0}
107+
self.fake_manager.slice_to_devices = {0: [FakeDevice()], 1: [FakeDevice()]}
108+
self.fake_pathwaysutils.elastic.get_active_slice_indices.return_value = {0, 1}
109+
110+
fake_recorder = Mock()
111+
fake_recorder.record_elastic_slice_counts = Mock()
112+
113+
elastic_utils.record_slice_state(fake_recorder)
114+
115+
fake_recorder.record_elastic_slice_counts.assert_called_once_with(
116+
available_slices=2,
117+
active_slices=1,
118+
total_slices=2
119+
)
120+
121+
fake_recorder.record_elastic_slice_counts.reset_mock()
122+
elastic_utils.record_slice_state(fake_recorder, active_slices_override=0)
123+
fake_recorder.record_elastic_slice_counts.assert_called_once_with(
124+
available_slices=2,
125+
active_slices=0,
126+
total_slices=2
127+
)
128+
100129
def test_elastic_enabled(self):
101130
config = FakeConfig()
102131
self.fake_pathwaysutils.is_pathways_backend_used.return_value = True
132+
self.fake_pathwaysutils.elastic = Mock()
133+
self.fake_pathwaysutils.elastic.get_active_slice_indices.return_value = [0, 1]
103134
config.elastic_enabled = True
104135
self.assertTrue(elastic_utils.elastic_enabled(config))
105136

@@ -144,6 +175,8 @@ def test_live_devices_no_pathways(self):
144175
def test_live_devices_pathways(self):
145176
"""Tests live_devices when pathways is used."""
146177
self.fake_pathwaysutils.is_pathways_backend_used.return_value = True
178+
self.fake_pathwaysutils.elastic = Mock()
179+
self.fake_pathwaysutils.elastic.get_active_slice_indices.return_value = [0, 1]
147180
device0 = FakeDevice(slice_index=0)
148181
device1 = FakeDevice(slice_index=1)
149182
self.fake_jax.devices.return_value = [device0, device1]
@@ -168,6 +201,8 @@ def test_live_devices_disabled(self):
168201
def test_elastic_retry_disabled(self):
169202
"""Tests elastic_retry when disabled but pathways is used."""
170203
self.fake_pathwaysutils.is_pathways_backend_used.return_value = True
204+
self.fake_pathwaysutils.elastic = Mock()
205+
self.fake_pathwaysutils.elastic.get_active_slice_indices.return_value = [0, 1]
171206
config = FakeConfig()
172207
config.elastic_enabled = False
173208
msg = (
@@ -319,28 +354,32 @@ def test_record_elastic_event_start(self):
319354
"""Tests recording an elastic slice down start."""
320355
elastic_utils.elastic_manager = self.fake_manager
321356
self.fake_manager.new_slice_event.is_set.return_value = False
357+
self.fake_manager.slice_to_devices = {0: [FakeDevice()], 1: [FakeDevice()]}
322358
fake_recorder = Mock()
323359
config = FakeConfig()
324360

325361
elastic_utils.record_elastic_event_start(fake_recorder, config)
326362

327-
fake_recorder.record_custom_badput_event_start_time.assert_called_once_with(
328-
custom_badput_event_type="elastic_slice_down"
363+
fake_recorder.record_elastic_wait_start_time.assert_called_once_with(
364+
event_type="elastic_slice_down"
329365
)
330-
self.assertEqual(elastic_utils.pending_elastic_event_type, "elastic_slice_down")
366+
fake_recorder.record_elastic_slice_counts.assert_called_once()
367+
self.assertEqual(elastic_utils.pending_elastic_event_type, 'elastic_slice_down')
331368

332369
def test_record_elastic_event_start_scale_up(self):
333370
"""Tests recording an elastic slice scale up start."""
334371
elastic_utils.elastic_manager = self.fake_manager
335372
self.fake_manager.new_slice_event.is_set.return_value = True
373+
self.fake_manager.slice_to_devices = {0: [FakeDevice()], 1: [FakeDevice()]}
336374
fake_recorder = Mock()
337375
config = FakeConfig()
338376

339377
elastic_utils.record_elastic_event_start(fake_recorder, config)
340378

341-
fake_recorder.record_custom_badput_event_start_time.assert_called_once_with(
342-
custom_badput_event_type="elastic_scale_up"
379+
fake_recorder.record_elastic_wait_start_time.assert_called_once_with(
380+
event_type="elastic_scale_up"
343381
)
382+
fake_recorder.record_elastic_slice_counts.assert_called_once()
344383

345384
def test_record_elastic_wait_end_and_reinit_start_noop_on_first_attempt(self):
346385
"""Tests recording elastic event end and elastic reinit start."""
@@ -349,36 +388,41 @@ def test_record_elastic_wait_end_and_reinit_start_noop_on_first_attempt(self):
349388

350389
elastic_utils.record_elastic_wait_end_and_reinit_start(fake_recorder)
351390

352-
fake_recorder.record_custom_badput_event_end_time.assert_not_called()
353-
fake_recorder.record_custom_badput_event_start_time.assert_not_called()
391+
fake_recorder.record_elastic_wait_end_time.assert_not_called()
392+
fake_recorder.record_elastic_reinit_start_time.assert_not_called()
354393
self.assertIsNone(elastic_utils.pending_reinit_recorder)
355394

356395
def test_record_elastic_wait_end_and_reinit_start(self):
357396
"""Test recording end of slice down and start of reinit."""
397+
elastic_utils.pending_elastic_event_type = 'elastic_slice_down'
398+
elastic_utils.elastic_manager = self.fake_manager
399+
self.fake_manager.active_slice_indices = {0}
400+
self.fake_manager.slice_to_devices = {0: [FakeDevice()], 1: [FakeDevice()]}
358401
elastic_utils.pending_elastic_event_type = "elastic_slice_down"
359402
fake_recorder = Mock()
360403

361404
elastic_utils.record_elastic_wait_end_and_reinit_start(fake_recorder)
362405

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"
406+
fake_recorder.record_elastic_wait_end_time.assert_called_once_with(
407+
event_type="elastic_slice_down"
368408
)
409+
fake_recorder.record_elastic_reinit_start_time.assert_called_once()
410+
fake_recorder.record_elastic_slice_counts.assert_called_once()
369411
self.assertIs(elastic_utils.pending_reinit_recorder, fake_recorder)
370412
self.assertIsNone(elastic_utils.pending_elastic_event_type)
371413

372414
def test_record_elastic_reinit_end(self):
373415
"""Tests recording end of elastic reinit."""
374416
fake_recorder = Mock()
375417
elastic_utils.pending_reinit_recorder = fake_recorder
418+
elastic_utils.elastic_manager = self.fake_manager
419+
self.fake_manager.active_slice_indices = {0}
420+
self.fake_manager.slice_to_devices = {0: [FakeDevice()], 1: [FakeDevice()]}
376421

377422
elastic_utils.record_elastic_reinit_end()
378423

379-
fake_recorder.record_custom_badput_event_end_time.assert_called_once_with(
380-
custom_badput_event_type="elastic_reinitialization"
381-
)
424+
fake_recorder.record_elastic_reinit_end_time.assert_called_once()
425+
fake_recorder.record_elastic_slice_counts.assert_called_once()
382426
self.assertIsNone(elastic_utils.pending_reinit_recorder)
383427

384428
def test_record_elastic_reinit_end_on_cold_start(self):
@@ -401,6 +445,8 @@ def __setattr__(self, name, value):
401445

402446
config = ReadOnlyConfig()
403447
self.fake_pathwaysutils.is_pathways_backend_used.return_value = True
448+
self.fake_pathwaysutils.elastic = Mock()
449+
self.fake_pathwaysutils.elastic.get_active_slice_indices.return_value = [0, 1]
404450

405451
# Should not raise ValueError
406452
elastic_utils.ensure_elastic_manager_initialized(config)

0 commit comments

Comments
 (0)