Skip to content

Commit 75f532b

Browse files
committed
Squashed commits: update GoodputRecorder and ElasticRecorder
1 parent 518ad8d commit 75f532b

3 files changed

Lines changed: 112 additions & 22 deletions

File tree

src/maxtext/common/goodput.py

Lines changed: 30 additions & 3 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
@@ -123,6 +137,19 @@ def create_goodput_recorder(config):
123137
return None
124138
if config.enable_goodput_recording:
125139
logger_name = f"goodput_{config.run_name}"
126-
recorder = goodput.GoodputRecorder(config.run_name, logger_name, jax.process_index() == 0)
140+
141+
# Detect if we should use the elastic-aware recorder
142+
recorder = None
143+
if config.elastic_enabled:
144+
try:
145+
from ml_goodput_measurement import goodput_elastic
146+
from maxtext.utils import elastic_utils
147+
recorder = goodput_elastic.ElasticGoodputRecorder(config.run_name, logger_name, jax.process_index() == 0)
148+
elastic_utils.record_slice_state(recorder)
149+
except ImportError:
150+
pass
151+
152+
if recorder is None:
153+
recorder = goodput.GoodputRecorder(config.run_name, logger_name, jax.process_index() == 0)
127154
return recorder
128155
return None

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: 60 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,8 @@ 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]
7375
self.fake_jax.process_index.return_value = 0
7476

7577
# Inject fakes into elastic_utils namespace
@@ -97,9 +99,37 @@ def tearDown(self):
9799
elastic_utils.pending_elastic_event_type = None
98100
super().tearDown()
99101

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

@@ -144,6 +174,8 @@ def test_live_devices_no_pathways(self):
144174
def test_live_devices_pathways(self):
145175
"""Tests live_devices when pathways is used."""
146176
self.fake_pathwaysutils.is_pathways_backend_used.return_value = True
177+
self.fake_pathwaysutils.elastic = Mock()
178+
self.fake_pathwaysutils.elastic.get_active_slice_indices.return_value = [0, 1]
147179
device0 = FakeDevice(slice_index=0)
148180
device1 = FakeDevice(slice_index=1)
149181
self.fake_jax.devices.return_value = [device0, device1]
@@ -168,6 +200,8 @@ def test_live_devices_disabled(self):
168200
def test_elastic_retry_disabled(self):
169201
"""Tests elastic_retry when disabled but pathways is used."""
170202
self.fake_pathwaysutils.is_pathways_backend_used.return_value = True
203+
self.fake_pathwaysutils.elastic = Mock()
204+
self.fake_pathwaysutils.elastic.get_active_slice_indices.return_value = [0, 1]
171205
config = FakeConfig()
172206
config.elastic_enabled = False
173207
msg = (
@@ -319,28 +353,32 @@ def test_record_elastic_event_start(self):
319353
"""Tests recording an elastic slice down start."""
320354
elastic_utils.elastic_manager = self.fake_manager
321355
self.fake_manager.new_slice_event.is_set.return_value = False
356+
self.fake_manager.slice_to_devices = {0: [FakeDevice()], 1: [FakeDevice()]}
322357
fake_recorder = Mock()
323358
config = FakeConfig()
324359

325360
elastic_utils.record_elastic_event_start(fake_recorder, config)
326361

327-
fake_recorder.record_custom_badput_event_start_time.assert_called_once_with(
328-
custom_badput_event_type="elastic_slice_down"
362+
fake_recorder.record_elastic_wait_start_time.assert_called_once_with(
363+
event_type="elastic_slice_down"
329364
)
330-
self.assertEqual(elastic_utils.pending_elastic_event_type, "elastic_slice_down")
365+
fake_recorder.record_elastic_slice_counts.assert_called_once()
366+
self.assertEqual(elastic_utils.pending_elastic_event_type, 'elastic_slice_down')
331367

332368
def test_record_elastic_event_start_scale_up(self):
333369
"""Tests recording an elastic slice scale up start."""
334370
elastic_utils.elastic_manager = self.fake_manager
335371
self.fake_manager.new_slice_event.is_set.return_value = True
372+
self.fake_manager.slice_to_devices = {0: [FakeDevice()], 1: [FakeDevice()]}
336373
fake_recorder = Mock()
337374
config = FakeConfig()
338375

339376
elastic_utils.record_elastic_event_start(fake_recorder, config)
340377

341-
fake_recorder.record_custom_badput_event_start_time.assert_called_once_with(
342-
custom_badput_event_type="elastic_scale_up"
378+
fake_recorder.record_elastic_wait_start_time.assert_called_once_with(
379+
event_type="elastic_scale_up"
343380
)
381+
fake_recorder.record_elastic_slice_counts.assert_called_once()
344382

345383
def test_record_elastic_wait_end_and_reinit_start_noop_on_first_attempt(self):
346384
"""Tests recording elastic event end and elastic reinit start."""
@@ -349,36 +387,41 @@ def test_record_elastic_wait_end_and_reinit_start_noop_on_first_attempt(self):
349387

350388
elastic_utils.record_elastic_wait_end_and_reinit_start(fake_recorder)
351389

352-
fake_recorder.record_custom_badput_event_end_time.assert_not_called()
353-
fake_recorder.record_custom_badput_event_start_time.assert_not_called()
390+
fake_recorder.record_elastic_wait_end_time.assert_not_called()
391+
fake_recorder.record_elastic_reinit_start_time.assert_not_called()
354392
self.assertIsNone(elastic_utils.pending_reinit_recorder)
355393

356394
def test_record_elastic_wait_end_and_reinit_start(self):
357395
"""Test recording end of slice down and start of reinit."""
396+
elastic_utils.pending_elastic_event_type = 'elastic_slice_down'
397+
elastic_utils.elastic_manager = self.fake_manager
398+
self.fake_manager.active_slice_indices = {0}
399+
self.fake_manager.slice_to_devices = {0: [FakeDevice()], 1: [FakeDevice()]}
358400
elastic_utils.pending_elastic_event_type = "elastic_slice_down"
359401
fake_recorder = Mock()
360402

361403
elastic_utils.record_elastic_wait_end_and_reinit_start(fake_recorder)
362404

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"
405+
fake_recorder.record_elastic_wait_end_time.assert_called_once_with(
406+
event_type="elastic_slice_down"
368407
)
408+
fake_recorder.record_elastic_reinit_start_time.assert_called_once()
409+
fake_recorder.record_elastic_slice_counts.assert_called_once()
369410
self.assertIs(elastic_utils.pending_reinit_recorder, fake_recorder)
370411
self.assertIsNone(elastic_utils.pending_elastic_event_type)
371412

372413
def test_record_elastic_reinit_end(self):
373414
"""Tests recording end of elastic reinit."""
374415
fake_recorder = Mock()
375416
elastic_utils.pending_reinit_recorder = fake_recorder
417+
elastic_utils.elastic_manager = self.fake_manager
418+
self.fake_manager.active_slice_indices = {0}
419+
self.fake_manager.slice_to_devices = {0: [FakeDevice()], 1: [FakeDevice()]}
376420

377421
elastic_utils.record_elastic_reinit_end()
378422

379-
fake_recorder.record_custom_badput_event_end_time.assert_called_once_with(
380-
custom_badput_event_type="elastic_reinitialization"
381-
)
423+
fake_recorder.record_elastic_reinit_end_time.assert_called_once()
424+
fake_recorder.record_elastic_slice_counts.assert_called_once()
382425
self.assertIsNone(elastic_utils.pending_reinit_recorder)
383426

384427
def test_record_elastic_reinit_end_on_cold_start(self):
@@ -401,6 +444,8 @@ def __setattr__(self, name, value):
401444

402445
config = ReadOnlyConfig()
403446
self.fake_pathwaysutils.is_pathways_backend_used.return_value = True
447+
self.fake_pathwaysutils.elastic = Mock()
448+
self.fake_pathwaysutils.elastic.get_active_slice_indices.return_value = [0, 1]
404449

405450
# Should not raise ValueError
406451
elastic_utils.ensure_elastic_manager_initialized(config)

0 commit comments

Comments
 (0)