| """ |
| Mamba3 SISO — pure-PyTorch reference implementation. |
| |
| Architecture per: https://goombalab.github.io/blog/2026/mamba3-part2/ |
| - Exponential-trapezoidal discretization with data-dependent lambda |
| - Complex SSM via RoPE on B/C projections (data-dependent angles) |
| - Data-dependent A via softplus gate from in_proj |
| - BCNorm for training stability |
| - in_proj layout: [z, x, B, C, dd_dt, dd_A, trap, angles] |
| |
| No Triton/TileLang dependencies — sequential scan, CPU-compatible. |
| """ |
|
|
| import math |
| import torch |
| import torch.nn as nn |
| import torch.nn.functional as F |
| from dataclasses import dataclass, field |
| from typing import Optional |
|
|
|
|
| @dataclass |
| class Mamba3Config: |
| d_model: int = 2560 |
| n_layer: int = 64 |
| vocab_size: int = 50288 |
| d_state: int = 128 |
| expand: int = 2 |
| headdim: int = 64 |
| ngroups: int = 1 |
| rope_fraction: float = 0.5 |
| dt_min: float = 0.001 |
| dt_max: float = 0.1 |
| dt_init_floor: float = 1e-4 |
| is_safe_A: bool = True |
| tie_embeddings: bool = True |
| pad_vocab_size_multiple: int = 16 |
|
|
|
|
| class Mamba3Mixer(nn.Module): |
| def __init__(self, config: Mamba3Config, layer_idx: Optional[int] = None): |
| super().__init__() |
| self.d_model = config.d_model |
| self.d_state = config.d_state |
| self.headdim = config.headdim |
| self.ngroups = config.ngroups |
| self.is_safe_A = config.is_safe_A |
| self.layer_idx = layer_idx |
|
|
| self.d_inner = int(config.expand * config.d_model) |
| assert self.d_inner % self.headdim == 0 |
| self.nheads = self.d_inner // self.headdim |
| self.heads_per_group = self.nheads // self.ngroups |
|
|
| |
| rope_dim = int(config.d_state * config.rope_fraction) |
| if rope_dim % 2 != 0: |
| rope_dim -= 1 |
| self.num_rope_angles = rope_dim // 2 |
|
|
| |
| self.d_in_proj = ( |
| 2 * self.d_inner |
| + 2 * self.d_state * self.ngroups |
| + 3 * self.nheads |
| + self.num_rope_angles |
| ) |
| self.in_proj = nn.Linear(self.d_model, self.d_in_proj, bias=False) |
|
|
| |
| _dt = torch.exp( |
| torch.rand(self.nheads) * (math.log(config.dt_max) - math.log(config.dt_min)) |
| + math.log(config.dt_min) |
| ).clamp(min=config.dt_init_floor) |
| self.dt_bias = nn.Parameter(_dt + torch.log(-torch.expm1(-_dt))) |
| self.dt_bias._no_weight_decay = True |
|
|
| |
| self.B_bias = nn.Parameter(torch.ones(self.nheads, self.d_state)) |
| self.C_bias = nn.Parameter(torch.ones(self.nheads, self.d_state)) |
|
|
| |
| self.B_norm = nn.RMSNorm(self.d_state, eps=1e-5) |
| self.C_norm = nn.RMSNorm(self.d_state, eps=1e-5) |
|
|
| |
| self.D = nn.Parameter(torch.ones(self.nheads)) |
| self.D._no_weight_decay = True |
|
|
| self.out_proj = nn.Linear(self.d_inner, self.d_model, bias=False) |
|
|
| @staticmethod |
| def _apply_rope(bc: torch.Tensor, cum_angles: torch.Tensor) -> torch.Tensor: |
| """ |
| Apply rotary embedding to B or C. |
| bc: (B, L, G, d_state) |
| cum_angles: (B, L, num_rope_angles) — cumulative rotation angles |
| Rotates the first 2*nr dimensions of d_state, leaves the rest. |
| """ |
| nr = cum_angles.shape[-1] |
| bc_rot, bc_static = bc[..., :2 * nr], bc[..., 2 * nr:] |
|
|
| x1, x2 = bc_rot[..., :nr], bc_rot[..., nr:] |
| cos_a = torch.cos(cum_angles).unsqueeze(2) |
| sin_a = torch.sin(cum_angles).unsqueeze(2) |
|
|
| return torch.cat([x1 * cos_a - x2 * sin_a, |
| x1 * sin_a + x2 * cos_a, |
| bc_static], dim=-1) |
|
|
| def forward(self, hidden_states: torch.Tensor, inference_params=None) -> torch.Tensor: |
| batch, seqlen, _ = hidden_states.shape |
|
|
| proj = self.in_proj(hidden_states) |
| z, x, B_raw, C_raw, dd_dt, dd_A, trap_raw, angles_raw = torch.split( |
| proj, |
| [ |
| self.d_inner, self.d_inner, |
| self.d_state * self.ngroups, |
| self.d_state * self.ngroups, |
| self.nheads, self.nheads, self.nheads, |
| self.num_rope_angles, |
| ], |
| dim=-1, |
| ) |
|
|
| |
| A = -F.softplus(dd_A.float()) |
| if self.is_safe_A: |
| A = A - 1.0 |
| DT = F.softplus(dd_dt.float() + self.dt_bias) |
| ADT = A * DT |
| alpha = torch.exp(ADT) |
|
|
| |
| trap = torch.sigmoid(trap_raw.float()) |
|
|
| |
| B_raw = B_raw.view(batch, seqlen, self.ngroups, self.d_state) |
| C_raw = C_raw.view(batch, seqlen, self.ngroups, self.d_state) |
| B_raw = self.B_norm(B_raw) |
| C_raw = self.C_norm(C_raw) |
| |
| B_bias_g = self.B_bias.view(self.ngroups, self.heads_per_group, self.d_state)[:, 0, :] |
| C_bias_g = self.C_bias.view(self.ngroups, self.heads_per_group, self.d_state)[:, 0, :] |
| B_raw = B_raw + B_bias_g |
| C_raw = C_raw + C_bias_g |
|
|
| |
| cum_angles = torch.cumsum(angles_raw.float(), dim=1) |
|
|
| |
| B = self._apply_rope(B_raw, cum_angles) |
| C = self._apply_rope(C_raw, -cum_angles) |
|
|
| |
| B = B.repeat_interleave(self.heads_per_group, dim=2) |
| C = C.repeat_interleave(self.heads_per_group, dim=2) |
|
|
| x = x.view(batch, seqlen, self.nheads, self.headdim) |
|
|
| |
| |
| h = torch.zeros(batch, self.nheads, self.headdim, self.d_state, |
| dtype=torch.float32, device=hidden_states.device) |
| x_prev = torch.zeros(batch, self.nheads, self.headdim, |
| dtype=torch.float32, device=hidden_states.device) |
| B_prev = torch.zeros(batch, self.nheads, self.d_state, |
| dtype=torch.float32, device=hidden_states.device) |
|
|
| outputs = [] |
| for t in range(seqlen): |
| alpha_t = alpha[:, t].view(batch, self.nheads, 1, 1) |
| DT_t = DT[:, t].view(batch, self.nheads, 1, 1) |
| trap_t = trap[:, t].view(batch, self.nheads, 1, 1) |
|
|
| B_t = B[:, t].float() |
| C_t = C[:, t].float() |
| x_t = x[:, t].float() |
|
|
| bx_curr = torch.einsum("bhp,bhs->bhps", x_t, B_t) |
| bx_prev = torch.einsum("bhp,bhs->bhps", x_prev, B_prev) |
|
|
| h = (alpha_t * h |
| + (1.0 - trap_t) * DT_t * alpha_t * bx_prev |
| + trap_t * DT_t * bx_curr) |
|
|
| |
| y_t = (torch.einsum("bhps,bhs->bhp", h, C_t) |
| + self.D.view(1, self.nheads, 1) * x_t) |
|
|
| outputs.append(y_t.to(hidden_states.dtype)) |
| x_prev = x_t |
| B_prev = B_t |
|
|
| y = torch.stack(outputs, dim=1) |
|
|
| |
| z = z.view(batch, seqlen, self.nheads, self.headdim) |
| y = F.rms_norm(y.float(), [self.headdim]).to(y.dtype) * F.silu(z.float().to(y.dtype)) |
|
|
| y = y.reshape(batch, seqlen, self.d_inner) |
| return self.out_proj(y) |
|
|
|
|
| class Mamba3Block(nn.Module): |
| def __init__(self, config: Mamba3Config, layer_idx: Optional[int] = None): |
| super().__init__() |
| self.norm = nn.RMSNorm(config.d_model, eps=1e-5) |
| self.mixer = Mamba3Mixer(config, layer_idx=layer_idx) |
|
|
| def forward(self, hidden_states: torch.Tensor, residual=None, inference_params=None): |
| residual = hidden_states if residual is None else residual |
| return self.mixer(self.norm(hidden_states), inference_params=inference_params) + residual |
|
|
|
|
| class Mamba3CausalLM(nn.Module): |
| def __init__(self, config: Mamba3Config): |
| super().__init__() |
| self.config = config |
| vocab = (math.ceil(config.vocab_size / config.pad_vocab_size_multiple) |
| * config.pad_vocab_size_multiple) |
| self.embeddings = nn.Embedding(vocab, config.d_model) |
| self.layers = nn.ModuleList( |
| [Mamba3Block(config, layer_idx=i) for i in range(config.n_layer)] |
| ) |
| self.norm_f = nn.RMSNorm(config.d_model, eps=1e-5) |
| self.lm_head = nn.Linear(config.d_model, vocab, bias=False) |
| if config.tie_embeddings: |
| self.lm_head.weight = self.embeddings.weight |
|
|
| def forward(self, input_ids: torch.Tensor, inference_params=None): |
| hidden_states = self.embeddings(input_ids) |
| residual = None |
| for layer in self.layers: |
| hidden_states = layer(hidden_states, residual=residual, |
| inference_params=inference_params) |
| residual = hidden_states |
| hidden_states = self.norm_f(hidden_states) |
| return {"logits": self.lm_head(hidden_states)} |
|
|