Write post-training checkpoints in MaxText's on-disk layout - #4700
Draft
ecnal-cienet wants to merge 7 commits into
Draft
Write post-training checkpoints in MaxText's on-disk layout#4700ecnal-cienet wants to merge 7 commits into
ecnal-cienet wants to merge 7 commits into
Conversation
|
Caution The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased. |
ecnal-cienet
force-pushed
the
feat/post-train-checkpoint-maxtext-layout
branch
from
August 3, 2026 02:39
7890810 to
35dd42f
Compare
ecnal-cienet
force-pushed
the
feat/post-train-checkpoint-maxtext-layout
branch
7 times, most recently
from
August 10, 2026 17:19
f8539d7 to
dd33305
Compare
ecnal-cienet
force-pushed
the
feat/post-train-checkpoint-maxtext-layout
branch
from
August 11, 2026 23:30
1377f84 to
f6454c7
Compare
mesakhcienet
marked this pull request as ready for review
August 17, 2026 03:36
mesakhcienet
requested review from
A9isha,
RissyRan,
SurbhiJainUSC,
aireenmei,
bvandermoon,
darisoy,
gagika,
gobbleturk,
huytransformer,
igorts-git,
jacoguzo,
jiangjy1982,
khatwanimohit,
parambole,
richjames0,
shralex,
vipannalla and
xibinliu
as code owners
August 17, 2026 03:36
mesakhcienet
requested review from
Lumosis,
NuojCheng,
abhinavclemson,
dipannita08,
hengtaoguo,
jrplatin,
jshin1394,
liudangyi,
michelle-yooh,
patemotter and
zxhe-sean
as code owners
August 17, 2026 03:36
mesakhcienet
marked this pull request as draft
August 17, 2026 03:36
ecnal-cienet
force-pushed
the
feat/post-train-checkpoint-maxtext-layout
branch
7 times, most recently
from
August 19, 2026 01:13
e445a06 to
5d20be1
Compare
mesakhcienet
force-pushed
the
feat/post-train-checkpoint-maxtext-layout
branch
3 times, most recently
from
August 19, 2026 11:14
d793fb3 to
e041558
Compare
Tunix writes its own checkpoint shape -- model_params/ and optimizer_state/ beside a custom_metadata -- while pre-training expects items/ holding params under the Linen params collection, opt_state, step and nnx_aux. A model trained by SFT, DPO, RL or distillation therefore could not be handed back to pre-training without converting it by hand. MaxTextLayoutCheckpointManager subclasses Tunix's manager and reshapes the tree on the way out: it restores the Linen collection level, strips the inject_hyperparams shell that optax wraps a scheduled optimizer in -- otherwise mu and nu land a level short of where pre-training looks for them -- and drops the adapter level a LoRA wrapper adds. install() swaps it in for the manager the base class built, closing that one so it does not leak its writer thread. Gradient clipping moves out of optax.chain for the same reason: chaining a stateless clip in front of the optimizer nests its state under an extra level that pre-training cannot read. RL keeps its actor/ level, since GRPO checkpoints an actor and a reference separately. The end-to-end scripts, the demo notebooks and the eval README follow the path from model_params to items. post_train_skip_checkpointing exists for runs that only want to load base weights and write nothing back.
Post-training reached for transformers' AutoTokenizer directly, so a run had to name a HuggingFace repo even when the model ships a sentencepiece or tiktoken asset in this repo, and gated repos needed a token for something the checkpoint already carried. The tokenizers built by build_tokenizer now serve post-training too. The two that are ours gain a chat template renderer, since RL applies one and only HuggingFace tokenizers carried that method. HFTokenizer forwards apply_chat_template explicitly rather than through __getattr__, so what it offers is visible rather than whatever transformers happens to expose.
Three call-site assumptions no longer hold against current vLLM and tpu_inference, and each one stops RL before a single rollout. EngineArgs dropped swap_space, and passing a field it no longer takes is a TypeError rather than something vLLM ignores, so the rollout asks the installed EngineArgs whether the field exists. tpu_inference mutates data_parallel_size and then deletes the sharding config before calling with_hf_config, leaving the rebuilt config to fail its own device-count assertion, so the patch recovers the size from device_indexes. is_init_field raises on the fields tpu_inference injects dynamically, which the patch treats as "not an init field" instead of letting it abort.
Tunix stops on max_steps only when it owns the checkpoint manager, and installing ours sets is_managed_externally, so distillation ran past the configured steps until the input pipeline ran dry. A run asked for five steps and kept going. The iterator now carries a batch budget of steps times gradient accumulation, minus whatever a resumed run already consumed, and raises StopIteration once spent. That bounds the run whoever owns the checkpoint manager.
llama3-8b appeared in HF_IDS but in none of the tables that actually convert weights, so it looked supported and failed on a missing key. Its MaxText config is byte-identical to llama3.1-8b, so it reuses that family's mapping, shapes and hooks; only the source repo differs. The note beside it records why mistral-7b is not registered the same way: wiring it to the Llama family converts cleanly, 291 of 291 arrays with every name and shape matching, and produces numerically wrong weights -- KL 1.32e-01 mean against the 3e-3 the repo's own conversion tests use. It needs its own mapping, not a borrowed one. The logit checker lends eos as a pad token for the tokenizers that ship without one, which those two do. Prompts are tokenized one at a time there, so nothing is padded and no result changes; it only satisfies the check that refuses the call.
Four harnesses, none of them wired into CI, all driven by hand. run_e2e_matrix.py trains a base checkpoint with every trainer and then reloads it from every other one, so a checkpoint written by SFT is proven readable by pre-training and the rest. run_vllm_matrix.py takes those checkpoints through vLLM decode. run_hf_convert_matrix.py rebuilds the converted base checkpoints the other two consume, which nothing else regenerates, and encodes which loader each model needs -- gemma3 has to be read through transformers because the param map follows the names transformers gives multimodal weights, and reading it lazily fails on a missing key that looks like a corrupt checkpoint. The XPK pair runs the same matrices on a cluster, for the models that do not fit one host. They check the tree that was written rather than the exit code, since a conversion or a training run can exit zero and leave nothing usable behind.
Trainers restore the old layout already, so a run resumed from one rewrites itself on its next save. Nothing does that for the checkpoints nobody is going to resume, and pre-training cannot read them: it looks for weights in the Linen params collection, and the old layout keeps the whole model state under model_params instead. The split between params and nnx_aux is why this needs a model rather than a rename. Weights and the rng counters that drive dropout sit mixed together under model_params, and only the model says which is which. An abstract model supplies the classification, and the arrays move through as numpy, so the conversion runs on CPU without materialising the model. It refuses a model that does not match the checkpoint, by name and by shape. Both mistakes are easy to make and neither announces itself: splitting by the wrong model routes the leaves it does not recognise into nnx_aux, and a config that differs only in size has every name right and fails much later, in the middle of a read.
ecnal-cienet
force-pushed
the
feat/post-train-checkpoint-maxtext-layout
branch
from
August 19, 2026 23:39
2563efb to
3df853b
Compare
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
Post-training saved through Tunix's checkpoint manager, which stores
nnx.state(model)verbatim under amodel_paramsitem. MaxText's on-disk layout is the Linen one: weights inparams/params, the optimizer inopt_stateandstep, and NNX-only state such as rngs innnx_aux. The two never matched, so a checkpoint from an SFT, DPO, RL or distillation run could not be loaded by pre-training, and loaders had started growing branches to read the post-training shape instead.This adds
MaxTextLayoutCheckpointManager, which converts in both directions and writes the sameitemstree everything else in MaxText reads. It lives undertrainers/post_trainrather thancommon/checkpointingbecause it subclasses Tunix's manager, andcommon/checkpointingis imported by pre-training and inference, which run without Tunix installed. Older checkpoints are still in the Tunix layout, somaybe_restorefalls back to the base class for those, andload_params_from_pathlearns to read them by restoring into the NNX state itself.The adapter level
DPO and RL train through
TunixMaxTextAdapter, whosebaselevel would otherwise reach the checkpoint. It is stripped from the weights and from the optimizer accumulators that mirror them, and put back on restore.Optimizer differences that blocked a full-state resume
train_dpopassedgradient_accumulation_stepsunconditionally, so Tunix wrapped the optimizer inoptax.MultiStepseven at 1. It now passesNonebelow 2, astrain_sftalready did.optax.clip_by_global_norminto the optimizer, nesting its state a level deeper than pre-training, which clips raw gradients in its train step.add_gradient_clippingapplies the same math inside the update and keeps the optimizer's own state tree.A DPO checkpoint now resumes into pre-training with its weights, its optimizer state and its step counter.
Checkpoint metadata
A checkpoint also records what the run that wrote it was configured as:
scan_layers, and the LoRA settings when there are any.verify_and_sync_scan_layersandlora_utils.sync_lora_metadataread it back to fill in a value the run left at its default, or to reject one that contradicts the checkpoint. Tunix passed none, so post-training checkpoints arrived with an empty dict and got neither behaviour. The manager now takes the run's config and records the same metadata pre-training does, through one shared builder (checkpoint_custom_metadata) so the two cannot drift apart. All four trainers pass their config, distillation the student's.Reading it back was broken too, and not only for post-training. The metadata belongs to the step, so it sits at
<step>/and not at<step>/items/, while every caller passesload_parameters_path, which by convention points at the item. Both readers have been getting an empty dict from pre-training checkpoints as well and silently doing nothing with it.load_checkpoint_metadatanow falls back to the parent directory, so both spellings work.Demo notebooks
The RL and SFT demo notebooks read the checkpoint back to convert it to HuggingFace format, so the paths they hardcode are updated. Both now point at the
itemsdirectory rather thanmodel_params. RL also drops theactorlevel: Tunix appended it only when it ownedcheckpoint_root_directory, which is nowNone, so the actor trainer writes straight tocheckpoint_diras SFT and DPO do.Tests
New unit tests, all runnable on CPU:
tests/post_training/unit/post_train_checkpointing_test.py— the bulk of the coverage. The saved tree is the MaxText layout and not the Tunix one; the adapter level is stripped from both weights and optimizer accumulators; weights and optimizer restore to what was saved; an absent checkpoint reports step 0; config-derived metadata is stamped the way pre-training does it, with a caller-supplied key winning over it; a missing config and a missing optimizer both still save; scanned and unscanned models each round-trip and are shown to differ on disk; a checkpoint in the old Tunix layout still restores through the base-class fallback; andinstallreplaces the manager, forwards the run config, closes the one it replaced, and restores the step.tests/post_training/unit/distillation_checkpointing_test.py— the student saves in MaxText layout and restores, itsscan_layersis recorded, andlearn_to_initmode leaves the optimizer out.tests/unit/checkpointing_test.py—load_checkpoint_metadatafalls back to the step directory and stops at the path it was given;checkpoint_custom_metadatarecordsscan_layerseither way, records LoRA only once there is a rank, and records nothing without a config.tests/unit/optimizers_test.py—add_gradient_clippingproduces the same updates as the chained form, clips gradients over the threshold, keeps the unclipped optimizer's state tree, forwards extra args to the inner optimizer, and composes with skip-step-on-spikes.tests/post_training/unit/train_{sft,dpo,rl,distill}_test.py— each trainer disables Tunix's own manager and installs this one with its config.Run the post-training suite:
and the pre-training side:
The
cpu-post-training-unitandcpu-unitCI jobs cover both. The notebook job exercises the changed RL and SFT demo paths end to end, including the checkpoint-to-HuggingFace conversion that reads the new layout back.Checklist
Before submitting this PR, please make sure (put X in square brackets):
gemini-reviewlabel.Checklist
Before submitting this PR, please make sure (put X in square brackets):
gemini-reviewlabel.