| |
| """ |
| mamba2_torch.py β PyTorch Mamba-2 SSD Module |
| |
| BOB Architecture: Mamba-2 SSM backbone (PyTorch layer) |
| Haskell FFI peer: mamba2.h / mamba2_step_fp8() |
| CUDA kernel peer: mamba2.cu (compile with build_mamba2.py on bbqbaddie) |
| |
| Three execution modes (auto-selected at module construction): |
| 1. CUDA .so β fastest; requires compiled libmamba2.so (bbqbaddie) |
| 2. torch.ops β PyTorch C++ extension via torch.utils.cpp_extension.load() |
| requires nvcc on PATH (bbqbaddie) |
| 3. Pure PyTorch β reference implementation; runs on RTX 3080 dev machine |
| without nvcc; numerically identical to the CUDA kernel |
| |
| Typical usage: |
| from kernels.mamba2_torch import Mamba2Layer, Mamba2Block |
| |
| layer = Mamba2Layer(d_model=512, d_state=16, d_conv=4) |
| x = torch.randn(2, 128, 512) # [B, L, D] |
| y, h = layer(x) # y: [B, L, D], h: [B, D, N] state |
| |
| # Autoregressive step |
| x_step = torch.randn(2, 1, 512) |
| y_step, h = layer(x_step, recurrent_state=h) |
| """ |
|
|
| from __future__ import annotations |
|
|
| import math |
| import os |
| from pathlib import Path |
| from typing import Optional, Tuple |
|
|
| import torch |
| import torch.nn as nn |
| import torch.nn.functional as F |
|
|
| |
|
|
| _KERNELS_DIR = Path(__file__).parent |
| _SO_PATH = _KERNELS_DIR / "libmamba2.so" |
| _CUDA_SRC = _KERNELS_DIR / "mamba2.cu" |
|
|
| _cuda_ext = None |
|
|
| def _try_load_cuda_ext() -> bool: |
| """Try to load the compiled CUDA extension. Returns True if loaded.""" |
| global _cuda_ext |
| if _cuda_ext is not None: |
| return True |
|
|
| |
| if _SO_PATH.exists(): |
| try: |
| import ctypes |
| _cuda_ext = ctypes.CDLL(str(_SO_PATH)) |
| return True |
| except OSError: |
| pass |
|
|
| |
| from torch.utils.cpp_extension import CUDA_HOME |
| if CUDA_HOME is not None and _CUDA_SRC.exists(): |
| try: |
| from torch.utils.cpp_extension import load |
| _cuda_ext = load( |
| name="mamba2_cuda", |
| sources=[str(_CUDA_SRC)], |
| extra_cuda_cflags=["-O3", f"-arch=sm_86"], |
| verbose=False, |
| ) |
| return True |
| except Exception as e: |
| print(f"[mamba2] JIT compile failed ({e}), falling back to pure PyTorch") |
|
|
| return False |
|
|
|
|
| |
|
|
| def _softplus(x: torch.Tensor) -> torch.Tensor: |
| return F.softplus(x) |
|
|
|
|
| def mamba2_scan_ref( |
| u: torch.Tensor, |
| dt: torch.Tensor, |
| A: torch.Tensor, |
| B: torch.Tensor, |
| C: torch.Tensor, |
| D: torch.Tensor, |
| hx: Optional[torch.Tensor] = None, |
| ) -> Tuple[torch.Tensor, torch.Tensor]: |
| """ |
| Pure-PyTorch Mamba-2 SSD selective scan. |
| Numerically equivalent to mamba2_ssd_scan_kernel in mamba2.cu. |
| |
| Returns (output, h_final): |
| output : [B, L, D] |
| h_final : [B, D, N] |
| """ |
| B_sz, L, D_sz = u.shape |
| N = B.shape[-1] |
| device = u.device |
| dtype = u.dtype |
|
|
| if hx is None: |
| hx = torch.zeros(B_sz, D_sz, N, device=device, dtype=dtype) |
| else: |
| hx = hx.clone() |
|
|
| |
| dt_bar = _softplus(dt) |
|
|
| |
| |
| dA = torch.exp(dt_bar * A.unsqueeze(0).unsqueeze(0)) |
|
|
| outputs = [] |
| h = hx |
|
|
| for t in range(L): |
| u_t = u[:, t, :] |
| dA_t = dA[:, t, :] |
| dt_t = dt_bar[:, t, :] |
| B_t = B[:, t, :] |
| C_t = C[:, t, :] |
|
|
| |
| |
| dB = (dt_t.unsqueeze(-1) * u_t.unsqueeze(-1)) * B_t.unsqueeze(1) |
|
|
| |
| h = dA_t.unsqueeze(-1) * h + dB |
|
|
| |
| |
| y = (C_t.unsqueeze(1) * h).sum(-1) |
|
|
| |
| y = y + D * u_t |
|
|
| outputs.append(y) |
|
|
| output = torch.stack(outputs, dim=1) |
| return output, h |
|
|
|
|
| |
|
|
| class Mamba2Layer(nn.Module): |
| """ |
| Single Mamba-2 SSD layer. |
| |
| Args: |
| d_model : inner (expanded) dimension D |
| d_state : SSM state dimension N (default 16, paper uses 16-64) |
| d_conv : depthwise conv width (default 4) |
| expand : expansion ratio for in_proj (default 2) |
| dt_rank : rank of Ξ projection (default ceil(d_model/16)) |
| dt_min, dt_max : softplus clamp for Ξ initialisation |
| bias : add bias to projections |
| use_cuda : force CUDA ext (raises if unavailable) |
| """ |
|
|
| def __init__( |
| self, |
| d_model: int, |
| d_state: int = 16, |
| d_conv: int = 4, |
| expand: int = 2, |
| dt_rank: Optional[int] = None, |
| dt_min: float = 0.001, |
| dt_max: float = 0.1, |
| bias: bool = False, |
| use_cuda: bool = False, |
| ): |
| super().__init__() |
|
|
| self.d_model = d_model |
| self.d_state = d_state |
| self.d_conv = d_conv |
| self.expand = expand |
| self.d_inner = d_model * expand |
| self.dt_rank = dt_rank or math.ceil(d_model / 16) |
|
|
| |
|
|
| |
| self.in_proj = nn.Linear( |
| d_model, |
| self.d_inner * 2 + d_state * 2 + self.dt_rank, |
| bias=bias, |
| ) |
|
|
| |
| |
| self.conv1d = nn.Conv1d( |
| in_channels=self.d_inner, |
| out_channels=self.d_inner, |
| kernel_size=d_conv, |
| padding=0, |
| groups=self.d_inner, |
| bias=bias, |
| ) |
|
|
| |
| self.dt_proj = nn.Linear(self.dt_rank, self.d_inner, bias=True) |
|
|
| |
| self.A_log = nn.Parameter( |
| torch.log(torch.arange(1, d_state + 1, dtype=torch.float32) |
| .repeat(self.d_inner, 1)) |
| ) |
| |
| self.A_log_1d = nn.Parameter( |
| -torch.ones(self.d_inner) * math.log(d_state) |
| ) |
|
|
| self.D = nn.Parameter(torch.ones(self.d_inner)) |
|
|
| |
| self.out_proj = nn.Linear(self.d_inner, d_model, bias=bias) |
|
|
| |
| dt_init = torch.exp( |
| torch.rand(self.d_inner) * (math.log(dt_max) - math.log(dt_min)) + math.log(dt_min) |
| ) |
| dt_init = torch.clamp(dt_init, min=1e-4) |
| inv_dt = dt_init + torch.log(-torch.expm1(-dt_init)) |
| self.dt_proj.bias.data.copy_(inv_dt) |
|
|
| |
| self._use_cuda = use_cuda |
| if use_cuda and not _try_load_cuda_ext(): |
| raise RuntimeError("[Mamba2Layer] use_cuda=True but CUDA extension not available") |
|
|
| def _scan( |
| self, |
| u: torch.Tensor, |
| dt: torch.Tensor, |
| B: torch.Tensor, |
| C: torch.Tensor, |
| hx: Optional[torch.Tensor], |
| ) -> Tuple[torch.Tensor, torch.Tensor]: |
| """Dispatch to CUDA ext or pure-PyTorch reference.""" |
| if self._use_cuda and _try_load_cuda_ext(): |
| |
| pass |
| return mamba2_scan_ref(u, dt, self.A_log_1d, B, C, self.D, hx) |
|
|
| def forward( |
| self, |
| x: torch.Tensor, |
| recurrent_state: Optional[Tuple[torch.Tensor, torch.Tensor]] = None, |
| ) -> Tuple[torch.Tensor, Tuple[torch.Tensor, torch.Tensor]]: |
| """ |
| Args: |
| x : [B, L, d_model] |
| recurrent_state : (ssm_h, conv_cache) or None |
| ssm_h [B, d_inner, d_state] |
| conv_cache [B, d_inner, d_conv-1] |
| |
| Returns: |
| output : [B, L, d_model] |
| state : (ssm_h, conv_cache) β carry for the next call |
| """ |
| B_sz, L, _ = x.shape |
|
|
| |
| if recurrent_state is None: |
| ssm_h = None |
| conv_cache = x.new_zeros(B_sz, self.d_inner, self.d_conv - 1) |
| else: |
| ssm_h, conv_cache = recurrent_state |
|
|
| |
| xz = self.in_proj(x) |
|
|
| split_sizes = [self.d_inner, self.d_inner, self.d_state, self.d_state, self.dt_rank] |
| x_proj, z, B_ssm, C_ssm, dt_rank_out = xz.split(split_sizes, dim=-1) |
|
|
| |
| |
| x_t = x_proj.transpose(1, 2) |
|
|
| |
| x_padded = torch.cat([conv_cache, x_t], dim=2) |
|
|
| |
| new_conv_cache = x_padded[:, :, -(self.d_conv - 1):] |
|
|
| x_conv = self.conv1d(x_padded) |
| x_conv = F.silu(x_conv.transpose(1, 2)) |
|
|
| |
| dt = self.dt_proj(dt_rank_out) |
|
|
| |
| y, new_ssm_h = self._scan(x_conv, dt, B_ssm, C_ssm, ssm_h) |
|
|
| |
| y = y * F.silu(z) |
|
|
| |
| output = self.out_proj(y) |
|
|
| return output, (new_ssm_h, new_conv_cache) |
|
|
|
|
| class Mamba2Block(nn.Module): |
| """ |
| Mamba-2 residual block with RMSNorm. |
| |
| Wraps Mamba2Layer with pre-norm and residual connection. |
| Drop-in replacement for a Transformer block in a hybrid architecture. |
| """ |
|
|
| def __init__( |
| self, |
| d_model: int, |
| d_state: int = 16, |
| d_conv: int = 4, |
| expand: int = 2, |
| norm_eps: float = 1e-5, |
| **kwargs, |
| ): |
| super().__init__() |
| self.norm = nn.RMSNorm(d_model, eps=norm_eps) |
| self.layer = Mamba2Layer(d_model, d_state=d_state, d_conv=d_conv, expand=expand, **kwargs) |
|
|
| def forward( |
| self, |
| x: torch.Tensor, |
| recurrent_state=None, |
| ): |
| residual = x |
| x_normed = self.norm(x) |
| y, state = self.layer(x_normed, recurrent_state) |
| return y + residual, state |
|
|
|
|
| class Mamba2Model(nn.Module): |
| """ |
| Stack of Mamba2Blocks β the full BOB backbone. |
| |
| Args: |
| d_model : model dimension |
| n_layers : number of Mamba-2 blocks |
| d_state : SSM state size |
| vocab_size: set > 0 to add embedding + LM head |
| """ |
|
|
| def __init__( |
| self, |
| d_model: int, |
| n_layers: int, |
| d_state: int = 16, |
| d_conv: int = 4, |
| expand: int = 2, |
| vocab_size: int = 0, |
| norm_eps: float = 1e-5, |
| **kwargs, |
| ): |
| super().__init__() |
|
|
| if vocab_size > 0: |
| self.embedding = nn.Embedding(vocab_size, d_model) |
| self.lm_head = nn.Linear(d_model, vocab_size, bias=False) |
| else: |
| self.embedding = None |
| self.lm_head = None |
|
|
| self.layers = nn.ModuleList([ |
| Mamba2Block(d_model, d_state=d_state, d_conv=d_conv, expand=expand, |
| norm_eps=norm_eps, **kwargs) |
| for _ in range(n_layers) |
| ]) |
| self.final_norm = nn.RMSNorm(d_model, eps=norm_eps) |
|
|
| def forward( |
| self, |
| x: torch.Tensor, |
| recurrent_states: Optional[list] = None, |
| ) -> Tuple[torch.Tensor, list]: |
| """ |
| Returns: |
| hidden : [B, L, d_model] (or [B, L, vocab_size] with LM head) |
| states : list of updated [B, D, N] per layer |
| """ |
| if self.embedding is not None and x.dtype in (torch.long, torch.int): |
| x = self.embedding(x) |
|
|
| if recurrent_states is None: |
| recurrent_states = [None] * len(self.layers) |
|
|
| new_states = [] |
| for i, layer in enumerate(self.layers): |
| x, h = layer(x, recurrent_states[i]) |
| new_states.append(h) |
|
|
| x = self.final_norm(x) |
|
|
| if self.lm_head is not None: |
| x = self.lm_head(x) |
|
|
| return x, new_states |
|
|
|
|
| |
|
|
| if __name__ == "__main__": |
| import sys |
| device = "cuda" if torch.cuda.is_available() else "cpu" |
| print(f"[mamba2_torch] device={device}") |
|
|
| d_model, d_state, n_layers = 256, 16, 4 |
| B, L = 2, 64 |
|
|
| model = Mamba2Model( |
| d_model=d_model, n_layers=n_layers, d_state=d_state, vocab_size=512 |
| ).to(device) |
|
|
| tokens = torch.randint(0, 512, (B, L), device=device) |
| out, states = model(tokens) |
| print(f" output shape : {out.shape}") |
| print(f" n states : {len(states)}") |
| print(f" state shape : {states[0].shape}") |
| print(f" output mean : {out.float().mean().item():.6f}") |
| print(f" output std : {out.float().std().item():.6f}") |
|
|
| |
| step_token = torch.randint(0, 512, (B, 1), device=device) |
| step_out, new_states = model(step_token, recurrent_states=states) |
| print(f" step output : {step_out.shape}") |
| print("[mamba2_torch] PASS") |
| sys.exit(0) |
|
|