Skip to content

Commit c4f1856

Browse files
physicsrobclaude
andcommitted
graph: width-safe gate for linear-layer fusion
fuse_consecutive_linears could blow up residual width by fusing across a chain boundary: the fused L2 stops heading its downstream Linear->ReLU->Linear chain, ejecting that chain's ReLU from MLP hidden slots (0 residual cols) into the residual stream (len(R) cols). At the DOOM 320x200 d8192 config that doubled peak width and broke the compile. Add skip_relu_ejecting (skip any ejecting fusion) and a budget-aware eject_budget variant (skip a same-width ejection group only when its siblings' combined width busts the budget). Default off = historical behavior. Conservative gate drops the production compile 57->48 layers (critical-path floor 47). Also drop the long-unused 'import torch'. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent c5bb4c3 commit c4f1856

1 file changed

Lines changed: 78 additions & 8 deletions

File tree

torchwright/graph/optimize.py

Lines changed: 78 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,44 @@
44
layer count and parameter overhead.
55
"""
66

7-
from typing import Dict, List, Set, Tuple
7+
from typing import Dict, List, Optional, Set, Tuple
88

9-
import torch
10-
11-
from torchwright.graph import Node, Concatenate
9+
from torchwright.graph import Node, Concatenate, ReLU
1210
from torchwright.graph.linear import Linear
1311

1412

13+
def _ejected_relu(l2: Linear, consumers: Dict[Node, List[Node]]):
14+
"""The ReLU that fusing INTO ``l2`` would eject into the residual stream,
15+
or ``None`` if the fusion ejects nothing.
16+
17+
``l2`` heads a downstream chain when ``l2 -> ReLU -> exactly-one-Linear``
18+
(the L1->ReLU->L2 shape the scheduler's ``_detect_chains_static`` collapses
19+
into one layer with the ReLU living in MLP hidden slots at ZERO residual
20+
columns). When ``l2`` becomes the L2-terminal of an upstream fusion it can
21+
no longer be that chain's L1, so the ReLU is demoted to a STANDALONE relu
22+
that allocates its own ``len(R)`` residual columns — the measured width
23+
blow-up. Returns the ReLU so the caller can size the ejection; ``l2`` that
24+
heads no chain ejects nothing and is always width-safe to fuse into.
25+
"""
26+
for relu in consumers.get(l2, []):
27+
if not isinstance(relu, ReLU):
28+
continue
29+
relu_consumers = consumers.get(relu, [])
30+
if (
31+
len(relu_consumers) == 1
32+
and isinstance(relu_consumers[0], Linear)
33+
and relu_consumers[0].inputs[0] is relu
34+
):
35+
return relu
36+
return None
37+
38+
1539
def fuse_consecutive_linears(
1640
output_nodes: Set[Node],
1741
verbose: bool = False,
42+
*,
43+
skip_relu_ejecting: bool = False,
44+
eject_budget: Optional[int] = None,
1845
) -> int:
1946
"""Fuse consecutive Linear nodes without intervening nonlinearities.
2047
@@ -36,6 +63,16 @@ def fuse_consecutive_linears(
3663
Args:
3764
output_nodes: The graph's output nodes (used to find all ancestors).
3865
verbose: Print fusion details.
66+
skip_relu_ejecting: When True, skip EVERY fusion whose L2 heads a
67+
downstream Linear->ReLU->Linear chain (the width-blow-up case),
68+
regardless of the ejected ReLU's width. Conservative — also drops
69+
harmless narrow-ReLU ejections. Default False.
70+
eject_budget: When set (residual columns available to ejections), use
71+
the budget-aware gate instead: group ejecting fusions by ejected-
72+
ReLU width and skip a group only when its siblings' combined width
73+
(count * width) would bust the budget — keeping the cheap narrow
74+
ejections for their depth win. Takes precedence over
75+
skip_relu_ejecting.
3976
4077
Returns:
4178
Number of fusions performed.
@@ -51,8 +88,11 @@ def fuse_consecutive_linears(
5188
if inp in consumers:
5289
consumers[inp].append(node)
5390

54-
# Find fusion candidates: Linear -> Linear where L1 has single consumer L2
55-
fusions: List[Tuple[Linear, Linear]] = []
91+
# Find fusion candidates: Linear -> Linear where L1 has single consumer L2.
92+
# Record the ReLU each candidate would eject into the residual stream (None
93+
# if width-safe) so the gate below can reason about ejection cost.
94+
gate_on = skip_relu_ejecting or eject_budget is not None
95+
candidates: List[Tuple[Linear, Linear, Optional[Node]]] = []
5696
for node in all_nodes:
5797
if not isinstance(node, Linear):
5898
continue
@@ -82,7 +122,31 @@ def fuse_consecutive_linears(
82122
if new_params > old_params:
83123
continue
84124

85-
fusions.append((l1, l2))
125+
candidates.append(
126+
(l1, l2, _ejected_relu(l2, consumers) if gate_on else None)
127+
)
128+
129+
# Width-aware safety gate: a fusion whose L2 heads a downstream
130+
# Linear->ReLU->Linear chain ejects that chain's ReLU from MLP hidden slots
131+
# into the residual stream (the width blow-up; see _ejected_relu).
132+
skip_ids: Set[int] = set()
133+
if eject_budget is not None:
134+
# Budget-aware: m identical-width ejecting siblings can co-reside for up
135+
# to m*width residual columns. Skip the whole group when that busts the
136+
# budget; keep narrow ejections (cheap) and fuse them for the depth win.
137+
groups: Dict[int, List[int]] = {}
138+
for i, (_l1, _l2, r) in enumerate(candidates):
139+
if r is not None:
140+
groups.setdefault(len(r), []).append(i)
141+
for width, idxs in groups.items():
142+
if width * len(idxs) > eject_budget:
143+
skip_ids.update(idxs)
144+
elif skip_relu_ejecting:
145+
skip_ids = {i for i, (_l1, _l2, r) in enumerate(candidates) if r is not None}
146+
147+
fusions: List[Tuple[Linear, Linear]] = [
148+
(l1, l2) for i, (l1, l2, _r) in enumerate(candidates) if i not in skip_ids
149+
]
86150

87151
# Sort upstream-first so L1→L2 is always processed before L2→L3.
88152
# Without this, if set iteration visits L3 before L2, we'd process
@@ -138,6 +202,8 @@ def fuse_consecutive_linears(
138202
def optimize_graph(
139203
output_nodes: Set[Node],
140204
verbose: bool = False,
205+
*,
206+
skip_relu_ejecting: bool = False,
141207
) -> None:
142208
"""Apply all graph optimization passes.
143209
@@ -147,7 +213,11 @@ def optimize_graph(
147213
Args:
148214
output_nodes: The graph's output nodes.
149215
verbose: Print optimization details.
216+
skip_relu_ejecting: Forwarded to :func:`fuse_consecutive_linears` —
217+
keep only width-safe fusions.
150218
"""
151-
fused = fuse_consecutive_linears(output_nodes, verbose=verbose)
219+
fused = fuse_consecutive_linears(
220+
output_nodes, verbose=verbose, skip_relu_ejecting=skip_relu_ejecting
221+
)
152222
if verbose:
153223
print(f"Graph optimization: fused {fused} Linear pairs")

0 commit comments

Comments
 (0)