Skip to content

Commit 406593e

Browse files
committed
feat(attention): 2D Ulysses+Ring attention with Global Virtual K-Centering
Implements 2D Ulysses + Ring distributed attention with exact fixed-m accumulation: - Global Virtual K-Centering: Computes key mean on real tokens and reduces across ring axis with jax.lax.pmean - Cross-Ring Safety Fallback: Collective jax.lax.pmin reduction on v_ok across all ring ranks - Hoists Q * log2(e) before Ulysses All-to-All to fuse into Q producer - Slices ragged KV tail when R=1, eliminating dead sequence padding - Fused RMSNorm + RoPE producer with exact bit-identical nnx.RMSNorm associativity - Strict topology and configuration validation guards
1 parent 6886a22 commit 406593e

12 files changed

Lines changed: 2085 additions & 378 deletions

‎src/maxdiffusion/configs/base_wan_27b.yml‎

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ svg_low_noise_density: -1.0
103103
# {"block_q":3328,"block_kv":2816,"block_kv_compute":256,
104104
# "block_kv_compute_in":256,"heads_per_tile":1,"vmem_limit_bytes":67108864}.
105105
svg_flash_block_sizes: {}
106-
attention: 'flash' # Supported attention: dot_product, flash, tokamax_flash, cudnn_flash_te, ring, tokamax_ring, tokamax_ring_custom, ulysses, ulysses_custom, ulysses_ring, ulysses_ring_custom, ulysses_ring_custom_bidir
106+
attention: 'flash' # Supported attention: dot_product, flash, tokamax_flash, cudnn_flash_te, ring, tokamax_ring, tokamax_ring_custom, ulysses, ulysses_custom, ulysses_custom_fixed_m, ulysses_custom_fixed_m_per_q_block, ulysses_ring, ulysses_ring_custom, ulysses_ring_custom_fixed_m, ulysses_ring_custom_fixed_m_per_q_block, ulysses_ring_custom_bidir
107107

108108

109109
#
@@ -112,9 +112,19 @@ attention: 'flash' # Supported attention: dot_product, flash, tokamax_flash, cud
112112
# CP4 (v7x-8): ulysses_shards=2 (R=2), BQ=9472
113113
# CP8 (v7x-8): ulysses_shards=4 (R=2), BQ=9472
114114
# CP16 (v7x-16): ulysses_shards=8 (R=2), BQ=9472
115+
#
116+
# WARNING: ulysses_shards only has an effect on the *ring* variants.
117+
# - Setting U == CP gives R=1, which is a DEGENERATE ring: no KV is rotated
118+
# and the result is mathematically identical to the non-ring
119+
# ulysses_custom* kernel. Such a run must not be reported as a ring result.
120+
# The attention layer logs a warning when this happens.
121+
# - The non-ring ulysses/ulysses_custom* kernels always use the full context
122+
# axis, so their Ulysses degree is fixed at CP. Passing a different
123+
# ulysses_shards to them now raises instead of being silently ignored.
115124
use_base2_exp: True
116125
use_experimental_scheduler: True
117-
# For attention=ulysses_ring, hidden Ulysses shard count; ring shards are context / this.
126+
use_k_centering: "auto" # auto: on for non-ring ulysses_custom_fixed_m* (free, virtual), off for ring paths (pmean + HBM copy)
127+
# For attention=ulysses_ring*, hidden Ulysses shard count; ring shards are context / this.
118128
ulysses_shards: -1
119129
# Splits Ulysses all-to-all into head-group chunks. The last chunk carries any remainder.
120130
# For communication-compute overlap to be effective, enable the following XLA flags:

‎src/maxdiffusion/kernels/custom_splash_attention.py‎

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -849,10 +849,8 @@ def v_index_map(h, i, j, *_):
849849
)
850850
kernel_args = (mk, q, k, v)
851851
else:
852-
if k_mean.shape[0] == num_q_heads and num_q_heads != num_kv_heads:
853-
k_mean = k_mean[::q_heads_per_kv_head]
854-
if k_mean.shape[0] < num_kv_heads or k_mean.shape[1] != head_dim_qk:
855-
raise ValueError(f"k_mean must have shape (>={num_kv_heads}, {head_dim_qk}), got {k_mean.shape}")
852+
if k_mean.shape != (num_kv_heads, head_dim_qk):
853+
raise ValueError(f"k_mean must have shape ({num_kv_heads}, {head_dim_qk}) indexed by KV head, got {k_mean.shape}")
856854
pad_h = (NUM_SUBLANES - (k_mean.shape[0] % NUM_SUBLANES)) % NUM_SUBLANES
857855
if pad_h > 0:
858856
k_mean = jnp.pad(k_mean, ((0, pad_h), (0, 0)))
@@ -1024,10 +1022,8 @@ def v_index_map(h, i, j, *_):
10241022
)
10251023
kernel_args = (mk, q, k, v)
10261024
else:
1027-
if k_mean.shape[0] == num_q_heads and num_q_heads != num_kv_heads:
1028-
k_mean = k_mean[::q_heads_per_kv_head]
1029-
if k_mean.shape[0] < num_kv_heads or k_mean.shape[1] != head_dim_qk:
1030-
raise ValueError(f"k_mean must have shape (>={num_kv_heads}, {head_dim_qk}), got {k_mean.shape}")
1025+
if k_mean.shape != (num_kv_heads, head_dim_qk):
1026+
raise ValueError(f"k_mean must have shape ({num_kv_heads}, {head_dim_qk}) indexed by KV head, got {k_mean.shape}")
10311027
pad_h = (NUM_SUBLANES - (k_mean.shape[0] % NUM_SUBLANES)) % NUM_SUBLANES
10321028
if pad_h > 0:
10331029
k_mean = jnp.pad(k_mean, ((0, pad_h), (0, 0)))
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
"""
2+
Copyright 2026 Google LLC
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
https://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
"""
16+
17+
"""Optimized fused producers for Wan Attention."""
18+
19+
from typing import Tuple
20+
21+
import jax
22+
import jax.numpy as jnp
23+
24+
25+
def fused_rmsnorm_rope(
26+
raw_q: jax.Array,
27+
raw_k: jax.Array,
28+
q_norm_scale: jax.Array,
29+
k_norm_scale: jax.Array,
30+
freqs_cis: jax.Array,
31+
q_heads: int = 40,
32+
kv_heads: int | None = None,
33+
dim_head: int = 128,
34+
eps: float = 1e-6,
35+
heads: int | None = None,
36+
k_eps: float | None = None,
37+
) -> Tuple[jax.Array, jax.Array]:
38+
"""Fusion-friendly FP32 RMSNorm + BF16 RoPE + Head Transposition producer.
39+
40+
Performs FP32 RMSNorm normalization for maximum stability, casts to input dtype
41+
(e.g. BF16), and applies RoPE rotation and head transposition in BF16 precision,
42+
avoiding excess FP32 VPU/VMEM cycles on long sequence lengths. Fully supports GQA
43+
where q_heads != kv_heads.
44+
45+
Args:
46+
raw_q: Raw query projection of shape [B, Sq, Dq] (where Dq = q_heads * dim_head).
47+
raw_k: Raw key projection of shape [B, Sk, Dk] (where Dk = kv_heads * dim_head).
48+
q_norm_scale: RMSNorm scale parameter for query of shape [Dq].
49+
k_norm_scale: RMSNorm scale parameter for key of shape [Dk].
50+
freqs_cis: Complex rotary embedding tensor of shape [1, 1, S, dim_head // 2].
51+
q_heads: Number of query attention heads.
52+
kv_heads: Number of key/value attention heads (defaults to q_heads for MHA).
53+
dim_head: Dimension of each attention head.
54+
eps: Epsilon for query RMSNorm numerical stability (and key if k_eps is None).
55+
heads: Deprecated alias for q_heads.
56+
k_eps: Optional separate epsilon for key RMSNorm numerical stability.
57+
58+
Returns:
59+
Transposed and RoPE-rotated (q_out, k_out) of shapes [B, q_heads, Sq, dim_head]
60+
and [B, kv_heads, Sk, dim_head].
61+
"""
62+
if heads is not None:
63+
q_heads = heads
64+
kv_heads = q_heads if kv_heads is None else kv_heads
65+
effective_k_eps = eps if k_eps is None else k_eps
66+
B, Sq, Dq = raw_q.shape
67+
_, Sk, Dk = raw_k.shape
68+
69+
if Dq != q_heads * dim_head:
70+
raise ValueError(f"raw_q feature dim ({Dq}) must equal q_heads ({q_heads}) * dim_head ({dim_head})")
71+
if Dk != kv_heads * dim_head:
72+
raise ValueError(f"raw_k feature dim ({Dk}) must equal kv_heads ({kv_heads}) * dim_head ({dim_head})")
73+
74+
# 1. FP32 RMSNorm for stability, then cast directly to target activation dtype.
75+
#
76+
# Association matters: Flax's `_normalize` computes `mul = rsqrt(var + eps)`,
77+
# then `mul *= scale`, then `y = x * mul` -- i.e. x * (rsqrt * scale). Folding
78+
# left-to-right as (x * rsqrt) * scale rounds differently and makes this path
79+
# drift from `nnx.RMSNorm` bit-for-bit. Keep the parenthesisation below in
80+
# step with Flax so the fused producer stays a pure fusion, not a numerical
81+
# change.
82+
q_fp32 = raw_q.astype(jnp.float32)
83+
q_rms = jax.lax.rsqrt(jnp.mean(jnp.square(q_fp32), axis=-1, keepdims=True) + eps)
84+
q_norm = (q_fp32 * (q_rms * q_norm_scale.astype(jnp.float32))).astype(raw_q.dtype)
85+
86+
k_fp32 = raw_k.astype(jnp.float32)
87+
k_rms = jax.lax.rsqrt(jnp.mean(jnp.square(k_fp32), axis=-1, keepdims=True) + effective_k_eps)
88+
k_norm = (k_fp32 * (k_rms * k_norm_scale.astype(jnp.float32))).astype(raw_k.dtype)
89+
90+
# 2. Reshape and transpose to [B, heads, S, dim_head]
91+
q_h = q_norm.reshape(B, Sq, q_heads, dim_head).transpose(0, 2, 1, 3)
92+
k_h = k_norm.reshape(B, Sk, kv_heads, dim_head).transpose(0, 2, 1, 3)
93+
94+
# 3. Direct RoPE with freqs_cis [1, 1, S, dim_head // 2] in input dtype
95+
cos = jnp.real(freqs_cis).astype(raw_q.dtype)
96+
sin = jnp.imag(freqs_cis).astype(raw_q.dtype)
97+
cos_q, sin_q = cos[:, :, :Sq, :], sin[:, :, :Sq, :]
98+
cos_k, sin_k = cos[:, :, :Sk, :], sin[:, :, :Sk, :]
99+
100+
q_pairs = q_h.reshape(B, q_heads, Sq, -1, 2)
101+
q_0, q_1 = q_pairs[..., 0], q_pairs[..., 1]
102+
q_out_0 = q_0 * cos_q - q_1 * sin_q
103+
q_out_1 = q_0 * sin_q + q_1 * cos_q
104+
q_out = jnp.stack([q_out_0, q_out_1], axis=-1).reshape(B, q_heads, Sq, dim_head)
105+
106+
k_pairs = k_h.reshape(B, kv_heads, Sk, -1, 2)
107+
k_0, k_1 = k_pairs[..., 0], k_pairs[..., 1]
108+
k_out_0 = k_0 * cos_k - k_1 * sin_k
109+
k_out_1 = k_0 * sin_k + k_1 * cos_k
110+
k_out = jnp.stack([k_out_0, k_out_1], axis=-1).reshape(B, kv_heads, Sk, dim_head)
111+
112+
return q_out, k_out

0 commit comments

Comments
 (0)