Skip to content

Commit 97e5645

Browse files
committed
feat: implement backward pass support for sparse MLA FP8 path-C fusion
1 parent 976c558 commit 97e5645

25 files changed

Lines changed: 7938 additions & 821 deletions

cppmega_mlx/models/hybrid_lm.py

Lines changed: 62 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,9 @@
3939
from cppmega_mlx.nn.ngram_hash import NgramHashEmbedding
4040
from cppmega_mlx.nn.structure_embedding import CppMegaStructureEmbedding
4141
from cppmega_mlx.recipes.pattern import ExpandedNamPattern, NamLayer, expand_nam_pattern
42-
from cppmega_mlx.runtime.path_c_physical_abi import PathCLogicalBufferOwner
4342
from cppmega_mlx.runtime.kernel_policy import KernelPath, selected_path
43+
from cppmega_mlx.runtime.path_c_physical_abi import PathCLogicalBufferOwner
44+
from cppmega_mlx.runtime.path_c_taps import emit_and_tap_path_c_tensor
4445
from cppmega_mlx.training.mtp import MinimalMTPHead, MTPLossConfig
4546

4647
if TYPE_CHECKING:
@@ -88,8 +89,10 @@ def __init__(
8889
aliases: Mapping[str, str | Sequence[str]] | None = None,
8990
*,
9091
owner_name: str = "HybridTinyLM.path_c_activation_capture",
92+
capture_gradients: bool = True,
9193
) -> None:
9294
self.owner_name = owner_name
95+
self.capture_gradients = capture_gradients
9396
self.aliases = {
9497
str(source): (str(target),)
9598
if isinstance(target, str)
@@ -106,7 +109,13 @@ def __call__(self, event: Mapping[str, Any]) -> None:
106109
logical_names = tuple(str(name) for name in event.get("logical_names", ()))
107110
for name in logical_names:
108111
self.buffers[name] = tensor
109-
for alias in self.aliases.get(name, ()):
112+
aliases = list(self.aliases.get(name, ()))
113+
if name.endswith("_grad"):
114+
aliases.extend(
115+
f"{alias}_grad"
116+
for alias in self.aliases.get(name[: -len("_grad")], ())
117+
)
118+
for alias in aliases:
110119
self.buffers[alias] = tensor
111120
self.events.append(event)
112121

@@ -507,22 +516,23 @@ def _path_c_activation_logical_names(self, name: str) -> tuple[str, ...]:
507516
return (f"{brick_name}_hidden_after",)
508517
return (f"{brick_name}_{name}",)
509518

510-
def _emit_path_c_activation(self, name: str, tensor: mx.array) -> None:
519+
def _emit_path_c_activation(self, name: str, tensor: mx.array) -> mx.array:
511520
probe = self._path_c_activation_probe()
512521
if probe is None:
513-
return
514-
probe(
515-
{
522+
return tensor
523+
return emit_and_tap_path_c_tensor(
524+
tensor,
525+
probe=probe,
526+
event={
516527
"name": name,
517528
"logical_names": self._path_c_activation_logical_names(name),
518-
"tensor": tensor,
519529
"layer_number": self.layer.number,
520530
"layer_index": self.path_c_layer_index,
521531
"route_symbol": self.layer.symbol,
522532
"backend": self.backend,
523533
"brick_name": self.path_c_brick_name,
524534
"profile_brick_name": getattr(self, "path_c_profile_brick_name", None),
525-
}
535+
},
526536
)
527537

528538
@property
@@ -560,17 +570,17 @@ def __call__(
560570
) -> mx.array:
561571
self.validate_backend()
562572
residual = hidden_states
563-
self._emit_path_c_activation("hidden", residual)
573+
residual = self._emit_path_c_activation("hidden", residual)
564574
delta = self.route_delta(
565575
hidden_states,
566576
mask,
567577
kv_cache=kv_cache,
568578
attention_layer_idx=attention_layer_idx,
569579
doc_ids=doc_ids,
570580
)
571-
self._emit_path_c_activation("delta", delta)
581+
delta = self._emit_path_c_activation("delta", delta)
572582
updated = residual + delta
573-
self._emit_path_c_activation("hidden_after", updated)
583+
updated = self._emit_path_c_activation("hidden_after", updated)
574584
if self.mhc is not None:
575585
return self.mhc([updated, residual])
576586
return updated
@@ -628,7 +638,7 @@ def route_delta(
628638
if kv_cache is not None and self.backend != "attention":
629639
raise ValueError("kv_cache may only be passed to attention route blocks")
630640
x = self.norm(hidden_states)
631-
self._emit_path_c_activation("normed", x)
641+
x = self._emit_path_c_activation("normed", x)
632642
if self.backend == "attention":
633643
delta = cast(CausalSelfAttention, self.block)(
634644
x,
@@ -637,16 +647,33 @@ def route_delta(
637647
layer_idx=attention_layer_idx,
638648
)
639649
elif self.backend == "mamba3":
640-
delta, _ = cast(Mamba3ReferenceBlock, self.block)(x)
650+
mamba3 = cast(Mamba3ReferenceBlock, self.block)
651+
probe = self._path_c_activation_probe()
652+
if probe is not None:
653+
h0 = mamba3.initial_h0(x.shape[0], x.dtype)
654+
h0 = self._emit_path_c_activation("mamba3_h0", h0)
655+
h0 = self._emit_path_c_activation("state_in", h0)
656+
delta, state = mamba3(x, h0=h0)
657+
self._emit_path_c_activation("state", state)
658+
else:
659+
delta, _ = mamba3(x)
641660
elif self.backend == "moe":
642661
delta = cast(ReferenceMoE, self.block)(x).output
643662
elif self.backend == "m2rnn":
644663
m2rnn = cast(M2RNNMixer, self.block)
645-
if selected_path("m2rnn") is KernelPath.PATH_C:
646-
delta, _ = m2rnn(
664+
use_explicit_state = (
665+
selected_path("m2rnn") is KernelPath.PATH_C
666+
or self._path_c_activation_probe() is not None
667+
)
668+
if use_explicit_state:
669+
h0 = m2rnn.initial_h0(x.shape[0], x.dtype)
670+
h0 = self._emit_path_c_activation("m2rnn_h0", h0)
671+
delta, state = m2rnn(
647672
x,
648-
h0=m2rnn.initial_h0(x.shape[0], x.dtype),
673+
h0=h0,
674+
return_state=True,
649675
)
676+
self._emit_path_c_activation("m2rnn_conv_state", state.conv_state)
650677
else:
651678
delta, _ = m2rnn(x)
652679
elif self.backend == "engram":
@@ -744,6 +771,20 @@ def attach_path_c_activation_probe(self, probe: PathCActivationProbe) -> int:
744771
raise TypeError("probe must be callable")
745772
for block in self.layers:
746773
block._path_c_activation_probe_callback = probe # type: ignore[attr-defined]
774+
attention = block.attention_block
775+
if attention is not None and callable(
776+
getattr(attention, "attach_path_c_prepared_probe", None)
777+
):
778+
attention.attach_path_c_prepared_probe(
779+
probe,
780+
logical_prefix=str(
781+
getattr(
782+
block,
783+
"path_c_profile_brick_name",
784+
block.path_c_brick_name,
785+
)
786+
),
787+
)
747788
return len(self.layers)
748789

749790
def detach_path_c_activation_probe(self) -> None:
@@ -752,6 +793,11 @@ def detach_path_c_activation_probe(self) -> None:
752793
for block in self.layers:
753794
if hasattr(block, "_path_c_activation_probe_callback"):
754795
delattr(block, "_path_c_activation_probe_callback")
796+
attention = block.attention_block
797+
if attention is not None and callable(
798+
getattr(attention, "detach_path_c_prepared_probe", None)
799+
):
800+
attention.detach_path_c_prepared_probe()
755801

756802
def path_c_parameter_logical_aliases(self) -> dict[str, tuple[str, ...]]:
757803
"""Map MLX parameter tree names to Path C logical input names."""

cppmega_mlx/nn/_tilelang/sparse_mla_fp8_path_c.py

Lines changed: 54 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929

3030
from dataclasses import dataclass
3131
from functools import lru_cache
32+
from collections.abc import Callable, Mapping
3233
from typing import Any, cast
3334

3435
import mlx.core as mx
@@ -99,6 +100,7 @@
99100
_SMFP8_IQKR_VEC = _SMFP8_QKR_DEFAULT_VEC
100101
_SMFP8_IQKR_BLOCK_K = _SMFP8_IQKR_RT * _SMFP8_IQKR_VEC
101102
_SMFP8_IQKR_K_WORDS = _SMFP8_IQKR_K // 4
103+
_SMFP8_IQKR_INDEX_DTYPE = "int32"
102104

103105
_SMFP8_APPLY_B = 1
104106
_SMFP8_APPLY_S = 1
@@ -123,6 +125,8 @@
123125
)
124126
_SMFP8_APPLY_OUT_SIZE = _SMFP8_APPLY_LANES * _SMFP8_APPLY_DV
125127
_SMFP8_APPLY_LSE_SIZE = _SMFP8_APPLY_LANES
128+
_SMFP8_APPLY_OUT_DTYPE = "float32"
129+
_SMFP8_APPLY_INDEX_DTYPE = "int32"
126130

127131
_SMFP8_BWD_B = 1
128132
_SMFP8_BWD_S = 1
@@ -143,9 +147,17 @@
143147
_SMFP8_BWD_IDX_SIZE = _SMFP8_BWD_B * _SMFP8_BWD_S * _SMFP8_BWD_G * _SMFP8_BWD_TOPK
144148
_SMFP8_BWD_DOUT_SIZE = _SMFP8_BWD_LANES * _SMFP8_BWD_DV
145149
_SMFP8_BWD_DOUT_DTYPE = "float32"
150+
_SMFP8_BWD_INDEX_DTYPE = "int32"
151+
_SMFP8_BWD_CLEAR_BATCH = _SMFP8_BWD_B
152+
_SMFP8_BWD_CLEAR_SEQ_LEN_KV = _SMFP8_BWD_SKV
153+
_SMFP8_BWD_CLEAR_KV_GROUP = _SMFP8_BWD_G
154+
_SMFP8_BWD_CLEAR_K = _SMFP8_BWD_K
146155
_SMFP8_BWD_CLEAR_TOTAL = _SMFP8_BWD_B * _SMFP8_BWD_SKV * _SMFP8_BWD_G * _SMFP8_BWD_K
147156
_SMFP8_BWD_CLEAR_THREADS = 256
148157
_SMFP8_PER_TOKEN_QUANT_THREADS = 256
158+
_SMFP8_PTQ_ROWS = 1
159+
_SMFP8_PTQ_K = 64
160+
_SMFP8_PTQ_INPUT_DTYPE = "float32"
149161
_SMFP8_PREPARE_Q_ROWS = 1
150162
_SMFP8_PREPARE_KV_ROWS = 1
151163
_SMFP8_PREPARE_ROWS = _SMFP8_PREPARE_Q_ROWS + _SMFP8_PREPARE_KV_ROWS
@@ -208,6 +220,24 @@ class SparseMLAFp8PathCDirectError(RuntimeError):
208220
"""Raised when a prepared-buffer tvm-ffi owner-output path cannot run."""
209221

210222

223+
def _emit_fp8_apply_runtime_buffer(
224+
probe: Callable[[Mapping[str, Any]], None] | None,
225+
*,
226+
name: str,
227+
tensor: mx.array,
228+
) -> None:
229+
if probe is None:
230+
return
231+
probe(
232+
{
233+
"name": name,
234+
"tensor": tensor,
235+
"producer_owner": "sparse_mla_fp8_path_c_apply",
236+
"producer_stage": "sparse_mla_fp8_apply",
237+
}
238+
)
239+
240+
211241
def _index_dtype_name(indices: mx.array, *, op_name: str) -> str:
212242
if indices.dtype == mx.int32:
213243
return "int32"
@@ -1848,9 +1878,6 @@ def fp8_sparse_mla_apply_kernel(
18481878
inv_sum = T.alloc_local((1,), "float32")
18491879
stride = T.alloc_local((1,), "int32")
18501880
gather_idx = T.alloc_local((1,), "int32")
1851-
kv_row_base_local = T.alloc_local((1,), "int32")
1852-
kv_scale_idx_local = T.alloc_local((1,), "int32")
1853-
dkv_idx_local = T.alloc_local((1,), "int32")
18541881

18551882
h = bx % _SMFP8_APPLY_H
18561883
b = bx // (_SMFP8_APPLY_H * _SMFP8_APPLY_S)
@@ -2984,6 +3011,7 @@ def sparse_mla_fp8_path_c_apply(
29843011
out: mx.array | None = None,
29853012
lse: mx.array | None = None,
29863013
output_dtype: mx.Dtype | None = None,
3014+
runtime_buffer_probe: Callable[[Mapping[str, Any]], None] | None = None,
29873015
) -> mx.array | tuple[mx.array, mx.array] | None:
29883016
"""Run fused FP8 Sparse-MLA Path C over prepared GPU buffers.
29893017
@@ -3071,6 +3099,21 @@ def sparse_mla_fp8_path_c_apply(
30713099
raise ValueError("sinks must be float32")
30723100
sinks_buf = sinks
30733101
has_sinks_buf = mx.array([1], dtype=mx.int32)
3102+
_emit_fp8_apply_runtime_buffer(
3103+
runtime_buffer_probe,
3104+
name="sparse_mla_sm_scale",
3105+
tensor=sm_scale_buf,
3106+
)
3107+
_emit_fp8_apply_runtime_buffer(
3108+
runtime_buffer_probe,
3109+
name="sparse_mla_sinks",
3110+
tensor=sinks_buf,
3111+
)
3112+
_emit_fp8_apply_runtime_buffer(
3113+
runtime_buffer_probe,
3114+
name="sparse_mla_has_sinks",
3115+
tensor=has_sinks_buf,
3116+
)
30743117
returned = kernel(
30753118
_flat_1d_view(q_fp8),
30763119
_flat_1d_view(q_scale),
@@ -3093,8 +3136,13 @@ def sparse_mla_fp8_path_c_apply(
30933136
raise RuntimeError(
30943137
"sparse_mla_fp8_path_c_apply: native TileLang tvm-ffi "
30953138
"graph-output dispatch did not return out/lse"
3096-
)
3139+
)
30973140
return None
3141+
_emit_fp8_apply_runtime_buffer(
3142+
runtime_buffer_probe,
3143+
name="lse",
3144+
tensor=cast(mx.array, returned[1]),
3145+
)
30983146
direct_out = cast(mx.array, returned[0]).reshape(
30993147
(batch, seq_len, heads, d_v_resolved)
31003148
)
@@ -3250,6 +3298,7 @@ def sparse_mla_fp8_path_c_apply_prepared_float(
32503298
force_path_c: bool = False,
32513299
causal: bool = False,
32523300
output_dtype: mx.Dtype | None = None,
3301+
runtime_buffer_probe: Callable[[Mapping[str, Any]], None] | None = None,
32533302
) -> mx.array:
32543303
"""Differentiable owner wrapper for prepared-buffer FP8 Path C apply.
32553304
@@ -3275,6 +3324,7 @@ def _apply(
32753324
sinks=sinks,
32763325
force_path_c=force_path_c,
32773326
output_dtype=_mx_float_dtype(output_dtype, default=q_in.dtype),
3327+
runtime_buffer_probe=runtime_buffer_probe,
32783328
)
32793329
if out is None:
32803330
if force_path_c:

0 commit comments

Comments
 (0)