Skip to content

feat(checkpoint): split checkpoints into decomposition | training items (S22/S33)#950

Open
ocg-goodfire wants to merge 5 commits into
feature/jaxfrom
bridge/task-checkpoint-split-decomposition-item
Open

feat(checkpoint): split checkpoints into decomposition | training items (S22/S33)#950
ocg-goodfire wants to merge 5 commits into
feature/jaxfrom
bridge/task-checkpoint-split-decomposition-item

Conversation

@ocg-goodfire

@ocg-goodfire ocg-goodfire commented Jul 6, 2026

Copy link
Copy Markdown
Collaborator

What

Checkpoints store two orbax items per step, and TrainState COMPOSES exactly those two — one representation shared by the trainer and the checkpoint:

  • decompositiontrain.Decomposition (V/U components + ci_fn): the trained product.
  • trainingtrain.TrainingItem (both optimizer states, persistent adversaries, step): the process.

TrainState = {decomposition, training}, so save_state/restore_step map straight onto its own fields (no _split/_join regrouping). Every consumer — open_jax_run (harvest/autointerp/clustering/app) and fine-tune init (S33) — restores only decomposition, against an abstract tree from jax.eval_shape(run_state.init_decomposition): zero knowledge of how training builds its optimizers or adversaries.

Why

The 2026-07-06 harvest wedge: open_jax_run rebuilt + restored the FULL TrainState, so on one consumer GPU an 8B run's PPGD bsc sources + Adam moments (~60GB, sharded ÷64 during training) materialized whole, twice → OOM/allocator-wedge. The interim fix (1ca895860, only on local harvest-tonight — superseded here) partial-restored {components, ci_fn} but still needed jax.eval_shape(init_train_state) including build_optimizers just to recover the saved tree's structure. Consumers knowing training internals was the design smell; the item split removes it at the storage boundary.

Compatibility — clean break, no in-code fallback

Clean checkpoint-format break. There is no dual-read of the pre-split single-default-item layout — every restore path reads the two-item layout only. Runs checkpointed before this change must be converted by an ad-hoc migration script; the compat logic does not live in the codebase. Both items save sharded — the no-on-loop-full-gather write property is unchanged.

SPEC

S22 + S33 amended (marked pending Oli sign-off in the text): the two-item layout, the TrainState-composes-both invariant, the consumer contract, and the clean-break/no-fallback decision are normative.

Validation

  • Suite green at default device count and XLA_FLAGS=--xla_force_host_platform_device_count=4: test_checkpoint, test_checkpoint_production_topology, test_generic_model_io, test_no_bake_invariant, test_llama8b, test_llama_simple_mlp, test_finetune_resume, stacked_parity, and the lab tms/resid_mlp toy suites. basedpyright + ruff clean on the change. (The one failure, test_pretrained_target_converts_with_wildcards, is the pre-existing tip failure, unrelated.)
  • Tests: two-item on-disk layout pin (cross-version contract), consumer host-restore vs saved state (bit-equal), two-item init_from_parent (S33: decomposition carries over; optimizer state / adversaries / step=0 stay the fresh reference's), and test_roundtrip_and_exact_resume still bit-exact through the composed state.
  • New-format e2e smoke (run on an earlier rev, persistent bsc adversary — the exact subtree that motivated the split): job 171284 trained 40 steps on 8 GPUs writing <step>/{decomposition,training}; job 171288 resumed from the two-item step-40 checkpoint; open_jax_run(p-f653e1fd) opened it in 6s on CPU and ran a clean harvest forward. Manual smoke config not committed.

Notes for review

  • restore_step keeps the format-coercion tail (device_put onto reference.format); fix(checkpoint): make orbax-restored TrainState donatable — solve the resume OOM (S22) #932 rewrites that same tail — whichever lands second has a small conflict.
  • orbax gotcha in the tests: a CheckpointManager pins an item's handler at first use, so the saving manager can't PyTreeRestore what it StandardSaved — consumers open their own manager.
  • Consequence of the clean break: pre-split runs (e.g. p-594db290) can't be opened by tip until migrated. The motivating harvest is separately blocked on consumer-config-schema-drift, so no live harvest regresses on this PR alone.

bridge thread: checkpoint-split-decomposition-item

Crew-Address: task/checkpoint-split-decomposition-item

ocg-goodfire and others added 3 commits July 6, 2026 10:01
…ms (S22/S33 amended)

Each step now stores two orbax items: decomposition (V/U components + ci_fn — the
trained product) and training (both optimizer states, persistent adversaries, step —
the process). Consumers (open_jax_run, fine-tune init S33) restore ONLY decomposition
via restore_decomposition[_to_host] against an eval_shape'd run_state.init_decomposition
reference — zero knowledge of training's optimizer/adversary structure, and none of the
~60GB opt-moment/bsc-source materialization that wedged single-GPU harvest restores
(supersedes the interim partial-restore 1ca895860, which still needed
eval_shape(init_train_state) incl. build_optimizers).

Runs saved before the split (single default item) keep writing that layout from their
immutable workspaces; every restore path dual-reads both layouts (_stored_as_split,
grounded in the stored dir layout: <step>/default vs <step>/decomposition). Both items
save sharded — the no-on-loop-full-gather property is unchanged. SPEC S22/S33 amended
(pending Oli sign-off). New tests: two-item layout pin, consumer host-restore, legacy
dual-read across restore_latest / restore_decomposition_to_host / init_from_parent.
Validated vs real stored data: p-594db290 (8B L18, 60GB-class ckpt) opens on CPU in 22s.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EwzrwmBiMpkvDovpGgVysn
The pile_ppgd_bsc_RESUMESMOKE_dp8.yaml was a manual e2e testing
artefact used to validate the two-item checkpoint format on 8 GPUs,
not code intended to be committed. Removing per review.

Crew-Address: task/checkpoint-split-decomposition-item
… break)

Per review: no in-code compat for pre-split `default`-item checkpoints.
Pre-split runs are migrated by an ad-hoc script when needed, rather than
bloating every restore path with a fallback.

- Remove `_stored_as_split` layout dispatch and `_legacy_partial_restore`.
- Collapse restore_step / restore_decomposition / restore_decomposition_to_host
  to the two-item (`decomposition`+`training`) path only.
- SPEC S22/S33, CLAUDE.md: state the clean break; drop dual-read wording.
- Replace the legacy dual-read test with a two-item init_from_parent test
  (keeps S33 fine-tune-init coverage: decomposition carries over, opt
  state/adversaries/step stay the fresh reference's).

Suite green at 1 and 4 simulated devices (param_decomp/tests/test_checkpoint.py, 7 passed).

Crew-Address: task/checkpoint-split-decomposition-item
Comment thread param_decomp/checkpoint.py Outdated
Comment on lines +58 to +78
def _split(state: TrainState) -> tuple[Decomposition, TrainingItem]:
return (
Decomposition(components=state.components, ci_fn=state.ci_fn),
TrainingItem(
components_opt_state=state.components_opt_state,
ci_fn_opt_state=state.ci_fn_opt_state,
adversaries=state.adversaries,
step=state.step,
),
)


def _join(decomposition: Decomposition, training: TrainingItem) -> TrainState:
return TrainState(
components=decomposition.components,
ci_fn=decomposition.ci_fn,
components_opt_state=training.components_opt_state,
ci_fn_opt_state=training.ci_fn_opt_state,
adversaries=training.adversaries,
step=training.step,
)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

are these actaully necessary? they smell

PD User (shared) and others added 2 commits July 8, 2026 03:59
…representation

Per review: rather than a flat TrainState that the checkpoint code regroups
into two items (the _split/_join helpers), TrainState now IS the composition of
the two checkpoint items:

    TrainState{decomposition: Decomposition, training: TrainingItem}

so save/restore map straight onto its own fields — _split/_join deleted, no
regrouping anywhere. TrainingItem moved from checkpoint.py to train.py (it's now
a TrainState field). SPEC S22 records the one-representation invariant.

Churn: every state.<field> access becomes state.decomposition.<field> /
state.training.<field> — the hot train step, run.py resume/warmup, run_state
init, slow_eval hidden-acts, the lab run/eval paths, and all constructing tests.
Pure regrouping, no numeric change.

Validated (1 + 4 sim devices): test_checkpoint, test_checkpoint_production_topology,
test_generic_model_io, test_no_bake_invariant, test_llama8b, test_llama_simple_mlp,
test_finetune_resume, stacked_parity, tms, resid_mlp — all green (the only failure,
test_pretrained_target_converts_with_wildcards, is the pre-existing tip failure).
basedpyright clean; ruff clean.

Crew-Address: task/checkpoint-split-decomposition-item
…position item

The pre-split-run migration the S22 clean format break requires, doubling as
the storage lever: a pre-split checkpoint is ~90% resume-only state (persistent
adversary + all optimizer moments); the tool partial-restores components+ci_fn
as host numpy and rewrites ckpts/<step> as the two-item layout with ONLY the
decomposition item, staged + tree-verified before an in-place swap. Dry-run by
default, --apply to strip. The run can no longer warm-resume — for finished or
abandoned runs only.

Validated on a scratch copy of p-e118ece9/ckpts/355000 (19.2 -> 2.1 GiB, 7s):
restore_decomposition_to_host over the stripped checkpoint is byte-equal to the
original across all 163 decomposition leaves. Handles both current (adversaries)
and old jax_runs-era (sources/sources_opt_state) key layouts; unknown keys fail.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RkKMDdM1PxaL66RmEJv8QN
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