Skip to content

[Bugfix][Training] Fix native Adam checkpoint resume and add parity checks - #427

Open
0z5a wants to merge 1 commit into
vllm-project:mainfrom
0z5a:fix/native-adam-checkpoint-resume
Open

0z5a wants to merge 1 commit into
vllm-project:mainfrom
0z5a:fix/native-adam-checkpoint-resume

Conversation

@0z5a

@0z5a 0z5a commented Sep 12, 2026

Copy link
Copy Markdown

Problem

Native Torch Adam/AdamW checkpoint resume fails at several boundaries:

  1. A fresh optimizer has no state when Megatron requests the load template, triggering the existing assertion.
  2. Parameter-shard loading attempts to copy step, although the scalar is restored from common checkpoint state.
  3. Sharing one restored step tensor across parameters makes a two-parameter optimizer advance from step 4 to step 6 after one update.

Changes

Update the Megatron Docker patch to accept the empty template, restore independent per-parameter step tensors, remove the checkpoint-only group field and retain the restored step while copying parameter shards.

Add regressions using the real Megatron methods and a fixed-batch comparison runner: eight uninterrupted updates versus four updates followed by four updates in a fresh process, including training offload and wake-up.

Validation

Check Result
Fresh publication checkout: checkpoint audit tests 20 passed
Full repository pre-commit hooks All 9 passed
Earlier Linux CPU validation 24 passed, including 4 native Megatron optimizer tests
Docker patch application Applies to pinned Megatron 1dcf0dafa884ad52ffb243625717a3471643e087; resulting optimizer source matches the tested runtime
Fixed-batch training processes All 3 completed: 8 uninterrupted, 4 prefix and 4 restored updates
Compared training states Prefix and first 3 restored updates matched uninterrupted training exactly

The fixture uses full Qwen3-0.6B with 596,049,920 parameters, BF16 model parameters, FP32 master parameters, native distributed AdamW and TP=PP=CP=1. The runner checks parameters, moments, steps, scheduler, RNG, gradients, metrics and saved training data. Missing moments and inconsistent steps are negative controls.

Draft status and limitations

No performance, convergence, larger-model, Transformer Engine or topology-resharding improvement is claimed.

Signed-off-by: 0z5a <192209249+0z5a@users.noreply.github.com>
@read-the-docs-community

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot 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.

Code Review

This pull request updates the Megatron patch to properly handle fresh native Torch Adam/AdamW optimizers during checkpoint loading, ensuring that steps are independent and updated correctly. It also introduces a comprehensive suite of integration and unit tests to verify checkpoint resume parity. The review feedback highlights a few improvement opportunities: cleaning up the temporary directory ray_temp in the parity runner to prevent disk space accumulation, and using weights_only=True when loading PyTorch checkpoints in the audit tests to enhance security and avoid deprecation warnings.

Comment on lines +47 to +79
try:
ray_temp = tempfile.mkdtemp(prefix="vime-resume-ray-")
(output / "ray-temp-dir.txt").write_text(ray_temp + "\n")
ray.init(
address="local",
num_cpus=12,
num_gpus=1,
include_dashboard=False,
object_store_memory=2 * 1024**3,
namespace=output.name,
_temp_dir=ray_temp,
_node_ip_address="127.0.0.1",
runtime_env={"env_vars": {"VIME_RESUME_AUDIT_DIR": str(output)}},
)
spec = importlib.util.spec_from_file_location("resume_parity_train", checkout / "train.py")
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
train_args = module.parse_args()
if args.stop_after is not None:
assert 0 < args.stop_after < train_args.num_rollout
# Do not change num_rollout: the scheduler horizon must be identical
# in the continuous run, split prefix, and resumed suffix.
module.range = lambda start, stop: range(start, min(stop, args.stop_after))
module.train(train_args)
(output / "train-returned.json").write_text(json.dumps({"completed": True}))
finally:
ray.shutdown()
sys.stdout.flush()
sys.stderr.flush()
for fd, original in zip([1, 2], saved_fds, strict=True):
os.dup2(original, fd)
os.close(original)
log.close()

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.

high

The temporary directory ray_temp created by tempfile.mkdtemp is not cleaned up after the training and audit logs are recorded. This can lead to disk space accumulation in /tmp on test machines or CI runners over time.

Please clean up the temporary directory in the finally block using shutil.rmtree.

    ray_temp = tempfile.mkdtemp(prefix="vime-resume-ray-")
    try:
        (output / "ray-temp-dir.txt").write_text(ray_temp + "\n")
        ray.init(
            address="local",
            num_cpus=12,
            num_gpus=1,
            include_dashboard=False,
            object_store_memory=2 * 1024**3,
            namespace=output.name,
            _temp_dir=ray_temp,
            _node_ip_address="127.0.0.1",
            runtime_env={"env_vars": {"VIME_RESUME_AUDIT_DIR": str(output)}},
        )
        spec = importlib.util.spec_from_file_location("resume_parity_train", checkout / "train.py")
        module = importlib.util.module_from_spec(spec)
        spec.loader.exec_module(module)
        train_args = module.parse_args()
        if args.stop_after is not None:
            assert 0 < args.stop_after < train_args.num_rollout
            # Do not change num_rollout: the scheduler horizon must be identical
            # in the continuous run, split prefix, and resumed suffix.
            module.range = lambda start, stop: range(start, min(stop, args.stop_after))
        module.train(train_args)
        (output / "train-returned.json").write_text(json.dumps({"completed": True}))
    finally:
        ray.shutdown()
        import shutil
        shutil.rmtree(ray_temp, ignore_errors=True)
        sys.stdout.flush()
        sys.stderr.flush()
        for fd, original in zip([1, 2], saved_fds, strict=True):
            os.dup2(original, fd)
            os.close(original)
        log.close()

Comment on lines +197 to +198
a = torch.load(continuous / f"dumps/train_data/{rid}.pt", map_location="cpu", weights_only=False)
b = torch.load(root / f"dumps/train_data/{rid}.pt", map_location="cpu", weights_only=False)

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.

security-medium medium

Using weights_only=False when loading PyTorch checkpoints is a security risk and can trigger deprecation warnings in newer PyTorch versions. Since the saved training data only contains standard PyTorch tensors and basic Python types, it is safer and cleaner to use weights_only=True.

Suggested change
a = torch.load(continuous / f"dumps/train_data/{rid}.pt", map_location="cpu", weights_only=False)
b = torch.load(root / f"dumps/train_data/{rid}.pt", map_location="cpu", weights_only=False)
a = torch.load(continuous / f"dumps/train_data/{rid}.pt", map_location="cpu", weights_only=True)
b = torch.load(root / f"dumps/train_data/{rid}.pt", map_location="cpu", weights_only=True)

@pytest.mark.parametrize("key", ["tokens", "loss_masks", "log_probs", "advantages"])
def test_rejects_changed_training_batch(evidence, key):
path = evidence[2] / "dumps/train_data/1.pt"
batch = torch.load(path, weights_only=False)

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.

security-medium medium

Using weights_only=False when loading PyTorch checkpoints is a security risk and can trigger deprecation warnings in newer PyTorch versions. Since the saved training data only contains standard PyTorch tensors and basic Python types, it is safer and cleaner to use weights_only=True.

Suggested change
batch = torch.load(path, weights_only=False)
batch = torch.load(path, weights_only=True)

@0z5a
0z5a marked this pull request as ready for review September 13, 2026 03:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant