Repository navigation
perf(attention): shard-major Ulysses output all-to-all (wan_ulysses_out_a2a="chunked"/"shard_major") - #511
perf(attention): shard-major Ulysses output all-to-all (wan_ulysses_out_a2a="chunked"/"shard_major")#511Perseus14 wants to merge 1 commit into
Conversation
There was a problem hiding this comment.
Code Review
This pull request introduces the wan_ulysses_out_a2a configuration option (supporting "flat", "chunked", and "shard_major" modes) to optimize the inverse Ulysses exchange and avoid expensive post-kernel relayout copies on TPUs. This is achieved by updating the custom splash and ring attention kernels to support 4D shard-major query inputs and output sharding, alongside adding a new test suite to verify correctness. The review feedback highlights a critical bug in _ulysses_ring_custom_attention where use_shard_major fails to check for transpose_out, potentially causing layout mismatches. Additionally, the reviewer suggests using the more idiomatic jnp.square instead of ** 2 in JAX operations and wrapping a long line in ring_attention_kernel.py to comply with PEP 8 line length guidelines.
2cabbb8 to
f12639a
Compare
f12639a to
86f71b7
Compare
86f71b7 to
df7da19
Compare
mbohlool
left a comment
There was a problem hiding this comment.
The main asks: a requested shard_major should not silently run flat, and CI should run the single-device kernel test.
df7da19 to
808d7bd
Compare
808d7bd to
01c2d43
Compare
6f8ec7d to
5b4eddb
Compare
5b4eddb to
52ec396
Compare
208901a to
09d9dab
Compare
09d9dab to
84a68e3
Compare
84a68e3 to
d98f3ab
Compare
…ut_a2a="chunked"/"shard_major")
Third of four pieces split out of the Wan fast-inference optimizations PR.
The switch defaults off ("flat") in the Wan YAMLs and in wan_runtime_options;
it is a plain config key (`"flat"` | `"chunked"` | `"shard_major"`) plumbed to
FlaxWanAttention / NNXAttentionOp / AttentionOp and down to the Ulysses
kernels through the dispatch context.
- `_ulysses_seq_to_heads` is the inverse Ulysses exchange (attention output,
head-sharded -> sequence-sharded), and validates `seq_len % num_shards == 0`
up front for all modes. `"flat"` is the previous
`all_to_all(split_axis=seq, concat_axis=1)`; XLA reorders the whole output
so the sequence axis is major before the collective and back again after,
four relayout copies per layer at the 720p shard shape (~1.5 ms on v6e,
~0.7 ms on tpu7x). `"chunked"` / `"shard_major"` uses a single
layout-preserving `all_to_all` over a reshaped `(num_shards, chunk)` leading
axis, so no relayout copies are inserted (also wired into the non-custom
`attention="ulysses"` branch). Same data movement, same values.
- With the custom splash kernel (`S_local % block_q == 0`, `D == 128`,
`heads_per_tile == 1`), the forward Q exchange also stays shard-major (4-D q,
`[B, U, H/U, S_local, D]`) and the kernel writes the shard-major output
directly (`out_num_shards`), on both the 1D Ulysses path (v6e) and the 2D
Ulysses+Ring path (tpu7x): `custom_splash_attention.py` gains a 4-D q
BlockSpec and a 4-D shard-major output for `_splash_attention_forward` and
`_splash_attention_forward_ring`; `ring_attention_kernel.py` threads
`out_num_shards` through `_custom_ring_attention_forward` /
`make_custom_ring_attention` and sizes the o/l/m accumulators accordingly
(`save_residuals` with `out_num_shards > 1` raises `NotImplementedError`:
the lse contract stays `(num_q_heads, orig_q_seq_len)`).
`_compute_fixed_m_metadata` accepts the 5-D shard-major query.
- `"shard_major"` vs `"chunked"`: `"shard_major"` is the strict mode that
requires the 4-D `q` / `out_num_shards` Pallas kernel path and raises
`ValueError` when any gate precondition (`use_custom_kernel`,
`S_local % block_q == 0`, `D == 128`, `heads_per_tile == 1`,
`not transpose_out`, `attention_mask is None`) fails; `"chunked"` is the
lenient mode that takes the 4-D kernel path when eligible and falls back to
the chunked inverse `all_to_all` in `_ulysses_seq_to_heads` otherwise.
- Reachability: at 720p/81f with CP=4 the unpadded per-device sequence is
18,900 tokens, which no launcher `block_q` divides (v6e `BQ=9472`, v7
`BQ=6400` at this PR), so at this PR the launcher's `v6e`/`v7` profiles set
`DEFAULT_ULYSSES_OUT_A2A=chunked`; once `feat/wan-fast-inference-optimizations`
pads the sequence to 18,944 (`wan_seq_pad=lane`) and sets v7 `BQ=4736`
(`9472 * 2 = 18,944` on v6e, `4736 * 4 = 18,944` on v7), it switches both
profiles to `DEFAULT_ULYSSES_OUT_A2A=shard_major`.
Tests
- `ulysses_out_a2a_test.py` (8): flat vs chunked vs shard_major parity of the
inverse exchange, the mode guard, the `seq_len % num_shards != 0` guard, the
`save_residuals + shard-major` guard, the strict `"shard_major"` kernel-gate
`ValueError` guard, the 4-D q ring kernel bit-exact vs 3-D (running in CI via
`interpret=True`), and bit-exact wrapper output for the shard-major 1D Ulysses
and 2D Ulysses+Ring paths vs the flat layout (spy-asserting
`out_num_shards == U`).
- `wan_runtime_options_test.py` (12): the new switch's YAML default and enum
validation.
- `end_to_end/tpu/run_wan_stack_tests.sh`: adds `ulysses_out_a2a_test.py`.
d98f3ab to
da01a58
Compare
Stacked on
feat/wan-token-patch-embed. Third of four pieces split out of #491. Addswan_ulysses_out_a2a: "chunked" | "shard_major"(default"flat"):_ulysses_seq_to_headsperforms the inverse Ulysses exchange as a single layout-preservingall_to_allover a reshaped(num_shards, chunk)leading axis, so XLA stops inserting the four relayout copies per layer that the flatsplit_axis=seqform costs at the 720p shard shape (~1.5 ms on v6e, ~0.7 ms on tpu7x). Same data movement, same values. With the custom splash kernel (S_local % block_q == 0,D == 128,heads_per_tile == 1) the forward Q exchange also stays shard-major (4-Dq) and the kernel writes the shard-major output directly (out_num_shards), on both the 1D Ulysses path (v6e) and the 2D Ulysses+Ring path (tpu7x):custom_splash_attention.pygains a 4-DqBlockSpec/ 4-D output,ring_attention_kernel.pythreadsout_num_shardsthrough the ring accumulators (save_residuals=Truewithout_num_shards > 1raisesNotImplementedError), and_compute_fixed_m_metadataaccepts the 5-D shard-major query.Reachability note: The Pallas shard-major Q/out path requires
S_local % block_q == 0. At 720p/81f withCP=4the per-device sequence is 18,900 tokens, which no launcherblock_qdivides (BQ=9472on v6e,BQ=6400on v7 at this PR), so on this PR alone the launcher'schunkedsetting only changes the inverse all-to-all; the 4-Dq/out_num_shardsPallas kernel path engages once#491(feat/wan-fast-inference-optimizations) pads the sequence to 18,944 (wan_seq_pad=lane) and setsBQ=4736(4736 × 4 = 18,944).E2E latency (TPU v6e-8, Wan 2.2 T2V-A14B, 720p / 81 frames / 40 steps,
CP=4, DP=2,wan_ulysses_out_a2a="chunked", DVFS unpinned): 128.9 s generate / 126.7 s denoise (29.6 s compile), down from 130.6 s generate / 128.5 s denoise on #510 (−1.7 s generate / −1.8 s denoise end-to-end from the layout-preserving inverse Ulysses all-to-all alone).Tests:
ulysses_out_a2a_test.py(6, new; flat vs chunked vs shard_major parity, mode guard,save_residuals+ shard-major guard, bit-exact 4-D ring kernel output, and bit-exact wrapper output on the 1D and 2D paths asserting via spy thatout_num_shards == U),wan_runtime_options_test.py(+enum validation).Stack: #477 → #507 → #478 → #508 → #479 → #488 → #509 → #510 → #511 → #491