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+
216415def 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