|
| 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