Files
2026-07-13 13:09:03 +08:00

1474 lines
60 KiB
Python

"""Context-parallel wrappers for fused Triton GDN kernels.
The fused non-CP kernels use a left-multiply recurrence for the KV state,
while ``cp_frame_gdn_scan`` uses a right-multiply recurrence. This module
adapts between those state conventions so context-parallel training can keep
the Triton fused prep/output kernels and replace only the middle scan with the
distributed CP scan.
* :func:`phase_a` returns ``I_P_kv`` and ``A`` representing the raw factor
``(I - k_rot * beta * k_rot.T)`` and the input
``A_t = (v * beta) @ k_rot.T``. **Decay is NOT folded in.**
* :func:`phase_b_triton` applies decay inside the kernel:
``M_t = decay_t * (I - P_t) @ M_{t-1} + A_t``.
* :func:`_build_transition_matrices` returns
``W_kv = decay_f * (I - k_rot*beta @ k_rot.T)`` with decay pre-folded. The
downstream eager scan uses ``S_t = S_{t-1} @ W_t + U_t`` (right-multiply),
so the right-multiply state is the transpose of Phase B's left-multiply
``M_t``.
Mapping (KV):
Phase B (left-multiply, decay outside I_P_kv):
M_t = decay_t * (I - P_t) @ M_{t-1} + A_t
cp_frame_gdn_scan (right-multiply):
S_t = S_{t-1} @ W_t + U_t with S_t = M_t.T
Therefore:
W_t = (decay_t * (I - P_t)).T
U_t = A_t.T
Mapping (Z):
Phase B and cp_frame_gdn_scan both use a left-multiply Z recurrence
(``z_t = decay_t * (I - P_z) @ z_{t-1} + B_t`` vs
``S_t = W_t @ S_{t-1} + U_t``); the only difference is again that decay
is folded into ``W_z`` for the eager / cp_scan path but applied inside
the Triton kernel for Phase B. Hence
W_z = decay_t * I_P_z (with no transpose)
U_z = B_z
Precision contract:
Phase B uses fp32 recurrence state. The adapter therefore promotes
per-frame transitions and decay to fp32 before multiplying, transposing,
and slicing. Phase C consumes fp32 state as well.
The padded ``BLOCK_D`` slice ``[head_dim:BLOCK_D, :BLOCK_D]`` and
``[:BLOCK_D, head_dim:BLOCK_D]`` is structurally inert in both fused Phase B
and the eager scan (Phase A writes zeros into those tiles via masked
``tl.store``). The adapter slices the active ``D x D`` sub-block before
transposing so garbage in the padded region cannot poison the recurrence.
"""
from __future__ import annotations
from dataclasses import dataclass
import torch
from torch import Tensor
from torch.distributed import ProcessGroup
def _resolve_use_checkpoint(use_checkpoint: bool | None, *leaves: Tensor | None) -> bool:
"""Resolve the ``use_checkpoint`` argument for the fused-CP entry points.
Args:
use_checkpoint: Explicit override (``True``/``False``) or ``None`` to
auto-detect from autograd state. ``None`` resolves to ``True``
iff :func:`torch.is_grad_enabled` AND any non-None leaf has
``requires_grad=True`` -- i.e., training mode with autograd
actually active. This mirrors reference CamCtrl SFT practice
(gradient_checkpointing on during training, off during eval).
*leaves: The input tensors that participate in autograd. ``None``
entries (e.g. optional norm weights) are ignored.
Returns:
The effective ``use_checkpoint`` flag.
"""
if use_checkpoint is None:
use_checkpoint = torch.is_grad_enabled() and any((t is not None) and t.requires_grad for t in leaves)
return bool(use_checkpoint)
__all__ = [
"CpFusedGdnRawResult",
"CpFusedTransitionBundle",
"_CpFusedGdnOutput",
"_CpFusedGdnPrep",
"cp_fused_cam_gdn_num_autograd",
"cp_fused_gdn_chunkwise_raw_autograd",
"cp_scan_states_to_phase_c_states",
"phase_a_to_cp_scan_transitions",
]
@dataclass(frozen=True)
class CpFusedTransitionBundle:
"""Transitions in :func:`cp_frame_gdn_scan` convention plus adapter metadata.
Attributes:
W_kv: ``(BH, T_local, D, D)`` -- right-multiply KV transition,
equal to ``(decay_t * (I - P_t)).T`` from Phase A. Always the
active ``D x D`` sub-block (NOT padded to ``BLOCK_D``).
U_kv: ``(BH, T_local, D, D)`` -- right-multiply KV input, equal
to ``A_t.T``.
W_z: ``(BH, T_local, D, D)`` -- left-multiply Z transition,
equal to ``decay_t * I_P_z`` (no transpose). Size-0 placeholder
when ``skip_z=True``.
U_z: ``(BH, T_local, D)`` -- left-multiply Z input, equal to
``B_z``. Size-0 placeholder when ``skip_z=True``.
block_d: Padded head dimension (``triton.next_power_of_2(D)``).
Required to re-pad scan outputs for Phase C consumption.
head_dim: Active head dimension ``D``.
"""
W_kv: Tensor
U_kv: Tensor
W_z: Tensor
U_z: Tensor
block_d: int
head_dim: int
def phase_a_to_cp_scan_transitions(
I_P_kv: Tensor,
A_kv: Tensor,
I_P_z: Tensor,
B_z: Tensor,
decay: Tensor,
*,
head_dim: int,
skip_z: bool = False,
) -> CpFusedTransitionBundle:
"""Map fused Phase A tensors to :func:`cp_frame_gdn_scan` transitions.
See module docstring for the convention derivation.
Args:
I_P_kv: ``(BH, T_local, BLOCK_D, BLOCK_D)`` -- raw
``(I - k_rot * beta * k_rot.T)`` from
``_phase_a_kv_kernel``. Decay is NOT folded in. May be bf16
(Phase A inter-phase bridge at ``dot_precision=0``) or fp32
(``dot_precision>=1``); the adapter always promotes to fp32
before multiplying.
A_kv: ``(BH, T_local, BLOCK_D, BLOCK_D)`` -- raw
``(v * beta) @ k_rot.T`` from ``_phase_a_kv_kernel``. Same
dtype contract as ``I_P_kv``.
I_P_z: ``(BH, T_local, BLOCK_D, BLOCK_D)`` -- raw
``(I - k * beta * k.T)`` for the Z stream. Ignored when
``skip_z=True`` (kernel allocates a 1-element placeholder).
B_z: ``(BH, T_local, BLOCK_D)`` -- raw ``k * beta``
summed over heads-S for the Z stream. Same placeholder
convention. Always fp32 in the fused Phase A path.
decay: Either ``(BH, T_local)`` or broadcastable to that shape.
Reshaped + cast to float32 internally so the per-frame
``decay_t`` scalar can be multiplied against the
``(BLOCK_D, BLOCK_D)`` tile in a single broadcast.
head_dim: Active head dimension ``D`` (Phase A pads to
``BLOCK_D = next_pow2(D)``; we slice the active sub-block here
to defensively isolate the recurrence from any garbage in
padded tiles).
skip_z: When True, return size-0 placeholders for ``W_z`` / ``U_z``.
Used by the camera-branch numerator-only scan.
CONTRACT: A ``skip_z=True`` bundle must not be passed directly
to :func:`cp_frame_gdn_scan`; callers must provide valid dummy Z
tensors or use a numerator-only scan path.
Returns:
:class:`CpFusedTransitionBundle` whose tensor fields live in the
:func:`cp_frame_gdn_scan` recurrence convention. All tensor fields
are fp32 regardless of input dtype (see "Precision contract" in
the module docstring).
"""
if I_P_kv.ndim != 4:
raise ValueError(
f"phase_a_to_cp_scan_transitions: expected I_P_kv with 4 dims "
f"(BH, T, BLOCK_D, BLOCK_D), got shape {tuple(I_P_kv.shape)}"
)
BH, T_local, BLOCK_D, BLOCK_D2 = I_P_kv.shape
if BLOCK_D != BLOCK_D2:
raise ValueError(
f"phase_a_to_cp_scan_transitions: I_P_kv last two dims must be " f"square, got {(BLOCK_D, BLOCK_D2)}"
)
if head_dim < 1 or head_dim > BLOCK_D:
raise ValueError(
f"phase_a_to_cp_scan_transitions: head_dim={head_dim} must " f"satisfy 1 <= head_dim <= BLOCK_D={BLOCK_D}"
)
if A_kv.shape != I_P_kv.shape:
raise ValueError(
f"phase_a_to_cp_scan_transitions: A_kv shape {tuple(A_kv.shape)} " f"!= I_P_kv shape {tuple(I_P_kv.shape)}"
)
# Match Phase B's fp32 recurrence state before multiplying by decay.
I_P_kv_f32 = I_P_kv.to(torch.float32) if I_P_kv.dtype != torch.float32 else I_P_kv
A_kv_f32 = A_kv.to(torch.float32) if A_kv.dtype != torch.float32 else A_kv
# Reshape decay to (BH, T_local, 1, 1) so the broadcast multiplies each
# (BLOCK_D, BLOCK_D) tile by its scalar decay_t.
decay_view = decay.reshape(BH, T_local).to(torch.float32).view(BH, T_local, 1, 1)
# Left-multiply form (Phase B convention) on the active D x D slice.
W_kv_left = decay_view * I_P_kv_f32[..., :head_dim, :head_dim]
# cp_frame_gdn_scan uses right-multiply, so S_t = M_t.T. Therefore
# transpose every transition / input pair.
W_kv = W_kv_left.transpose(-1, -2).contiguous()
U_kv = A_kv_f32[..., :head_dim, :head_dim].transpose(-1, -2).contiguous()
if skip_z:
# NUM_ONLY camera-branch callers do not consume Z. We materialise
# size-0 placeholders rather than fake (BH, T_local, D, D) tensors
# so any accidental downstream read crashes loudly with a shape
# mismatch instead of silently producing wrong numbers.
#
# CONTRACT: callers MUST NOT hand a `skip_z=True` bundle directly
# to `cp_frame_gdn_scan`; see the function docstring under `skip_z`.
W_z = torch.empty(0, device=I_P_kv.device, dtype=torch.float32)
U_z = torch.empty(0, device=I_P_kv.device, dtype=torch.float32)
else:
# Z is already left-multiply in both conventions; just slice + fold
# decay in (same as W_z = decay_f * I_P_z in _build_transition_matrices).
I_P_z_f32 = I_P_z.to(torch.float32) if I_P_z.dtype != torch.float32 else I_P_z
B_z_f32 = B_z.to(torch.float32) if B_z.dtype != torch.float32 else B_z
W_z = (decay_view * I_P_z_f32[..., :head_dim, :head_dim]).contiguous()
U_z = B_z_f32[..., :head_dim].contiguous()
return CpFusedTransitionBundle(
W_kv=W_kv,
U_kv=U_kv,
W_z=W_z,
U_z=U_z,
block_d=BLOCK_D,
head_dim=head_dim,
)
def cp_scan_states_to_phase_c_states(
S_kv: Tensor,
S_z: Tensor,
*,
block_d: int,
) -> tuple[Tensor, Tensor]:
"""Re-pad and re-transpose cp_frame_gdn_scan output for Phase C consumption.
Phase C (:func:`phase_c` in ``fused_gdn_chunkwise.py``) expects the
state ``M_t`` in its native left-multiply convention, padded to
``BLOCK_D``. This inverts the operations performed by
:func:`phase_a_to_cp_scan_transitions` on the state side.
Args:
S_kv: ``(BH, T_local, head_dim, head_dim)`` -- corrected KV
recurrence state in :func:`cp_frame_gdn_scan` (right-multiply)
convention.
S_z: ``(BH, T_local, head_dim)`` -- corrected Z state.
block_d: Padded head dimension used by the Triton kernels.
Returns:
``(M_kv_padded, M_z_padded)`` where
``M_kv_padded`` has shape ``(BH, T_local, block_d, block_d)`` and
contents ``S_kv.T`` over the active ``head_dim`` slice with zeros
in the padded tile, and
``M_z_padded`` has shape ``(BH, T_local, block_d)`` with the
``head_dim`` slice populated and zeros in the pad.
"""
if S_kv.ndim != 4:
raise ValueError(
f"cp_scan_states_to_phase_c_states: expected S_kv with 4 dims, " f"got shape {tuple(S_kv.shape)}"
)
BH, T_local, head_dim, head_dim2 = S_kv.shape
if head_dim != head_dim2:
raise ValueError(
f"cp_scan_states_to_phase_c_states: S_kv last two dims must be " f"square, got {(head_dim, head_dim2)}"
)
if head_dim > block_d:
raise ValueError(f"cp_scan_states_to_phase_c_states: head_dim={head_dim} must " f"be <= block_d={block_d}")
M_kv_padded = torch.zeros(BH, T_local, block_d, block_d, device=S_kv.device, dtype=S_kv.dtype)
# Inverse transpose (right-multiply S -> left-multiply M).
M_kv_padded[..., :head_dim, :head_dim] = S_kv.transpose(-1, -2)
M_z_padded = torch.zeros(BH, T_local, block_d, device=S_z.device, dtype=S_z.dtype)
M_z_padded[..., :head_dim] = S_z
return M_kv_padded, M_z_padded
@dataclass(frozen=True)
class CpFusedGdnRawResult:
"""Raw numerator/denominator output of the fused GDN CP scan.
Returned by :func:`cp_fused_gdn_chunkwise_raw_autograd`. Carries the
``(num, den)`` pair plus optional terminal-state fields when the caller
requested ``truncate_to_active``.
Attributes:
num: ``(B, N_local, H, D)`` -- raw numerator before output gate /
projection / final divide. dtype matches Phase C output:
bf16 at ``dot_precision=0``, fp32 at ``dot_precision>=1``.
den: ``(B, H, N_local)`` -- raw denominator. Same dtype contract
as ``num``.
terminal_state_kv: ``(BH, D, D)`` fp32, present only when
``truncate_to_active`` was set on the call; ``None`` otherwise.
Identical on every CP rank.
terminal_state_z: ``(BH, D)`` fp32, same condition.
"""
num: Tensor
den: Tensor
terminal_state_kv: Tensor | None = None
terminal_state_z: Tensor | None = None
class _CpFusedGdnPrep(torch.autograd.Function):
"""RMSNorm + Phase A + transition adapter as a single autograd Function.
Forward composes:
1. Full-channel RMSNorm on Q and K channels of ``qkv``. V is not
normalized.
2. :func:`phase_a` on the normalized ``qkv_normed`` with identity
``inv_rms`` / norm-weight (so the Phase A kernel does no further
norm). Returns ``(I_P_kv, A, I_P_z, B_z)``.
3. :func:`phase_a_to_cp_scan_transitions` adapts to the
:func:`cp_frame_gdn_scan` convention. Returns
``CpFusedTransitionBundle(W_kv, U_kv, W_z, U_z, ...)``.
Backward composes:
1. Inverse adapter VJP: ``(dW_kv, dU_kv, dW_z, dU_z) -> (dI_P_kv,
dA, dI_P_z, dB_z, ddecay)``. ``dI_P_*`` / ``dA`` are padded back
to ``(BH, F, BLOCK_D, BLOCK_D)`` and ``dB_z`` to ``(BH, F,
BLOCK_D)`` for Phase A backward kernels.
2. :func:`phase_a_kv_bwd` on ``(dA, -dI_P_kv)`` (since the kernel
takes ``dP_kv`` and ``I_P_kv = I - P_kv``, ``dP_kv = -dI_P_kv``).
Returns ``(dK_kv_bhfsd, dV_bhfsd, dbeta_kv_bhfs)``.
3. :func:`phase_a_z_bwd` analogous -> ``(dK_z_bhfsd, dbeta_z_bhfs)``.
4. :func:`fused_rope_unrope_bwd` combines the K-channel grads
coming out of ``phase_a_*_bwd`` (with ``dQ_kv``/``dQ_z`` zero
since Q has no Phase A grad) and unrope+relu-masks them ->
``(_dQ_zero_via_relu, dK_normed_bhfsd_from_phase_a)``.
5. Add the ``dqkv_normed`` upstream grad contributions:
- Q: ``dQ_normed_total = dqkv_normed[:, :, 0]_bhfsd`` (no Phase A path).
- K: ``dK_normed_total = dK_normed_bhfsd_from_phase_a + dqkv_normed[:, :, 1]_bhfsd``.
- V: ``dV_total_bnhd = dV_from_phase_a_kv_bwd_bnhd + dqkv_normed[:, :, 2]_bnhd``.
6. Per-channel RMSNorm VJP for Q and K -> ``dQ_raw``, ``dK_raw``,
``dq_norm_w``, ``dk_norm_w``.
7. Stack ``dqkv = stack([dQ_raw, dK_raw, dV_total], dim=2)``.
The decay grad is accumulated entirely inside step 1 (since
``W_kv = decay * I_P_kv`` and ``W_z = decay * I_P_z``); the Phase A
backward kernels do not contribute to ``ddecay``.
The qkv_normed output is differentiable: ``_CpFusedGdnOutput`` consumes
it and its backward produces ``dqkv_normed`` which is summed with the
Phase A chain above to yield the full ``dqkv``.
"""
@staticmethod
def forward(
ctx,
qkv: Tensor,
beta: Tensor,
decay: Tensor,
q_norm_weight: Tensor | None,
k_norm_weight: Tensor | None,
rope_cos: Tensor,
rope_sin: Tensor,
F: int,
S: int,
k_scale: float,
norm_eps: float = 1e-5,
dot_precision: int = 0,
):
from diffusion.model.ops.fused_gdn_chunkwise import phase_a
B, N, three, H, D = qkv.shape
if three != 3:
raise ValueError(f"_CpFusedGdnPrep.forward: qkv dim 2 must equal 3, got {three}")
if N != F * S:
raise ValueError(f"_CpFusedGdnPrep.forward: N={N} must equal F*S={F * S}")
C = H * D
device = qkv.device
fp32 = torch.float32
dtype = qkv.dtype
# Track missing norm weights so backward returns None for those slots.
ctx.q_nw_was_none = q_norm_weight is None
ctx.k_nw_was_none = k_norm_weight is None
# When both weights are None, q_norm/k_norm are identity modules.
skip_rmsnorm = ctx.q_nw_was_none and ctx.k_nw_was_none
ctx.skip_rmsnorm = skip_rmsnorm
if q_norm_weight is None:
q_norm_weight = torch.ones(C, device=device, dtype=fp32)
if k_norm_weight is None:
k_norm_weight = torch.ones(C, device=device, dtype=fp32)
# 1. Full-channel RMSNorm on Q and K.
q_raw_v = qkv[:, :, 0] # view, same dtype as qkv
k_raw_v = qkv[:, :, 1]
if skip_rmsnorm:
# Identity contract: qkv_normed === qkv. Save ones for
# q_inv_rms / k_inv_rms so the backward RMSNorm-VJP
# bookkeeping is well-defined but the VJP degenerates to
# the identity.
q_inv_rms = torch.ones(B, N, device=device, dtype=fp32)
k_inv_rms = torch.ones(B, N, device=device, dtype=fp32)
qkv_normed = qkv
else:
q_inv_rms = torch.rsqrt((q_raw_v.float().pow(2)).sum(dim=(-2, -1)) / C + norm_eps)
k_inv_rms = torch.rsqrt((k_raw_v.float().pow(2)).sum(dim=(-2, -1)) / C + norm_eps)
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_v.float() * q_inv_rms[:, :, None, None] * q_nw_hd[None, None]).to(dtype)
qkv_normed[:, :, 1] = (k_raw_v.float() * k_inv_rms[:, :, None, None] * k_nw_hd[None, None]).to(dtype)
# 2. phase_a with identity inv_rms / norm_w so the kernel does no re-norm.
dummy_inv = torch.ones(B, N, device=device, dtype=fp32)
dummy_nw = torch.ones(C, device=device, dtype=fp32)
I_P_kv, A, I_P_z, B_z = phase_a(
qkv_normed,
beta,
dummy_inv,
dummy_inv,
dummy_nw,
dummy_nw,
rope_cos,
rope_sin,
F=F,
S=S,
k_scale=k_scale,
norm_eps=norm_eps,
dot_precision=dot_precision,
)
# 3. Adapter: Phase A layout -> cp_frame_gdn_scan convention.
bundle = phase_a_to_cp_scan_transitions(
I_P_kv,
A,
I_P_z,
B_z,
decay,
head_dim=D,
skip_z=False,
)
# Save for backward.
ctx.save_for_backward(
qkv,
beta,
decay,
q_norm_weight,
k_norm_weight,
q_inv_rms,
k_inv_rms,
rope_cos,
rope_sin,
I_P_kv,
A,
I_P_z,
B_z,
)
ctx.shape = (B, N, H, D, F, S, C)
ctx.k_scale = float(k_scale)
ctx.dot_precision = int(dot_precision)
return bundle.W_kv, bundle.U_kv, bundle.W_z, bundle.U_z, qkv_normed
@staticmethod
def backward(ctx, dW_kv, dU_kv, dW_z, dU_z, dqkv_normed):
# Inline imports keep top-of-file lightweight.
from diffusion.model.ops.fused_gdn_chunkwise_bwd import (
_resolve_bwd_block_s,
fused_rope_relu_fwd,
fused_rope_unrope_bwd,
phase_a_kv_bwd,
phase_a_z_bwd,
)
(
qkv,
beta,
decay,
q_norm_weight,
k_norm_weight,
q_inv_rms,
k_inv_rms,
rope_cos,
rope_sin,
I_P_kv,
A_kv,
I_P_z,
B_z,
) = ctx.saved_tensors
B, N, H, D, F, S, C = ctx.shape
k_scale = ctx.k_scale
dot_precision = ctx.dot_precision
BLOCK_S = _resolve_bwd_block_s()
device = qkv.device
fp32 = torch.float32
dtype = qkv.dtype
BH = B * H
q_nw_hd = q_norm_weight.reshape(H, D)
k_nw_hd = k_norm_weight.reshape(H, D)
# 1. Inverse adapter VJP.
# Forward adapter with full-pad slicing applied:
# W_kv_left[bh, f, :D, :D] = decay[bh, f] * I_P_kv_active[bh, f, :D, :D]
# W_kv[bh, f, d1, d2] = W_kv_left[bh, f, d2, d1] (transpose)
# U_kv[bh, f, d1, d2] = A_kv_active[bh, f, d2, d1] (transpose)
# W_z[bh, f, d1, d2] = decay[bh, f] * I_P_z_active[bh, f, d1, d2]
# U_z[bh, f, d] = B_z_active[bh, f, d]
# All cast to fp32 first; B_z is already fp32.
decay_f = decay.reshape(BH, F).to(fp32)
decay_view = decay_f.view(BH, F, 1, 1)
# Active D x D slice in fp32.
I_P_kv_active = I_P_kv[..., :D, :D].to(fp32)
I_P_z_active = I_P_z[..., :D, :D].to(fp32)
# Inputs to inverse VJP in fp32.
dW_kv_f = dW_kv.to(fp32)
dU_kv_f = dU_kv.to(fp32)
dW_z_f = dW_z.to(fp32)
dU_z_f = dU_z.to(fp32)
# Convert W_kv = (decay * I_P_kv_active).T; its VJP for I_P_kv_active is
# decay * dW_kv.T. Same logic for U_kv (just transpose, no decay).
dI_P_kv_active = decay_view * dW_kv_f.transpose(-1, -2)
dA_kv_active = dU_kv_f.transpose(-1, -2)
# W_z and U_z are not transposed (per adapter).
dI_P_z_active = decay_view * dW_z_f
dB_z_active = dU_z_f # (BH, F, D)
# ddecay contributions from W_kv and W_z (note W_kv = (decay * I_P_kv).T,
# so d/d(decay) = sum_{d1,d2}( dW_kv[d1,d2] * I_P_kv[d2,d1] )
# = sum_{d1,d2}( dW_kv * I_P_kv.T )
# which is identical to sum( dI_P_kv_active * I_P_kv_active ) / decay
# but the cleaner formulation is direct:
ddecay_from_kv = (dW_kv_f * I_P_kv_active.transpose(-1, -2)).sum(dim=(-1, -2))
ddecay_from_z = (dW_z_f * I_P_z_active).sum(dim=(-1, -2))
ddecay = (ddecay_from_kv + ddecay_from_z).reshape(B, H, F)
# Pad active grads back to BLOCK_D shape for the Triton kernels.
# (phase_a_kv_bwd / phase_a_z_bwd pad internally if needed, but we
# pass D x D which they will pad. The kernels accept either D x D or
# BLOCK_D x BLOCK_D inputs; see the `pad_DxD` helpers in those
# driver functions.)
# Sign flip: kernel expects dP (where P = I - I_P), so dP = -dI_P.
dP_kv = (-dI_P_kv_active).contiguous()
dP_z = (-dI_P_z_active).contiguous()
dA_kv_for_kernel = dA_kv_active.contiguous()
dB_z_for_kernel = dB_z_active.contiguous()
# 2. Reconstruct qkv_normed for the rope/relu recomputation.
q_raw_v = qkv[:, :, 0]
k_raw_v = qkv[:, :, 1]
skip_rmsnorm = getattr(ctx, "skip_rmsnorm", False)
if skip_rmsnorm:
qkv_normed = qkv
else:
qkv_normed = qkv.clone()
qkv_normed[:, :, 0] = (q_raw_v.float() * q_inv_rms[:, :, None, None] * q_nw_hd[None, None]).to(dtype)
qkv_normed[:, :, 1] = (k_raw_v.float() * k_inv_rms[:, :, None, None] * k_nw_hd[None, None]).to(dtype)
# 3. Recompute Q/K post-relu masks in BHFSD layout.
def bnhd_to_bhfsd(x):
return x.permute(0, 2, 1, 3).reshape(B, H, F, S, D).reshape(BH, F, S, D).contiguous()
def bhfsd_to_bnhd(x):
return x.reshape(BH, F * S, D).reshape(B, H, N, D).permute(0, 2, 1, 3).contiguous()
Q_normed_bhfsd = bnhd_to_bhfsd(qkv_normed[:, :, 0])
K_normed_bhfsd = bnhd_to_bhfsd(qkv_normed[:, :, 1])
V_bhfsd = bnhd_to_bhfsd(qkv[:, :, 2])
# fused_rope_relu_fwd returns (Q_post_relu, K_post_relu, Q_for_num, K_kv).
# We need: K_kv_bhfsd (post-rope, key chain for Phase A KV) and
# K_post_relu_bhfsd (no rope on K_z, key chain for Phase A Z).
Q_post_relu_bhfsd, K_post_relu_bhfsd, _Q_for_num_bhfsd, K_kv_bhfsd = fused_rope_relu_fwd(
Q_normed_bhfsd,
K_normed_bhfsd,
rope_cos,
rope_sin,
k_scale,
F,
S,
)
del Q_normed_bhfsd, K_normed_bhfsd
# K for the Z stream is K_post_relu (no rope applied, see Phase A
# Z kernel which does NOT apply rope to K_z).
K_z_bhfsd = K_post_relu_bhfsd
beta_bhfs = beta.reshape(BH, F, S).float()
# 4. Phase-A KV backward.
dK_kv_bhfsd, dV_bhfsd, dbeta_kv = phase_a_kv_bwd(
K_kv_bhfsd.contiguous(),
V_bhfsd.contiguous(),
beta_bhfs,
dA_kv_for_kernel,
dP_kv,
D,
BLOCK_S=BLOCK_S,
dot_precision=dot_precision,
)
# 5. Phase-A Z backward.
dK_z_bhfsd, dbeta_z = phase_a_z_bwd(
K_z_bhfsd.contiguous(),
beta_bhfs,
dB_z_for_kernel,
dP_z,
D,
BLOCK_S=BLOCK_S,
dot_precision=dot_precision,
)
# 6. Combine via fused_rope_unrope_bwd. Q has no Phase-A contribution.
dQ_zero_kv = torch.zeros_like(dK_kv_bhfsd)
dQ_zero_z = torch.zeros_like(dK_z_bhfsd)
# Note: fused_rope_unrope_bwd internally multiplies dK channel by
# k_scale (the K-side relu+scale flip). Q channel uses no scale.
# Sanity: outputs are post-RMSNorm but pre-RMSNorm-VJP gradients.
dQ_normed_via_relu_rope_bhfsd, dK_normed_from_phase_a_bhfsd = fused_rope_unrope_bwd(
dQ_zero_kv,
dK_kv_bhfsd,
dQ_zero_z,
dK_z_bhfsd,
Q_post_relu_bhfsd,
K_post_relu_bhfsd,
rope_cos,
rope_sin,
k_scale,
F,
S,
)
del dQ_zero_kv, dQ_zero_z, dK_kv_bhfsd, dK_z_bhfsd
del Q_post_relu_bhfsd, K_post_relu_bhfsd, K_kv_bhfsd
# Q's Phase A grad is structurally zero because both inputs were zero.
del dQ_normed_via_relu_rope_bhfsd
# 7. Add upstream dqkv_normed contribution.
# dqkv_normed shape: (B, N, 3, H, D), dtype = dtype.
# Channel layout: 0 = Q, 1 = K, 2 = V.
dqkv_normed_Q_bnhd = dqkv_normed[:, :, 0].contiguous()
dqkv_normed_K_bnhd = dqkv_normed[:, :, 1].contiguous()
dqkv_normed_V_bnhd = dqkv_normed[:, :, 2].contiguous()
# Convert Q to BHFSD (for RMSNorm VJP we'll bring back to BNHD).
# Q has no Phase A contribution, so dQ_normed_total_bnhd is just the
# upstream Q channel.
dQ_normed_total_bnhd = dqkv_normed_Q_bnhd.to(fp32)
# K: add upstream to phase-A-chain K grad.
dK_normed_from_phase_a_bnhd = bhfsd_to_bnhd(dK_normed_from_phase_a_bhfsd)
del dK_normed_from_phase_a_bhfsd
dK_normed_total_bnhd = dK_normed_from_phase_a_bnhd.to(fp32) + dqkv_normed_K_bnhd.to(fp32)
del dK_normed_from_phase_a_bnhd
# V: phase_a_kv_bwd's dV is in BHFSD; convert to BNHD then add upstream.
dV_from_phase_a_bnhd = bhfsd_to_bnhd(dV_bhfsd)
del dV_bhfsd
dV_total_bnhd = dV_from_phase_a_bnhd.to(fp32) + dqkv_normed_V_bnhd.to(fp32)
del dV_from_phase_a_bnhd
# 8. RMSNorm VJP.
# d/dx = inv_rms*w*d/dy - (inv_rms^3 / C) * x * sum(w*d/dy*x)
if skip_rmsnorm:
dQ_raw = dQ_normed_total_bnhd
dK_raw = dK_normed_total_bnhd
dq_nw = torch.zeros(C, device=device, dtype=fp32)
dk_nw = torch.zeros(C, device=device, dtype=fp32)
del dQ_normed_total_bnhd, dK_normed_total_bnhd
else:
q_raw_f = q_raw_v.float()
q_irms = q_inv_rms[:, :, None, None]
gw_q = dQ_normed_total_bnhd * q_nw_hd[None, None]
dq_nw = (dQ_normed_total_bnhd * q_raw_f * q_irms).sum(dim=(0, 1)).reshape(-1)
corr_q = (gw_q * q_raw_f).sum(dim=(-2, -1), keepdim=True)
dQ_raw = q_irms * gw_q - (q_irms**3) / C * q_raw_f * corr_q
del dQ_normed_total_bnhd, gw_q, corr_q, q_raw_f
k_raw_f = k_raw_v.float()
k_irms = k_inv_rms[:, :, None, None]
gw_k = dK_normed_total_bnhd * k_nw_hd[None, None]
dk_nw = (dK_normed_total_bnhd * k_raw_f * k_irms).sum(dim=(0, 1)).reshape(-1)
corr_k = (gw_k * k_raw_f).sum(dim=(-2, -1), keepdim=True)
dK_raw = k_irms * gw_k - (k_irms**3) / C * k_raw_f * corr_k
del dK_normed_total_bnhd, gw_k, corr_k, k_raw_f
# 9. Stack dqkv = [dQ_raw, dK_raw, dV_total] along the channel dim.
dqkv = torch.stack(
[dQ_raw.to(dtype), dK_raw.to(dtype), dV_total_bnhd.to(dtype)],
dim=2,
)
# 10. Reshape dbeta / ddecay to match input shapes.
dbeta_total = (dbeta_kv + dbeta_z).reshape(B, H, F, S)
# ddecay was already reshaped to (B, H, F) above.
# Preserve PyTorch's None-gradient contract for omitted norm weights.
dq_nw_out = None if ctx.q_nw_was_none else dq_nw.to(q_norm_weight.dtype)
dk_nw_out = None if ctx.k_nw_was_none else dk_nw.to(k_norm_weight.dtype)
return (
dqkv,
dbeta_total.to(beta.dtype),
ddecay.to(decay.dtype),
dq_nw_out,
dk_nw_out,
None, # rope_cos
None, # rope_sin
None, # F
None, # S
None, # k_scale
None, # norm_eps
None, # dot_precision
)
class _CpFusedGdnOutput(torch.autograd.Function):
"""Inverse state adapter + Phase C as a single autograd Function.
Forward composes one scan direction and does not accumulate reverse Phase C
state.
1. :func:`cp_scan_states_to_phase_c_states` re-pads + re-transposes the
cp_frame_gdn_scan states ``(S_kv, S_z)`` to the BLOCK_D-padded
left-multiply layout that Phase C expects.
2. :func:`phase_c` with dummy ``q_inv_rms`` / ``q_norm_w`` (RMSNorm is
already baked into ``qkv_normed`` from :class:`_CpFusedGdnPrep`).
Returns raw ``(num, den)`` BEFORE the output divide (the caller
is expected to fuse the divide downstream).
Backward composes the Phase C VJP, inverse-state-adapter VJP, and
Q-channel rope/relu VJP. K/V/decay handling lives in
:class:`_CpFusedGdnPrep`.
1. :func:`phase_c_bwd` on ``(Q_for_num_bhfsd, M_combined, dnum_bhfsd)``
-> ``(dQ_kv_bhfsd, dM_C_active)`` where ``M_combined = M_kv_active``
(CP single-direction has only forward contribution).
2. Manual Z-chain VJP:
``dQ_z = (dden * z_active).to(dtype)`` and
``dz_C = (Q_for_den.float() * dden.float()).sum(dim=2)``
3. Inverse-state-adapter VJP for ``(dS_kv, dS_z)``: the forward
inverse adapter does ``M_kv_padded[..., :D, :D] = S_kv.T`` and
``M_z_padded[..., :D] = S_z``. Its VJP is
``dS_kv = dM_C_active.transpose(-1, -2)`` and
``dS_z = dz_C_active``. ``phase_c_bwd`` already returns
``dM_C`` trimmed to the active ``D x D`` slice, so no explicit
slice is needed here.
4. Q-channel rope/relu VJP via :func:`fused_rope_unrope_bwd` with
**zero** K-channel inputs (K does not flow through Phase C; only
Q does). The returned ``dK_normed`` is structurally zero and is
discarded.
5. Assemble ``dqkv_normed``: Q channel = ``dQ_normed_bnhd``, K and V
channels are zero.
Returns 9 tensors matching the 9 forward inputs.
Notes:
* ``q_norm_weight`` is **NOT** taken as an input to Output's forward
by design. Phase C consumes ``qkv_normed`` (which already has the Q RMSNorm scale
baked in), so the kernel runs with ``dummy_nw = ones(C)``. The
gradient for ``q_norm_weight`` flows back through ``dqkv_normed``
into :class:`_CpFusedGdnPrep`'s backward, which owns the RMSNorm
VJP.
* ``z_active`` and ``M_kv_active`` correspond to the post-update
state at each frame from the CP scan output (right-multiply
convention transposed back to left-multiply). In the bidi
reference these would be ``M_fwd_full[:, 1:]`` and
``z_fwd_full[:, 1:]`` (1-shifted to align with post-update at
frame ``f``). CP's ``cp_frame_gdn_scan`` already emits the
post-update state at each frame, so no shift is needed.
"""
@staticmethod
def forward(
ctx,
qkv_normed: Tensor, # (B, N, 3, H, D) bf16, RMS-normed
rope_cos: Tensor, # (N, D) fp32, CP-local
rope_sin: Tensor, # (N, D) fp32, CP-local
S_kv: Tensor, # (BH, F, head_dim, head_dim) fp32
S_z: Tensor, # (BH, F, head_dim) fp32
block_d: int,
F: int,
S: int,
dot_precision: int = 0,
):
from diffusion.model.ops.fused_gdn_chunkwise import phase_c
B, N, three, H, D = qkv_normed.shape
if three != 3:
raise ValueError(f"_CpFusedGdnOutput.forward: qkv_normed dim 2 must equal 3, got {three}")
if N != F * S:
raise ValueError(f"_CpFusedGdnOutput.forward: N={N} must equal F*S={F * S}")
BH = B * H
if S_kv.shape != (BH, F, D, D):
raise ValueError(
f"_CpFusedGdnOutput.forward: S_kv shape {tuple(S_kv.shape)} != " f"(BH={BH}, F={F}, D={D}, D={D})"
)
if S_z.shape != (BH, F, D):
raise ValueError(f"_CpFusedGdnOutput.forward: S_z shape {tuple(S_z.shape)} != " f"(BH={BH}, F={F}, D={D})")
device = qkv_normed.device
fp32 = torch.float32
C = H * D
# 1. Inverse state adapter: cp_scan output -> Phase C state layout.
# (BH, F, head_dim, head_dim) right-multiply -> (BH, F, BLOCK_D, BLOCK_D)
# left-multiply (transpose + pad).
M_kv_padded, M_z_padded = cp_scan_states_to_phase_c_states(S_kv, S_z, block_d=block_d)
# 2. Phase C with dummy norm; qkv_normed already carries RMSNorm.
dummy_inv = torch.ones(B, N, device=device, dtype=fp32)
dummy_nw = torch.ones(C, device=device, dtype=fp32)
num, den = phase_c(
qkv_normed,
dummy_inv,
dummy_nw,
rope_cos,
rope_sin,
M_kv_padded,
M_z_padded,
F=F,
S=S,
dot_precision=dot_precision,
accumulate=False,
)
# Save for backward. We keep M_kv_padded for phase_c_bwd's M input and
# M_z_padded for the manual Z-chain VJP (we'll slice it to active D
# there). We DON'T save num/den because Output's backward only needs
# them via the divide VJP, which is done by the CALLER (Output returns
# raw num/den; the divide VJP happens outside this Function in the
# composition wrapper).
ctx.save_for_backward(
qkv_normed,
rope_cos,
rope_sin,
M_kv_padded,
M_z_padded,
)
ctx.shape = (B, N, H, D, F, S, C)
ctx.block_d = int(block_d)
ctx.dot_precision = int(dot_precision)
return num, den
@staticmethod
def backward(ctx, dnum, dden):
from diffusion.model.ops.fused_gdn_chunkwise_bwd import (
_resolve_bwd_block_s,
fused_rope_relu_fwd,
fused_rope_unrope_bwd,
phase_c_bwd,
)
(
qkv_normed,
rope_cos,
rope_sin,
M_kv_padded,
M_z_padded,
) = ctx.saved_tensors
B, N, H, D, F, S, C = ctx.shape
dot_precision = ctx.dot_precision
BLOCK_S = _resolve_bwd_block_s()
fp32 = torch.float32
dtype = qkv_normed.dtype
BH = B * H
# 1. Slice active M/z and recompute Q rope/relu intermediates. The CP
# scan already returns post-update state aligned with each frame.
M_kv_active = M_kv_padded[:, :, :D, :D].to(fp32).contiguous() # (BH, F, D, D)
z_active = M_z_padded[:, :, :D].to(fp32).contiguous() # (BH, F, D)
del M_kv_padded, M_z_padded
# 2. Recompute Q_post_relu, Q_for_num, etc. from qkv_normed.
# We need:
# - Q_for_num_bhfsd (post-rope, post-relu Q) for phase_c_bwd input
# - Q_for_den_bhfsd = Q_post_relu_bhfsd for the manual Z-chain VJP
# - Q_post_relu_bhfsd, K_post_relu_bhfsd for the relu-mask in
# fused_rope_unrope_bwd
# K_kv/K_z are not used by Output's bwd (K does not enter Phase C);
# we still receive them from fused_rope_relu_fwd but discard.
def bnhd_to_bhfsd(x):
return x.permute(0, 2, 1, 3).reshape(B, H, F, S, D).reshape(BH, F, S, D).contiguous()
def bhfsd_to_bnhd(x):
return x.reshape(BH, F * S, D).reshape(B, H, N, D).permute(0, 2, 1, 3).contiguous()
Q_normed_bhfsd = bnhd_to_bhfsd(qkv_normed[:, :, 0])
K_normed_bhfsd = bnhd_to_bhfsd(qkv_normed[:, :, 1])
# k_scale: Output's forward doesn't take k_scale (it's only consumed by
# Phase C internally and by fused_rope_relu_fwd). Phase C reads
# k_scale=1.0 implicitly because the K stream there is gated by
# `q_norm_w`, not k_norm_w. For the rope/relu recomputation of K
# (whose output we'll discard), k_scale=1.0 is harmless.
# However: fused_rope_unrope_bwd internally multiplies dK_normed by
# k_scale; since dK_kv and dK_z inputs are zero, dK_normed=0 regardless.
# So we can pass any k_scale here; use 1.0 for consistency with
# phase_c's internal expectation that the Q channel is unscaled.
k_scale_for_recompute = 1.0
Q_post_relu_bhfsd, K_post_relu_bhfsd, Q_for_num_bhfsd, _K_kv_bhfsd_unused = fused_rope_relu_fwd(
Q_normed_bhfsd,
K_normed_bhfsd,
rope_cos,
rope_sin,
k_scale_for_recompute,
F,
S,
)
del Q_normed_bhfsd, K_normed_bhfsd, _K_kv_bhfsd_unused
Q_for_den_bhfsd = Q_post_relu_bhfsd # alias: Phase C den path uses post-relu, pre-rope Q
# 3. Phase-C backward. M_combined = M_kv_active for CP
# single-direction, and phase_c_bwd returns active (BH, F, D, D).
dnum_bhfsd = bnhd_to_bhfsd(dnum)
dQ_kv_bhfsd, dM_C = phase_c_bwd(
Q_for_num_bhfsd.contiguous(),
M_kv_active,
dnum_bhfsd,
D,
BLOCK_S=BLOCK_S,
dot_precision=dot_precision,
)
del dnum_bhfsd, M_kv_active
# 4. Manual Z-chain VJP.
# In bidi: z_combined = z_fwd_full[:, 1:] + z_rev_full[:, :F].
# In CP single-direction: z_combined = z_active (no reverse term).
dden_bhfs = dden.reshape(BH, F, S).contiguous() # bf16 / fp32 same as den dtype
# dQ_z is bf16 (or matches qkv_normed dtype). Cast at the end of unrope.
# Use unsqueeze convention from the reference: dden (BH, F, S) -> (BH, F, S, 1);
# z_active (BH, F, D) -> (BH, F, 1, D); broadcast to (BH, F, S, D).
dQ_z_bhfsd = (dden_bhfs.float().unsqueeze(-1) * z_active.unsqueeze(2)).to(dtype)
# dz_C: contribution to dM_z from Q_for_den.
# Q_for_den_bhfsd is (BH, F, S, D), dden_bhfs is (BH, F, S).
dz_C = (Q_for_den_bhfsd.float() * dden_bhfs.float().unsqueeze(-1)).sum(dim=2) # (BH, F, D)
# 5. Inverse-state-adapter VJP: (dM_C, dz_C) -> (dS_kv, dS_z).
# Forward inverse adapter (cp_scan_states_to_phase_c_states):
# M_kv_padded[..., :D, :D] = S_kv.transpose(-1, -2)
# M_z_padded[..., :D] = S_z
# phase_c_bwd already returned dM_C trimmed to the active D x D slice,
# so the slice step is free. Just transpose for KV; identity for Z.
dS_kv = dM_C.transpose(-1, -2).contiguous() # (BH, F, D, D) fp32
dS_z = dz_C.contiguous() # (BH, F, D) fp32
del dM_C, dz_C, z_active
# 6. Q-channel rope/relu VJP. K does NOT enter Phase C, so
# dK_kv = dK_z = 0. fused_rope_unrope_bwd accepts zero K inputs;
# the kernel multiplies dK_normed by (K_relu > 0) * k_scale which is
# zero anyway. We discard the returned dK_normed.
dQ_kv_bhfsd_typed = dQ_kv_bhfsd.to(dtype) if dQ_kv_bhfsd.dtype != dtype else dQ_kv_bhfsd
dK_kv_zero = torch.zeros_like(dQ_kv_bhfsd_typed)
dK_z_zero = torch.zeros_like(dQ_z_bhfsd)
dQ_normed_bhfsd, _dK_normed_bhfsd_zero = fused_rope_unrope_bwd(
dQ_kv_bhfsd_typed,
dK_kv_zero,
dQ_z_bhfsd,
dK_z_zero,
Q_post_relu_bhfsd,
K_post_relu_bhfsd,
rope_cos,
rope_sin,
k_scale_for_recompute,
F,
S,
)
del dQ_kv_bhfsd, dQ_kv_bhfsd_typed, dQ_z_bhfsd, dK_kv_zero, dK_z_zero
del Q_post_relu_bhfsd, K_post_relu_bhfsd, Q_for_num_bhfsd, Q_for_den_bhfsd
del _dK_normed_bhfsd_zero # K's grad from Output is structurally zero
# Reshape BHFSD -> BNHD for the Q channel of dqkv_normed.
dQ_normed_bnhd = bhfsd_to_bnhd(dQ_normed_bhfsd)
del dQ_normed_bhfsd
# 7. Assemble dqkv_normed: Q channel = dQ_normed, K = 0, V = 0.
# K and V do not enter Phase C, so their grads from this Function are
# structurally zero. Allocate with the same dtype/device as qkv_normed.
dqkv_normed = torch.zeros_like(qkv_normed)
dqkv_normed[:, :, 0] = dQ_normed_bnhd.to(dtype)
# [:, :, 1] (K) and [:, :, 2] (V) remain zero.
del dQ_normed_bnhd
return (
dqkv_normed, # qkv_normed
None, # rope_cos
None, # rope_sin
dS_kv, # S_kv
dS_z, # S_z
None, # block_d
None, # F
None, # S
None, # dot_precision
)
def cp_fused_gdn_chunkwise_raw_autograd(
qkv: Tensor,
beta: Tensor,
decay: Tensor,
q_norm_weight: Tensor | None,
k_norm_weight: Tensor | None,
rope_cos: Tensor,
rope_sin: Tensor,
*,
F: int,
S: int,
group: ProcessGroup,
k_scale: float = 1.0,
norm_eps: float = 1e-5,
eps: float = 1e-6,
dot_precision: int = 0,
reverse_rank_order: bool = False,
truncate_to_active: int | None = None,
use_checkpoint: bool | None = None,
rope_cos_q: Tensor | None = None,
rope_sin_q: Tensor | None = None,
) -> CpFusedGdnRawResult:
"""End-to-end differentiable CP fused GDN raw entry (num, den).
Composes :class:`_CpFusedGdnPrep` -> :func:`cp_frame_gdn_scan` ->
:class:`_CpFusedGdnOutput` with **full autograd** through
``qkv``, ``beta``, ``decay``, ``q_norm_weight``, ``k_norm_weight``.
This is the reference training-path entry for the fused CP main branch.
Pipeline:
1. :class:`_CpFusedGdnPrep` fuses RMSNorm + :func:`phase_a` +
:func:`phase_a_to_cp_scan_transitions` with a hand-written VJP
that composes :func:`phase_a_kv_bwd` + :func:`phase_a_z_bwd` +
:func:`fused_rope_unrope_bwd` + RMSNorm VJP.
2. :func:`cp_frame_gdn_scan` is differentiable
(:class:`FrameGDNScan` + :class:`_CPAllGatherMerge`).
3. :class:`_CpFusedGdnOutput` fuses
:func:`cp_scan_states_to_phase_c_states` + :func:`phase_c` with
a hand-written VJP that composes :func:`phase_c_bwd` + manual
Z-chain VJP + inverse-state-adapter VJP + Q-channel
:func:`fused_rope_unrope_bwd`.
The Q channel of ``qkv`` accumulates grads ONLY from Output's
backward (RMSNorm VJP for Q lives in Prep). The K channel
accumulates grads from BOTH Output's backward (added to the
``qkv_normed`` K channel) AND Prep's backward (via Phase A KV/Z).
The V channel accumulates ONLY from Prep's backward (Phase A KV
returns ``dV``; Phase C does not consume V).
Args:
qkv: ``(B, N, 3, H, D)`` bf16/fp32 local CP-rank slice. Channel
0 = Q, 1 = K, 2 = V.
beta: ``(B, H, F, S)`` bf16/fp32 per-token update gate.
decay: ``(B, H, F)`` bf16/fp32 per-frame decay.
q_norm_weight: ``(H*D,)`` fp32 or ``None`` (defaults to ones).
k_norm_weight: ``(H*D,)`` fp32 or ``None`` (defaults to ones).
rope_cos: ``(N, D)`` fp32 CP-local RoPE cosines.
rope_sin: ``(N, D)`` fp32 CP-local RoPE sines.
F: Local frame count (``N // S``).
S: Spatial token count per frame.
group: CP process group.
k_scale: K scale factor used by Phase A (typically ``D ** -0.5``).
norm_eps: RMSNorm epsilon. Default ``1e-5``.
eps: Currently unused at this layer; reserved for the final
``num / (den + eps)`` divide done by the caller.
dot_precision: 0 = TF32 bf16 bridge (default); 1 = TF32 fp32
bridge; 2 = IEEE fp32 + fp32 bridge.
reverse_rank_order: If True, :func:`cp_frame_gdn_scan` traverses
the rank order in reverse. Used by BidirectionalGDN's
backward recurrence consumer.
truncate_to_active: terminal state at
global position ``truncate_to_active - 1`` is broadcast to
all CP ranks and returned in the result. When ``None``
(default), the result's terminal-state fields are ``None``.
use_checkpoint: When ``True``, wrap the Prep ->
``cp_frame_gdn_scan`` -> Output pipeline in
``torch.utils.checkpoint.checkpoint(use_reentrant=False)`` so
the saved tensors held by ``_CpFusedGdnPrep`` (13 tensors
incl. ``qkv``/``decay``/``I_P_kv``/``A``/``I_P_z``/``B_z``)
and ``_CpFusedGdnOutput`` (5 tensors incl.
``M_kv_padded``/``M_z_padded``) are discarded after forward
and recomputed during backward. Trades ~10-20% extra backward
compute for substantially lower forward peak memory. Mirrors
the eager ``_forward_cp_scan`` path's ``grad_checkpoint``
wrap around ``_build_transition_matrices``.
When ``None`` (default), the flag auto-detects from autograd
state: ``True`` iff :func:`torch.is_grad_enabled` AND any of
``qkv``/``beta``/``decay``/``q_norm_weight``/``k_norm_weight``
has ``requires_grad=True``. This matches reference CamCtrl
SFT practice (gradient_checkpointing default on during
training, off during eval / no_grad). When ``True``/``False``,
it is an explicit override.
Returns:
:class:`CpFusedGdnRawResult` with:
``num`` ``(B, N_local, H, D)`` raw numerator before output
divide. Grad flows back to qkv/beta/decay/norm weights.
``den`` ``(B, H, N_local)`` raw denominator. Same.
``terminal_state_kv`` / ``terminal_state_z`` fp32, present
only when ``truncate_to_active`` was set.
Notes:
* BLOCK_D is derived as ``triton.next_power_of_2(head_dim)``
where ``head_dim = S_kv.shape[-1]``. For reference D=112 this
yields BLOCK_D=128.
* Numerator-only / camera-branch (``skip_z=True``) is NOT
supported by this entry; :class:`_CpFusedGdnPrep` calls the
adapter with ``skip_z=False``.
"""
del eps # API symmetry only; final divide is done by the caller.
# Module-level local import to avoid a heavyweight top-level
# dependency on the distributed/context_parallel subtree and the
# triton package (which is a runtime-only dep of the fused kernels).
import triton
from diffusion.distributed.context_parallel.distributed_scan import (
CpFrameGdnScanResult,
cp_frame_gdn_scan,
)
use_checkpoint_resolved = _resolve_use_checkpoint(
use_checkpoint,
qkv,
beta,
decay,
q_norm_weight,
k_norm_weight,
)
rope_cos_q = rope_cos if rope_cos_q is None else rope_cos_q
rope_sin_q = rope_sin if rope_sin_q is None else rope_sin_q
def _inner_pipeline(
qkv_in: Tensor,
beta_in: Tensor,
decay_in: Tensor,
q_nw_in: Tensor | None,
k_nw_in: Tensor | None,
rope_cos_k_in: Tensor,
rope_sin_k_in: Tensor,
rope_cos_q_in: Tensor,
rope_sin_q_in: Tensor,
) -> tuple[Tensor, Tensor, Tensor | None, Tensor | None]:
"""Composes Prep -> cp_frame_gdn_scan -> Output.
Returns ``(num, den, terminal_state_kv, terminal_state_z)``;
the terminal states are ``None`` when ``truncate_to_active`` is
``None`` and tensors otherwise. ``torch.utils.checkpoint`` accepts
``None`` returns as long as the closure shape is consistent across
forward and the recomputed forward in backward, which it is here
(``truncate_to_active`` is captured from the outer scope).
"""
# 1. Prep: RMSNorm + phase_a + adapter.
W_kv, U_kv, W_z, U_z, qkv_normed = _CpFusedGdnPrep.apply(
qkv_in,
beta_in,
decay_in,
q_nw_in,
k_nw_in,
rope_cos_k_in,
rope_sin_k_in,
F,
S,
float(k_scale),
float(norm_eps),
int(dot_precision),
)
# 2. CP scan.
if truncate_to_active is None:
scan_result = cp_frame_gdn_scan(
W_kv,
U_kv,
W_z,
U_z,
group=group,
reverse=reverse_rank_order,
)
S_kv, S_z = scan_result
terminal_state_kv_inner = None
terminal_state_z_inner = None
else:
scan_result = cp_frame_gdn_scan(
W_kv,
U_kv,
W_z,
U_z,
group=group,
reverse=reverse_rank_order,
truncate_to_active=int(truncate_to_active),
)
# Defensive type check so a scan API mismatch fails before
# feeding invalid state downstream.
if not isinstance(scan_result, CpFrameGdnScanResult):
raise TypeError(
"cp_fused_gdn_chunkwise_raw_autograd: expected "
"CpFrameGdnScanResult from cp_frame_gdn_scan(truncate_to_active="
f"{truncate_to_active}), got {type(scan_result).__name__}"
)
S_kv = scan_result.S_kv_all
S_z = scan_result.S_z_all
terminal_state_kv_inner = scan_result.terminal_state_kv
terminal_state_z_inner = scan_result.terminal_state_z
# (3) BLOCK_D derivation: padded head dim for Phase C consumption.
# S_kv shape: (BH, F, head_dim, head_dim).
head_dim = S_kv.shape[-1]
block_d = triton.next_power_of_2(head_dim)
# (4) Output: inverse adapter + phase_c (autograd-aware).
num_inner, den_inner = _CpFusedGdnOutput.apply(
qkv_normed,
rope_cos_q_in,
rope_sin_q_in,
S_kv,
S_z,
int(block_d),
F,
S,
int(dot_precision),
)
return num_inner, den_inner, terminal_state_kv_inner, terminal_state_z_inner
if use_checkpoint_resolved:
from torch.utils.checkpoint import checkpoint as _grad_checkpoint
num, den, terminal_state_kv, terminal_state_z = _grad_checkpoint(
_inner_pipeline,
qkv,
beta,
decay,
q_norm_weight,
k_norm_weight,
rope_cos,
rope_sin,
rope_cos_q,
rope_sin_q,
use_reentrant=False,
)
else:
num, den, terminal_state_kv, terminal_state_z = _inner_pipeline(
qkv,
beta,
decay,
q_norm_weight,
k_norm_weight,
rope_cos,
rope_sin,
rope_cos_q,
rope_sin_q,
)
return CpFusedGdnRawResult(
num=num,
den=den,
terminal_state_kv=terminal_state_kv,
terminal_state_z=terminal_state_z,
)
def cp_fused_cam_gdn_num_autograd(
q: Tensor,
k: Tensor,
v: Tensor,
beta: Tensor,
decay: Tensor,
*,
F: int,
S: int,
group: ProcessGroup,
reverse_rank_order: bool = False,
truncate_to_active: int | None = None,
eps_recurrence: float = 0.0,
use_checkpoint: bool | None = None,
) -> tuple[Tensor, Tensor | None]:
"""End-to-end differentiable CP camera-branch (num-only) **forward** scan.
Composes pure-PyTorch transition build + the autograd-aware
:func:`cp_frame_gdn_scan` + pure-PyTorch numerator output projection
into a single autograd-correct path. The KV recurrence is
``M_t = decay_t * (I - k_rot*beta @ k_rot^T) @ M_{t-1} + (v*beta) @ k_rot^T``
(camera num-only -- no Z denominator). All ops are vanilla PyTorch
matmul/elementwise, so autograd flows back to ``q``/``k``/``v``/
``beta``/``decay`` natively without any custom VJP.
The role of this wrapper relative to the main-branch fused entry
(:func:`cp_fused_gdn_chunkwise_raw_autograd`) is more conservative:
* The main branch reuses :func:`phase_a` / :func:`phase_c` Triton
kernels with custom backward for forward speedup.
* The camera branch uses **pure-PyTorch** transition build +
output projection. The "fused" part of the camera path lives
outside this function: it is the upstream
:func:`cam_prep_func_with_grad` Triton kernel which fuses
RMSNorm + ReLU + K-scale + UCPE-projmat + RoPE on the raw QKV.
Args:
q: ``(B, H, D, N)`` -- post-UCPE+RoPE rotated camera queries.
k: ``(B, H, D, N)`` -- post-UCPE+RoPE rotated camera keys.
v: ``(B, H, D, N)`` -- post-UCPE camera values.
beta: ``(B, H, F, S)`` or ``(B, H, F)`` -- per-token update
gate (camera-discounted). Reshaped internally to ``(B, H,
F, 1, S)`` so the broadcast against ``(B, H, F, D, S)``
frame tensors works.
decay: ``(B, H, F)`` -- per-frame decay.
F: Local frame count (``N // S``).
S: Spatial token count per frame.
group: CP process group.
reverse_rank_order: Forwarded to :func:`cp_frame_gdn_scan`.
truncate_to_active: When set, ``cp_frame_gdn_scan``
masks padded positions and returns a terminal-state KV that
is broadcast to all ranks. We surface it as the second
tuple element so the caller can resume a local non-CP gen
scan from that boundary state.
eps_recurrence: API symmetry only; the camera num-only path performs
no divide so this is currently unused.
use_checkpoint: When ``True``, wrap the transition
build -> ``cp_frame_gdn_scan`` -> output projection pipeline in
``torch.utils.checkpoint.checkpoint(use_reentrant=False)`` so
the saved intermediates (``k_rot_beta``, ``W_kv``, ``U_kv``,
``S_kv_all``, ``out_5d``) are discarded after forward and
recomputed during backward. Trades ~10-20% extra backward
compute for substantially lower forward peak memory.
When ``None`` (default), auto-detect ``True`` iff
:func:`torch.is_grad_enabled` AND any of ``q``/``k``/``v``/
``beta``/``decay`` has ``requires_grad=True``. Matches
reference CamCtrl SFT practice.
Returns:
``(out_num, terminal_state_kv)`` where ``out_num`` has shape
``(B, H, D, N)`` (camera num-only output, no divide) and
``terminal_state_kv`` has shape ``(BH, D, D)`` when
``truncate_to_active`` was provided, else ``None``.
"""
del eps_recurrence # API symmetry
from diffusion.distributed.context_parallel.distributed_scan import (
CpFrameGdnScanResult,
cp_frame_gdn_scan,
)
if q.shape != k.shape or q.shape != v.shape:
raise ValueError(
f"cp_fused_cam_gdn_num_autograd: q/k/v shape mismatch -- "
f"q={tuple(q.shape)}, k={tuple(k.shape)}, v={tuple(v.shape)}"
)
if q.ndim != 4:
raise ValueError(
f"cp_fused_cam_gdn_num_autograd: expected q with 4 dims (B, H, D, N), got shape {tuple(q.shape)}"
)
B, H, D, N = q.shape
if N != F * S:
raise ValueError(f"cp_fused_cam_gdn_num_autograd: N={N} != F*S={F * S} (F={F}, S={S})")
use_checkpoint_resolved = _resolve_use_checkpoint(
use_checkpoint,
q,
k,
v,
beta,
decay,
)
def _inner_pipeline(
q_in: Tensor,
k_in: Tensor,
v_in: Tensor,
beta_in: Tensor,
decay_in: Tensor,
) -> tuple[Tensor, Tensor | None]:
"""Composes transition build -> cp_frame_gdn_scan -> output projection.
Returns ``(out, terminal_state_kv)``; the terminal state is
``None`` when ``truncate_to_active`` is ``None``.
"""
# 1. Reshape (B, H, D, N) -> frame layout (B, H, F, D, S).
# Use ``view`` + ``permute`` to match the eager camera branch layout.
def _to_frame(t: Tensor) -> Tensor:
return t.view(B, H, D, F, S).permute(0, 1, 3, 2, 4).contiguous()
q_f = _to_frame(q_in)
k_f = _to_frame(k_in)
v_f = _to_frame(v_in)
if beta_in.ndim == 4:
# beta is per-token (B, H, F, S) -- inject the D singleton at dim 3
# so the frame broadcast (B, H, F, 1, S) works against (B, H, F, D, S).
beta_f = beta_in.unsqueeze(3)
elif beta_in.ndim == 3:
# Per-frame (B, H, F) -> (B, H, F, 1, 1).
beta_f = beta_in.view(B, H, F, 1, 1)
else:
raise ValueError(f"cp_fused_cam_gdn_num_autograd: beta.ndim must be 3 or 4, got {beta_in.ndim}")
decay_f = decay_in.view(B, H, F, 1, 1)
I = torch.eye(D, device=q_in.device, dtype=q_in.dtype).reshape(1, 1, 1, D, D)
BH = B * H
# 2. Build transitions (single-path: KV only, Z zeroed).
# ``k_rot`` is used in both spots and v carries the input. Zero Z
# matches the single-path numerator-only camera path.
k_rot_beta = k_f * beta_f
W_kv = decay_f * (I - torch.matmul(k_rot_beta, k_f.transpose(-1, -2)))
U_kv = torch.matmul(v_f * beta_f, k_f.transpose(-1, -2))
W_kv = W_kv.reshape(BH, F, D, D).contiguous()
U_kv = U_kv.reshape(BH, F, D, D).contiguous()
# Z is zeroed for the single-path numerator-only scan -- the
# downstream output projection ignores the Z output and the scan's
# backward returns zero gradients through the dummy Z slot. This
# matches the eager numerator-only path.
W_z = torch.zeros(BH, F, D, D, device=q_in.device, dtype=W_kv.dtype)
U_z = torch.zeros(BH, F, D, device=q_in.device, dtype=W_kv.dtype)
# 3. Distributed scan with autograd-aware all-gather merge.
if truncate_to_active is None:
scan_result = cp_frame_gdn_scan(
W_kv,
U_kv,
W_z,
U_z,
group=group,
reverse=reverse_rank_order,
)
S_kv_all, _ = scan_result # discard zeroed Z output
terminal_state_kv_inner = None
else:
scan_result = cp_frame_gdn_scan(
W_kv,
U_kv,
W_z,
U_z,
group=group,
reverse=reverse_rank_order,
truncate_to_active=int(truncate_to_active),
)
if not isinstance(scan_result, CpFrameGdnScanResult):
raise TypeError(
"cp_fused_cam_gdn_num_autograd: expected CpFrameGdnScanResult from "
f"cp_frame_gdn_scan(truncate_to_active={truncate_to_active}), "
f"got {type(scan_result).__name__}"
)
S_kv_all = scan_result.S_kv_all
terminal_state_kv_inner = scan_result.terminal_state_kv
# 4. Output projection: out[b,h,f] = S_kv[b,h,f] @ q_rot[b,h,f].
# cp_frame_gdn_scan returns S_kv in right-multiply
# convention (S_t = S_{t-1} @ W_t + U_t), so by the transpose
# convention noted in the module docstring, M_t = S_t.T.
S_kv_5d = S_kv_all.view(B, H, F, D, D)
out_5d = torch.matmul(S_kv_5d, q_f) # (B, H, F, D, S)
# Permute back to (B, H, D, N).
out_inner = out_5d.permute(0, 1, 3, 2, 4).reshape(B, H, D, N).contiguous()
return out_inner, terminal_state_kv_inner
if use_checkpoint_resolved:
from torch.utils.checkpoint import checkpoint as _grad_checkpoint
out, terminal_state_kv = _grad_checkpoint(
_inner_pipeline,
q,
k,
v,
beta,
decay,
use_reentrant=False,
)
else:
out, terminal_state_kv = _inner_pipeline(q, k, v, beta, decay)
return out, terminal_state_kv