Skip to content

checkpoints: last.ckpt as a symlink, not a second full copy - #672

Draft
RyanPCo wants to merge 1 commit into
ryanco/opscale-ladderfrom
ryanco/ckpt-save-last-link
Draft

RyanPCo wants to merge 1 commit into
ryanco/opscale-ladderfrom
ryanco/ckpt-save-last-link

Conversation

@RyanPCo

@RyanPCo RyanPCo commented Sep 19, 2026 •

Copy link
Copy Markdown
Contributor

Writes last.ckpt as a relative symlink to the epoch checkpoint just saved (save_last: link), not a second full copy. With save_last: true, ModelCheckpoint calls _save_checkpoint twice per checkpoint event and writes the same tensors twice, ~21 GB each time on Pi 0.5.

Behaviour changes

  • last.ckpt is now a symlink in callbacks/checkpoints.yaml. Anything that copies checkpoint dirs must preserve links (rsync -l), or last.ckpt is lost.
  • A requeue with a dangling or missing last.ckpt now raises. A link can dangle where a file could not: the target deleted by hand, last.ckpt dropped by an rsync without -l, or a preemption between Lightning's unlink and relink. The requeue path used to warn and silently restart the run from scratch. It now raises FileNotFoundError when last.ckpt dangles, or is missing while the dir holds other .ckpt files, and names the ln -s fix. Only an empty checkpoint dir still falls back to the launch-time ckpt_path.
  • The callback relinks on every train epoch once any checkpoint exists, not only on checkpoint epochs. That adds one os.remove + os.symlink + strategy.barrier per epoch.

Why it is safe

  • save_top_k is -1, so the target is never collected.
  • _should_remove_checkpoint returns False when previous == current, which is always the case for last.ckpt.
  • Resume is unaffected: _requeue_resume_path gates on os.path.isfile, which follows symlinks.

Cost removed

On the now-retired ABC-DiT-L the second copy measured 9 GB and ~1.3 min, inside a checkpoint event that blocked the run for 2.7–3.4 min of a 42.3-min 1000-step cycle. Pi 0.5 checkpoints are ~21 GB.

Tests

New in tests/unit/test_checkpoint_resume.py:

  • test_save_last_link_writes_a_symlink_not_a_second_copy drives a real two-epoch fit. link leaves a relative symlink to the newest epoch checkpoint; true leaves a regular file the same size as it, which is the copy this removes.
  • test_checkpoint_callback_links_last_instead_of_copying checks the composed config.
  • test_requeue_resumes_through_a_symlinked_last_ckpt, test_requeue_with_a_dangling_last_ckpt_raises, test_requeue_with_checkpoints_but_no_last_ckpt_raises.

🤖 Generated with Claude Code

https://claude.ai/code/session_01HjCQBqQCr2igFEMR7yUovw

RyanPCo commented Sep 19, 2026 •

Copy link
Copy Markdown
Contributor Author

Warning

This pull request is not mergeable via GitHub because a downstack PR is open. Once all requirements are satisfied, merge this PR as a stack on Graphite.
Learn more

This stack of pull requests is managed by Graphite. Learn more about stacking.

@RyanPCo
RyanPCo force-pushed the ryanco/ckpt-save-last-link branch from 2ae6b54 to af92a04 Compare September 20, 2026 19:09
@RyanPCo
RyanPCo force-pushed the ryanco/opscale-ladder branch from f50f94c to c9c284f Compare September 20, 2026 19:09
@RyanPCo
RyanPCo force-pushed the ryanco/ckpt-save-last-link branch from af92a04 to a5c1dcb Compare September 21, 2026 18:45
@RyanPCo
RyanPCo force-pushed the ryanco/opscale-ladder branch from c9c284f to 72d026d Compare September 21, 2026 18:45
@RyanPCo
RyanPCo force-pushed the ryanco/ckpt-save-last-link branch from a5c1dcb to ac3ee5d Compare September 22, 2026 13:18
@RyanPCo
RyanPCo force-pushed the ryanco/opscale-ladder branch from 72d026d to bbac4a0 Compare September 22, 2026 13:18
@RyanPCo
RyanPCo force-pushed the ryanco/opscale-ladder branch from bfcf288 to 9a70f91 Compare October 12, 2026 20:26
@RyanPCo
RyanPCo force-pushed the ryanco/ckpt-save-last-link branch from a7e6042 to 286b8e8 Compare October 12, 2026 21:50
@RyanPCo
RyanPCo force-pushed the ryanco/opscale-ladder branch from 9a70f91 to 05f1e3e Compare October 12, 2026 21:50
@RyanPCo
RyanPCo force-pushed the ryanco/opscale-ladder branch from 05f1e3e to bbbf371 Compare October 12, 2026 23:19
@RyanPCo
RyanPCo force-pushed the ryanco/ckpt-save-last-link branch from 286b8e8 to 1215a37 Compare October 12, 2026 23:19
@RyanPCo RyanPCo mentioned this pull request Oct 19, 2026
@RyanPCo
RyanPCo force-pushed the ryanco/opscale-ladder branch from bbbf371 to 8ac8f46 Compare October 19, 2026 19:47
@RyanPCo
RyanPCo force-pushed the ryanco/ckpt-save-last-link branch 2 times, most recently from feea4f0 to 7f6c8c3 Compare October 19, 2026 20:22
@RyanPCo
RyanPCo force-pushed the ryanco/ckpt-save-last-link branch from 7f6c8c3 to e38fa26 Compare October 30, 2026 18:27

@RyanPCo RyanPCo left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Automated adversarial review (read-only; findings were checked by a second skeptical pass before posting): 1 nit.

Comment thread egomimic/trainHydra.py Outdated
"restore the target or relink last.ckpt to an existing checkpoint."
)
ckpt_dir = os.path.dirname(requeue)
existing = [

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

nit (simplification): existing = glob.glob(os.path.join(ckpt_dir, "*.ckpt")) replaces the listdir/isdir/endswith comprehension and already returns [] for a missing directory.

ModelCheckpoint with `save_last: true` calls _save_checkpoint a second time for
last.ckpt, so every checkpoint event writes the same tensors twice -- ~21 GB a
time on Pi 0.5 (the size tests/integration/test_train_step_gpu.py records). It
was measured at 9 GB and ~1.3 min on the now-retired ABC-DiT-L, inside an event
that blocked the run for 2.7-3.4 min of a 42.3-min 1000-step cycle. "link" writes a relative symlink to the epoch
checkpoint just saved instead.

Safe here because save_top_k is -1, so the target is never collected, and
_should_remove_checkpoint returns False when previous == current, which is
always the case for last.ckpt. Resume is unaffected: _requeue_resume_path gates
on os.path.isfile, which follows symlinks.

A link can dangle where a file could not: the target deleted by hand, last.ckpt
dropped by an rsync without -l, or a preemption between Lightning's unlink and
relink. isfile is False in all three, and the requeue path used to warn and
silently restart the run from scratch. It now raises when last.ckpt dangles or
is missing next to other checkpoints; only an empty checkpoint dir falls back.

One behaviour difference worth knowing: with "link" the callback relinks on
EVERY train epoch once any checkpoint exists, not only on checkpoint epochs, so
there is an extra os.remove + os.symlink + strategy.barrier per epoch.

The test drives a real two-epoch fit and asserts both halves: "link" leaves a
relative symlink to the newest epoch checkpoint, and `true` leaves a regular
file the same size as it -- the copy this removes.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01M5dTGNydAZbRP1EsXgbp4W
@RyanPCo
RyanPCo force-pushed the ryanco/opscale-ladder branch from 256238b to f8a8ff4 Compare October 30, 2026 23:05
@RyanPCo
RyanPCo force-pushed the ryanco/ckpt-save-last-link branch from e38fa26 to 4869a7f Compare October 30, 2026 23:05
@RyanPCo

RyanPCo commented Oct 30, 2026

Copy link
Copy Markdown
Contributor Author

Review findings: status after the fixup pass. The fixes are squashed into this PR's commits, and the whole stack was force-pushed.

Fixed

  • Hand-rolled .ckpt scan could use glob: Now glob.glob(os.path.join(glob.escape(ckpt_dir), '*.ckpt')). The escape stops brackets in run directory names from being read as a pattern.

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