Download kernels/mamba2_torch.py from Snapkitty/nvidia-stack: direct link, hf CLI and curl.
- Browser
- Download file 15.3 kB
-
https://huggingface.co/Snapkitty/nvidia-stack/resolve/main/kernels/mamba2_torch.py
- Command line
-
hf download hf://Snapkitty/nvidia-stack/kernels/mamba2_torch.py
-
curl -L -o mamba2_torch.py https://huggingface.co/Snapkitty/nvidia-stack/resolve/main/kernels/mamba2_torch.py
15.3 kB
| #!/usr/bin/env python3 | |
| """ | |
| 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 | |
| # ββ Optional compiled extension ββββββββββββββββββββββββββββββββββββββββββββββ | |
| _KERNELS_DIR = Path(__file__).parent | |
| _SO_PATH = _KERNELS_DIR / "libmamba2.so" | |
| _CUDA_SRC = _KERNELS_DIR / "mamba2.cu" | |
| _cuda_ext = None # loaded lazily | |
| 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 | |
| # Path 1: pre-compiled .so (set by build_mamba2.py on bbqbaddie) | |
| if _SO_PATH.exists(): | |
| try: | |
| import ctypes | |
| _cuda_ext = ctypes.CDLL(str(_SO_PATH)) | |
| return True | |
| except OSError: | |
| pass | |
| # Path 2: torch.utils.cpp_extension JIT compile (needs nvcc) | |
| 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 | |
| # ββ Pure-PyTorch selective scan (reference, trainable) ββββββββββββββββββββββ | |
| def _softplus(x: torch.Tensor) -> torch.Tensor: | |
| return F.softplus(x) | |
| def mamba2_scan_ref( | |
| u: torch.Tensor, # [B, L, D] | |
| dt: torch.Tensor, # [B, L, D] | |
| A: torch.Tensor, # [D] | |
| B: torch.Tensor, # [B, L, N] | |
| C: torch.Tensor, # [B, L, N] | |
| D: torch.Tensor, # [D] | |
| hx: Optional[torch.Tensor] = None, # [B, D, N] | |
| ) -> 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: [B, L, D] β softplus | |
| dt_bar = _softplus(dt) | |
| # dA: [B, L, D] β decay factors | |
| # A is [D], a_log negative | |
| dA = torch.exp(dt_bar * A.unsqueeze(0).unsqueeze(0)) # [B, L, D] | |
| outputs = [] | |
| h = hx # [B, D, N] | |
| for t in range(L): | |
| u_t = u[:, t, :] # [B, D] | |
| dA_t = dA[:, t, :] # [B, D] | |
| dt_t = dt_bar[:, t, :] # [B, D] | |
| B_t = B[:, t, :] # [B, N] | |
| C_t = C[:, t, :] # [B, N] | |
| # dB[b, d, n] = dt_t[b,d] * B_t[b,n] * u_t[b,d] | |
| # Shape: [B, D, N] | |
| dB = (dt_t.unsqueeze(-1) * u_t.unsqueeze(-1)) * B_t.unsqueeze(1) | |
| # h[b, d, n] = dA_t[b,d] * h[b,d,n] + dB[b,d,n] | |
| h = dA_t.unsqueeze(-1) * h + dB | |
| # y[b, d] = sum_n C_t[b, n] * h[b, d, n] | |
| # C_t: [B, N] β [B, 1, N]; h: [B, D, N] | |
| y = (C_t.unsqueeze(1) * h).sum(-1) # [B, D] | |
| # skip connection | |
| y = y + D * u_t | |
| outputs.append(y) | |
| output = torch.stack(outputs, dim=1) # [B, L, D] | |
| return output, h | |
| # ββ nn.Module ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| 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 # D in the kernel | |
| self.dt_rank = dt_rank or math.ceil(d_model / 16) | |
| # ββ Projections ββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # in_proj: x β [z, x, B, C, dt] (single matmul) | |
| self.in_proj = nn.Linear( | |
| d_model, | |
| self.d_inner * 2 + d_state * 2 + self.dt_rank, | |
| bias=bias, | |
| ) | |
| # Causal depthwise conv β padding handled manually so conv cache | |
| # can be carried across autoregressive steps (no auto-padding). | |
| 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, | |
| ) | |
| # dt projection: dt_rank β d_inner | |
| self.dt_proj = nn.Linear(self.dt_rank, self.d_inner, bias=True) | |
| # SSM parameters | |
| self.A_log = nn.Parameter( | |
| torch.log(torch.arange(1, d_state + 1, dtype=torch.float32) | |
| .repeat(self.d_inner, 1)) # [D, N] β not used in scan | |
| ) | |
| # We use a single [D] A vector (log-sum over state dim) | |
| self.A_log_1d = nn.Parameter( | |
| -torch.ones(self.d_inner) * math.log(d_state) | |
| ) | |
| self.D = nn.Parameter(torch.ones(self.d_inner)) | |
| # out_proj: d_inner β d_model | |
| self.out_proj = nn.Linear(self.d_inner, d_model, bias=bias) | |
| # dt softplus clamp init | |
| 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) | |
| # Try to load CUDA extension | |
| 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(): | |
| # CUDA ext path β swap in ctypes call on bbqbaddie when .so is ready | |
| 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 | |
| # Unpack or initialise recurrent state | |
| 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 | |
| # ββ Split input projection ββββββββββββββββββββββββββββββββββββββββ | |
| xz = self.in_proj(x) # [B, L, 2*D + 2*N + dt_rank] | |
| 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) | |
| # ββ Causal depthwise conv with cache βββββββββββββββββββββββββββββ | |
| # x_proj: [B, L, D] β [B, D, L] for conv1d | |
| x_t = x_proj.transpose(1, 2) # [B, D, L] | |
| # Left-pad with conv cache to preserve causality | |
| x_padded = torch.cat([conv_cache, x_t], dim=2) # [B, D, d_conv-1+L] | |
| # Update conv cache: keep last (d_conv-1) tokens | |
| new_conv_cache = x_padded[:, :, -(self.d_conv - 1):] # [B, D, d_conv-1] | |
| x_conv = self.conv1d(x_padded) # [B, D, L] | |
| x_conv = F.silu(x_conv.transpose(1, 2)) # [B, L, D] | |
| # ββ dt ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| dt = self.dt_proj(dt_rank_out) # [B, L, D] | |
| # ββ SSM scan βββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| y, new_ssm_h = self._scan(x_conv, dt, B_ssm, C_ssm, ssm_h) | |
| # ββ Gated output βββββββββββββββββββββββββββββββββββββββββββββββββ | |
| y = y * F.silu(z) | |
| # ββ Output projection βββββββββββββββββββββββββββββββββββββββββββββ | |
| 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, # [B, L, d_model] or [B, L] token ids | |
| recurrent_states: Optional[list] = None, # list of [B, D, N] per layer | |
| ) -> 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 | |
| # ββ Quick sanity check (run directly) ββββββββββββββββββββββββββββββββββββββββ | |
| 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}") # [2, 64, 512] | |
| print(f" n states : {len(states)}") # 4 | |
| print(f" state shape : {states[0].shape}") # [2, D_inner, 16] | |
| print(f" output mean : {out.float().mean().item():.6f}") | |
| print(f" output std : {out.float().std().item():.6f}") | |
| # Autoregressive step | |
| 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}") # [2, 1, 512] | |
| print("[mamba2_torch] PASS") | |
| sys.exit(0) | |