Skip to content

fix(wan2.1): causal VAE decode — T latents must yield 1+(T−1)·4 frames, not 4·T - #38

Open
xocialize wants to merge 1 commit into
Blaizzy:mainfrom
xocialize:fix/wan21-causal-vae-decode
Open

fix(wan2.1): causal VAE decode — T latents must yield 1+(T−1)·4 frames, not 4·T#38
xocialize wants to merge 1 commit into
Blaizzy:mainfrom
xocialize:fix/wan21-causal-vae-decode

Conversation

@xocialize

Copy link
Copy Markdown

Bug

WanVAE.decode (wan_2) runs Decoder3d over the whole latent sequence, and the upsample3d path temporally doubles every frame — so T latent frames decode to 4·T output frames. The reference Wan2.1 decoder (Wan-AI/Wan2.1 vae.py, also diffusers AutoencoderKLWan) decodes causally one latent frame at a time: the first chunk sets the 'Rep' sentinel and skips time_conv, so the first latent frame is not temporally upsampled — T → 1 + (T−1)·4 frames (65 for 17 latents, not 68).

Beyond the wrong frame count, the whole-sequence path produces different pixels at the start: comparing against the reference decoder on identical latents, boundary frames differ by up to 0.21 (abs, in [-1,1] range), decaying over ~8 frames, with a +3-frame phase shift across the rest (~190 ms at 16 fps — breaks A/V sync for downstream use).

Interestingly tiling.py already documents both mappings and has causal_temporal support — decode_tiled just passes causal_temporal=False for Wan2.1, which contradicts the reference.

Fix

Mirrors the existing encode path (which already does cached chunked processing correctly):

  • Resample: cached upsample3d branch with the 'Rep' first-chunk sentinel, structured like the existing cached downsample3d branch
  • Decoder3d.__call__: accepts feat_cache/feat_idx (mirror of Encoder3d)
  • WanVAE.decode: per-latent-frame chunked decode + _count_decoder_cache_slots (mirror of _count_encoder_cache_slots); peak memory is now flat in T as a side effect
  • decode_tiled: per-tile chunked decode, causal_temporal=True
  • tests: frame-count (T=1→1, 2→5, 5→17) and encode/decode roundtrip

Whole-sequence behavior is preserved when feat_cache=None is passed to the decoder directly.

Verification

Against the reference PyTorch Wan2.1 VAE (Wan2.1_VAE.pth, fp32, CPU), identical latents:

T latents ref frames before after max_abs vs ref
1 1 4 1 2.6e-6
5 17 20 17 6.3e-6

tests/ pass (test_wan_vae.py 80/80 including the new cases; full suite 387 passed — the one failure, test_wan_tiling.py::TestNonCausalTemporal::test_causal_vs_noncausal_output_size, fails identically on current main and is unrelated).

Found while porting SCAIL-2 (Wan2.1-I2V fork) on top of the wan_2 substrate — happy to adjust scope/style.

🤖 Generated with Claude Code

WanVAE.decode ran the decoder over the whole sequence, where upsample3d
temporally doubles every frame: T latents -> 4*T output frames. The
reference Wan2.1 decoder is causal-chunked: the first latent frame is not
temporally upsampled ('Rep' first-chunk sentinel skips time_conv), giving
1+(T-1)*4 frames. Beyond the wrong count, the whole-sequence path produces
different boundary frames (up to 0.21 abs in [-1,1] terms, decaying over
~8 frames) and a +3-frame phase shift versus reference output.

- Resample: add cached upsample3d branch with the 'Rep' sentinel,
  mirroring the existing cached downsample3d branch
- Decoder3d: accept feat_cache/feat_idx (mirror of Encoder3d)
- WanVAE.decode: chunked per-latent-frame decode with
  _count_decoder_cache_slots (mirror of the encode path); memory is now
  flat in T as a side effect
- decode_tiled: per-tile chunked decode; causal_temporal=True (the tiling
  module already documents this mapping)
- tests: frame-count and encode/decode roundtrip coverage

Verified against the reference PyTorch Wan2.1 VAE (Wan2.1_VAE.pth, fp32
CPU): max_abs 2.6e-6 (T=1) / 6.3e-6 (T=5) on every output frame.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant