Monody-Exp / tiny_gdn /kdn /layer.py
kerzgrr's picture
Monody-Exp: monody-pass-7 EMA weights
5c63f8f verified
Raw History Blame Contribute Delete
10.7 kB
"""Diagonal Kalman Delta Network token mixer (arXiv:2609.07816)."""
from __future__ import annotations
import math
from typing import Any
import torch
from einops import rearrange
from fla.layers.utils import get_layer_cache, update_layer_cache
from fla.modules import FusedRMSNormGated, ShortConvolution
from torch import nn
from tiny_gdn.kdn import recurrence
from tiny_gdn.kdn.kernels import (
_diag_kdn_frontend_precumsum_paired,
_diag_kdn_gain,
_diag_kdn_normalize,
chunk_kalman,
)
SUPPORTED_HEAD_DIMS = (64, 128)
class DiagonalKalmanDeltaNet(nn.Module):
"""Delta-rule memory whose write strength is a per-channel Kalman gain.
Each head keeps a diagonal posterior covariance over its key channels:
channels that have absorbed little evidence take large corrections and
well-determined ones small corrections, replacing KDA's scalar ``beta``.
Stateless CUDA BF16 calls run the fused Triton kernels. Cached prefill,
decode, padding masks, and other dtypes use the exact PyTorch recurrences.
The cache holds ``recurrent_state = (memory [B,H,K,K], precision [B,H,K])``
in float32 plus the three short-convolution states.
"""
def __init__(
self,
hidden_size: int,
head_dim: int,
num_heads: int,
*,
conv_size: int = 4,
norm_eps: float = 1e-5,
layer_idx: int | None = None,
) -> None:
super().__init__()
if head_dim not in SUPPORTED_HEAD_DIMS:
raise ValueError(
f"Diagonal KDN head_dim must be one of {SUPPORTED_HEAD_DIMS}, got {head_dim}"
)
if num_heads <= 0 or hidden_size <= 0:
raise ValueError("hidden_size and num_heads must be positive")
if conv_size <= 0:
raise ValueError("conv_size must be positive")
self.hidden_size = hidden_size
self.head_dim = head_dim
self.num_heads = num_heads
self.key_dim = num_heads * head_dim
self.layer_idx = layer_idx
self.scale = head_dim**-0.5
self.info_scale = float(head_dim)
self.q_proj = nn.Linear(hidden_size, self.key_dim, bias=False)
self.k_proj = nn.Linear(hidden_size, self.key_dim, bias=False)
self.v_proj = nn.Linear(hidden_size, self.key_dim, bias=False)
self.q_conv1d, self.k_conv1d, self.v_conv1d = (
ShortConvolution(
hidden_size=self.key_dim,
kernel_size=conv_size,
bias=False,
activation="silu",
)
for _ in range(3)
)
self.f_proj = nn.Sequential(
nn.Linear(hidden_size, head_dim, bias=False),
nn.Linear(head_dim, self.key_dim, bias=False),
)
self.A_log = nn.Parameter(
torch.log(torch.empty(num_heads, dtype=torch.float32).uniform_(1, 16))
)
self.A_log._no_weight_decay = True
dt = torch.exp(
torch.rand(self.key_dim, dtype=torch.float32)
* (math.log(0.1) - math.log(0.001))
+ math.log(0.001)
).clamp(min=1e-4)
self.dt_bias = nn.Parameter(dt + torch.log(-torch.expm1(-dt)))
self.dt_bias._no_weight_decay = True
self.omega_proj = nn.Sequential(
nn.Linear(hidden_size, head_dim, bias=False),
nn.Linear(head_dim, self.key_dim, bias=True),
)
self.r_proj = nn.Linear(hidden_size, num_heads, bias=True)
self.initial_precision_param = nn.Parameter(
torch.full(
(num_heads,),
recurrence.initial_precision_logit(),
dtype=torch.float32,
)
)
self.initial_precision_param._no_weight_decay = True
self.g_proj = nn.Sequential(
nn.Linear(hidden_size, head_dim, bias=False),
nn.Linear(head_dim, self.key_dim, bias=True),
)
self.o_norm = FusedRMSNormGated(head_dim, activation="sigmoid", eps=norm_eps)
self.o_proj = nn.Linear(self.key_dim, hidden_size, bias=False)
self.reset_noise_parameters()
def reset_noise_parameters(self) -> None:
"""Isotropic process noise and a neutral observation-noise bias at init.
Blanket initializers overwrite these, so callers re-run this afterwards.
"""
nn.init.zeros_(self.omega_proj[1].weight)
nn.init.zeros_(self.omega_proj[1].bias)
nn.init.zeros_(self.r_proj.bias)
def _fused_supported(self, hidden_states: torch.Tensor) -> bool:
return hidden_states.is_cuda and hidden_states.dtype == torch.bfloat16
def _split_heads(self, tensor: torch.Tensor) -> torch.Tensor:
return rearrange(tensor, "... (h d) -> ... h d", d=self.head_dim).contiguous()
def _fused(
self,
q: torch.Tensor,
k: torch.Tensor,
v: torch.Tensor,
retention_raw: torch.Tensor,
omega_raw: torch.Tensor,
r_raw: torch.Tensor,
) -> torch.Tensor:
q_memory, k_gain, k_memory = _diag_kdn_normalize(q, k)
g_cumsum, alpha, omega, r, initial_precision = (
_diag_kdn_frontend_precumsum_paired(
retention_raw,
omega_raw,
r_raw,
self.A_log.float().contiguous(),
self.dt_bias.float().view(self.num_heads, self.head_dim).contiguous(),
self.initial_precision_param.float().contiguous(),
)
)
kappa = _diag_kdn_gain(
k_gain, alpha, omega, r, initial_precision, omega_raw, r_raw
)
return chunk_kalman(q_memory, k_memory, kappa, v, g_cumsum, self.scale)
def _stateful(
self,
q: torch.Tensor,
k: torch.Tensor,
v: torch.Tensor,
retention_raw: torch.Tensor,
omega_raw: torch.Tensor,
r_raw: torch.Tensor,
state: tuple[torch.Tensor, torch.Tensor] | None,
token_mask: torch.Tensor | None,
) -> tuple[torch.Tensor, tuple[torch.Tensor, torch.Tensor]]:
log_alpha, omega, r, initial_precision = recurrence.kalman_noise(
retention_raw,
omega_raw,
r_raw,
self.A_log,
self.dt_bias,
self.initial_precision_param,
)
q, k = recurrence.normalize_query_key(q, k)
v = v.to(q.dtype)
if token_mask is not None:
k, v, log_alpha, omega = recurrence.mask_padding(
token_mask, k, v, log_alpha, omega
)
if state is None:
memory, precision = recurrence.initial_state(
initial_precision,
batch_size=q.shape[0],
key_dim=self.head_dim,
value_dim=self.head_dim,
)
else:
memory, precision = state
if q.shape[1] == 1:
output, memory, precision = recurrence.diag_kdn_step(
q[:, 0],
k[:, 0],
v[:, 0],
log_alpha[:, 0],
omega[:, 0],
r[:, 0],
memory,
precision,
scale=self.scale,
info_scale=self.info_scale,
)
output = output[:, None]
else:
output, memory, precision = recurrence.diag_kdn_chunked(
q,
k,
v,
log_alpha,
omega,
r,
memory,
precision,
scale=self.scale,
info_scale=self.info_scale,
)
return output, (memory, precision)
def forward(
self,
hidden_states: torch.Tensor,
attention_mask: torch.Tensor | None = None,
past_key_values: Any | None = None,
use_cache: bool = False,
output_attentions: bool = False,
**kwargs: Any,
) -> tuple[torch.Tensor, None, Any | None]:
if hidden_states.ndim != 3 or hidden_states.shape[-1] != self.hidden_size:
raise ValueError(
f"hidden_states must have shape [B, T, {self.hidden_size}]"
)
if attention_mask is not None and attention_mask.ndim != 2:
raise ValueError("attention_mask must be a [batch, sequence] padding mask")
if output_attentions:
raise ValueError("Diagonal KDN has no attention weights to return")
length = hidden_states.shape[1]
last_state = get_layer_cache(self, past_key_values)
token_mask = (
None
if attention_mask is None
else attention_mask[:, -length:].to(device=hidden_states.device, dtype=torch.bool)
)
conv_mask = (
None if token_mask is None else token_mask.to(hidden_states.dtype)
)
conv_q, conv_k, conv_v = (
(None, None, None) if last_state is None else last_state["conv_state"]
)
q, conv_q = self.q_conv1d(
x=self.q_proj(hidden_states),
mask=conv_mask,
cache=conv_q,
output_final_state=use_cache,
)
k, conv_k = self.k_conv1d(
x=self.k_proj(hidden_states),
mask=conv_mask,
cache=conv_k,
output_final_state=use_cache,
)
v, conv_v = self.v_conv1d(
x=self.v_proj(hidden_states),
mask=conv_mask,
cache=conv_v,
output_final_state=use_cache,
)
q, k, v = (self._split_heads(tensor) for tensor in (q, k, v))
retention_raw = self._split_heads(self.f_proj(hidden_states))
omega_raw = self._split_heads(self.omega_proj(hidden_states))
r_raw = self.r_proj(hidden_states).contiguous()
recurrent_state = None if last_state is None else last_state["recurrent_state"]
stateless = recurrent_state is None and token_mask is None and not use_cache
if stateless and self._fused_supported(hidden_states):
output = self._fused(q, k, v, retention_raw, omega_raw, r_raw)
else:
output, recurrent_state = self._stateful(
q, k, v, retention_raw, omega_raw, r_raw, recurrent_state, token_mask
)
update_layer_cache(
self,
past_key_values,
recurrent_state=recurrent_state,
conv_state=(conv_q, conv_k, conv_v),
offset=length,
)
output = self.o_norm(
output.to(v.dtype),
self._split_heads(self.g_proj(hidden_states)),
)
output = self.o_proj(rearrange(output, "b t h d -> b t (h d)"))
return output, None, past_key_values