44layer 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
1210from 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+
1539def 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(
138202def 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