2250 lines
85 KiB
Python
2250 lines
85 KiB
Python
"""Fused-BiGDN Triton kernels used by SANA-WM GDN attention blocks.
|
|
|
|
Includes the unified forward kernel, backward kernels, RoPE/RMS helpers, and
|
|
autograd wrappers used by the Triton GDN attention blocks.
|
|
|
|
Precision knob: env var ``FUSED_GDN_PRECISION`` or ``PRECISION_OVERRIDE``:
|
|
0=IEEE fp32 dots, 1=TF32, 2=bf16 TC + fp32 state [default], 3=bf16 TC + bf16 state.
|
|
"""
|
|
|
|
# ruff: noqa: E501
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
|
|
import torch
|
|
import triton
|
|
import triton.language as tl
|
|
|
|
# =====================================================================
|
|
# GPU-adaptive kernel config
|
|
# =====================================================================
|
|
|
|
|
|
def _get_kernel_config() -> dict:
|
|
"""Return optimal kernel parameters for the current GPU.
|
|
|
|
STATE_FP32: use fp32 state_prev when SRAM is large enough.
|
|
- bf16 state_prev: ~96KB total SRAM (fits GB10's 101KB).
|
|
- fp32 state_prev: ~128KB total SRAM (needs H100's 228KB+).
|
|
"""
|
|
if not torch.cuda.is_available():
|
|
return {"BLOCK_S": 64, "num_stages": 1, "num_warps": 4, "STATE_FP32": False}
|
|
smem = torch.cuda.get_device_properties(0).shared_memory_per_multiprocessor
|
|
state_fp32 = smem >= 150 * 1024 # H100 (228KB) yes, GB10 (101KB) no
|
|
return {"BLOCK_S": 64, "num_stages": 1, "num_warps": 8, "STATE_FP32": state_fp32}
|
|
|
|
|
|
_KCFG = None
|
|
|
|
|
|
def _kcfg():
|
|
global _KCFG
|
|
if _KCFG is None:
|
|
_KCFG = _get_kernel_config()
|
|
return _KCFG
|
|
|
|
|
|
# precision=0 → IEEE fp32 dots + fp32 state (DOT_PRECISION=2, STATE_FP32=1)
|
|
# precision=1 → TF32 dots + fp32 state (DOT_PRECISION=1, STATE_FP32=1)
|
|
# precision=2 → bf16 dots + fp32 state (DOT_PRECISION=0, STATE_FP32=1) [default]
|
|
# precision=3 → bf16 dots + bf16 state (DOT_PRECISION=0, STATE_FP32=0)
|
|
def _precision_params(precision: int) -> tuple:
|
|
if precision == 0:
|
|
return 2, True
|
|
elif precision == 1:
|
|
return 1, True
|
|
elif precision == 3:
|
|
return 0, False
|
|
else: # default
|
|
return 0, True
|
|
|
|
|
|
_env_prec = os.environ.get("FUSED_GDN_PRECISION", None)
|
|
PRECISION_OVERRIDE: int | None = int(_env_prec) if _env_prec is not None else None
|
|
|
|
|
|
def _resolve_launch_config() -> tuple:
|
|
"""Returns (prec, dot_prec, state_fp32, num_warps).
|
|
|
|
Uses ``PRECISION_OVERRIDE`` when set; otherwise falls back to ``_kcfg()``
|
|
(which picks ``STATE_FP32`` based on per-GPU SRAM). ``num_warps`` is
|
|
clamped to 4 when dots run on fp32 operands (more registers needed).
|
|
"""
|
|
cfg = _kcfg()
|
|
prec = PRECISION_OVERRIDE if PRECISION_OVERRIDE is not None else 2
|
|
dot_prec, state_fp32 = _precision_params(prec)
|
|
if PRECISION_OVERRIDE is None:
|
|
state_fp32 = cfg["STATE_FP32"]
|
|
nw = cfg["num_warps"]
|
|
if dot_prec >= 1:
|
|
nw = min(nw, 4)
|
|
return prec, dot_prec, state_fp32, nw
|
|
|
|
|
|
def _prepare_launch(D: int, beta: torch.Tensor, decay: torch.Tensor) -> tuple:
|
|
"""Shared launcher preamble.
|
|
|
|
Returns (BLOCK_D, BLOCK_S, dot_prec, state_fp32, nw, cfg, beta_c, decay_c).
|
|
``beta_c`` / ``decay_c`` are the contiguous copies the kernel needs.
|
|
"""
|
|
BLOCK_D = triton.next_power_of_2(D)
|
|
cfg = _kcfg()
|
|
BLOCK_S = cfg["BLOCK_S"]
|
|
_, dot_prec, state_fp32, nw = _resolve_launch_config()
|
|
return BLOCK_D, BLOCK_S, dot_prec, state_fp32, nw, cfg, beta.contiguous(), decay.contiguous()
|
|
|
|
|
|
# =====================================================================
|
|
# Unified forward Triton Mega-Kernel (inference-only variant)
|
|
# =====================================================================
|
|
# Fuses: RMSNorm + ReLU + k_scale + RoPE + BiGDN recurrence.
|
|
#
|
|
# Inputs:
|
|
# qkv (B, N, 3, H, D) interleaved — strides passed explicitly.
|
|
# beta (B, H, F, S), decay (B, H, F) contiguous.
|
|
# q_norm_w, k_norm_w (H*D,) full-channel — only read when QK_NORM=1.
|
|
# rope_cos, rope_sin (N, D) contiguous.
|
|
# q_inv_rms, k_inv_rms (B, N) full-channel — only read when USE_PRECOMPUTED_RMS=1.
|
|
#
|
|
# Outputs:
|
|
# out (B, N, H, D) = num / (den + eps) — unused by BiGDN wrappers.
|
|
# num (B, N, H, D) = numerator before divide (summed across directions).
|
|
# den (B, H, N) = denominator before divide (summed across directions).
|
|
#
|
|
# NOTE (inference-only build): upstream also supports SAVE_STATE,
|
|
# LOAD_INIT_STATE, SAVE_FINAL_STATE for training backward / state caching.
|
|
# Those constexpr branches are preserved in the kernel so the source stays
|
|
# 1-for-1 with upstream (they compile away when launched with flags=0).
|
|
|
|
|
|
@triton.jit
|
|
def _fused_gdn_kernel(
|
|
# ---- interleaved QKV : (B, N, 3, H, D) ----
|
|
qkv_ptr,
|
|
stride_b: tl.constexpr,
|
|
stride_n: tl.constexpr,
|
|
stride_3: tl.constexpr,
|
|
stride_h: tl.constexpr,
|
|
stride_d: tl.constexpr,
|
|
# ---- gates ----
|
|
beta_ptr,
|
|
decay_ptr,
|
|
# ---- inv-RMS (B, N) — only read when USE_PRECOMPUTED_RMS=1 ----
|
|
q_inv_rms_ptr,
|
|
k_inv_rms_ptr,
|
|
# ---- norm weights (H*D,) full-channel — only read when QK_NORM=1 ----
|
|
q_norm_w_ptr,
|
|
k_norm_w_ptr,
|
|
# ---- RoPE tables (N, D) contiguous ----
|
|
rope_cos_ptr,
|
|
rope_sin_ptr,
|
|
# ---- outputs ----
|
|
out_ptr, # (B, N, H, D)
|
|
num_ptr, # (B, N, H, D)
|
|
den_ptr, # (B, H, N)
|
|
# ---- saved-state dummies (unused in this build but kept for signature parity) ----
|
|
saved_state_ptr,
|
|
saved_z_ptr,
|
|
saved_state_curr_ptr,
|
|
saved_z_curr_ptr,
|
|
init_state_kv_ptr,
|
|
init_state_z_ptr,
|
|
final_state_kv_ptr,
|
|
final_state_z_ptr,
|
|
# ---- scalars / dims ----
|
|
H: tl.constexpr,
|
|
F: tl.constexpr,
|
|
S: tl.constexpr,
|
|
D: tl.constexpr,
|
|
K_SCALE,
|
|
NORM_EPS: tl.constexpr,
|
|
EPS: tl.constexpr,
|
|
QK_NORM: tl.constexpr,
|
|
USE_PRECOMPUTED_RMS: tl.constexpr,
|
|
STATE_FP32: tl.constexpr,
|
|
DOT_PRECISION: tl.constexpr,
|
|
REVERSE: tl.constexpr,
|
|
SAVE_STATE: tl.constexpr,
|
|
LOAD_INIT_STATE: tl.constexpr,
|
|
SAVE_FINAL_STATE: tl.constexpr,
|
|
BLOCK_D: tl.constexpr,
|
|
BLOCK_S: tl.constexpr,
|
|
):
|
|
# ---- dot product precision / operand dtype ----
|
|
if DOT_PRECISION >= 1:
|
|
dot_dtype = tl.float32
|
|
else:
|
|
dot_dtype = tl.bfloat16
|
|
dot_ip: tl.constexpr = "ieee" if DOT_PRECISION == 2 else "tf32"
|
|
|
|
# ---- program → (batch, head) ----
|
|
pid = tl.program_id(0)
|
|
pid_b = pid // H
|
|
pid_h = pid % H
|
|
N = F * S
|
|
bh = pid_b * H + pid_h
|
|
|
|
# ---- base pointers ----
|
|
qkv_bh = qkv_ptr + pid_b * stride_b + pid_h * stride_h
|
|
out_bh = out_ptr + pid_b * (N * H * D) + pid_h * D
|
|
num_bh = num_ptr + pid_b * (N * H * D) + pid_h * D
|
|
den_bh = den_ptr + bh * N
|
|
beta_bh = beta_ptr + bh * (F * S)
|
|
decay_bh = decay_ptr + bh * F
|
|
if SAVE_STATE:
|
|
st_bh = saved_state_ptr + bh * F * BLOCK_D * BLOCK_D
|
|
sz_bh = saved_z_ptr + bh * F * BLOCK_D
|
|
stc_bh = saved_state_curr_ptr + bh * F * BLOCK_D * BLOCK_D
|
|
szc_bh = saved_z_curr_ptr + bh * F * BLOCK_D
|
|
|
|
# ---- D-index helpers ----
|
|
offs_d = tl.arange(0, BLOCK_D)
|
|
mask_d = offs_d < D
|
|
offs_d_pair = offs_d ^ 1
|
|
mask_d_pair = offs_d_pair < D
|
|
D_inv = 1.0 / D
|
|
|
|
# ---- full-channel norm weights (only when QK_NORM=1) ----
|
|
nw_offset = pid_h * D
|
|
if QK_NORM:
|
|
q_nw = tl.load(q_norm_w_ptr + nw_offset + offs_d, mask=mask_d, other=0.0).to(tl.float32)
|
|
k_nw = tl.load(k_norm_w_ptr + nw_offset + offs_d, mask=mask_d, other=0.0).to(tl.float32)
|
|
q_nw_pair = tl.load(q_norm_w_ptr + nw_offset + offs_d_pair, mask=mask_d_pair, other=0.0).to(tl.float32)
|
|
k_nw_pair = tl.load(k_norm_w_ptr + nw_offset + offs_d_pair, mask=mask_d_pair, other=0.0).to(tl.float32)
|
|
|
|
k_scale = K_SCALE
|
|
offs_dd = offs_d[:, None] * BLOCK_D + offs_d[None, :]
|
|
mask_dd = mask_d[:, None] & mask_d[None, :]
|
|
|
|
# ---- double-buffer state ----
|
|
if LOAD_INIT_STATE:
|
|
init_kv_bh = init_state_kv_ptr + bh * BLOCK_D * BLOCK_D
|
|
state_curr = tl.load(init_kv_bh + offs_dd, mask=mask_dd, other=0.0).to(tl.float32)
|
|
init_z_bh = init_state_z_ptr + bh * BLOCK_D
|
|
state_z_curr = tl.load(init_z_bh + offs_d, mask=mask_d, other=0.0).to(tl.float32)
|
|
else:
|
|
state_curr = tl.zeros([BLOCK_D, BLOCK_D], dtype=tl.float32)
|
|
state_z_curr = tl.zeros([BLOCK_D], dtype=tl.float32)
|
|
if STATE_FP32:
|
|
state_prev = tl.zeros([BLOCK_D, BLOCK_D], dtype=tl.float32)
|
|
else:
|
|
state_prev = tl.zeros([BLOCK_D, BLOCK_D], dtype=tl.bfloat16)
|
|
state_z_prev = tl.zeros([BLOCK_D], dtype=tl.float32)
|
|
|
|
# ========================================================
|
|
# Temporal loop — serial over F
|
|
# ========================================================
|
|
for f_iter in range(F):
|
|
if REVERSE:
|
|
q_frame = F - 1 - f_iter
|
|
kv_frame = F - f_iter if f_iter > 0 else 0 # unused at f=0
|
|
skip_update = f_iter == 0
|
|
else:
|
|
q_frame = f_iter
|
|
kv_frame = f_iter
|
|
skip_update = False
|
|
|
|
# ---- decay + state snapshot ----
|
|
if REVERSE and f_iter == 0:
|
|
g = 1.0
|
|
else:
|
|
g = tl.load(decay_bh + kv_frame).to(tl.float32)
|
|
state_curr = state_curr * g
|
|
state_z_curr = state_z_curr * g
|
|
if STATE_FP32:
|
|
state_prev = state_curr + 0.0
|
|
else:
|
|
state_prev = state_curr.to(tl.bfloat16)
|
|
state_z_prev = state_z_curr
|
|
|
|
if SAVE_STATE:
|
|
st_f = st_bh + q_frame * BLOCK_D * BLOCK_D
|
|
tl.store(st_f + offs_dd, state_prev, mask=mask_dd)
|
|
tl.store(sz_bh + q_frame * BLOCK_D + offs_d, state_z_prev, mask=mask_d)
|
|
|
|
# ------------------------------------------
|
|
# Pass 1 — State Accumulation
|
|
# ------------------------------------------
|
|
if skip_update == False:
|
|
kv_n_base = kv_frame * S
|
|
f_beta = beta_bh + kv_frame * S
|
|
|
|
for s0 in range(0, S, BLOCK_S):
|
|
offs_s = s0 + tl.arange(0, BLOCK_S)
|
|
mask_s = offs_s < S
|
|
mask_sd = mask_s[:, None] & mask_d[None, :]
|
|
mask_sd_pair = mask_s[:, None] & mask_d_pair[None, :]
|
|
n_idx = kv_n_base + offs_s
|
|
|
|
k_ptrs = qkv_bh + n_idx[:, None] * stride_n + 1 * stride_3 + offs_d[None, :] * stride_d
|
|
v_ptrs = qkv_bh + n_idx[:, None] * stride_n + 2 * stride_3 + offs_d[None, :] * stride_d
|
|
K_raw = tl.load(k_ptrs, mask=mask_sd, other=0.0).to(tl.float32)
|
|
V_raw = tl.load(v_ptrs, mask=mask_sd, other=0.0).to(tl.float32)
|
|
|
|
if QK_NORM:
|
|
if USE_PRECOMPUTED_RMS:
|
|
k_inv_rms = tl.load(k_inv_rms_ptr + pid_b * N + n_idx, mask=mask_s, other=1.0).to(tl.float32)
|
|
else:
|
|
k_var = tl.sum(K_raw * K_raw, axis=1) * D_inv
|
|
k_inv_rms = 1.0 / tl.sqrt(k_var + NORM_EPS)
|
|
K_normed = K_raw * k_inv_rms[:, None] * k_nw[None, :]
|
|
else:
|
|
K_normed = K_raw
|
|
K = tl.where(K_normed > 0, K_normed, 0.0) * k_scale
|
|
|
|
k_pair_ptrs = qkv_bh + n_idx[:, None] * stride_n + 1 * stride_3 + offs_d_pair[None, :] * stride_d
|
|
K_pair_raw = tl.load(k_pair_ptrs, mask=mask_sd_pair, other=0.0).to(tl.float32)
|
|
if QK_NORM:
|
|
K_pair_normed = K_pair_raw * k_inv_rms[:, None] * k_nw_pair[None, :]
|
|
else:
|
|
K_pair_normed = K_pair_raw
|
|
K_pair = tl.where(K_pair_normed > 0, K_pair_normed, 0.0) * k_scale
|
|
|
|
rope_ptrs = n_idx[:, None] * D + offs_d[None, :]
|
|
Cos = tl.load(rope_cos_ptr + rope_ptrs, mask=mask_sd, other=1.0).to(tl.float32)
|
|
Sin = tl.load(rope_sin_ptr + rope_ptrs, mask=mask_sd, other=0.0).to(tl.float32)
|
|
K_rot = K * Cos + K_pair * Sin
|
|
|
|
bt = tl.load(f_beta + offs_s, mask=mask_s, other=0.0).to(tl.float32)
|
|
|
|
K_rot_dc = K_rot.to(dot_dtype)
|
|
V_pred = tl.dot(K_rot_dc, state_prev.to(dot_dtype), out_dtype=tl.float32, input_precision=dot_ip)
|
|
dv = (V_raw - V_pred) * bt[:, None]
|
|
state_curr += tl.dot(tl.trans(K_rot), dv, out_dtype=tl.float32, input_precision="tf32")
|
|
|
|
z_hat = tl.sum(K * state_z_prev[None, :], axis=1)
|
|
dz = (1.0 - z_hat) * bt
|
|
state_z_curr += tl.sum(K * dz[:, None], axis=0)
|
|
|
|
if SAVE_STATE:
|
|
stc_f = stc_bh + q_frame * BLOCK_D * BLOCK_D
|
|
tl.store(stc_f + offs_dd, state_curr, mask=mask_dd)
|
|
tl.store(szc_bh + q_frame * BLOCK_D + offs_d, state_z_curr, mask=mask_d)
|
|
|
|
# ------------------------------------------
|
|
# Pass 2 — Output (reads state_curr, inclusive)
|
|
# ------------------------------------------
|
|
state_out = state_curr.to(dot_dtype)
|
|
state_z_out = state_z_curr
|
|
q_n_base = q_frame * S
|
|
|
|
for s0 in range(0, S, BLOCK_S):
|
|
offs_s = s0 + tl.arange(0, BLOCK_S)
|
|
mask_s = offs_s < S
|
|
mask_sd = mask_s[:, None] & mask_d[None, :]
|
|
mask_sd_pair = mask_s[:, None] & mask_d_pair[None, :]
|
|
n_idx = q_n_base + offs_s
|
|
|
|
q_ptrs = qkv_bh + n_idx[:, None] * stride_n + 0 * stride_3 + offs_d[None, :] * stride_d
|
|
q_pair_ptrs = qkv_bh + n_idx[:, None] * stride_n + 0 * stride_3 + offs_d_pair[None, :] * stride_d
|
|
Q_raw = tl.load(q_ptrs, mask=mask_sd, other=0.0).to(tl.float32)
|
|
Q_pair_raw = tl.load(q_pair_ptrs, mask=mask_sd_pair, other=0.0).to(tl.float32)
|
|
|
|
if QK_NORM:
|
|
if USE_PRECOMPUTED_RMS:
|
|
q_inv_rms = tl.load(q_inv_rms_ptr + pid_b * N + n_idx, mask=mask_s, other=1.0).to(tl.float32)
|
|
else:
|
|
q_var = tl.sum(Q_raw * Q_raw, axis=1) * D_inv
|
|
q_inv_rms = 1.0 / tl.sqrt(q_var + NORM_EPS)
|
|
Q_normed = Q_raw * q_inv_rms[:, None] * q_nw[None, :]
|
|
Q_pair_normed = Q_pair_raw * q_inv_rms[:, None] * q_nw_pair[None, :]
|
|
else:
|
|
Q_normed = Q_raw
|
|
Q_pair_normed = Q_pair_raw
|
|
Q = tl.where(Q_normed > 0, Q_normed, 0.0)
|
|
Q_pair = tl.where(Q_pair_normed > 0, Q_pair_normed, 0.0)
|
|
|
|
rope_ptrs = n_idx[:, None] * D + offs_d[None, :]
|
|
Cos = tl.load(rope_cos_ptr + rope_ptrs, mask=mask_sd, other=1.0).to(tl.float32)
|
|
Sin = tl.load(rope_sin_ptr + rope_ptrs, mask=mask_sd, other=0.0).to(tl.float32)
|
|
Q_rot = Q * Cos + Q_pair * Sin
|
|
|
|
num = tl.dot(Q_rot.to(dot_dtype), state_out, out_dtype=tl.float32, input_precision=dot_ip)
|
|
den = tl.sum(Q * state_z_out[None, :], axis=1)
|
|
|
|
result = num / (den[:, None] + EPS)
|
|
out_ptrs = out_bh + n_idx[:, None] * (H * D) + offs_d[None, :]
|
|
num_ptrs = num_bh + n_idx[:, None] * (H * D) + offs_d[None, :]
|
|
tl.store(out_ptrs, result.to(tl.bfloat16), mask=mask_sd)
|
|
tl.store(num_ptrs, num.to(tl.bfloat16), mask=mask_sd)
|
|
tl.store(den_bh + n_idx, den.to(tl.bfloat16), mask=mask_s)
|
|
|
|
if SAVE_FINAL_STATE:
|
|
final_kv_bh = final_state_kv_ptr + bh * BLOCK_D * BLOCK_D
|
|
tl.store(final_kv_bh + offs_dd, state_curr, mask=mask_dd)
|
|
final_z_bh = final_state_z_ptr + bh * BLOCK_D
|
|
tl.store(final_z_bh + offs_d, state_z_curr, mask=mask_d)
|
|
|
|
|
|
# =====================================================================
|
|
# Python wrappers
|
|
# =====================================================================
|
|
|
|
|
|
def prepare_rope_tables(rotary_emb, N: int, D: int, device) -> tuple[torch.Tensor, torch.Tensor]:
|
|
"""Complex rotary_emb `(1, 1, N, D//2)` → expanded (N, D) cos/sin tables.
|
|
|
|
Encodes the interleaved-pair rotation
|
|
y[2i] = x[2i]*cos[i] - x[2i+1]*sin[i]
|
|
y[2i+1] = x[2i]*sin[i] + x[2i+1]*cos[i]
|
|
as y[d] = x[d]*cos_exp[d] + x[d^1]*sin_exp[d]
|
|
where sin_exp[2i] = -sin[i], sin_exp[2i+1] = +sin[i].
|
|
|
|
Returns (cos_exp, sin_exp) both (N, D) float32, contiguous.
|
|
"""
|
|
if rotary_emb is None:
|
|
return (
|
|
torch.ones(N, D, device=device, dtype=torch.float32),
|
|
torch.zeros(N, D, device=device, dtype=torch.float32),
|
|
)
|
|
freqs = rotary_emb.squeeze(0).squeeze(0) # (N, D//2) complex
|
|
cos_half = freqs.real.float()
|
|
sin_half = freqs.imag.float()
|
|
rope_cos = cos_half.repeat_interleave(2, dim=-1)
|
|
rope_sin = torch.stack([-sin_half, sin_half], dim=-1).reshape(N, D)
|
|
return rope_cos.contiguous(), rope_sin.contiguous()
|
|
|
|
|
|
def _precompute_inv_rms(qkv: torch.Tensor, idx: int, C: int, eps: float = 1e-5) -> torch.Tensor:
|
|
"""Compute 1/RMS for one component of QKV over the full C = H*D channel dim.
|
|
|
|
Args:
|
|
qkv: (B, N, 3, H, D)
|
|
idx: 0 for Q, 1 for K, 2 for V
|
|
C: H*D (channel count)
|
|
eps: RMSNorm epsilon
|
|
|
|
Returns:
|
|
inv_rms: (B, N) float32
|
|
"""
|
|
raw = qkv[:, :, idx].float() # (B, N, H, D)
|
|
sq_sum = (raw * raw).sum(dim=(-2, -1)) # (B, N)
|
|
return torch.rsqrt(sq_sum / C + eps)
|
|
|
|
|
|
# =====================================================================
|
|
# Fused single-pass Q+K inverse-RMS Triton kernel
|
|
# =====================================================================
|
|
# Single Triton launch that reads each `(b, n)` row of `qkv` once and emits
|
|
# both `q_inv_rms[b, n]` and `k_inv_rms[b, n]`. Replaces two separate PyTorch
|
|
# scans (cast→square→sum→rsqrt) over `qkv[:, :, 0]` and `qkv[:, :, 1]`.
|
|
#
|
|
# Layout assumed: `qkv` is (B, N, 3, H, D) contiguous, so the C = H*D channels
|
|
# for a given (b, n, qkv_idx) live in a contiguous memory span.
|
|
|
|
|
|
@triton.jit
|
|
def _fused_qk_inv_rms_kernel(
|
|
qkv_ptr, # *T_in (B, N, 3, H, D), contiguous
|
|
q_inv_rms_ptr, # *float32 (B, N)
|
|
k_inv_rms_ptr, # *float32 (B, N)
|
|
N: tl.constexpr,
|
|
C: tl.constexpr, # H * D
|
|
eps,
|
|
BLOCK_C: tl.constexpr,
|
|
):
|
|
bn_id = tl.program_id(0)
|
|
qkv_row_stride = 3 * C
|
|
row_base = bn_id * qkv_row_stride
|
|
q_base = row_base
|
|
k_base = row_base + C
|
|
|
|
offs = tl.arange(0, BLOCK_C)
|
|
mask = offs < C
|
|
|
|
q_vals = tl.load(qkv_ptr + q_base + offs, mask=mask, other=0.0).to(tl.float32)
|
|
k_vals = tl.load(qkv_ptr + k_base + offs, mask=mask, other=0.0).to(tl.float32)
|
|
|
|
q_sq = tl.sum(q_vals * q_vals, axis=0)
|
|
k_sq = tl.sum(k_vals * k_vals, axis=0)
|
|
|
|
inv_c = 1.0 / C
|
|
q_inv = tl.rsqrt(q_sq * inv_c + eps)
|
|
k_inv = tl.rsqrt(k_sq * inv_c + eps)
|
|
|
|
tl.store(q_inv_rms_ptr + bn_id, q_inv)
|
|
tl.store(k_inv_rms_ptr + bn_id, k_inv)
|
|
|
|
|
|
def fused_qk_inv_rms(
|
|
qkv: torch.Tensor,
|
|
eps: float = 1e-5,
|
|
) -> tuple[torch.Tensor, torch.Tensor]:
|
|
"""Single-pass Triton fused Q+K inverse-RMS.
|
|
|
|
Replaces ``(_precompute_inv_rms(qkv, 0, C, eps), _precompute_inv_rms(qkv, 1, C, eps))``
|
|
with one launch that reads each ``(b, n)`` row of ``qkv`` exactly once.
|
|
|
|
Args:
|
|
qkv: (B, N, 3, H, D) contiguous tensor, any fp dtype.
|
|
eps: RMSNorm epsilon.
|
|
|
|
Returns:
|
|
(q_inv_rms, k_inv_rms), each (B, N) float32 contiguous.
|
|
"""
|
|
assert qkv.is_contiguous(), "qkv must be contiguous (B, N, 3, H, D)"
|
|
assert qkv.dim() == 5 and qkv.shape[2] == 3, f"expected (B, N, 3, H, D), got {tuple(qkv.shape)}"
|
|
B, N, _, H, D = qkv.shape
|
|
C = H * D
|
|
q_inv_rms = torch.empty((B, N), dtype=torch.float32, device=qkv.device)
|
|
k_inv_rms = torch.empty((B, N), dtype=torch.float32, device=qkv.device)
|
|
BLOCK_C = triton.next_power_of_2(C)
|
|
_fused_qk_inv_rms_kernel[(B * N,)](
|
|
qkv,
|
|
q_inv_rms,
|
|
k_inv_rms,
|
|
N=N,
|
|
C=C,
|
|
eps=eps,
|
|
BLOCK_C=BLOCK_C,
|
|
)
|
|
return q_inv_rms, k_inv_rms
|
|
|
|
|
|
@triton.jit
|
|
def _fused_bidi_merge_kernel(
|
|
num_fwd_ptr,
|
|
num_bwd_ptr,
|
|
den_fwd_ptr,
|
|
den_bwd_ptr,
|
|
gate_ptr,
|
|
out_ptr,
|
|
B,
|
|
N,
|
|
H,
|
|
D,
|
|
eps,
|
|
snum_b,
|
|
snum_n,
|
|
snum_h,
|
|
snum_d,
|
|
sden_b,
|
|
sden_h,
|
|
sden_n,
|
|
APPLY_GATE: tl.constexpr,
|
|
PRE_SUMMED: tl.constexpr,
|
|
BLOCK_N: tl.constexpr,
|
|
BLOCK_D: tl.constexpr,
|
|
):
|
|
pid_bh = tl.program_id(0)
|
|
pid_n = tl.program_id(1)
|
|
b = pid_bh // H
|
|
h = pid_bh % H
|
|
|
|
offs_n = pid_n * BLOCK_N + tl.arange(0, BLOCK_N)
|
|
offs_d = tl.arange(0, BLOCK_D)
|
|
mask_n = offs_n < N
|
|
mask_d = offs_d < D
|
|
mask_nd = mask_n[:, None] & mask_d[None, :]
|
|
|
|
num_base = b * snum_b + offs_n[:, None] * snum_n + h * snum_h + offs_d[None, :] * snum_d
|
|
nf = tl.load(num_fwd_ptr + num_base, mask=mask_nd, other=0.0).to(tl.float32)
|
|
den_base = b * sden_b + h * sden_h + offs_n * sden_n
|
|
df = tl.load(den_fwd_ptr + den_base, mask=mask_n, other=0.0).to(tl.float32)
|
|
|
|
if PRE_SUMMED:
|
|
num_total = nf
|
|
den_total = df + eps
|
|
else:
|
|
nb = tl.load(num_bwd_ptr + num_base, mask=mask_nd, other=0.0).to(tl.float32)
|
|
db = tl.load(den_bwd_ptr + den_base, mask=mask_n, other=0.0).to(tl.float32)
|
|
num_total = nf + nb
|
|
den_total = df + db + eps
|
|
out_val = num_total / den_total[:, None]
|
|
|
|
if APPLY_GATE:
|
|
g = tl.load(gate_ptr + num_base, mask=mask_nd, other=0.0).to(tl.float32)
|
|
silu_g = g * (1.0 / (1.0 + tl.exp(-g)))
|
|
out_val = out_val * silu_g
|
|
|
|
tl.store(out_ptr + num_base, out_val.to(tl.bfloat16), mask=mask_nd)
|
|
|
|
|
|
def fused_bidi_merge(
|
|
num_fwd: torch.Tensor,
|
|
num_bwd: torch.Tensor | None,
|
|
den_fwd: torch.Tensor,
|
|
den_bwd: torch.Tensor | None,
|
|
eps: float,
|
|
gate: torch.Tensor | None = None,
|
|
) -> torch.Tensor:
|
|
pre_summed = num_bwd is None
|
|
assert (num_bwd is None) == (den_bwd is None), "num_bwd/den_bwd must both be None or both provided"
|
|
if not pre_summed:
|
|
assert num_fwd.shape == num_bwd.shape and den_fwd.shape == den_bwd.shape
|
|
assert num_fwd.dtype == num_bwd.dtype and den_fwd.dtype == den_bwd.dtype
|
|
B, N, H, D = num_fwd.shape
|
|
out = torch.empty(
|
|
B, N, H, D, device=num_fwd.device, dtype=(torch.float32 if num_fwd.dtype == torch.float32 else torch.bfloat16)
|
|
)
|
|
BLOCK_D = triton.next_power_of_2(D)
|
|
BLOCK_N = 64
|
|
grid = (B * H, triton.cdiv(N, BLOCK_N))
|
|
if gate is not None:
|
|
assert gate.shape == (B, N, H, D), f"gate shape {gate.shape} != {(B, N, H, D)}"
|
|
gate_arg = gate
|
|
apply_gate = 1
|
|
else:
|
|
gate_arg = num_fwd
|
|
apply_gate = 0
|
|
num_bwd_arg = num_bwd if num_bwd is not None else num_fwd
|
|
den_bwd_arg = den_bwd if den_bwd is not None else den_fwd
|
|
_fused_bidi_merge_kernel[grid](
|
|
num_fwd,
|
|
num_bwd_arg,
|
|
den_fwd,
|
|
den_bwd_arg,
|
|
gate_arg,
|
|
out,
|
|
B,
|
|
N,
|
|
H,
|
|
D,
|
|
float(eps),
|
|
num_fwd.stride(0),
|
|
num_fwd.stride(1),
|
|
num_fwd.stride(2),
|
|
num_fwd.stride(3),
|
|
den_fwd.stride(0),
|
|
den_fwd.stride(1),
|
|
den_fwd.stride(2),
|
|
APPLY_GATE=apply_gate,
|
|
PRE_SUMMED=1 if pre_summed else 0,
|
|
BLOCK_N=BLOCK_N,
|
|
BLOCK_D=BLOCK_D,
|
|
)
|
|
return out
|
|
|
|
|
|
# =====================================================================
|
|
# Single-direction GDN entry point (delegates to chunkwise)
|
|
# =====================================================================
|
|
|
|
|
|
def fused_gdn_func(
|
|
qkv: torch.Tensor, # (B, N, 3, H, D)
|
|
q_inv_rms: torch.Tensor, # (B, N) float32
|
|
k_inv_rms: torch.Tensor, # (B, N) float32
|
|
q_norm_weight: torch.Tensor, # (C,) = (H*D,) float32
|
|
k_norm_weight: torch.Tensor, # (C,) float32
|
|
rope_cos: torch.Tensor, # (N, D) float32
|
|
rope_sin: torch.Tensor, # (N, D) float32
|
|
beta: torch.Tensor, # (B, H, F, S)
|
|
decay: torch.Tensor, # (B, H, F)
|
|
F: int,
|
|
S: int,
|
|
k_scale: float,
|
|
eps: float = 1e-6,
|
|
reverse: bool = False,
|
|
init_state_kv: torch.Tensor | None = None,
|
|
init_state_z: torch.Tensor | None = None,
|
|
save_final_state: bool = False,
|
|
) -> tuple[torch.Tensor, torch.Tensor] | tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]:
|
|
"""One direction of fused BiGDN via the unified kernel.
|
|
|
|
Args:
|
|
qkv .. eps: see kernel signature.
|
|
reverse: forward (False) or anti-causal (True) scan.
|
|
init_state_kv: optional ``(B*H, BLOCK_D, BLOCK_D)`` fp32 contiguous
|
|
tensor holding the forward-scan KV state at the END of a prefix
|
|
sequence (i.e., AFTER the prefix's last update, BEFORE any further
|
|
decay applied by this call). When provided, the kernel resumes the
|
|
scan from this state instead of zero. ``BLOCK_D = next_pow2(D)``.
|
|
Only the top-left ``D x D`` submatrix of the tile is read.
|
|
init_state_z: optional ``(B*H, BLOCK_D)`` fp32 contiguous companion
|
|
for the Z denominator state. Must be provided iff ``init_state_kv``
|
|
is provided.
|
|
save_final_state: when True, allocate fresh fp32 zero buffers for the
|
|
final KV / Z state (after the last frame's update) and pass them to
|
|
the kernel for write-out. Returns the buffers as additional outputs.
|
|
|
|
Returns:
|
|
``(num, den)`` — bf16 numerator ``(B, N, H, D)`` and denominator
|
|
``(B, H, N)`` before divide.
|
|
|
|
When ``save_final_state=True``, also returns
|
|
``(final_state_kv, final_state_z)`` fp32 with shapes
|
|
``(B*H, BLOCK_D, BLOCK_D)`` and ``(B*H, BLOCK_D)``.
|
|
|
|
Raises:
|
|
NotImplementedError: if any state I/O argument is set together with
|
|
``reverse=True``. The kernel supports state passing in both
|
|
directions, but state I/O is only defined for the forward direction
|
|
here to avoid silent misuse.
|
|
"""
|
|
# Dispatch both the stateless bidi case and the stateful forward path to
|
|
# chunkwise so split-equivalence uses one numeric implementation.
|
|
# Bypass via env: FUSED_GDN_FORCE_LEGACY=1.
|
|
if os.environ.get("FUSED_GDN_FORCE_LEGACY", "0") != "1":
|
|
from diffusion.model.ops.fused_gdn_chunkwise import (
|
|
fused_gdn_func_chunkwise,
|
|
fused_gdn_stateful_chunkwise,
|
|
)
|
|
|
|
# Validate state I/O args upfront — preserves the legacy fused_gdn_func's
|
|
# validation contract (callers depend on these specific ValueError /
|
|
# NotImplementedError signatures, e.g., test_state_validation).
|
|
if (init_state_kv is None) != (init_state_z is None):
|
|
raise ValueError(
|
|
"fused_gdn_func: init_state_kv and init_state_z must be provided together "
|
|
"(both None or both fp32 tensors)."
|
|
)
|
|
if reverse and (init_state_kv is not None or save_final_state):
|
|
raise NotImplementedError(
|
|
"fused_gdn_func: state passing (init_state_kv / init_state_z / "
|
|
"save_final_state) is only supported for the forward direction "
|
|
"(reverse=False)."
|
|
)
|
|
if init_state_kv is not None:
|
|
B_q, _N, _three, H_q, D_q = qkv.shape
|
|
BLOCK_D_q = triton.next_power_of_2(D_q)
|
|
expected_kv = (B_q * H_q, BLOCK_D_q, BLOCK_D_q)
|
|
expected_z = (B_q * H_q, BLOCK_D_q)
|
|
if tuple(init_state_kv.shape) != expected_kv:
|
|
raise ValueError(
|
|
f"fused_gdn_func: init_state_kv shape {tuple(init_state_kv.shape)} != " f"expected {expected_kv}."
|
|
)
|
|
if tuple(init_state_z.shape) != expected_z:
|
|
raise ValueError(
|
|
f"fused_gdn_func: init_state_z shape {tuple(init_state_z.shape)} != " f"expected {expected_z}."
|
|
)
|
|
if init_state_kv.dtype != torch.float32 or init_state_z.dtype != torch.float32:
|
|
raise ValueError(
|
|
f"fused_gdn_func: init_state_kv/init_state_z must be fp32 "
|
|
f"(got {init_state_kv.dtype}, {init_state_z.dtype})."
|
|
)
|
|
if not init_state_kv.is_contiguous() or not init_state_z.is_contiguous():
|
|
raise ValueError("fused_gdn_func: init_state_kv / init_state_z must be contiguous.")
|
|
|
|
# Stateless path
|
|
if init_state_kv is None and init_state_z is None and not save_final_state:
|
|
return fused_gdn_func_chunkwise(
|
|
qkv,
|
|
q_inv_rms,
|
|
k_inv_rms,
|
|
q_norm_weight,
|
|
k_norm_weight,
|
|
rope_cos,
|
|
rope_sin,
|
|
beta,
|
|
decay,
|
|
F=F,
|
|
S=S,
|
|
k_scale=k_scale,
|
|
eps=eps,
|
|
reverse=reverse,
|
|
)
|
|
|
|
# Stateful path: shape-adapt state I/O.
|
|
# state_kv: (B*H, BLOCK_D, BLOCK_D) row-major as M[K_feat, V_feat]
|
|
# chunkwise stateful: takes user-facing (B, H, D_in, D_out) and transposes
|
|
# internally to (B*H, D_out, D_in) for kernel storage.
|
|
# state_z: (B*H, BLOCK_D)
|
|
# chunkwise stateful: (B, H, D, 1) or (B, H, D)
|
|
B, N, _three, H, D = qkv.shape
|
|
BLOCK_D = triton.next_power_of_2(D)
|
|
|
|
ck_init_kv = None
|
|
ck_init_z = None
|
|
if init_state_kv is not None:
|
|
# (B*H, BLOCK_D, BLOCK_D) → (B, H, BLOCK_D, BLOCK_D)[:, :, :D, :D]
|
|
# then transpose so chunkwise's internal `.transpose(-1, -2)` undoes it.
|
|
ck_init_kv = init_state_kv.view(B, H, BLOCK_D, BLOCK_D)[:, :, :D, :D].transpose(-1, -2).contiguous()
|
|
if init_state_z is not None:
|
|
# (B*H, BLOCK_D) → (B, H, BLOCK_D)[:, :, :D] → (B, H, D, 1)
|
|
ck_init_z = init_state_z.view(B, H, BLOCK_D)[:, :, :D].unsqueeze(-1).contiguous()
|
|
|
|
result = fused_gdn_stateful_chunkwise(
|
|
qkv,
|
|
q_inv_rms,
|
|
k_inv_rms,
|
|
q_norm_weight,
|
|
k_norm_weight,
|
|
rope_cos,
|
|
rope_sin,
|
|
beta,
|
|
decay,
|
|
F=F,
|
|
S=S,
|
|
k_scale=k_scale,
|
|
eps=eps,
|
|
reverse=reverse,
|
|
init_state_kv=ck_init_kv,
|
|
init_state_z=ck_init_z,
|
|
return_final_state=save_final_state,
|
|
)
|
|
|
|
if not save_final_state:
|
|
return result # (num, den)
|
|
|
|
num, den, ck_state_kv, ck_state_z = result
|
|
# chunkwise returns state_kv as (B, H, D, D), [K_feat, V_feat] (post its
|
|
# internal back-transpose). Convert to stateful (B*H, BLOCK_D, BLOCK_D)
|
|
# by transposing back to internal storage and padding to BLOCK_D.
|
|
out_state_kv = torch.zeros(B * H, BLOCK_D, BLOCK_D, device=qkv.device, dtype=torch.float32)
|
|
out_state_kv[:, :D, :D] = ck_state_kv.transpose(-1, -2).reshape(B * H, D, D)
|
|
out_state_z = torch.zeros(B * H, BLOCK_D, device=qkv.device, dtype=torch.float32)
|
|
out_state_z[:, :D] = ck_state_z.squeeze(-1).reshape(B * H, D)
|
|
return num, den, out_state_kv, out_state_z
|
|
|
|
B, N, three, H, D = qkv.shape
|
|
assert three == 3
|
|
|
|
BLOCK_D, BLOCK_S, dot_prec, state_fp32, nw, cfg, beta, decay = _prepare_launch(D, beta, decay)
|
|
|
|
has_init_state = init_state_kv is not None or init_state_z is not None
|
|
if reverse and (has_init_state or save_final_state):
|
|
raise NotImplementedError(
|
|
"fused_gdn_func: state passing (init_state_kv / init_state_z / "
|
|
"save_final_state) is only supported for the forward direction "
|
|
"(reverse=False). The chunk-causal anti-causal pass resets state "
|
|
"per chunk and has no global cross-prefix state to cache."
|
|
)
|
|
|
|
if has_init_state:
|
|
if init_state_kv is None or init_state_z is None:
|
|
raise ValueError(
|
|
"fused_gdn_func: init_state_kv and init_state_z must be "
|
|
"provided together (got "
|
|
f"init_state_kv={'set' if init_state_kv is not None else 'None'}, "
|
|
f"init_state_z={'set' if init_state_z is not None else 'None'})."
|
|
)
|
|
expected_kv_shape = (B * H, BLOCK_D, BLOCK_D)
|
|
expected_z_shape = (B * H, BLOCK_D)
|
|
if tuple(init_state_kv.shape) != expected_kv_shape:
|
|
raise ValueError(
|
|
f"fused_gdn_func: init_state_kv shape {tuple(init_state_kv.shape)} "
|
|
f"does not match expected {expected_kv_shape} (BLOCK_D=next_pow2(D)={BLOCK_D})."
|
|
)
|
|
if tuple(init_state_z.shape) != expected_z_shape:
|
|
raise ValueError(
|
|
f"fused_gdn_func: init_state_z shape {tuple(init_state_z.shape)} "
|
|
f"does not match expected {expected_z_shape}."
|
|
)
|
|
if init_state_kv.dtype != torch.float32 or init_state_z.dtype != torch.float32:
|
|
raise ValueError(
|
|
"fused_gdn_func: init_state_kv and init_state_z must be fp32 "
|
|
f"(got {init_state_kv.dtype}, {init_state_z.dtype})."
|
|
)
|
|
if not init_state_kv.is_contiguous() or not init_state_z.is_contiguous():
|
|
raise ValueError("fused_gdn_func: init_state_kv and init_state_z must be contiguous.")
|
|
if init_state_kv.device != qkv.device or init_state_z.device != qkv.device:
|
|
raise ValueError("fused_gdn_func: init_state_* must live on the same device as qkv.")
|
|
load_init = 1
|
|
init_kv_arg = init_state_kv
|
|
init_z_arg = init_state_z
|
|
else:
|
|
load_init = 0
|
|
init_kv_arg = None # placeholder set below
|
|
|
|
if save_final_state:
|
|
final_state_kv = torch.zeros(B * H, BLOCK_D, BLOCK_D, device=qkv.device, dtype=torch.float32)
|
|
final_state_z = torch.zeros(B * H, BLOCK_D, device=qkv.device, dtype=torch.float32)
|
|
save_final = 1
|
|
else:
|
|
final_state_kv = None
|
|
final_state_z = None
|
|
save_final = 0
|
|
|
|
num = torch.empty(B, N, H, D, device=qkv.device, dtype=qkv.dtype)
|
|
den = torch.empty(B, H, N, device=qkv.device, dtype=qkv.dtype)
|
|
dummy = torch.empty(1, device=qkv.device, dtype=torch.float32)
|
|
|
|
# Resolve pointer args for the unused slots to a shared scratch tensor;
|
|
# the kernel compiles the corresponding load/store away when the
|
|
# constexpr flag is 0.
|
|
init_kv_ptr = init_kv_arg if load_init else dummy
|
|
init_z_ptr = init_z_arg if load_init else dummy
|
|
final_kv_ptr = final_state_kv if save_final else dummy
|
|
final_z_ptr = final_state_z if save_final else dummy
|
|
|
|
_fused_gdn_kernel[(B * H,)](
|
|
qkv,
|
|
qkv.stride(0),
|
|
qkv.stride(1),
|
|
qkv.stride(2),
|
|
qkv.stride(3),
|
|
qkv.stride(4),
|
|
beta,
|
|
decay,
|
|
q_inv_rms,
|
|
k_inv_rms,
|
|
q_norm_weight,
|
|
k_norm_weight,
|
|
rope_cos,
|
|
rope_sin,
|
|
num, # `out_ptr` reuses `num` buffer (result immediately overwritten below)
|
|
num,
|
|
den,
|
|
dummy,
|
|
dummy,
|
|
dummy,
|
|
dummy, # saved-state dummies (SAVE_STATE=0)
|
|
init_kv_ptr,
|
|
init_z_ptr,
|
|
final_kv_ptr,
|
|
final_z_ptr,
|
|
H=H,
|
|
F=F,
|
|
S=S,
|
|
D=D,
|
|
K_SCALE=k_scale,
|
|
NORM_EPS=1e-5, # unused with USE_PRECOMPUTED_RMS=1
|
|
EPS=eps,
|
|
QK_NORM=1,
|
|
USE_PRECOMPUTED_RMS=1,
|
|
STATE_FP32=1 if state_fp32 else 0,
|
|
DOT_PRECISION=dot_prec,
|
|
REVERSE=1 if reverse else 0,
|
|
SAVE_STATE=0,
|
|
LOAD_INIT_STATE=load_init,
|
|
SAVE_FINAL_STATE=save_final,
|
|
BLOCK_D=BLOCK_D,
|
|
BLOCK_S=BLOCK_S,
|
|
num_stages=cfg["num_stages"],
|
|
num_warps=nw,
|
|
)
|
|
if save_final_state:
|
|
return num, den, final_state_kv, final_state_z
|
|
return num, den
|
|
|
|
|
|
def fused_bigdn_func(
|
|
qkv: torch.Tensor, # (B, N, 3, H, D)
|
|
q_inv_rms: torch.Tensor, # (B, N) — pre-computed via `_precompute_inv_rms`
|
|
k_inv_rms: torch.Tensor, # (B, N)
|
|
q_norm_weight: torch.Tensor, # (C,) float32
|
|
k_norm_weight: torch.Tensor, # (C,)
|
|
rope_cos: torch.Tensor, # (N, D)
|
|
rope_sin: torch.Tensor, # (N, D)
|
|
beta: torch.Tensor, # (B, H, F, S)
|
|
decay: torch.Tensor, # (B, H, F)
|
|
F: int,
|
|
S: int,
|
|
k_scale: float,
|
|
eps: float = 1e-6,
|
|
# -- chunk-causal extensions (not in upstream; see adapter notes below) --
|
|
qkv_bwd: torch.Tensor | None = None,
|
|
beta_bwd: torch.Tensor | None = None,
|
|
decay_bwd: torch.Tensor | None = None,
|
|
q_inv_rms_bwd: torch.Tensor | None = None,
|
|
k_inv_rms_bwd: torch.Tensor | None = None,
|
|
) -> torch.Tensor:
|
|
"""Full bidirectional fused GDN.
|
|
|
|
Returns: out (B, N, H, D) bf16 = (num_fwd + num_bwd) / (den_fwd + den_bwd + eps).
|
|
|
|
Chunk-causal extensions (optional):
|
|
For chunk-causal GDN we need to zero state at chunk boundaries in the
|
|
BACKWARD direction only. Pass separately pre-processed backward tensors
|
|
(decay_bwd with zeros at boundary frames, and optionally qkv_bwd /
|
|
beta_bwd with K/V or beta zeroed at boundary frames). If any `*_bwd`
|
|
argument is None, the forward tensor is reused.
|
|
"""
|
|
if (
|
|
os.environ.get("FUSED_GDN_FORCE_LEGACY", "0") != "1"
|
|
and qkv_bwd is None
|
|
and beta_bwd is None
|
|
and decay_bwd is None
|
|
and q_inv_rms_bwd is None
|
|
and k_inv_rms_bwd is None
|
|
):
|
|
from diffusion.model.ops.fused_gdn_chunkwise import fused_bigdn_bidi_chunkwise
|
|
|
|
return fused_bigdn_bidi_chunkwise(
|
|
qkv,
|
|
q_inv_rms,
|
|
k_inv_rms,
|
|
q_norm_weight,
|
|
k_norm_weight,
|
|
rope_cos,
|
|
rope_sin,
|
|
beta,
|
|
decay,
|
|
F=F,
|
|
S=S,
|
|
k_scale=k_scale,
|
|
eps=eps,
|
|
)
|
|
|
|
num_fwd, den_fwd = fused_gdn_func(
|
|
qkv,
|
|
q_inv_rms,
|
|
k_inv_rms,
|
|
q_norm_weight,
|
|
k_norm_weight,
|
|
rope_cos,
|
|
rope_sin,
|
|
beta,
|
|
decay,
|
|
F=F,
|
|
S=S,
|
|
k_scale=k_scale,
|
|
eps=eps,
|
|
reverse=False,
|
|
)
|
|
num_bwd, den_bwd = fused_gdn_func(
|
|
qkv if qkv_bwd is None else qkv_bwd,
|
|
q_inv_rms if q_inv_rms_bwd is None else q_inv_rms_bwd,
|
|
k_inv_rms if k_inv_rms_bwd is None else k_inv_rms_bwd,
|
|
q_norm_weight,
|
|
k_norm_weight,
|
|
rope_cos,
|
|
rope_sin,
|
|
beta if beta_bwd is None else beta_bwd,
|
|
decay if decay_bwd is None else decay_bwd,
|
|
F=F,
|
|
S=S,
|
|
k_scale=k_scale,
|
|
eps=eps,
|
|
reverse=True,
|
|
)
|
|
# num: (B, N, H, D), den: (B, H, N). Fuse then divide.
|
|
total_num = num_fwd + num_bwd
|
|
total_den = (den_fwd + den_bwd).permute(0, 2, 1).unsqueeze(-1) # (B, N, H, 1)
|
|
return total_num / (total_den + eps)
|
|
|
|
|
|
# =====================================================================
|
|
# Backward / autograd Functions
|
|
# =====================================================================
|
|
# Adds:
|
|
# 1. ``_fused_gdn_bwd_kernel`` -- Triton jit kernel that replays the
|
|
# forward recurrence in reverse time using per-frame state snapshots
|
|
# written by the forward kernel under ``SAVE_STATE=1``.
|
|
# 2. ``_run_fwd_save`` -- helper that runs the existing forward
|
|
# ``_fused_gdn_kernel`` with ``SAVE_STATE=1``. Adapted to pass our
|
|
# extra ``init_state_kv_ptr / init_state_z_ptr / final_state_kv_ptr /
|
|
# final_state_z_ptr`` pointers + ``LOAD_INIT_STATE / SAVE_FINAL_STATE``
|
|
# constexpr flags (all unused on the autograd path -> dummy / 0).
|
|
# 3. ``FusedGDNFunction`` -- autograd Function for unidirectional GDN
|
|
# with ``QK_NORM=1`` (in-kernel per-head RMSNorm).
|
|
# 4. ``FusedBiGDNFunction`` -- autograd Function for bidirectional BiGDN.
|
|
# Pre-normalizes Q/K in PyTorch with full-channel RMSNorm, runs the
|
|
# forward kernel twice with ``QK_NORM=0 + SAVE_STATE=1``, fuses
|
|
# ``(num_fwd + num_bwd) / (den_fwd + den_bwd + eps)``. Backward
|
|
# computes ``dnum / dden`` from upstream ``dout`` and runs the bwd
|
|
# kernel twice with ``BIDI_MODE=1``.
|
|
# 5. Python wrappers ``fused_gdn_forward_with_grad`` /
|
|
# ``fused_bigdn_forward_with_grad`` -- drop-in autograd-enabled
|
|
# replacements for ``fused_gdn_func`` / ``fused_bigdn_func``.
|
|
#
|
|
# Chunk-causal autograd support: ``FusedBiGDNFunction`` (and the public
|
|
# wrapper ``fused_bigdn_forward_with_grad``) accepts optional
|
|
# ``beta_bwd`` / ``decay_bwd`` overrides for the reverse-direction
|
|
# kernel call -- exactly the same masking convention used by the
|
|
# inference path ``fused_bigdn_func``. When provided, the reverse
|
|
# direction's forward and backward kernels both run on these masked
|
|
# tensors, and the backward returns separate gradient tensors
|
|
# (``dbeta_bwd`` / ``ddecay_bwd``) so autograd can route them back
|
|
# through any ``clone() + index = 0`` masking the caller applied.
|
|
|
|
|
|
@triton.jit
|
|
def _fused_gdn_bwd_kernel(
|
|
# ---- original inputs ----
|
|
qkv_ptr,
|
|
stride_b: tl.constexpr,
|
|
stride_n: tl.constexpr,
|
|
stride_3: tl.constexpr,
|
|
stride_h: tl.constexpr,
|
|
stride_d: tl.constexpr,
|
|
beta_ptr,
|
|
decay_ptr,
|
|
q_norm_w_ptr,
|
|
k_norm_w_ptr,
|
|
rope_cos_ptr,
|
|
rope_sin_ptr,
|
|
# ---- saved from forward ----
|
|
saved_state_ptr, # (B*H, F, BLOCK_D, BLOCK_D) -- state_prev snapshots
|
|
saved_z_ptr, # (B*H, F, BLOCK_D)
|
|
saved_state_curr_ptr, # (B*H, F, BLOCK_D, BLOCK_D) -- state_curr (after update)
|
|
saved_z_curr_ptr, # (B*H, F, BLOCK_D)
|
|
# ---- upstream gradient / pre-computed dnum ----
|
|
dout_ptr, # GDN mode: (B, N, H, D) upstream grad. BiDI mode: pre-computed dnum
|
|
# ---- BiDI mode: external dden ----
|
|
dden_ext_ptr, # BiDI mode: (B, H, N) pre-computed dden. GDN mode: unused
|
|
# ---- output gradients ----
|
|
dqkv_ptr, # (B, N, 3, H, D) -- same layout as qkv
|
|
dbeta_ptr, # (B, H, F, S)
|
|
ddecay_ptr, # (B, H, F)
|
|
# ---- dims ----
|
|
H: tl.constexpr,
|
|
F: tl.constexpr,
|
|
S: tl.constexpr,
|
|
D: tl.constexpr,
|
|
K_SCALE,
|
|
NORM_EPS: tl.constexpr,
|
|
EPS: tl.constexpr,
|
|
QK_NORM: tl.constexpr,
|
|
STATE_FP32: tl.constexpr,
|
|
REVERSE_BWD: tl.constexpr, # 0=backward of forward GDN, 1=backward of reversed GDN
|
|
BIDI_MODE: tl.constexpr, # 0=GDN (compute dnum/dden), 1=BiGDN (use provided)
|
|
DOT_PRECISION: tl.constexpr, # 0=bf16 TC, 1=TF32 TC, 2=IEEE fp32
|
|
BLOCK_D: tl.constexpr,
|
|
BLOCK_S: tl.constexpr,
|
|
):
|
|
pid = tl.program_id(0)
|
|
pid_b = pid // H
|
|
pid_h = pid % H
|
|
N: tl.constexpr = F * S
|
|
bh = pid_b * H + pid_h
|
|
|
|
qkv_bh = qkv_ptr + pid_b * stride_b + pid_h * stride_h
|
|
dqkv_bh = dqkv_ptr + pid_b * stride_b + pid_h * stride_h
|
|
dout_bh = dout_ptr + pid_b * (N * H * D) + pid_h * D
|
|
beta_bh = beta_ptr + bh * (F * S)
|
|
decay_bh = decay_ptr + bh * F
|
|
dbeta_bh = dbeta_ptr + bh * (F * S)
|
|
ddecay_bh = ddecay_ptr + bh * F
|
|
st_bh = saved_state_ptr + bh * F * BLOCK_D * BLOCK_D
|
|
sz_bh = saved_z_ptr + bh * F * BLOCK_D
|
|
stc_bh = saved_state_curr_ptr + bh * F * BLOCK_D * BLOCK_D
|
|
szc_bh = saved_z_curr_ptr + bh * F * BLOCK_D
|
|
if BIDI_MODE:
|
|
dden_ext_bh = dden_ext_ptr + bh * N
|
|
|
|
offs_d = tl.arange(0, BLOCK_D)
|
|
mask_d = offs_d < D
|
|
offs_d_pair = offs_d ^ 1
|
|
mask_d_pair = offs_d_pair < D
|
|
|
|
nw_offset = pid_h * D
|
|
if QK_NORM:
|
|
q_nw = tl.load(q_norm_w_ptr + nw_offset + offs_d, mask=mask_d, other=0.0).to(tl.float32)
|
|
k_nw = tl.load(k_norm_w_ptr + nw_offset + offs_d, mask=mask_d, other=0.0).to(tl.float32)
|
|
q_nw_pair = tl.load(q_norm_w_ptr + nw_offset + offs_d_pair, mask=mask_d_pair, other=0.0).to(tl.float32)
|
|
k_nw_pair = tl.load(k_norm_w_ptr + nw_offset + offs_d_pair, mask=mask_d_pair, other=0.0).to(tl.float32)
|
|
|
|
D_inv = 1.0 / D
|
|
k_scale = K_SCALE
|
|
|
|
# Dot precision: mirror forward kernel
|
|
if DOT_PRECISION >= 1:
|
|
dot_dtype = tl.float32
|
|
else:
|
|
dot_dtype = tl.bfloat16
|
|
dot_ip: tl.constexpr = "ieee" if DOT_PRECISION == 2 else "tf32"
|
|
|
|
# Gradient matmuls: always use bf16 TC + TF32 input precision (matching PyTorch backward)
|
|
grad_dtype = tl.bfloat16
|
|
grad_ip: tl.constexpr = "tf32"
|
|
|
|
# ---- Gradient state accumulators (reverse time) ----
|
|
dstate = tl.zeros([BLOCK_D, BLOCK_D], dtype=tl.float32)
|
|
dstate_z = tl.zeros([BLOCK_D], dtype=tl.float32)
|
|
|
|
for f_rev in range(F):
|
|
# Backward iterates in reverse of forward direction.
|
|
if REVERSE_BWD:
|
|
f = f_rev # backward of reversed GDN: iterate 0..F-1
|
|
# In fwd_save REVERSE, q_frame=f had kv_frame=f+1 (or skip at f=F-1).
|
|
kv_frame_bwd = f + 1 if f < F - 1 else f
|
|
skip_bwd = f == F - 1 # f=F-1 was dummy step (f_iter=0 in fwd)
|
|
else:
|
|
f = F - 1 - f_rev # backward of forward GDN: iterate F-1..0
|
|
kv_frame_bwd = f
|
|
skip_bwd = False
|
|
q_n_base = f * S
|
|
kv_n_base = kv_frame_bwd * S
|
|
f_beta = beta_bh + kv_frame_bwd * S
|
|
|
|
# ---- Load state_curr for Pass 2 output (both directions use inclusive) ----
|
|
st_f = st_bh + f * BLOCK_D * BLOCK_D
|
|
offs_dd = offs_d[:, None] * BLOCK_D + offs_d[None, :]
|
|
mask_dd = mask_d[:, None] & mask_d[None, :]
|
|
|
|
stc_f = stc_bh + f * BLOCK_D * BLOCK_D
|
|
P_state = tl.load(stc_f + offs_dd, mask=mask_dd, other=0.0)
|
|
Pz_state = tl.load(szc_bh + f * BLOCK_D + offs_d, mask=mask_d, other=0.0)
|
|
if STATE_FP32 == 0:
|
|
P_state = P_state.to(tl.float32)
|
|
|
|
# Decay: for REVERSE_BWD, use decay[kv_frame] matching fwd_save.
|
|
if REVERSE_BWD and skip_bwd:
|
|
g = 1.0
|
|
elif REVERSE_BWD:
|
|
g = tl.load(decay_bh + kv_frame_bwd).to(tl.float32)
|
|
else:
|
|
g = tl.load(decay_bh + f).to(tl.float32)
|
|
|
|
# ========================================================
|
|
# Pass 2 backward: Output gradients -> dQ, dstate, dstate_z
|
|
# ========================================================
|
|
for s0 in range(0, S, BLOCK_S):
|
|
offs_s = s0 + tl.arange(0, BLOCK_S)
|
|
mask_s = offs_s < S
|
|
mask_sd = mask_s[:, None] & mask_d[None, :]
|
|
mask_sd_pair = mask_s[:, None] & mask_d_pair[None, :]
|
|
n_idx = q_n_base + offs_s # Q data from q_frame
|
|
|
|
# Load dout; recompute Q, Q_pair, Q_rot, num, den from saved P_f/Pz_f.
|
|
dout_ptrs = dout_bh + n_idx[:, None] * (H * D) + offs_d[None, :]
|
|
d_out = tl.load(dout_ptrs, mask=mask_sd, other=0.0).to(tl.float32)
|
|
|
|
# Recompute Q, Q_pair, Q_rot (same as forward).
|
|
q_ptrs = qkv_bh + n_idx[:, None] * stride_n + 0 * stride_3 + offs_d[None, :] * stride_d
|
|
Q_raw = tl.load(q_ptrs, mask=mask_sd, other=0.0).to(tl.float32)
|
|
q_pair_ptrs = qkv_bh + n_idx[:, None] * stride_n + 0 * stride_3 + offs_d_pair[None, :] * stride_d
|
|
Q_pair_raw = tl.load(q_pair_ptrs, mask=mask_sd_pair, other=0.0).to(tl.float32)
|
|
|
|
if QK_NORM:
|
|
q_var = tl.sum(Q_raw * Q_raw, axis=1) * D_inv
|
|
q_inv_rms = 1.0 / tl.sqrt(q_var + NORM_EPS)
|
|
Q_normed = Q_raw * q_inv_rms[:, None] * q_nw[None, :]
|
|
Q_pair_normed = Q_pair_raw * q_inv_rms[:, None] * q_nw_pair[None, :]
|
|
else:
|
|
Q_normed = Q_raw
|
|
Q_pair_normed = Q_pair_raw
|
|
Q = tl.where(Q_normed > 0, Q_normed, 0.0)
|
|
Q_pair = tl.where(Q_pair_normed > 0, Q_pair_normed, 0.0)
|
|
|
|
rope_ptrs = n_idx[:, None] * D + offs_d[None, :]
|
|
Cos = tl.load(rope_cos_ptr + rope_ptrs, mask=mask_sd, other=1.0).to(tl.float32)
|
|
Sin = tl.load(rope_sin_ptr + rope_ptrs, mask=mask_sd, other=0.0).to(tl.float32)
|
|
Q_rot = Q * Cos + Q_pair * Sin
|
|
|
|
# Compute dnum and dden.
|
|
if BIDI_MODE:
|
|
# BiGDN: dnum and dden pre-computed externally from total num/den.
|
|
dnum = d_out # dout_ptr already contains pre-computed dnum
|
|
dden = tl.load(dden_ext_bh + n_idx, mask=mask_s, other=0.0).to(tl.float32)
|
|
else:
|
|
# GDN: recompute num/den using direction-appropriate state.
|
|
num_tile = tl.dot(
|
|
Q_rot.to(dot_dtype), P_state.to(dot_dtype), out_dtype=tl.float32, input_precision=dot_ip
|
|
)
|
|
den_tile = tl.sum(Q * Pz_state[None, :], axis=1)
|
|
inv_den = 1.0 / (den_tile + EPS)
|
|
dnum = d_out * inv_den[:, None]
|
|
dden = -tl.sum(d_out * num_tile, axis=1) * inv_den * inv_den
|
|
|
|
# dstate += Q_rot^T @ dnum (state contribution from num = Q_rot @ P_state).
|
|
dstate = dstate + tl.dot(
|
|
tl.trans(Q_rot.to(grad_dtype)),
|
|
dnum.to(grad_dtype),
|
|
out_dtype=tl.float32,
|
|
input_precision=grad_ip,
|
|
)
|
|
|
|
# dstate_z += sum(dden * Q, axis=0) (Pz contribution from den = Q . Pz).
|
|
dstate_z += tl.sum(dden[:, None] * Q, axis=0)
|
|
|
|
# dQ_rot = dnum @ P_state^T (uses state that forward's output read).
|
|
dQ_rot = tl.dot(
|
|
dnum.to(grad_dtype),
|
|
tl.trans(P_state.to(grad_dtype)),
|
|
out_dtype=tl.float32,
|
|
input_precision=grad_ip,
|
|
)
|
|
|
|
# dQ_from_den = dden * Pz_state.
|
|
dQ_from_den = dden[:, None] * Pz_state[None, :]
|
|
|
|
# RoPE inverse for Q: store dQ_rot, reload at paired indices.
|
|
# Store dQ_rot temporarily to dqkv[Q] at normal d positions.
|
|
dq_ptrs = dqkv_bh + n_idx[:, None] * stride_n + 0 * stride_3 + offs_d[None, :] * stride_d
|
|
tl.store(dq_ptrs, dQ_rot.to(tl.bfloat16), mask=mask_sd)
|
|
# The XOR-paired channel can be owned by another warp. Synchronize
|
|
# both sides of the scratch roundtrip before dqkv is overwritten.
|
|
tl.debug_barrier()
|
|
|
|
# Load dQ_rot at paired positions.
|
|
dq_pair_ptrs = dqkv_bh + n_idx[:, None] * stride_n + 0 * stride_3 + offs_d_pair[None, :] * stride_d
|
|
dQ_rot_pair = tl.load(dq_pair_ptrs, mask=mask_sd_pair, other=0.0).to(tl.float32)
|
|
tl.debug_barrier()
|
|
|
|
# RoPE inverse: dQ = dQ_rot * Cos - dQ_rot_pair * Sin.
|
|
dQ = dQ_rot * Cos - dQ_rot_pair * Sin + dQ_from_den
|
|
|
|
# ReLU backward.
|
|
relu_mask_q = (Q_normed > 0).to(tl.float32)
|
|
dQ_normed = dQ * relu_mask_q
|
|
|
|
# Norm backward (QK_NORM) or direct (no norm).
|
|
if QK_NORM:
|
|
gw = dQ_normed * q_nw[None, :]
|
|
corr = tl.sum(gw * Q_raw, axis=1) * D_inv * q_inv_rms * q_inv_rms
|
|
dQ_raw = q_inv_rms[:, None] * (gw - Q_raw * corr[:, None])
|
|
else:
|
|
dQ_raw = dQ_normed
|
|
|
|
# Store final dQ_raw to dqkv[Q].
|
|
tl.store(dq_ptrs, dQ_raw.to(tl.bfloat16), mask=mask_sd)
|
|
|
|
# Both directions use inclusive output (state_curr), so capture dDelta AFTER Pass 2.
|
|
dDelta = dstate
|
|
dDelta_z = dstate_z
|
|
|
|
# ========================================================
|
|
# Reload state_prev for Pass 1 backward (reuse P_state variable)
|
|
# ========================================================
|
|
P_state = tl.load(st_f + offs_dd, mask=mask_dd, other=0.0)
|
|
if STATE_FP32 == 0:
|
|
P_state = P_state.to(tl.float32)
|
|
Pz_state = tl.load(sz_bh + f * BLOCK_D + offs_d, mask=mask_d, other=0.0)
|
|
|
|
# ========================================================
|
|
# Pass 1 backward: State update gradients -> dK, dV, dbeta, dstate
|
|
# Skip for REVERSE_BWD dummy frame (skip_bwd=True) to avoid clobbering.
|
|
# ========================================================
|
|
if skip_bwd == False:
|
|
for s0 in range(0, S, BLOCK_S):
|
|
offs_s = s0 + tl.arange(0, BLOCK_S)
|
|
mask_s = offs_s < S
|
|
mask_sd = mask_s[:, None] & mask_d[None, :]
|
|
mask_sd_pair = mask_s[:, None] & mask_d_pair[None, :]
|
|
n_idx = kv_n_base + offs_s # K/V from kv_frame
|
|
|
|
# Recompute K, K_pair, K_rot, V.
|
|
k_ptrs = qkv_bh + n_idx[:, None] * stride_n + 1 * stride_3 + offs_d[None, :] * stride_d
|
|
v_ptrs = qkv_bh + n_idx[:, None] * stride_n + 2 * stride_3 + offs_d[None, :] * stride_d
|
|
K_raw = tl.load(k_ptrs, mask=mask_sd, other=0.0).to(tl.float32)
|
|
V_raw = tl.load(v_ptrs, mask=mask_sd, other=0.0).to(tl.float32)
|
|
|
|
k_pair_ptrs = qkv_bh + n_idx[:, None] * stride_n + 1 * stride_3 + offs_d_pair[None, :] * stride_d
|
|
K_pair_raw = tl.load(k_pair_ptrs, mask=mask_sd_pair, other=0.0).to(tl.float32)
|
|
|
|
if QK_NORM:
|
|
k_var = tl.sum(K_raw * K_raw, axis=1) * D_inv
|
|
k_inv_rms = 1.0 / tl.sqrt(k_var + NORM_EPS)
|
|
K_normed = K_raw * k_inv_rms[:, None] * k_nw[None, :]
|
|
K_pair_normed = K_pair_raw * k_inv_rms[:, None] * k_nw_pair[None, :]
|
|
else:
|
|
K_normed = K_raw
|
|
K_pair_normed = K_pair_raw
|
|
K = tl.where(K_normed > 0, K_normed, 0.0) * k_scale
|
|
K_pair = tl.where(K_pair_normed > 0, K_pair_normed, 0.0) * k_scale
|
|
|
|
rope_ptrs = n_idx[:, None] * D + offs_d[None, :]
|
|
Cos = tl.load(rope_cos_ptr + rope_ptrs, mask=mask_sd, other=1.0).to(tl.float32)
|
|
Sin = tl.load(rope_sin_ptr + rope_ptrs, mask=mask_sd, other=0.0).to(tl.float32)
|
|
K_rot = K * Cos + K_pair * Sin
|
|
|
|
bt = tl.load(f_beta + offs_s, mask=mask_s, other=0.0).to(tl.float32)
|
|
|
|
# Recompute V_pred and delta_v.
|
|
K_rot_dc = K_rot.to(dot_dtype)
|
|
V_pred = tl.dot(K_rot_dc, P_state.to(dot_dtype), out_dtype=tl.float32, input_precision=dot_ip)
|
|
delta_v = (V_raw - V_pred) * bt[:, None]
|
|
|
|
# ---- KV stream backward ----
|
|
ddelta_v = tl.dot(
|
|
K_rot.to(grad_dtype),
|
|
dDelta.to(grad_dtype),
|
|
out_dtype=tl.float32,
|
|
input_precision=grad_ip,
|
|
)
|
|
|
|
dK_rot_from_delta = tl.dot(
|
|
delta_v.to(grad_dtype),
|
|
tl.trans(dDelta.to(grad_dtype)),
|
|
out_dtype=tl.float32,
|
|
input_precision=grad_ip,
|
|
)
|
|
|
|
dV = ddelta_v * bt[:, None]
|
|
dbeta_kv = tl.sum(ddelta_v * (V_raw - V_pred), axis=1)
|
|
|
|
dV_pred = -ddelta_v * bt[:, None]
|
|
dK_rot_from_vpred = tl.dot(
|
|
dV_pred.to(grad_dtype),
|
|
tl.trans(P_state.to(grad_dtype)),
|
|
out_dtype=tl.float32,
|
|
input_precision=grad_ip,
|
|
)
|
|
|
|
dstate = dstate + tl.dot(
|
|
tl.trans(K_rot.to(grad_dtype)),
|
|
dV_pred.to(grad_dtype),
|
|
out_dtype=tl.float32,
|
|
input_precision=grad_ip,
|
|
)
|
|
|
|
dK_rot = dK_rot_from_delta + dK_rot_from_vpred
|
|
|
|
# ---- Z stream backward ----
|
|
z_hat = tl.sum(K * Pz_state[None, :], axis=1)
|
|
dz = (1.0 - z_hat) * bt
|
|
|
|
ddz = tl.sum(K * dDelta_z[None, :], axis=1)
|
|
dz_hat = -ddz * bt
|
|
dK_z = dDelta_z[None, :] * dz[:, None] + dz_hat[:, None] * Pz_state[None, :]
|
|
dstate_z = dstate_z + tl.sum(dz_hat[:, None] * K, axis=0)
|
|
|
|
dbeta_z = ddz * (1.0 - z_hat)
|
|
dbeta_total = dbeta_kv + dbeta_z
|
|
tl.store(dbeta_bh + kv_frame_bwd * S + offs_s, dbeta_total.to(tl.bfloat16), mask=mask_s)
|
|
|
|
# ---- RoPE inverse for K ----
|
|
dk_ptrs = dqkv_bh + n_idx[:, None] * stride_n + 1 * stride_3 + offs_d[None, :] * stride_d
|
|
tl.store(dk_ptrs, dK_rot.to(tl.bfloat16), mask=mask_sd)
|
|
# Match the dQ synchronization: all temporary values must be
|
|
# visible before paired loads and consumed before overwrites.
|
|
tl.debug_barrier()
|
|
dk_pair_ptrs = dqkv_bh + n_idx[:, None] * stride_n + 1 * stride_3 + offs_d_pair[None, :] * stride_d
|
|
dK_rot_pair = tl.load(dk_pair_ptrs, mask=mask_sd_pair, other=0.0).to(tl.float32)
|
|
tl.debug_barrier()
|
|
|
|
dK_from_kv = dK_rot * Cos - dK_rot_pair * Sin
|
|
dK_total = dK_from_kv + dK_z
|
|
|
|
relu_mask_k = (K_normed > 0).to(tl.float32)
|
|
dK_normed = dK_total * k_scale * relu_mask_k
|
|
|
|
if QK_NORM:
|
|
gw_k = dK_normed * k_nw[None, :]
|
|
corr_k = tl.sum(gw_k * K_raw, axis=1) * D_inv * k_inv_rms * k_inv_rms
|
|
dK_raw = k_inv_rms[:, None] * (gw_k - K_raw * corr_k[:, None])
|
|
else:
|
|
dK_raw = dK_normed
|
|
|
|
tl.store(dk_ptrs, dK_raw.to(tl.bfloat16), mask=mask_sd)
|
|
dv_ptrs = dqkv_bh + n_idx[:, None] * stride_n + 2 * stride_3 + offs_d[None, :] * stride_d
|
|
tl.store(dv_ptrs, dV.to(tl.bfloat16), mask=mask_sd)
|
|
|
|
# ========================================================
|
|
# Decay backward (inside skip_bwd guard)
|
|
# ========================================================
|
|
is_first_frame = f_rev == F - 1
|
|
if is_first_frame:
|
|
ddecay_f = 0.0
|
|
else:
|
|
inv_g = 1.0 / (g + 1e-12)
|
|
ddecay_kv = tl.sum(dstate * P_state) * inv_g
|
|
ddecay_z_val = tl.sum(dstate_z * Pz_state) * inv_g
|
|
ddecay_f = ddecay_kv + ddecay_z_val
|
|
tl.store(ddecay_bh + kv_frame_bwd, ddecay_f)
|
|
|
|
# Propagate gradient through decay: dS_{f-1} = g[f] * dP_f.
|
|
dstate = dstate * g
|
|
dstate_z = dstate_z * g
|
|
|
|
|
|
# =====================================================================
|
|
# Forward-with-state-save helper (for autograd Functions)
|
|
# =====================================================================
|
|
|
|
|
|
def _run_fwd_save(
|
|
qkv,
|
|
beta,
|
|
decay,
|
|
q_norm_weight,
|
|
k_norm_weight,
|
|
rope_cos,
|
|
rope_sin,
|
|
F: int,
|
|
S: int,
|
|
k_scale: float,
|
|
norm_eps: float,
|
|
eps: float,
|
|
qk_norm: bool,
|
|
reverse: bool,
|
|
cfg,
|
|
):
|
|
"""Run forward kernel for one direction with ``SAVE_STATE=1``.
|
|
|
|
Returns ``(num, den, saved_state, saved_z, saved_state_curr, saved_z_curr)``.
|
|
The forward kernel writes ``out = num/(den+eps)`` first and then overwrites
|
|
the same buffer with raw ``num``, so the returned ``num`` tensor holds raw
|
|
numerator values (matching the BiGDN combine-then-divide convention).
|
|
"""
|
|
B, N, three, H, D = qkv.shape
|
|
BLOCK_D, BLOCK_S, dot_prec, state_fp32, nw, _, beta, decay = _prepare_launch(D, beta, decay)
|
|
|
|
num_out = torch.empty(B, N, H, D, device=qkv.device, dtype=qkv.dtype)
|
|
den_out = torch.empty(B, H, N, device=qkv.device, dtype=qkv.dtype)
|
|
state_dtype = torch.float32 if state_fp32 else torch.bfloat16
|
|
saved_state = torch.empty(B * H, F, BLOCK_D, BLOCK_D, device=qkv.device, dtype=state_dtype)
|
|
saved_z = torch.empty(B * H, F, BLOCK_D, device=qkv.device, dtype=torch.float32)
|
|
saved_state_curr = torch.empty(B * H, F, BLOCK_D, BLOCK_D, device=qkv.device, dtype=torch.float32)
|
|
saved_z_curr = torch.empty(B * H, F, BLOCK_D, device=qkv.device, dtype=torch.float32)
|
|
# The kernel writes ``out = num/(den+eps)`` first then overwrites with raw num
|
|
# in the same buffer. Reuse num_out as the (discarded) ``out`` slot so the
|
|
# final contents end up being raw num.
|
|
out_discard = num_out
|
|
dummy_inv = torch.empty(1, device=qkv.device, dtype=torch.float32)
|
|
|
|
_fused_gdn_kernel[(B * H,)](
|
|
qkv,
|
|
qkv.stride(0),
|
|
qkv.stride(1),
|
|
qkv.stride(2),
|
|
qkv.stride(3),
|
|
qkv.stride(4),
|
|
beta,
|
|
decay,
|
|
dummy_inv,
|
|
dummy_inv, # unused inv_rms ptrs (USE_PRECOMPUTED_RMS=0)
|
|
q_norm_weight,
|
|
k_norm_weight,
|
|
rope_cos,
|
|
rope_sin,
|
|
out_discard,
|
|
num_out,
|
|
den_out,
|
|
saved_state,
|
|
saved_z,
|
|
saved_state_curr,
|
|
saved_z_curr,
|
|
dummy_inv,
|
|
dummy_inv,
|
|
dummy_inv,
|
|
dummy_inv, # init/final-state dummies
|
|
H=H,
|
|
F=F,
|
|
S=S,
|
|
D=D,
|
|
K_SCALE=k_scale,
|
|
NORM_EPS=norm_eps,
|
|
EPS=eps,
|
|
QK_NORM=1 if qk_norm else 0,
|
|
USE_PRECOMPUTED_RMS=0,
|
|
STATE_FP32=1 if state_fp32 else 0,
|
|
DOT_PRECISION=dot_prec,
|
|
REVERSE=1 if reverse else 0,
|
|
SAVE_STATE=1,
|
|
LOAD_INIT_STATE=0,
|
|
SAVE_FINAL_STATE=0,
|
|
BLOCK_D=BLOCK_D,
|
|
BLOCK_S=BLOCK_S,
|
|
num_stages=cfg["num_stages"],
|
|
num_warps=nw,
|
|
)
|
|
return num_out, den_out, saved_state, saved_z, saved_state_curr, saved_z_curr
|
|
|
|
|
|
# =====================================================================
|
|
# Unidirectional GDN autograd Function
|
|
# =====================================================================
|
|
|
|
|
|
class FusedGDNFunction(torch.autograd.Function):
|
|
"""Autograd Function for unidirectional fused GDN with in-kernel RMSNorm.
|
|
|
|
Forward runs ``_fused_gdn_kernel`` with ``QK_NORM=1`` and ``SAVE_STATE=1``,
|
|
saving per-frame state snapshots for backward. Backward runs
|
|
``_fused_gdn_bwd_kernel`` with ``BIDI_MODE=0`` (kernel computes
|
|
``dnum``/``dden`` from upstream ``dout``).
|
|
"""
|
|
|
|
@staticmethod
|
|
def forward(
|
|
ctx,
|
|
qkv,
|
|
beta,
|
|
decay,
|
|
q_norm_weight,
|
|
k_norm_weight,
|
|
rope_cos,
|
|
rope_sin,
|
|
F: int,
|
|
S: int,
|
|
k_scale: float = 1.0,
|
|
norm_eps: float = 1e-6,
|
|
eps: float = 1e-6,
|
|
qk_norm: bool = True,
|
|
):
|
|
B, N, three, H, D = qkv.shape
|
|
assert three == 3 and N == F * S
|
|
|
|
BLOCK_D, BLOCK_S, dot_prec, state_fp32, nw, cfg, beta, decay = _prepare_launch(D, beta, decay)
|
|
|
|
if q_norm_weight is None:
|
|
q_norm_weight = torch.ones(D, device=qkv.device, dtype=torch.float32)
|
|
if k_norm_weight is None:
|
|
k_norm_weight = torch.ones(D, device=qkv.device, dtype=torch.float32)
|
|
|
|
out = torch.empty(B, N, H, D, device=qkv.device, dtype=qkv.dtype)
|
|
|
|
# Saved states for backward.
|
|
state_dtype = torch.float32 if state_fp32 else torch.bfloat16
|
|
saved_state = torch.empty(B * H, F, BLOCK_D, BLOCK_D, device=qkv.device, dtype=state_dtype)
|
|
saved_z = torch.empty(B * H, F, BLOCK_D, device=qkv.device, dtype=torch.float32)
|
|
saved_state_curr = torch.empty(B * H, F, BLOCK_D, BLOCK_D, device=qkv.device, dtype=torch.float32)
|
|
saved_z_curr = torch.empty(B * H, F, BLOCK_D, device=qkv.device, dtype=torch.float32)
|
|
|
|
# Dummy num/den for forward kernel (still writes them but we discard).
|
|
num_out = torch.empty(B, N, H, D, device=qkv.device, dtype=qkv.dtype)
|
|
den_out = torch.empty(B, H, N, device=qkv.device, dtype=qkv.dtype)
|
|
dummy_inv = torch.empty(1, device=qkv.device, dtype=torch.float32)
|
|
|
|
_fused_gdn_kernel[(B * H,)](
|
|
qkv,
|
|
qkv.stride(0),
|
|
qkv.stride(1),
|
|
qkv.stride(2),
|
|
qkv.stride(3),
|
|
qkv.stride(4),
|
|
beta,
|
|
decay,
|
|
dummy_inv,
|
|
dummy_inv, # unused inv_rms ptrs (USE_PRECOMPUTED_RMS=0)
|
|
q_norm_weight,
|
|
k_norm_weight,
|
|
rope_cos,
|
|
rope_sin,
|
|
out,
|
|
num_out,
|
|
den_out,
|
|
saved_state,
|
|
saved_z,
|
|
saved_state_curr,
|
|
saved_z_curr,
|
|
dummy_inv,
|
|
dummy_inv,
|
|
dummy_inv,
|
|
dummy_inv, # init/final-state dummies
|
|
H=H,
|
|
F=F,
|
|
S=S,
|
|
D=D,
|
|
K_SCALE=k_scale,
|
|
NORM_EPS=norm_eps,
|
|
EPS=eps,
|
|
QK_NORM=1 if qk_norm else 0,
|
|
USE_PRECOMPUTED_RMS=0,
|
|
STATE_FP32=1 if state_fp32 else 0,
|
|
DOT_PRECISION=dot_prec,
|
|
REVERSE=0,
|
|
SAVE_STATE=1,
|
|
LOAD_INIT_STATE=0,
|
|
SAVE_FINAL_STATE=0,
|
|
BLOCK_D=BLOCK_D,
|
|
BLOCK_S=BLOCK_S,
|
|
num_stages=cfg["num_stages"],
|
|
num_warps=nw,
|
|
)
|
|
del num_out, den_out
|
|
|
|
ctx.save_for_backward(
|
|
qkv,
|
|
beta,
|
|
decay,
|
|
q_norm_weight,
|
|
k_norm_weight,
|
|
rope_cos,
|
|
rope_sin,
|
|
saved_state,
|
|
saved_z,
|
|
saved_state_curr,
|
|
saved_z_curr,
|
|
)
|
|
ctx.F = F
|
|
ctx.S = S
|
|
ctx.k_scale = k_scale
|
|
ctx.norm_eps = norm_eps
|
|
ctx.eps = eps
|
|
ctx.qk_norm = qk_norm
|
|
ctx.dot_prec = dot_prec
|
|
return out
|
|
|
|
@staticmethod
|
|
def backward(ctx, dout):
|
|
(
|
|
qkv,
|
|
beta,
|
|
decay,
|
|
q_norm_weight,
|
|
k_norm_weight,
|
|
rope_cos,
|
|
rope_sin,
|
|
saved_state,
|
|
saved_z,
|
|
saved_state_curr,
|
|
saved_z_curr,
|
|
) = ctx.saved_tensors
|
|
|
|
B, N, three, H, D = qkv.shape
|
|
F_val = ctx.F
|
|
S = ctx.S
|
|
|
|
BLOCK_D, BLOCK_S_BWD, _, _, _, cfg, beta, decay = _prepare_launch(D, beta, decay)
|
|
dqkv = torch.zeros_like(qkv)
|
|
dbeta = torch.zeros_like(beta)
|
|
ddecay = torch.zeros_like(decay)
|
|
|
|
# Dummy dden_ext (unused in GDN mode).
|
|
dden_ext = torch.empty(1, device=qkv.device, dtype=torch.float32)
|
|
|
|
# Progressive num_warps reduction on tmem overflow.
|
|
nw = cfg["num_warps"]
|
|
if ctx.dot_prec >= 1:
|
|
nw = min(nw, 4)
|
|
while nw >= 1:
|
|
try:
|
|
_fused_gdn_bwd_kernel[(B * H,)](
|
|
qkv,
|
|
qkv.stride(0),
|
|
qkv.stride(1),
|
|
qkv.stride(2),
|
|
qkv.stride(3),
|
|
qkv.stride(4),
|
|
beta,
|
|
decay,
|
|
q_norm_weight,
|
|
k_norm_weight,
|
|
rope_cos,
|
|
rope_sin,
|
|
saved_state,
|
|
saved_z,
|
|
saved_state_curr,
|
|
saved_z_curr,
|
|
dout.contiguous(),
|
|
dden_ext,
|
|
dqkv,
|
|
dbeta,
|
|
ddecay,
|
|
H=H,
|
|
F=F_val,
|
|
S=S,
|
|
D=D,
|
|
K_SCALE=ctx.k_scale,
|
|
NORM_EPS=ctx.norm_eps,
|
|
EPS=ctx.eps,
|
|
QK_NORM=1 if ctx.qk_norm else 0,
|
|
STATE_FP32=1 if cfg["STATE_FP32"] else 0,
|
|
REVERSE_BWD=0,
|
|
BIDI_MODE=0,
|
|
DOT_PRECISION=ctx.dot_prec,
|
|
BLOCK_D=BLOCK_D,
|
|
BLOCK_S=BLOCK_S_BWD,
|
|
num_stages=cfg["num_stages"],
|
|
num_warps=nw,
|
|
)
|
|
break
|
|
except Exception as e:
|
|
if "OutOfResources" in str(type(e).__name__) or "out of resource" in str(e).lower():
|
|
nw = nw // 2
|
|
if nw < 1:
|
|
raise RuntimeError(
|
|
"FusedGDN backward: Triton kernel OutOfResources at all warp "
|
|
f"counts (8, 4, 2, 1). Most recent error: {e}"
|
|
) from e
|
|
else:
|
|
raise
|
|
|
|
return dqkv, dbeta, ddecay, None, None, None, None, None, None, None, None, None, None
|
|
|
|
|
|
def fused_gdn_forward_with_grad(
|
|
qkv: torch.Tensor,
|
|
beta: torch.Tensor,
|
|
decay: torch.Tensor,
|
|
q_norm_weight: torch.Tensor | None,
|
|
k_norm_weight: torch.Tensor | None,
|
|
rope_cos: torch.Tensor,
|
|
rope_sin: torch.Tensor,
|
|
F: int,
|
|
S: int,
|
|
k_scale: float = 1.0,
|
|
norm_eps: float = 1e-6,
|
|
eps: float = 1e-6,
|
|
qk_norm: bool = True,
|
|
) -> torch.Tensor:
|
|
"""Drop-in autograd-enabled replacement for the unidirectional GDN path.
|
|
|
|
Unlike ``fused_gdn_func`` (which expects pre-computed ``q_inv_rms``/
|
|
``k_inv_rms``), this wrapper computes per-head RMSNorm inside the
|
|
Triton kernel (``QK_NORM=1`` / ``USE_PRECOMPUTED_RMS=0``) so the
|
|
backward kernel can reproduce the exact same normed Q/K when
|
|
replaying the recurrence.
|
|
"""
|
|
return FusedGDNFunction.apply(
|
|
qkv,
|
|
beta,
|
|
decay,
|
|
q_norm_weight,
|
|
k_norm_weight,
|
|
rope_cos,
|
|
rope_sin,
|
|
F,
|
|
S,
|
|
k_scale,
|
|
norm_eps,
|
|
eps,
|
|
qk_norm,
|
|
)
|
|
|
|
|
|
# =====================================================================
|
|
# Bidirectional BiGDN autograd Function (full-channel RMSNorm in Python)
|
|
# =====================================================================
|
|
|
|
|
|
class FusedBiGDNFunction(torch.autograd.Function):
|
|
"""Autograd Function for bidirectional fused BiGDN.
|
|
|
|
Full-channel RMSNorm is applied in Python (so the norm backward can
|
|
couple all heads correctly), then the kernel runs with ``QK_NORM=0``
|
|
on the pre-normed QKV. Forward and reverse directions are run
|
|
separately with ``SAVE_STATE=1``, and combined as
|
|
``out = (num_fwd + num_bwd) / (den_fwd + den_bwd + eps)``.
|
|
|
|
Backward computes ``dnum`` and ``dden`` from upstream ``dout`` and
|
|
runs the bwd kernel twice (forward + reverse) with ``BIDI_MODE=1``.
|
|
Norm backward is computed in Python (full-channel RMSNorm couples
|
|
all heads).
|
|
|
|
Chunk-causal masking (optional):
|
|
Pass ``beta_bwd`` and/or ``decay_bwd`` to override the beta/decay
|
|
tensors used by the **reverse-direction** kernel calls (forward
|
|
save + backward). The forward direction always uses the
|
|
unmasked ``beta`` / ``decay``. This mirrors the inference path
|
|
in :func:`fused_bigdn_func` and unlocks chunk-causal autograd
|
|
training: callers typically build ``beta_bwd`` / ``decay_bwd``
|
|
as ``beta.clone()`` / ``decay.clone()`` with interior chunk
|
|
boundaries zeroed, so the anti-causal scan resets state at
|
|
every chunk boundary.
|
|
|
|
When ``beta_bwd`` is ``None``, the kernel-emitted reverse-
|
|
direction beta gradient is summed into the forward-direction
|
|
gradient (returned via the ``beta`` slot) and the ``beta_bwd``
|
|
gradient slot returns ``None``. When ``beta_bwd`` is provided,
|
|
the two gradient streams are kept separate: the forward-
|
|
direction gradient flows through the ``beta`` slot and the
|
|
reverse-direction gradient flows through the ``beta_bwd`` slot
|
|
so autograd can route them through any ``clone() + index = 0``
|
|
masking applied by the caller. ``decay_bwd`` is handled
|
|
identically.
|
|
"""
|
|
|
|
@staticmethod
|
|
def forward(
|
|
ctx,
|
|
qkv,
|
|
beta,
|
|
decay,
|
|
q_norm_weight,
|
|
k_norm_weight,
|
|
rope_cos,
|
|
rope_sin,
|
|
F: int,
|
|
S: int,
|
|
k_scale: float = 1.0,
|
|
norm_eps: float = 1e-5,
|
|
eps: float = 1e-6,
|
|
beta_bwd: torch.Tensor | None = None,
|
|
decay_bwd: torch.Tensor | None = None,
|
|
):
|
|
B, N, three, H, D = qkv.shape
|
|
C = H * D
|
|
assert three == 3 and N == F * S
|
|
cfg = _kcfg()
|
|
|
|
if q_norm_weight is None:
|
|
q_norm_weight = torch.ones(C, device=qkv.device, dtype=torch.float32)
|
|
if k_norm_weight is None:
|
|
k_norm_weight = torch.ones(C, device=qkv.device, dtype=torch.float32)
|
|
|
|
# Full-channel RMSNorm: inv_rms over all H*D dims.
|
|
q_raw = qkv[:, :, 0].float() # (B, N, H, D)
|
|
k_raw = qkv[:, :, 1].float()
|
|
q_inv_rms = torch.rsqrt((q_raw * q_raw).sum(dim=(-2, -1)) / C + norm_eps) # (B, N)
|
|
k_inv_rms = torch.rsqrt((k_raw * k_raw).sum(dim=(-2, -1)) / C + norm_eps)
|
|
|
|
# Apply norm to Q and K: Q_normed = Q_raw * inv_rms * weight.
|
|
q_nw_hd = q_norm_weight.reshape(H, D)
|
|
k_nw_hd = k_norm_weight.reshape(H, D)
|
|
qkv_normed = qkv.clone()
|
|
qkv_normed[:, :, 0] = (q_raw * q_inv_rms[:, :, None, None] * q_nw_hd[None, None]).to(qkv.dtype)
|
|
qkv_normed[:, :, 1] = (k_raw * k_inv_rms[:, :, None, None] * k_nw_hd[None, None]).to(qkv.dtype)
|
|
|
|
# Reverse-direction beta/decay overrides for chunk-causal masking.
|
|
# When the caller supplies ``beta_bwd`` / ``decay_bwd`` (typically
|
|
# ``beta.clone()`` / ``decay.clone()`` with interior chunk-boundary
|
|
# frames zeroed), the reverse-direction kernel reads them instead of
|
|
# the unmasked tensors so the anti-causal scan resets state at chunk
|
|
# boundaries. The forward (causal) direction always uses the
|
|
# unmasked ``beta`` / ``decay``.
|
|
beta_for_bwd_dir = beta_bwd if beta_bwd is not None else beta
|
|
decay_for_bwd_dir = decay_bwd if decay_bwd is not None else decay
|
|
|
|
# Run forward-save with QK_NORM=0 on pre-normed data.
|
|
dummy_nw = torch.ones(D, device=qkv.device, dtype=torch.float32)
|
|
num_fwd, den_fwd, sv_fwd, sz_fwd, svc_fwd, szc_fwd = _run_fwd_save(
|
|
qkv_normed,
|
|
beta,
|
|
decay,
|
|
dummy_nw,
|
|
dummy_nw,
|
|
rope_cos,
|
|
rope_sin,
|
|
F,
|
|
S,
|
|
k_scale,
|
|
norm_eps,
|
|
eps,
|
|
False,
|
|
False,
|
|
cfg,
|
|
)
|
|
num_bwd, den_bwd, sv_bwd, sz_bwd, svc_bwd, szc_bwd = _run_fwd_save(
|
|
qkv_normed,
|
|
beta_for_bwd_dir,
|
|
decay_for_bwd_dir,
|
|
dummy_nw,
|
|
dummy_nw,
|
|
rope_cos,
|
|
rope_sin,
|
|
F,
|
|
S,
|
|
k_scale,
|
|
norm_eps,
|
|
eps,
|
|
False,
|
|
True,
|
|
cfg,
|
|
)
|
|
|
|
# Combine: out = (num_fwd + num_bwd) / (den_fwd + den_bwd + eps).
|
|
total_num = num_fwd.float() + num_bwd.float()
|
|
total_den = den_fwd.float() + den_bwd.float()
|
|
total_den_exp = total_den.permute(0, 2, 1).unsqueeze(-1) # (B, N, H, 1)
|
|
out = (total_num / (total_den_exp + eps)).to(qkv.dtype)
|
|
|
|
# Save ``beta_bwd`` / ``decay_bwd`` (possibly ``None``) so the
|
|
# backward pass can (a) replay the reverse-direction kernel against
|
|
# the same masked inputs, and (b) decide whether to keep the
|
|
# reverse-direction beta/decay gradients separate (caller-supplied
|
|
# override) or fold them into the forward-direction gradient
|
|
# (no override, legacy behaviour).
|
|
ctx.save_for_backward(
|
|
qkv,
|
|
beta,
|
|
decay,
|
|
q_norm_weight,
|
|
k_norm_weight,
|
|
q_inv_rms,
|
|
k_inv_rms,
|
|
rope_cos,
|
|
rope_sin,
|
|
sv_fwd,
|
|
sz_fwd,
|
|
svc_fwd,
|
|
szc_fwd,
|
|
sv_bwd,
|
|
sz_bwd,
|
|
svc_bwd,
|
|
szc_bwd,
|
|
out,
|
|
total_den.to(qkv.dtype),
|
|
beta_bwd,
|
|
decay_bwd,
|
|
)
|
|
_, _dot_prec, _, _ = _resolve_launch_config()
|
|
ctx.dot_prec = _dot_prec
|
|
ctx.F = F
|
|
ctx.S = S
|
|
ctx.k_scale = k_scale
|
|
ctx.norm_eps = norm_eps
|
|
ctx.eps = eps
|
|
return out
|
|
|
|
@staticmethod
|
|
def backward(ctx, dout):
|
|
(
|
|
qkv,
|
|
beta,
|
|
decay,
|
|
q_norm_weight,
|
|
k_norm_weight,
|
|
q_inv_rms,
|
|
k_inv_rms,
|
|
rope_cos,
|
|
rope_sin,
|
|
sv_fwd,
|
|
sz_fwd,
|
|
svc_fwd,
|
|
szc_fwd,
|
|
sv_bwd,
|
|
sz_bwd,
|
|
svc_bwd,
|
|
szc_bwd,
|
|
out,
|
|
total_den_saved,
|
|
beta_bwd_saved,
|
|
decay_bwd_saved,
|
|
) = ctx.saved_tensors
|
|
|
|
# Track whether the caller supplied separate ``beta_bwd`` /
|
|
# ``decay_bwd`` overrides; this controls whether the reverse-
|
|
# direction kernel gradients are summed into the forward-direction
|
|
# slot (legacy behaviour) or routed back through dedicated grad
|
|
# slots so autograd can flow through the caller's masking ops
|
|
# (``clone() + index = 0``).
|
|
has_beta_bwd = beta_bwd_saved is not None
|
|
has_decay_bwd = decay_bwd_saved is not None
|
|
|
|
B, N, three, H, D = qkv.shape
|
|
C = H * D
|
|
|
|
# Recompute qkv_normed (avoid saving B*N*3*H*D extra tensor).
|
|
q_raw = qkv[:, :, 0].float()
|
|
k_raw = qkv[:, :, 1].float()
|
|
q_nw_hd = q_norm_weight.reshape(H, D)
|
|
k_nw_hd = k_norm_weight.reshape(H, D)
|
|
qkv_normed = qkv.clone()
|
|
qkv_normed[:, :, 0] = (q_raw * q_inv_rms[:, :, None, None] * q_nw_hd[None, None]).to(qkv.dtype)
|
|
qkv_normed[:, :, 1] = (k_raw * k_inv_rms[:, :, None, None] * k_nw_hd[None, None]).to(qkv.dtype)
|
|
F_val = ctx.F
|
|
S = ctx.S
|
|
eps = ctx.eps
|
|
BLOCK_D, _, _, _, _, cfg, beta, decay = _prepare_launch(D, beta, decay)
|
|
|
|
# Reverse-direction beta/decay actually fed to the reverse kernel.
|
|
# When the caller supplied an override, we replay against the
|
|
# masked tensor; otherwise we reuse the unmasked beta/decay so the
|
|
# legacy summing path is bit-identical to the pre-extension
|
|
# behaviour.
|
|
if has_beta_bwd:
|
|
beta_for_bwd_dir = beta_bwd_saved.contiguous()
|
|
else:
|
|
beta_for_bwd_dir = beta
|
|
if has_decay_bwd:
|
|
decay_for_bwd_dir = decay_bwd_saved.contiguous()
|
|
else:
|
|
decay_for_bwd_dir = decay
|
|
|
|
# ---- Pre-compute dnum and dden ----
|
|
total_den_exp = total_den_saved.float().permute(0, 2, 1).unsqueeze(-1)
|
|
inv_total_den = 1.0 / (total_den_exp + eps)
|
|
dnum = (dout.float() * inv_total_den).to(qkv.dtype).contiguous()
|
|
dden = (
|
|
(-(dout.float() * out.float()).sum(dim=-1) * inv_total_den.squeeze(-1))
|
|
.permute(0, 2, 1)
|
|
.to(qkv.dtype)
|
|
.contiguous()
|
|
)
|
|
del out, total_den_saved, total_den_exp, inv_total_den
|
|
|
|
dummy_nw = torch.ones(D, device=qkv.device, dtype=torch.float32)
|
|
|
|
# ---- Backward for forward direction (QK_NORM=0, operates on normed QKV) ----
|
|
dqkv_fwd = torch.zeros_like(qkv)
|
|
dbeta_fwd = torch.zeros_like(beta)
|
|
ddecay_fwd = torch.zeros_like(decay)
|
|
|
|
def _run_triton_bwd(sv, sz, svc, szc, dqkv_out, dbeta_out, ddecay_out, reverse_bwd, beta_kernel, decay_kernel):
|
|
"""Try backward kernel with progressively fewer warps on tmem overflow.
|
|
|
|
``beta_kernel`` / ``decay_kernel`` are passed as explicit
|
|
arguments (instead of closing over the outer-scope ``beta`` /
|
|
``decay``) so the reverse-direction call can replay against the
|
|
chunk-causal-masked tensors (``beta_bwd`` / ``decay_bwd``) when
|
|
present, while the forward-direction call always uses the
|
|
unmasked ``beta`` / ``decay``.
|
|
"""
|
|
nw = cfg["num_warps"]
|
|
if ctx.dot_prec >= 1:
|
|
nw = min(nw, 4)
|
|
while nw >= 1:
|
|
try:
|
|
_fused_gdn_bwd_kernel[(B * H,)](
|
|
qkv_normed,
|
|
qkv_normed.stride(0),
|
|
qkv_normed.stride(1),
|
|
qkv_normed.stride(2),
|
|
qkv_normed.stride(3),
|
|
qkv_normed.stride(4),
|
|
beta_kernel,
|
|
decay_kernel,
|
|
dummy_nw,
|
|
dummy_nw,
|
|
rope_cos,
|
|
rope_sin,
|
|
sv,
|
|
sz,
|
|
svc,
|
|
szc,
|
|
dnum,
|
|
dden,
|
|
dqkv_out,
|
|
dbeta_out,
|
|
ddecay_out,
|
|
H=H,
|
|
F=F_val,
|
|
S=S,
|
|
D=D,
|
|
K_SCALE=ctx.k_scale,
|
|
NORM_EPS=ctx.norm_eps,
|
|
EPS=eps,
|
|
QK_NORM=0,
|
|
STATE_FP32=1 if cfg["STATE_FP32"] else 0,
|
|
REVERSE_BWD=reverse_bwd,
|
|
BIDI_MODE=1,
|
|
DOT_PRECISION=ctx.dot_prec,
|
|
BLOCK_D=BLOCK_D,
|
|
BLOCK_S=cfg["BLOCK_S"],
|
|
num_stages=cfg["num_stages"],
|
|
num_warps=nw,
|
|
)
|
|
return # success
|
|
except Exception as e:
|
|
if "OutOfResources" in str(type(e).__name__) or "out of resource" in str(e).lower():
|
|
nw = nw // 2
|
|
if nw >= 1:
|
|
continue
|
|
raise RuntimeError(
|
|
"FusedBiGDN backward: Triton kernel OutOfResources at all warp counts "
|
|
f"(8, 4, 2, 1). Most recent error: {e}"
|
|
) from e
|
|
else:
|
|
raise
|
|
|
|
_run_triton_bwd(sv_fwd, sz_fwd, svc_fwd, szc_fwd, dqkv_fwd, dbeta_fwd, ddecay_fwd, 0, beta, decay)
|
|
del sv_fwd, sz_fwd, svc_fwd, szc_fwd
|
|
|
|
# ---- Backward for reversed direction (replays against masked beta/decay if any) ----
|
|
# Allocate kernel-output gradients with the exact shape the kernel
|
|
# writes — these always match the input ``beta_for_bwd_dir`` /
|
|
# ``decay_for_bwd_dir`` shapes (override or fall-back).
|
|
dqkv_bwd = torch.zeros_like(qkv)
|
|
dbeta_bwd_kernel = torch.zeros_like(beta_for_bwd_dir)
|
|
ddecay_bwd_kernel = torch.zeros_like(decay_for_bwd_dir)
|
|
|
|
_run_triton_bwd(
|
|
sv_bwd,
|
|
sz_bwd,
|
|
svc_bwd,
|
|
szc_bwd,
|
|
dqkv_bwd,
|
|
dbeta_bwd_kernel,
|
|
ddecay_bwd_kernel,
|
|
1,
|
|
beta_for_bwd_dir,
|
|
decay_for_bwd_dir,
|
|
)
|
|
del sv_bwd, sz_bwd, svc_bwd, szc_bwd
|
|
del qkv_normed, dnum, dden
|
|
|
|
# Q/K/V gradient is always summed: qkv is shared by both directions.
|
|
dqkv_fwd += dqkv_bwd
|
|
del dqkv_bwd
|
|
|
|
# Beta gradient: route depends on whether the caller supplied an
|
|
# override. With override -> keep separate (so autograd routes the
|
|
# reverse-direction grad through the caller's clone+mask op).
|
|
# Without override -> sum into the forward-direction grad
|
|
# (legacy behaviour, bit-identical to pre-extension code).
|
|
if has_beta_bwd:
|
|
dbeta = dbeta_fwd
|
|
dbeta_bwd_out: torch.Tensor | None = dbeta_bwd_kernel
|
|
else:
|
|
dbeta_fwd += dbeta_bwd_kernel
|
|
dbeta = dbeta_fwd
|
|
dbeta_bwd_out = None
|
|
del dbeta_bwd_kernel
|
|
|
|
# Decay gradient: same routing logic, independent of beta override.
|
|
if has_decay_bwd:
|
|
ddecay = ddecay_fwd
|
|
ddecay_bwd_out: torch.Tensor | None = ddecay_bwd_kernel
|
|
else:
|
|
ddecay_fwd += ddecay_bwd_kernel
|
|
ddecay = ddecay_fwd
|
|
ddecay_bwd_out = None
|
|
del ddecay_bwd_kernel
|
|
|
|
dqkv_normed = dqkv_fwd
|
|
|
|
# ---- Full-channel RMSNorm backward for Q and K ----
|
|
# y = x * inv_rms * w -> dL/dx = inv_rms*w*dL/dy - inv_rms^3/C * x * sum(w*dL/dy*x)
|
|
# Process Q and K sequentially to reduce peak fp32 memory.
|
|
|
|
# Q norm backward.
|
|
q_irms = q_inv_rms[:, :, None, None]
|
|
dq_normed = dqkv_normed[:, :, 0].float()
|
|
gw_q = dq_normed * q_nw_hd[None, None]
|
|
dq_nw = (dq_normed * q_raw * q_irms).sum(dim=(0, 1)).reshape(-1)
|
|
corr_q = (gw_q * q_raw).sum(dim=(-2, -1), keepdim=True)
|
|
dqkv_normed[:, :, 0] = (q_irms * gw_q - (q_irms**3) / C * q_raw * corr_q).to(qkv.dtype)
|
|
del dq_normed, gw_q, corr_q, q_raw, q_irms
|
|
|
|
# K norm backward.
|
|
k_irms = k_inv_rms[:, :, None, None]
|
|
dk_normed = dqkv_normed[:, :, 1].float()
|
|
gw_k = dk_normed * k_nw_hd[None, None]
|
|
dk_nw = (dk_normed * k_raw * k_irms).sum(dim=(0, 1)).reshape(-1)
|
|
corr_k = (gw_k * k_raw).sum(dim=(-2, -1), keepdim=True)
|
|
dqkv_normed[:, :, 1] = (k_irms * gw_k - (k_irms**3) / C * k_raw * corr_k).to(qkv.dtype)
|
|
del dk_normed, gw_k, corr_k, k_raw, k_irms
|
|
|
|
return (
|
|
dqkv_normed,
|
|
dbeta,
|
|
ddecay,
|
|
dq_nw.to(q_norm_weight.dtype),
|
|
dk_nw.to(k_norm_weight.dtype),
|
|
None, # rope_cos
|
|
None, # rope_sin
|
|
None, # F
|
|
None, # S
|
|
None, # k_scale
|
|
None, # norm_eps
|
|
None, # eps
|
|
dbeta_bwd_out, # beta_bwd
|
|
ddecay_bwd_out, # decay_bwd
|
|
)
|
|
|
|
|
|
def fused_bigdn_forward_with_grad(
|
|
qkv: torch.Tensor,
|
|
beta: torch.Tensor,
|
|
decay: torch.Tensor,
|
|
q_norm_weight: torch.Tensor | None,
|
|
k_norm_weight: torch.Tensor | None,
|
|
rope_cos: torch.Tensor,
|
|
rope_sin: torch.Tensor,
|
|
F: int,
|
|
S: int,
|
|
k_scale: float = 1.0,
|
|
norm_eps: float = 1e-5,
|
|
eps: float = 1e-6,
|
|
beta_bwd: torch.Tensor | None = None,
|
|
decay_bwd: torch.Tensor | None = None,
|
|
) -> torch.Tensor:
|
|
"""Bidirectional fused BiGDN with autograd support (full-channel RMSNorm).
|
|
|
|
Unlike ``fused_bigdn_func`` (which expects pre-computed ``q_inv_rms`` /
|
|
``k_inv_rms``), this wrapper computes the full-channel inv-RMS in Python
|
|
so the norm backward can flow through the autograd graph naturally.
|
|
|
|
Chunk-causal masking (optional):
|
|
Pass ``beta_bwd`` and/or ``decay_bwd`` to override the beta/decay
|
|
tensors used by the **reverse-direction** kernel only. These are
|
|
typically built by the caller as ``beta.clone()`` / ``decay.clone()``
|
|
with interior chunk-boundary frames zeroed, so the anti-causal scan
|
|
resets state at chunk boundaries while the causal scan keeps full
|
|
context. The reverse-direction beta/decay gradients are routed
|
|
back through the ``beta_bwd`` / ``decay_bwd`` slots (instead of
|
|
being summed into the forward-direction grad), which lets autograd
|
|
flow the reverse-direction gradient through the caller's
|
|
``clone() + index = 0`` masking op.
|
|
|
|
When ``beta_bwd`` / ``decay_bwd`` is ``None`` (default), behaviour
|
|
is bit-identical to the pre-extension full-sequence-bidirectional
|
|
path: the reverse-direction kernel uses the unmasked ``beta`` /
|
|
``decay`` and its kernel-emitted gradient is summed into the
|
|
forward-direction gradient before being returned.
|
|
"""
|
|
return FusedBiGDNFunction.apply(
|
|
qkv,
|
|
beta,
|
|
decay,
|
|
q_norm_weight,
|
|
k_norm_weight,
|
|
rope_cos,
|
|
rope_sin,
|
|
F,
|
|
S,
|
|
k_scale,
|
|
norm_eps,
|
|
eps,
|
|
beta_bwd,
|
|
decay_bwd,
|
|
)
|