Wan-Animate-2 (authored by @kelseyee) - #14413
Open
yiyixuxu wants to merge 17 commits into
Open
Conversation
Model (`transformer_wan_animate_2.py`): - Replace the `forward(*args, method=...)` dispatch and the split `forward_ref`/`forward_gen` with a single documented `forward(..., kv_cache_mode="extract"|"cached")` returning `Transformer2DModelOutput`, following the Flux2 KV-cache precedent. The `SelfAttention`/`CrossAttention` pre/post split becomes a regular `WanAnimate2Attention` (`AttentionModuleMixin`) with processors that run through `dispatch_attention_fn` - native SDPA by default, any backend via `set_attention_backend`; only the in-context generation path is pinned to `flex`, since its attention pattern is expressed as a `BlockMask`. The hard `flash_attn` requirement is gone. - `IncontextAttentionBlock` was a pure pass-through around `AttentionBlock`; merged into one `WanAnimate2TransformerBlock` (checkpoint keys lose the `.block.` segment, handled in the single-file mapping). - KV cache is a `WanAnimate2KVCache` object instead of bare dicts passed through `forward`. Accelerate hooks copy dict arguments, so the dict version breaks under `enable_model_cpu_offload` (the reference pass fills a copy and the generation pass KeyErrors); the object passes through by reference, and `_skip_keys = ["kv_cache"]` covers group offloading. - Remove all autocast in favour of the `transformer_wan.py` dtype discipline (fp32 modulation with `.type_as` casts at block boundaries), so the model runs natively in bf16 and is no longer CUDA-only in principle. Replace the local float64 `sinusoidal_embedding_1d` with the existing `Timesteps` class. - Remove dead code: the unreachable padding mask in the reference path (the pipeline always fills `seq_len` exactly), `init_weights`, `load_from_official_state_dict`, and the unused `window_size`/`qk_norm`/ `sparse_type`/`log_scale` config flags (`log_scale` ships as 0.0, making the flex `score_mod` a no-op). Pipeline: call sites updated to the merged forward, autocast wrappers replaced with explicit casts at the call boundary. Also: - Fill in `convert_wan_animate_2_transformer_to_diffusers` with the actual key mapping (block unwrap + attention renames); it was a prefix-strip no-op. - Revert the `pipeline_utils.py` try/except import shims - the stub exception classes silently break real `except OfflineModeIsEnabled` handling; upgrade `huggingface_hub` to the `setup.py` pin instead. - Drop the modular pipeline for now; it needs its own pass and is not part of the initial release surface. Numerics: the refactored model matches the reference implementation at 2.47e-05 max relative difference in fp32 over all 40 layers on real weights, and end-to-end outputs match the reference pipeline to a max pixel difference of 7e-5 (PSNR 119 dB) when kernels and environment are held fixed. The checkpoint key rename is a pure rename - all 1303 tensors bitwise identical. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Each segment allocates a fresh KV cache holding the reference tokens for every layer -- tens of GB at high resolution. Holding the previous segment's cache alive while the next one is built fragmented the allocator enough to OOM mid-run on an 80GB card. Move finished frames to CPU, clear the cache and drop the per-segment latents before starting the next segment. `out_frames` is deliberately kept: the next segment conditions on its tail. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A list of frames does not carry the rate it was sampled at, so a pipeline that has to resample its input to the frame rate the model works at cannot get that number from `load_video` — even though imageio hands it to us and we throw it away. Add an opt-in `return_fps`; GIFs get it from the frame duration. Opt-in, so every existing caller keeps returning a plain list of images. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The pipeline reached for decord and cv2 to do what the processors already do. decord was an undeclared dependency imported inside `__call__`, and cv2 is not a diffusers requirement — it is in the deps table but not in `install_requires`, and only consisid and `export_utils` touch it, both behind local imports. `WanAnimateImageProcessor` already letterboxes for `WanAnimatePipeline`, which is what `padding_resize` and `resize_by_area` were hand-rolling: keep the aspect ratio, fill the remainder with black. Add the video counterpart and use both, so the driving video arrives as frames from `load_video` like every other video pipeline takes it, with `driving_video_fps` carrying the one thing a frame list cannot — decord used to read the source rate itself, and the 30 -> 24 resample is load-bearing. Also drop the `seed` argument in favour of the `generator` we already accept, and sample with `self.scheduler` instead of building a scheduler per call from `flow_solver` and `sample_shift`. The registered scheduler was dead weight before; the checkpoints now carry the right one, and swapping it is documented. `_encode_vae` was defined but unused while its body was inlined three times. Preprocessing is equivalent, not identical: output dimensions and resampled frame indices match exactly, and content-aligned the difference is 0.2% of full range — PIL lanczos against cv2 INTER_AREA. PIL also centres the paste one row lower than cv2 did; each path's crop follows its own paste. Seeded end to end on the distilled model that lands at 23.7 dB, the same band the attention refactor already sits in, with an identical contact sheet. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Seed-matched against the approved commit, everything the pipeline hands the transformer is bit-identical -- noise, timesteps, text embeddings, geometry -- except the resized pixels, and the entire pixel residual traced to three resize facts, each verified in isolation (decode and normalization are proven byte-identical): - kernel choice: bilinear matches the driving frames' `INTER_LINEAR` upscale (same filter; only exact-half ties round differently, at most one 8-bit level per pixel), bicubic measures closest to `INTER_AREA` for the reference image's downscale of the PIL kernels diffusers exposes - paste placement: cv2 letterboxes at `(height - src_h) // 2`, PIL at `height // 2 - src_h // 2` -- one row apart when the frame is even and the content odd, previously the largest input difference - the interim `WanAnimateVideoProcessor` inherited `WanAnimateImageProcessor`'s `__init__`, whose bare `super().__init__()` re-registers every shared config field with the parent's defaults -- `resample` was silently lanczos `WanAnimate2VideoProcessor` replaces it: one class, its own `register_to_config` init that does not chain into the decorated parents, the reference paste convention, and per-instance kernels (bicubic for the reference image, bilinear for the driving video). `WanAnimateImageProcessor` and the merged Wan-Animate pipeline are untouched. First-segment output agreement with the approved commit at matched seed is 27.5 dB (base) / 28.9 dB (distilled), above the 25.1 dB the approved commit scores against itself when only the attention backend changes. Divergence in later segments is the chained conditioning amplifying any perturbation, numerical noise included, and is documented where the processors are built. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
The docs for this PR live here. All of your documentation changes will be reflected on that endpoint. The docs are available until 30 days after the last update. |
Ground-up modular decomposition of WanAnimate2Pipeline in modular_pipelines/wan_animate_2/, verified bit-identical to the standard pipeline (tiny fp32 with and without CFG, and the real distilled checkpoint in bf16). - Outer segment loop as LoopSequentialPipelineBlocks (helios style), with the per-segment driving VAE encode, previous-frame conditioning, KV-cache reference extraction, scheduler reset, hand-written denoise loop, and in-loop decode (each segment conditions on the previous segment's decoded pixels) as separate loop blocks. - CFG through the guider; `is_uncondtion` rides the guider's per-branch tuple inputs. Two presets: WanAnimate2Blocks (guidance_scale=3.0) and WanAnimate2DistilledBlocks (guidance_scale=1.0). - Segment-invariant work hoisted out of the loop: text/CLIP encoders (the driving-frame CLIP context is computed once, not per segment), reference VAE encode, and segment geometry. - WanAnimate2VideoProcessor moves into the modular folder; the standard pipeline imports it from there, and pipelines/wan/image_processor.py is back to zero net change. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Top-level blockset children now follow the family convention
(text_encoder / image_encoder / video_encoder / vae_encoder / denoise /
decode), each poppable and usable standalone, each a flat sequence of
leaf blocks (wan-i2v / flux2 shape).
- Hoist the driving-video VAE encode out of the segment loop into the
vae_encoder group: the Wan VAE is causal in time so each slice is
encoded separately, but all slices are known upfront. The loop keeps
only the genuinely sequential work (prev-frame conditioning and
per-segment decode).
- Drop the guider from the text encoder step: the denoise step owns the
guider spec (removing the 3.0-vs-1.0 spec conflict in the distilled
preset), and the text encoder encodes the negative prompt when the
pipeline's guider requires unconditional embeddings or one is passed
explicitly -- standalone, nothing is encoded unless asked.
- Rename to canonical step names: ProcessImagesInputStep /
ProcessVideosInputStep (flux/qwen convention), {Image,Video}{Clip,Vae}
EncoderStep leaves, short EncodeStep group names.
- Rename clip_len -> segment_frame_length and first_num ->
prev_segment_conditioning_frames, matching the merged Wan-Animate v1
pipeline's argument names.
Verified bit-identical to the standard pipeline after each change (tiny
fp32 base+distilled, real distilled checkpoint bf16).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…sor names - Move the driving-video VAE encode back into the segment loop (per-segment causal encode, symmetric with the in-loop decode); the size check between the image/video preprocess outputs runs once in prepare_segments - Rename the research-code conditioning tensors to the merged Wan-Animate v1 names: y_ref -> reference_image_latents, y -> reference_latents, y_reft -> prev_segment_cond_latents; condition_latents/condition_y -> driving_video_latents/driving_video_condition - Rename the preprocessed video state to driving_video_pixels; derive latent/pixel dims from tensors instead of passing latent_height/latent_width - Collapse the four crop_* ints into a single crop_region tuple - Default height/width on the video preprocess step for standalone use; clarifying comments (zigzag padding, loop-carried state) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…sets - tests/modular_pipelines/wan_animate_2/: ModularPipelineTesterMixin + ModularGuiderTesterMixin against YiYiXu/tiny-wan-animate-2-modular and -distilled-modular (25 passed / 15 skipped); batch tests skipped (the pipeline is unbatched), guider test threshold lowered with rationale - Use randn_tensor for segment noise in both pipelines so CPU generators work; CUDA-generator path unchanged (parity re-verified bit-identical) - Describe every InputParam (templates where available); no auto-docstring TODOs remain - Narrow the core denoise steps' outputs to segment_frames — the only product the decode step consumes - Make the distilled blockset file self-contained: it assembles its own image/video encoder groups from the leaf blocks instead of importing the base blockset's classes Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Modular-only pipeline page (ModularPipeline.from_pretrained example with offloading + compile, both presets, area-based height/width semantics) and the transformer model page; toctree entries sorted. References the official Wan-AI hub ids, which will need the converted weights and modular_model_index.json before the examples run as written. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Import sorting in the two __init__s and doc-builder docstring reflow; no behavior changes. make quality now exits clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Under eager initialization (DIFFUSERS_SLOW_IMPORT, used by the doc build) `pipelines` -> `pipeline_wan_animate_2` -> `modular_pipelines.wan_animate_2` -> `modular_pipeline` -> `pipelines` is a cycle; importing the processor inside `__init__` breaks it. Goes away entirely with the standard pipeline's pre-merge removal. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
yiyixuxu
commented
Aug 12, 2026
|
|
||
| self.vae_scale_factor_temporal = self.vae.config.scale_factor_temporal if getattr(self, "vae", None) else 4 | ||
| self.vae_scale_factor_spatial = self.vae.config.scale_factor_spatial if getattr(self, "vae", None) else 8 | ||
| # Wan-Animate-2 letterboxes the reference image and the driving video into the same frame: aspect |
Collaborator
Author
There was a problem hiding this comment.
@kelseyee
we replaced the cv2/decord-based resizing (resize_by_area / padding_resize) with diffusers' PIL/torch video processor, so cv2 and decord are no longer dependencies -> it cause a very slight difference in output
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.
part of #14412
base
official_base.mp4
distilled
official_distilled.mp4