Skip to content

[multi-lora v2 3/7] Per-slot checkpoints with optimizer state - #2842

Merged
yueming-yuan merged 1 commit into
mainfrom
yueming/per-slot-checkpoint
Sep 16, 2026
Merged

yueming-yuan merged 1 commit into
mainfrom
yueming/per-slot-checkpoint

Conversation

@yueming-yuan

@yueming-yuan yueming-yuan commented Aug 31, 2026 •

Copy link
Copy Markdown
Collaborator

Co-authored with @yushengsu-thu.

Save and restore one adapter slot independently. Checkpoints contain adapter weight shards, per-rank Adam state and step counters, and FP32 master parameters; loading optimizer state requires the same parallel topology.

Checkpoint load/save execution failures, including filesystem errors, propagate to TrainerCell and terminate the current single-cell Tinker service. lora/actor.py and controller commands expose save_slot and load_slot(..., ckpt_path=..., load_optimizer=...). A TODO in checkpoint_io.py records deferred IO failure isolation.

Checkpoint names are relative symlinks to immutable version directories. The shared writer completes shards and metadata before atomically replacing the public link. A process crash leaves the old or new complete checkpoint reachable; older versions remain on disk for readers that already resolved them. Legacy directory checkpoints remain readable, but overwriting them is unsupported: save under a new name. Successful phases retain their collective barriers; execution errors still propagate to the trainer cell.

Stacked on #2841.

Validation:

  • pre-commit run --all-files and incremental git diff --check passed on every updated component head. All previous heads remain ancestors; the final Unify HF snapshot and checkpoint writing #3198 tree exactly matches the reviewed implementation.
  • Process-exit probes at both sides of publication lost the public path before the fix; afterward it resolves to the complete old or new version, and a subsequent save succeeds. A parameterized regression is included in the existing [multi-lora v2 7/7] Tinker gateway example and tests #2849 checkpoint test file.
  • Sampler-to-training load probes with and without optimizer state now return a user failure before any backend load, while another tenant continues training. Repeated SAVE → LOAD verifies distinct retained versions and loading from a pinned version.
  • Focused checkpoint continuation passed on 2 H200 GPUs (DP=2, TP/PP/EP=1), using real Megatron DDP, MultiLoRA layers, SlotOptimizer and distributed checkpoint IO. After three Adam steps, slot 0 was saved and restored into a fresh optimizer on slot 1; weights, FP32 masters, Adam moments, parameter-group metadata and step counters matched with zero tolerance before and after the next identical-gradient update. Rounding masters to BF16 and restarting from weights alone were both detected by negative controls.
  • The enabled regression is tests/e2e/lora/test_slot_checkpoint_resume.py (Hopper, LoRA, 2-GPU stage). The tested final tree is Unify HF snapshot and checkpoint writing #3198 fd63c7ec8c41c2e0a61d273f0cd5a3504275ada7, with Megatron 73b54618f7e58e0f25f619bcfecbe2640765475a, Bridge 40b930897717941cfe2bd9806f417e28bf1bfa65, PyTorch 2.13.0+cu130 and Transformer Engine 2.17.0. Checkpoints were on GPFS. pre-commit run --all-files passed.
  • This isolates checkpoint/Adam continuation with identical post-reduction gradients; it does not validate forward/backward numerics, topology changes, MoE/TP/PP layouts, server restart, or failure recovery. Fresh hosted CPU/NVIDIA CI is pending; broad GPU selection remains on Unify HF snapshot and checkpoint writing #3198. The separate Tinker cookbook acceptance test remains disabled pending GPU validation.

ci-megatron-pr: 73b54618f7e58e0f25f619bcfecbe2640765475a
ci-sglang-pr: 02b5e123c490c988d21526d0dd7837fefdd0e1da

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Claude Code Review

This repository is configured for manual code reviews. Comment @claude review for a one-time review, or @claude review always to subscribe this PR to a review on every future push.

Tip: disable this comment in your organization's Code Review settings.

@yueming-yuan
yueming-yuan force-pushed the yueming/loss-per-batch branch from 5aa9371 to 1900fdd Compare August 31, 2026 23:51
@yueming-yuan
yueming-yuan force-pushed the yueming/per-slot-checkpoint branch from 630b838 to 86a4575 Compare August 31, 2026 23:51
@yueming-yuan
yueming-yuan force-pushed the yueming/loss-per-batch branch from 1900fdd to 7b4f6ff Compare September 1, 2026 00:49
@yueming-yuan
yueming-yuan force-pushed the yueming/per-slot-checkpoint branch from 86a4575 to bbb1215 Compare September 1, 2026 00:49
@yueming-yuan
yueming-yuan force-pushed the yueming/loss-per-batch branch from 7b4f6ff to e6c02c0 Compare September 1, 2026 03:25
@yueming-yuan
yueming-yuan force-pushed the yueming/per-slot-checkpoint branch from bbb1215 to c2ec43d Compare September 1, 2026 03:25
@yueming-yuan
yueming-yuan force-pushed the yueming/loss-per-batch branch from e6c02c0 to 974a894 Compare September 1, 2026 03:40
@yueming-yuan
yueming-yuan force-pushed the yueming/per-slot-checkpoint branch from c2ec43d to 47eefc1 Compare September 1, 2026 03:40
@yueming-yuan
yueming-yuan force-pushed the yueming/loss-per-batch branch from 974a894 to e6abb46 Compare September 1, 2026 05:27
@yueming-yuan
yueming-yuan force-pushed the yueming/per-slot-checkpoint branch from 47eefc1 to 3dc33bc Compare September 1, 2026 05:27
@yueming-yuan
yueming-yuan force-pushed the yueming/loss-per-batch branch from e6abb46 to e3016e8 Compare September 1, 2026 19:29
@yueming-yuan
yueming-yuan force-pushed the yueming/per-slot-checkpoint branch 2 times, most recently from a0bb8d8 to c3a6fcb Compare September 1, 2026 19:32
@yueming-yuan
yueming-yuan force-pushed the yueming/loss-per-batch branch from 90bfd2a to 923ad13 Compare September 1, 2026 20:31
@yueming-yuan
yueming-yuan force-pushed the yueming/per-slot-checkpoint branch 2 times, most recently from 4a22314 to d9e4f5b Compare September 1, 2026 20:46
@yueming-yuan
yueming-yuan force-pushed the yueming/loss-per-batch branch from 923ad13 to fe0c476 Compare September 2, 2026 22:04
@yueming-yuan
yueming-yuan force-pushed the yueming/per-slot-checkpoint branch from d9e4f5b to 76a25dc Compare September 2, 2026 22:05
@yueming-yuan
yueming-yuan force-pushed the yueming/loss-per-batch branch from fe0c476 to 2d57c78 Compare September 4, 2026 08:56
@yueming-yuan
yueming-yuan force-pushed the yueming/per-slot-checkpoint branch from 616f2c8 to 6cea0c6 Compare September 9, 2026 23:12
@yueming-yuan
yueming-yuan force-pushed the yueming/loss-per-batch branch from 90e05d5 to 51f34fa Compare September 9, 2026 23:12
@yueming-yuan yueming-yuan changed the title [multi-lora v2 3/6] Per-slot checkpoints with optimizer state [multi-lora v2 3/7] Per-slot checkpoints with optimizer state Sep 9, 2026
@yueming-yuan
yueming-yuan force-pushed the yueming/per-slot-checkpoint branch from 04bf60a to b038a65 Compare September 10, 2026 07:15
def apply_shards():
loaded = load_adapter(model, slot, shards["weights"])
assert loaded > 0, f"loaded 0 adapter tensors from {checkpoint_dir / _weight_shard_name()}"
optimizer.reload_model_params()

@nanjiangwill nanjiangwill Sep 10, 2026 •

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.

optimizer is the outer LayerWise/Chained optimizer, so this reload walks every slot’s child and copies BF16 model weights into FP32 masters. Loading slot S therefore rounds the masters of every live slot; only S is restored from masters below. The preceding lora_executor.load_slot() call does the same. Add a slot-local reload helper and use it in both paths, with a two-slot test that keeps slot B’s FP32 master different from its BF16 model value.

shards: dict = {}

def read_shards():
shards["weights"] = torch.load(checkpoint_dir / _weight_shard_name(), map_location="cpu", weights_only=True)

@nanjiangwill nanjiangwill Sep 10, 2026 •

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.

The read phase validates only optimizer list lengths. It does not validate master/moment tensor shapes or adapter weight keys and shapes. load_adapter() copies matching tensors first and reports missing or unused keys afterward, so a bad shard can mutate part of the live slot before raising. Validate the complete weight and optimizer tensor layout in read_shards() before any copy, or add a validation-only Bridge helper.

if old_dir.exists():
shutil.rmtree(old_dir)
os.replace(final_dir, old_dir)
os.replace(tmp_dir, final_dir)

@nanjiangwill nanjiangwill Sep 10, 2026 •

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.

This code moved; the current comment is here: #2842 (comment)

return ckpt._weight_shard_name()


def test_a_missing_optim_shard_leaves_the_slot_untouched(tmp_path):

@nanjiangwill nanjiangwill Sep 10, 2026 •

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.

These tests cover read-phase failures only. They never apply a valid checkpoint, so they do not verify FP32 masters, Adam moments and step counters, weights-only loading, or equivalence of the next Adam update. Add a CPU round-trip test for exact continuation. Include a second live slot whose FP32 master differs from its BF16 model value so the test also catches cross-slot reloads.

old_dir = final_dir.parent / f"_old_{final_dir.name}"
if old_dir.exists():
shutil.rmtree(old_dir)
os.replace(final_dir, old_dir)

@nanjiangwill nanjiangwill Sep 10, 2026 •

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.

write_checkpoint_dir() now rolls back a caught failure from the second rename, but the published path is still absent between final_dir -> old_dir and tmp_dir -> final_dir. A process exit in that window leaves the last valid checkpoint only under _old_<name>, so readers lose the public path. Also, shutil.rmtree(old_dir) still runs inside the publish phase, so cleanup failure reports the save as failed after the replacement is committed. Use immutable version directories plus an atomically replaced pointer/exchange for crash atomicity, and make old-version cleanup best-effort.

@yueming-yuan
yueming-yuan force-pushed the yueming/per-slot-checkpoint branch from a72f7f1 to 7c8d7a4 Compare September 10, 2026 21:32
@yueming-yuan
yueming-yuan force-pushed the yueming/per-slot-checkpoint branch from 7c8d7a4 to fec9abe Compare September 10, 2026 23:34
@yueming-yuan
yueming-yuan force-pushed the yueming/per-slot-checkpoint branch 2 times, most recently from 7ae642f to b8af08f Compare September 11, 2026 00:30
@yueming-yuan
yueming-yuan force-pushed the yueming/per-slot-checkpoint branch from 50a63b5 to 3c59f65 Compare September 11, 2026 06:47
@yueming-yuan
yueming-yuan force-pushed the yueming/per-slot-checkpoint branch from 3c59f65 to 53f60d4 Compare September 12, 2026 01:52
@yueming-yuan
yueming-yuan force-pushed the yueming/per-slot-checkpoint branch from 53f60d4 to f7a9659 Compare September 14, 2026 18:41
@yueming-yuan
yueming-yuan force-pushed the yueming/loss-per-batch branch 2 times, most recently from 2e97600 to 331ed96 Compare September 14, 2026 21:41
@yueming-yuan
yueming-yuan force-pushed the yueming/per-slot-checkpoint branch from f7a9659 to a9f2099 Compare September 14, 2026 21:41
@yueming-yuan yueming-yuan mentioned this pull request Sep 15, 2026
7 of 15 tasks
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.

3 participants