Skip to content

Commit f222b75

Browse files
committed
feat: implement FP8 sparse MLA prepare kernel and integrate dynamic Path-C fusion scheduling logic
1 parent cede3ac commit f222b75

12 files changed

Lines changed: 10535 additions & 231 deletions

cppmega_mlx/models/hybrid_lm.py

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from __future__ import annotations
99

1010
from dataclasses import asdict, dataclass
11-
from typing import Literal, TypedDict, cast
11+
from typing import TYPE_CHECKING, Literal, TypedDict, cast
1212

1313
import mlx.core as mx
1414
import mlx.nn as nn
@@ -32,6 +32,9 @@
3232
from cppmega_mlx.runtime.kernel_policy import KernelPath, selected_path
3333
from cppmega_mlx.training.mtp import MinimalMTPHead, MTPLossConfig
3434

35+
if TYPE_CHECKING:
36+
from cppmega_mlx.runtime.path_c_fusion import PathCFusionRegion
37+
3538
HybridBackend = Literal["attention", "mamba3", "moe", "m2rnn", "engram", "concept"]
3639
HybridBlockModule = (
3740
CausalSelfAttention
@@ -618,6 +621,36 @@ def route_symbols(self) -> tuple[str, ...]:
618621
def route_roles(self) -> tuple[str, ...]:
619622
return tuple(layer.role for layer in self.pattern.layers)
620623

624+
@property
625+
def path_c_bricks(self) -> tuple[dict[str, str], ...]:
626+
return tuple(
627+
{
628+
"name": f"layer_{index}_{block.layer.symbol.lower()}",
629+
"kind": block.backend,
630+
"route_symbol": block.layer.symbol,
631+
}
632+
for index, block in enumerate(self.layers)
633+
)
634+
635+
def path_c_fusion_regions(
636+
self,
637+
*,
638+
include_backward: bool = False,
639+
min_route_bricks: int = 2,
640+
) -> tuple[PathCFusionRegion, ...]:
641+
"""Return Path C fusion candidate regions derived from this model."""
642+
643+
from cppmega_mlx.runtime.path_c_fusion import (
644+
build_path_c_model_regions_from_model,
645+
)
646+
647+
return build_path_c_model_regions_from_model(
648+
self,
649+
region_prefix="hybrid_tiny_lm_path_c",
650+
include_backward=include_backward,
651+
min_route_bricks=min_route_bricks,
652+
)
653+
621654
def __call__(
622655
self,
623656
input_ids: mx.array,

cppmega_mlx/nn/_tilelang/sparse_mla_fp8_path_c.py

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,12 @@
146146
_SMFP8_BWD_CLEAR_TOTAL = _SMFP8_BWD_B * _SMFP8_BWD_SKV * _SMFP8_BWD_G * _SMFP8_BWD_K
147147
_SMFP8_BWD_CLEAR_THREADS = 256
148148
_SMFP8_PER_TOKEN_QUANT_THREADS = 256
149+
_SMFP8_PREPARE_Q_ROWS = 1
150+
_SMFP8_PREPARE_KV_ROWS = 1
151+
_SMFP8_PREPARE_ROWS = _SMFP8_PREPARE_Q_ROWS + _SMFP8_PREPARE_KV_ROWS
152+
_SMFP8_PREPARE_K = 64
153+
_SMFP8_PREPARE_INPUT_DTYPE = "float32"
154+
_SMFP8_PREPARE_STORAGE_DTYPE = "uint8"
149155

150156

151157
@dataclass(frozen=True)
@@ -2709,6 +2715,100 @@ def fp8_per_token_quant(
27092715
return fp8_per_token_quant
27102716

27112717

2718+
def make_fp8_sparse_mla_prepare_kernel(
2719+
*,
2720+
q_rows: int,
2721+
kv_rows: int,
2722+
K: int,
2723+
in_dtype: str,
2724+
storage_dtype: str = "uint8",
2725+
) -> Any:
2726+
"""Build the FP8 prepare producer PrimFunc for fused train-block schedules.
2727+
2728+
``post_y`` is a flat q-then-kv carrier. The kernel emits first-class
2729+
prepared Sparse-MLA FP8 buffers and per-token scales so the downstream
2730+
apply node can stay inside the same TileLang/TVM region.
2731+
"""
2732+
2733+
if q_rows <= 0 or kv_rows <= 0 or K <= 0:
2734+
raise ValueError("q_rows, kv_rows, and K must be positive")
2735+
if storage_dtype not in {"uint8", "float8_e4m3"}:
2736+
raise ValueError(
2737+
"storage_dtype must be 'uint8' or 'float8_e4m3'; "
2738+
f"got {storage_dtype!r}"
2739+
)
2740+
2741+
import tilelang.language as T
2742+
from tilelang.tileop.metal_quant import float_to_fp8_e4m3fn_bits
2743+
2744+
T = cast(Any, T)
2745+
g = globals()
2746+
g.update(
2747+
_SMFP8_PREPARE_Q_ROWS=int(q_rows),
2748+
_SMFP8_PREPARE_KV_ROWS=int(kv_rows),
2749+
_SMFP8_PREPARE_ROWS=int(q_rows) + int(kv_rows),
2750+
_SMFP8_PREPARE_K=int(K),
2751+
_SMFP8_PREPARE_INPUT_DTYPE=str(in_dtype),
2752+
_SMFP8_PREPARE_STORAGE_DTYPE=str(storage_dtype),
2753+
)
2754+
2755+
def encode_fp8(normalized):
2756+
if storage_dtype == "uint8":
2757+
return float_to_fp8_e4m3fn_bits(normalized)
2758+
return T.cast(normalized, "float8_e4m3")
2759+
2760+
@T.prim_func
2761+
def fp8_sparse_mla_prepare(
2762+
post_y: T.Tensor(
2763+
(_SMFP8_PREPARE_ROWS * _SMFP8_PREPARE_K,),
2764+
_SMFP8_PREPARE_INPUT_DTYPE,
2765+
),
2766+
q_fp8: T.Tensor(
2767+
(_SMFP8_PREPARE_Q_ROWS * _SMFP8_PREPARE_K,),
2768+
_SMFP8_PREPARE_STORAGE_DTYPE,
2769+
),
2770+
q_scale: T.Tensor((_SMFP8_PREPARE_Q_ROWS,), "float32"),
2771+
kv_fp8: T.Tensor(
2772+
(_SMFP8_PREPARE_KV_ROWS * _SMFP8_PREPARE_K,),
2773+
_SMFP8_PREPARE_STORAGE_DTYPE,
2774+
),
2775+
kv_scale: T.Tensor((_SMFP8_PREPARE_KV_ROWS,), "float32"),
2776+
):
2777+
with T.Kernel(_SMFP8_PREPARE_ROWS, threads=_SMFP8_PER_TOKEN_QUANT_THREADS) as row:
2778+
x_abs = T.alloc_fragment((_SMFP8_PREPARE_K,), "float32")
2779+
row_amax = T.alloc_fragment((1,), "float32")
2780+
base = row * _SMFP8_PREPARE_K
2781+
for k in T.Parallel(_SMFP8_PREPARE_K):
2782+
x_abs[k] = T.abs(T.cast(post_y[base + k], "float32"))
2783+
T.reduce_max(x_abs, row_amax, dim=0, clear=True)
2784+
row_scale = T.max(
2785+
row_amax[0] * T.cast(1.0 / 448.0, "float32"),
2786+
T.cast(1.0e-12, "float32"),
2787+
)
2788+
if row < _SMFP8_PREPARE_Q_ROWS:
2789+
if T.get_thread_binding(0) == 0:
2790+
q_scale[row] = row_scale
2791+
for k in T.Parallel(_SMFP8_PREPARE_K):
2792+
normalized = T.alloc_var("float32")
2793+
normalized = T.cast(post_y[base + k], "float32") / row_scale
2794+
normalized = T.max(normalized, T.cast(-448.0, "float32"))
2795+
normalized = T.min(normalized, T.cast(448.0, "float32"))
2796+
q_fp8[base + k] = encode_fp8(normalized)
2797+
else:
2798+
kv_row = row - _SMFP8_PREPARE_Q_ROWS
2799+
kv_base = kv_row * _SMFP8_PREPARE_K
2800+
if T.get_thread_binding(0) == 0:
2801+
kv_scale[kv_row] = row_scale
2802+
for k in T.Parallel(_SMFP8_PREPARE_K):
2803+
normalized = T.alloc_var("float32")
2804+
normalized = T.cast(post_y[base + k], "float32") / row_scale
2805+
normalized = T.max(normalized, T.cast(-448.0, "float32"))
2806+
normalized = T.min(normalized, T.cast(448.0, "float32"))
2807+
kv_fp8[kv_base + k] = encode_fp8(normalized)
2808+
2809+
return fp8_sparse_mla_prepare
2810+
2811+
27122812
@lru_cache(maxsize=128)
27132813
def _fp8_per_token_quant_tvm_ffi_kernel_for(
27142814
rows: int,
@@ -3619,6 +3719,7 @@ def fp8_sparse_mla_indexed_qk_reduce_path_c_status(
36193719
"lower_fp8_sparse_mla_indexed_qk_reduce_msl",
36203720
"lower_fp8_sparse_mla_qk_reduce_msl",
36213721
"lower_fp8_sparse_mla_qk_msl",
3722+
"make_fp8_sparse_mla_prepare_kernel",
36223723
"make_fp8_sparse_mla_indexed_qk_reduce_kernel",
36233724
"make_fp8_sparse_mla_qk_reduce_kernel",
36243725
"make_fp8_sparse_mla_qk_kernel",

cppmega_mlx/recipes/model_factory.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,19 @@ def tiny_smoke_config(self, **overrides) -> HybridTinyConfig:
254254
params.update(overrides)
255255
return HybridTinyConfig(**params)
256256

257+
@property
258+
def path_c_bricks(self) -> tuple[dict[str, str], ...]:
259+
"""Return allocation-free brick descriptors for Path C auto-discovery."""
260+
261+
return tuple(
262+
{
263+
"name": f"{self.name}_brick_{index}_{symbol}",
264+
"kind": symbol,
265+
"route_symbol": symbol,
266+
}
267+
for index, symbol in enumerate(self.pattern)
268+
)
269+
257270
def build_model(
258271
self,
259272
*,

0 commit comments

Comments
 (0)