Skip to content

fix(checkpoint): load base weights under DDP and MegatronFSDP - #3597

Open
ralovets wants to merge 2 commits into
NVIDIA-NeMo:mainfrom
ralovets:ralovets/fix/ddp-base-checkpoint-load
Open

fix(checkpoint): load base weights under DDP and MegatronFSDP#3597
ralovets wants to merge 2 commits into
NVIDIA-NeMo:mainfrom
ralovets:ralovets/fix/ddp-base-checkpoint-load

Conversation

@ralovets

Copy link
Copy Markdown
Contributor

What does this PR do ?

Loads the pretrained checkpoint for MODEL_ARCH_MAPPING models under strategy: ddp and
strategy: megatron_fsdp. These models previously trained from random weights, with no
error or warning.

The pre-shard load in apply_model_infrastructure was gated on is_meta_device. Its
else branch assumes the weights already came from from_pretrained, which is true only
on the HuggingFace fallback path. AutoModel's own models are built architecture-only, and
auto_model.py:517 excludes DDPManager and MegatronFSDPManager from meta init, so the
checkpoint read was skipped. checkpoint_already_loaded = True then suppressed the
post-shard load. The fix branches on weights_already_loaded, which is already computed
in auto_model.py and already used at the post-shard load site.

Changelog

Fix

  • nemo_automodel/_transformers/infrastructure.py
    • In the load_before_shard path, branch on weights_already_loaded instead of
      is_meta_device when deciding whether to read the base checkpoint.
    • initialize_model_weights stays gated on is_meta_device. Only meta models need
      their parameter shells materialized.

Tests

  • tests/unit_tests/_transformers/test_infrastructure.py (3 CPU tests, load decision)
    • test_load_before_shard_loads_checkpoint_when_init_left_weights_unloaded: reads the
      checkpoint when init left the weights unloaded. Fails without the fix.
    • test_load_before_shard_skips_checkpoint_when_init_already_loaded_weights: skips the
      read when from_pretrained already populated the weights.
    • test_load_before_shard_populates_unloaded_model_from_checkpoint: end to end against
      a real safetensors checkpoint, asserts every parameter holds its value. Fails without
      the fix.
  • tests/unit_tests/_transformers/test_auto_model.py (1 CPU test, flag computation)
    • test_custom_model_under_ddp_still_needs_its_checkpoint: asserts is_meta_device and
      weights_already_loaded are both False for a MODEL_ARCH_MAPPING model under
      DDPManager.

Before your PR is "Ready for review"

Pre checks:

  • Make sure you read and followed Contributor guidelines
  • Did you write any new necessary tests?
  • Did you add or update any necessary documentation? Not applicable. No public API,
    config key, or documented behavior changes.

Additional Information

Reproducing

# Saves a tiny Llama checkpoint whose every weight is 0.5, loads it back through
# NeMoAutoModelForCausalLM.from_pretrained, and counts how many parameters hold that
# value. Needs a GPU: from_pretrained calls torch.cuda.current_device().
#
#   torchrun --nproc-per-node 1 repro.py ddp             # 0/21 before the fix, 21/21 after
#   torchrun --nproc-per-node 2 repro.py megatron_fsdp   # 0/12 before the fix, 12/12 after
#   torchrun --nproc-per-node 1 repro.py fsdp2           # 21/21 either way

import sys
import tempfile

import torch
from transformers import LlamaConfig, LlamaForCausalLM

from nemo_automodel import NeMoAutoModelForCausalLM
from nemo_automodel.components.distributed import DistributedSetup, initialize_distributed

config = LlamaConfig(hidden_size=64, intermediate_size=128, num_hidden_layers=2, num_attention_heads=4, vocab_size=256)
reference = LlamaForCausalLM(config)
with torch.no_grad():
    for param in reference.parameters():
        param.fill_(0.5)
checkpoint = tempfile.mkdtemp()
reference.save_pretrained(checkpoint)

strategy = sys.argv[1] if len(sys.argv) > 1 else "ddp"
dist_env = initialize_distributed("nccl")
setup = DistributedSetup.build(strategy=strategy, world_size=dist_env.world_size)
model = NeMoAutoModelForCausalLM.from_pretrained(checkpoint, distributed_setup=setup)

# Skip 0-element shards: megatron_fsdp leaves some ranks holding empty slices,
# and `.all()` on an empty tensor is vacuously True.
shards = [p.detach().to_local() if hasattr(p, "to_local") else p.detach() for p in model.parameters()]
shards = [t for t in shards if t.numel() > 0]
loaded = sum(int(t.eq(0.5).all()) for t in shards)
print(f"\n[{strategy}] {loaded}/{len(shards)} params hold the checkpoint value")

AutoModel's own model implementations (anything in MODEL_ARCH_MAPPING) trained
from random weights under `strategy: ddp` and `strategy: megatron_fsdp`, with no
error or warning.

`apply_model_infrastructure` gated the base-checkpoint read on `is_meta_device`,
whose `else` branch assumed "not on meta" implies "weights already loaded". That
holds for the HuggingFace fallback path, where `from_pretrained` populates the
weights during init. It does not hold for custom implementations, which
`_init_model` builds with `model_cls(hf_config)` -- architecture only.
`auto_model.py` excludes DDPManager and MegatronFSDPManager from meta-device
init, so every custom model under those two wrappers took the `else` branch and
never read its checkpoint. Setting `checkpoint_already_loaded = True` then
suppressed the post-shard load that would otherwise have caught it.

Branch on `weights_already_loaded` instead, which is the flag that answers
whether the model still needs its checkpoint. It was already computed in
`auto_model.py` and already consulted at the post-shard load site.

fsdp2 is unaffected above world size 1: its mesh has a real `dp_shard` axis, so
`dp_shard_size > 1` routes it to the post-shard load. megatron_fsdp is affected
at every world size, its mesh axes being (dp, cp, tp) with no `dp_shard`.

Verified on 2 GPUs with a tiny Llama checkpoint of all-0.5 weights and with
Qwen3-1.7B, comparing the language-modeling loss on a known sentence:

  strategy       GPUs   tensors holding 0.5        Qwen3-1.7B loss
  ddp            1      0/21        -> 21/21       11.5824 -> 2.9304
  ddp            2      0/21        -> 21/21       12.6028 -> 2.9304
  megatron_fsdp  1      0/21        -> 21/21       12.4241 -> 2.9304
  megatron_fsdp  2      0/12 local  -> 12/12       12.2534 -> 2.9304
  fsdp2          1      21/21       -> 21/21        2.9304 -> 2.9304
  fsdp2          2      21/21       -> 21/21        2.9375 -> 2.9375

ln(151936) = 11.93, so every broken row sat at or above a uniform distribution
over the vocabulary, and the values were not reproducible run to run. After the
fix all five unsharded configurations agree exactly. The fsdp2 2-GPU value is
unchanged and differs only because that is the one configuration applying
FSDP2's bf16 MixedPrecisionPolicy.

Adds four CPU tests. Three cover the load decision: two pinning it for either
value of `weights_already_loaded`, and one end-to-end check that a model built
without weights ends up holding the checkpoint's tensors. Two of those three
fail without this change. The fourth pins the flag computation that feeds the
decision, so a regression in the manager exclusion or in
`weights_already_loaded` cannot silently reintroduce random-weight training.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: Roman Ralovets <roman@ralovets.com>
@ralovets
ralovets requested a review from a team as a code owner August 20, 2026 13:31
@copy-pr-bot

copy-pr-bot Bot commented Aug 20, 2026

Copy link
Copy Markdown

This pull request requires additional validation before any workflows can run on NVIDIA's runners.

Pull request vetters can view their responsibilities here.

Contributors can view more details about this message here.

@akoumpa

akoumpa commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

/ok to test ee5f50a

@yuhezhang-ai yuhezhang-ai left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for fixing it! LGTM

@akoumpa
akoumpa enabled auto-merge (squash) August 20, 2026 20:02
@svcnvidia-nemo-ci svcnvidia-nemo-ci added the waiting-on-customer Waiting on the original author to respond label Aug 20, 2026
@akoumpa

akoumpa commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

/ok to test 6078fbc

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

community-request waiting-on-customer Waiting on the original author to respond

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants