Skip to content

Commit a420021

Browse files
authored
fix(ckpt): initialize model parallel for builder loads (#5942)
Signed-off-by: Yu Yao <yaoyu.094@gmail.com> Signed-off-by: yaoyu-33 <yaoyu.094@gmail.com>
1 parent a5e17dc commit a420021

2 files changed

Lines changed: 174 additions & 13 deletions

File tree

‎src/megatron/bridge/training/model_load_save.py‎

Lines changed: 32 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,27 @@ def temporary_distributed_context(backend: str = "gloo") -> Generator[None, None
161161
dist.destroy_process_group()
162162

163163

164+
def _get_or_initialize_pg_collection(
165+
model_cfg: TransformerConfig | ModelConfig,
166+
) -> ProcessGroupCollection:
167+
"""Return MPU process groups, initializing model-parallel state when needed."""
168+
if not parallel_state.is_initialized():
169+
parallel_state.initialize_model_parallel(
170+
tensor_model_parallel_size=model_cfg.tensor_model_parallel_size,
171+
pipeline_model_parallel_size=model_cfg.pipeline_model_parallel_size,
172+
virtual_pipeline_model_parallel_size=model_cfg.virtual_pipeline_model_parallel_size,
173+
context_parallel_size=model_cfg.context_parallel_size or 1,
174+
expert_model_parallel_size=model_cfg.expert_model_parallel_size or 1,
175+
expert_tensor_parallel_size=model_cfg.expert_tensor_parallel_size,
176+
)
177+
if torch.cuda.is_available() and torch.cuda.device_count() > 0:
178+
from megatron.core.tensor_parallel import model_parallel_cuda_manual_seed
179+
180+
model_parallel_cuda_manual_seed(0)
181+
182+
return ProcessGroupCollection.use_mpu_process_groups()
183+
184+
164185
def load_tokenizer(checkpoint_path: str, **kwargs) -> MegatronTokenizer:
165186
"""Create a tokenizer from a training checkpoint.
166187
@@ -355,13 +376,18 @@ def build_and_load_model(
355376

356377
def _call_model_provider(model_cfg):
357378
"""Handles provider call for both MBridge and MLM providers."""
358-
if isinstance(model_cfg, ModelProviderMixin):
359-
if hasattr(model_cfg, "finalize"):
360-
model_cfg.finalize()
361-
return model_cfg.provide_distributed_model(wrap_with_ddp=False, use_cpu_initialization=use_cpu_init)
362-
elif isinstance(model_cfg, ModelConfig):
379+
if isinstance(model_cfg, (ModelProviderMixin, ModelConfig)):
363380
if hasattr(model_cfg, "finalize"):
364381
model_cfg.finalize()
382+
pg_collection = _get_or_initialize_pg_collection(model_cfg)
383+
384+
if isinstance(model_cfg, ModelProviderMixin):
385+
return model_cfg.provide_distributed_model(
386+
wrap_with_ddp=False,
387+
use_cpu_initialization=use_cpu_init,
388+
pg_collection=pg_collection,
389+
)
390+
365391
builder_cls = model_cfg.get_builder_cls()
366392
builder = builder_cls(model_cfg)
367393
# Note: `use_cpu_initialization` is not passed as an explicit kwarg here,
@@ -371,9 +397,7 @@ def _call_model_provider(model_cfg):
371397
# the flag on the config object. This is intentional — we do not want
372398
# to duplicate TransformerConfig fields like `use_cpu_initialization`
373399
# as kwargs on `build_distributed_models`.
374-
return builder.build_distributed_models(
375-
ProcessGroupCollection.use_mpu_process_groups(), wrap_with_ddp=False
376-
)
400+
return builder.build_distributed_models(pg_collection, wrap_with_ddp=False)
377401
else:
378402
assert model_type in ("gpt", "hybrid", "mamba"), f"model type {model_type} not supported."
379403
assert megatron_args is not None, "megatron_args must be provided if the checkpoint is from MegatronLM."

‎tests/unit_tests/training/test_model_load_save.py‎

Lines changed: 142 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@
2020

2121
import pytest
2222
import torch
23+
import torch.distributed as dist
24+
from megatron.core import parallel_state
2325
from megatron.core.transformer.pipeline_parallel_layer_layout import PipelineParallelLayerLayout
2426
from megatron.training.models.base import ModelConfig
2527

@@ -238,7 +240,11 @@ def test_temporary_distributed_context_nccl(self, mock_seed, mock_os, mock_socke
238240
mock_socket_instance.getsockname.return_value = ("localhost", 12345)
239241
mock_socket.socket.return_value.__enter__.return_value = mock_socket_instance
240242

241-
with temporary_distributed_context(backend="nccl"):
243+
with (
244+
patch("megatron.bridge.training.model_load_save.torch.cuda.is_available", return_value=True),
245+
patch("megatron.bridge.training.model_load_save.torch.cuda.device_count", return_value=1),
246+
temporary_distributed_context(backend="nccl"),
247+
):
242248
pass
243249

244250
mock_dist.init_process_group.assert_called_once_with(
@@ -250,6 +256,74 @@ def test_temporary_distributed_context_nccl(self, mock_seed, mock_os, mock_socke
250256
mock_dist.destroy_process_group.assert_called_once()
251257

252258

259+
class TestGetOrInitializePgCollection:
260+
"""Test shared model-parallel process-group setup for Bridge model configs."""
261+
262+
@patch("megatron.core.tensor_parallel.model_parallel_cuda_manual_seed")
263+
@patch("megatron.bridge.training.model_load_save.torch.cuda.device_count", return_value=1)
264+
@patch("megatron.bridge.training.model_load_save.torch.cuda.is_available", return_value=True)
265+
@patch("megatron.bridge.training.model_load_save.ProcessGroupCollection")
266+
@patch("megatron.bridge.training.model_load_save.parallel_state")
267+
def test_initializes_model_parallel_and_cuda_rng(
268+
self,
269+
mock_parallel_state,
270+
mock_pg_collection,
271+
mock_cuda_available,
272+
mock_cuda_device_count,
273+
mock_seed,
274+
):
275+
"""Initialize missing MPU state before returning its process groups."""
276+
model_cfg = SimpleNamespace(
277+
tensor_model_parallel_size=2,
278+
pipeline_model_parallel_size=4,
279+
virtual_pipeline_model_parallel_size=2,
280+
context_parallel_size=0,
281+
expert_model_parallel_size=None,
282+
expert_tensor_parallel_size=1,
283+
)
284+
expected_pg_collection = Mock()
285+
mock_parallel_state.is_initialized.return_value = False
286+
mock_pg_collection.use_mpu_process_groups.return_value = expected_pg_collection
287+
288+
result = model_load_save._get_or_initialize_pg_collection(model_cfg)
289+
290+
assert result is expected_pg_collection
291+
mock_parallel_state.initialize_model_parallel.assert_called_once_with(
292+
tensor_model_parallel_size=2,
293+
pipeline_model_parallel_size=4,
294+
virtual_pipeline_model_parallel_size=2,
295+
context_parallel_size=1,
296+
expert_model_parallel_size=1,
297+
expert_tensor_parallel_size=1,
298+
)
299+
mock_seed.assert_called_once_with(0)
300+
mock_pg_collection.use_mpu_process_groups.assert_called_once_with()
301+
302+
@patch("megatron.core.tensor_parallel.model_parallel_cuda_manual_seed")
303+
@patch("megatron.bridge.training.model_load_save.torch.cuda.is_available", return_value=True)
304+
@patch("megatron.bridge.training.model_load_save.ProcessGroupCollection")
305+
@patch("megatron.bridge.training.model_load_save.parallel_state")
306+
def test_reuses_initialized_model_parallel_state(
307+
self,
308+
mock_parallel_state,
309+
mock_pg_collection,
310+
mock_cuda_available,
311+
mock_seed,
312+
):
313+
"""Do not reinitialize or reseed an existing MPU state."""
314+
model_cfg = Mock()
315+
expected_pg_collection = Mock()
316+
mock_parallel_state.is_initialized.return_value = True
317+
mock_pg_collection.use_mpu_process_groups.return_value = expected_pg_collection
318+
319+
result = model_load_save._get_or_initialize_pg_collection(model_cfg)
320+
321+
assert result is expected_pg_collection
322+
mock_parallel_state.initialize_model_parallel.assert_not_called()
323+
mock_seed.assert_not_called()
324+
mock_pg_collection.use_mpu_process_groups.assert_called_once_with()
325+
326+
253327
class TestLoadMegatronModel:
254328
"""Test load_megatron_model function."""
255329

@@ -361,6 +435,7 @@ def _finalized_layout(checkpoint_path, model_cfg, *args):
361435
else:
362436
assert result is None
363437

438+
@patch("megatron.bridge.training.model_load_save._get_or_initialize_pg_collection")
364439
@patch("megatron.bridge.training.model_load_save.temporary_distributed_context")
365440
@patch("megatron.bridge.training.checkpointing._load_model_weights_from_checkpoint")
366441
@patch("megatron.bridge.utils.instantiate_utils.instantiate")
@@ -377,6 +452,7 @@ def test_load_mbridge_saved_model(
377452
mock_instantiate,
378453
mock_load_weights,
379454
mock_temp_dist,
455+
mock_get_pg_collection,
380456
):
381457
# Setup mocks
382458
mock_dist.is_available.return_value = False
@@ -394,6 +470,8 @@ def test_load_mbridge_saved_model(
394470
mock_model_cfg.use_cpu_initialization = False
395471

396472
mock_instantiate.return_value = mock_model_cfg
473+
expected_pg_collection = Mock()
474+
mock_get_pg_collection.return_value = expected_pg_collection
397475
expected_result = {"layer.weight": torch.randn(2, 2)}
398476
mock_load_weights.return_value = expected_result
399477

@@ -408,32 +486,37 @@ def test_load_mbridge_saved_model(
408486
mock_run_config.assert_called_once()
409487
mock_instantiate.assert_called_once_with(mock_run_cfg_dict["model"])
410488
mock_cpu_context.assert_called_once()
411-
mock_model_cfg.provide_distributed_model.assert_called_once()
489+
mock_get_pg_collection.assert_called_once_with(mock_model_cfg)
490+
mock_model_cfg.provide_distributed_model.assert_called_once_with(
491+
wrap_with_ddp=False,
492+
use_cpu_initialization=True,
493+
pg_collection=expected_pg_collection,
494+
)
412495
mock_load_weights.assert_called_once_with(ckpt_path, [mock_model], return_state_dict=True)
413496
assert mock_model_cfg.params_dtype == torch.bfloat16
414497

415498
result = load_megatron_model(ckpt_path, return_state_dict=False, use_cpu_init=True)
416499
assert result == [mock_model]
417500
mock_load_weights.assert_called_with(ckpt_path, [mock_model], return_state_dict=False)
418501

502+
@patch("megatron.bridge.training.model_load_save._get_or_initialize_pg_collection")
419503
@patch("megatron.bridge.training.model_load_save.temporary_distributed_context")
420504
@patch("megatron.bridge.training.checkpointing._load_model_weights_from_checkpoint")
421505
@patch("megatron.bridge.training.checkpointing.read_run_config")
422506
@patch("megatron.bridge.training.checkpointing.get_checkpoint_run_config_filename")
423507
@patch("megatron.bridge.training.model_load_save.megatron_cpu_init_context")
424508
@patch("megatron.bridge.training.model_load_save.dist")
425-
@patch("megatron.bridge.training.model_load_save.ProcessGroupCollection")
426509
@patch("megatron.bridge.training.model_load_save.ModelConfig.from_dict")
427510
def test_load_mbridge_saved_model_config(
428511
self,
429512
mock_from_dict,
430-
mock_pg_collection,
431513
mock_dist,
432514
mock_cpu_context,
433515
mock_run_config_fname,
434516
mock_run_config,
435517
mock_load_weights,
436518
mock_temp_dist,
519+
mock_get_pg_collection,
437520
):
438521
"""Test loading a model when config yaml contains a serialized ModelConfig instance."""
439522
# Setup mocks
@@ -464,7 +547,7 @@ def test_load_mbridge_saved_model_config(
464547
mock_from_dict.return_value = mock_model_cfg
465548

466549
mock_mpu_pgs = Mock()
467-
mock_pg_collection.use_mpu_process_groups.return_value = mock_mpu_pgs
550+
mock_get_pg_collection.return_value = mock_mpu_pgs
468551

469552
expected_result = {"layer.weight": torch.randn(2, 2)}
470553
mock_load_weights.return_value = expected_result
@@ -482,6 +565,7 @@ def test_load_mbridge_saved_model_config(
482565
mock_cpu_context.assert_called_once()
483566
mock_model_cfg.finalize.assert_called_once()
484567
mock_model_cfg.get_builder_cls.assert_called_once()
568+
mock_get_pg_collection.assert_called_once_with(mock_model_cfg)
485569
mock_builder_cls.assert_called_once_with(mock_model_cfg)
486570
mock_builder.build_distributed_models.assert_called_once_with(
487571
mock_mpu_pgs,
@@ -494,6 +578,53 @@ def test_load_mbridge_saved_model_config(
494578
assert result == [mock_model]
495579
mock_load_weights.assert_called_with(ckpt_path, [mock_model], return_state_dict=False)
496580

581+
@patch("megatron.bridge.training.checkpointing._load_model_weights_from_checkpoint")
582+
@patch("megatron.bridge.training.checkpointing.read_run_config")
583+
@patch("megatron.bridge.training.checkpointing.get_checkpoint_run_config_filename")
584+
@patch("megatron.bridge.training.model_load_save.ModelConfig.from_dict")
585+
def test_load_builder_config_initializes_model_parallel_with_existing_default_group(
586+
self,
587+
mock_from_dict,
588+
mock_run_config_fname,
589+
mock_run_config,
590+
mock_load_weights,
591+
tmp_path,
592+
):
593+
"""Load a builder checkpoint when the caller owns only the default process group."""
594+
mock_run_config_fname.return_value = tmp_path / "run_config.yaml"
595+
mock_run_config.return_value = {
596+
"model": {"tensor_model_parallel_size": 1, "_builder_": "import.path.to.SomeModelBuilder"}
597+
}
598+
599+
mock_model = Mock()
600+
mock_model_cfg = Mock(spec=GPTModelConfig)
601+
mock_model_cfg.params_dtype = torch.float32
602+
mock_model_cfg.bf16 = False
603+
mock_model_cfg.fp16 = False
604+
mock_model_cfg.use_cpu_initialization = False
605+
mock_model_cfg.finalize = Mock()
606+
mock_builder = Mock()
607+
mock_builder.build_distributed_models.return_value = [mock_model]
608+
mock_model_cfg.get_builder_cls.return_value = Mock(return_value=mock_builder)
609+
mock_from_dict.return_value = mock_model_cfg
610+
611+
(tmp_path / "run_config.yaml").touch()
612+
rendezvous = tmp_path / "gloo_rendezvous"
613+
dist.init_process_group("gloo", init_method=f"file://{rendezvous}", rank=0, world_size=1)
614+
try:
615+
assert not parallel_state.is_initialized()
616+
617+
result = load_megatron_model(tmp_path, use_cpu_init=True)
618+
619+
assert result == [mock_model]
620+
assert parallel_state.is_initialized()
621+
mock_builder.build_distributed_models.assert_called_once()
622+
mock_load_weights.assert_called_once_with(tmp_path, [mock_model], return_state_dict=False)
623+
finally:
624+
if parallel_state.is_initialized():
625+
parallel_state.destroy_model_parallel()
626+
dist.destroy_process_group()
627+
497628
@pytest.mark.parametrize("model_type", ["gpt", "hybrid", "mamba", "resnet"])
498629
@patch("megatron.bridge.training.model_load_save.temporary_distributed_context")
499630
@patch("megatron.bridge.training.mlm_compat.model._hybrid_provider")
@@ -582,6 +713,7 @@ def test_load_mlm_saved_model(
582713
with pytest.raises(AssertionError, match=f"model type {model_type} not supported."):
583714
load_megatron_model(ckpt_path, model_type=model_type, return_state_dict=True, use_cpu_init=True)
584715

716+
@patch("megatron.bridge.training.model_load_save._get_or_initialize_pg_collection")
585717
@patch("megatron.bridge.training.model_load_save.temporary_distributed_context")
586718
@patch("megatron.bridge.training.checkpointing._load_model_weights_from_checkpoint")
587719
@patch("megatron.bridge.utils.instantiate_utils.instantiate")
@@ -598,6 +730,7 @@ def test_load_megatron_model_skip_temp_dist_context(
598730
mock_instantiate,
599731
mock_load_weights,
600732
mock_temp_dist,
733+
mock_get_pg_collection,
601734
):
602735
"""Test loading model when distributed is already initialized."""
603736

@@ -617,6 +750,7 @@ def test_load_megatron_model_skip_temp_dist_context(
617750
mock_model_cfg.use_cpu_initialization = False
618751

619752
mock_instantiate.return_value = mock_model_cfg
753+
mock_get_pg_collection.return_value = Mock()
620754

621755
with tempfile.TemporaryDirectory() as ckpt_path:
622756
config_file = Path(ckpt_path) / "run_config.yaml"
@@ -626,6 +760,7 @@ def test_load_megatron_model_skip_temp_dist_context(
626760
assert result == mock_model
627761
mock_temp_dist.assert_not_called()
628762

763+
@patch("megatron.bridge.training.model_load_save._get_or_initialize_pg_collection")
629764
@patch("megatron.bridge.training.model_load_save.temporary_distributed_context")
630765
@patch("megatron.bridge.training.post_training.checkpointing.load_modelopt_state")
631766
@patch("megatron.bridge.training.post_training.checkpointing.has_modelopt_state")
@@ -646,6 +781,7 @@ def test_load_mbridge_saved_model_with_modelopt_state(
646781
mock_has_modelopt_state,
647782
mock_load_modelopt_state,
648783
mock_temp_dist,
784+
mock_get_pg_collection,
649785
):
650786
"""Test loading model when modelopt state exists and model supports it."""
651787
# Setup mocks
@@ -665,6 +801,7 @@ def test_load_mbridge_saved_model_with_modelopt_state(
665801
mock_model_cfg.restore_modelopt_state = False # Initially False
666802

667803
mock_instantiate.return_value = mock_model_cfg
804+
mock_get_pg_collection.return_value = Mock()
668805
expected_result = {"layer.weight": torch.randn(2, 2)}
669806
mock_load_weights.return_value = expected_result
670807

0 commit comments

Comments
 (0)