Skip to content

perf(wan): XLA-fused RMSNorm + RoPE producer for self-attention Q/K - #508

Open
Perseus14 wants to merge 1 commit into
feat/ring-attentionfrom
feat/fused-rmsnorm-rope-xla
Open

Perseus14 wants to merge 1 commit into
feat/ring-attentionfrom
feat/fused-rmsnorm-rope-xla

Conversation

@Perseus14

@Perseus14 Perseus14 commented Oct 15, 2026 •

Copy link
Copy Markdown
Collaborator

Stacked on #478 (feat/ring-attention). Adds fused_rmsnorm_rope (kernels/fused_producers.py), an XLA-level fused FP32-RMSNorm + RoPE + head-transpose producer for the self-attention Q/K chain, and uses it in FlaxWanAttention.__call__ when rotary_emb is given, qk_norm is on, and the call is self-attention (cross-attention and the no-RoPE path keep the separate norm_q/norm_k/_apply_rope steps). It mirrors nnx.RMSNorm's association (x * (rsqrt(var + eps) * scale)) — bit-for-bit in float32 and for bfloat16 activations with a bfloat16 scale, and within 1 bfloat16 ULP when raw_q is bfloat16 and scale is float32 (where nnx.RMSNorm.promote_dtype rounds the scale to bfloat16 first while the fused producer keeps it in float32) — takes separate q/k epsilons, and accepts kv_heads for GQA. It is jax.jit-wrapped (heads/dim/eps static): inlined under the pipeline's outer jit, but when a full-size block is called eagerly (as wan_vace_transformer_test does at 75,600 tokens) XLA fuses the chain instead of materialising ~1.5 GB of intermediates, which OOMed that test on the v4-8 CI runner. #488's Pallas _fused_rope_producer builds on this XLA producer.

E2E latency (TPU v6e-8, Wan 2.2 T2V-A14B, 720p / 81 frames / 40 steps, CP=4, DP=2, 1D Ulysses U=4, DVFS unpinned): 138.2 s generate / 136.0 s denoise, 67.1 s compile (neutral step time under full-block XLA fusion vs #478's 138.3 s / 136.1 s; eliminates the ~1.5 GB eager intermediate allocation).

Tests: fused_producers_test.py (8, new, CPU), added to run_wan_stack_tests.sh.

Stack: #477 → #507 → #478 → #508 → #479 → #488 → #509 → #510 → #511 → #491

@github-actions

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces an optimized fused_rmsnorm_rope producer for Wan Attention to perform FP32 RMSNorm, cast to input dtype, and apply RoPE rotation and head transposition, along with corresponding unit tests. Feedback on the changes highlights a critical issue where accessing self.norm_q.scale[...] directly will raise a TypeError since Flax NNX Param objects are not subscriptable, recommending the use of .value instead. Additionally, it is suggested to cast the sliced rotary embedding tensors (cos and sin) to their respective query and key dtypes rather than globally casting to raw_q.dtype to avoid potential precision loss in mixed-precision configurations.

Comment thread src/maxdiffusion/models/attention_flax.py
Comment thread src/maxdiffusion/kernels/fused_producers.py Outdated
Adds `fused_rmsnorm_rope` (kernels/fused_producers.py) and uses it in
`FlaxWanAttention.__call__` for the self-attention Q/K producer chain.

What
- `fused_rmsnorm_rope(raw_q, raw_k, q_scale, k_scale, freqs_cis, ...)` does
  FP32 RMSNorm, casts back to the input dtype, then applies RoPE and the
  head transposition in input dtype, so no FP32 RoPE intermediates are
  materialised at long sequence lengths. It mirrors nnx.RMSNorm's association
  (`x * (rsqrt(var + eps) * scale)`): bit-for-bit in f32, and in bf16 when the
  norm scale is bf16. With bf16 activations and an f32 scale, nnx.RMSNorm's
  `promote_dtype` rounds the scale to bf16 before folding it into the rsqrt,
  while the fused producer keeps the scale in f32 and rounds only the final
  product; the two then differ by at most one bf16 ulp. It takes separate q/k
  epsilons and accepts an optional `kv_heads` for GQA (defaults to `q_heads`).
- It is `jax.jit`-wrapped with heads/dim/eps static: under the pipeline's
  outer jit it is simply inlined, but when a full-size block is called
  eagerly (as wan_vace_transformer_test does at 75,600 tokens) XLA fuses the
  chain instead of materialising ~1.5 GB of extra intermediates, which OOMed
  that test on the v4-8 CI runner.
- `FlaxWanAttention` takes the fused path when `rotary_emb` is given,
  `qk_norm` is on and the call is self-attention; cross-attention and the
  no-RoPE path keep the separate norm_q / norm_k / `_apply_rope` steps.
  `self.eps` is kept so the fused call can fall back to the layer epsilon
  when a norm module does not expose `epsilon`.

Tests
- fused_producers_test.py (8, new, CPU): bit-for-bit match with
  nnx.RMSNorm in f32 and in bf16 (bf16 scale), the bf16-activation /
  f32-scale case pinned to <= 1 bf16 ulp of nnx.RMSNorm, the reassociated
  form is shown to differ, fused Q matches flax RMSNorm, separate q/k
  epsilons are applied, non-trivial RoPE in bfloat16 matches the unfused
  reference, and GQA shapes/values.
- Added to end_to_end/tpu/run_wan_stack_tests.sh.
Verified (this head): TPU v6e-8 (jax 0.11.2), every file of
end_to_end/tpu/run_wan_stack_tests.sh at this head run directly with unittest:
custom_splash_fixed_m 32, ring_fixed_m 24, dot_fallback_layout 6,
tile_size_grid_search 16, attention_config_guards 20, custom_splash_unpadded 12,
fused_producers 8 -- 118 tests, all OK.
@Perseus14
Perseus14 force-pushed the feat/fused-rmsnorm-rope-xla branch from 519699f to 36b073d Compare October 15, 2026 19:20
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