Repository navigation
Conversation
There was a problem hiding this comment.
Code Review
This pull request optimizes memory usage and performance for the Wan model by replacing eager nnx.vmap with a jitted, scanned, and batch-sharded implementation (_scanned_compute_kv) for cross-attention KV cache computation. It also introduces dynamic vae_spatial mesh extent selection in the VAE pipeline to prevent OOMs and improve decoding speed. A critical issue was identified in the JIT compilation of _scanned_compute_kv, where blocks_graphdef must be marked as a static argument to avoid a JAX TypeError since it is not a valid JAX PyTree.
Perseus14
force-pushed
the
perf/wan-batch-sharded-kv-cache-vae-mesh
branch
from
October 25, 2026 20:23
2a78845 to
182357d
Compare
Two memory fixes for Wan 2.2 T2V at per-device batch >= 1, motivated by a
B=1..16 batch-scaling sweep at 720p/81f/40 steps on v6e-8 and tpu7x-8.
1. Cross-attention KV cache (transformer_wan.py)
compute_kv_cache ran an eager nnx.vmap over all 40 layers on replicated
prompt embeds, so every device materialised the full CFG-doubled cache
plus the FP32 RMSNorm temporaries for every layer at once. It is now one
jitted executable that pins the batch axis to ('data', 'fsdp') on input
and output and scans the layers one at a time. The cross-attn consumer
already re-constrains K/V to that layout, so nothing is resharded at use
time; each device just holds its own B_local slice.
If the batch is not divisible by data * fsdp (e.g. data_parallelism > 1
with per_device_batch_size = 1 and CFG off, the case the pipelines
already tolerate for the CFG cache) the batch constraint is skipped and
the cache stays replicated as before, rather than raising inside the jit.
Sharding the 512-token text axis on 'context' as well was tried and
rejected: XLA hoists the per-layer all-gather out of the layer scan, so
the forward program's reservation grew by 5.8 GiB (net loss on v6e-8).
2. VAE mesh (wan_pipeline.py), T2V pipelines only
With vae_spatial=-1 (auto) the VAE mesh is now ('redundant', 'vae_spatial')
= (N, 1) when the global batch is a multiple of the device count N, i.e.
one video per device with no cross-chip collectives, and (1, N) otherwise
(unchanged). Mixed meshes were measured and are not a win: at B=2 on
v6e-8 a (2, 4) mesh needed a 21.2 GiB decode program vs 15.5 GiB for
(1, 8). An explicit vae_spatial still overrides.
The batch-aware choice is gated by a new batch_parallel_vae argument on
_create_common_components, enabled only by wan_pipeline_2_1 / 2_2. The
VAE hard-constrains activations to P('redundant', ..., 'vae_spatial', ...),
so a (N, 1) mesh requires every tensor through the VAE to have a batch
divisible by N. That holds for T2V (only the final full-batch decode) but
not for VACE (reference images encoded one at a time, [1, 1, H, W, C]) or
the i2v / animate conditioning encodes; those keep the (1, N) mesh.
Tests: new tests/wan/wan_vae_mesh_test.py (CPU, 8 host devices) covers the
_auto_vae_spatial table for B in {0..16} gated and ungated, a guard that only
the T2V pipelines set the gate, and _batch_sharding /
_shard_kv_cache_on_batch layouts including the non-divisible fallback.
Measured on main (DP=2, CP=4 unless noted, use_kv_cache=true):
tpu7x-8 B=2 peak HBM 27.49 -> 16.65 GiB, denoise 270.5 -> 269.8 s
tpu7x-8 B=8 peak HBM 68.12 -> 24.32 GiB, VAE decode 41.6 -> 6.3 s,
denoise 897.8 -> 893.4 s
v6e-8 B=8 (DP=1, FSDP=2, CP=4, vae_decode_chunk=1) main OOMs in
compute_kv_cache (6.25 GiB alloc, 4.99 GiB free); with this
change it completes: peak 17.41 GiB, denoise 1302.6 s,
VAE decode 4.8 s.
Perseus14
force-pushed
the
perf/wan-batch-sharded-kv-cache-vae-mesh
branch
from
October 25, 2026 23:18
182357d to
98ea89f
Compare
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.
Two memory fixes for Wan 2.2 T2V at per-device batch >= 1, from a B=1..16 batch-scaling sweep at 720p / 81 frames / 40 steps on v6e-8 and tpu7x-8.
1. Cross-attention KV cache is batch-sharded and scanned (
transformer_wan.py)compute_kv_cacheran an eagernnx.vmapover all 40 layers on replicated prompt embeds, so every device materialised the full CFG-doubled cache plus the FP32 RMSNorm temporaries for every layer at once (~6.8 GiB per video per device at 720p). It is now a single jitted executable that pins the batch axis to('data', 'fsdp')on input and output andnnx.scans the layers one at a time. The cross-attention consumer already re-constrains K/V to(BATCH, CROSS_ATTN_HEAD, None, D_KV), so nothing is resharded at use time — each device simply holds its ownB_localslice.If the batch is not divisible by
data * fsdp(e.g.data_parallelism > 1withper_device_batch_size = 1and CFG off — the same case the pipelines already tolerate for the CFG cache), the batch constraint is skipped and the cache stays replicated, as before, instead of raising inside the jit.2.
vae_spatial=-1picks a batch-aware VAE mesh for T2V (wan_pipeline.py)The VAE mesh is
('redundant', 'vae_spatial'); batch is sharded onredundant, width / conv channels onvae_spatial. With the old auto value (vae_spatial = N) the batch axis is fully replicated, so atB = Nevery chip holds all N videos' activations and the causal conv stack pays ~10x (and OOMs on 32 GiB parts). Auto now resolves to(N, 1)— one video per device, no cross-chip collectives — when the global batch is a multiple of the device count, and to the unchanged(1, N)otherwise. Mixed meshes were measured and are not a win (at B=2 on v6e-8,(2, 4)needed a 21.2 GiB decode program vs 15.5 GiB for(1, 8)). An explicitvae_spatialstill overrides.Scope: the batch-aware choice is gated by a new
batch_parallel_vaeargument on_create_common_components, enabled only by the T2V pipelines (wan_pipeline_2_1,wan_pipeline_2_2). The VAE hard-constrains every activation toP('redundant', …, 'vae_spatial', …), so a(N, 1)mesh requires every tensor that goes through the VAE to have a batch divisible by N. That holds for T2V (the only VAE call is the final full-batch decode) but not for VACE (reference images are encoded one at a time,[1, 1, H, W, C]) or the i2v / animate conditioning encodes. Those pipelines keep the previous(1, N)mesh, so nothing changes for them.Measurements (on
main;DP=2, CP=4,use_kv_cache=trueunless noted)DP=1, FSDP=2, CP=4,vae_decode_chunk=1)compute_kv_cache(6.25 GiB alloc, 4.99 GiB free)Denoise time is unchanged within noise; the wins are −64% peak HBM and 6.6x faster VAE decode at B=8 on tpu7x-8, and B=8 with the KV cache becoming possible at all on v6e-8.
Validation
DP=2, CP=4): scanned cache output isP(None, 'data')withB_localshards and matches a per-layer eager reference to 1e-5.tests/wan/wan_vae_mesh_test.py(CPU, 8 host devices):_auto_vae_spatialtable for B in {0,1,2,3,4,8,12,16} with the gate on and off, a guard that only the T2V pipelines setbatch_parallel_vae=True, and_batch_sharding/_shard_kv_cache_on_batchlayout checks including the non-divisible-batch fallback.wan_kv_cache_test.pyis TPU-only (attention=flash); not runnable on CPU before or after this change.pyinkclean. Note: the Linter workflow is currently red onmainfor two unrelated files; style: fix pyink formatting on main (2 files) #503 fixes that.