Skip to content
Kanishk Sama
← Projects

ARM Selective-Scan Kernel for Mamba Inference

Hand-optimized Rust and Arm NEON kernels for Mamba's selective scan, replacing PyTorch's sequential CPU fallback and validated against high-precision references at every layer from scalar code to the PyTorch integration.

MLSoftwareVerification
Period
Summer 2026
Role
3-person team
Stack
Rust · Arm NEON SIMD · Rayon · PyTorch · Python
Source
Repository Team repository, hosted under teammate @AdityaP9116's GitHub.
6.39–8.99×
speedup vs. torch.compile on Arm CPUs (1D bidirectional scan)

The problem

Mamba and other state-space models are supposed to be a good fit for CPU inference — linear time in sequence length, constant memory, none of an attention mechanism’s quadratic cost. In practice, PyTorch’s own implementation falls back to a slow, sequential loop on CPU, with no optimized kernel path available. That gap is what stands between a state-space model and a normal CPU deployment — AWS Graviton, Oracle Ampere, Apple Silicon — that has no GPU to fall back on.

What I built

Working in a 3-person team, I optimized and benchmarked the 1D bidirectional selective scan — one of several scan variants the project covers — implemented as safe Rust with hand-written Arm NEON vectorization and multithreaded execution. The recurrence at the center of a selective scan is sequential by definition, so the speedup comes from getting everything around that recurrence — the elementwise math, the discretization step, the memory layout — onto SIMD lanes and across cores, not from parallelizing the scan itself.

The result: 6.39–8.99× faster than torch.compile on Arm CPUs for that scan.

Verification

Kernel code that silently produces slightly-wrong numbers is worse than kernel code that crashes — a state-space model keeps generating plausible-looking output on a subtly broken scan, and nothing downstream complains. So validation had to check the actual arithmetic at every stage the code passes through, not just that it ran:

  • Scalar vs. NEON. The hand-vectorized kernel checked against a plain scalar implementation of the same recurrence before multithreading was introduced as a second variable.
  • Single-threaded vs. multithreaded. Confirming that splitting work across cores changed the wall-clock time and nothing else.
  • PyTorch integration path. The full path — custom-op registration, tensor marshaling, the actual call into the Rust kernel — checked end to end against high-precision references, not just the math in isolation.

Each layer was validated against a high-precision reference before moving to the next, so a mismatch could be attributed to the layer that introduced it rather than discovered only as a diffuse “the output looks off” at the end.

What I’d do next

  • Extend past 1D bidirectional. The team’s kernels now cover additional scan variants and model sizes beyond what I benchmarked directly — the same validate-each-layer approach should carry over cleanly.
  • Profile the non-recurrence cost. Once the scan itself is fast, the elementwise steps around it — discretization, gating — start to dominate wall-clock time, which is where the next round of optimization work would go.