Skip to content

Commit 90f608c

Browse files
committed
feat(path-c): sparse-MLA grouped QK/PV via Metal-4 matmul2d (6.65-73.5x), explicit shape-dispatch
Tier-3 #3/#4/#5: sparse-MLA decode QK/PV are M=1 vecmat per query lane (can't tile a cooperative micro-tile). The matmul2d-admissible formulation is GROUPED: the head_kv query heads of one KV group share the gathered KV, so QK = [head_kv,qk_dim] @ [topk,qk_dim]^T (M=head_kv), PV = [head_kv,topk] @ [topk,d_v] -- legal cooperative tiles when head_kv>=16/topk>=32/d_v>=32 (DeepSeek-V3 head_kv=128). New grouped-GEMM surfaces (purely additive, +900/-0; existing per-lane kernels untouched): - bf16 sparse_mla_path_c: make_sparse_mla_grouped_gemm_matmul2d_kernel + grouped_qk/pv_path_c - fp8 sparse_mla_fp8_path_c: make_fp8_sparse_mla_qk_reduce_matmul2d_kernel (e4m3->half dequant into cooperative inputs, scale after K-reduce) - blockscaled: make_blockscaled_sparse_mla_qk_reduce_matmul2d_kernel (E8M0 per-K-block scale folded into half cooperative inputs before the raw-product GEMM) M4 production tvm-ffi: parity 0.00000 across all shapes incl DeepSeek 128x64x576 (QK) / 128x512x2048 (PV); timing vs per-head M=1 dot4 launches 6.65x (head_kv16) -> 73.5x (head_kv128). 176 passed, all failures pre-existing on origin (stash-confirmed). RULE #1: explicit _coop_tile_for dispatch -- sub-tile shapes RAISE (bf16/blockscaled) or use verified dot4 (fp8); matmul2d compile-fail RAISES, no silent dot4 fallback. Follow-up: wire grouped GEMM into the fused forward's group loop.
1 parent 9fb1ee6 commit 90f608c

3 files changed

Lines changed: 900 additions & 0 deletions

File tree

cppmega_mlx/nn/_tilelang/sparse_mla_blockscaled_path_c.py

Lines changed: 289 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)