Skip to content

Commit c8283ef

Browse files
committed
refactor: unify Path-C scratch buffer management with coalesced ABI banks and optimized TIR emission
1 parent c6018d5 commit c8283ef

25 files changed

Lines changed: 2485 additions & 542 deletions

cppmega_mlx/_mlx_lm_imports.py

Lines changed: 71 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,13 @@
1010

1111
from collections.abc import Iterator
1212
from contextlib import contextmanager
13+
from functools import lru_cache
14+
import sys
15+
from typing import Any
1316
import warnings
1417

18+
import mlx.core as mx
19+
1520

1621
_SENTENCEPIECE_SWIG_WARNING_MESSAGES = (
1722
r"builtin type SwigPyPacked has no __module__ attribute",
@@ -39,9 +44,72 @@ def suppress_sentencepiece_swig_warnings() -> Iterator[None]:
3944

4045

4146
_install_sentencepiece_swig_filters()
42-
with suppress_sentencepiece_swig_warnings():
43-
from mlx_lm.models.base import scaled_dot_product_attention
44-
from mlx_lm.models.cache import KVCache, QuantizedKVCache
47+
48+
49+
def _load_mlx_lm_scaled_dot_product_attention() -> object:
50+
if "tvm" in sys.modules and "mlx_lm.models.base" not in sys.modules:
51+
raise RuntimeError(
52+
"MLX-LM quantized attention cannot be imported after TVM/LLVM in "
53+
"the same process; run inference and TileLang compile in separate "
54+
"processes or load MLX-LM before TVM"
55+
)
56+
with suppress_sentencepiece_swig_warnings():
57+
from mlx_lm.models.base import scaled_dot_product_attention
58+
59+
return scaled_dot_product_attention
60+
61+
62+
def scaled_dot_product_attention(
63+
queries: mx.array,
64+
keys: mx.array | tuple[mx.array, ...],
65+
values: mx.array | tuple[mx.array, ...],
66+
cache: Any,
67+
scale: float,
68+
mask: mx.array | None,
69+
sinks: mx.array | None = None,
70+
) -> mx.array:
71+
"""MLX-LM-compatible SDPA without importing its Torch/Triton stack by default."""
72+
73+
needs_mlx_lm_quantized_path = hasattr(cache, "bits") or (
74+
isinstance(keys, tuple)
75+
and cache is not None
76+
and cache.__class__.__name__ == "TurboQuantKVCache"
77+
)
78+
if needs_mlx_lm_quantized_path:
79+
sdpa = _load_mlx_lm_scaled_dot_product_attention()
80+
return sdpa(
81+
queries,
82+
keys,
83+
values,
84+
cache=cache,
85+
scale=scale,
86+
mask=mask,
87+
sinks=sinks,
88+
)
89+
return mx.fast.scaled_dot_product_attention(
90+
queries,
91+
keys,
92+
values,
93+
scale=scale,
94+
mask=mask,
95+
sinks=sinks,
96+
)
97+
98+
99+
@lru_cache(maxsize=1)
100+
def _load_kv_cache_classes() -> tuple[type[object], type[object]]:
101+
with suppress_sentencepiece_swig_warnings():
102+
from mlx_lm.models.cache import KVCache, QuantizedKVCache
103+
104+
return KVCache, QuantizedKVCache
105+
106+
107+
def __getattr__(name: str) -> object:
108+
if name == "KVCache":
109+
return _load_kv_cache_classes()[0]
110+
if name == "QuantizedKVCache":
111+
return _load_kv_cache_classes()[1]
112+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
45113

46114

47115
__all__ = [

cppmega_mlx/inference/engine.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,16 @@
33
from __future__ import annotations
44

55
from dataclasses import dataclass, replace
6-
from typing import TypeAlias, cast
6+
from typing import TYPE_CHECKING, Any, TypeAlias, cast
77

88
import mlx.core as mx
99

10-
from cppmega_mlx._mlx_lm_imports import KVCache, QuantizedKVCache
1110
from cppmega_mlx.inference.quantization import make_quantized_kv_cache
1211

13-
_LayerCache: TypeAlias = KVCache | QuantizedKVCache
12+
if TYPE_CHECKING:
13+
from mlx_lm.models.cache import KVCache
14+
15+
_LayerCache: TypeAlias = Any
1416
_StateTree: TypeAlias = mx.array | tuple["_StateTree", ...]
1517

1618

@@ -53,6 +55,8 @@ class ContiguousKVCache:
5355
"""A thin validated wrapper around one MLX-LM KV cache per layer."""
5456

5557
def __init__(self, config: ContiguousKVCacheConfig) -> None:
58+
from cppmega_mlx._mlx_lm_imports import KVCache
59+
5660
self.config = config
5761
self.layers: list[_LayerCache] = [
5862
_make_layer_cache(config) for _ in range(config.num_layers)
@@ -311,6 +315,8 @@ def prefill_contiguous_kv_cache(
311315
destination.config.batch_size,
312316
)
313317
dst_layer.state = copied_state
318+
from cppmega_mlx._mlx_lm_imports import QuantizedKVCache
319+
314320
if isinstance(dst_layer, QuantizedKVCache):
315321
dst_layer.meta_state = src_layer.meta_state
316322

@@ -340,6 +346,8 @@ def _make_layer_cache(config: ContiguousKVCacheConfig) -> _LayerCache:
340346
return make_quantized_kv_cache(
341347
bits=config.kv_bits, group_size=config.kv_group_size
342348
)
349+
from cppmega_mlx._mlx_lm_imports import KVCache
350+
343351
return KVCache()
344352

345353

cppmega_mlx/inference/quantization.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,13 @@
44

55
from collections.abc import Callable, Iterable
66
from dataclasses import dataclass
7+
from typing import TYPE_CHECKING
78

89
import mlx.nn as nn
910

10-
from cppmega_mlx._mlx_lm_imports import KVCache, QuantizedKVCache
11+
if TYPE_CHECKING:
12+
from mlx_lm.models.cache import KVCache, QuantizedKVCache
13+
1114
_SUPPORTED_BITS = frozenset({4, 8})
1215
_SUPPORTED_GROUP_SIZES = frozenset({32, 64, 128})
1316
_SUPPORTED_MODES = frozenset({"affine"})
@@ -127,6 +130,8 @@ def make_quantized_kv_cache(
127130
"""Create an mlx-lm ``QuantizedKVCache`` with q4 defaults."""
128131

129132
_validate_quant_args(bits=bits, group_size=group_size, mode="affine")
133+
from cppmega_mlx._mlx_lm_imports import QuantizedKVCache
134+
130135
return QuantizedKVCache(group_size=group_size, bits=bits)
131136

132137

@@ -139,6 +144,8 @@ def quantize_kv_cache(
139144
"""Convert an existing mlx-lm ``KVCache`` to ``QuantizedKVCache``."""
140145

141146
_validate_quant_args(bits=bits, group_size=group_size, mode="affine")
147+
from cppmega_mlx._mlx_lm_imports import KVCache, QuantizedKVCache
148+
142149
if isinstance(cache, QuantizedKVCache):
143150
if cache.bits != bits or cache.group_size != group_size:
144151
raise ValueError(

cppmega_mlx/nn/attention.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010
import mlx.core as mx
1111
import mlx.nn as nn
1212

13-
from cppmega_mlx._mlx_lm_imports import scaled_dot_product_attention
1413
from cppmega_mlx.inference.engine import ContiguousKVCache
1514
from cppmega_mlx.runtime.kernel_policy import KernelPath, record_dispatch, selected_path
1615
from cppmega_mlx.runtime.path_c_taps import emit_and_tap_path_c_tensor
@@ -1056,6 +1055,8 @@ def __call__(
10561055
key_length=key_length,
10571056
)
10581057
sinks = _validate_attention_sinks(sinks, self.config.num_q_heads)
1058+
from cppmega_mlx._mlx_lm_imports import scaled_dot_product_attention
1059+
10591060
out = scaled_dot_product_attention(
10601061
q,
10611062
k,

cppmega_mlx/runtime/path_c_fusion.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2916,7 +2916,7 @@ def _workspace_edge_buffers_for_attested_template(
29162916
return ()
29172917
if not _region_has_attention_kv_workspace_edges(region):
29182918
return ()
2919-
return ("kv_fp8", "kv_scale")
2919+
return ("q_fp8", "q_scale", "kv_fp8", "kv_scale")
29202920

29212921

29222922
def _region_has_attention_kv_workspace_edges(region: PathCFusionRegion) -> bool:
@@ -2925,7 +2925,8 @@ def _region_has_attention_kv_workspace_edges(region: PathCFusionRegion) -> bool:
29252925
node_by_name[edge.producer].op_name == "attention_qkv_projection"
29262926
and node_by_name[edge.consumer].op_name
29272927
in {"sparse_mla_fp8_apply", "sparse_mla_fp8_apply_bwd"}
2928-
and _canonical_path_c_edge_buffer_name(edge.input) in {"kv_fp8", "kv_scale"}
2928+
and _canonical_path_c_edge_buffer_name(edge.input)
2929+
in {"q_fp8", "q_scale", "kv_fp8", "kv_scale"}
29292930
for edge in region.edges
29302931
)
29312932

@@ -3540,6 +3541,10 @@ def _infer_edges(nodes: Sequence[FusionNode]) -> tuple[FusionEdge, ...]:
35403541

35413542
def _canonical_path_c_edge_buffer_name(buffer_name: str) -> str:
35423543
name = str(buffer_name)
3544+
if name.endswith("_q_fp8"):
3545+
return "q_fp8"
3546+
if name.endswith("_q_scale"):
3547+
return "q_scale"
35433548
if name.endswith("_kv_fp8"):
35443549
return "kv_fp8"
35453550
if name.endswith("_kv_scale"):
@@ -3557,7 +3562,8 @@ def _inferred_edge_lifetime(
35573562
producer_node.op_name == "attention_qkv_projection"
35583563
and consumer_node.op_name
35593564
in {"sparse_mla_fp8_apply", "sparse_mla_fp8_apply_bwd"}
3560-
and _canonical_path_c_edge_buffer_name(buffer_name) in {"kv_fp8", "kv_scale"}
3565+
and _canonical_path_c_edge_buffer_name(buffer_name)
3566+
in {"q_fp8", "q_scale", "kv_fp8", "kv_scale"}
35613567
):
35623568
return "workspace"
35633569
return "internal"

cppmega_mlx/runtime/path_c_fusion_launcher.py

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,9 @@
5353
"tl.fusion.physical_abi.physical_buffer_shapes",
5454
)
5555
_ROW_DISPATCH_GRID_CHUNKS = "grid_chunks"
56+
_FORWARD_ONLY_GATE = 0
57+
_BACKWARD_GATE = 1
58+
_BACKWARD_FORWARD_PREPASS_GATE = 2
5659

5760

5861
def _decode_attr(prim_func: Any, key: str) -> Any:
@@ -98,6 +101,7 @@ class PathCAbiManifest:
98101
bank_shapes: Mapping[str, tuple[int, ...]]
99102
bank_dtypes: Mapping[str, str]
100103
internal_scratch_buffers: tuple[str, ...]
104+
internal_scratch_aliases: Mapping[str, Mapping[str, Any]]
101105
cotangent_seed_buffers: tuple[str, ...]
102106
top_level_scratch_shapes: Mapping[str, tuple[int, ...]]
103107
top_level_scratch_dtypes: Mapping[str, str]
@@ -152,6 +156,10 @@ def load_path_c_abi_manifest(prim_func: Any) -> PathCAbiManifest:
152156
scratch_raw = (
153157
json.loads(str(scratch_attr)) if scratch_attr is not None else []
154158
)
159+
scratch_alias_attr = prim_func.attrs.get("tl.fusion.internal_scratch_abi_aliases")
160+
scratch_aliases = (
161+
json.loads(str(scratch_alias_attr)) if scratch_alias_attr is not None else {}
162+
)
155163

156164
cotangent_raw = prim_func.attrs.get("tl.fusion.train_step_loss_cotangent_abi")
157165
cotangent_buffers: tuple[str, ...]
@@ -206,6 +214,7 @@ def load_path_c_abi_manifest(prim_func: Any) -> PathCAbiManifest:
206214
bank_shapes=bank_shapes,
207215
bank_dtypes=bank_dtypes,
208216
internal_scratch_buffers=tuple(scratch_raw),
217+
internal_scratch_aliases=scratch_aliases,
209218
cotangent_seed_buffers=cotangent_buffers,
210219
top_level_scratch_shapes=top_level_shapes,
211220
top_level_scratch_dtypes=top_level_dtypes,
@@ -274,8 +283,8 @@ def _flat_into_bank(
274283
f"expected {placement.dtype}, got {value.dtype}"
275284
)
276285
flat = value.reshape((placement.size,))
277-
indices = mx.arange(placement.offset, placement.offset + placement.size, dtype=mx.int32)
278-
return bank.at[indices].add(flat - bank[indices])
286+
slot = slice(placement.offset, placement.offset + placement.size)
287+
return bank.at[slot].add(flat - bank[slot])
279288

280289

281290
def _bank_slot_view(
@@ -489,6 +498,8 @@ def __call__(
489498
# (uses top_level_scratch_shapes).
490499
scratch_buffers: dict[str, mx.array] = {}
491500
for name in manifest.internal_scratch_buffers:
501+
if name in manifest.internal_scratch_aliases:
502+
continue
492503
if name in manifest.logical_to_physical:
493504
placement = manifest.logical_to_physical[name]
494505
scratch_buffers[name] = mx.zeros(
@@ -545,7 +556,7 @@ def __call__(
545556
positional.append(0)
546557
elif logical_name == manifest.backward_gate_param:
547558
backward_gate_param_index = len(positional)
548-
positional.append(1 if run_backward else 0)
559+
positional.append(_BACKWARD_GATE if run_backward else _FORWARD_ONLY_GATE)
549560
else:
550561
raise ValueError(
551562
f"compiled PrimFunc has an unexpected parameter {param_name!r} "
@@ -676,19 +687,16 @@ def launch_chunk(chunk_index: int, subchunk_index: int) -> None:
676687
if row_subchunk_param_index is not None:
677688
positional[row_subchunk_param_index] = int(subchunk_index)
678689
apply_returned_outputs(self._kernel(*positional))
679-
eval_positionals()
680690

681691
if run_backward and backward_gate_param_index is not None:
682-
positional[backward_gate_param_index] = 0
683-
forward_subchunk_count = int(manifest.row_subchunk_count or 1)
692+
positional[backward_gate_param_index] = _BACKWARD_FORWARD_PREPASS_GATE
684693
for chunk_index in range(chunk_count):
685-
for subchunk_index in range(forward_subchunk_count):
686-
launch_chunk(chunk_index, subchunk_index)
694+
launch_chunk(chunk_index, 0)
687695
final_forward_states = snapshot_logical_buffers(
688696
_STATE_AFTER_LOGICAL_BUFFERS
689697
)
690698
restore_logical_buffers(initial_backward_states)
691-
positional[backward_gate_param_index] = 1
699+
positional[backward_gate_param_index] = _BACKWARD_GATE
692700
launch_chunk(0, 0)
693701
restore_logical_buffers(final_forward_states)
694702
else:

0 commit comments

Comments
 (0)