@@ -1916,6 +1916,293 @@ def blockscaled_sparse_mla_qk_path_c_status(
19161916 )
19171917
19181918
1919+ # --- Metal-4 cooperative-tensor (matmul2d) grouped E8M0 QK reducer ------------
1920+ #
1921+ # The M=1 ``make_blockscaled_sparse_mla_qk_reduce_kernel`` runs one query row
1922+ # against the gathered top-k KV rows, folding the per-K-block E8M0 scale into
1923+ # each scalar dot4 term. When ``head_kv`` query heads of a KV group are tiled
1924+ # together, the QK tile is a ``[head_kv, qk_dim] @ [topk, qk_dim]^T`` GEMM. The
1925+ # cooperative-tensor ``matmul2d`` accumulates raw products, so the E8M0
1926+ # per-K-block scales are folded into the half cooperative-input tiles BEFORE the
1927+ # GEMM: ``A_shared[i,kk] = e4m3_to_half(A_fp8[i,kk]) * e8m0_to_float(A_scale[kb])``
1928+ # (likewise B). For group shapes that admit a legal cooperative tile this beats
1929+ # the per-row scalar reduction; sub-tile group shapes RAISE in the dispatcher
1930+ # (RULE #1: explicit shape-correct dispatch, no silent gate).
1931+
1932+ _BSFP8_QKMM_M = 16
1933+ _BSFP8_QKMM_N = 32
1934+ _BSFP8_QKMM_K = 64
1935+ _BSFP8_QKMM_BM = 16
1936+ _BSFP8_QKMM_BN = 32
1937+ _BSFP8_QKMM_BK = 32
1938+ _BSFP8_QKMM_THREADS = 32
1939+ _BSFP8_QKMM_BS = 1
1940+ _BSFP8_QKMM_SCALE_BLOCKS = 2
1941+
1942+
1943+ def make_blockscaled_sparse_mla_qk_reduce_matmul2d_kernel (
1944+ * ,
1945+ M : int ,
1946+ N : int ,
1947+ K : int ,
1948+ BM : int ,
1949+ BN : int ,
1950+ BK : int ,
1951+ threads : int ,
1952+ B_SCALE_ROWS : int | None = None ,
1953+ ) -> Any :
1954+ """Build the grouped E8M0 QK tile as a Metal-4 cooperative-tensor GEMM.
1955+
1956+ Computes ``C[M, N] = (dequant(A_fp8, A_scale) @ dequant(B_fp8, B_scale)^T)``
1957+ where ``M = head_kv`` query rows share the gathered top-k KV matrix and the
1958+ E8M0 per-K-block (size 32) scales are folded into the half cooperative-input
1959+ tiles before the GEMM.
1960+
1961+ ``BM/BN/BK/threads`` MUST be a legal cooperative tile dividing ``M/N/K`` and
1962+ ``BK`` must be a multiple of ``E8M0_BLOCK_SIZE`` so each shared K-tile maps
1963+ to whole E8M0 scale blocks. A violated precondition RAISES (RULE #1).
1964+ """
1965+
1966+ if M <= 0 or N <= 0 or K <= 0 :
1967+ raise ValueError (
1968+ f"grouped E8M0 QK matmul2d requires positive M/N/K; got M={ M } N={ N } K={ K } "
1969+ )
1970+ if K % E8M0_BLOCK_SIZE != 0 :
1971+ raise ValueError (
1972+ f"grouped E8M0 QK matmul2d requires K divisible by { E8M0_BLOCK_SIZE } ; got K={ K } "
1973+ )
1974+ if M % BM or N % BN or K % BK :
1975+ raise ValueError (
1976+ "grouped E8M0 QK matmul2d tile does not divide the GEMM extents: "
1977+ f"M={ M } N={ N } K={ K } BM={ BM } BN={ BN } BK={ BK } "
1978+ )
1979+ if BM % 16 or BN % 32 or BK % 16 :
1980+ raise ValueError (
1981+ "grouped E8M0 QK matmul2d tile violates the Metal-4 cooperative "
1982+ f"constraint (BM%16, BN%32, BK%16): BM={ BM } BN={ BN } BK={ BK } "
1983+ )
1984+ if BK % E8M0_BLOCK_SIZE != 0 :
1985+ raise ValueError (
1986+ f"grouped E8M0 QK matmul2d requires BK divisible by { E8M0_BLOCK_SIZE } ; got BK={ BK } "
1987+ )
1988+ b_scale_rows = N if B_SCALE_ROWS is None else int (B_SCALE_ROWS )
1989+ if b_scale_rows not in (1 , N ):
1990+ raise ValueError (f"B_SCALE_ROWS must be 1 or N={ N } ; got { b_scale_rows } " )
1991+
1992+ from tilelang import language as T
1993+ from tilelang .tileop .metal_quant import e8m0_to_float
1994+ from tvm .target import Target
1995+
1996+ T = cast (Any , T )
1997+
1998+ scale_blocks = K // E8M0_BLOCK_SIZE
1999+ g = globals ()
2000+ g .update (
2001+ T = T ,
2002+ Target = Target ,
2003+ e8m0_to_float = e8m0_to_float ,
2004+ _BSFP8_QKMM_M = int (M ),
2005+ _BSFP8_QKMM_N = int (N ),
2006+ _BSFP8_QKMM_K = int (K ),
2007+ _BSFP8_QKMM_BM = int (BM ),
2008+ _BSFP8_QKMM_BN = int (BN ),
2009+ _BSFP8_QKMM_BK = int (BK ),
2010+ _BSFP8_QKMM_THREADS = int (threads ),
2011+ _BSFP8_QKMM_BS = int (b_scale_rows ),
2012+ _BSFP8_QKMM_SCALE_BLOCKS = int (scale_blocks ),
2013+ )
2014+
2015+ @T .prim_func
2016+ def blockscaled_sparse_mla_qk_reduce_matmul2d (
2017+ A_fp8 : T .Tensor ((_BSFP8_QKMM_M , _BSFP8_QKMM_K ), "float8_e4m3" ),
2018+ A_scale : T .Tensor ((_BSFP8_QKMM_M , _BSFP8_QKMM_SCALE_BLOCKS ), "uint8" ),
2019+ B_fp8 : T .Tensor ((_BSFP8_QKMM_N , _BSFP8_QKMM_K ), "float8_e4m3" ),
2020+ B_scale : T .Tensor ((_BSFP8_QKMM_BS , _BSFP8_QKMM_SCALE_BLOCKS ), "uint8" ),
2021+ C : T .Tensor ((_BSFP8_QKMM_M , _BSFP8_QKMM_N ), "float32" ),
2022+ ):
2023+ with T .Kernel (
2024+ T .ceildiv (_BSFP8_QKMM_N , _BSFP8_QKMM_BN ),
2025+ T .ceildiv (_BSFP8_QKMM_M , _BSFP8_QKMM_BM ),
2026+ threads = _BSFP8_QKMM_THREADS ,
2027+ ) as (bx , by ):
2028+ A_shared = T .alloc_shared ((_BSFP8_QKMM_BM , _BSFP8_QKMM_BK ), "float16" , scope = "shared" )
2029+ B_shared = T .alloc_shared ((_BSFP8_QKMM_BN , _BSFP8_QKMM_BK ), "float16" , scope = "shared" )
2030+ C_shared = T .alloc_shared ((_BSFP8_QKMM_BM , _BSFP8_QKMM_BN ), "float32" , scope = "shared" )
2031+ T .clear (C_shared )
2032+ for ko in T .serial (T .ceildiv (_BSFP8_QKMM_K , _BSFP8_QKMM_BK )):
2033+ for i , kk in T .Parallel (_BSFP8_QKMM_BM , _BSFP8_QKMM_BK ):
2034+ a_val = T .alloc_var ("float32" )
2035+ a_kb = (ko * _BSFP8_QKMM_BK + kk ) // E8M0_BLOCK_SIZE
2036+ a_val = T .cast (A_fp8 [by * _BSFP8_QKMM_BM + i , ko * _BSFP8_QKMM_BK + kk ], "float32" )
2037+ A_shared [i , kk ] = T .cast (
2038+ a_val * e8m0_to_float (A_scale [by * _BSFP8_QKMM_BM + i , a_kb ]), "float16"
2039+ )
2040+ for j , kk in T .Parallel (_BSFP8_QKMM_BN , _BSFP8_QKMM_BK ):
2041+ b_val = T .alloc_var ("float32" )
2042+ b_kb = (ko * _BSFP8_QKMM_BK + kk ) // E8M0_BLOCK_SIZE
2043+ b_row = 0 if _BSFP8_QKMM_BS == 1 else (bx * _BSFP8_QKMM_BN + j )
2044+ b_val = T .cast (B_fp8 [bx * _BSFP8_QKMM_BN + j , ko * _BSFP8_QKMM_BK + kk ], "float32" )
2045+ B_shared [j , kk ] = T .cast (
2046+ b_val * e8m0_to_float (B_scale [b_row , b_kb ]), "float16"
2047+ )
2048+ T .gemm (A_shared , B_shared , C_shared , transpose_B = True )
2049+ T .copy (C_shared , C [by * _BSFP8_QKMM_BM , bx * _BSFP8_QKMM_BN ])
2050+
2051+ return blockscaled_sparse_mla_qk_reduce_matmul2d
2052+
2053+
2054+ @lru_cache (maxsize = 128 )
2055+ def _qk_reduce_matmul2d_kernel_for_blockscaled (
2056+ M : int ,
2057+ N : int ,
2058+ K : int ,
2059+ BM : int ,
2060+ BN : int ,
2061+ BK : int ,
2062+ threads : int ,
2063+ b_scale_rows : int ,
2064+ ) -> Any :
2065+ """Build and cache the tvm-ffi cooperative-tensor grouped E8M0 QK reducer."""
2066+
2067+ import tilelang
2068+
2069+ prim = make_blockscaled_sparse_mla_qk_reduce_matmul2d_kernel (
2070+ M = M ,
2071+ N = N ,
2072+ K = K ,
2073+ BM = BM ,
2074+ BN = BN ,
2075+ BK = BK ,
2076+ threads = threads ,
2077+ B_SCALE_ROWS = b_scale_rows ,
2078+ )
2079+ return tilelang .compile (
2080+ prim ,
2081+ target = _msl_transform ._as_metal_target (TILELANG_METAL_E8M0_SPARSE_MLA_TARGET ),
2082+ execution_backend = "tvm_ffi" ,
2083+ out_idx = 4 ,
2084+ )
2085+
2086+
2087+ def _aligned_contiguous_bsfp8 (array : mx .array ) -> mx .array :
2088+ """Fresh, 64-byte-aligned, row-major copy (tvm-ffi DLPack requires align=64)."""
2089+
2090+ if array .dtype == mx .uint8 :
2091+ fresh = mx .bitwise_or (array , mx .array (0 , dtype = mx .uint8 ))
2092+ else :
2093+ fresh = array + mx .array (0 , dtype = array .dtype )
2094+ return mx .contiguous (fresh )
2095+
2096+
2097+ def blockscaled_sparse_mla_qk_reduce_grouped_path_c (
2098+ A_fp8 : mx .array ,
2099+ A_scale : mx .array ,
2100+ B_fp8 : mx .array ,
2101+ B_scale : mx .array ,
2102+ out : mx .array ,
2103+ ) -> mx .array :
2104+ """Run the grouped E8M0 QK tile with explicit matmul2d/scalar dispatch.
2105+
2106+ ``A_fp8`` is ``(M=head_kv, K)`` e4m3 uint8 storage, ``A_scale`` its
2107+ ``(M, K/32)`` E8M0 uint8 block scales; ``B_fp8`` the gathered ``(N=topk, K)``
2108+ KV rows with ``(N or 1, K/32)`` E8M0 scales; ``out`` the caller-owned
2109+ ``(M, N)`` fp32 score tile.
2110+
2111+ Routes to the Metal-4 cooperative-tensor matmul2d GEMM (E8M0 scales folded
2112+ into the half cooperative inputs) when ``_coop_tile_for(M, N, K)`` is legal;
2113+ otherwise RAISES -- the M=1 scalar reducer remains the shape-correct path for
2114+ sub-tile groups (callers route those through ``blockscaled_sparse_mla_qk_
2115+ reduce_path_c``). RULE #1: explicit shape dispatch, no silent fallback here.
2116+ """
2117+
2118+ if not can_run_metal ():
2119+ raise RuntimeError (
2120+ "blockscaled_sparse_mla_qk_reduce_grouped_path_c: MLX Metal backend "
2121+ "unavailable."
2122+ )
2123+ if A_fp8 .ndim != 2 or B_fp8 .ndim != 2 :
2124+ raise ValueError (
2125+ "blockscaled_sparse_mla_qk_reduce_grouped_path_c: A_fp8/B_fp8 must be "
2126+ f"2D; got A={ tuple (A_fp8 .shape )} B={ tuple (B_fp8 .shape )} "
2127+ )
2128+ if A_fp8 .dtype != mx .uint8 or B_fp8 .dtype != mx .uint8 :
2129+ raise ValueError (
2130+ "blockscaled_sparse_mla_qk_reduce_grouped_path_c: A_fp8/B_fp8 must be "
2131+ f"uint8 e4m3 storage; got { A_fp8 .dtype } , { B_fp8 .dtype } "
2132+ )
2133+ if A_scale .dtype != mx .uint8 or B_scale .dtype != mx .uint8 :
2134+ raise TypeError (
2135+ "blockscaled_sparse_mla_qk_reduce_grouped_path_c: A_scale/B_scale must "
2136+ f"be uint8 E8M0 block scales; got { A_scale .dtype } , { B_scale .dtype } "
2137+ )
2138+ M = int (A_fp8 .shape [0 ])
2139+ K = int (A_fp8 .shape [1 ])
2140+ N = int (B_fp8 .shape [0 ])
2141+ if M <= 0 or N <= 0 or K <= 0 or int (B_fp8 .shape [1 ]) != K :
2142+ raise ValueError (
2143+ "blockscaled_sparse_mla_qk_reduce_grouped_path_c: A/B shape mismatch: "
2144+ f"A={ tuple (A_fp8 .shape )} B={ tuple (B_fp8 .shape )} "
2145+ )
2146+ if K % E8M0_BLOCK_SIZE != 0 :
2147+ raise ValueError (
2148+ "blockscaled_sparse_mla_qk_reduce_grouped_path_c: K must be divisible "
2149+ f"by { E8M0_BLOCK_SIZE } ; got K={ K } "
2150+ )
2151+ scale_blocks = K // E8M0_BLOCK_SIZE
2152+ if tuple (A_scale .shape ) != (M , scale_blocks ):
2153+ raise ValueError (
2154+ "blockscaled_sparse_mla_qk_reduce_grouped_path_c: A_scale must have "
2155+ f"shape ({ M } , { scale_blocks } ); got { tuple (A_scale .shape )} "
2156+ )
2157+ if tuple (B_scale .shape ) not in ((1 , scale_blocks ), (N , scale_blocks )):
2158+ raise ValueError (
2159+ "blockscaled_sparse_mla_qk_reduce_grouped_path_c: B_scale must have "
2160+ f"shape (1, { scale_blocks } ) or ({ N } , { scale_blocks } ); got { tuple (B_scale .shape )} "
2161+ )
2162+ if tuple (out .shape ) != (M , N ) or out .dtype != mx .float32 :
2163+ raise ValueError (
2164+ "blockscaled_sparse_mla_qk_reduce_grouped_path_c: out must be fp32 "
2165+ f"shape ({ M } , { N } ); got { tuple (out .shape )} { out .dtype } "
2166+ )
2167+
2168+ from cppmega_mlx .nn ._tilelang .fp8_matmul_path_c import _coop_tile_for
2169+
2170+ coop = _coop_tile_for (M , N , K )
2171+ if coop is None :
2172+ raise ValueError (
2173+ "blockscaled_sparse_mla_qk_reduce_grouped_path_c: grouped shape "
2174+ f"M={ M } N={ N } K={ K } admits no legal Metal-4 cooperative tile "
2175+ "(needs M%16==0, N%32==0, K%16==0). Sub-tile group shapes are served "
2176+ "by the M=1 scalar reducer (blockscaled_sparse_mla_qk_reduce_path_c)."
2177+ )
2178+ bm , bn , bk , threads = coop
2179+ if bk % E8M0_BLOCK_SIZE != 0 :
2180+ raise ValueError (
2181+ "blockscaled_sparse_mla_qk_reduce_grouped_path_c: cooperative tile "
2182+ f"BK={ bk } is not a multiple of E8M0 block size { E8M0_BLOCK_SIZE } ; the "
2183+ f"per-K-block scale fold is undefined for M={ M } N={ N } K={ K } ."
2184+ )
2185+ b_scale_rows = 1 if int (B_scale .shape [0 ]) == 1 else N
2186+ A_fp8_a = _aligned_contiguous_bsfp8 (A_fp8 )
2187+ A_scale_a = _aligned_contiguous_bsfp8 (A_scale )
2188+ B_fp8_a = _aligned_contiguous_bsfp8 (B_fp8 )
2189+ B_scale_a = _aligned_contiguous_bsfp8 (B_scale )
2190+ mx .eval (A_fp8_a , A_scale_a , B_fp8_a , B_scale_a )
2191+ try :
2192+ kernel = _qk_reduce_matmul2d_kernel_for_blockscaled (
2193+ M , N , K , bm , bn , bk , threads , b_scale_rows
2194+ )
2195+ except Exception as exc :
2196+ raise RuntimeError (
2197+ "blockscaled_sparse_mla_qk_reduce_grouped_path_c: cooperative-tensor "
2198+ f"matmul2d compile failed for M={ M } N={ N } K={ K } tile={ coop } "
2199+ f"({ type (exc ).__name__ } : { exc } )."
2200+ ) from exc
2201+ returned = kernel (A_fp8_a , A_scale_a , B_fp8_a , B_scale_a , out )
2202+ result = returned [0 ] if isinstance (returned , (list , tuple )) else returned
2203+ return cast (mx .array , result )
2204+
2205+
19192206__all__ = [
19202207 "E8M0_BLOCK_SIZE" ,
19212208 "E8M0_LAYOUT" ,
@@ -1928,7 +2215,9 @@ def blockscaled_sparse_mla_qk_path_c_status(
19282215 "blockscaled_sparse_mla_qk_path_c_status" ,
19292216 "blockscaled_sparse_mla_qk_reduce_msl_features" ,
19302217 "blockscaled_sparse_mla_qk_reduce_path_c" ,
2218+ "blockscaled_sparse_mla_qk_reduce_grouped_path_c" ,
19312219 "blockscaled_sparse_mla_qk_reduce_path_c_status" ,
2220+ "make_blockscaled_sparse_mla_qk_reduce_matmul2d_kernel" ,
19322221 "blockscaled_sparse_mla_qk_scaled_matmul_probe_status" ,
19332222 "lower_blockscaled_sparse_mla_qk_msl" ,
19342223 "lower_blockscaled_sparse_mla_qk_reduce_msl" ,
0 commit comments