Repository navigation
Conversation
There was a problem hiding this comment.
Code Review
This pull request refactors the spatial self-attention block in both autoencoder_kl_wan.py and autoencoder_kl_wan_2p2.py to simplify the reshaping and splitting of the QKV projections before passing them to jax.nn.dot_product_attention. Additionally, a new test test_wan_attention_matches_pytorch has been added to wan_vae_test.py along with a PyTorch reference implementation TorchWanAttentionBlock to verify that the JAX attention blocks produce equivalent outputs to PyTorch. There are no review comments to address, and I have no feedback to provide.
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.
Summary
WanAttentionBlock(the self-attention in the mid-block of the Wan VAE encoder and decoder) builds q, k and v in the PyTorch SDPA layout(batch, heads, seq, dim)and passes them tojax.nn.dot_product_attention, which takes(batch, seq, heads, dim). JAX therefore sees a sequence of length 1 with HW heads. Softmax over a single key is 1, so the attention output is exactlyv, and the block computesproj(v) + xinstead of attending over the HW positions of the frame.This PR reshapes q, k and v to
(N*T, H*W, 1, C)(one head over the spatial positions), which is what the PyTorchAutoencoderKLWancomputes. With the fix, the MaxDiffusion VAE matches diffusers to within float32 rounding on real Wan2.1 weights (max abs diff 2e-5 or less).AutoencoderKLWanis the VAE used by the Wan 2.1 and Wan 2.2 A14B pipelines, so this affects every encode and decode there.autoencoder_kl_wan_2p2.pyhas the same block; it is not imported anywhere at the moment, and I fixed it too so the two copies stay consistent.Issue
In
WanAttentionBlock.__call__:Minimal check on CPU with random weights:
main prints
0.0, this PR prints2.41.The VAE weights are trained with the attention in place, so this changes real outputs. I compared the MaxDiffusion Wan2.1 VAE (
Wan-AI/Wan2.1-T2V-1.3B-Diffusers, loaded withload_wan_vae) against diffusersAutoencoderKLWanwith the same weights, both in float32 on CPU, on the first 9 frames of the publichiker.mp4test video at 256x448 (script below):I also saw it end to end, with a separate JAX implementation of the same decoder that I could switch between the two attention behaviours (so these numbers are not from this PR's code). For a Wan2.2 I2V-A14B clip (832x464, 81 frames) generated on a TPU v6e and compared with the PyTorch pipeline on a GPU from the same initial noise, fixing only the decoder attention raised the mean PSNR vs the GPU output from 25.65 to 25.96 dB, and frame 0 from 36.4 to 38.7 dB.
Changes
models/wan/autoencoder_kl_wan.py:WanAttentionBlockreshapesqkvto(N*T, H*W, 1, 3C), splits on the channel axis and passes the result tojax.nn.dot_product_attentiondirectly. The four transposes go away.models/wan/autoencoder_kl_wan_2p2.py: the same fix in its copy ofWanAttentionBlock.tests/wan/wan_vae_test.py: addsTorchWanAttentionBlock, a PyTorch reference written the same way as the diffusers block (like the existingTorchWanRMS_norm), andtest_wan_attention_matches_pytorch, which copies its weights into both JAX blocks and compares outputs on a small random input (atol/rtol 1e-5). On main both sub-tests fail (max abs diff 1.6).The block now does the work it is meant to do, so it costs more than before. With the default
vae_encode_chunk: 4andvae_decode_chunk: 1the mid-block sees one latent frame per call. At 720p that is 90x160 = 14,400 positions, so the XLA attention path holds at least a 14,400 x 14,400 float32 score matrix (about 0.8 GB) per latent frame in the call;vae_encode_chunk: -1puts all latent frames in one call and scales this up. Withvae_spatialsharding, XLA gathers qkv and every device computes the full attention (I checked the compiled HLO on 8 host CPU devices: one all-gather, the same count as on main, and the sharded output matches the unsharded one to 5e-7). Constraining q to be sharded along the sequence would split that work; I kept this change minimal. In the separate implementation above, the samejax.nn.dot_product_attentioncall added about 0.02 s to a whole 81-frame 480p decode on a TPU v6e.Testing
python -m pytest src/maxdiffusion/tests/wan/wan_vae_test.py -k attention(CPU, jax 0.11.2, flax 0.12.10, torch 2.10.0): 2 passed with this PR. On main,test_wan_attention_matches_pytorchfails for both blocks.python -m pytest src/maxdiffusion/tests/wan/wan_vae_test.pyon CPU, withouttest_load_checkpoint(which downloads the 14B VAE): 12 passed, including the full-size encode and decode tests.pyink --check --pyink-indentation=2 --line-length=125on the three files: unchanged.pylint --fail-under=7 src/maxdiffusion/: passes, no new messages in the touched files.ruff check .: passes.Comparison script against diffusers (CPU, float32)