Skip to content

Commit dbf79fd

Browse files
committed
feat(mamba3 F0): tensor-core CUDA precompute prim (cb + summary_states T.gemm) on sm_121
Add chunk_precompute_fwd_cuda_prim — the sm_121/gb10 twin of the serial chunk_precompute_fwd_metal_prim — re-expressing F0's two head-independent serial scalar loops (the §22/§23 MEASURED 24.57ms lever) as tensor-core T.gemm: (1) cb[L,L] = C[L,N] @ B[L,N]^T via T.gemm(C_shared, B_shared, cb_frag, transpose_B=True) per (chunk,group), guarded head_idx%heads_per_group==0 (Tri-Dao _bmm_chunk_fwd). (2) summary_states[P,N] = (decay*dt-weighted x)^T @ B via T.gemm(x_dec_shared, B_shared, states_frag, transpose_A=True) per (chunk,head) — the dominant P*N=4096 serial loop. decay*dt folded into the x operand (Tri-Dao _chunk_state_fwd), B kept native fp16; output stays fp32. dA_cumsum STAYS a single-lane serial inclusive scan (copied verbatim — it is a scan, not a GEMM). Mirrors the proven F2 chunk_scan_fwd_cuda_prim idiom: register-fragment fp32 C accumulators, plain scope=shared fp16 operands, NO make_swizzled_layout, disable_tma=True on every copy. Selected in build_chunk_precompute_metal when the resolved target is CUDA (mirrors compile_chunk_scan_fwd_metal's per-target branch) with pass_configs {tl.disable_tma_lower, tl.disable_warp_specialized}; a per-threadgroup smem gate (48.5KB at prod L=P=N=64 << 99KB) and an m16n8k16 divisibility gate RAISE on violation (RULE #1: no serial fallback). dtype contract unchanged (§23): operands fp16, fp32 fragment accumulate, cb fp16, summary_states fp32. The existing serial Metal prim is BYTE-IDENTICAL (verified sha256) and F1 is untouched (verified sha256). No Metal/MSL backend file touched. Local-only (py_compile + AST cross-symbol + smem-budget); GB10 measures.
1 parent 82c320b commit dbf79fd

1 file changed

Lines changed: 252 additions & 1 deletion

File tree

cppmega_mlx/nn/_tilelang/mamba3_chunked_precompute_core.py

Lines changed: 252 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
"MAMBA3_CHUNK_PRECOMPUTE_OP_NAME",
4343
"MAMBA3_INTER_CHUNK_RECUR_OP_NAME",
4444
"chunk_precompute_fwd_metal_prim",
45+
"chunk_precompute_fwd_cuda_prim",
4546
"inter_chunk_recur_fwd_metal_prim",
4647
"chunk_precompute_fwd_grid",
4748
"inter_chunk_recur_fwd_grid",
@@ -213,6 +214,204 @@ def main(
213214
return main
214215

215216

217+
def chunk_precompute_fwd_cuda_prim(
218+
batch: int,
219+
seqlen: int,
220+
chunk_size: int,
221+
ngroups: int,
222+
nheads: int,
223+
headdim: int,
224+
dstate: int,
225+
*,
226+
threads: int = 128,
227+
) -> Any:
228+
"""CUDA / sm_121 twin of :func:`chunk_precompute_fwd_metal_prim` (gb10).
229+
230+
IDENTICAL F0 math; the two head-independent serial scalar loops of the Metal
231+
prim are re-expressed as TENSOR-CORE ``T.gemm`` (the §22/§23 measured lever —
232+
the Metal prim's ``cb`` per-``(li,si)`` dot over ``dstate`` and its
233+
``summary_states`` ``P*N=4096``-cell serial-``l`` accumulate were the 24.57 ms
234+
F0 term). ``dA_cumsum`` STAYS a single-lane serial inclusive scan (it is a
235+
scan, NOT a GEMM) and is copied verbatim from the Metal prim.
236+
237+
The two GEMMs, both per the proven sibling CUDA SSD precedent
238+
(:func:`mamba3_chunked_scan_core.chunk_scan_fwd_cuda_prim`): register-fragment
239+
fp32 C accumulators (:func:`T.alloc_fragment`), plain ``scope="shared"`` fp16
240+
operands (NO ``make_swizzled_layout``), and ``disable_tma=True`` on every
241+
global<->shared copy (sm_121 TMA tensormap mis-aligns at these 64-tile dims ->
242+
"Invalid TMA descriptor arguments" at RUN). The compile site
243+
(:func:`build_chunk_precompute_metal`) selects this prim when the resolved
244+
target is CUDA and passes the matching
245+
``{tl.disable_tma_lower, tl.disable_warp_specialized}`` pass_configs.
246+
247+
(1) cb[L,L] = C[L,N] @ B[L,N]^T (per (chunk, group); written once per group
248+
via the ``head_idx % heads_per_group == 0`` guard, exactly as the Metal
249+
prim, since cb is head-independent within a group):
250+
T.gemm(C_shared[L,N], B_shared[L,N], cb_frag[L,L], transpose_B=True)
251+
-> cb_frag[li,si] = sum_n C[li,n]*B[si,n] (Tri-Dao _bmm_chunk_fwd).
252+
253+
(2) summary_states[P,N] = (decay*dt-weighted x)^T @ B (per (chunk, head),
254+
the DOMINANT term). The per-row decay*dt is folded into the x OPERAND
255+
(exactly Tri-Dao _chunk_state_fwd folds ``scale`` into its operand; this
256+
keeps the fp32 output exact and B in native fp16 as the serial reads it):
257+
x_dec[l,p] = exp2((dacs[L-1]-dacs[l])*p) * dt[l] * x[l,p]
258+
T.gemm(x_dec_shared[L,P], B_shared[L,N], states_frag[P,N], transpose_A=True)
259+
-> states_frag[p,n] = sum_l x_dec[l,p]*B[l,n].
260+
261+
DTYPE CONTRACT (unchanged, §23 F1/F2): operands fp16, fragment accumulate
262+
fp32, ``cb`` stored fp16, ``summary_states`` stored fp32 (= accum_dtype; F1
263+
reads it fp32 — NO fp16 downcast on the store, the §23 N=64 segfault class).
264+
265+
DECAY/SIGN: ``A = -softplus(...) <= 0`` so ``a_row <= 0`` and
266+
``dacs[L-1]-dacs[l] <= 0``; the decay uses ``exp2((dacs[L-1]-dacs[l])*p)`` with
267+
NO ``minimum(...,0.0)`` clamp (a no-op for this sign convention; the serial
268+
prim omits it, so adding it would diverge from the parity reference).
269+
270+
SMEM at prod ``L=P=N=64`` (fp16 = 2B, fp32 = 4B): a_row[L]+dacs[L] fp32 0.5 KB
271+
+ C_shared[L,N] 8 KB + B_shared[L,N] 8 KB + cb_store[L,L] fp16 8 KB +
272+
x_dec_shared[L,P] 8 KB + states_store[P,N] fp32 16 KB = 48.5 KB combined
273+
(cb_frag/states_frag live in REGISTERS), << the gb10 ~99 KB budget. See the
274+
compile-site assertion in :func:`build_chunk_precompute_metal`.
275+
276+
RULE #1: this is the ONE CUDA path when selected; a tile/MMA-divisibility/
277+
compile failure RAISES with where+what — it NEVER falls back to a serial loop.
278+
"""
279+
import tilelang # noqa: F401 (parity with the sibling cuda prim import block)
280+
import tilelang.language as T
281+
282+
if seqlen % chunk_size != 0:
283+
raise ValueError(
284+
f"chunk_precompute_fwd_cuda_prim: seqlen ({seqlen}) must be "
285+
f"divisible by chunk_size ({chunk_size}); no padding (RULE #1)"
286+
)
287+
if nheads % ngroups != 0:
288+
raise ValueError(
289+
f"chunk_precompute_fwd_cuda_prim: nheads ({nheads}) must be "
290+
f"divisible by ngroups ({ngroups})"
291+
)
292+
# m16n8k16 fp16 MMA divisibility for both GEMMs: cb is M=L,N=L,K=dstate;
293+
# states is M=headdim,N=dstate,K=chunk_size. RAISE (no silent pad, RULE #1).
294+
for axis_name, axis_val in (
295+
("chunk_size(L)", chunk_size),
296+
("dstate(N/K)", dstate),
297+
("headdim(P)", headdim),
298+
):
299+
if axis_val % 16 != 0:
300+
raise ValueError(
301+
f"chunk_precompute_fwd_cuda_prim: {axis_name}={axis_val} is not a "
302+
f"multiple of 16; the fp16 m16n8k16 tensor-core MMA requires "
303+
f"K%16==0 and M%16==0/N%8==0 for the cb/summary_states GEMMs. No "
304+
f"silent padding (RULE #1) — re-tile or pad the caller buffers."
305+
)
306+
307+
dtype = T.float16
308+
accum_dtype = T.float32
309+
nchunks = seqlen // chunk_size
310+
heads_per_group = nheads // ngroups
311+
L = chunk_size
312+
p = _LOG2E
313+
314+
@T.prim_func
315+
def main(
316+
x: T.Tensor((batch, seqlen, nheads, headdim), dtype), # type: ignore
317+
B: T.Tensor((batch, seqlen, ngroups, dstate), dtype), # type: ignore
318+
C: T.Tensor((batch, seqlen, ngroups, dstate), dtype), # type: ignore
319+
A: T.Tensor((nheads), dtype), # type: ignore
320+
dt: T.Tensor((batch, seqlen, nheads), dtype), # type: ignore
321+
cb: T.Tensor((batch, nchunks, ngroups, chunk_size, chunk_size), dtype), # type: ignore
322+
dA_cumsum: T.Tensor((batch, nheads, nchunks, chunk_size), dtype), # type: ignore
323+
summary_states: T.Tensor((batch, nchunks, nheads, headdim, dstate), accum_dtype), # type: ignore
324+
):
325+
with T.Kernel(batch * nchunks, nheads, threads=threads) as (bx, by):
326+
batch_idx = bx % batch
327+
chunk_idx = bx // batch
328+
head_idx = by
329+
group_idx = head_idx // heads_per_group
330+
base = chunk_idx * chunk_size
331+
332+
a_row = T.alloc_shared((L,), accum_dtype)
333+
dacs = T.alloc_shared((L,), accum_dtype)
334+
# GEMM operands/outputs: plain scope="shared", fp16; register-fragment
335+
# fp32 C accumulators (the CUDA T.gemm asserts C is a fragment).
336+
C_shared = T.alloc_shared((L, dstate), dtype, scope="shared")
337+
B_shared = T.alloc_shared((L, dstate), dtype, scope="shared")
338+
cb_frag = T.alloc_fragment((L, L), accum_dtype)
339+
cb_store = T.alloc_shared((L, L), dtype, scope="shared")
340+
x_dec_shared = T.alloc_shared((L, headdim), dtype, scope="shared")
341+
states_frag = T.alloc_fragment((headdim, dstate), accum_dtype)
342+
states_store = T.alloc_shared((headdim, dstate), accum_dtype, scope="shared")
343+
344+
# --- dA_cumsum: a[l] = A[h]*dt[l]; inclusive cumsum over l. ---
345+
# (STAYS a scan — copied verbatim from the Metal prim; NOT a GEMM.)
346+
for l in T.Parallel(L):
347+
a_row[l] = T.Cast(accum_dtype, A[head_idx]) * T.Cast(
348+
accum_dtype, dt[batch_idx, base + l, head_idx]
349+
)
350+
T.sync_threads()
351+
if T.get_thread_binding(0) == 0:
352+
acc = T.alloc_local((1,), accum_dtype)
353+
acc[0] = T.Cast(accum_dtype, 0)
354+
for l in T.serial(L):
355+
acc[0] = acc[0] + a_row[l]
356+
dacs[l] = acc[0]
357+
T.sync_threads()
358+
for l in T.Parallel(L):
359+
dA_cumsum[batch_idx, head_idx, chunk_idx, l] = T.Cast(dtype, dacs[l])
360+
361+
# --- (1) cb = C @ B^T per (group, chunk): T.gemm, write once/group. ---
362+
if head_idx % heads_per_group == 0:
363+
T.copy(
364+
C[batch_idx, base : base + L, group_idx, 0:dstate],
365+
C_shared,
366+
disable_tma=True,
367+
)
368+
T.copy(
369+
B[batch_idx, base : base + L, group_idx, 0:dstate],
370+
B_shared,
371+
disable_tma=True,
372+
)
373+
T.clear(cb_frag)
374+
# cb_frag[li,si] = sum_n C[li,n]*B[si,n] = (C @ B^T)[li,si].
375+
T.gemm(C_shared, B_shared, cb_frag, transpose_B=True)
376+
T.copy(cb_frag, cb_store)
377+
T.copy(
378+
cb_store,
379+
cb[batch_idx, chunk_idx, group_idx, 0:L, 0:L],
380+
disable_tma=True,
381+
)
382+
T.sync_threads()
383+
384+
# --- (2) summary_states[p,n] = sum_l (decay[l]*dt[l]*x[l,p]) * B[l,n] ---
385+
# Fold per-row decay*dt into the x operand (Tri-Dao _chunk_state_fwd);
386+
# decay[l] = exp(dA_cs[L-1]-dA_cs[l]) = exp2((dacs[L-1]-dacs[l])*p).
387+
for lp in T.Parallel(L * headdim):
388+
ll = lp // headdim
389+
pp = lp % headdim
390+
decay = T.exp2((dacs[L - 1] - dacs[ll]) * p)
391+
dt_l = T.Cast(accum_dtype, dt[batch_idx, base + ll, head_idx])
392+
x_l = T.Cast(accum_dtype, x[batch_idx, base + ll, head_idx, pp])
393+
x_dec_shared[ll, pp] = T.Cast(dtype, decay * dt_l * x_l)
394+
# Re-stage B for the group (B_shared was last written for the cb GEMM
395+
# only under the head-0 guard; every head needs it here).
396+
T.copy(
397+
B[batch_idx, base : base + L, group_idx, 0:dstate],
398+
B_shared,
399+
disable_tma=True,
400+
)
401+
T.clear(states_frag)
402+
# states_frag[p,n] = sum_l x_dec[l,p]*B[l,n]; transpose_A contracts L.
403+
T.gemm(x_dec_shared, B_shared, states_frag, transpose_A=True)
404+
# Output stays fp32 (= accum_dtype); F1 reads it fp32. NO downcast.
405+
T.copy(states_frag, states_store)
406+
T.copy(
407+
states_store,
408+
summary_states[batch_idx, chunk_idx, head_idx, 0:headdim, 0:dstate],
409+
disable_tma=True,
410+
)
411+
412+
return main
413+
414+
216415
def build_chunk_precompute_metal(
217416
batch: int,
218417
seqlen: int,
@@ -232,20 +431,72 @@ def build_chunk_precompute_metal(
232431
params; pass PRE-ZEROED contiguous buffers positionally (no ``out_idx``
233432
allocation), and synchronize the device after. RULE #1: compile failures
234433
propagate.
434+
435+
F0 is the SECOND chunked stage (after F2) whose prim BODY differs by backend:
436+
the Metal prim runs the two head-independent reductions (``cb`` dot,
437+
``summary_states`` accumulate) as serial scalar loops; the CUDA prim
438+
(:func:`chunk_precompute_fwd_cuda_prim`) re-expresses them as tensor-core
439+
``T.gemm`` (register-fragment C, plain shared operands, ``disable_tma`` copies)
440+
and compiles with the sm_121 ``{tl.disable_tma_lower, tl.disable_warp_specialized}``
441+
escape hatch — the SAME per-target codegen choice
442+
:func:`mamba3_chunked_scan_core.compile_chunk_scan_fwd_metal` makes for F2
443+
(RULE #1: explicit per-target branch, NOT a silent fallback to the serial
444+
loop). ``dA_cumsum`` stays a serial scan on both backends.
235445
"""
236446
import tilelang
237447

238448
from cppmega_mlx.nn._tilelang.mamba3_chunked_scan_core import (
239449
_resolve_chunked_compile_target,
240450
)
241451

452+
resolved_target = _resolve_chunked_compile_target(target)
453+
kind = str(getattr(getattr(resolved_target, "kind", None), "name", "")).lower()
454+
if "cuda" in kind:
455+
# SMEM BUDGET GATE (gb10 ~99 KB per-threadgroup dynamic smem). The CUDA
456+
# prim's shared allocs at the requested shapes (fp16=2B, fp32=4B):
457+
# a_row[L]+dacs[L] fp32 + C_shared[L,N]fp16 + B_shared[L,N]fp16 +
458+
# cb_store[L,L]fp16 + x_dec_shared[L,P]fp16 + states_store[P,N]fp32.
459+
# RAISE (RULE #1) if it would exceed the budget — never silently re-tile.
460+
smem_bytes = (
461+
2 * chunk_size * 4 # a_row + dacs (fp32)
462+
+ chunk_size * dstate * 2 # C_shared (fp16)
463+
+ chunk_size * dstate * 2 # B_shared (fp16)
464+
+ chunk_size * chunk_size * 2 # cb_store (fp16)
465+
+ chunk_size * headdim * 2 # x_dec_shared (fp16)
466+
+ headdim * dstate * 4 # states_store (fp32)
467+
)
468+
_GB10_SMEM_BUDGET = 99 * 1024
469+
if smem_bytes >= _GB10_SMEM_BUDGET:
470+
raise ValueError(
471+
f"build_chunk_precompute_metal(cuda): per-threadgroup smem "
472+
f"{smem_bytes} B (L={chunk_size},P={headdim},N={dstate}) >= the "
473+
f"gb10 budget {_GB10_SMEM_BUDGET} B; re-tile the F0 cuda prim "
474+
f"(RULE #1: no silent over-budget launch)."
475+
)
476+
prim = chunk_precompute_fwd_cuda_prim(
477+
batch, seqlen, chunk_size, ngroups, nheads, headdim, dstate, **kwargs
478+
)
479+
# sm_121: disable TMA + warp-specialized lowering (the GEMM operand copies
480+
# otherwise lower to a TMA tensormap that mis-aligns at these 64-tile dims
481+
# -> "Invalid TMA descriptor arguments" at RUN). Same escape hatch as F2.
482+
pass_configs = {
483+
"tl.disable_tma_lower": True,
484+
"tl.disable_warp_specialized": True,
485+
}
486+
return tilelang.compile(
487+
prim,
488+
out_idx=[5, 6, 7],
489+
target=resolved_target,
490+
pass_configs=pass_configs,
491+
)
492+
242493
prim = chunk_precompute_fwd_metal_prim(
243494
batch, seqlen, chunk_size, ngroups, nheads, headdim, dstate, **kwargs
244495
)
245496
return tilelang.compile(
246497
prim,
247498
out_idx=[5, 6, 7],
248-
target=_resolve_chunked_compile_target(target),
499+
target=resolved_target,
249500
)
250501

251502

0 commit comments

Comments
 (0)