With enable_torch_compile_vae (#1734, opt-in) inductor autotunes kernels per
process, so chunk decodes on other ranks differ from the serial rank's decode
the way two serial processes differ (GB200 @124f: max 63/255 on <0.5% of
pixels, mean ~1e-2/255; audio and chunk 0 bit-identical). Eager decoder (the
default) stays bitwise-equal to serial decode_to_pixels - measured, both
strategies, x3, 124f+345f.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two schedule fixes sized from the first GB200 tray measurements (job 2659,
serial 7.7s/21.2s at 124f/345f):
1. Every rank now decodes its round-0 chunk BEFORE the metadata broadcast.
Non-leader ranks previously blocked on the broadcast until the leader
finished chunk 0, serializing a full extra chunk-decode into round 0
(visible as 1.9x instead of ~2.6x at 7 chunks / 4 ranks). Same reorder
on the encode path.
2. The leader's per-chunk joining work (blend, denormalize, clamp, output
copies) moves to a dedicated CUDA side stream. It depends only on
already-gathered segments, but on the main stream it delayed the
leader's next-round decode and therefore every rank's next collective
(~0.1s/chunk on the critical path). Gathered storage is pinned to the
assembly stream via record_stream; the driver drains the stream in a
finally so an exception cannot leave an in-flight DMA into the output
buffer. Stream placement does not change op order or values, so the
bitwise-parity contract is untouched.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Under SP>1 the H3 video VAE decoded all temporal chunks serially on the
output rank while the other ranks idled (#1703's gate), and every rank
encoded the full reference video redundantly. Chunk decodes and clip
encodes have no cross-chunk data dependency - only the joining (overlap
blend, trim, denormalize, moment concat) is sequential - so both are
round-robined across the sequence-parallel ranks:
- fastvideo/models/vaes/minimax_h3_parallel.py: decode_to_pixels_parallel
gathers each round's decoded segments (body+halo tail, one contiguous
slice per chunk) to the SP group's first rank via NCCL gather (or
all_gather, FASTVIDEO_VAE_PARALLEL_DECODE_STRATEGY), which replays the
serial blend/trim/denormalize/copy semantics with the same VAE methods -
bitwise-equal to serial decode_to_pixels by construction. Placeholder
rounds keep collective participation uniform; the leader decodes chunk 0
first and broadcasts dtype/shape metadata so placeholders never guess the
autocast dtype. encode_pixels_parallel all-gathers per-clip moments
(latent-sized) so every rank keeps the identical full posterior,
preserving the all-ranks-hold-latents contract.
- decoding stage: output gate moves from world rank 0 to the SP group's
first rank (identical in the single-group e2e case; correct for the
trainer validation callback, which consumes each group leader's batch);
with vae_parallel_decode every rank enters the decode body so no
rank-dependent branch guards the collectives.
- latent preparation: opt-in clip-parallel reference encode on the same
seam (vae_parallel_encode).
- knobs: FastVideoArgs.vae_parallel_decode/encode (+ --vae-parallel-decode,
--vae-parallel-encode, FASTVIDEO_VAE_PARALLEL_DECODE/ENCODE env
parse-once adapters), default OFF.
- _copy_chunk_pixels factored out of _decode_to_pixels so serial and
parallel share one output-copy path (behavior unchanged).
Tests: threaded fake-group CPU suite drives the real SPMD functions
end-to-end (world sizes 2-5, both strategies, pad/blend/trim geometries,
token_drop=0, batched slicing, placeholder rounds) bit-exact vs the serial
APIs; GPU regression (torchrun world>1 gated) asserts bitwise parity under
fp16 autocast with real NCCL plus repeat-determinism x3.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Carried blocker fix for PR #1734 on this integration stack, per pr1734.md
finding F1 (reproduced on GB200/torch 2.12): with
@torch.compile(mode="reduce-overhead") on _stitch_tiles, the stitched canvas
is a CUDA-graph static buffer, and the collect-then-torch.cat consumers -
_decode (via _decode_chunks), _encode, _encode_pixels, encode_keyframe - hold
each chunk/clip result across the next _stitch_tiles replay, which overwrites
the pooled storage. First tiled decode() with >=2 temporal chunks (any real
video) and tiled encode() of >17 frames raised:
RuntimeError: Error: accessing tensor output of CUDAGraphs that has been
overwritten by a subsequent run. ... line ..., in _stitch_tiles:
return torch.cat(result_rows, dim=-2)
Fix: .clone() the stitch output at the two eager call sites (_encode_clip
tiled return, _decode_clip tiled return) so no cudagraph-owned storage
escapes the tile driver; the clone is read before the next replay, so it is
race-free by construction. The streaming _decode_to_pixels path was already
safe (full copy-out per chunk before the next decode) and stays correct at
one extra D2D copy per chunk (~tens of microseconds vs the decode compute).
This follows the option (a) recommendation in the review; reduce-overhead is
retained on _stitch_tiles/_project_decoder_tile.
Tests:
- test_decode_clip_emits_tiled_stage_ranges updated: the tile driver now
returns a caller-owned copy (is-not + equal), pinning the ownership
contract at the mock level.
- New CUDA regression gate (the review's must-add test):
test_tiled_decode_and_encode_survive_cudagraph_buffer_reuse_on_cuda - real
tiled decode() (2 temporal chunks, 2x2 spatial tiles) and encode()/
encode_pixels() (2 clips) with _stitch_tiles unmocked, asserting bitwise
repeat-consistency plus value parity against the fully eager tile helpers
(via _torchdynamo_orig_callable). Red on the unfixed merge (cudagraph
overwrite RuntimeError on the first decode), green with this fix.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>