Skip to content

Commit d76bfdf

Browse files
committed
Merge remote-tracking branch 'origin/main'
2 parents fcc5267 + fbbfa80 commit d76bfdf

33 files changed

Lines changed: 4240 additions & 446 deletions

.DS_Store

2 KB
Binary file not shown.

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,3 +60,5 @@ reports/profiling/*.json
6060
reports/v4/m*
6161
docs/tilelang_ports/mamba3_path_b_vs_c.diff
6262
docs/tilelang_ports/mamba3_path_c_lowered.metal
63+
reports/raw/*
64+
vbgui/.DS_Store

cppmega_mlx/nn/_tilelang/m2rnn.py

Lines changed: 950 additions & 114 deletions
Large diffs are not rendered by default.

cppmega_mlx/nn/_tilelang/mamba3.py

Lines changed: 69 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,28 @@
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
1128
This module implements the Mamba3 MIMO selective-scan kernel in vendor MSL,
1229
without 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

Comments
 (0)