| """ |
| Linear-attention token mixers (GatedDeltaNet / GLA) as drop-in replacements for |
| the inner attention of MoEBlock / MoEGeneratorBlock, using **method-B context |
| injection** to stay semantically identical to the softmax-attention baseline. |
| |
| Method-B injection |
| ------------------ |
| The baseline MoEBlock attention does: kv = cat([x, vlm_ctx]); attn(q=x, k=kv, v=kv) |
| i.e. each token attends to itself+others AND to the projected VLM/gen context. |
| |
| GatedDeltaNet / GLA are pure self-mixing recurrences over an input sequence (q,k,v |
| all come from the same hidden_states; there is no external-KV port). To reproduce |
| "tokens can read the VLM context", we **prepend** the projected context tokens to the |
| input sequence, run the linear-attention recurrence over [ctx ; x], then **slice off** |
| the context prefix and keep only the outputs at the x positions: |
| |
| seq = cat([ctx, x], dim=1) # ctx is already projected to hidden_size |
| out_seq = linear_attn(seq) # causal recurrence: x positions see ctx (which precedes them) |
| out = out_seq[:, ctx_len:, :] # keep only the x outputs |
| |
| Because these operators are causal, putting ctx FIRST guarantees every x token can |
| read the full context (ctx precedes all x), exactly matching the attention mask where |
| x->ctx is fully visible. The x->x interaction is causal (left-to-right), which is the |
| intended behavior for the vision expert (autoregressive image tokens) and is exactly |
| the "action causality" we want to probe on the action expert. |
| |
| This keeps the MoEBlock shell (adaLN / residual / MLP / vlm_proj / gen_proj) untouched |
| and only swaps the token-mixing operator, so an ablation attention-vs-TTT-vs-GLA-vs-GDN |
| differs ONLY in the operator. |
| """ |
| import torch |
| import torch.nn as nn |
|
|
| from .fla.layers.gated_deltanet import GatedDeltaNet |
|
|
|
|
| class LinearAttnMixer(nn.Module): |
| """Wraps a fla linear-attention layer (GatedDeltaNet / GLA) with method-B |
| context injection and the MoEBlock calling convention `forward(x, info, ctx) |
| -> (out, aux)`. |
| |
| Args: |
| kind: "gdn" (GatedDeltaNet) or "gla" (GatedLinearAttention, if vendored). |
| dim: model hidden size (= MoEBlock hidden_size). |
| num_heads: number of heads for the linear-attention layer. |
| head_dim: per-head dim (GatedDeltaNet needs num_heads*head_dim ~= 0.75*dim |
| by its own design; we pass dim//num_heads by default and let the |
| layer's own asserts guard validity). |
| """ |
|
|
| def __init__(self, kind, dim, num_heads, head_dim=None, layer_idx=0): |
| super().__init__() |
| self.kind = kind |
| self.dim = dim |
| if kind == "gdn": |
| |
| |
| |
| if head_dim is None: |
| hd = int(0.75 * dim) // num_heads |
| hd = max(16, (hd // 16) * 16) |
| else: |
| hd = head_dim |
| self.layer = GatedDeltaNet( |
| hidden_size=dim, |
| head_dim=hd, |
| num_heads=num_heads, |
| expand_v=2, |
| mode="chunk", |
| use_gate=True, |
| use_short_conv=True, |
| layer_idx=layer_idx, |
| ) |
| elif kind == "gla": |
| from .fla.layers.gla import GatedLinearAttention |
| self.layer = GatedLinearAttention( |
| hidden_size=dim, |
| num_heads=num_heads, |
| mode="chunk", |
| use_short_conv=False, |
| layer_idx=layer_idx, |
| ) |
| else: |
| raise ValueError(f"Unknown linear-attn kind: {kind}") |
|
|
| def forward(self, x, info=None, ctx=None, *args): |
| """x: [B, L, dim]; ctx: [B, M, dim] already projected (method-B). Returns |
| (out[B, L, dim], None) to match the (attn_out, _) unpacking in MoEBlock.""" |
| if ctx is not None and ctx.shape[1] > 0: |
| ctx_len = ctx.shape[1] |
| seq = torch.cat([ctx, x], dim=1) |
| else: |
| ctx_len = 0 |
| seq = x |
| out_seq, _, _ = self.layer(seq) |
| out = out_seq[:, ctx_len:, :] |
| return out, None |
|
|