custom
code
sovereign-compute
nvidia-stack / kernels /mamba2_torch.py
SNAPKITTYWEST's picture
chore: push from SNAPKITTYWEST local build
e92f76f verified
Raw
History Blame Contribute Delete
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)