2828FP8_PATH_C_LEGACY_MLX_FAST_ENV = "CPPMEGA_FP8_PATH_C_LEGACY_MLX_FAST"
2929
3030_FP8_MM_M = 16
31- _FP8_MM_N = 16
31+ _FP8_MM_N = 32
3232_FP8_MM_K = 32
3333_FP8_MM_BM = 16
34- _FP8_MM_BN = 16
34+ _FP8_MM_BN = 32
3535_FP8_MM_BK = 32
36+ _FP8_MM_THREADS = 32
3637_FP8_MM_NUM_STAGES = 0
3738_FP8_MM_C_DTYPE = "float32"
3839
3940
41+ def _coop_tile_for (M : int , N : int , K : int ) -> tuple [int , int , int , int ]:
42+ """Pick a Metal-4 cooperative-tensor (matmul2d) tile (BM, BN, BK, threads).
43+
44+ The cooperative ``mpp::tensor_ops::matmul2d`` micro-tile is 16x32x16: the
45+ selector in ``src/backend/metal/op/gemm.cc`` only emits ``matmul2d`` when
46+ ``BM % 16 == 0``, ``BN % 32 == 0``, ``BK % 16 == 0`` and the threadgroup's
47+ warp count (``threads / 32``) partitions cleanly into ``BM/16 x BN/32``
48+ warp tiles. These tiles were measured on M4 Max to beat both the legacy
49+ LUT-dot4 Path C accumulation and the pure-MLX FP8 reference across the
50+ production matmul shapes (128^3..1024^3); see the bench receipt.
51+ """
52+ # Ordered best-first per shape (measured on M4 Max). Each entry keeps the
53+ # cooperative-tensor threadgroup memory under the 32 KiB Metal budget that
54+ # the tvm-ffi runtime enforces:
55+ # shmem ~= BM*BK*2 (half A) + BN*BK*2 (half B) + BM*BN*4 (fp32 C) + pad.
56+ candidates = (
57+ (32 , 64 , 32 , 128 ), # ~16 KiB; best for 256/1024^3
58+ (32 , 32 , 32 , 64 ), # ~8 KiB; best for 512^3
59+ (32 , 64 , 64 , 128 ), # ~24 KiB
60+ (16 , 32 , 32 , 32 ), # ~4 KiB
61+ (16 , 32 , 16 , 32 ), # smallest legal cooperative tile
62+ )
63+ shmem_budget = 32 * 1024
64+ for bm , bn , bk , threads in candidates :
65+ if M % bm or N % bn or K % bk :
66+ continue
67+ if (M // bm ) <= 0 or (N // bn ) <= 0 :
68+ continue
69+ num_warps = threads // 32
70+ max_m = bm // 16
71+ max_n = bn // 32
72+ if max_m <= 0 or max_n <= 0 :
73+ continue
74+ # warp partition must divide num_warps into the BM/16 x BN/32 grid.
75+ if num_warps != max_m * max_n :
76+ continue
77+ # Conservative shared-memory estimate (padded for matmul2d layout).
78+ shmem = bm * bk * 2 + bn * bk * 2 + bm * bn * 4 + 2 * 1024
79+ if shmem > shmem_budget :
80+ continue
81+ return bm , bn , bk , threads
82+ # Fallback: smallest legal cooperative tile (single warp, 16x32x16).
83+ return 16 , 32 , 16 , 32
84+
85+
4086@dataclass (frozen = True )
4187class FP8MatmulPathCStatus :
4288 available : bool
@@ -72,6 +118,19 @@ def _fp8_scaled_matmul_kernel_template(
72118 B_scale : T .Tensor ((1 ,), "float32" ), # type: ignore[name-defined] # noqa: F821
73119 C : T .Tensor ((_FP8_MM_M , _FP8_MM_N ), _FP8_MM_C_DTYPE ), # type: ignore[name-defined] # noqa: F821
74120):
121+ """Legacy LUT-dot4 dense FP8 matmul (per-output-cell scalar accumulation).
122+
123+ This is the original Path C emission: one Metal thread owns one output
124+ cell and walks K via ``T.metal_fp8_e4m3_dot4`` packed LUT decode. It is
125+ ~1.7x slower than the pure-MLX FP8 reference at the production matmul
126+ shapes, but it is the path the tvm-ffi owner-output runtime can launch
127+ today, so it remains the safe owner-output fallback.
128+
129+ The faster Metal-4 cooperative-tensor ``matmul2d`` emission lives in
130+ ``_fp8_scaled_matmul2d_kernel_template`` and is selected by
131+ ``_make_owner_output_matmul_kernel`` whenever the active TileLang backend
132+ can launch cooperative-tensor kernels.
133+ """
75134 with T .Kernel ( # type: ignore[name-defined] # noqa: F821
76135 T .ceildiv (_FP8_MM_N , _FP8_MM_BN ), # type: ignore[name-defined] # noqa: F821
77136 T .ceildiv (_FP8_MM_M , _FP8_MM_BM ), # type: ignore[name-defined] # noqa: F821
@@ -93,6 +152,56 @@ def _fp8_scaled_matmul_kernel_template(
93152 )
94153
95154
155+ def _fp8_scaled_matmul2d_kernel_template (
156+ A_fp8 : T .Tensor ((_FP8_MM_M , _FP8_MM_K ), "float8_e4m3" ), # type: ignore[name-defined] # noqa: F821
157+ A_scale : T .Tensor ((1 ,), "float32" ), # type: ignore[name-defined] # noqa: F821
158+ B_fp8 : T .Tensor ((_FP8_MM_N , _FP8_MM_K ), "float8_e4m3" ), # type: ignore[name-defined] # noqa: F821
159+ B_scale : T .Tensor ((1 ,), "float32" ), # type: ignore[name-defined] # noqa: F821
160+ C : T .Tensor ((_FP8_MM_M , _FP8_MM_N ), _FP8_MM_C_DTYPE ), # type: ignore[name-defined] # noqa: F821
161+ ):
162+ """Dense FP8 matmul via the Metal-4 cooperative-tensor ``matmul2d`` GEMM.
163+
164+ Instead of the legacy per-output-cell ``T.metal_fp8_e4m3_dot4`` (LUT
165+ scalar accumulation), this dequantizes the FP8 ``e4m3`` tiles to half
166+ into ``shared`` cooperative-input buffers and runs the cooperative
167+ ``mpp::tensor_ops::matmul2d`` GEMM (triggered because the accumulator
168+ ``Cs`` is in ``shared`` scope). The per-tensor scales are applied to the
169+ shared accumulator once after the K reduction.
170+
171+ The FP8->half dequant routes each byte through an explicit ``float32``
172+ scratch var so the Metal backend emits the per-element
173+ ``__tvm_fp8_e4m3_to_half`` decode. A direct vectorized ``T.cast`` of an
174+ FP8 buffer to ``half4`` mis-lowers to a raw ``(half4)(uchar4)`` integer
175+ cast (the LUT decode is skipped), which is numerically wrong; the scalar
176+ scratch breaks that vectorization while keeping ``T.Parallel`` thread
177+ coverage.
178+ """
179+ with T .Kernel ( # type: ignore[name-defined] # noqa: F821
180+ T .ceildiv (_FP8_MM_N , _FP8_MM_BN ), # type: ignore[name-defined] # noqa: F821
181+ T .ceildiv (_FP8_MM_M , _FP8_MM_BM ), # type: ignore[name-defined] # noqa: F821
182+ threads = _FP8_MM_THREADS ,
183+ ) as (bx , by ):
184+ A_shared = T .alloc_shared ((_FP8_MM_BM , _FP8_MM_BK ), "float16" , scope = "shared" ) # type: ignore[name-defined] # noqa: F821
185+ B_shared = T .alloc_shared ((_FP8_MM_BN , _FP8_MM_BK ), "float16" , scope = "shared" ) # type: ignore[name-defined] # noqa: F821
186+ C_shared = T .alloc_shared ((_FP8_MM_BM , _FP8_MM_BN ), _FP8_MM_C_DTYPE , scope = "shared" ) # type: ignore[name-defined] # noqa: F821
187+ T .clear (C_shared )
188+ for ko in T .serial (T .ceildiv (_FP8_MM_K , _FP8_MM_BK )): # type: ignore[name-defined] # noqa: F821
189+ for i , kk in T .Parallel (_FP8_MM_BM , _FP8_MM_BK ): # type: ignore[name-defined] # noqa: F821
190+ a_val = T .alloc_var ("float32" ) # type: ignore[name-defined] # noqa: F821
191+ a_val = T .cast (A_fp8 [by * _FP8_MM_BM + i , ko * _FP8_MM_BK + kk ], "float32" ) # type: ignore[name-defined] # noqa: F821
192+ A_shared [i , kk ] = T .cast (a_val , "float16" ) # type: ignore[name-defined] # noqa: F821
193+ for j , kk in T .Parallel (_FP8_MM_BN , _FP8_MM_BK ): # type: ignore[name-defined] # noqa: F821
194+ b_val = T .alloc_var ("float32" ) # type: ignore[name-defined] # noqa: F821
195+ b_val = T .cast (B_fp8 [bx * _FP8_MM_BN + j , ko * _FP8_MM_BK + kk ], "float32" ) # type: ignore[name-defined] # noqa: F821
196+ B_shared [j , kk ] = T .cast (b_val , "float16" ) # type: ignore[name-defined] # noqa: F821
197+ T .gemm (A_shared , B_shared , C_shared , transpose_B = True ) # type: ignore[name-defined] # noqa: F821
198+ sa = A_scale [0 ]
199+ sb = B_scale [0 ]
200+ for i , j in T .Parallel (_FP8_MM_BM , _FP8_MM_BN ): # type: ignore[name-defined] # noqa: F821
201+ C_shared [i , j ] = C_shared [i , j ] * sa * sb
202+ T .copy (C_shared , C [by * _FP8_MM_BM , bx * _FP8_MM_BN ]) # type: ignore[name-defined] # noqa: F821
203+
204+
96205def _make_scaled_matmul_kernel (
97206 * ,
98207 M : int ,
@@ -104,6 +213,7 @@ def _make_scaled_matmul_kernel(
104213 num_stages : int ,
105214 c_dtype : str = "float32" ,
106215) -> Any :
216+ """Build the legacy LUT-dot4 owner-output prim (unchanged emission)."""
107217 import tilelang
108218 from tilelang import language as T
109219 from tvm .target import Target
@@ -123,6 +233,72 @@ def _make_scaled_matmul_kernel(
123233 return tilelang .language .prim_func (_fp8_scaled_matmul_kernel_template )
124234
125235
236+ def _make_scaled_matmul2d_kernel (
237+ * ,
238+ M : int ,
239+ N : int ,
240+ K : int ,
241+ num_stages : int = 0 ,
242+ c_dtype : str = "float32" ,
243+ ) -> Any :
244+ """Build the Metal-4 cooperative-tensor (``matmul2d``) owner-output prim.
245+
246+ The cooperative path imposes its own block / warp constraints
247+ (``BM % 16``, ``BN % 32``, ``BK % 16``, warp partition); the legacy dot4
248+ ``BM/BN/BK`` arguments are not necessarily legal cooperative tiles, so a
249+ measured-best cooperative tile is derived from the GEMM extents.
250+ """
251+ import tilelang
252+ from tilelang import language as T
253+ from tvm .target import Target
254+
255+ bm , bn , bk , threads = _coop_tile_for (int (M ), int (N ), int (K ))
256+
257+ globals ().update (
258+ T = T ,
259+ Target = Target ,
260+ _FP8_MM_M = int (M ),
261+ _FP8_MM_N = int (N ),
262+ _FP8_MM_K = int (K ),
263+ _FP8_MM_BM = int (bm ),
264+ _FP8_MM_BN = int (bn ),
265+ _FP8_MM_BK = int (bk ),
266+ _FP8_MM_THREADS = int (threads ),
267+ _FP8_MM_NUM_STAGES = int (num_stages ),
268+ _FP8_MM_C_DTYPE = str (c_dtype ),
269+ )
270+ return tilelang .language .prim_func (_fp8_scaled_matmul2d_kernel_template )
271+
272+
273+ FP8_PATH_C_MATMUL2D_ENV = "CPPMEGA_FP8_PATH_C_MATMUL2D"
274+
275+
276+ def _matmul2d_owner_output_enabled () -> bool :
277+ """Whether to route the owner-output GEMM through Metal-4 ``matmul2d``.
278+
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+ """
296+ import os
297+
298+ val = os .environ .get (FP8_PATH_C_MATMUL2D_ENV , "" ).strip ().lower ()
299+ return val in {"1" , "true" , "yes" , "on" }
300+
301+
126302_FP8_MATMUL_TVM_FFI_KERNEL_CACHE : dict [
127303 tuple [int , int , int , int , int , int , int , str ],
128304 Any ,
@@ -158,22 +334,52 @@ def _fp8_matmul_tvm_ffi_kernel_for(
158334
159335 import tilelang
160336
161- prim = _make_scaled_matmul_kernel (
162- M = M ,
163- N = N ,
164- K = K ,
165- BM = BM ,
166- BN = BN ,
167- BK = BK ,
168- num_stages = num_stages ,
169- c_dtype = c_dtype ,
170- )
171- kernel = tilelang .compile (
172- prim ,
173- target = _msl_transform ._as_metal_target (TILELANG_METAL_MATMUL_TARGET ),
174- execution_backend = "tvm_ffi" ,
175- out_idx = - 1 ,
176- )
337+ target = _msl_transform ._as_metal_target (TILELANG_METAL_MATMUL_TARGET )
338+
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
348+ 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 :
367+ prim = _make_scaled_matmul_kernel (
368+ M = M ,
369+ N = N ,
370+ K = K ,
371+ BM = BM ,
372+ BN = BN ,
373+ BK = BK ,
374+ num_stages = num_stages ,
375+ c_dtype = c_dtype ,
376+ )
377+ kernel = tilelang .compile (
378+ prim ,
379+ target = target ,
380+ execution_backend = "tvm_ffi" ,
381+ out_idx = - 1 ,
382+ )
177383 with _FP8_MATMUL_TVM_FFI_KERNEL_CACHE_LOCK :
178384 _FP8_MATMUL_TVM_FFI_KERNEL_CACHE [cache_key ] = kernel
179385 return kernel
@@ -391,6 +597,7 @@ def fp8_matmul_path_c_status() -> FP8MatmulPathCStatus:
391597 "FP8MatmulPathCLegacyError" ,
392598 "FP8MatmulPathCStatus" ,
393599 "FP8_PATH_C_LEGACY_MLX_FAST_ENV" ,
600+ "FP8_PATH_C_MATMUL2D_ENV" ,
394601 "TILELANG_METAL_MATMUL_TARGET" ,
395602 "fp8_matmul_path_c_status" ,
396603 "fp8_scaled_matmul_path_c_direct" ,
0 commit comments