@@ -439,6 +439,304 @@ def mamba3_mimo_fwd_cuda_eager(
439439 return y , h_last
440440
441441
442+ # ---------------------------------------------------------------------------
443+ # mamba3 (bwd) — vendored CUDA-safe copy of the production reverse scan
444+ # ---------------------------------------------------------------------------
445+ #
446+ # Ports the Metal single-kernel scratch backward
447+ # (``mamba3_path_c._bwd_scratch_partial_kernel_for``) to CUDA. Per (b, h, p)
448+ # lane the kernel (1) recomputes the forward state scan into a global scratch
449+ # slab ``h_steps_scratch[b, t, h, p, n]`` (decayed recurrence
450+ # ``h = exp(A*dt)*h + x*B``), then (2) runs the reverse-time VJP scan to emit
451+ # *per-lane partial* gradients. The P-axis reductions that turn those partials
452+ # into dB/dC/dA/ddt/dD are done in MLX outside the kernel (identical axes to
453+ # ``mamba3_path_c._mamba3_mimo_bwd_path_c_bf16_snapshot_kernel``):
454+ #
455+ # dB = sum_p dB_lane_grad (B,S,H,STATE,HEADDIM) -> (B,S,H,STATE)
456+ # dC = sum_p dC_lane_grad (B,S,H,STATE,HEADDIM) -> (B,S,H,STATE)
457+ # dA = sum_p dA_lane_grad (B,S,H,HEADDIM) -> (B,S,H)
458+ # ddt = sum_p ddt_lane_grad (B,S,H,HEADDIM) -> (B,S,H)
459+ # dD = sum_{b,p} dD_lane_grad (B,H,HEADDIM) -> (H,)
460+ #
461+ # dx/dz/dh0 are per-lane (no reduction). The recurrence/VJP math is byte-for-byte
462+ # the Metal scratch kernel; only the codegen primitives change for nvcc:
463+ # * lane index from ``T.get_thread_binding()`` + block id, not the Metal
464+ # ``tir.metal.thread_position_in_grid_x`` intrinsic;
465+ # * ``T.alloc_local`` one-slot accumulators instead of ``T.alloc_var(init=)``
466+ # (the latter emits a non-lvalue ``float v = 0`` that nvcc rejects here).
467+ # All carriers fp32 (the production fp32 EAGER carrier path). Defined at module
468+ # scope (no ``from __future__ import annotations``) so the ``@T.prim_func``
469+ # ``get_type_hints`` re-evaluation resolves the shape names.
470+
471+
472+ def _make_mamba3_bwd_cuda_prim (
473+ BATCH : int ,
474+ SEQ : int ,
475+ HEADS : int ,
476+ HEADDIM : int ,
477+ STATE : int ,
478+ ) -> Any :
479+ """Build a CUDA-safe Mamba3 MIMO backward (lane-grad partials) prim_func."""
480+
481+ import tilelang .language as T
482+
483+ LANES = BATCH * HEADS * HEADDIM
484+ THREADS = min (256 , max (1 , LANES ))
485+ ad = "float32"
486+
487+ @T .prim_func
488+ def bwd (
489+ dy : T .Tensor ((BATCH , SEQ , HEADS , HEADDIM ), "float32" ),
490+ x : T .Tensor ((BATCH , SEQ , HEADS , HEADDIM ), "float32" ),
491+ B : T .Tensor ((BATCH , SEQ , HEADS , STATE ), "float32" ),
492+ C : T .Tensor ((BATCH , SEQ , HEADS , STATE ), "float32" ),
493+ z : T .Tensor ((BATCH , SEQ , HEADS , HEADDIM ), "float32" ),
494+ A : T .Tensor ((BATCH , SEQ , HEADS ), "float32" ),
495+ dt : T .Tensor ((BATCH , SEQ , HEADS ), "float32" ),
496+ D : T .Tensor ((HEADS ,), "float32" ),
497+ h0 : T .Tensor ((BATCH , HEADS , HEADDIM , STATE ), "float32" ),
498+ dx : T .Tensor ((BATCH , SEQ , HEADS , HEADDIM ), "float32" ),
499+ dz : T .Tensor ((BATCH , SEQ , HEADS , HEADDIM ), "float32" ),
500+ dB_lane_grad : T .Tensor ((BATCH , SEQ , HEADS , STATE , HEADDIM ), "float32" ),
501+ dC_lane_grad : T .Tensor ((BATCH , SEQ , HEADS , STATE , HEADDIM ), "float32" ),
502+ dA_lane_grad : T .Tensor ((BATCH , SEQ , HEADS , HEADDIM ), "float32" ),
503+ ddt_lane_grad : T .Tensor ((BATCH , SEQ , HEADS , HEADDIM ), "float32" ),
504+ dD_lane_grad : T .Tensor ((BATCH , HEADS , HEADDIM ), "float32" ),
505+ dh0 : T .Tensor ((BATCH , HEADS , HEADDIM , STATE ), "float32" ),
506+ ):
507+ h_steps_scratch = T .alloc_global (
508+ (BATCH , SEQ + 1 , HEADS , HEADDIM , STATE ), ad
509+ )
510+ with T .Kernel (T .ceildiv (LANES , THREADS ), threads = THREADS ) as bx :
511+ lane = T .get_thread_binding ()
512+ gl = bx * THREADS + lane
513+ h_state = T .alloc_local ((STATE ,), ad )
514+ dh = T .alloc_local ((STATE ,), ad )
515+ decay = T .alloc_local ((1 ,), ad )
516+ dD_acc = T .alloc_local ((1 ,), ad )
517+ y_state = T .alloc_local ((1 ,), ad )
518+ dx_inp = T .alloc_local ((1 ,), ad )
519+ d_decay = T .alloc_local ((1 ,), ad )
520+ if gl < LANES :
521+ p = gl % HEADDIM
522+ h = (gl // HEADDIM ) % HEADS
523+ b = gl // (HEADDIM * HEADS )
524+ D_h = D [h ]
525+
526+ # Forward recompute into the global scratch slab.
527+ for n in T .serial (STATE ):
528+ h_state [n ] = h0 [b , h , p , n ]
529+ h_steps_scratch [b , 0 , h , p , n ] = h_state [n ]
530+ for t in T .serial (SEQ ):
531+ decay [0 ] = T .exp (A [b , t , h ] * dt [b , t , h ])
532+ x_val = x [b , t , h , p ]
533+ for n in T .serial (STATE ):
534+ h_state [n ] = decay [0 ] * h_state [n ] + x_val * B [b , t , h , n ]
535+ h_steps_scratch [b , t + 1 , h , p , n ] = h_state [n ]
536+
537+ # Reverse-time VJP scan.
538+ for n in T .serial (STATE ):
539+ h_state [n ] = h_steps_scratch [b , SEQ , h , p , n ]
540+ dh [n ] = 0.0
541+ dD_acc [0 ] = 0.0
542+
543+ for rt in T .serial (SEQ ):
544+ t = SEQ - 1 - rt
545+ A_val = A [b , t , h ]
546+ dt_val = dt [b , t , h ]
547+ decay [0 ] = T .exp (A_val * dt_val )
548+ x_val = x [b , t , h , p ]
549+ z_val = z [b , t , h , p ]
550+ dY = dy [b , t , h , p ]
551+
552+ y_state [0 ] = 0.0
553+ for n in T .serial (STATE ):
554+ y_state [0 ] = y_state [0 ] + h_state [n ] * C [b , t , h , n ]
555+ y_skipped = y_state [0 ] + D_h * x_val
556+ sig_z = 1.0 / (1.0 + T .exp (- z_val ))
557+ silu_z = z_val * sig_z
558+ silu_dz = sig_z * (1.0 + z_val * (1.0 - sig_z ))
559+ d_silu = dY * y_skipped
560+ d_y_skipped = dY * silu_z
561+
562+ dz [b , t , h , p ] = d_silu * silu_dz
563+ dD_acc [0 ] = dD_acc [0 ] + d_y_skipped * x_val
564+
565+ dx_inp [0 ] = 0.0
566+ d_decay [0 ] = 0.0
567+ for n in T .serial (STATE ):
568+ C_val = C [b , t , h , n ]
569+ B_val = B [b , t , h , n ]
570+ h_prev = h_steps_scratch [b , t , h , p , n ]
571+ dh_n = dh [n ] + d_y_skipped * C_val
572+ dC_lane_grad [b , t , h , n , p ] = d_y_skipped * h_state [n ]
573+ dB_lane_grad [b , t , h , n , p ] = dh_n * x_val
574+ dx_inp [0 ] = dx_inp [0 ] + dh_n * B_val
575+ d_decay [0 ] = d_decay [0 ] + dh_n * h_prev
576+ dh [n ] = dh_n * decay [0 ]
577+ h_state [n ] = h_prev
578+
579+ dx_skip = d_y_skipped * D_h
580+ dx [b , t , h , p ] = dx_skip + dx_inp [0 ]
581+
582+ d_logdecay = d_decay [0 ] * decay [0 ]
583+ dA_lane_grad [b , t , h , p ] = d_logdecay * dt_val
584+ ddt_lane_grad [b , t , h , p ] = d_logdecay * A_val
585+
586+ for n in T .serial (STATE ):
587+ dh0 [b , h , p , n ] = dh [n ]
588+ dD_lane_grad [b , h , p ] = dD_acc [0 ]
589+
590+ return bwd
591+
592+
593+ @functools .lru_cache (maxsize = 128 )
594+ def _mamba3_bwd_cuda_kernel (
595+ BATCH : int ,
596+ SEQ : int ,
597+ HEADS : int ,
598+ HEADDIM : int ,
599+ STATE : int ,
600+ ) -> Any :
601+ """Compile the CUDA-safe Mamba3 MIMO backward kernel (caller-owned outs)."""
602+
603+ import tilelang
604+
605+ prim = _make_mamba3_bwd_cuda_prim (BATCH , SEQ , HEADS , HEADDIM , STATE )
606+ return tilelang .compile (prim , target = "cuda" , out_idx = None )
607+
608+
609+ def mamba3_mimo_bwd_cuda_eager (
610+ dy : mx .array ,
611+ x : mx .array ,
612+ B : mx .array ,
613+ C : mx .array ,
614+ z : mx .array ,
615+ A : mx .array ,
616+ dt : mx .array ,
617+ D : mx .array ,
618+ h0 : mx .array ,
619+ ) -> tuple [
620+ mx .array , mx .array , mx .array , mx .array , mx .array , mx .array , mx .array , mx .array
621+ ] | None :
622+ """TileLang-CUDA EAGER Mamba3 MIMO backward.
623+
624+ Returns grads ``(dx, dB, dC, dz, dA, ddt, dD, dh0)`` (matching the VJP order
625+ of the primals ``x, B, C, z, A, dt, D, h0``) cast back to the corresponding
626+ input dtypes, or ``None`` on any failure so the caller can fall back to the
627+ pure-MLX reference VJP. fp32 carriers only (matches the production fp32
628+ EAGER path). The kernel emits per-(b,h,p) lane partials for the gradients
629+ that are reduced over the HEADDIM/P axis; the reductions happen here in MLX.
630+ """
631+
632+ ok , _reason = cuda_eager_available ()
633+ if not ok :
634+ return None
635+
636+ from cppmega_mlx .nn ._tilelang .mamba3 import _validate_inputs
637+
638+ try :
639+ import torch
640+ except Exception :
641+ return None
642+
643+ batch , seq , heads , headdim , state = _validate_inputs (x , B , C , z , A , dt , D , h0 )
644+ if seq == 0 :
645+ return (
646+ mx .zeros_like (x ),
647+ mx .zeros_like (B ),
648+ mx .zeros_like (C ),
649+ mx .zeros_like (z ),
650+ mx .zeros_like (A ),
651+ mx .zeros_like (dt ),
652+ mx .zeros_like (D ),
653+ mx .zeros_like (h0 ),
654+ )
655+
656+ dyf = dy .astype (mx .float32 )
657+ xf = x .astype (mx .float32 )
658+ Bf = B .astype (mx .float32 )
659+ Cf = C .astype (mx .float32 )
660+ zf = z .astype (mx .float32 )
661+ Af = A .astype (mx .float32 )
662+ dtf = dt .astype (mx .float32 )
663+ Df = D .astype (mx .float32 )
664+ h0f = h0 .astype (mx .float32 )
665+
666+ try :
667+ kernel = _mamba3_bwd_cuda_kernel (batch , seq , heads , headdim , state )
668+ dy_t = _mlx_to_torch_cuda (dyf )
669+ x_t = _mlx_to_torch_cuda (xf )
670+ B_t = _mlx_to_torch_cuda (Bf )
671+ C_t = _mlx_to_torch_cuda (Cf )
672+ z_t = _mlx_to_torch_cuda (zf )
673+ A_t = _mlx_to_torch_cuda (Af )
674+ dt_t = _mlx_to_torch_cuda (dtf )
675+ D_t = _mlx_to_torch_cuda (Df )
676+ h0_t = _mlx_to_torch_cuda (h0f )
677+
678+ dx_t = torch .zeros (
679+ batch , seq , heads , headdim , dtype = torch .float32 , device = "cuda"
680+ )
681+ dz_t = torch .zeros (
682+ batch , seq , heads , headdim , dtype = torch .float32 , device = "cuda"
683+ )
684+ dB_lg_t = torch .zeros (
685+ batch , seq , heads , state , headdim , dtype = torch .float32 , device = "cuda"
686+ )
687+ dC_lg_t = torch .zeros (
688+ batch , seq , heads , state , headdim , dtype = torch .float32 , device = "cuda"
689+ )
690+ dA_lg_t = torch .zeros (
691+ batch , seq , heads , headdim , dtype = torch .float32 , device = "cuda"
692+ )
693+ ddt_lg_t = torch .zeros (
694+ batch , seq , heads , headdim , dtype = torch .float32 , device = "cuda"
695+ )
696+ dD_lg_t = torch .zeros (
697+ batch , heads , headdim , dtype = torch .float32 , device = "cuda"
698+ )
699+ dh0_t = torch .zeros (
700+ batch , heads , headdim , state , dtype = torch .float32 , device = "cuda"
701+ )
702+ kernel (
703+ dy_t ,
704+ x_t ,
705+ B_t ,
706+ C_t ,
707+ z_t ,
708+ A_t ,
709+ dt_t ,
710+ D_t ,
711+ h0_t ,
712+ dx_t ,
713+ dz_t ,
714+ dB_lg_t ,
715+ dC_lg_t ,
716+ dA_lg_t ,
717+ ddt_lg_t ,
718+ dD_lg_t ,
719+ dh0_t ,
720+ )
721+ torch .cuda .synchronize ()
722+ except Exception :
723+ return None
724+
725+ # Reduce the per-(b,h,p) lane partials over the P axis (matches the Metal
726+ # bf16-snapshot reduction axes exactly).
727+ dx = _torch_cuda_to_mlx (dx_t , x .dtype )
728+ dz = _torch_cuda_to_mlx (dz_t , z .dtype )
729+ dh0_g = _torch_cuda_to_mlx (dh0_t , h0 .dtype )
730+ dB = _torch_cuda_to_mlx (dB_lg_t .sum (dim = 4 ), B .dtype )
731+ dC = _torch_cuda_to_mlx (dC_lg_t .sum (dim = 4 ), C .dtype )
732+ dA = _torch_cuda_to_mlx (dA_lg_t .sum (dim = 3 ), A .dtype )
733+ ddt = _torch_cuda_to_mlx (ddt_lg_t .sum (dim = 3 ), dt .dtype )
734+ dD = _torch_cuda_to_mlx (dD_lg_t .sum (dim = (0 , 2 )), D .dtype )
735+ return dx , dB , dC , dz , dA , ddt , dD , dh0_g
736+
737+
738+
739+
442740# ---------------------------------------------------------------------------
443741# FP8 Sparse-MLA — per-token FP8 producer (CUDA-safe) + prepared-buffer apply
444742# ---------------------------------------------------------------------------
@@ -1025,6 +1323,7 @@ def m2rnn_supported_cuda_eager() -> tuple[bool, str]:
10251323 "cuda_eager_available" ,
10261324 "sparse_mla_fwd_cuda_eager" ,
10271325 "mamba3_mimo_fwd_cuda_eager" ,
1326+ "mamba3_mimo_bwd_cuda_eager" ,
10281327 "fp8_per_token_quant_cuda_eager" ,
10291328 "fp8_sparse_mla_apply_cuda_eager" ,
10301329 "m2rnn_mapped_packed_post_fwd_cuda_eager" ,
0 commit comments