perf(minimax-h3): shard the video VAE decode across the SP group - #469
leviking98z-rgb wants to merge 6 commits into
Conversation
1c8c4f0 to
d70d63d
Compare
UNIRL_H3_STAGE_TIMING=<path> appends per-rank JSONL rows splitting generate() into embed / noise / denoise / video-decode / audio-decode. Unset (default) the timer costs five perf_counter calls and writes nothing. Added to find where a MiniMax-H3 rollout actually spends its time before optimising anything.
The stage timer showed video decode is 73.7% of a sample (51.4s of 69.8s at 768x768x124/SP8) and that all 8 SP ranks decode the SAME video, because SP gathers at norm_out and DP_SCATTER's collect keeps only sp_rank 0. Spread the seven temporal _decode_clip calls over the SP group and all-gather them; the blend/trim/concat post-loop is byte-identical to the vendor _decode. Env-gated (UNIRL_H3_DECODE_SHARD=1) at this point -- promoted to a real config flag in a later commit.
The recipe's VideoPickScore scores only the middle frame, so an identical reward_mean cannot prove all 124 frames survived a decode refactor. The sha can, and it comes for free inside the same A/B run.
video_decode_shard_across_sp replaces the env gate; default false, and at sp_size == 1 the stock vae.decode(...) path is taken unchanged. Adds the fail-closed status exchange so a decode failure on one rank cannot strand its peers in the gather, plus a README Gotcha recording why sharding by SAMPLE does not work (SP all-to-alls force the group into lockstep). Measured on 8xH20, real Flow-GRPO rollout at 768x768x124/SP8: video decode 51.47s -> 8.22s (6.26x) step_time 3536.2s -> 2160.7s (-38.9%) reward_mean identical to the bit; peak memory unchanged
A ragged clip on one rank was caught locally AFTER the probe exchange, so only that rank raised while its peers stayed blocked in the payload all_gather -- an 1800s NCCL timeout in a real job, not an error. Derive the shape agreement from the gathered probes instead, so every rank raises together. Found by an injected-fault test on 8 ranks: with a wrong clip extent on rank 3, 1/8 raised before this commit and 8/8 raise after, with no hang in either fault mode (decoder exception, ragged extent).
d70d63d to
dfb66da
Compare
…decode The FSDP backend only calls initialize_sequence_parallel_state() when sp_size > 1, so at sp_size == 1 there is no VeOmni parallel state. Probing it there raises 'The product of parallel sizes should be equal to the world size.', which poisoned the DevicePool and killed the run before the first rollout -- i.e. enabling video_decode_shard_across_sp made an sp_size=1 job fail outright, even though the flag is supposed to be inert there. Guard the lookup and fall back to the stock decode. Found by running the recipe at sp_size=1 (world=8 dp=8 sp=1); no OOM, the job died on this instead.
150-rollout SP1/SP8 stability follow-upI ran the longer real Flow-GRPO check requested after the original one-rollout A/B. Both arms used the same fixed source and inputs, 8×H20 each, 192×192×124, 10 denoise steps, seed 42, four samples per prompt, two updates per batch, and
Validation checks:
W&B: SP1, SP8. Each cloud run has 150 unique rollout steps; the offline-to-online sync introduced no duplicates. Interpretation: this is a long-horizon liveness/stability check, including the SP1 fallback fixed in |
Summary
Under Ulysses SP the MiniMax-H3 video VAE decode runs
sp_sizetimes for one kept result.SP shards only the trainable transformer, and the SP hooks gather at
norm_out, so everyrank finishes the denoise loop holding identical full-sequence latents and then decodes
the whole video independently.
Dispatch.DP_SCATTER's collect(
unirl/distributed/group/dispatch.py) keeps onlysp_rank == 0, so the other ranks'decodes are discarded.
Stage timing on a real Flow-GRPO rollout (768×768×124,
sp_size=8,forward_batch_size=1)shows this dominates the step — not the 33B DiT:
All 8 ranks log that same 51.4s decode at the same wall clock — 1439s wasted per 32-sample
rollout, 41% of step time.
This PR spreads the decoder's independent temporal
_decode_clipcalls (7 at this geometry)over the SP group and all-gathers them. The blend/trim/concat post-loop is byte-for-byte the
vendor logic, so only the expensive clip forwards move. Opt-in via
bundle.config.video_decode_shard_across_sp(defaultfalse); atsp_size == 1the stockvae.decode(...)path is taken unchanged, andunirl/models/minimax_h3/vendor/is untouchedso a re-vendor stays a recopy.
Why not shard by sample (the more obvious fix): SP attention does an all-to-all in every
block, so the group must enter each denoise in lockstep. A rank that skipped its decode would
simply block in the next collective and save nothing.
Related Issue
N/A.
Test Plan
Hardware 8×H20; MiniMax-H3 33B;
diffusion/minimax_h3/minimax_h3_t2va_trainsidewith+backend.fsdp_cfg.sp_size=8,num_devices=8 batch_size=8 samples_per_prompt=4(32 samples),768×768×124, 10 denoise steps,
sde_indices=[0,3,6], real VideoPickScore+CLAP reward,num_updates_per_batch=2,+offload_train_during_reward=true, seed 42.End-to-end A/B, one rollout per arm, flag off vs on:
perf/step_time_sperf/generate_time_srollout/reward_meanBoth arms
exit_code=0,train/optimizer_updates=2, no NaN. Sanity: decode saved32 × (51.47 − 8.22) = 1384sagainst a measured step delta of1375.5s, so the stepimprovement is fully explained by the decode and nothing else silently changed.
Bit-exactness — real VAE weights, GPU, fp32, same process, stock
vae.decode(...)vs thesharded path, 3 seeds:
reward_meanalone is not a sufficient check here (VideoPickScore scores only the middleframe), so each arm also hashed every decoded video: the 32 SHA-256 values match exactly
across arms, and all 8 ranks agree within each arm.
Algorithm equivalence across world sizes (CPU, gloo): 3 geometries (37/22/12 latent
frames) × world 1/2/4/8 = 12 cases, all
max|Δ| == 0, including chunk counts that do notdivide evenly by the world size.
Fail-closed under injected faults (CPU, gloo, 8 ranks), fault on rank 3:
An earlier iteration failed the ragged case (1/8 raised, 7 blocked in the gather — an 1800s
NCCL timeout in a real job); the shape agreement is now derived from the gathered probes so
every rank raises together.
Not run:
pre-commit/repo lint (no local environment for it on the cluster where this wasmeasured); multi-node SP.
Compatibility / Risk
video_decode_shard_across_sp, defaultfalse— existing recipes andcheckpoints unaffected, decode byte-identical when off.
sp_size == 1, which is every configuration onmaintoday.video per sample over NVLink.
17n+5frame counts at768×768; a geometry producing ragged clips raises a clear collective error on every rank
rather than hanging.
Reviewer Notes
developed against a fork branch that adds native Ulysses SP for H3. If that is not a
direction you want, this PR is moot and I'll close it — that is the main thing I'd like
feedback on before polishing further.
_decode_clip,_blendand the tiling attributes are read from the vendored autoencoder.They are private-by-name; a re-vendor that renames them would need this updated. Tiling
itself cannot be disabled — the released frames are the blended-tile ones — which is why the
geometry is reproduced rather than bypassed.
decoder calls per sample); sharding those too adds ~0.8pp by projection and was left out as
not worth the extra stitching complexity.
than 7×. Longer clips divide better.
unirl/models/wan21/wan_video_vae.pyhas a similarly-shapedtiled_parallel_decode, but itis unreachable (
sp_group=Nonedefault, no caller) and shards spatial tiles of a differentVAE; nothing existing covers H3.
Checklist