[new-model] Add Wan2.2-Animate-14B character animation/replacement inference - #1765
Open
SuhaanCoding wants to merge 10 commits into
Open
[new-model] Add Wan2.2-Animate-14B character animation/replacement inference#1765SuhaanCoding wants to merge 10 commits into
SuhaanCoding wants to merge 10 commits into
Conversation
Contributor
Merge Protections🔴 1 of 1 protections blocking · waiting on 👀 reviews and 🤖 CI
🔴 PR merge requirementsWaiting for
This rule is failing.
|
SuhaanCoding
marked this pull request as ready for review
August 27, 2026 11:44
SuhaanCoding
pushed a commit
to SuhaanCoding/FastVideo
that referenced
this pull request
Aug 27, 2026
Control build for the CI stall seen on hao-ai-lab#1765 (pr-fastcheck hao-ai-lab#931/hao-ai-lab#932): a different branch from the same author/fork, to separate a wedged runner from anything branch-specific. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
SuhaanCoding
force-pushed
the
wan-animate
branch
from
August 27, 2026 17:21
817fa9b to
0c0356a
Compare
Batch 1 of the Animate port: WanAnimateArchConfig transcribed verbatim from Wan-AI/Wan2.2-Animate-14B-Diffusers transformer/config.json (all 26 keys verified field-for-field against the manifest), and a WanAnimate14BConfig pipeline config subclassing the I2V line -- Animate is a dense 14B on the Wan2.1-I2V skeleton (in_dim 36 channel-concat, CLIP ViT-H image branch, Wan2.1 VAE), not the Wan2.2 MoE. Field names match the checkpoint config keys exactly so update_model_arch overlays them at load time; __post_init__ asserts the in_channels = 2*latent_channels + 4 decomposition and the dense face-adapter indexing rule (adapter i serves block i*5), which the checkpoint does not record. Model, stages, and registry land in follow-up batches. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ests Batch 2 of the Animate port. The tower needed no new block: Animate is the stock Wan-I2V transformer (36ch channel-concat input, CLIP image branch, single timestep, standard RoPE), so WanAnimateTransformer3DModel subclasses WanTransformer3DModel and adds only the three Animate pieces: a pose patchifier added to every frame but the reference slot, the LIA motion encoder + causal face funnel (wan_animate_face.py), and 8 dense face-adapter cross-attention hatches applied after every 5th block. Ported against both references (official wan/modules/animate/ and the merged diffusers WanAnimateTransformer3DModel); tensor names match the official Diffusers-format checkpoint verbatim, so only the two patchifiers need mapping entries. The StyleGAN2 runtime weight scaling (unit-scale stored weights x 1/sqrt(fan_in) at forward) is reproduced exactly and pinned by tests -- loading those tensors into vanilla layers succeeds and is silently wrong. Blur kernels are non-persistent buffers rebuilt by materialize_non_persistent_buffers after meta-device loading. 29 tests: config transcription + guard asserts, the weight-scaling contract, per-frame face-attention confinement, pose skip-ref isolation, adapter firing order, meta-device survival, and two weight-gated tests (strict meta mapping of every checkpoint tensor; raw no-autocast bf16 CUDA forward) gated on WAN_ANIMATE_MODEL_PATH. Sequence parallelism is explicitly out of scope for v1 (the face adapter's per-frame reshape needs the whole sequence on one rank); forward raises on sp_world_size > 1. Registry needs no alias: EntryClass auto-discovery matches the checkpoint's _class_name. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Batch 3 of the Animate port. Five stages transcribed against diffusers' WanAnimatePipeline and the official runner: - AnimateLatentPreparationStage: +1 latent frame (the reference slot). - AnimateConditioningLatentsStage: assembles the 20-channel y = [4ch folded I2V mask | 16ch cond latent] x [ref frame | target frames] into batch.image_latent; animation mode encodes a black video through the VAE (VAE(zeros) != zero latents), replace mode encodes the background video and inverts + nearest-downsamples the character mask. - AnimatePoseVideoEncodingStage: skeleton video -> VAE latents (argmax, standard normalisation), T_lat frames (no pose on the ref slot). - AnimateFaceVideoStage: face crops as raw pixels at motion_encoder_size, [-1, 1]. - AnimateDecodingStage: decode latents[:, :, 1:] (drop the ref slot). DenoisingStage passes pose_latents/face_pixel_values by name (dropped for every other model); the CFG negative pass keeps pose and blanks the face to -1, matching diffusers -- though Animate defaults to guidance 1.0, so it rarely fires. The existing image_latent channel-concat path provides the final [noise 16 | mask 4 | cond 16] = 36ch DiT input. v1 scope: single 77-frame segment; multi-segment refert chaining is a follow-up on top of these stages. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Batch 4 of the Animate port. Threads the five new inputs (pose_video_path, face_video_path, background_video_path, mask_video_path, animate_mode) through every public surface: SamplingParam (+CLI args), the typed schema's InputConfig, the parser roundtrip test, and the schema-parity inventory (which also declares the WanAnimate14BConfig-owned refert_num). Registers the pipeline: Wan-AI/Wan2.2-Animate-14B-Diffusers resolves via path + an "animate" detector to WanAnimate14BConfig with the new wan_animate_14b preset (720p-area, 77 frames @ 30 fps, 20 UniPC steps, guidance 1.0 -- the official runtime defaults; CFG off because the prompt is non-core for this model). animate_mode arriving as None from the API layer normalises to "animation" in the conditioning stage. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Batch 5 of the Animate port. The rank-128 relighting LoRA (replace mode only) ships PEFT-format in the native repo and is absent from the Diffusers-format one. Two pieces make it loadable through the existing LoRAPipeline --lora-path mechanism: - wan_animate_relight_lora.py repacks it (key renames only, no tensors modified): strips PEFT's base_model.model. prefix / .default. infix, emits per-layer lora_alpha from adapter_config.json so alpha/rank scaling matches PEFT, and can meta-validate that every target layer resolves onto a FastVideo model parameter. - The Animate LoRA name mapping extends the base Wan mapping with the I2V image-KV heads (cross_attn.k_img/v_img -> attn2.add_k_proj/ add_v_proj), which the base mapping never needed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Batch 6 of the Animate port: a runnable example (animation mode, the official runtime defaults, documents the preprocessed-input contract and where replace mode's bg/mask/LoRA fit) and the two support-matrix rows. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Four parallel reviews (model core / stages / configs+scripts / tests), findings verified against the diffusers reference and applied. Bugs fixed: - motion_encoder.motion_synthesis_weight now pins to fp32 via the loader's _get_parameter_dtype hook: diffusers keeps it in _keep_in_fp32_modules, and the QR basis is re-derived from it every forward, so bf16 rounding changed the basis itself. - FASTVIDEO_ATTENTION_BACKEND=VIDEO_SPARSE_ATTN now raises at construction: the base __init__ would build VSA blocks whose to_gate_compress weights the checkpoint lacks, and the loader's ALLOWED_NEW_PARAM_PATTERNS whitelist zero-fills exactly that name. - The relight-LoRA repack now emits every key (including the per-layer lora_alpha entries) in FastVideo naming: the loader strips .lora_alpha before mapping and no mapping regex can then match, so native-named alphas were silently dropped and the LoRA merged at scale 1.0. --validate also initialises a size-1 distributed group, without which model construction asserts. - vae_tiling was ignored by both new encode stages; unknown animate_mode values (including diffusers' spelling "animate") were silently treated as animation mode and now raise; height/width must be multiples of 16 (the reference raises, we silently truncated a latent row); _pad_frames IndexError'd on 0/1-frame driving videos. Vacuous tests fixed: the confinement test's channel-constant perturbation was exactly cancelled by the affine-free pre-norm LayerNorm; the adapter-order test used stride 1 (idx%1 and idx//1 are degenerate); the padding-token test compared zeros to a zero-init parameter; the orthonormality test asserted a property of LAPACK without calling the module (now a scale-invariance test). New tests: motion-layer dtype cast, batched-clip face non-interleaving, the full LIA channel table on meta, _pad_frames and _fold_i2v_mask. Dead code removed: refert_num (no reader), 31 lines of CLI args behind a method with zero callers, an unreachable duplicate guard in the face attention, a redundant dtype cast, a no-op device hop, never-varied constructor knobs, a no-op mask_len guard, an unreachable None-filter. Mapping helpers inlined as dict unions; backends tuple now lives on the arch config; FSDP shard predicate no longer matches the motion encoder's res_blocks; int(math.log2) -> bit_length. Comments: invented metaphors and references to out-of-repo code replaced with plain statements; inverted assert message fixed; divergences from the reference (letterbox resize, motion mask) now stated where they occur. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ale) First run on real weights (DGX Spark, 33/36 passed) settled two things: - The Animate checkpoint ships no blocks.N.attn2.norm_added_q -- the dead parameter FastVideo's WanI2VCrossAttention allocates only because base Wan-I2V checkpoints carry it (never used in forward). The strict load failed on 40 missing keys; the model now replaces it with nn.Identity at construction so both load directions are clean. - The tiny-model init helper used a flat std=0.02, which double-shrinks the StyleGAN2-style motion-encoder layers (they apply 1/sqrt(fan_in) at runtime and expect unit-scale weights, as the real checkpoint has): the face signal attenuated over its ~15-layer path until it was bitwise invisible at the output, failing test_forward_responds_to_pose_and_face. Weights now get unit gain per layer, unit scale in the motion encoder. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
yapf reformatting (two files), one mypy annotation (_pad_frames's
accumulator), and one codespell reword ("refert" in a comment; unlike
S2V's 'padd' nothing needs the literal spelling, so no ignore entry).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
SuhaanCoding
force-pushed
the
wan-animate
branch
from
August 28, 2026 14:49
0c0356a to
9c39220
Compare
Build hao-ai-lab#975 ran during a runner outage (every PR built 14:30-17:10Z failed all six lanes). Rebuilding on a healthy runner. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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.
Purpose
Add Wan2.2-Animate-14B inference: character animation driven by a reference image plus a driving video (skeleton + face crops), with an animation mode (character on the reference image's background) and a replacement mode (character composited into the driving video's scene).
The implementation follows FastVideo's component, pipeline-stage, preset, registry, and checkpoint-loading abstractions. It loads the official Diffusers-layout checkpoint (
Wan-AI/Wan2.2-Animate-14B-Diffusers) directly — no weights conversion or upload is needed — and does not change the routing or behavior of existing pipelines. Driving inputs are the files the official preprocessing produces (wan/modules/animate/preprocessin the Wan2.2 repo); this PR does not re-implement that preprocessing, matching the choice the diffusers integration made.Follow-up to #1683 (S2V); related community interest in #1126 (distilling this model).
Changes
WanAnimateTransformer3DModelas a subclass of the existing Wan transformer: the tower is unchanged Wan-I2V; the port adds a pose patch embedding (applied to every latent frame except the reference slot), a LIA-style motion encoder + face encoder, and 8 per-frame face cross-attention blocks (one after every 5th transformer block).sqrt(fan_in)at forward), and pinmotion_synthesis_weightto fp32 via the loader's per-parameter dtype hook (an orthonormal basis is re-derived from it every forward).wan_animate_14bpreset (77 frames, 30 fps, 20 steps, guidance 1.0 — the official runtime defaults), and registry resolution.pose_video_path,face_video_path,background_video_path,mask_video_path,animate_mode), with schema-parity inventory and parser-test updates.Test Plan
Numerical parity against diffusers was checked with a standalone two-phase harness (each phase loads one implementation in fp32 from the same checkpoint, identical fixed-seed inputs, forward hooks at every stage); happy to attach the script or fold it into
tests/local_tests/if wanted.Test Results
All runs on a DGX Spark (GB10) against the official checkpoint.
Test output
source video:
https://github.com/user-attachments/assets/339706fa-0e4c-477a-85fe-c154b565510e
source ref:

source face:
https://github.com/user-attachments/assets/d9cfa32d-1133-4206-ba9a-6c983e6eaac1
source pose:
https://github.com/user-attachments/assets/e172b655-d5f3-4219-b02d-c437bfd6c82c
output:
https://github.com/user-attachments/assets/023960c3-f864-49c1-aeb2-da61bf098c91
Note: shares a few files with #1683 (S2V) so whichever merges second needs a small rebase.