Phase 3: real FSDP, jit+NamedSharding, measurement, mid-epoch resume - #15
Closed
AshishKumar4 wants to merge 5 commits into
Closed
Phase 3: real FSDP, jit+NamedSharding, measurement, mid-epoch resume#15AshishKumar4 wants to merge 5 commits into
AshishKumar4 wants to merge 5 commits into
Conversation
The training step ran under shard_map with in_specs=(P(), P(), P('data'),
P('data')) - arg 0 fully replicated, so parameters and optimizer state sat on
every device. That was pure DDP; there was no FSDP to speak of, and model size
was capped by one device's memory.
Replace it with jax.jit + NamedSharding over a two-axis ('data','fsdp') mesh
and let GSPMD derive the collectives:
- Parameter layout comes from a shape-based heuristic applied to every leaf of
the abstract state, so optimizer moments and the EMA copy inherit their
params' spec through tx.init. No model file declares partitioning.
- The hand-written pmean over gradients is gone. The loss is already a mean
over the batch-sharded axis, so its gradient carries the all-reduce.
- The per-device fold_in RNG hack is gone with the device-index argument;
threefry is partitionable, so one key per step is correct.
- donate_argnums now donates the train state instead of the batch, which
required best_state to stop aliasing a live state - it is a host-side numpy
tree now, which is what get_best_state() already handed out.
Batches are assembled with jax.make_array_from_process_local_data instead of
the manual split/device_put in form_global_array, and the host-to-device
transfer moved into a two-deep prefetch thread so it overlaps compute. The
dead DataLoaderWithMesh prefetch path is deleted rather than left as a second
way to do the same thing.
Checkpoints now save and restore sharded arrays in place via orbax
restore_args, instead of gathering the whole state onto the host with
get_np_tree and then immediately blocking on wait_until_finished. The on-disk
layout is unchanged, so existing pretrained checkpoints and the inference
loader keep working.
Numerical parity is the gate: single-device, 8-device and 2-way-FSDP runs are
asserted to agree over 20 steps.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Activation memory is what caps trainable model size, so make the DiT-family blocks rematerializable. ModulatedBlock and MMDiTBlock are wrapped through one helper, with dots_with_no_batch_dims_saveable so the big matmuls' outputs survive and the recompute stays cheap. Two things the tests forced out: - `train` picks a Python branch inside the blocks, so remat traced it and blew up on a bool conversion. It has to be static, which means the block call sites pass their arguments positionally now. - The S5 mixer carries complex64 residuals, and saving a residual goes through jax.lax.reduce_precision, which rejects complex dtypes. Those blocks get policy=None (recompute everything) rather than a silently degraded policy. Also adds --grad_accum_steps, which wraps the optimizer in optax.MultiSteps, and --fsdp_size / --fsdp_min_param_size / --profile_steps / --compilation_cache_dir / --log_every to reach the trainer's new knobs. Remat is asserted to leave the parameter tree, forward values and gradients unchanged, so turning it on cannot invalidate a checkpoint. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The performance work is only evaluable if the instrumentation is trustworthy, so it gets tested like the training maths: FLOPs come back positive from the compiled step, samples/sec and step_time_ms are internally consistent, MFU scales the right way with time and device count and is skipped rather than guessed at on hardware with no published peak, the profiler actually writes a trace, and the compilation cache directory is really configured. Divergence detection is covered both ways: a run whose loss is always NaN stops with the consecutive-step count in the message, and a healthy run runs to completion without tripping it. That check now accumulates on device and is read at the logging cadence, so it costs no per-step host sync. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…igration orphaned Resuming rebuilt the template from self.dataset_state, which is always None on a fresh trainer, so the iterator position was written to the checkpoint and then never read back - every resume still restarted the epoch. The template is built from the checkpoint's own structure now, which also keeps checkpoints written before iterator tracking (and those from iterators that cannot report a position) restorable. Grain reports its position as JSON bytes and tensorstore has no dtype for that, so the bytes ride along as a uint8 array and are decoded on restore. Saving is async now, so tests that read a checkpoint back in the same process have to pass through wait_for_checkpoints(); one of them was racing. Also removes what the shard_map migration orphaned: shutil in simple_trainer (its only user was the deleted move_contents_to_subdir), the unused distributed_training local in the train step, the device-count locals in both validation loops, and jnp in dataloaders. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Owner
Author
|
Landed on |
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.
Sixth PR of the revamp series — stacked on #14. Merges cleanly onto it (verified).
FSDP is real now
Before this PR the mesh was a single
'data'axis and the train step passedP()for the train state — parameters, EMA and both Adam moments were replicated on every device. I verified that empirically before and after; same inspection, opposite result:fsdp_sizePartitionSpec()PartitionSpec('fsdp',)The mesh is now
('data','fsdp'), and the parameter spec tree is derived once ingenerate_statesfromjax.eval_shapeplus a size heuristic (--fsdp_min_param_size); optimizer state and EMA inherit the param specs throughtx.init. Zero model-file changes — exactly as designed.fsdp_size=1reproduces the old replicated behavior.shard_map is gone
grep -rn shard_map flaxdiff/returns 0. The train step isjax.jitwithin_shardings/out_shardingsand donation; GSPMD derives the collectives, so the hand-writtenpmean, thelocal_device_indexargument and thefold_indevice-index RNG hack are all deleted (jax_threefry_partitionablealready defaults True on jax 0.9).Two parity gates guard the migration, both runnable on CPU with simulated devices: single-device vs multi-device loss trajectories agree, and FSDP vs replicated agree.
Measurement, which did not exist at all before
train/step_time_ms,train/samples_per_secand an MFU estimate now go to wandb;--profile_stepswrites ajax.profilertrace;--compilation_cache_direnables the persistent XLA cache (the biggest single win for restart-heavy TPU workflows).Async dispatch unblocked
The old loop did
if not jnp.isfinite(loss)every step — a Python branch on a device array, forcing a host sync that defeated JAX's async dispatch.is_finiteis now an output of the jitted step and the consecutive-bad-step counter stays on device (jnp.where), checked on host only at the logging cadence. Batch H2D moved into a prefetch iterator so transfer overlaps compute, anddonate_argnumsnow donates the state (the large buffer) rather than the batch, which required breaking thebest_statealiasing first.Checkpointing caught up with the sharding
Sharded orbax save/restore with
restore_argsbuilt from the target sharding tree — replacing a full host gather (np.arrayon every leaf) that immediately calledwait_until_finished(), defeating its own AsyncCheckpointer. Restore onto a different mesh is tested. Grain iterator state is checkpointed too, so resume is finally mid-epoch-correct — previously the data iterator restarted from the top of the epoch on every resume.Capacity
--rematrematerializes transformer blocks (dots_with_no_batch_dims_saveable) — this sets your maximum trainable model size.--grad_accum_stepsaddsoptax.MultiSteps, tested to hold params still until the accumulation boundary and to survive the sharding heuristic.Verification
113 passed (from 73), re-run by me independently.
tests/test_parallelism.pyadds 19 tests covering the sharding heuristic, mesh construction, prefetch ordering/error propagation, both parity gates, FSDP sharding proof, checkpoint roundtrip, cross-mesh restore, gradient accumulation and mid-epoch resume.Not done: the unified precision policy (deliberately scoped last and left out; the ad-hoc
dtype=threading remains).🤖 Generated with Claude Code