| """C/R-conditioned KDA recurrent linear attention expert. |
| |
| Keeps exact Q/K/V/C/R attention as the global path; this expert carries |
| long-running sequence state. Forget/write gates are conditioned on intent |
| context ``C`` and relation ``R`` glyphs. Output blend is zero-init so existing |
| checkpoints remain identity-compatible. |
| |
| Uses ``fla.ops.kda.chunk_kda`` for CUDA execution and a tensor-native reference |
| recurrence for CPU execution. A CUDA kernel/import failure is surfaced instead |
| of silently changing the production algorithm. |
| """ |
| from __future__ import annotations |
|
|
| from typing import cast |
|
|
| import torch |
| import torch.nn as nn |
| import torch.nn.functional as F |
|
|
| from resynthesis.config import GLYPH_DIM |
|
|
| RESYNTHESIS_KDA_LOG_DECAY_FLOOR = -5.0 |
|
|
|
|
| class CRConditionedKDAExpert(nn.Module): |
| """Channel-decayed recurrent attention conditioned on C/R planes.""" |
|
|
| def __init__( |
| self, |
| hidden_size: int, |
| num_heads: int, |
| *, |
| glyph_dim: int = GLYPH_DIM, |
| head_dim: int | None = None, |
| ) -> None: |
| super().__init__() |
| heads = max(1, int(num_heads)) |
| width = int(hidden_size) |
| if width % heads != 0 and head_dim is None: |
| while heads > 1 and width % heads != 0: |
| heads -= 1 |
| self.hidden_size = width |
| self.num_heads = heads |
| self.head_dim = int(head_dim) if head_dim is not None else width // heads |
| self.glyph_dim = int(glyph_dim) |
| inner = self.num_heads * self.head_dim |
| |
| |
| self.q_proj = nn.Conv1d( |
| width, |
| inner, |
| kernel_size=1, |
| groups=self.num_heads, |
| bias=False, |
| ) |
| self.k_proj = nn.Conv1d( |
| width, |
| inner, |
| kernel_size=1, |
| groups=self.num_heads, |
| bias=False, |
| ) |
| self.v_proj = nn.Conv1d( |
| width, |
| inner, |
| kernel_size=1, |
| groups=self.num_heads, |
| bias=False, |
| ) |
| self.out_proj = nn.Conv1d( |
| inner, |
| width, |
| kernel_size=1, |
| groups=self.num_heads, |
| bias=False, |
| ) |
| |
| |
| |
| |
| |
| self.output_gate_proj = nn.Linear(width, inner, bias=False) |
| self.output_norm = nn.RMSNorm(self.head_dim) |
| |
| self.forget_hidden_proj = nn.Conv1d( |
| width, |
| inner, |
| kernel_size=1, |
| groups=self.num_heads, |
| bias=True, |
| ) |
| self.forget_intent_proj = nn.Linear( |
| self.glyph_dim, |
| inner, |
| bias=False, |
| ) |
| self.forget_relation_proj = nn.Linear( |
| self.glyph_dim, |
| inner, |
| bias=False, |
| ) |
| self.write_hidden_proj = nn.Conv1d( |
| width, |
| self.num_heads, |
| kernel_size=1, |
| groups=self.num_heads, |
| bias=True, |
| ) |
| self.write_intent_proj = nn.Linear( |
| self.glyph_dim, |
| self.num_heads, |
| bias=False, |
| ) |
| self.write_relation_proj = nn.Linear( |
| self.glyph_dim, |
| self.num_heads, |
| bias=False, |
| ) |
| self.short_conv = nn.Conv1d( |
| width, |
| width, |
| kernel_size=3, |
| padding=2, |
| groups=width, |
| bias=False, |
| ) |
| |
| |
| |
| |
| |
| |
| self.decay_log_scale = nn.Parameter(torch.zeros(self.num_heads)) |
| self.blend_scale = nn.Parameter(torch.zeros(())) |
| self._reset() |
|
|
| def _reset(self) -> None: |
| for module in ( |
| self.q_proj, |
| self.k_proj, |
| self.v_proj, |
| self.out_proj, |
| ): |
| nn.init.xavier_uniform_(module.weight) |
| nn.init.xavier_uniform_(self.output_gate_proj.weight) |
| nn.init.ones_(self.output_norm.weight) |
| nn.init.zeros_(self.decay_log_scale) |
| nn.init.xavier_uniform_(self.forget_hidden_proj.weight) |
| forget_bias_t = self.forget_hidden_proj.bias |
| if forget_bias_t is None: |
| raise RuntimeError("KDA forget projection has no trained bias") |
| nn.init.zeros_(forget_bias_t) |
| nn.init.xavier_uniform_(self.forget_intent_proj.weight) |
| nn.init.xavier_uniform_(self.forget_relation_proj.weight) |
| nn.init.xavier_uniform_(self.write_hidden_proj.weight) |
| write_bias_t = self.write_hidden_proj.bias |
| if write_bias_t is None: |
| raise RuntimeError("KDA write projection has no trained bias") |
| nn.init.zeros_(write_bias_t) |
| nn.init.xavier_uniform_(self.write_intent_proj.weight) |
| nn.init.xavier_uniform_(self.write_relation_proj.weight) |
| nn.init.dirac_(self.short_conv.weight) |
| nn.init.zeros_(self.blend_scale) |
|
|
| @staticmethod |
| def _project_sequence( |
| projection: nn.Conv1d, |
| tensor: torch.Tensor, |
| ) -> torch.Tensor: |
| projected_t = cast(torch.Tensor, projection(tensor.transpose(1, 2))) |
| return projected_t.transpose(1, 2) |
|
|
| def _reshape_heads(self, tensor: torch.Tensor) -> torch.Tensor: |
| batch, seq, _ = tensor.shape |
| return tensor.view(batch, seq, self.num_heads, self.head_dim) |
|
|
| def _bounded_log_decay( |
| self, |
| forget_raw: torch.Tensor, |
| ) -> torch.Tensor: |
| """Map learned decay logits into the finite recurrent log range.""" |
|
|
| if ( |
| forget_raw.ndim != 4 |
| or forget_raw.shape[-2:] != ( |
| self.num_heads, |
| self.head_dim, |
| ) |
| ): |
| raise ValueError("KDA forget-logit geometry differs") |
| decay_scale_t = self.decay_log_scale.exp().view( |
| 1, |
| 1, |
| self.num_heads, |
| 1, |
| ) |
| return torch.sigmoid(decay_scale_t * forget_raw).mul( |
| RESYNTHESIS_KDA_LOG_DECAY_FLOOR |
| ) |
|
|
| def forward( |
| self, |
| hidden: torch.Tensor, |
| *, |
| intent_glyph_context: torch.Tensor, |
| relation_glyph_context: torch.Tensor, |
| ) -> torch.Tensor: |
| if hidden.ndim != 3 or hidden.shape[-1] != self.hidden_size: |
| raise ValueError("KDA expert hidden geometry differs") |
| if intent_glyph_context.shape[:2] != hidden.shape[:2]: |
| raise ValueError("KDA intent context geometry differs") |
| if relation_glyph_context.shape[:2] != hidden.shape[:2]: |
| raise ValueError("KDA relation context geometry differs") |
| |
| conv_in = hidden.transpose(1, 2) |
| conv_out = self.short_conv(conv_in)[..., : hidden.shape[1]].transpose(1, 2) |
| x = F.silu(conv_out) |
| intent_t = intent_glyph_context.to(dtype=x.dtype) |
| relation_t = relation_glyph_context.to(dtype=x.dtype) |
| q = self._reshape_heads(self._project_sequence(self.q_proj, x)) |
| k = self._reshape_heads(self._project_sequence(self.k_proj, x)) |
| v = self._reshape_heads(self._project_sequence(self.v_proj, x)) |
| q = F.normalize(q, dim=-1) |
| k = F.normalize(k, dim=-1) |
| |
| forget_raw = ( |
| self._project_sequence(self.forget_hidden_proj, x) |
| + self.forget_intent_proj(intent_t) |
| + self.forget_relation_proj(relation_t) |
| ).view( |
| hidden.shape[0], |
| hidden.shape[1], |
| self.num_heads, |
| self.head_dim, |
| ) |
| write_logits_t = ( |
| self._project_sequence(self.write_hidden_proj, x) |
| + self.write_intent_proj(intent_t) |
| + self.write_relation_proj(relation_t) |
| ) |
| out = self._run_kda_from_logits( |
| q, |
| k, |
| v, |
| forget_raw, |
| write_logits_t, |
| ) |
| normalized_out = self.output_norm(out) |
| flat = normalized_out.reshape(hidden.shape[0], hidden.shape[1], -1) |
| output_gate_t = torch.sigmoid(self.output_gate_proj(x)) |
| gated_flat = output_gate_t * flat |
| projected = self._project_sequence( |
| self.out_proj, |
| gated_flat.to(dtype=hidden.dtype), |
| ) |
| return torch.tanh(self.blend_scale) * projected |
|
|
| def _run_kda_from_logits( |
| self, |
| q: torch.Tensor, |
| k: torch.Tensor, |
| v: torch.Tensor, |
| forget_raw: torch.Tensor, |
| write_logits: torch.Tensor, |
| ) -> torch.Tensor: |
| """Run KDA while retaining the native fused safe-gate CUDA path. |
| |
| FLA's KDA backend owns the same lower-bounded recurrent activation. |
| Supplying the logits and per-head ``A_h`` directly lets it keep gate |
| activation, 16-token safe rescaling, and write sigmoid inside the |
| kernel. CPU and the one-token zero-state shortcut materialize the |
| identical equations explicitly. |
| """ |
|
|
| if q.is_cuda and q.shape[1] > 1: |
| from fla.ops.kda import chunk_kda |
|
|
| out, _state = chunk_kda( |
| q.contiguous(), |
| k.contiguous(), |
| v.contiguous(), |
| forget_raw.contiguous(), |
| write_logits.contiguous(), |
| use_gate_in_kernel=True, |
| use_beta_sigmoid_in_kernel=True, |
| safe_gate=True, |
| lower_bound=RESYNTHESIS_KDA_LOG_DECAY_FLOOR, |
| A_log=self.decay_log_scale.contiguous(), |
| ) |
| if not isinstance(out, torch.Tensor): |
| raise RuntimeError("FLA KDA returned a non-tensor output") |
| return out |
| return self._run_kda( |
| q, |
| k, |
| v, |
| self._bounded_log_decay(forget_raw), |
| torch.sigmoid(write_logits), |
| ) |
|
|
| def _run_kda( |
| self, |
| q: torch.Tensor, |
| k: torch.Tensor, |
| v: torch.Tensor, |
| g: torch.Tensor, |
| beta: torch.Tensor, |
| ) -> torch.Tensor: |
| if q.shape[1] == 1: |
| |
| |
| |
| |
| |
| q_t = q[:, 0] |
| k_t = k[:, 0] |
| v_t = v[:, 0] |
| beta_t = beta[:, 0].unsqueeze(-1).float() |
| scale = q_t.shape[-1] ** -0.5 |
| alignment_t = ( |
| (k_t.float() * q_t.float()).sum(dim=-1, keepdim=True) |
| * scale |
| ) |
| forget_zero_t = g[:, :1].sum(dim=-1, keepdim=True).mul(0) |
| return ( |
| beta_t.mul(v_t.float()) |
| .mul(alignment_t) |
| .to(dtype=q.dtype) |
| .unsqueeze(1) |
| + forget_zero_t.to(dtype=q.dtype) |
| ) |
|
|
| |
| |
| |
| |
| |
| |
| if q.is_cuda: |
| from fla.ops.kda import chunk_kda |
|
|
| out, _state = chunk_kda( |
| q.contiguous(), |
| k.contiguous(), |
| v.contiguous(), |
| g.contiguous(), |
| beta.contiguous(), |
| ) |
| if not isinstance(out, torch.Tensor): |
| raise RuntimeError("FLA KDA returned a non-tensor output") |
| return out |
| return self._reference_kda(q, k, v, g, beta) |
|
|
| @staticmethod |
| def _reference_kda( |
| q: torch.Tensor, |
| k: torch.Tensor, |
| v: torch.Tensor, |
| g: torch.Tensor, |
| beta: torch.Tensor, |
| ) -> torch.Tensor: |
| """O(T) reference KDA / gated delta-rule recurrence for CPU.""" |
|
|
| batch, seq, heads, dim = q.shape |
| value_dim = v.shape[-1] |
| state = q.new_zeros(batch, heads, dim, value_dim) |
| outputs = q.new_empty(batch, seq, heads, value_dim) |
| scale = dim ** -0.5 |
| for t in range(seq): |
| alpha = torch.exp(g[:, t]).clamp(0.0, 1.0) |
| bt = beta[:, t].unsqueeze(-1).unsqueeze(-1) |
| kt = k[:, t].unsqueeze(-1) |
| vt = v[:, t].unsqueeze(-2) |
| state = state * alpha.unsqueeze(-1) |
| |
| read = torch.matmul(state.transpose(-1, -2), k[:, t].unsqueeze(-1)) |
| state = state - bt * torch.matmul(kt, read.transpose(-1, -2)) |
| state = state + bt * torch.matmul(kt, vt) |
| ot = torch.matmul( |
| state.transpose(-1, -2), |
| q[:, t].mul(scale).unsqueeze(-1), |
| ).squeeze(-1) |
| outputs[:, t] = ot |
| return outputs |
|
|