|
| 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