"""Fugue: masked-diffusion language models on a bidirectional Gated DeltaNet-2 hybrid. Fugue keeps Senryu-2's block (4-branch Gated Residual stream, SiTU-GLU, full-rank sigmoid output gates) and makes every token mixer bidirectional: - Bidirectional GDN-2 reads each segment left to right and right to left over one set of q/k/v, erase/write and output projections. Each direction has its own short convolutions and low-rank decay (ViG). The backward scan runs on the segment reversed in place, so both scans share ``cu_seqlens``, and it reads its state before the current token (Hydra), so each token's own key and value enter its output once. - Bidirectional attention is grouped-query softmax attention with per-head QK-norm, partial RoPE and a sigmoid output gate. Masked diffusion (MDLM, LLaDA): each window draws a mask ratio t and every token becomes ``<|mask|>`` with probability t. The model predicts the originals; ``<|mask|>`` itself always gets probability zero. There is no time input; the mask count carries it. The trainer picks the per-token weight: 1/t for the ELBO, or 1 (unweighted masked cross-entropy). Evaluation always uses the ELBO (docs/architecture.md, "Fugue"). Decoding runs equal-length canvases as a batch rather than packed, with no host sync, so a CUDA graph can capture each call: ``canvas_logits`` scores picked positions. It can also cache everything outside the block being denoised (Fast-dLLM's DualCache): ``refresh_block`` runs the whole canvas once and keeps, per layer, each scan's state and conv window where it enters the block and the attention keys and values; ``block_logits`` then runs only the block's tokens against them. Text outside the block keeps its features from the refresh. Fugue-1 (``fugue_block_size`` > 0) writes fixed-size blocks left to right instead, over a causal clean stream whose cache is exact; ``tiny_gdn.fugue_block`` holds that mode's training forward and decoding, and the whole-sequence methods here refuse it. """ from __future__ import annotations import itertools import math from collections.abc import Callable, Iterator from dataclasses import dataclass, field from pathlib import Path from typing import Any import torch import torch.nn.functional as F from einops import rearrange, repeat from fla.modules import FusedRMSNormGated, ShortConvolution from fla.modules.rotary import rotary_embedding from fla.ops.gdn2 import chunk_gdn2 from fla.ops.gdn2.fused_recurrent import fused_recurrent_gdn2_fwd from safetensors.torch import save_model from torch import nn from torch.utils.checkpoint import checkpoint from tiny_gdn.config import FUGUE_MODEL_TYPE, TinyGDNConfig from tiny_gdn.fused_ops import weighted_linear_cross_entropy from tiny_gdn.haiku_layers import SiTUGLU from tiny_gdn.model import TinyGDNOutput from tiny_gdn.nn_common import RMSNorm from tiny_gdn.senryu2 import ( GatedResidual, RotaryTables, mark_adamw_only, mark_gain, mark_muon_row_blocks, ) IGNORE_INDEX = -100 # Smallest mask ratio drawn in training; the 1/t weight stays below 1,000. MIN_MASK_RATIO = 1e-3 # --------------------------------------------------------------------------- layout @dataclass(frozen=True) class SegmentLayout: """Segments packed back to back in one row, and index maps within each segment.""" cu_seqlens: torch.Tensor # Each position's mirror inside its segment; applying it twice is the identity. reverse_index: torch.Tensor # The next and previous position in the same segment; segment ends map to themselves. next_index: torch.Tensor previous_index: torch.Tensor segment_start: torch.Tensor @dataclass(frozen=True) class ScanEdge: """One scan direction of a GDN-2 layer where it enters the block: its state and q/k/v conv windows.""" state: torch.Tensor convs: tuple[torch.Tensor, torch.Tensor, torch.Tensor] @dataclass(frozen=True) class GatedDeltaNetEdges: forward: ScanEdge backward: ScanEdge @dataclass(frozen=True) class AttentionContext: """Rotated keys and values of the whole canvas; each block step overwrites the block's rows.""" keys: torch.Tensor values: torch.Tensor LayerCache = GatedDeltaNetEdges | AttentionContext @dataclass class BlockCapture: """What a decoding forward keeps for later block-only steps. Per scan direction, in scan order, ``lead`` marks the tokens the scan reads before it reaches the block (the prefix forwards, the suffix backwards) and ``lead_count`` counts them. Both are tensors, so one CUDA graph serves every block. """ lead: tuple[torch.Tensor, torch.Tensor] lead_count: tuple[torch.Tensor, torch.Tensor] edges: dict[tuple[int, int], ScanEdge] = field(default_factory=dict) attention: dict[int, AttentionContext] = field(default_factory=dict) @classmethod def around(cls, positions: torch.Tensor, length: int) -> BlockCapture: """The capture for the block at consecutive ``positions`` of ``length``-token rows.""" scan = torch.arange(length, device=positions.device) before = positions[0] after = length - 1 - positions[-1] return cls(lead=(scan < before, scan < after), lead_count=(before, after)) @dataclass(frozen=True) class BlockCache: """Everything outside the block ``[start, stop)`` as the last full forward saw it. ``offsets`` (the block start per row) and ``positions`` are tensors, so a CUDA graph captured against one cache serves every block of the canvas once ``copy_`` has loaded that block's cache into it. """ layers: tuple[LayerCache, ...] rotary: tuple[torch.Tensor, torch.Tensor] offsets: torch.Tensor positions: torch.Tensor @property def rows(self) -> int: return self.offsets.shape[0] @property def width(self) -> int: return self.positions.shape[0] def tensors(self) -> Iterator[torch.Tensor]: for layer in self.layers: if isinstance(layer, AttentionContext): yield from (layer.keys, layer.values) else: for edge in (layer.forward, layer.backward): yield edge.state yield from edge.convs yield from self.rotary yield from (self.offsets, self.positions) def clone(self) -> BlockCache: def edge(source: ScanEdge) -> ScanEdge: q, k, v = (conv.clone() for conv in source.convs) return ScanEdge(state=source.state.clone(), convs=(q, k, v)) return BlockCache( layers=tuple( AttentionContext(keys=layer.keys.clone(), values=layer.values.clone()) if isinstance(layer, AttentionContext) else GatedDeltaNetEdges(forward=edge(layer.forward), backward=edge(layer.backward)) for layer in self.layers ), rotary=(self.rotary[0].clone(), self.rotary[1].clone()), offsets=self.offsets.clone(), positions=self.positions.clone(), ) def copy_(self, source: BlockCache) -> None: for target, value in zip(self.tensors(), source.tensors(), strict=True): target.copy_(value) @dataclass class FugueContext: """Per-forward layout shared by every block. Training and evaluation pack all rows into one ``[1, length]`` sequence split by ``layout.cu_seqlens``. Decoding runs ``[rows, length]`` canvases that are each one whole segment as a batch (``packed=False``), so no kernel needs the boundaries on the host. """ length: int layout: SegmentLayout # Set when every segment has this length, so attention runs as one batched call. segment_length: int | None rotary: tuple[torch.Tensor, torch.Tensor] capture: BlockCapture | None = None rows: int = 1 packed: bool = True @property def cu_seqlens(self) -> torch.Tensor | None: return self.layout.cu_seqlens if self.packed else None def whole_row_layout(length: int, device: torch.device) -> SegmentLayout: """The layout of rows that are each one segment of ``length``, built without a host sync.""" positions = torch.arange(length, device=device) return SegmentLayout( cu_seqlens=torch.arange(2, device=device, dtype=torch.int32) * length, reverse_index=positions.flip(0), next_index=(positions + 1).clamp(max=length - 1), previous_index=(positions - 1).clamp(min=0), segment_start=positions.eq(0), ) def segment_layout(lengths: torch.Tensor) -> SegmentLayout: lengths = lengths.to(torch.long) cu_seqlens = F.pad(lengths.cumsum(0), (1, 0)) total = int(cu_seqlens[-1]) positions = torch.arange(total, device=lengths.device) segment = torch.repeat_interleave(torch.arange(lengths.numel(), device=lengths.device), lengths) offset = positions - cu_seqlens[segment] segment_start = offset.eq(0) segment_end = offset.eq(lengths[segment] - 1) return SegmentLayout( cu_seqlens=cu_seqlens.to(torch.int32), reverse_index=cu_seqlens[segment] + cu_seqlens[segment + 1] - 1 - positions, next_index=torch.where(segment_end, positions, positions + 1), previous_index=torch.where(segment_start, positions, positions - 1), segment_start=segment_start, ) # ------------------------------------------------------------------- token mixers class BidirectionalGatedDeltaNet2(nn.Module): """GDN-2 in both directions over shared projections, averaged before the output gate. S_t = (I - k_t (b_t ⊙ k_t)ᵀ) Diag(exp g_t) S_{t-1} + k_t (w_t ⊙ v_t)ᵀ runs left to right and right to left; o_t = ½ q_tᵀ (S→_t + S←_{t+1}). Index 0 of each per-direction module or parameter is the forward scan, index 1 the backward one. """ def __init__(self, config: TinyGDNConfig, layer_index: int) -> None: super().__init__() hidden = config.hidden_size self.layer_index = layer_index self.num_heads = config.linear_num_heads self.num_v_heads = config.linear_num_value_heads self.head_k_dim = config.linear_head_dim self.head_v_dim = int(config.linear_head_dim * config.linear_expand_v) self.key_dim = self.num_heads * self.head_k_dim self.value_dim = self.num_v_heads * self.head_v_dim self.allow_neg_eigval = config.allow_negative_eigenvalues kernel = config.linear_conv_kernel_dim self.q_proj = nn.Linear(hidden, self.key_dim, bias=False) self.k_proj = nn.Linear(hidden, self.key_dim, bias=False) self.v_proj = nn.Linear(hidden, self.value_dim, bias=False) self.q_conv1d = nn.ModuleList( ShortConvolution(self.key_dim, kernel, bias=False, activation="silu") for _ in range(2) ) self.k_conv1d = nn.ModuleList( ShortConvolution(self.key_dim, kernel, bias=False, activation="silu") for _ in range(2) ) self.v_conv1d = nn.ModuleList( ShortConvolution(self.value_dim, kernel, bias=False, activation="silu") for _ in range(2) ) self.f_proj = nn.ModuleList( nn.Sequential( nn.Linear(hidden, self.head_v_dim, bias=False), nn.Linear(self.head_v_dim, self.key_dim, bias=False), ) for _ in range(2) ) self.b_proj = nn.Linear(hidden, self.key_dim, bias=False) self.w_proj = nn.Linear(hidden, self.value_dim, bias=False) self.A_log = nn.Parameter(torch.empty(2, self.num_heads, dtype=torch.float32).uniform_(1, 16).log()) dt = torch.exp( torch.rand(2, 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))) if config.linear_full_rank_gate: self.g_proj: nn.Module = nn.Linear(hidden, self.value_dim, bias=True) mark_adamw_only(self.g_proj.weight) else: self.g_proj = nn.Sequential( nn.Linear(hidden, self.head_v_dim, bias=False), nn.Linear(self.head_v_dim, self.value_dim, bias=True), ) mark_adamw_only(*(layer.weight for layer in self.g_proj)) self.o_norm = FusedRMSNormGated(self.head_v_dim, activation="sigmoid", eps=config.rms_norm_eps) self.o_proj = nn.Linear(self.value_dim, hidden, bias=False) mark_gain(self.A_log, self.dt_bias) mark_adamw_only(*(layer.weight for decay in self.f_proj for layer in decay)) for projection in (self.q_proj, self.k_proj, self.b_proj): mark_muon_row_blocks(projection.weight, self.num_heads) for projection in (self.v_proj, self.w_proj): mark_muon_row_blocks(projection.weight, self.num_v_heads) def convolutions(self, direction: int) -> tuple[ShortConvolution, ShortConvolution, ShortConvolution]: return self.q_conv1d[direction], self.k_conv1d[direction], self.v_conv1d[direction] def heads( self, direction: int, q: torch.Tensor, k: torch.Tensor, v: torch.Tensor, f: torch.Tensor, b: torch.Tensor, w: torch.Tensor, ) -> tuple[torch.Tensor, ...]: """Per-head ``(q, k, v, g, b, w)`` for the scan kernels, decay applied and key heads repeated.""" g = F.softplus(f + self.dt_bias[direction]) q, k, g, b = (rearrange(tensor, "... (h d) -> ... h d", d=self.head_k_dim) for tensor in (q, k, g, b)) v, w = (rearrange(tensor, "... (h d) -> ... h d", d=self.head_v_dim) for tensor in (v, w)) g = -self.A_log[direction].float().exp().unsqueeze(-1) * g if self.num_v_heads > self.num_heads: groups = self.num_v_heads // self.num_heads q, k, g, b = (repeat(tensor, "... h d -> ... (h g) d", g=groups) for tensor in (q, k, g, b)) return q, k, v, g, b, w def scan( self, direction: int, hidden_states: torch.Tensor, projected: tuple[torch.Tensor, ...], layout: SegmentLayout, cu_seqlens: torch.Tensor | None, capture: BlockCapture | None = None, ) -> torch.Tensor: """One direction over every segment of ``layout``; ``cu_seqlens`` None scans rows that are one segment each.""" f = self.f_proj[direction](hidden_states).float() q, k, v, f, b, w = (*projected[:3], f, *projected[3:]) if direction == 1: q, k, v, f, b, w = (tensor.index_select(1, layout.reverse_index) for tensor in (q, k, v, f, b, w)) unconvolved = (q, k, v) q_conv, k_conv, v_conv = self.convolutions(direction) q, _ = q_conv(x=q, cu_seqlens=cu_seqlens) k, _ = k_conv(x=k, cu_seqlens=cu_seqlens) v, _ = v_conv(x=v, cu_seqlens=cu_seqlens) if direction == 1: # Each query reads the state one step early; its output moves back a step below. q = q.index_select(1, layout.next_index) q, k, v, g, b, w = self.heads(direction, q, k, v, f, b, w) if capture is not None: self._capture_edge(direction, unconvolved, (k, v, g, b, w), capture) out, _ = chunk_gdn2( q=q, k=k, v=v, g=g, b=b, w=w, initial_state=None, output_final_state=False, use_qk_l2norm_in_kernel=True, cu_seqlens=cu_seqlens, ) if direction == 0: return out out = out.index_select(1, layout.previous_index).masked_fill(layout.segment_start[None, :, None, None], 0.0) return out.index_select(1, layout.reverse_index) def _capture_edge( self, direction: int, unconvolved: tuple[torch.Tensor, torch.Tensor, torch.Tensor], scanned: tuple[torch.Tensor, ...], capture: BlockCapture, ) -> None: """Keep this direction's state and conv windows after the tokens it reads before the block. The scan reruns over the whole row with zero key and decay past the lead, which leaves the state unchanged, so its shape is the same for every block. """ lead, count = capture.lead[direction], capture.lead_count[direction] width = self.q_conv1d[direction].kernel_size[0] # A conv window holds the last `width` inputs, oldest first, zeros before the row starts. slots = count - width + torch.arange(width, device=lead.device) present = slots.ge(0)[None, :, None] q_window, k_window, v_window = ( (tensor.index_select(1, slots.clamp(min=0)) * present).transpose(1, 2).contiguous() for tensor in unconvolved ) k, v, g, b, w = scanned keep = lead[None, :, None, None] k = k * keep _, state = chunk_gdn2( q=k, k=k, v=v, g=g * keep, b=b, w=w, output_final_state=True, use_qk_l2norm_in_kernel=True ) capture.edges[(self.layer_index, direction)] = ScanEdge(state=state, convs=(q_window, k_window, v_window)) def scan_block( self, direction: int, hidden_states: torch.Tensor, projected: tuple[torch.Tensor, ...], edge: ScanEdge, ) -> torch.Tensor: """``scan`` over ``[rows, width]`` block tokens, entering from ``edge``.""" f = self.f_proj[direction](hidden_states).float() q, k, v, f, b, w = (*projected[:3], f, *projected[3:]) if direction == 1: q, k, v, f, b, w = (tensor.flip(1) for tensor in (q, k, v, f, b, w)) # The kernels update conv windows in place; the cache must stay as the refresh left it. q_conv, k_conv, v_conv = self.convolutions(direction) q, _ = q_conv(x=q, cache=edge.convs[0].clone()) k, _ = k_conv(x=k, cache=edge.convs[1].clone()) v, _ = v_conv(x=v, cache=edge.convs[2].clone()) q, k, v, g, b, w = self.heads(direction, q, k, v, f, b, w) if direction == 1: # A leading slot with zero key, value and decay passes the edge state # through, so query i reads the state before token i (Hydra). q = F.pad(q, (0, 0, 0, 0, 0, 1)) k, v, g, b, w = (F.pad(tensor, (0, 0, 0, 0, 1, 0)) for tensor in (k, v, g, b, w)) out, _ = fused_recurrent_gdn2_fwd( q=q.contiguous(), k=k.contiguous(), v=v.contiguous(), g=g.contiguous(), b=b.contiguous(), w=w.contiguous(), initial_state=edge.state, use_qk_l2norm_in_kernel=True, ) if direction == 0: return out return out[:, :-1].flip(1) def project(self, hidden_states: torch.Tensor) -> tuple[torch.Tensor, ...]: b = self.b_proj(hidden_states).sigmoid() if self.allow_neg_eigval: b = b * 2.0 return ( self.q_proj(hidden_states), self.k_proj(hidden_states), self.v_proj(hidden_states), b, self.w_proj(hidden_states).sigmoid(), ) def gated_output(self, out: torch.Tensor, hidden_states: torch.Tensor) -> torch.Tensor: gate = rearrange(self.g_proj(hidden_states), "... (h d) -> ... h d", d=self.head_v_dim) out = self.o_norm(out, gate) return self.o_proj(rearrange(out, "b t h d -> b t (h d)")) def forward(self, hidden_states: torch.Tensor, context: FugueContext) -> torch.Tensor: projected = self.project(hidden_states) out = 0.5 * ( self.scan(0, hidden_states, projected, context.layout, context.cu_seqlens, context.capture) + self.scan(1, hidden_states, projected, context.layout, context.cu_seqlens, context.capture) ) return self.gated_output(out, hidden_states) def forward_block(self, hidden_states: torch.Tensor, cache: LayerCache, block: BlockCache) -> torch.Tensor: """``forward`` for ``[rows, width]`` block tokens between the cached edges.""" if not isinstance(cache, GatedDeltaNetEdges): raise TypeError(f"layer {self.layer_index} needs GDN-2 edges, got {type(cache).__name__}") projected = self.project(hidden_states) out = 0.5 * ( self.scan_block(0, hidden_states, projected, cache.forward) + self.scan_block(1, hidden_states, projected, cache.backward) ) return self.gated_output(out, hidden_states) class BidirectionalAttention(nn.Module): """Grouped-query softmax attention over each whole segment. Per-head zero-centered RMSNorm on q and k, partial RoPE, and a full-rank sigmoid output gate, as in Senryu-2's Parallax layers without the causal mask. """ def __init__(self, config: TinyGDNConfig, layer_index: int) -> None: super().__init__() self.layer_index = layer_index self.num_heads = config.num_attention_heads self.num_kv_heads = config.num_key_value_heads self.head_dim = config.attention_head_dim self.scale = self.head_dim**-0.5 query_width = self.num_heads * self.head_dim kv_width = self.num_kv_heads * self.head_dim hidden = config.hidden_size self.q_proj = nn.Linear(hidden, query_width, bias=False) self.k_proj = nn.Linear(hidden, kv_width, bias=False) self.v_proj = nn.Linear(hidden, kv_width, bias=False) self.g_proj = nn.Linear(hidden, query_width, bias=True) self.o_proj = nn.Linear(query_width, hidden, bias=False) self.q_norm = RMSNorm(self.head_dim, config.rms_norm_eps) self.k_norm = RMSNorm(self.head_dim, config.rms_norm_eps) mark_muon_row_blocks(self.q_proj.weight, self.num_heads) for projection in (self.k_proj, self.v_proj): mark_muon_row_blocks(projection.weight, self.num_kv_heads) mark_adamw_only(self.g_proj.weight) def attend(self, q: torch.Tensor, k: torch.Tensor, v: torch.Tensor) -> torch.Tensor: """``[n, T, H, D]`` queries over ``[n, T, H_kv, D]`` keys/values, no mask.""" out = F.scaled_dot_product_attention( q.transpose(1, 2), k.transpose(1, 2), v.transpose(1, 2), scale=self.scale, enable_gqa=self.num_kv_heads != self.num_heads, ) return out.transpose(1, 2) def forward(self, hidden_states: torch.Tensor, context: FugueContext) -> torch.Tensor: rows, length = hidden_states.shape[:2] q = self.q_norm(self.q_proj(hidden_states).view(rows, length, self.num_heads, self.head_dim)) k = self.k_norm(self.k_proj(hidden_states).view(rows, length, self.num_kv_heads, self.head_dim)) v = self.v_proj(hidden_states).view(rows, length, self.num_kv_heads, self.head_dim) cosine, sine = context.rotary q, k = ( rotary_embedding(tensor, cosine, sine, seqlen_offsets=0, cu_seqlens=context.cu_seqlens) for tensor in (q, k) ) if context.capture is not None: context.capture.attention[self.layer_index] = AttentionContext(keys=k, values=v) if context.segment_length is not None: segments = rows * length // context.segment_length out = self.attend( q.view(segments, context.segment_length, self.num_heads, self.head_dim), k.view(segments, context.segment_length, self.num_kv_heads, self.head_dim), v.view(segments, context.segment_length, self.num_kv_heads, self.head_dim), ).reshape(rows, length, self.num_heads, self.head_dim) else: bounds = context.layout.cu_seqlens.tolist() out = torch.cat( [ self.attend(q[:, start:stop], k[:, start:stop], v[:, start:stop]) for start, stop in itertools.pairwise(bounds) ], dim=1, ) gate = torch.sigmoid(self.g_proj(hidden_states)).view(rows, length, self.num_heads, self.head_dim) return self.o_proj((out * gate).reshape(rows, length, self.num_heads * self.head_dim)) def forward_block(self, hidden_states: torch.Tensor, cache: LayerCache, block: BlockCache) -> torch.Tensor: """``[rows, width]`` block queries over the cached canvas, with the block's own keys and values.""" if not isinstance(cache, AttentionContext): raise TypeError(f"layer {self.layer_index} needs attention context, got {type(cache).__name__}") rows, width = hidden_states.shape[:2] q = self.q_norm(self.q_proj(hidden_states).view(rows, width, self.num_heads, self.head_dim)) k = self.k_norm(self.k_proj(hidden_states).view(rows, width, self.num_kv_heads, self.head_dim)) v = self.v_proj(hidden_states).view(rows, width, self.num_kv_heads, self.head_dim) cosine, sine = block.rotary q, k = (rotary_embedding(tensor, cosine, sine, seqlen_offsets=block.offsets) for tensor in (q, k)) cache.keys.index_copy_(1, block.positions, k) cache.values.index_copy_(1, block.positions, v) out = self.attend(q, cache.keys, cache.values) gate = torch.sigmoid(self.g_proj(hidden_states)).view(rows, width, self.num_heads, self.head_dim) return self.o_proj((out * gate).reshape(rows, width, self.num_heads * self.head_dim)) # ------------------------------------------------------------------------- blocks class FugueBlock(nn.Module): def __init__(self, config: TinyGDNConfig, layer_index: int) -> None: super().__init__() self.layer_index = layer_index self.layer_type = config.layer_types[layer_index] residual = (config.residual_branches, config.hidden_size, config.residual_gate_rank, config.rms_norm_eps) self.mixer_residual = GatedResidual(*residual, write=True) self.mlp_residual = GatedResidual(*residual, write=True) if self.layer_type == "bigdn2": self.token_mixer: nn.Module = BidirectionalGatedDeltaNet2(config, layer_index) elif self.layer_type == "bidirectional": self.token_mixer = BidirectionalAttention(config, layer_index) else: raise ValueError(f"Fugue has no {self.layer_type} layers") self.mlp = SiTUGLU(config) mark_muon_row_blocks(self.mlp.gate_up_proj.weight, 2) def mix(self, residual: torch.Tensor, mixer: Callable[[torch.Tensor], torch.Tensor]) -> torch.Tensor: hidden_states, scales = self.mixer_residual.read(residual) residual = GatedResidual.write(residual, scales, mixer(hidden_states).reshape(hidden_states.shape)) hidden_states, scales = self.mlp_residual.read(residual) return GatedResidual.write(residual, scales, self.mlp(hidden_states)) def forward(self, residual: torch.Tensor, context: FugueContext) -> torch.Tensor: return self.mix( residual, lambda hidden: self.token_mixer(hidden.view(context.rows, context.length, -1), context) ) def forward_block(self, residual: torch.Tensor, cache: LayerCache, block: BlockCache) -> torch.Tensor: return self.mix( residual, lambda hidden: self.token_mixer.forward_block(hidden.view(block.rows, block.width, -1), cache, block), ) # -------------------------------------------------------------------------- noise def stratified_mask_ratios( count: int, *, generator: torch.Generator, device: torch.device | str, minimum: float = MIN_MASK_RATIO, ) -> torch.Tensor: """``count`` low-discrepancy mask ratios in ``[minimum, 1]``: one uniform offset, evenly spaced.""" offset = torch.rand((), generator=generator, device=device) strata = (offset + torch.arange(count, device=device, dtype=torch.float32) / count) % 1.0 order = torch.randperm(count, generator=generator, device=device) return minimum + (1.0 - minimum) * strata[order] LOSS_WEIGHTINGS = ("elbo", "unweighted") def mask_tokens( tokens: torch.Tensor, valid: torch.Tensor, ratios: torch.Tensor, *, mask_token_id: int, generator: torch.Generator, weighting: str = "elbo", ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]: """Forward noise: ``(noisy ids, labels with -100 where kept, per-token weights)`` for ``[B, T]`` tokens. Weights are 1/t for the ELBO and 1 for unweighted masked cross-entropy. """ if weighting not in LOSS_WEIGHTINGS: raise ValueError(f"weighting must be one of: {', '.join(LOSS_WEIGHTINGS)}") draws = torch.rand(tokens.shape, generator=generator, device=tokens.device) masked = (draws < ratios[:, None]) & valid noisy = torch.where(masked, torch.full_like(tokens, mask_token_id), tokens) labels = torch.where(masked, tokens, torch.full_like(tokens, IGNORE_INDEX)) row_weights = 1.0 / ratios if weighting == "elbo" else torch.ones_like(ratios) return noisy, labels, row_weights[:, None].expand_as(tokens) def mask_exact_counts( tokens: torch.Tensor, valid: torch.Tensor, counts: torch.Tensor, *, mask_token_id: int, generator: torch.Generator, ) -> tuple[torch.Tensor, torch.Tensor]: """Mask exactly ``counts[b]`` valid positions of each row, uniformly: ``(noisy ids, labels)``. With ``l ~ U{1..L}`` masked and weight L/l, this is LLaDA's lower-variance ELBO estimator; per token it is the mean CE over the masked positions. """ draws = torch.rand(tokens.shape, generator=generator, device=tokens.device).masked_fill(~valid, 2.0) ranks = draws.argsort(dim=1).argsort(dim=1) masked = ranks < counts[:, None] noisy = torch.where(masked, torch.full_like(tokens, mask_token_id), tokens) labels = torch.where(masked, tokens, torch.full_like(tokens, IGNORE_INDEX)) return noisy, labels # -------------------------------------------------------------------------- model class FugueForMaskedDiffusion(nn.Module): """Bidirectional GDN-2 hybrid that predicts the tokens hidden behind ``<|mask|>``.""" def __init__(self, config: TinyGDNConfig) -> None: super().__init__() if config.model_type != FUGUE_MODEL_TYPE or config.mask_token_id is None: raise ValueError("FugueForMaskedDiffusion needs a Fugue config with a mask_token_id") self.config = config self.mask_token_id = config.mask_token_id # Fugue-1's block size; 0 for whole-sequence denoising (Rondo). self.block_size = config.fugue_block_size self.embed_tokens = nn.Embedding(config.vocab_size, config.hidden_size) self.layers = nn.ModuleList(FugueBlock(config, index) for index in range(config.num_hidden_layers)) self.final_residual = GatedResidual( config.residual_branches, config.hidden_size, config.residual_gate_rank, config.rms_norm_eps, write=False, ) self.rotary = RotaryTables(config.rotary_dim, config.rope_theta) self.checkpointed_layers: frozenset[int] = frozenset() self.apply(self._initialize_module) self._initialize_residual_projections() # ---------------------------------------------------------------- setup def _initialize_module(self, module: nn.Module) -> None: if isinstance(module, nn.Linear): nn.init.normal_(module.weight, mean=0.0, std=self.config.initializer_range) if module.bias is not None: nn.init.zeros_(module.bias) elif isinstance(module, nn.Embedding): nn.init.normal_(module.weight, mean=0.0, std=self.config.initializer_range) def _initialize_residual_projections(self) -> None: residual_std = self.config.initializer_range / math.sqrt(2 * self.config.num_hidden_layers) for layer in self.layers: nn.init.normal_(layer.token_mixer.o_proj.weight, mean=0.0, std=residual_std) nn.init.normal_(layer.mlp.down_proj.weight, mean=0.0, std=residual_std) def enable_gradient_checkpointing(self, enabled: bool = True, *, layers: int | None = None) -> None: """Recompute block activations in backward; ``layers`` picks GDN-2 blocks first.""" if not enabled: if layers is not None: raise ValueError("layers needs gradient checkpointing enabled") self.checkpointed_layers = frozenset() return count = len(self.layers) if layers is None else layers if not 1 <= count <= len(self.layers): raise ValueError(f"layers must be between 1 and {len(self.layers)}") cheapest_replay_first = sorted( range(len(self.layers)), key=lambda index: self.layers[index].layer_type != "bigdn2", ) self.checkpointed_layers = frozenset(cheapest_replay_first[:count]) def describe_gradient_checkpointing(self) -> str: if not self.checkpointed_layers: return "off" layer_types = sorted({self.layers[index].layer_type for index in self.checkpointed_layers}) return f"{len(self.checkpointed_layers)}/{len(self.layers)} blocks ({', '.join(layer_types)})" def project_to_vocabulary(self, hidden_states: torch.Tensor) -> torch.Tensor: """Logits over the vocabulary; ``<|mask|>`` is never a prediction (logit -inf).""" logits = F.linear(hidden_states, self.embed_tokens.weight) logits[..., self.mask_token_id] = float("-inf") return logits # -------------------------------------------------------------- forward def _require_whole_sequence(self) -> None: if self.block_size: raise ValueError( "this Fugue model denoises blocks over a causal clean stream " f"(fugue_block_size={self.block_size}); train and decode it with tiny_gdn.fugue_block" ) def _run_block(self, index: int, residual: torch.Tensor, context: FugueContext) -> torch.Tensor: layer = self.layers[index] if self.training and index in self.checkpointed_layers: return checkpoint(layer, residual, context, use_reentrant=False) return layer(residual, context) def forward( self, input_ids: torch.Tensor, attention_mask: torch.Tensor | None = None, *, labels: torch.Tensor | None = None, label_weights: torch.Tensor | None = None, loss_normalizer: float | None = None, z_loss_coefficient: float = 0.0, return_logits: bool = True, logits_mask: torch.Tensor | None = None, return_hidden_states: bool = False, ) -> TinyGDNOutput: """Score masked positions of ``[B, T]`` rows; each row is one bidirectional segment. ``labels`` holds the original token at scored positions and -100 elsewhere; ``loss = Σ w·CE / loss_normalizer`` (default: the number of scored positions). ``logits_mask`` limits logits to its true positions and returns them flattened ``[selected, vocab]`` in row-major order. """ self._require_whole_sequence() if input_ids.ndim != 2: raise ValueError("input_ids must have shape [batch, sequence]") batch, length = input_ids.shape if length > self.config.max_position_embeddings: raise ValueError("Input exceeds max_position_embeddings") device = input_ids.device if attention_mask is None or bool(attention_mask.all()): index = None lengths = torch.full((batch,), length, dtype=torch.long, device=device) segment_length: int | None = length else: if attention_mask.shape != input_ids.shape: raise ValueError("attention_mask must match input_ids") valid = attention_mask.reshape(-1).bool() index = valid.nonzero().flatten() lengths = attention_mask.bool().sum(dim=1) if bool((lengths == 0).any()): raise ValueError("every row needs at least one unmasked token") segment_length = None def pack(tensor: torch.Tensor) -> torch.Tensor: flat = tensor.reshape(batch * length, *tensor.shape[2:]) return flat if index is None else flat.index_select(0, index) tokens = pack(input_ids) embeddings = self.embed_tokens(tokens) context = FugueContext( length=tokens.numel(), layout=segment_layout(lengths), segment_length=segment_length, rotary=self.rotary.get(tokens.numel(), device, embeddings.dtype), ) residual = embeddings.unsqueeze(1).expand(-1, self.config.residual_branches, -1) for layer_index in range(len(self.layers)): residual = self._run_block(layer_index, residual, context) hidden_states, _ = self.final_residual.read(residual) total_loss = main_loss = z_loss = None if labels is not None: if labels.shape != input_ids.shape: raise ValueError("labels must match input_ids") targets = pack(labels) scored = targets.ne(IGNORE_INDEX).nonzero().flatten() weights = ( torch.ones(scored.numel(), dtype=torch.float32, device=device) if label_weights is None else pack(label_weights).index_select(0, scored).float() ) normalizer = float(scored.numel()) if loss_normalizer is None else float(loss_normalizer) if scored.numel() == 0: total_loss = hidden_states.sum() * 0.0 main_loss = total_loss.detach() z_loss = total_loss.detach() else: objective, losses, log_partitions = weighted_linear_cross_entropy( hidden_states.index_select(0, scored), self.embed_tokens.weight, targets.index_select(0, scored), weights, z_loss_coefficient=z_loss_coefficient, excluded_token_id=self.mask_token_id, ) total_loss = objective / max(normalizer, 1.0) main_loss = (weights * losses).sum() / max(normalizer, 1.0) z_loss = log_partitions.square().mean() logits = None if return_logits: if logits_mask is not None: selected = pack(logits_mask).nonzero().flatten() logits = self.project_to_vocabulary(hidden_states.index_select(0, selected)) elif index is None: logits = self.project_to_vocabulary(hidden_states).view(batch, length, -1) else: full = hidden_states.new_zeros(batch * length, hidden_states.shape[-1]) full.index_copy_(0, index, hidden_states) logits = self.project_to_vocabulary(full).view(batch, length, -1) return TinyGDNOutput( loss=total_loss, logits=logits, main_loss=main_loss, mtp_loss=None, z_loss=z_loss if z_loss_coefficient > 0.0 else None, hidden_states=hidden_states if return_hidden_states else None, ) # ------------------------------------------------------------- decoding def _decode(self, canvas: torch.Tensor, capture: BlockCapture | None) -> torch.Tensor: """Final hidden states ``[rows, T, hidden]`` of unpadded canvases, run as a batch without host syncs.""" self._require_whole_sequence() rows, length = canvas.shape if length > self.config.max_position_embeddings: raise ValueError("Input exceeds max_position_embeddings") embeddings = self.embed_tokens(canvas.reshape(-1)) context = FugueContext( length=length, layout=whole_row_layout(length, canvas.device), segment_length=length, rotary=self.rotary.get(length, canvas.device, embeddings.dtype), capture=capture, rows=rows, packed=False, ) residual = embeddings.unsqueeze(1).expand(-1, self.config.residual_branches, -1) for layer in self.layers: residual = layer(residual, context) hidden_states, _ = self.final_residual.read(residual) return hidden_states.view(rows, length, -1) def canvas_logits(self, canvas: torch.Tensor, positions: torch.Tensor) -> torch.Tensor: """Logits ``[rows, picks, vocab]`` at ``positions`` ``[rows, picks]`` of ``[rows, T]`` canvases.""" hidden_states = self._decode(canvas, None) picked = hidden_states.gather(1, positions.unsqueeze(-1).expand(-1, -1, hidden_states.shape[-1])) return self.project_to_vocabulary(picked) def refresh_block(self, canvas: torch.Tensor, positions: torch.Tensor) -> tuple[torch.Tensor, BlockCache]: """Full forward of ``[rows, T]`` canvases: logits ``[rows, width, vocab]`` for the block and its cache. ``positions`` holds the block's consecutive canvas positions, after at least one token. The cache keeps ``positions`` itself, so a graph that refreshes from a static tensor caches a block that later calls can move. """ rows, length = canvas.shape capture = BlockCapture.around(positions, length) hidden_states = self._decode(canvas, capture) logits = self.project_to_vocabulary(hidden_states.index_select(1, positions)) layers: list[LayerCache] = [] for index, layer in enumerate(self.layers): if layer.layer_type == "bigdn2": layers.append(GatedDeltaNetEdges(forward=capture.edges[(index, 0)], backward=capture.edges[(index, 1)])) else: layers.append(capture.attention[index]) cache = BlockCache( layers=tuple(layers), rotary=self.rotary.get(length, canvas.device, self.embed_tokens.weight.dtype), offsets=positions[:1].to(torch.int32).repeat(rows), positions=positions, ) return logits, cache def block_logits(self, block_ids: torch.Tensor, cache: BlockCache) -> torch.Tensor: """Logits ``[rows, width, vocab]`` for the block's current ``[rows, width]`` tokens against ``cache``.""" self._require_whole_sequence() if block_ids.shape != (cache.rows, cache.width): raise ValueError("block_ids must match the cached block") embeddings = self.embed_tokens(block_ids.reshape(-1)) residual = embeddings.unsqueeze(1).expand(-1, self.config.residual_branches, -1) for layer, layer_cache in zip(self.layers, cache.layers, strict=True): residual = layer.forward_block(residual, layer_cache, cache) hidden_states, _ = self.final_residual.read(residual) return self.project_to_vocabulary(hidden_states).view(cache.rows, cache.width, -1) # ------------------------------------------------------------ reporting def parameter_report(self) -> dict[str, int]: total = sum(parameter.numel() for parameter in self.parameters()) embeddings = self.embed_tokens.weight.numel() return { "deployable_core": total, "training_total": total, "embedding": embeddings, "mtp_auxiliary": 0, "non_embedding_core": total - embeddings, "ngram_table": 0, } def save_checkpoint(self, output_dir: Path) -> None: output_dir.mkdir(parents=True, exist_ok=True) self.config.save_json(output_dir / "config.json") save_model(self, output_dir / "model.safetensors") def extra_repr(self) -> str: return f"core_parameters={self.parameter_report()['deployable_core']:,}" def get_architecture_metadata(self) -> dict[str, Any]: if self.block_size: objective = [ ( f"block diffusion over {self.block_size}-token blocks: a causal clean stream with a next-token " "loss, and a noisy stream denoising each block against the clean prefix (exact block cache)" ), "token editing: corrupted visible tokens are trained back to the originals", "3:1 Gated DeltaNet-2 + attention hybrid, bidirectional inside the active block", ( "GDN-2 noisy reads: the clean forward state where the block starts, plus forward and " "backward scans inside the block (exclusive backward read, Hydra)" ), ] else: objective = [ "masked diffusion (MDLM/LLaDA, SUBS: mask never predicted, low-discrepancy mask ratios, no time input)", "3:1 bidirectional Gated DeltaNet-2 + bidirectional attention hybrid", ( "bidirectional GDN-2: shared projections, per-direction short convs and low-rank decay, " "exclusive backward read (Hydra)" ), ] return { "architecture": self.config.architecture, "layer_types": list(self.config.layer_types), "effective_num_layers": self.config.effective_num_layers, "shared_layer_indices": [], "parameter_report": self.parameter_report(), "features": [ *objective, "4-branch Gated Residual stream (gated read, per-branch write scales)", "full-rank sigmoid output gates", "partial RoPE, per-head QK normalization", "SiTU-GLU", "tied input-output embeddings", ], }