22
33.. todo:: wave-6: port to TileLang DSL.
44
5- The direct-MSL source has no safe inverse transform into TileLang DSL. The
6- production native route must be regenerated from a TileLang PrimFunc and
7- launched with ``execution_backend="tvm_ffi"``; until that rewrite lands this
8- module is only a legacy direct-MSL fallback. Its backward path writes final
9- owner gradients with atomic adds instead of exposing P-axis partial buffers.
5+ Both ``_FWD_KERNEL_SOURCE`` and ``_BWD_KERNEL_SOURCE`` are sequential
6+ selective-scan kernels with **per-thread cumulative state** (``float
7+ h_state[STATE]`` for fwd, ``float dh[STATE]`` plus a persistent
8+ ``h_steps_scratch[tid][t][n]`` slab and reverse iteration for bwd). They do
9+ not fit the canonical TileLang DSL idiom (tile-parallel ``T.Parallel`` /
10+ ``T.Pipelined`` over a static iteration space) without a non-trivial
11+ rewrite that introduces ``T.serial(reverse=True)`` over ``t`` and treats
12+ the per-thread carry as a ``T.alloc_fragment`` of static shape.
13+
14+ The MSL-extraction adapter
15+ (:func:`cppmega_mlx.nn._tilelang._msl_extraction.extract_msl_from_engine_artifact`,
16+ commit ``00d6d90``) is in tree and the prerequisite ``return_msl=True``
17+ kwarg on ``dispatch_lower`` works for the simpler tile-parallel kernels
18+ (``topk_selector`` already flipped). Porting these scan kernels remains a
19+ wave-6 line item: write ``mamba3_mimo_fwd_prim`` /
20+ ``mamba3_mimo_bwd_prim`` ``@T.prim_func`` factories, route through
21+ ``dispatch_lower(prim, "metal", return_msl=True)``, then feed the
22+ extracted MSL string into the existing ``_msl_transform.make_metal_kernel``
23+ call site so the 12 mlx + 2 cppmega call sites stay numerically identical.
24+
25+ Until then this module stays on its hand-written MSL source to preserve
26+ numerical parity. See ``MIGRATION_PLAN.md`` and the wave-6 tracker.
1027
1128This module implements the Mamba3 MIMO selective-scan kernel in vendor MSL,
1229without depending on TileLang's TVM-Metal lowering. The forward kernel is the
@@ -130,42 +147,15 @@ class Mamba3MetalStatus:
130147_FWD_KERNEL_HEADER = """
131148 #include <metal_stdlib>
132149 using namespace metal;
133-
134- inline void cppmega_atomic_add_float(device float* ptr, float value) {
135- device atomic_uint* atomic_ptr = reinterpret_cast<device atomic_uint*>(ptr);
136- uint old_bits = atomic_load_explicit(atomic_ptr, memory_order_relaxed);
137- while (true) {
138- float old_value = as_type<float>(old_bits);
139- uint new_bits = as_type<uint>(old_value + value);
140- uint expected = old_bits;
141- if (atomic_compare_exchange_weak_explicit(
142- atomic_ptr,
143- &expected,
144- new_bits,
145- memory_order_relaxed,
146- memory_order_relaxed)) {
147- break;
148- }
149- old_bits = expected;
150- }
151- }
152150"""
153151
154152
155- # V7-N04 status (wave-6 DSL port — deferred-by-design):
156- # The Mamba3 MIMO forward is a sequential time-loop scan with per-thread
157- # `float h_state[STATE]` cumulative state. TileLang's `T.Parallel` idiom
158- # targets tile-parallel kernels; a time-serial reduction inside a tile
159- # does not map cleanly onto it. We considered three lowerings:
160- # (a) `T.serial(T)` plus per-thread fragment for h_state — TileLang
161- # loop nest fragment allocator does not currently expose a
162- # per-thread persistent buffer scope.
163- # (b) `T.atomic_add` to a global h_state and reset between batches —
164- # atomics blow the SIMDgroup occupancy on Apple Metal.
165- # (c) raw MSL through `make_metal_kernel(source=...)` — kept (below),
166- # is what ships today, and matches the Path A/B contract.
167- # Option (c) is the working path; the DSL port stays open as a wave-6+
168- # item but is not blocking. See cppmega-mlx-N04 (deferred).
153+ # TODO(wave-6): port to TileLang DSL. Sequential time-loop scan with
154+ # per-thread ``float h_state[STATE]`` cumulative state does not fit the
155+ # tile-parallel ``T.Parallel`` idiom cleanly. Once a ``mamba3_mimo_fwd_prim``
156+ # ``@T.prim_func`` exists, route through
157+ # ``dispatch_lower(prim, "metal", return_msl=True)`` and feed the extracted
158+ # MSL into ``make_metal_kernel(source=...)`` to keep the runtime contract.
169159_FWD_KERNEL = _msl_transform .make_metal_kernel (
170160 name = "cppmega_mamba3_mimo_fwd" ,
171161 input_names = ["x" , "B_proj" , "C_proj" , "z" , "A" , "dt" , "D" , "h0" ],
@@ -192,16 +182,16 @@ class Mamba3MetalStatus:
192182 // Outputs:
193183 // dx [B, T, H, P]
194184 // dz [B, T, H, P]
195- // dB [B, T, H, N]
196- // dC [B, T, H, N]
197- // dA [B, T, H]
198- // ddt [B, T, H]
199- // dD [H]
185+ // dB_partial [B, T, H, P, N] -- caller sums over P
186+ // dC_partial [B, T, H, P, N] -- caller sums over P
187+ // dA_partial [B, T, H, P] -- caller sums over P
188+ // ddt_partial [B, T, H, P] -- caller sums over P
189+ // dD_partial [B, H, P] -- caller sums over (B, P)
200190 // dh0 [B, H, P, N]
201191 //
202192 // One thread per (b, h, p) lane. The (b, h, p) decomposition keeps each
203- // lane fully owning a single P slice. P-axis contributions are accumulated
204- // into final owner outputs with fp32 atomics .
193+ // lane fully owning a single P slice, so per-lane partial outputs do not
194+ // need atomics. The caller reduces partials into final shapes .
205195
206196 uint tid = thread_position_in_grid.x;
207197 uint total_lanes = uint(BATCH) * uint(HEADS) * uint(HEADDIM);
@@ -287,9 +277,12 @@ class Mamba3MetalStatus:
287277 dh[n] += d_y_skipped * float(C_proj[bc_idx + n]);
288278 }
289279
280+ // Stride for the (B, T, H, P, N) partial buffers.
281+ uint partial_n_base = ((b * uint(SEQ) + t) * uint(HEADS) * uint(HEADDIM)
282+ + h * uint(HEADDIM) + p) * uint(STATE);
290283 for (uint n = 0; n < uint(STATE); ++n) {
291- cppmega_atomic_add_float(&dC[bc_idx + n], d_y_skipped * float(h_steps_scratch[scratch_t + n]));
292- cppmega_atomic_add_float(&dB[bc_idx + n], dh[n] * x_val);
284+ dC_partial[partial_n_base + n] = T_OUT( d_y_skipped * float(h_steps_scratch[scratch_t + n]));
285+ dB_partial[partial_n_base + n] = T_OUT( dh[n] * x_val);
293286 }
294287
295288 // dx contribution.
@@ -314,8 +307,9 @@ class Mamba3MetalStatus:
314307 }
315308 }
316309 float d_logdecay = d_decay * decay;
317- cppmega_atomic_add_float(&dA[adt_idx], d_logdecay * dt_val);
318- cppmega_atomic_add_float(&ddt[adt_idx], d_logdecay * A_val);
310+ uint adt_partial_idx = ((b * uint(SEQ) + t) * uint(HEADS) + h) * uint(HEADDIM) + p;
311+ dA_partial[adt_partial_idx] = T_OUT(d_logdecay * dt_val);
312+ ddt_partial[adt_partial_idx] = T_OUT(d_logdecay * A_val);
319313
320314 // Propagate dh through decay.
321315 for (uint n = 0; n < uint(STATE); ++n) {
@@ -328,29 +322,28 @@ class Mamba3MetalStatus:
328322 for (uint n = 0; n < uint(STATE); ++n) {
329323 dh0[h_base + n] = T_OUT(dh[n]);
330324 }
331- cppmega_atomic_add_float(&dD[h], dD_acc);
325+ uint dD_idx = ((b) * uint(HEADS) + h) * uint(HEADDIM) + p;
326+ dD_partial[dD_idx] = T_OUT(dD_acc);
332327"""
333328
334329
335- # V7-N04 status (wave-6 backward DSL port — deferred-by-design):
336- # Reverse-time scan that pairs a per-thread `float dh[STATE]`
337- # accumulator with a persistent `h_steps_scratch[tid][t][n]` slab.
338- # `T.serial(reverse=True)` exists but lowering the P-axis owner-output
339- # atomics through native TVM-FFI still requires a fragment-scope
340- # buffer alias that TileLang 0.1.x does not expose. Raw MSL (kept
341- # below) implements the same math correctly and ships in production.
342- # DSL port tracked as cppmega-mlx-N04 (deferred).
330+ # TODO(wave-6): port to TileLang DSL. Reverse-time scan with both per-thread
331+ # ``float dh[STATE]`` accumulator and a persistent ``h_steps_scratch[tid][t][n]``
332+ # slab; needs ``T.serial(reverse=True)`` over ``t`` plus careful fragment
333+ # layout to keep the per-lane partial outputs (dB_partial, dC_partial,
334+ # dA_partial, ddt_partial) atomics-free. Same flip pattern as the fwd above
335+ # once the prim_func exists.
343336_BWD_KERNEL = _msl_transform .make_metal_kernel (
344337 name = "cppmega_mamba3_mimo_bwd" ,
345338 input_names = ["dy" , "x" , "B_proj" , "C_proj" , "z" , "A" , "dt" , "D" , "h0" ],
346339 output_names = [
347340 "dx" ,
348341 "dz" ,
349- "dB " ,
350- "dC " ,
351- "dA " ,
352- "ddt " ,
353- "dD " ,
342+ "dB_partial " ,
343+ "dC_partial " ,
344+ "dA_partial " ,
345+ "ddt_partial " ,
346+ "dD_partial " ,
354347 "dh0" ,
355348 "h_steps_scratch" ,
356349 ],
@@ -551,25 +544,15 @@ def _mamba3_mimo_bwd_metal_kernel(
551544 output_shapes = [
552545 (batch , seq , heads , headdim ), # dx
553546 (batch , seq , heads , headdim ), # dz
554- (batch , seq , heads , state ), # dB
555- (batch , seq , heads , state ), # dC
556- (batch , seq , heads ), # dA
557- (batch , seq , heads ), # ddt
558- (heads ,), # dD
547+ (batch , seq , heads , headdim , state ), # dB_partial
548+ (batch , seq , heads , headdim , state ), # dC_partial
549+ (batch , seq , heads , headdim ), # dA_partial
550+ (batch , seq , heads , headdim ), # ddt_partial
551+ (batch , heads , headdim ), # dD_partial
559552 (batch , heads , headdim , state ), # dh0
560553 (batch * heads * headdim , seq , state ), # h_steps_scratch
561554 ]
562- output_dtypes = [
563- cast_dtype ,
564- cast_dtype ,
565- mx .float32 ,
566- mx .float32 ,
567- mx .float32 ,
568- mx .float32 ,
569- mx .float32 ,
570- cast_dtype ,
571- cast_dtype ,
572- ]
555+ output_dtypes = [cast_dtype ] * len (output_shapes )
573556 try :
574557 outputs = _msl_transform .dispatch (
575558 cast (_msl_transform .MetalKernel , _BWD_KERNEL ),
@@ -579,11 +562,16 @@ def _mamba3_mimo_bwd_metal_kernel(
579562 grid = (total_lanes , 1 , 1 ),
580563 threadgroup = (threads , 1 , 1 ),
581564 template = template ,
582- init_value = 0 ,
583565 )
584566 except Exception :
585567 return None
586- dx_ , dz_ , dB , dC , dA , ddt , dD , dh0_ , _h_scratch = outputs
568+ dx_ , dz_ , dB_partial , dC_partial , dA_partial , ddt_partial , dD_partial , dh0_ , _h_scratch = outputs
569+ # Reduce P-dimension partials.
570+ dB = mx .sum (dB_partial , axis = 3 ) # -> (B, T, H, N)
571+ dC = mx .sum (dC_partial , axis = 3 ) # -> (B, T, H, N)
572+ dA = mx .sum (dA_partial , axis = 3 ) # -> (B, T, H)
573+ ddt = mx .sum (ddt_partial , axis = 3 ) # -> (B, T, H)
574+ dD = mx .sum (dD_partial , axis = (0 , 2 )) # -> (H,)
587575 return (
588576 dx_ .astype (x .dtype ),
589577 dB .astype (B .dtype ),
0 commit comments