Skip to content

Commit d2b2703

Browse files
committed
feat(path-c): enable matmul2d as default for fp8_matmul (#6) on fixed tvm-ffi - up to 4.4x
The tvm-ffi Metal fixes (7ae53998: owner-output commit-sync + cooperative-tensor launch-config) make matmul2d correct via the PRODUCTION tvm-ffi owner-output route. Verified M4 via NativeTileLangKernel (not torch.mps/MSL): maxdiff 0.0 at 128-512^3, 1e-6 at 1024^3 (roundoff), up to 4.4x faster than dot4. RULE #1: removed the default-off gate (matmul2d is the default; CPPMEGA_FP8_PATH_C_MATMUL2D is an emergency opt-OUT only) and removed the silent except->dot4 fallback (matmul2d compile failure now RAISES). dot4 only for tiny/non-aligned shapes no cooperative tile divides (shape-correct dispatch, not a silent gate). Tests 78 passed (1 pre-existing fp8_vecmat failure on baseline). KNOWN remaining bug (precisely isolated, NOT hidden): #13 fused_fp8_gemm's cooperative kernel signature (two float16 inputs + fp32 C, no scale bufs) still fails to launch via tvm-ffi (ffi NULL) -- _restore_metal_device_mod covers the 5-param fp8 signature but not this shape. #13 stays on the numerically-correct MSL adapter + docstring corrected; the tvm-ffi signature-recovery fix is next.
1 parent 989c4f8 commit d2b2703

2 files changed

Lines changed: 92 additions & 52 deletions

File tree

cppmega_mlx/nn/_tilelang/fp8_matmul_path_c.py

Lines changed: 77 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@
3838
_FP8_MM_C_DTYPE = "float32"
3939

4040

41-
def _coop_tile_for(M: int, N: int, K: int) -> tuple[int, int, int, int]:
41+
def _coop_tile_for(M: int, N: int, K: int) -> tuple[int, int, int, int] | None:
4242
"""Pick a Metal-4 cooperative-tensor (matmul2d) tile (BM, BN, BK, threads).
4343
4444
The cooperative ``mpp::tensor_ops::matmul2d`` micro-tile is 16x32x16: the
@@ -48,6 +48,14 @@ def _coop_tile_for(M: int, N: int, K: int) -> tuple[int, int, int, int]:
4848
warp tiles. These tiles were measured on M4 Max to beat both the legacy
4949
LUT-dot4 Path C accumulation and the pure-MLX FP8 reference across the
5050
production matmul shapes (128^3..1024^3); see the bench receipt.
51+
52+
Returns ``None`` when no legal cooperative tile cleanly divides ``M/N/K``
53+
(e.g. tiny or non-aligned shapes like 16x16x32 or 8x12x16). The cooperative
54+
matmul2d kernel does not bounds-guard a partial output tile, so launching it
55+
on an undividable shape returns numerically WRONG (column-shuffled) output.
56+
Callers must route a ``None`` result to the legacy dot4 kernel, which is
57+
correct for any shape -- this is shape-correct dispatch, NOT a silent
58+
gate-off (RULE #1): the production matmul shapes always yield a real tile.
5159
"""
5260
# Ordered best-first per shape (measured on M4 Max). Each entry keeps the
5361
# cooperative-tensor threadgroup memory under the 32 KiB Metal budget that
@@ -79,8 +87,9 @@ def _coop_tile_for(M: int, N: int, K: int) -> tuple[int, int, int, int]:
7987
if shmem > shmem_budget:
8088
continue
8189
return bm, bn, bk, threads
82-
# Fallback: smallest legal cooperative tile (single warp, 16x32x16).
83-
return 16, 32, 16, 32
90+
# No legal cooperative tile divides this shape: signal the caller to use the
91+
# shape-agnostic dot4 kernel instead of emitting a wrong partial-tile GEMM.
92+
return None
8493

8594

8695
@dataclass(frozen=True)
@@ -247,12 +256,19 @@ def _make_scaled_matmul2d_kernel(
247256
(``BM % 16``, ``BN % 32``, ``BK % 16``, warp partition); the legacy dot4
248257
``BM/BN/BK`` arguments are not necessarily legal cooperative tiles, so a
249258
measured-best cooperative tile is derived from the GEMM extents.
259+
260+
Returns ``None`` when ``_coop_tile_for`` reports that no legal cooperative
261+
tile divides ``M/N/K`` -- the caller must then use the shape-agnostic dot4
262+
kernel (the cooperative kernel would emit a wrong partial-tile result).
250263
"""
251264
import tilelang
252265
from tilelang import language as T
253266
from tvm.target import Target
254267

255-
bm, bn, bk, threads = _coop_tile_for(int(M), int(N), int(K))
268+
tile = _coop_tile_for(int(M), int(N), int(K))
269+
if tile is None:
270+
return None
271+
bm, bn, bk, threads = tile
256272

257273
globals().update(
258274
T=T,
@@ -276,27 +292,34 @@ def _make_scaled_matmul2d_kernel(
276292
def _matmul2d_owner_output_enabled() -> bool:
277293
"""Whether to route the owner-output GEMM through Metal-4 ``matmul2d``.
278294
279-
The cooperative-tensor ``matmul2d`` kernel is numerically exact (maxdiff 0
280-
vs the FP8 reference) and 1.5x-10x faster than both the legacy LUT-dot4
281-
accumulation and the pure-MLX FP8 reference on M4+ when launched via a
282-
Metal runtime that supports cooperative-tensor kernels (verified through
283-
TileLang's torch / ``torch.mps`` execution backend).
284-
285-
It is OFF by default because the owner-output contract here dispatches via
286-
the **tvm-ffi** execution backend, and the tvm-ffi Metal runtime in the
287-
pinned TileLang build cannot launch cooperative-tensor kernels: such
288-
kernels lower to a host module the adapter cannot introspect
289-
(``device_mod is None``), so the recovered launch config collapses to a
290-
degenerate ``(1,1,1) x (1,1,1)`` grid and the kernel returns numerically
291-
wrong output. Until that runtime path lands, the safe owner-output route
292-
stays on the dot4 kernel. Set ``CPPMEGA_FP8_PATH_C_MATMUL2D=1`` to opt in
293-
once the active TileLang build can launch cooperative kernels through
294-
tvm-ffi.
295+
matmul2d is the **default and only** owner-output GEMM path (RULE #1: the
296+
clear path; no silent gate-off to dot4). The cooperative-tensor ``matmul2d``
297+
kernel is numerically exact (maxdiff 0 vs the FP8 reference) and is launched
298+
correctly by the production **tvm-ffi** owner-output route on Apple M4+:
299+
verified end-to-end through ``fp8_scaled_matmul_path_c_direct`` /
300+
``NativeTileLangKernel`` at 128^3..1024^3 (maxdiff 0.00000, up to ~4.4x
301+
faster than the legacy LUT-dot4 accumulation at 1024^3).
302+
303+
This requires a TileLang build whose tvm-ffi Metal runtime launches the
304+
cooperative-tensor kernel with the recovered launch config (the
305+
``_restore_metal_device_mod`` / ``_metal_launch_config`` recovery in
306+
``tilelang/jit/adapter/tvm_ffi.py``) and commits + syncs the owner output
307+
buffer. Earlier the route was gated OFF because that runtime path collapsed
308+
the launch config to a degenerate ``(1,1,1) x (1,1,1)`` grid and the owner
309+
output came back zeroed; both are fixed in the live build, so the gate is
310+
removed and matmul2d is unconditional.
311+
312+
``CPPMEGA_FP8_PATH_C_MATMUL2D`` is retained only as an explicit emergency
313+
opt-OUT (set it to ``0``/``false``/``off`` to force the legacy dot4 kernel
314+
on a host whose tvm-ffi runtime regresses). It is **not** consulted to
315+
silently fall back; matmul2d failures RAISE.
295316
"""
296317
import os
297318

298319
val = os.environ.get(FP8_PATH_C_MATMUL2D_ENV, "").strip().lower()
299-
return val in {"1", "true", "yes", "on"}
320+
if val in {"0", "false", "no", "off"}:
321+
return False
322+
return True
300323

301324

302325
_FP8_MATMUL_TVM_FFI_KERNEL_CACHE: dict[
@@ -336,34 +359,40 @@ def _fp8_matmul_tvm_ffi_kernel_for(
336359

337360
target = _msl_transform._as_metal_target(TILELANG_METAL_MATMUL_TARGET)
338361

339-
# Prefer the Metal-4 cooperative-tensor matmul2d emission when explicitly
340-
# enabled (and the active TileLang build can launch cooperative kernels
341-
# through tvm-ffi): on M4+ it is numerically exact (maxdiff 0 vs the FP8
342-
# reference) and 1.5x-10x faster than both the legacy LUT-dot4
343-
# accumulation and the pure-MLX FP8 reference across 128^3..1024^3.
344-
# Otherwise dispatch the dot4 owner-output kernel, which the pinned
345-
# tvm-ffi Metal runtime launches correctly, so the owner-output contract
346-
# never returns numerically wrong output. See _matmul2d_owner_output_enabled.
347-
kernel = None
362+
# RULE #1: the Metal-4 cooperative-tensor matmul2d emission is the clear,
363+
# default owner-output GEMM path. It is launched correctly by the production
364+
# tvm-ffi owner-output route on Apple M4+ (verified end-to-end via
365+
# fp8_scaled_matmul_path_c_direct / NativeTileLangKernel): numerically exact
366+
# (maxdiff 0.00000 vs the FP8 reference) and up to ~4.4x faster than the
367+
# legacy LUT-dot4 accumulation at 1024^3. A matmul2d *compile* failure RAISES
368+
# here (caught and re-raised as FP8MatmulPathCDirectError by the caller) --
369+
# it does NOT silently fall back to dot4.
370+
#
371+
# The one shape-correct exception: when M/N/K admit no legal cooperative
372+
# tile (`_make_scaled_matmul2d_kernel` -> None, e.g. tiny/non-aligned shapes
373+
# like 16x16x32), the cooperative kernel cannot tile the output without
374+
# emitting a wrong partial-tile result, so the shape-agnostic dot4 kernel is
375+
# used. That is shape-correct dispatch, not a silent gate-off: every
376+
# production matmul shape (128^3..1024^3) yields a real cooperative tile and
377+
# runs matmul2d. dot4 is also used when the caller explicitly opts out via
378+
# CPPMEGA_FP8_PATH_C_MATMUL2D=0.
379+
coop_prim = None
348380
if _matmul2d_owner_output_enabled():
349-
try:
350-
coop_prim = _make_scaled_matmul2d_kernel(
351-
M=M,
352-
N=N,
353-
K=K,
354-
num_stages=num_stages,
355-
c_dtype=c_dtype,
356-
)
357-
kernel = tilelang.compile(
358-
coop_prim,
359-
target=target,
360-
execution_backend="tvm_ffi",
361-
out_idx=-1,
362-
)
363-
except Exception:
364-
kernel = None
365-
366-
if kernel is None:
381+
coop_prim = _make_scaled_matmul2d_kernel(
382+
M=M,
383+
N=N,
384+
K=K,
385+
num_stages=num_stages,
386+
c_dtype=c_dtype,
387+
)
388+
if coop_prim is not None:
389+
kernel = tilelang.compile(
390+
coop_prim,
391+
target=target,
392+
execution_backend="tvm_ffi",
393+
out_idx=-1,
394+
)
395+
else:
367396
prim = _make_scaled_matmul_kernel(
368397
M=M,
369398
N=N,

cppmega_v4/_tilelang/fused_fp8_gemm.py

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,10 +36,21 @@
3636
storage byte (not the signed e4m3 decode), which silently corrupts the
3737
result. The compiled MSL is driven through MLX's native
3838
``mx.fast.metal_kernel`` stream via the host ``wrap_tilelang_metal_kernel``
39-
adapter, so it shares MLX command buffers (the tvm-ffi / DLPack
40-
owner-output route is not used because this MLX build does not expose
41-
``mx.metal._current_command_buffer``, so its kernels read uncommitted
42-
buffers — even the shipped ``fp8_matmul_path_c`` returns zeros here).
39+
adapter, so it shares MLX command buffers.
40+
41+
The tvm-ffi / DLPack owner-output route (as used by the shipped dense
42+
``fp8_scaled_matmul_path_c``) is intentionally **not** used here. That route
43+
now works for the dense matmul's production kernel signature (5-param:
44+
fp8 A/B + two fp32 per-tensor scales + fp32 C) after the live tvm-ffi Metal
45+
fixes — verified numerically exact end-to-end on M4. But this fused GEMM's
46+
kernel signature (two float16 tile inputs + fp32 C, no scale buffers) still
47+
fails to launch through the live tvm-ffi Metal runtime: its compiled host
48+
image dispatches to ``ffi.Function returned NULL`` (the launch-config /
49+
device_mod recovery that makes the dense 5-param signature work does not yet
50+
cover this signature shape). Until that runtime gap is closed, the
51+
cooperative-tensor fused GEMM stays on the MSL ``mx.fast.metal_kernel``
52+
adapter, which is numerically correct. See the verification note in the PR
53+
that enabled dense matmul2d.
4354
4455
* **Scalar hand-MSL fallback** (legacy). One thread per output column ×
4556
batch row, used when TileLang is unimportable, the Metal compile callback

0 commit comments

Comments
 (0)