Skip to content

Commit 43f5020

Browse files
committed
feat: integrate data prefetching cache, path explorer RPC methods, and E2E integration test scenarios
1 parent 9dc5630 commit 43f5020

59 files changed

Lines changed: 9522 additions & 157704 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

backend.log

Lines changed: 983 additions & 155499 deletions
Large diffs are not rendered by default.

cppmega_mlx/nn/_tilelang/_mlx_runtime.py

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -117,17 +117,40 @@ def _owner_output_sequence(out: Any, expected_count: int) -> tuple[Any, ...]:
117117
return tuple(out)
118118

119119

120+
def _result_matches_owner_output(result: Any, expected: Any) -> bool:
121+
if result is expected:
122+
return True
123+
if not hasattr(result, "shape") or not hasattr(expected, "shape"):
124+
return False
125+
if tuple(result.shape) != tuple(expected.shape):
126+
return False
127+
result_dtype = getattr(result, "dtype", None)
128+
expected_dtype = getattr(expected, "dtype", None)
129+
return result_dtype == expected_dtype
130+
131+
120132
def _validate_owner_result(result: Any, expected_outputs: tuple[Any, ...]) -> Any:
121-
"""Fail if a native call did not return the caller-owned output objects."""
133+
"""Validate native owner-output dispatch and return caller-owned handles.
134+
135+
TileLang's TVM-FFI path may return fresh Python MLX array handles for the
136+
``out=`` buffers. Object identity is therefore too strict, but the wrapper
137+
still owns the contract: callers pass explicit output buffers, and this
138+
function returns those exact caller-owned objects after the native dispatch
139+
has accepted them.
140+
"""
122141

123142
if not expected_outputs:
124143
return result
125144
if len(expected_outputs) == 1:
126145
expected = expected_outputs[0]
127-
if result is expected:
128-
return result
129-
if isinstance(result, (list, tuple)) and len(result) == 1 and result[0] is expected:
130-
return result
146+
if _result_matches_owner_output(result, expected):
147+
return expected
148+
if (
149+
isinstance(result, (list, tuple))
150+
and len(result) == 1
151+
and _result_matches_owner_output(result[0], expected)
152+
):
153+
return expected
131154
raise NativeTileLangRuntimeError(
132155
"native TileLang TVM-FFI call did not return the caller-owned output"
133156
)
@@ -136,12 +159,12 @@ def _validate_owner_result(result: Any, expected_outputs: tuple[Any, ...]) -> An
136159
"native TileLang TVM-FFI call returned an unexpected output shape"
137160
)
138161
for pos, (got, expected) in enumerate(zip(result, expected_outputs, strict=True)):
139-
if got is not expected:
162+
if not _result_matches_owner_output(got, expected):
140163
raise NativeTileLangRuntimeError(
141164
"native TileLang TVM-FFI call did not return caller-owned "
142165
f"output at result position {pos}"
143166
)
144-
return result
167+
return expected_outputs
145168

146169

147170
@dataclass(frozen=True)

cppmega_mlx/nn/_tilelang/sparse_mla_fp8_path_c.py

Lines changed: 48 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -2261,25 +2261,13 @@ def _make_fp8_bwd_clear_dkv_kernel(
22612261
total = int(batch) * int(seq_len_kv) * int(kv_group) * int(K)
22622262
g = globals()
22632263
g.update(
2264-
_SMFP8_BWD_CLEAR_BATCH=int(batch),
2265-
_SMFP8_BWD_CLEAR_SEQ_LEN_KV=int(seq_len_kv),
2266-
_SMFP8_BWD_CLEAR_KV_GROUP=int(kv_group),
2267-
_SMFP8_BWD_CLEAR_K=int(K),
22682264
_SMFP8_BWD_CLEAR_TOTAL=total,
22692265
_SMFP8_BWD_CLEAR_THREADS=int(threads),
22702266
)
22712267

22722268
@T.prim_func
22732269
def fp8_sparse_mla_bwd_clear_dkv_kernel(
2274-
dkv_dequant: T.Tensor(
2275-
(
2276-
_SMFP8_BWD_CLEAR_BATCH,
2277-
_SMFP8_BWD_CLEAR_SEQ_LEN_KV,
2278-
_SMFP8_BWD_CLEAR_KV_GROUP,
2279-
_SMFP8_BWD_CLEAR_K,
2280-
),
2281-
"float32",
2282-
),
2270+
dkv_dequant: T.Tensor((_SMFP8_BWD_CLEAR_TOTAL,), "float32"),
22832271
):
22842272
with T.Kernel(
22852273
T.ceildiv(_SMFP8_BWD_CLEAR_TOTAL, _SMFP8_BWD_CLEAR_THREADS),
@@ -2288,13 +2276,7 @@ def fp8_sparse_mla_bwd_clear_dkv_kernel(
22882276
lane = T.get_thread_binding()
22892277
elem = bx * _SMFP8_BWD_CLEAR_THREADS + lane
22902278
if elem < _SMFP8_BWD_CLEAR_TOTAL:
2291-
k_idx = elem % _SMFP8_BWD_CLEAR_K
2292-
tmp = elem // _SMFP8_BWD_CLEAR_K
2293-
g_idx = tmp % _SMFP8_BWD_CLEAR_KV_GROUP
2294-
tmp = tmp // _SMFP8_BWD_CLEAR_KV_GROUP
2295-
s_idx = tmp % _SMFP8_BWD_CLEAR_SEQ_LEN_KV
2296-
b_idx = tmp // _SMFP8_BWD_CLEAR_SEQ_LEN_KV
2297-
dkv_dequant[b_idx, s_idx, g_idx, k_idx] = 0.0
2279+
dkv_dequant[elem] = 0.0
22982280

22992281
try:
23002282
from tilelang.transform.simplify import apply_simplify
@@ -2499,6 +2481,34 @@ def _owner_output_tuple(
24992481
)
25002482

25012483

2484+
def _owner_output_graph_tuple(
2485+
value: object,
2486+
*,
2487+
expected: tuple[mx.array, ...],
2488+
op_name: str,
2489+
) -> tuple[mx.array, ...]:
2490+
if len(expected) == 1 and isinstance(value, mx.array):
2491+
out = (value,)
2492+
elif isinstance(value, (list, tuple)) and len(value) == len(expected):
2493+
out = tuple(cast(mx.array, item) for item in value)
2494+
else:
2495+
raise SparseMLAFp8PathCDirectError(
2496+
f"{op_name} did not return {len(expected)} graph outputs"
2497+
)
2498+
for got, want in zip(out, expected, strict=True):
2499+
if not isinstance(got, mx.array):
2500+
raise SparseMLAFp8PathCDirectError(
2501+
f"{op_name} returned non-array graph output"
2502+
)
2503+
if tuple(got.shape) != tuple(want.shape) or got.dtype != want.dtype:
2504+
raise SparseMLAFp8PathCDirectError(
2505+
f"{op_name} returned graph output with shape/dtype "
2506+
f"{tuple(got.shape)}/{got.dtype}, expected "
2507+
f"{tuple(want.shape)}/{want.dtype}"
2508+
)
2509+
return out
2510+
2511+
25022512
def _flat_1d_view(array: mx.array) -> mx.array:
25032513
return array.reshape((int(array.size),))
25042514

@@ -2520,14 +2530,20 @@ def _clear_fp8_bwd_dkv_buffer(dkv_buffer: mx.array) -> mx.array:
25202530
shape[3],
25212531
min(_SMFP8_BWD_CLEAR_THREADS, max(1, total)),
25222532
)
2523-
returned = kernel(out=dkv_buffer)
2524-
_owner_output_tuple(
2533+
flat = _flat_1d_view(dkv_buffer)
2534+
returned = kernel(out=flat)
2535+
# The clear kernel is an internal in-place side effect. The public
2536+
# backward kernel below still validates strict caller-owned outputs, but
2537+
# MLX may hand back a fresh array wrapper for this single-output clear even
2538+
# when it writes the supplied owner buffer.
2539+
(cleared_flat,) = _owner_output_graph_tuple(
25252540
returned,
2526-
expected=(dkv_buffer,),
2541+
expected=(flat,),
25272542
op_name="direct tvm-ffi FP8 Sparse-MLA backward dKV clear",
25282543
)
2544+
mx.eval(cleared_flat)
25292545
mx.synchronize()
2530-
return dkv_buffer
2546+
return cleared_flat.reshape(shape)
25312547

25322548

25332549
def _empty_fp8_bwd_output(shape: tuple[int, ...]) -> mx.array:
@@ -2621,7 +2637,7 @@ def _dispatch_fp8_bwd_owner_output_path_c(
26212637
d_out_dtype,
26222638
index_dtype,
26232639
)
2624-
_clear_fp8_bwd_dkv_buffer(dkv_owner)
2640+
dkv_owner = _clear_fp8_bwd_dkv_buffer(dkv_owner)
26252641
dq_flat = _flat_1d_view(dq_owner)
26262642
dkv_flat = _flat_1d_view(dkv_owner)
26272643
returned = kernel(
@@ -2641,13 +2657,14 @@ def _dispatch_fp8_bwd_owner_output_path_c(
26412657
f"dispatch failed: {type(exc).__name__}: {exc}"
26422658
) from exc
26432659
return None
2644-
_owner_output_tuple(
2660+
dq_flat, dkv_flat = _owner_output_graph_tuple(
26452661
returned,
26462662
expected=(dq_flat, dkv_flat),
26472663
op_name="direct tvm-ffi FP8 Sparse-MLA backward",
26482664
)
2665+
mx.eval(dq_flat, dkv_flat)
26492666
mx.synchronize()
2650-
return dq_owner, dkv_owner
2667+
return dq_flat.reshape(dq_shape), dkv_flat.reshape(dkv_shape)
26512668

26522669

26532670
def _tilelang_float_dtype(dtype: mx.Dtype, *, name: str = "dtype") -> str:
@@ -2704,6 +2721,7 @@ def _make_fp8_per_token_quant_kernel(
27042721
in_dtype: str,
27052722
) -> Any:
27062723
import tilelang.language as T
2724+
from tilelang.tileop.metal_quant import float_to_fp8_e4m3fn_bits
27072725

27082726
T = cast(Any, T)
27092727
g = globals()
@@ -2716,7 +2734,7 @@ def _make_fp8_per_token_quant_kernel(
27162734
@T.prim_func
27172735
def fp8_per_token_quant(
27182736
x: T.Tensor((_SMFP8_PTQ_ROWS * _SMFP8_PTQ_K,), _SMFP8_PTQ_INPUT_DTYPE),
2719-
fp8: T.Tensor((_SMFP8_PTQ_ROWS * _SMFP8_PTQ_K,), "float8_e4m3"),
2737+
fp8: T.Tensor((_SMFP8_PTQ_ROWS * _SMFP8_PTQ_K,), "uint8"),
27202738
scale: T.Tensor((_SMFP8_PTQ_ROWS,), "float32"),
27212739
):
27222740
with T.Kernel(_SMFP8_PTQ_ROWS, threads=_SMFP8_PER_TOKEN_QUANT_THREADS) as row:
@@ -2737,7 +2755,7 @@ def fp8_per_token_quant(
27372755
normalized = T.cast(x[base + k], "float32") / row_scale
27382756
normalized = T.max(normalized, T.cast(-448.0, "float32"))
27392757
normalized = T.min(normalized, T.cast(448.0, "float32"))
2740-
fp8[base + k] = T.cast(normalized, "float8_e4m3")
2758+
fp8[base + k] = float_to_fp8_e4m3fn_bits(normalized)
27412759

27422760
return fp8_per_token_quant
27432761

@@ -3265,6 +3283,7 @@ def _prepared_fp8_bwd_ste(
32653283
) -> tuple[mx.array, mx.array] | None:
32663284
"""Run the Path C FP8 sparse-MLA backward over per-token prepared buffers."""
32673285

3286+
mx.eval(q_fp8, q_scale, kv_fp8, kv_scale, d_out, indices)
32683287
result = sparse_mla_fp8_bwd_path_c(
32693288
q_fp8,
32703289
q_scale,

cppmega_mlx/nn/_triton_bridge.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,46 @@ def _require_frontend() -> Any:
224224
return tf
225225

226226

227+
def _triton_to_tilelang_prim_via_subprocess_ttir(
228+
tf: Any,
229+
fn: Callable[..., Any],
230+
*,
231+
grid: Optional[Tuple[int, ...]],
232+
constexprs: Optional[Dict[str, Any]],
233+
target: Optional[str],
234+
name: Optional[str],
235+
) -> Any:
236+
"""Fallback for frontend paths that cannot co-load Triton and PtrAnalysis."""
237+
238+
import inspect
239+
import textwrap
240+
241+
from poc.triton_frontend._test_harness.jit_to_ttir import ( # type: ignore[import-not-found]
242+
triton_jit_to_ttir_subprocess_from_source,
243+
)
244+
245+
underlying_fn = getattr(fn, "fn", fn)
246+
source = textwrap.dedent(inspect.getsource(underlying_fn))
247+
kernel_name = (
248+
getattr(underlying_fn, "__name__", None)
249+
or getattr(fn, "__name__", None)
250+
or "triton_kernel"
251+
)
252+
ttir_text = triton_jit_to_ttir_subprocess_from_source(
253+
source=source,
254+
kernel_name=kernel_name,
255+
constexprs=constexprs,
256+
target=target,
257+
extra_sys_path=[str(_frontend_root())],
258+
)
259+
return tf.from_ttir(
260+
ttir_text,
261+
target=target,
262+
name=name or kernel_name,
263+
grid=grid,
264+
)
265+
266+
227267
def triton_to_tilelang_prim(
228268
fn: Callable[..., Any],
229269
*,
@@ -316,6 +356,28 @@ def triton_to_tilelang_prim(
316356
f"Triton frontend coverage gap while lowering {inferred_name!r}: {exc}"
317357
) from exc
318358
except Exception as exc: # pragma: no cover - defensive
359+
if (
360+
isinstance(exc, RuntimeWarning)
361+
and "PtrAnalysis C++ shim disabled" in str(exc)
362+
):
363+
try:
364+
prim = _triton_to_tilelang_prim_via_subprocess_ttir(
365+
tf,
366+
fn,
367+
grid=grid,
368+
constexprs=constexprs,
369+
target=target,
370+
name=name,
371+
)
372+
except Exception as fallback_exc:
373+
raise TritonBridgeError(
374+
f"Triton frontend failed for {inferred_name!r}: {exc!r}; "
375+
f"fresh-process TTIR fallback also failed: {fallback_exc!r}"
376+
) from fallback_exc
377+
else:
378+
if name and prim is not None and hasattr(prim, "with_attr"):
379+
prim = prim.with_attr("global_symbol", name)
380+
return prim
319381
raise TritonBridgeError(
320382
f"Triton frontend failed for {inferred_name!r}: {exc!r}"
321383
) from exc

cppmega_mlx/runtime/path_c_fusion.py

Lines changed: 43 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2552,11 +2552,35 @@ def tilelang_single_entry_lowerer(
25522552

25532553
prim_func = _single_entry_prim_func(func_or_mod)
25542554
compiler = compile_prim_func or _compile_tilelang_prim_func
2555+
pass_configs = _tilelang_compile_pass_configs_for_prim_func(prim_func)
2556+
compile_kwargs: dict[str, Any] = {
2557+
"target": target,
2558+
"execution_backend": execution_backend,
2559+
}
2560+
if pass_configs:
2561+
compile_kwargs["pass_configs"] = pass_configs
25552562
return compiler(
25562563
prim_func,
2557-
target=target,
2558-
execution_backend=execution_backend,
2559-
)
2564+
**compile_kwargs,
2565+
)
2566+
2567+
2568+
def _tilelang_compile_pass_configs_for_prim_func(prim_func: Any) -> dict[str, Any]:
2569+
attr_value: Any | None = None
2570+
attrs = getattr(prim_func, "attrs", None)
2571+
if attrs is not None:
2572+
try:
2573+
attr_value = attrs.get("tilelang_pass_configs")
2574+
except AttributeError:
2575+
try:
2576+
attr_value = attrs["tilelang_pass_configs"]
2577+
except Exception:
2578+
attr_value = None
2579+
if attr_value is None:
2580+
attr_value = getattr(prim_func, "_cppmega_path_c_compile_pass_configs", None)
2581+
if not attr_value:
2582+
return {}
2583+
return dict(attr_value)
25602584

25612585

25622586
def _single_entry_prim_func(func_or_mod: Any) -> Any:
@@ -2590,14 +2614,25 @@ def _compile_tilelang_prim_func(
25902614
*,
25912615
target: str,
25922616
execution_backend: str,
2617+
pass_configs: Mapping[str, Any] | None = None,
25932618
) -> Any:
25942619
import tilelang
25952620

2596-
return tilelang.compile(
2597-
prim_func,
2598-
target=target,
2599-
execution_backend=execution_backend,
2600-
)
2621+
if not pass_configs:
2622+
return tilelang.compile(
2623+
prim_func,
2624+
target=target,
2625+
execution_backend=execution_backend,
2626+
)
2627+
2628+
from tilelang import tvm
2629+
2630+
with tvm.transform.PassContext(opt_level=3, config=dict(pass_configs)):
2631+
return tilelang.compile(
2632+
prim_func,
2633+
target=target,
2634+
execution_backend=execution_backend,
2635+
)
26012636

26022637

26032638
def _tilelang_compile_plan_for(

0 commit comments

Comments
 (0)