Repository navigation
[4/N] Unify worker-side MP4 encoding and wire Wan into it - #7048
hsliuustc0106 merged 13 commits into
Conversation
|
Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits. |
|
This PR appears to be related to model: Wan. Model owners: @bjf-frz @specture724, please review your own changes and leave a short self-review comment describing what you checked. PRs without author self-review may not be assigned a reviewer. Please take a look when you have a chance. If you would like an automated review, mention @vllm-omni-review-bot in a comment. |
7df80e3 to
f847196
Compare
f847196 to
d8806eb
Compare
9b79bf9 to
ab7e0fc
Compare
|
[P1] Abort the S2V MP4 session when clip generation fails. After an earlier clip starts encoders, a later denoise/VAE failure exits [P2] Preserve serving codec defaults for Wan pre-encoding. With only Reviewed Local A/B used one reserved L20X, Python 3.12, torch 2.13.0, vLLM 0.28.0, diffusers 0.40.0, and the real Wan2.1-T2V-1.3B VAE (revision
Mean stage latency fell 8.29%, but output equivalence did not hold: both pairs retained 257 frames, with decoded RGB MAE 3.426/255, maximum difference 56, and PSNR 34.56 dB. These two observations do not establish output-quality equivalence or a full-request speedup; denoising, HTTP and IPC were not measured. |
fee2684 to
af8aa1c
Compare
|
Re-reviewed [P1] Abort the S2V MP4 session when clip generation fails. A second-clip denoise failure still exits [P2] Preserve serving codec defaults for Wan pre-encoding. With only The update is mostly a rebase onto merged #7018. The T2V adaptation preserves typed media on the output-owning rank and the legacy empty placeholder on non-owner ranks; I found no additional regression in that change. Validation: 246 focused CPU tests passed across shared chunked video, Wan/S2V, H3, T2V forward and typed-media contracts. An additional T2V smoke check with model/VAE stubs and real CPU encoding passed for two requests × two outputs: the owner produced four playable MP4s; the simulated non-owner returned empty outputs without a typed-media batch error. Both earlier findings were reproduced on this head, and all reproducer-owned encoder threads were cleaned up. Python 3.12, torch 2.13.0, vLLM 0.29.0, PyAV 18.1.0. The earlier GPU A/B was not rerun and is not current-head performance evidence. Full S2V denoising, HTTP end-to-end behavior and multi-GPU execution were not tested in this re-review. |
|
@MuweiZ any more comments? |
| if current_omni_platform.is_available(): | ||
| current_omni_platform.empty_cache() | ||
| if mp4_session is not None: | ||
| mp4_session.push(clip_video) |
There was a problem hiding this comment.
[P3] Encode only on the output-owning VAE-PP rank. After the broadcast every rank of _vae_pp_group holds the clip, so every rank pushes into its own session and runs quantize + D2H + a full H.264/AAC encode per clip — but the executor collects only rank 0's forward response (num_responses = 1 in multiproc_executor.py), so peer output is discarded. T2V/I2V/H3 all encode owner-only because their peer ranks receive no chunks. The broadcast itself must stay (the AR motion loop needs frames on all ranks); gating the push on the pp-group owner leaves peers with an empty session, and finish() == [] is already tolerated downstream (the T2V non-owner path).
|
Re-reviewed
Architecture: the owner-only encoding convention holds for T2V/I2V/H3 — peers receive no chunks, Performance: the body's claims match the implementation. CI on this head: main / AMD / NPU / build 3.11+3.12 / pre-commit / RTD green. |
| ) | ||
| num_steps = common.num_inference_steps or num_inference_steps | ||
| num_outputs_per_prompt = common.num_outputs_per_prompt or 1 | ||
| preencode_mp4 = resolve_wan_preencode_mp4(common, output_type=output_type or "np") |
There was a problem hiding this comment.
Validate common.output_type here so preencoding rejects pil, pt, and latent requests.
71b4892 to
b2d7c0f
Compare
Give the Wan chunk callback its production consumer. With preencode_mp4, each committed temporal chunk is quantized on the accelerator, transferred once, and pushed to one bounded encoder per batch entry, so host transfer and H.264 encoding overlap the remaining decode instead of following it. The pre-encoded path emits MP4 bytes, so it rejects rather than silently downgrades what it cannot serve: frame interpolation, which needs the frames it no longer materializes, and non-array output types. Encoding fps mirrors the fallback the serving layer applies, so the bytes carry the rate the response advertises without reporting one. Wan2.1-T2V-1.3B VAE weights, L20X, latent [1,16,65,60,104] -> 257 frames at 480x832: full decode plus one-shot mux ~13.05 s, wired pre-encode ~9.68 s, a ~26% reduction at an identical frame count. Consumer batching trades 65 host transfers for 13; wall clock is within noise of the per-chunk path on this GPU. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: specture724 <specture724@gmail.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: specture724 <specture724@gmail.com>
… them S2V already decodes one clip per iteration and needs each clip's trailing frames to build the next clip's motion latents, so it has no use for the finer temporal callback. Hand each finished clip to a bounded encoder instead of accumulating clips on the host and concatenating: H.264 encoding then overlaps the next clip's denoise and decode, and the full video is never materialized. The waveform is muxed into each container, so nothing downstream re-attaches audio. Covered by driving the real forward() over a two-clip loop on CPU: both clips reach the encoder, every output decodes as a playable container with an audio stream, and each request's waveform lands on its own outputs. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: specture724 <specture724@gmail.com>
[1/N] landed the seam as a dedicated decode_with_chunks method behind the SupportsChunkedVAEDecode protocol, rather than an on_chunk keyword on decode(), so the consumer called a signature that no longer exists. Call the capability method and gate on the published type guard, which turns an unsupported VAE into a clear error instead of a TypeError from deep inside the decode call. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: specture724 <specture724@gmail.com>
Producing chunks is the model's business: Wan walks a causal frame-by-frame loop, H3 walks overlapping clips, and Wan S2V decodes one clip per autoregressive iteration. What happens after a chunk is committed is not -- every one of them quantizes to uint8, moves to the host once, and lands in a bounded encoder -- yet each model carried its own copy of that, so a capability meant to be model-agnostic had a per-backend consumer bolted onto it. Move the consumer into diffusion/utils/chunked_video.py: ChunkedVideoMP4Session for producers that drive themselves, and decode_to_mp4() for producers that can be driven through the capability. Batching, per-batch-entry encoders, audio muxing, the requested-size crop, and quantization from the producer's declared pixel range are all parameters now. H3, Wan T2V/I2V, and Wan S2V share it; a model that wants worker-side encoding implements one method and declares one range. Real Wan2.1-T2V-1.3B weights decode to the same 257 frames with an unchanged pre-encode SHA, so the move is behavior-preserving. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: specture724 <specture724@gmail.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: specture724 <specture724@gmail.com>
Signed-off-by: specture724 <specture724@gmail.com>
… request Every rank of a distributed H3 VAE group drives the temporal collectives, but only the owner receives chunks, so the shared consumer returns no container on a peer. The H3 wrapper treated that as a violated single-output invariant and raised, turning normal peer-rank silence into a request failure. Return empty bytes there instead -- the pre-encoded counterpart of the empty tensor the full decode leaves on a peer -- and enforce the single-container invariant only on the owner. Two new cases drive the real H3 decode_to_mp4 and post_decode chain on a peer-rank VAE rather than the shared fixture. Also correct two descriptions that claimed more than the code does: the chunk transfer is a synchronous .cpu() inside the producer callback, so what overlaps the remaining decode is the CPU encode of earlier chunks rather than the copy; and the encoder's bound covers pending raw frames in chunks, not total request memory, since the muxed container still grows with output duration. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: specture724 <specture724@gmail.com>
Signed-off-by: specture724 <specture724@gmail.com>
Under VAE patch parallelism every rank of the VAE group receives each decoded S2V clip by broadcast, because the autoregressive motion loop needs the frames everywhere. Each rank then also pushed the clip into its own MP4 session and paid quantization, a host transfer, and a full H.264/AAC encode, although the executor keeps only the owning rank's response. Record ownership from the decode itself, where peers get an empty placeholder, and push only on the owner. The broadcast and motion loop are unchanged. A peer now finishes with no containers, the same empty result T2V, I2V, and H3 non-owners already return, which the per-request split and downstream paths tolerate. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: specture724 <specture724@gmail.com>
S2V resolved the pre-encode guard against the forward keyword argument, which defaults to np, instead of the request's own output_type. A request asking for pil, pt, or latent therefore passed the guard and received MP4 bytes, while T2V and I2V reject the same combination. Read output_type from the sampling params first, as T2V does, and keep the keyword as the default for direct callers. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: specture724 <specture724@gmail.com>
… reads Rebasing onto main picked up the UniPC single-shift fix, which passes self._flow_shift into set_timesteps. The pre-encode stub builds its pipeline with object.__new__ and never set that attribute, so every pre-encode test raised AttributeError on the new call. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: specture724 <specture724@gmail.com>
b2d7c0f to
eaad9eb
Compare
…ct#7048) Signed-off-by: specture724 <specture724@gmail.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com> Co-authored-by: Hongsheng Liu <liuhongsheng4@huawei.com> Signed-off-by: Matthieu Laneuville <matthieu.laneuville@surf.nl>
…ct#7048) Signed-off-by: specture724 <specture724@gmail.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com> Co-authored-by: Hongsheng Liu <liuhongsheng4@huawei.com>
[4/4] Unify worker-side MP4 encoding and wire Wan into it
Part of the RFC #6872 implementation stack (4/4), stacked on #7016, #7017, and
#7018. GitHub can't set a fork branch as another PR's base, so this targets
mainlike the rest of the stack; the diff includes the earlier PRs' commitsuntil they merge.
Summary
[1/N] added the Wan VAE chunk callback but nothing called it. This wires it up
and, now that a second consumer exists, moves the consumer itself into the
framework.
Producing chunks is the model's business: Wan walks a causal frame-by-frame
loop, H3 walks overlapping clips, and Wan S2V decodes one clip per
autoregressive iteration. What happens after a chunk is committed is not --
every one of them quantizes to uint8, moves to the host once, and lands in a
bounded encoder. Each model carried its own copy of that, so a capability meant
to be model-agnostic had a per-backend consumer bolted onto it.
diffusion/utils/chunked_video.pynow owns that half:ChunkedVideoMP4Sessionfor producers that drive themselves (Wan S2V pushesfinished clips out of its autoregressive loop).
decode_to_mp4()for producers that can be driven throughSupportsChunkedVAEDecode.Batching, per-batch-entry encoders, audio muxing, the requested-size crop, and
quantization from the producer's declared
chunk_value_rangeare parameters.H3, Wan T2V/I2V, and Wan S2V share it:
wan2_2/chunked_mp4.pydrops from 219to 77 lines (only request-option plumbing remains) and H3's
decode_to_mp4keeps only what is H3-specific -- decoding audio first, the crop, the fixed
rate. A model that wants worker-side encoding now implements one method and
declares one pixel range.
With
preencode_mp4, each committed chunk leaves the accelerator while laterchunks are still decoding, so host transfer and H.264 encoding overlap the
remaining decode instead of following it. The pre-encoded path emits MP4 bytes,
so it rejects rather than silently downgrades what it cannot serve:
enable_frame_interpolation, which needs the frames it no longer materializes,and non-array output types.
Validation
Two measurements, at different scopes. They are not comparable to each other.
VAE decode stage, T2V — real Wan2.1-T2V-1.3B VAE weights, L20X, latent
[1,16,65,60,104]→ 257 frames at 480×832. Denoising is excluded, so thisisolates the stage the callback changes:
~26% off the decode-plus-encode stage at an identical 257-frame count.
Consumer batching trades 65 host transfers for 13; wall clock is within noise of
the per-chunk path on this GPU, so the default is about transfer count, not
latency.
Full request, S2V — real Wan2.2-S2V-14B weights, L20X, TP=1, 448×448, 77
frames, 4 steps, warmed up, both orders run to expose ordering effects. The
baseline's MP4 encoding happens downstream of
generate(), so it is measuredand added:
generate()~3% end to end. The win is small here because denoising dominates a full S2V
request and the mux is only ~0.47 s of ~16.4 s; it is the same ~0.5 s of
encoding, now absorbed into the decode loop. An earlier unwarmed run showed
~32%, which was entirely first-call overhead — that number is not real.
Output equivalence: 77 frames on both paths, audio stream present in the
pre-encoded container. The consumer move is behavior-preserving: real
Wan2.1-T2V-1.3B weights decode to the same 257 frames with an unchanged
pre-encode SHA. Decoded pixels differ by lossy H.264 re-encoding
(
max_abs 37,mean_abs 1.70), which is expected; the bit-exact signal is thepre-encode tensor comparison in [1/3].
Tests: 11 new cases for the framework consumer, covering per-batch-entry
containers, the crop, audio muxing, ranks that own no output, a VAE that does
not declare the capability, and the property the shared quantizer exists for --
a Wan
[-1, 1]chunk and an H3[0, 1]chunk of the same picture quantize tothe same pixels. Wan's own cases cover transfer coalescing and every rejected
combination. Two more drive the real S2V
forward()over a two-clip loop onCPU, checking that both clips reach the encoder, that every output decodes as a
playable container with audio, and that each request's waveform lands on its own
outputs.
Known limitation
With VAE patch parallelism, S2V broadcasts decoded frames to every rank because
the autoregressive loop needs them, so every rank encodes its own identical
copy. This matches the existing full-decode path, which likewise concatenates
the same tensor on every rank, and is left alone here rather than changing which
rank produces output.
🤖 Generated with Claude Code