fix(checkpoint): make orbax-restored TrainState donatable — solve the resume OOM (S22)#932
Open
ocg-goodfire wants to merge 3 commits into
Open
fix(checkpoint): make orbax-restored TrainState donatable — solve the resume OOM (S22)#932ocg-goodfire wants to merge 3 commits into
ocg-goodfire wants to merge 3 commits into
Conversation
…first resumed step can donate (S22) Orbax-restored arrays (per-shard device_put + make_array_from_single_device_arrays) are not reliably donatable to the jitted train step (jax#18617, never fixed upstream); dispatch-time donation failure is SILENT (the 'donated buffers were not usable' warning is lowering-time only), so the first resumed step peaks ~one full TrainState above any fresh step -- the full32L resume OOM (lore 2026-07-03--resume-oom-is-buffer-donation-asymmetry). restore_step now passes the restored tree through a non-donating identity jit pinned to the reference's format: its outputs are indistinguishable from fresh-init state, at a transient 2x-state cost restore already paid. Also adds a bytes-weighted buffer-reuse guard after the first resumed step (run.py) so any future donation failure is loud, and pins the donatability contract in test_checkpoint.py (repro: the raw restored tree flakily fails pointer-aliasing on the CPU backend; the re-materialised tree never does). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Broken at HEAD since the residual-fed -> token-input contract change (same fallout class as #919): clean_output(batch) hits embed_tokens with a float32 indexer -> TypeError. Validation-stack item 3 was unrunnable. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
pile_pgd1 shrunk to 8 GPUs / evals+wandb off; header documents the scancel-after-save -> pd-lm --run_id resubmit procedure. Used to validate the donation fix end-to-end (SIGTERM save at step 70 -> resume -> silent guard, losses continue, same per-rank peak). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
Three commits:
restore_stepre-materialises the restored tree as jit outputs (non-donating identity jit pinned toreference.formatviaout_shardings), replacing thedevice_put(format)coercion. Orbax-restored arrays (per-sharddevice_put+make_array_from_single_device_arrays) are not reliably donatable to the jitted train step (jax#18617, never fixed upstream). When dispatch-time donation fails it is silent (theSome donated buffers were not usablewarning is lowering-time only —jax/_src/interpreters/mlir.py), and the first resumed step peaks ~one fullTrainStateabove any fresh step: the full32L resume OOM (lore2026-07-03--resume-oom-is-buffer-donation-asymmetry). Jit outputs are indistinguishable from fresh-init state; the transient 2×-state cost equals the peak restore already paid whilereferenceand the restored tree coexisted. Non-donating deliberately: a plain copy cannot hard-fail at dispatch (GPU errors loudly on donation of externally-referenced buffers), and the reuse donation would buy is off the step's peak. Covers the fine-tune path too (init_from_parent→restore_step).Plus a bytes-weighted buffer-reuse guard after the first resumed step (
run.py): if <95% of state bytes were reused, each affected rank printsDONATION FAILED …— the silent failure mode is silent no more.invariance_check.pyfed float residuals where the target now takes token ids (same fallout class as fix(hidden-acts-eval): derive waist leading from CI output, not token inputs (S31) #919) — validation-stack item 3 was unrunnable. Now feedsrandinttokens.dp8 save→resume smoke config documenting the scancel-after-save →
pd-lm --run_idresubmit procedure.Related Issue
Bridge task
solve-resumption-oom; upstream jax#18617.Motivation and Context
Large-state donating runs can train fresh but die on any preemption/requeue: full32L resumes OOM'd at the default mem fraction, and for
p-8e2380e2(attn C=4096) the mem-fraction band-aid is unusable in both directions (0.97 starves NCCL clique init; even max fraction cannot fit fresh-peak + one state). The restored state must donate.How Has This Been Tested?
repro/repro_donation.py): orbax StandardSave/Restore + donating jitted step, donation observed directly via per-shardunsafe_buffer_pointeraliasing +memory_stats. On CPU, raw restored state flakily fails donation on a random subset of leaves — exclusively the sharded ones; the identity-jit re-materialisation fixes it deterministically. (GPU 1-dev and 8-proc×1-GPU multi-controller runs did not reproduce the raw failure at small scale — 48/48 rank×mode combos aliased — so CPU + the at-scale diag logs are the only places the failure is visible; the fix makes the restored arrays constructively equivalent to fresh-init ones rather than relying on that.)test_checkpoint.py::test_restored_state_is_donatablepins the contract (pointer aliasing through a donating jit); ran 3× stable at 4 sim devices.param_decomp/tests/green at default device count AND--xla_force_host_platform_device_count=4;make typeclean;invariance_check.pypasses at 4 sim devices (worst rel 1.02e-04).pd-lm --run_idresubmit →resumed from checkpoint step 70, first resumed step trains, no DONATION FAILED line from any rank, losses continue the trajectory, per-rank peak identical (18.22 GB). Run + workspaces deleted after.p-05101e15was GC'd;p-8e2380e2is live). The guard turns any residual at-scale failure loud on the next real resume.Does this PR introduce a breaking change?
No.
restore_stepnow requires the reference on a single device set (one jit over the tree) — production references already are (_ensure_global); the sharded test fixture was aligned to that contract.🤖 Generated with Claude Code