mamba3

Mamba-3 MIMO chunked forward and recurrent decode on NVIDIA GPUs, loadable through kernels. The reference baselines are the mamba3_MIMO_chunk_ref and mamba3_MIMO_step_ref programs from state-spaces/mamba, matched to 2.2e-6 in float32.

Upstream ships these paths as a pinned tilelang==0.1.8 kernel for the forward and a CUTLASS-DSL kernel for decode, and Mamba3(is_mimo=True) raises in its constructor when TileLang is absent rather than falling back. This is the same two operations as one compiled extension with no TileLang, TVM-FFI, CUTLASS-DSL or quack dependency.

Usage

from kernels import get_kernel

m3 = get_kernel("phanerozoic/mamba3", version=1, trust_remote_code=True)

# prefill: q, k are [B, S, R, Gqk, N]; v, z are [B, S, H, P]
y = m3.forward(q, k, v, q_bias, k_bias, mimo_v, mimo_o, angles, adt, dt, trap,
               z=z, mimo_z=mimo_z, D=D, chunk_size=16)

# decode: one token per call, state carried
st = m3.DecodeState(B, H, headdim=P, dstate=N, rank=R, nangles=Na)
y_t = st.step(q_t, k_t, v_t, q_bias, k_bias, mimo_v, mimo_o, angles_t,
              adt_t, dt_t, trap_t, z=z_t, mimo_z=mimo_z, D=D)

version selects the release branch; trust_remote_code is required by kernels for publishers without the trusted-publisher mark. angles are the raw per-step rotation rates; the cumulative sum is taken internally in float32.

API

Symbol Purpose
forward(q, k, v, ..., chunk_size, norm_weight) chunked forward, [B, S, H, P] out, differentiable
DecodeState(batch, nheads, headdim, dstate, rank, nangles) carried decode state
DecodeState.step(...) one decode token, [B, H, P] out
cumulative_angles(angles, dt), chunk_decay(adt, chunk_size) schedule helpers
dispatch_paths(mimo_rank, dstate, headdim, chunk_size, bf16) which kernels a geometry selects

norm_weight selects the fused head-wise RMSNorm pre-gate path. All three ops carry meta kernels, so the forward traces under torch.compile(fullgraph=True) without a graph break.

Method

The rank axis is flattened into the row index i = c*R + r, so a chunk tile is (C*R, N) against (C*R, P) and the forward is the SSD decomposition: chunk_state builds kv[n], state_passing scans it, chunk_scan computes q_inter @ states[n] + mask(Q Kᵀ) V + D v. Keys carry factor = γ_t + Δ_{t+1}(1 - λ_{t+1}), the single-SSD form of the trapezoidal recurrence, and the intra diagonal block is rescaled to γ_t. Contractions are register-blocked 4x4 on a 16x16 thread grid only where the tensor cores are not reachable. The float32 contractions split each operand into a tf32 head and tail and run hi*hi + hi*lo + lo*hi; the dropped lo*lo term is below the fp32 rounding of the sum, and a single tf32 pass would not hold the float32 contract. Rotation is applied while staging and never materialized.

Decode runs the recurrence directly, S ← αS + β(kprevᵀ vprev) + γ(kᵀv), one warp per p row with the rank axis in registers. The trapezoidal term reaches one step back, so the previous key and value are carried state.

mimo_v, mimo_o and mimo_z are (H, R, P) elementwise weights, so rank expansion is a broadcast multiply and the fold is a contraction over r alone. Rotation angles stay float32 at any input dtype, since they accumulate over the sequence.

On the bfloat16 path the operands are staged by ldmatrix and contracted by mma.sync.m16n8k16 written out directly, because nvcc's wmma never emits ldmatrix for bf16 and falls back to scalar loads and matrix-move fixups. The rank, the head dimension and the tile counts each warp owns are template arguments, so the accumulators index statically and stay in registers across the whole d loop and the epilogue, and the index arithmetic carries no integer division. Every shared tile is padded off a multiple of the 32 banks. The causal weight, the chunk decay and the D term all fold into the accumulator drain, D v_r riding the intra product on A's diagonal, so the float A tile never exists and the output tile reuses the space its operands occupied. The rank weights and the decay column are staged once per block, a rotary pair is one wide load, and the head axis varies fastest across the grid so that blocks sharing a chunk's q and k are resident together. The state scan issues its chunk loads four ahead of the carry that consumes them; the fma order is unchanged, and the writes land on the addresses just read.

Measured

RTX 6000 Ada, H=32, P=64, N=128, R=4, C=16. Forward against the torch reference program, which is what runs when TileLang is unavailable:

seqlen kernel torch speedup peak memory
512 0.331 ms 3.312 ms 10.02x 62 vs 375 MB
1024 0.586 ms 7.860 ms 13.42x 131 vs 741 MB
2048 1.531 ms 17.609 ms 11.50x 254 vs 1472 MB
4096 3.186 ms 40.460 ms 12.70x 499 vs 2935 MB

Decode against the eager step, with the state traffic it sustains:

batch kernel eager speedup state
1 0.0245 ms 0.754 ms 30.8x 86 GB/s
4 0.0398 ms 0.749 ms 18.8x 211 GB/s
16 0.0665 ms 0.773 ms 11.6x 505 GB/s
64 0.1659 ms 2.030 ms 12.2x 809 GB/s
256 0.9697 ms 7.492 ms 7.7x 554 GB/s

At batch 64 the decode step is at this card's memory roofline. At batch 1 it is launch- and dependency-latency bound; capture it in a CUDA graph.

Against upstream's CuTe decode kernel on an H200, H=64, P=64, N=128, R=4, bfloat16, gated, eight tokens a call. Upstream's kernel takes q and k already rotated, so the comparison is against its Triton rotary step and its CuTe step together, which is what one call here does; the CuTe kernel alone is the column beside it. Both sides agree with the reference program to 5.1e-3, and with each other to 5.6e-3, which is bfloat16 rounding:

batch kernel rotary + cute cute alone ratio
1 0.812 ms 2.901 ms 0.646 ms 3.57x
4 0.813 2.885 0.645 3.55x
16 1.180 2.894 0.638 2.45x
64 3.563 2.894 0.742 0.81x
256 12.738 3.051 1.973 0.24x

The two scale differently. This kernel is work-proportional in batch; upstream's pair is nearly flat from 1 to 256, and its rotary step is four fifths of it, so the crossover is near batch 32. Below it this kernel leads by up to 3.6x, above it upstream does, by 4x at batch 256.

Against upstream's TileLang forward rather than the reference program, on an RTX 3070 Ti Laptop at sm_86 in bfloat16. Both sides run in one process from one float32 master set, each cast to its own dtype contract:

S H R C kernel tilelang ratio
512 8 4 16 0.180 ms 0.398 ms 2.21x
1024 8 4 16 0.297 ms 0.789 ms 2.66x
2048 8 4 16 0.544 ms 1.628 ms 2.99x
4096 8 4 16 1.054 ms 3.316 ms 3.15x
2048 8 1 64 0.252 ms 0.514 ms 2.04x
2048 32 4 16 2.189 ms 1.862 ms 0.85x

On an H200 at sm_90, where upstream's float32 forward also fits its 168 KB dynamic shared-memory request and both dtypes can be compared:

S H R C kernel tilelang ratio kernel f32 tilelang f32 ratio
512 8 4 16 0.114 ms 0.204 ms 1.79x 0.183 ms 0.281 ms 1.54x
1024 8 4 16 0.106 0.383 3.62x 0.308 0.554 1.80x
2048 8 4 16 0.181 0.754 4.16x 0.598 1.140 1.91x
4096 8 4 16 0.378 1.641 4.34x 1.236 2.547 2.06x
2048 32 4 16 0.712 1.035 1.45x 2.217 1.311 0.59x
2048 8 1 64 0.100 0.267 2.68x 0.211 0.365 1.73x

The head-count row is part-dependent. This kernel is work-proportional, one block per (batch, head, chunk); upstream's unit of parallelism absorbs 32 heads, so it is underutilized below H=32 and saturated at it. On sm_86 that saturation is enough to take the row, and on sm_90 it is not. The float32 column leads at H=8 and trails at H=32, the one geometry either dtype loses on this part. Upstream's float32 forward requests 168,128 bytes of dynamic shared memory per block, and 180,992 at C = 64. The opt-in ceiling is 101,376 bytes on consumer and workstation Ampere and Ada. This kernel's float32 forward requests 39,104 bytes at the same geometry. The harness is benchmarks/bench_vs_upstream.py; TODO.md records the rest.

Backward

forward is an autograd function. The backward is two chunk-parallel passes around a reverse state scan, recomputing chunk intermediates rather than storing them: the first recovers O and takes the epilogue adjoint, the scan runs dSt[n] = dSt_part[n] + decay[n] dSt[n+1] in reverse, and the second produces dQ, dKf and dV before the rotation adjoint lands on q, k and the biases. Gradients cover the activations, every (H,R,P) rank weight, D, the norm weight, and the schedule itself: dt, adt, trap and the rotation rates.

It is reproducible. The grid is (batch, chunk) with the heads looped inside, so every gradient indexed by (b, s, ...) is owned by a single block and is accumulated in head order rather than by atomics; per-head parameters go to a per-block slot reduced in block order, and the two cross-chunk terms, the shifted half of the trapezoidal factor and the scan's decay adjoint, are staged and summed in a second ordered pass. No floating-point atomic appears anywhere in the backward source, and repeated runs return the forward and all 14 gradients bitwise identical.

The two chunk workspaces and the recomputed states are [B, H, Nc, N, P] each and dO is [B, H, Nc, C*R, P], so peak allocation is roughly the forward's plus that. At the geometry above:

seqlen forward forward + backward
512 55 MB 129 MB
1024 109 MB 258 MB
2048 218 MB 513 MB
4096 435 MB 1025 MB

Correctness

Every figure below is the worst case over that program's grid, measured on an RTX 6000 Ada against state-spaces/mamba's own reference programs.

program checks worst
tests/test_mamba3.py decode ranks 1 to 8, grouped-query, both rotation fractions 3.0e-7
tests/test_mamba3.py forward against mamba3_MIMO_chunk_ref 2.5e-6
tests/test_mamba3.py forward vs decode partial rotation 1.3e-6
tests/test_mamba3.py ragged sequence length not a multiple of the chunk 3.3e-6
tests/test_mamba3.py fused norm against the upstream fused-norm reference 3.6e-7
tests/test_mamba3.py bfloat16 against the float32 kernel 9.4e-3
tests/test_mamba3.py long sequences S to 32,768, accumulated angle to 3,378 radians 3.5e-6
tests/test_bwd.py 15 raw gradients against autograd through the reference 7.6e-7
tests/test_autograd.py the wrapped function end to end, cumsum adjoints included 6.4e-7
tests/test_bwd_bf16.py the bfloat16 backward against the float32 one 6.8e-3
tests/test_bigchunk.py the streamed intra path up to C*R = 256 6.1e-7
ref/test_ref.py this repo's step reference against upstream's 2.3e-7
tests/test_dispatch.py all five launcher paths against the step reference 1.5e-2
tests/test_determinism.py forward and 14 gradients over 3 repeats bitwise
tests/test_fixes.py meta kernels, fullgraph=True, the backward memory guard pass

Upstream's chunk reference omits the cos/sin padding its step reference performs, so its pairwise path is only defined at Na == N/2; partial rotation is checked against decode instead.

The rotation angle is a cumulative sum over the sequence and grows without bound, so it is evaluated with a range-reduced sincos rather than the approximate intrinsic, whose argument reduction would make the residual proportional to the angle. Agreement with the reference is flat in sequence length: 2.3e-6 at S = 2048 and 3.5e-6 at S = 32768, where the accumulated angle reaches 3,378 radians. The residual is set by the three-term tf32 split, which accumulates over the N/8 contraction tiles, so it is the figure to watch if N grows; MAMBA3_SCAN=fma returns the float32 scan to 1.4e-6 at a third less throughput.

Requirements and limits

  • NVIDIA GPU with compute capability 8.0+; float32 or bfloat16 values, float32 schedule and rotation angles.
  • N even, Na <= N/2, mimo_rank <= 8, nheads divisible by the query-group count. Sequence length and chunk size are arbitrary; above C*R = 64 the forward switches to a row-block streamed intra term so nothing scales with (C*R)². C = 64 / mimo_rank remains the fastest setting, since the intra term is O(S*C) and grows faster than the state workspace shrinks.
  • The backward has no streamed path, so its tiles stay resident and its shared memory grows as (C*R)² and C*R*P. Both entry points check the requirement against the device's opt-in maximum and raise naming the two figures rather than failing at launch. A 99 KB device admits C*R = 64 at headdim = 64; C*R = 128, or headdim = 128 at C*R = 64, needs the forward instead.
  • Gradients are computed in float32 and returned in the input dtype, and the backward computes all of them whether or not each input requires grad.
  • chunk_state and chunk_scan both contract on the tensor cores in both dtypes, float32 through the three-pass tf32 split and bfloat16 natively. The tensor-core kernels are instantiated for mimo_rank in {1, 2, 4, 8} and headdim in {16, 32, 64, 128}, and the scans additionally want chunk_size * mimo_rank == 64; any other geometry takes the FMA path. dispatch_paths reports which kernels a geometry selects, and MAMBA3_SCAN=fma demotes the float32 scan to FMA for an A/B in one process.
  • Packaged for compute capabilities 8.0, 8.6, 8.9, 9.0, 10.0 and 12.0. The whole suite runs on 8.6 and 8.9; the others are build targets.

References

Lahoti, Li, Chen, Wang, Bick, Kolter, Dao, Gu, "Mamba-3: Improved Sequence Modeling using State Space Principles" (ICLR 2026, arXiv:2603.15569); Dao and Gu, the SSD chunked scan of Mamba-2; state-spaces/mamba as the reference implementation.

License

Apache-2.0.

Downloads last month
1
apache-2.0
arxiv: 2603.15569
Supported hardwares new
CUDA
8.08.68.99.010.012.0
GPU
B300
288GB
NVIDIA SXM
B200
192GB
NVIDIA SXM
H200
141GB
NVIDIA SXM
H100
80GB
GPU
H800
80GB
GPU
H20
96GB
GPU
L40s
48GB
GPU
L40
48GB
GPU
L20
48GB
GPU
L4
24GB
DGX Spark
GB10
128GB
GPU
RTX PRO 6000 WS
96GB
GPU
RTX PRO 6000 Max-Q
96GB
GPU
RTX PRO 5000
48GB
GPU
RTX PRO 4500 WS
32GB
GPU
RTX PRO 4000
24GB
GPU
RTX PRO 4000 SFF
24GB
GPU
RTX PRO 2000
16GB
GPU
RTX 6000 Ada
48GB
GPU
RTX 5880 Ada
48GB
RTX
RTX 5000 Ada
32GB
GPU
RTX 4500 Ada
24GB
RTX
RTX 4000 Ada
20GB
RTX
RTX 4000 SFF Ada
20GB
GPU
RTX 3500 Ada Mobile
12GB
GPU
RTX 2000 Ada
16GB
GPU
RTX A6000
48GB
GPU
RTX A5000
8GB
GPU
RTX A5000 Max-Q
16GB
GPU
RTX A5000 Mobile
16GB
GPU
RTX A4000
16GB
GPU
RTX A4000 Max-Q
8GB
GPU
RTX A4000 Mobile
8GB
GPU
RTX A3000 Mobile
6GB
GPU
RTX A2000
6GB
GPU
RTX A2000 Embedded
4GB
GPU
RTX A2000 Max-Q
4GB
GPU
RTX A2000 Mobile
4GB
GPU
A800
40GB
GPU
A100
80GB
GPU
A40
48GB
GPU
A30
24GB
GPU
A10
24GB
GPU
A2
16GB
RTX
RTX 5090
32GB
RTX
RTX 5090 D
32GB
RTX
RTX 5090 Mobile
24GB
RTX
RTX 5080
16GB
RTX
RTX 5080 Mobile
16GB
RTX
RTX 5070
12GB
RTX
RTX 5070 Mobile
8GB
RTX
RTX 5070 Ti
16GB
RTX
RTX 5070 Ti Mobile
12GB
RTX
RTX 5060 Ti
16GB
RTX
RTX 5060
8GB
RTX
RTX 5060 Mobile
8GB
RTX
RTX 5050
8GB
RTX
RTX 5050 Mobile
8GB
RTX
RTX 4090
24GB
RTX
RTX 4090D
24GB
RTX
RTX 4090 Mobile
16GB
RTX
RTX 4080 SUPER
16GB
RTX
RTX 4080
16GB
RTX
RTX 4080 Mobile
12GB
RTX
RTX 4070
12GB
RTX
RTX 4070 Mobile
8GB
RTX
RTX 4070 Ti
12GB
RTX
RTX 4070 Super
12GB
RTX
RTX 4070 Ti Super
16GB
RTX
RTX 4060
8GB
RTX
RTX 4060 Ti
8GB
RTX
RTX 4090 Laptop
16GB
RTX
RTX 4080 Laptop
12GB
RTX
RTX 4070 Laptop
8GB
RTX
RTX 4060 Laptop
8GB
RTX
RTX 4050 Laptop
6GB
RTX
RTX 3090
24GB
RTX
RTX 3090 Ti
24GB
RTX
RTX 3080
12GB
RTX
RTX 3080 Ti
12GB
RTX
RTX 3080 Mobile
16GB
RTX
RTX 3070
8GB
RTX
RTX 3070 Ti
8GB
RTX
RTX 3070 Ti Mobile
8GB
RTX
RTX 3060 Ti
8GB
RTX
RTX 3060
12GB
RTX
RTX 3060 Mobile
6GB
RTX
RTX 3050 Mobile
4GB
GPU
RTX 2050 Mobile
4GB
Jetson
Jetson AGX Orin 64GB
64GB
Jetson
Jetson AGX Orin 32GB
32GB
Jetson
Jetson Orin NX 16GB
16GB
Jetson
Jetson Orin NX 8GB
8GB
Jetson
Jetson Orin Nano 8GB
8GB
Jetson
Jetson Orin Nano 4GB
4GB
OS
linux
Arch
x86_64
Kernel Builder
22e2aad