Skip to content

Commit 9781f64

Browse files
committed
feat(path-c): span-but-dont-fuse - fused chain region absorbs moe (E) as eager edge (flag, default off)
The Path C fused region was limited to one contiguous run of surface-eligible bricks (M/R/A); every moe (E) brick splits the chain (path_c_fusion.py region builder), so for the model [A,E,M,E,A,E,M,E,A,E,M,R,A] the only region was layers 10-12 -> layers 0-9 run as a slow eager prefix every step. This is target-agnostic (Metal == CUDA: eligibility set has no target check). Finding: ReferenceMoE (nn/moe.py) is DENSE/STATIC - every expert runs over the full batch then masks by routing weight; the only data-dependent op (top-k argpartition) is under stop_gradient, so routing does not enter the gradient graph. So moe can be spanned safely. Add CPPMEGA_PATH_C_SPAN_EAGER_EDGE (default OFF): a non-fusible brick bracketed by eligible bricks is absorbed into the region as an in-chain EAGER EDGE - extends the region span but emits NO fused surface; each eager-edge boundary re-seeds the next fused sub-segment from a runtime {brick}_eager_edge_in buffer (dropped-moe-delta is structurally impossible). (_can_absorb_eager_edge, splitter change, is_eager_edge flag, _append_path_c_eager_edge_boundary.) Metal proof (route AEMEAEMEAEMRA): region 10-12 -> 0-12 (start_layer 10->0, eager prefix 10->0, in-region 3/13 -> 13/13; fused ops still M/R/A only, 5 E as eager edges). Gradient parity flag OFF vs ON: loss bit-identical, all 343 grad leaves max-abs-diff 0. Prefix fwd+grad (seq=128): 119.26ms -> 10.44ms (~11.4x). Target-agnostic (no Metal/CUDA branch). Default OFF -> byte-identical; tests 203+116+20 pass (4 failing pre-existing on pristine). Remaining (next): wire the direct-chain runtime EXECUTOR to run eager-edge moe layers inside the chained step + merge eager-edge grads in the bridge, to convert the grown span into a bound per-step win; then enable the flag on the critical path.
1 parent 1405036 commit 9781f64

1 file changed

Lines changed: 195 additions & 7 deletions

File tree

cppmega_mlx/runtime/path_c_fusion.py

Lines changed: 195 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,21 @@ def _path_c_default_target() -> str:
8686

8787

8888
FUSION_MODE_ENV = "CPPMEGA_PATH_C_FUSION"
89+
# Opt-in lever (target-agnostic, identical on Metal and CUDA): when enabled,
90+
# the auto-discovery region splitter does NOT close the current fused segment
91+
# at a route brick that lacks a surface lowerer (e.g. ``E``/moe). Instead, when
92+
# such a brick is *bracketed* by surface-eligible bricks (an eligible run has
93+
# already started and at least one more eligible brick follows), the brick is
94+
# absorbed into the region as an in-chain EAGER EDGE: it is recorded in the
95+
# region span / metadata but emits NO fused kernel surface. The fused chain
96+
# therefore still contains only M/R/A surfaces (so backward math is unchanged),
97+
# while the region span grows across the eager-edge brick. Growing the span
98+
# moves the region's first brick earlier, which is what shrinks the eager
99+
# prefix that the direct-chain training runtime runs before the fused region.
100+
# Off by default so the fused-chain ABI, paths B/D/E, and every existing
101+
# acceptance/region test stay byte-identical until an eager-edge-capable
102+
# runtime opts in.
103+
SPAN_EAGER_EDGE_ENV = "CPPMEGA_PATH_C_SPAN_EAGER_EDGE"
89104
DEFAULT_MAX_PATH_B_CACHE_GIB = 55.0
90105
DEFAULT_MAX_PATH_B_MEDIAN_STEP_S = 10.0
91106
DEFAULT_MIN_C_OVER_B = 1.0
@@ -190,6 +205,13 @@ class _ResolvedPathCModelBrick:
190205
route_symbol: str
191206
attention_qkv_has_bias: bool = True
192207
attention_out_proj_has_bias: bool = True
208+
# When True this brick is carried as an in-chain EAGER EDGE: it counts
209+
# toward the region span (so the region's first brick / start layer can
210+
# move earlier) but it emits NO fused kernel surface and its delta is
211+
# produced eagerly by the runtime. Only set for bricks whose route symbol
212+
# has no surface lowerer (e.g. ``E``/moe) and only when the span-eager-edge
213+
# lever is enabled.
214+
is_eager_edge: bool = False
193215

194216

195217
@dataclass(frozen=True)
@@ -237,6 +259,23 @@ def selected_path_c_fusion_mode(
237259
return _FUSION_MODE_ALIASES.get(raw.strip().lower(), PathCFusionMode.OFF)
238260

239261

262+
_TRUTHY_FLAG_VALUES = frozenset({"1", "true", "yes", "on", "force"})
263+
264+
265+
def path_c_span_eager_edges_enabled(
266+
env: Mapping[str, str] | None = None,
267+
) -> bool:
268+
"""Return whether the region splitter may span non-fusible eager edges.
269+
270+
Controlled by ``CPPMEGA_PATH_C_SPAN_EAGER_EDGE`` (see ``SPAN_EAGER_EDGE_ENV``).
271+
Target-agnostic: there is no Metal/CUDA branch; the same flag governs both.
272+
Off by default.
273+
"""
274+
275+
raw = (env or os.environ).get(SPAN_EAGER_EDGE_ENV, "")
276+
return raw.strip().lower() in _TRUTHY_FLAG_VALUES
277+
278+
240279
@dataclass(frozen=True)
241280
class Z3SyncSpec:
242281
"""Z3-backed sync/async scheduling attachment for a fusion plan."""
@@ -953,6 +992,31 @@ def build_path_c_model_regions_from_route_symbols(
953992
return tuple(regions)
954993

955994

995+
def _can_absorb_eager_edge(
996+
resolved_bricks: Sequence[_ResolvedPathCModelBrick],
997+
*,
998+
index: int,
999+
segment: Sequence[_ResolvedPathCModelBrick],
1000+
) -> bool:
1001+
"""Return whether a non-fusible brick may be carried as an eager edge.
1002+
1003+
An eager edge is only valid when it is *bracketed* by surface-eligible
1004+
bricks: the current fused segment must already contain at least one
1005+
surface-eligible brick (so there is a producer hidden state to carry the
1006+
residual forward), and at least one later brick must also be
1007+
surface-eligible (so the segment does not terminate on a non-fused edge).
1008+
This keeps every fused chain bounded by real kernel surfaces while letting
1009+
the region span grow across the eager edge.
1010+
"""
1011+
1012+
if not any(not b.is_eager_edge for b in segment):
1013+
return False
1014+
for later in resolved_bricks[index + 1 :]:
1015+
if _path_c_model_brick_surface_lowerer_for(later.route_symbol) is not None:
1016+
return True
1017+
return False
1018+
1019+
9561020
def build_path_c_model_regions_from_bricks(
9571021
bricks: Sequence[Any],
9581022
*,
@@ -973,6 +1037,7 @@ def build_path_c_model_regions_from_bricks(
9731037
_resolved_path_c_model_brick(brick, index=index)
9741038
for index, brick in enumerate(bricks)
9751039
)
1040+
span_eager_edges = path_c_span_eager_edges_enabled()
9761041
regions: list[PathCFusionRegion] = []
9771042
segment: list[_ResolvedPathCModelBrick] = []
9781043
segment_start = 0
@@ -982,6 +1047,15 @@ def build_path_c_model_regions_from_bricks(
9821047
segment_start = index
9831048
segment.append(brick)
9841049
continue
1050+
if span_eager_edges and _can_absorb_eager_edge(
1051+
resolved_bricks, index=index, segment=segment
1052+
):
1053+
# Carry the non-fusible brick (e.g. moe) as an in-chain eager edge:
1054+
# it extends the region span but emits no fused surface, so the
1055+
# fused chain stays surface-eligible-only and gradients are
1056+
# unchanged. The runtime computes its delta eagerly.
1057+
segment.append(replace(brick, is_eager_edge=True))
1058+
continue
9851059
_append_path_c_model_brick_segment(
9861060
regions,
9871061
segment,
@@ -1027,11 +1101,15 @@ def build_path_c_model_region_from_bricks(
10271101
if not resolved_bricks:
10281102
raise ValueError("bricks must contain at least one model brick")
10291103
supported_symbols = _path_c_supported_route_symbols()
1104+
# Eager-edge bricks intentionally carry an unsupported route symbol (no
1105+
# surface lowerer); they extend the region span but are never lowered to a
1106+
# fused surface, so they are exempt from the supported-symbol check.
10301107
unsupported = sorted(
10311108
{
10321109
brick.route_symbol
10331110
for brick in resolved_bricks
10341111
if brick.route_symbol not in supported_symbols
1112+
and not brick.is_eager_edge
10351113
}
10361114
)
10371115
if unsupported:
@@ -1041,9 +1119,13 @@ def build_path_c_model_region_from_bricks(
10411119
f"{unsupported!r}"
10421120
)
10431121
resolved_shape_env = shape_env or _path_c_model_shape_env_from_config(model_config)
1122+
eager_edge_names = tuple(
1123+
brick.name for brick in resolved_bricks if brick.is_eager_edge
1124+
)
10441125
metadata: dict[str, Any] = {
10451126
"path_c_route_symbols": tuple(brick.route_symbol for brick in resolved_bricks),
10461127
"path_c_bricks": tuple(_path_c_brick_metadata(brick) for brick in resolved_bricks),
1128+
"path_c_eager_edge_bricks": eager_edge_names,
10471129
"path_c_acceptance_tags": tuple(str(tag) for tag in acceptance_tags),
10481130
"path_c_acceptance_fixture_abi": False,
10491131
}
@@ -1134,6 +1216,10 @@ def _resolved_path_c_model_brick(
11341216
*,
11351217
index: int,
11361218
) -> _ResolvedPathCModelBrick:
1219+
# An already-resolved brick is returned unchanged so the eager-edge flag
1220+
# (and any other resolved attributes) survive a re-resolution pass.
1221+
if isinstance(brick, _ResolvedPathCModelBrick):
1222+
return brick
11371223
kind = _brick_attr(brick, "kind")
11381224
route_symbol = _path_c_route_symbol_from_brick(brick)
11391225
raw_name = _brick_attr(brick, "name")
@@ -1169,6 +1255,8 @@ def _path_c_brick_metadata(
11691255
"kind": brick.kind,
11701256
"route_symbol": brick.route_symbol,
11711257
}
1258+
if brick.is_eager_edge:
1259+
metadata["eager_edge"] = "true"
11721260
if brick.route_symbol == "A" and not brick.attention_qkv_has_bias:
11731261
metadata["attention_qkv_has_bias"] = "false"
11741262
if brick.route_symbol == "A" and not brick.attention_out_proj_has_bias:
@@ -1392,7 +1480,16 @@ def _append_path_c_model_brick_segment(
13921480
shape_env: PathCModelShapeEnv | None,
13931481
acceptance_tags: Sequence[str],
13941482
) -> None:
1395-
if len(segment) < min_route_bricks:
1483+
# A fused chain must be bounded by real kernel surfaces, so drop any
1484+
# trailing eager-edge bricks (no surface) before emitting the region.
1485+
trimmed = list(segment)
1486+
while trimmed and trimmed[-1].is_eager_edge:
1487+
trimmed.pop()
1488+
segment = trimmed
1489+
# ``min_route_bricks`` counts surface-eligible (fused) bricks only; eager
1490+
# edges extend the span but do not themselves form a fusible region.
1491+
fused_count = sum(1 for brick in segment if not brick.is_eager_edge)
1492+
if fused_count < min_route_bricks:
13961493
return
13971494
end = segment_start + len(segment) - 1
13981495
regions.append(
@@ -1676,6 +1773,31 @@ def _path_c_model_surfaces_from_bricks(
16761773
if not bricks:
16771774
raise ValueError("bricks must contain at least one route")
16781775

1776+
# Eager-edge bricks (e.g. moe) extend the region span but are NOT lowered
1777+
# to fused surfaces -- the direct-chain runtime computes their delta
1778+
# eagerly and stitches the hidden state across the gap. Each fused
1779+
# sub-segment between two eager edges keeps a byte-identical M/R/A ABI; at
1780+
# an eager-edge boundary the surface chain DOES NOT connect the previous
1781+
# fused brick's output directly to the next fused brick (that would silently
1782+
# drop the eager delta). Instead the next fused sub-segment re-seeds its
1783+
# residual/route hidden from a runtime-provided eager-edge output buffer,
1784+
# so the ABI explicitly records the dependency and a compile-and-run cannot
1785+
# bypass the eager moe.
1786+
#
1787+
# ``eager_edge_before[i]`` is True when an eager-edge brick sits in the
1788+
# original span immediately before the i-th *fused* brick.
1789+
eager_edge_before: list[bool] = []
1790+
pending_eager_edge = False
1791+
for brick in bricks:
1792+
if brick.is_eager_edge:
1793+
pending_eager_edge = True
1794+
continue
1795+
eager_edge_before.append(pending_eager_edge)
1796+
pending_eager_edge = False
1797+
bricks = tuple(brick for brick in bricks if not brick.is_eager_edge)
1798+
if not bricks:
1799+
raise ValueError("region must contain at least one fusible route brick")
1800+
16791801
context = _PathCBrickSurfaceLoweringContext(
16801802
residual_hidden=initial_hidden,
16811803
route_hidden=initial_hidden,
@@ -1711,12 +1833,27 @@ def _path_c_model_surfaces_from_bricks(
17111833
)
17121834
result = lowerer.emit(brick, context)
17131835
if index + 1 < len(bricks):
1714-
_append_path_c_inter_brick_residual_norm(
1715-
context,
1716-
producer_brick=brick,
1717-
norm_brick=bricks[index + 1],
1718-
delta=result.delta_output,
1719-
)
1836+
if eager_edge_before[index + 1]:
1837+
# An eager edge sits between this fused brick and the next one.
1838+
# Finalize this brick's residual (producer side) into a
1839+
# caller-visible ``hidden_after`` buffer, then start a fresh
1840+
# fused sub-segment whose residual/route hidden is the
1841+
# runtime-provided eager-edge output. The runtime is contracted
1842+
# to write the eager moe delta into this buffer; the surface
1843+
# chain never connects the two sub-segments directly.
1844+
_append_path_c_eager_edge_boundary(
1845+
context,
1846+
producer_brick=brick,
1847+
norm_brick=bricks[index + 1],
1848+
delta=result.delta_output,
1849+
)
1850+
else:
1851+
_append_path_c_inter_brick_residual_norm(
1852+
context,
1853+
producer_brick=brick,
1854+
norm_brick=bricks[index + 1],
1855+
delta=result.delta_output,
1856+
)
17201857
return tuple(context.surfaces)
17211858

17221859

@@ -1901,6 +2038,57 @@ def _append_path_c_inter_brick_residual_norm(
19012038
context.route_hidden = next_hidden
19022039

19032040

2041+
def _append_path_c_eager_edge_boundary(
2042+
context: _PathCBrickSurfaceLoweringContext,
2043+
*,
2044+
producer_brick: _ResolvedPathCModelBrick,
2045+
norm_brick: _ResolvedPathCModelBrick,
2046+
delta: str,
2047+
) -> None:
2048+
"""Close a fused sub-segment at an eager-edge (e.g. moe) boundary.
2049+
2050+
Emits the producer's residual fold into ``{producer}_hidden_after`` -- the
2051+
hidden state the runtime feeds into the eager moe brick(s) -- then re-seeds
2052+
the surface walk so the next fused sub-segment reads its residual/route
2053+
hidden from a runtime-provided ``{norm_brick}_eager_edge_in`` buffer. The
2054+
surface chain therefore never connects the two sub-segments directly; the
2055+
eager-edge output buffer is an explicit ABI dependency the runtime must
2056+
populate (with the eager moe delta already folded in) before the next
2057+
sub-segment runs. This keeps each fused sub-segment's M/R/A math identical
2058+
while making the dropped-delta failure mode impossible to reach silently.
2059+
"""
2060+
2061+
norm_name = f"{producer_brick.name}_residual_norm"
2062+
hidden_after = f"{producer_brick.name}_hidden_after"
2063+
eager_normed = f"{norm_name}_hidden"
2064+
context.surfaces.append(
2065+
_residual_norm_surface(
2066+
name=norm_name,
2067+
hidden=context.residual_hidden,
2068+
delta=delta,
2069+
norm_weight=f"{norm_name}_weight",
2070+
hidden_after=hidden_after,
2071+
next_hidden=eager_normed,
2072+
)
2073+
)
2074+
# Runtime-provided seed for the next fused sub-segment: the residual
2075+
# baseline carrying the eager moe delta, plus the entry RMSNorm the next
2076+
# fused brick consumes.
2077+
eager_edge_in = f"{norm_brick.name}_eager_edge_in"
2078+
entry_norm_name = f"{norm_brick.name}_entry_rmsnorm"
2079+
entry_normed_hidden = f"{entry_norm_name}_hidden"
2080+
context.surfaces.append(
2081+
_entry_rmsnorm_surface(
2082+
name=entry_norm_name,
2083+
hidden=eager_edge_in,
2084+
norm_weight=f"{entry_norm_name}_weight",
2085+
normed_hidden=entry_normed_hidden,
2086+
)
2087+
)
2088+
context.residual_hidden = eager_edge_in
2089+
context.route_hidden = entry_normed_hidden
2090+
2091+
19042092
def _residual_norm_surface(
19052093
*,
19062094
name: str,

0 commit comments

Comments
 (0)