Skip to content

Commit 3568ad8

Browse files
committed
feat(mamba3): fused smem-resident SSD forward (F0+F1+F2) + fix N=64 F2 prev_states dtype segfault
NEW: cppmega_mlx/nn/_tilelang/mamba3_ssd_fused_fwd.py — ONE smem-resident CUDA kernel (gb10/sm_121) that fuses F0 precompute + intra-chunk scan + F1 inter-chunk carry per (batch,chunk,head). Persistent per-(batch,head) threadgroup loops the chunk axis serially carrying state[headdim,dstate] fp32 RESIDENT in shared memory — cb/dA_cumsum/summary_states/prev_states never round-trip to global (the MEASURED 16.37 ms F0 lever). Env-gated CPPMEGA_MAMBA3_SSD_FUSED_FWD (default OFF); the un-fused F0/F1/F2 + Metal path stay byte-identical. smem budget @ prod N=64,P=64,L=64 = 82,688 B (80.75 KiB) < GB10 99 KB (101,376 B) cap, 18,688 B headroom (fp16 operand tiles + fp32 accumulation; all-fp32 staging would be 112.75 KiB and OVERFLOW). The builder RAISES (RULE #1) if the budget exceeds the cap — never truncates dstate columns, never falls back to the slow un-fused chain. Algebra verified fp64-exact (5.6e-17) vs the eager F0+F1+F2 reference for BOTH Output and final_state. FIX (Phase 0, the prod N=64 'segfault'): chunk_scan_fwd_cuda_prim declared prev_states fp16, but F1 writes it fp32 (accum_dtype, design §3.3) and the bwd B2 prim already declares it fp32 — the 2B-typed read of a 4B buffer walked the dstate stride OOB -> launch crash at N=64. Declare prev_states accum_dtype (the producer's dtype); the T.copy downcasts to the fp16 GEMM operand. Metal prim untouched. No GPU on Mac: validated py_compile + ast + cross-symbol + fp64 algebra parity. GB10 phase builds/measures.
1 parent 81a9f51 commit 3568ad8

2 files changed

Lines changed: 521 additions & 1 deletion

File tree

cppmega_mlx/nn/_tilelang/mamba3_chunked_scan_core.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -409,7 +409,18 @@ def main(
409409
dt: T.Tensor((batch, nheads, nchunks, chunk_size), dtype), # type: ignore
410410
dA_cumsum: T.Tensor((batch, nheads, nchunks, chunk_size), dtype), # type: ignore
411411
C: T.Tensor((batch, seqlen, ngroups, dstate), dtype), # type: ignore
412-
prev_states: T.Tensor((batch, nchunks, nheads, headdim, dstate), dtype), # type: ignore
412+
# DTYPE CONTRACT FIX (the prod N=64 "segfault"): F1
413+
# (``inter_chunk_recur_fwd_metal_prim``) WRITES ``prev_states`` as
414+
# ``accum_dtype`` (fp32, design §3.3/§6.6) and the bench/caller allocates it
415+
# fp32; the BACKWARD B2 prim already declares it fp32 (backward_core line
416+
# 191). This FORWARD prim previously declared it ``dtype`` (fp16) — a
417+
# 4-byte/elem buffer read by a 2-byte-typed kernel walks the dstate stride
418+
# out of bounds -> OOB global read -> the launch "segfault" at N=64. Reading
419+
# it as fp32 (the producer's dtype) is the ONE-path fix; ``prev_state_shared``
420+
# below stays fp16 because the off-diagonal ``T.gemm`` contracts in fp16
421+
# (the value is downcast on the smem copy, matching the Metal twin's fp16
422+
# GEMM operand). RULE #1: surface+fix the contract break, do not paper over.
423+
prev_states: T.Tensor((batch, nchunks, nheads, headdim, dstate), accum_dtype), # type: ignore
413424
D: T.Tensor((nheads), dtype), # type: ignore
414425
Output: T.Tensor((batch, seqlen, nheads, headdim), dtype), # type: ignore
415426
):
@@ -432,6 +443,12 @@ def main(
432443
dA_cs_m_shared = T.alloc_shared((block_M), dtype)
433444
scale_m_local = T.alloc_fragment((block_M), accum_dtype)
434445
C_shared = T.alloc_shared((block_M, block_Dstate), dtype)
446+
# prev_states is fp32 in global (accum_dtype, F1's output). The
447+
# ``T.copy`` below downcasts the 4B/elem global read into this fp16 smem
448+
# GEMM operand (matching the Metal twin's fp16 off-diagonal contraction).
449+
# The FIX is the fp32 prim-parameter dtype above: with the buffer typed
450+
# fp32 the copy walks the correct dstate stride (4B) instead of the
451+
# previous fp16 stride (2B), which read out of bounds at N=64.
435452
prev_state_shared = T.alloc_shared((block_N, block_Dstate), dtype)
436453
D_local = T.alloc_fragment((1), accum_dtype)
437454
x_residual_shared = T.alloc_shared((block_M, block_N), dtype)

0 commit comments

Comments
 (0)