Skip to content

Commit 50401a0

Browse files
committed
feat(v4/fusion): Stage E — auto_compile pattern matcher over region plans
Adds cppmega_v4.fusion.auto_compile, the bridge between the Stage C planner and Path C codegen. Given a FusionRegionPlan from plan_fusion_regions(), it: 1. Classifies the region into one of 9 RegionPattern labels based on category mix + planner-chosen backend. 2. Looks up each brick's PathCBrickScheduleDescriptor in the V4-extended registry (Stage B). 3. Asks cppmega_mlx.runtime.path_c_fusion_schedules's public make_path_c_descriptor_schedule_template() for a callable schedule template, tagged with a stable v4_autocompile_<pattern> entry symbol so codegen logs are self-documenting. The cppmega_mlx plugin is invoked READ-ONLY — nothing inside it is modified. Pass-through patterns (single-brick regions, dlpack-handoff chains) skip template generation entirely; their bricks fall back to their native kernel (Path B Metal / mlx-lm SDPA / upstream). Public surface: - RegionPattern (Enum): SINGLE_BRICK_PASSTHROUGH, DLPACK_HANDOFF_CHAIN, LINEAR_ATTN_SCAN_WITH_NORM, LINEAR_ATTN_SCAN, SSM_CHUNKWISE_SCAN, SDPA_WITH_OUTPUT_PROJ, MOE_ROUTE_COMBINE, NORM_OR_PROJ_CHAIN, GENERIC_DESCRIPTOR_TEMPLATE - AutoCompiledRegion: plan + pattern + descriptors + schedule_template - detect_region_pattern(plan) - auto_compile_region(plan, kinds, *, registry=None) - auto_compile_plan(plans, kinds_by_name, *, registry=None) Tests (tests/v4/test_fusion_stage_e.py, 30 cases): - pattern detection per category mix - auto_compile error handling (kind mismatch, unknown descriptor) - template metadata threading (entry symbol, brick_ops tuple) - pass-through vs compiled-template behaviour - system: every architecture preset (12) auto-compiles cleanly - system: Qwen3-Next / Ling 2.6 produce expected pattern labels Full v4 regression: 564 passed / 5 skipped / 0 failed. Closes the Auto-Fusion roadmap (Stages A-E from Auto-FusionLayerBricks.md).
1 parent 1cb48e1 commit 50401a0

3 files changed

Lines changed: 536 additions & 0 deletions

File tree

cppmega_v4/fusion/__init__.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,13 @@
1717

1818
from __future__ import annotations
1919

20+
from cppmega_v4.fusion.auto_compile import (
21+
AutoCompiledRegion,
22+
RegionPattern,
23+
auto_compile_plan,
24+
auto_compile_region,
25+
detect_region_pattern,
26+
)
2027
from cppmega_v4.fusion.auto_planner import (
2128
DEFAULT_MAX_REGION_SIZE,
2229
DEFAULT_MAX_SHARED_MEM_BYTES,
@@ -48,13 +55,18 @@
4855
__all__ = [
4956
"DEFAULT_MAX_REGION_SIZE",
5057
"DEFAULT_MAX_SHARED_MEM_BYTES",
58+
"AutoCompiledRegion",
5159
"BrickGraph",
5260
"BrickNode",
5361
"FusionEligibility",
5462
"FusionRegionPlan",
63+
"RegionPattern",
64+
"auto_compile_plan",
65+
"auto_compile_region",
5566
"auto_fuse_model",
5667
"build_v4_extended_registry",
5768
"can_fuse_pair",
69+
"detect_region_pattern",
5870
"dlpack_available",
5971
"from_block_specs",
6072
"from_mlx_model",

cppmega_v4/fusion/auto_compile.py

Lines changed: 257 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,257 @@
1+
"""Stage E — pattern-matching auto_compile over FusionRegionPlans.
2+
3+
Given a region plan from :func:`plan_fusion_regions`, this module:
4+
5+
1. Detects which TileLang schedule template best fits the region
6+
(LinearAttn+Norm, SDPA+OProj, MoE route/combine, etc.).
7+
2. Pulls the per-brick descriptors from the V4-extended registry.
8+
3. Asks ``cppmega_mlx.runtime.path_c_fusion_schedules`` for a callable
9+
schedule template that accepts a FusionRegion and returns a TIR
10+
PrimFunc — the *codegen* artefact Path C lowering consumes.
11+
12+
The plugin (``cppmega_mlx``) is invoked **read-only** via its public
13+
:func:`make_path_c_descriptor_schedule_template` API — nothing inside
14+
``cppmega_mlx`` is modified. When a region's pattern is purely a
15+
single-brick passthrough, or when the planner picked a
16+
``dlpack_handoff`` backend, no template is built (compilation falls
17+
through to the brick's own native kernel — Path B Metal, mlx-lm SDPA,
18+
or upstream).
19+
20+
This module is the bridge between the planner (Stage C) and Path C
21+
codegen; it does not itself emit MSL / metallib — that happens later
22+
when the resulting template is passed to :func:`compile_path_c_region`.
23+
24+
Public surface:
25+
- :class:`RegionPattern` — enumerated pattern label
26+
- :class:`AutoCompiledRegion` — planner output + pattern + template
27+
- :func:`detect_region_pattern`
28+
- :func:`auto_compile_region`
29+
- :func:`auto_compile_plan`
30+
"""
31+
32+
from __future__ import annotations
33+
34+
from collections.abc import Callable, Sequence
35+
from dataclasses import dataclass
36+
from enum import Enum
37+
38+
from cppmega_mlx.runtime.path_c_fusion_schedules import (
39+
PathCBrickScheduleDescriptor,
40+
PathCBrickScheduleDescriptorRegistry,
41+
make_path_c_descriptor_schedule_template,
42+
)
43+
from cppmega_v4.fusion.auto_planner import FusionRegionPlan
44+
from cppmega_v4.fusion.descriptor_synthesizer import build_v4_extended_registry
45+
46+
47+
class RegionPattern(str, Enum):
48+
"""Recognised region shapes. Strings so they're JSON-friendly."""
49+
50+
SINGLE_BRICK_PASSTHROUGH = "single_brick_passthrough"
51+
DLPACK_HANDOFF_CHAIN = "dlpack_handoff_chain"
52+
LINEAR_ATTN_SCAN_WITH_NORM = "linear_attn_scan_with_norm"
53+
LINEAR_ATTN_SCAN = "linear_attn_scan"
54+
SSM_CHUNKWISE_SCAN = "ssm_chunkwise_scan"
55+
SDPA_WITH_OUTPUT_PROJ = "sdpa_with_output_proj"
56+
MOE_ROUTE_COMBINE = "moe_route_combine"
57+
NORM_OR_PROJ_CHAIN = "norm_or_proj_chain"
58+
GENERIC_DESCRIPTOR_TEMPLATE = "generic_descriptor_template"
59+
60+
61+
def detect_region_pattern(plan: FusionRegionPlan) -> RegionPattern:
62+
"""Classify a FusionRegionPlan by category mix and backend.
63+
64+
Detection rules (first match wins):
65+
66+
1. size == 1 → SINGLE_BRICK_PASSTHROUGH
67+
2. backend == "dlpack_handoff" → DLPACK_HANDOFF_CHAIN
68+
3. linear_attn ∈ cats AND norm_or_proj ∈ cats → LINEAR_ATTN_SCAN_WITH_NORM
69+
4. all linear_attn → LINEAR_ATTN_SCAN
70+
5. all ssm → SSM_CHUNKWISE_SCAN
71+
6. sdpa_attention ∈ cats AND norm_or_proj ∈ cats → SDPA_WITH_OUTPUT_PROJ
72+
7. moe ∈ cats AND norm_or_proj ∈ cats → MOE_ROUTE_COMBINE
73+
8. all norm_or_proj → NORM_OR_PROJ_CHAIN
74+
9. else → GENERIC_DESCRIPTOR_TEMPLATE
75+
"""
76+
if plan.size == 1:
77+
return RegionPattern.SINGLE_BRICK_PASSTHROUGH
78+
if plan.backend == "dlpack_handoff":
79+
return RegionPattern.DLPACK_HANDOFF_CHAIN
80+
81+
cats = set(plan.categories)
82+
if "linear_attn" in cats and "norm_or_proj" in cats:
83+
return RegionPattern.LINEAR_ATTN_SCAN_WITH_NORM
84+
if cats == {"linear_attn"}:
85+
return RegionPattern.LINEAR_ATTN_SCAN
86+
if cats == {"ssm"}:
87+
return RegionPattern.SSM_CHUNKWISE_SCAN
88+
if "sdpa_attention" in cats and "norm_or_proj" in cats:
89+
return RegionPattern.SDPA_WITH_OUTPUT_PROJ
90+
if "moe" in cats and "norm_or_proj" in cats:
91+
return RegionPattern.MOE_ROUTE_COMBINE
92+
if cats == {"norm_or_proj"}:
93+
return RegionPattern.NORM_OR_PROJ_CHAIN
94+
return RegionPattern.GENERIC_DESCRIPTOR_TEMPLATE
95+
96+
97+
_PATTERNS_WITHOUT_TEMPLATE: frozenset[RegionPattern] = frozenset({
98+
RegionPattern.SINGLE_BRICK_PASSTHROUGH,
99+
RegionPattern.DLPACK_HANDOFF_CHAIN,
100+
})
101+
102+
103+
@dataclass(frozen=True)
104+
class AutoCompiledRegion:
105+
"""A planner region plus the pattern label, the per-brick descriptors,
106+
and the TileLang schedule template that Path C codegen will consume.
107+
108+
For size-1 regions and dlpack-handoff regions the schedule_template
109+
is ``None`` — the region runs through each brick's native kernel
110+
and the planner annotation is enough.
111+
"""
112+
113+
plan: FusionRegionPlan
114+
pattern: RegionPattern
115+
descriptors: tuple[PathCBrickScheduleDescriptor, ...]
116+
schedule_template: Callable[..., object] | None
117+
reason: str
118+
119+
@property
120+
def has_compiled_template(self) -> bool:
121+
return self.schedule_template is not None
122+
123+
124+
_DEFAULT_REGISTRY_CACHE: PathCBrickScheduleDescriptorRegistry | None = None
125+
126+
127+
def _default_registry() -> PathCBrickScheduleDescriptorRegistry:
128+
global _DEFAULT_REGISTRY_CACHE
129+
if _DEFAULT_REGISTRY_CACHE is None:
130+
_DEFAULT_REGISTRY_CACHE = build_v4_extended_registry()
131+
return _DEFAULT_REGISTRY_CACHE
132+
133+
134+
def _collect_descriptors(
135+
plan: FusionRegionPlan,
136+
registry: PathCBrickScheduleDescriptorRegistry,
137+
) -> tuple[PathCBrickScheduleDescriptor, ...]:
138+
"""Return the descriptor for each brick in plan order.
139+
140+
The brick *kind* is recovered from the per-position category-to-kind
141+
inverse via the plan's ``brick_names`` slot — but since the planner
142+
doesn't carry kinds in its output, we rely on each region's
143+
categories list aligning with the op_names already registered. In
144+
practice we use ``op_name == brick_name`` only when the brick name
145+
happens to match a registered op; otherwise we look up by the
146+
canonical kind stored in the BrickGraph the planner came from.
147+
148+
To keep this self-contained, the function expects callers to use
149+
:func:`auto_compile_region` with the same BrickGraph used to build
150+
the plan, or to pass kinds explicitly.
151+
"""
152+
raise NotImplementedError(
153+
"use auto_compile_region(plan, kinds=...) — descriptors are looked "
154+
"up by brick kind, which the FusionRegionPlan does not carry"
155+
)
156+
157+
158+
def _build_template(
159+
descriptors: Sequence[PathCBrickScheduleDescriptor],
160+
pattern: RegionPattern,
161+
) -> Callable[..., object]:
162+
"""Wrap the cppmega_mlx descriptor->template machinery with a stable
163+
entry symbol derived from the pattern. The pattern label becomes
164+
part of the generated PrimFunc's symbol so codegen logs are
165+
self-documenting."""
166+
entry = f"v4_autocompile_{pattern.value}"
167+
return make_path_c_descriptor_schedule_template(
168+
descriptors, entry_symbol=entry
169+
)
170+
171+
172+
def auto_compile_region(
173+
plan: FusionRegionPlan,
174+
kinds: Sequence[str],
175+
*,
176+
registry: PathCBrickScheduleDescriptorRegistry | None = None,
177+
) -> AutoCompiledRegion:
178+
"""Build the :class:`AutoCompiledRegion` for a single planner plan.
179+
180+
``kinds`` must be the brick kind for each entry of ``plan.brick_names``
181+
in the same order. The descriptors are looked up in the supplied
182+
registry (default: the V4-extended registry from
183+
:func:`cppmega_v4.fusion.descriptor_synthesizer.build_v4_extended_registry`).
184+
185+
For pass-through patterns the descriptors are still returned (so
186+
callers can inspect them) but ``schedule_template`` is ``None``.
187+
"""
188+
if len(kinds) != plan.size:
189+
raise ValueError(
190+
f"kinds length ({len(kinds)}) must match plan.size ({plan.size})"
191+
)
192+
reg = registry or _default_registry()
193+
descriptors: list[PathCBrickScheduleDescriptor] = []
194+
missing: list[str] = []
195+
for k in kinds:
196+
d = reg.descriptor_for(k)
197+
if d is None:
198+
missing.append(k)
199+
else:
200+
descriptors.append(d)
201+
if missing:
202+
raise KeyError(
203+
f"no descriptor registered for brick kind(s): {missing}; "
204+
"extend the registry (cppmega_v4.fusion.descriptor_synthesizer) "
205+
"or pass a custom registry"
206+
)
207+
208+
pattern = detect_region_pattern(plan)
209+
if pattern in _PATTERNS_WITHOUT_TEMPLATE:
210+
return AutoCompiledRegion(
211+
plan=plan,
212+
pattern=pattern,
213+
descriptors=tuple(descriptors),
214+
schedule_template=None,
215+
reason=(
216+
"size-1 region — falls through to native kernel"
217+
if pattern is RegionPattern.SINGLE_BRICK_PASSTHROUGH
218+
else "dlpack_handoff backend — no fused PrimFunc emitted"
219+
),
220+
)
221+
222+
template = _build_template(descriptors, pattern)
223+
return AutoCompiledRegion(
224+
plan=plan,
225+
pattern=pattern,
226+
descriptors=tuple(descriptors),
227+
schedule_template=template,
228+
reason=f"pattern={pattern.value}; descriptor-template ready for codegen",
229+
)
230+
231+
232+
def auto_compile_plan(
233+
plans: Sequence[FusionRegionPlan],
234+
kinds_by_name: dict[str, str],
235+
*,
236+
registry: PathCBrickScheduleDescriptorRegistry | None = None,
237+
) -> list[AutoCompiledRegion]:
238+
"""Apply :func:`auto_compile_region` to a list of plans.
239+
240+
``kinds_by_name`` maps every brick name across all plans to its kind
241+
— typically built once from the BrickGraph: ``{n.name: n.kind for n
242+
in graph.nodes}``.
243+
"""
244+
out: list[AutoCompiledRegion] = []
245+
for plan in plans:
246+
kinds = [kinds_by_name[n] for n in plan.brick_names]
247+
out.append(auto_compile_region(plan, kinds, registry=registry))
248+
return out
249+
250+
251+
__all__ = [
252+
"AutoCompiledRegion",
253+
"RegionPattern",
254+
"auto_compile_plan",
255+
"auto_compile_region",
256+
"detect_region_pattern",
257+
]

0 commit comments

Comments
 (0)