Skip to content

Commit 2b32624

Browse files
physicsrobclaude
andcommitted
fix(compile): skip in-place weight trims when streaming (on_layer_compiled)
The ONNX streaming path (compile_to_onnx) installs an on_layer_compiled callback that extracts each layer's weights and NULLs the tensors per layer, bounding peak memory to one dense layer's worth. The post-loop trim_unused_heads / trim_unused_slots then sliced those now-None tensors -> 'NoneType' object is not subscriptable (heads) / 'bool' object has no attribute 'any' (slots), but only for graphs with UNUSED heads/slots (n < n_heads); small graphs where every head is used took the no-slice branch, so the onnxruntime-skipped compile_to_onnx tests never caught it. The DOOM forward (sparse attention: ~1-6 of 40 heads/layer used) trips it. Skip both in-place trims when on_layer_compiled is set: the streamed sparse export already drops the zero heads/slots, so the in-memory trim is both impossible (weights freed) and redundant. Non-streaming compile_headless is unchanged. Adds tests/compile/forward/test_streaming_trim.py (no onnxruntime needed) — fails without the fix (TypeError at attn.py:161), passes with it. Validated: full forward-compile dir green (235 passed); compile_to_onnx of the DOOM token-I/O forward now completes at 2.18GB peak / 55MB ONNX (was an OOM via dense compile_headless). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 729f6c1 commit 2b32624

2 files changed

Lines changed: 90 additions & 12 deletions

File tree

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
"""Regression: a streaming compile must SKIP the post-loop in-place weight trims.
2+
3+
``compile_to_onnx`` passes an ``on_layer_compiled`` callback that extracts each
4+
layer's weights and NULLs the tensors per layer (to bound peak memory to one
5+
dense layer's worth regardless of depth — see export._make_stream_layer_weights_cb).
6+
The post-loop ``trim_unused_heads`` / ``trim_unused_slots`` then slice those
7+
tensors; if they run after the streaming null they hit ``NoneType`` and crash
8+
(``'NoneType' object is not subscriptable`` / ``'bool' object has no attribute
9+
'any'``).
10+
11+
The onnxruntime-based ``compile_to_onnx`` tests are skipped when onnxruntime is
12+
absent, so this no-onnxruntime test pins the trim/null ordering directly: with a
13+
streaming callback present, ``forward_compile`` must complete (trims skipped).
14+
The streamed sparse export already drops the zero heads/slots, so the in-memory
15+
trim is both impossible and redundant in that mode.
16+
"""
17+
18+
import torch
19+
20+
from torchwright.compiler.forward.compile import forward_compile
21+
from torchwright.graph import PosEncoding
22+
from torchwright.ops.inout_nodes import create_input
23+
from torchwright.ops.linear_relu_linear import linear_relu_linear
24+
25+
26+
def _null_layer_weights(layer) -> None:
27+
"""Mimic the ONNX streaming callback: free each layer's dense weights."""
28+
layer.attn.attn.query_matrix = None
29+
layer.attn.attn.key_matrix = None
30+
layer.attn.attn.value_matrix = None
31+
layer.attn.attn.output_matrix = None
32+
layer.mlp.linear1.output_matrix = None
33+
layer.mlp.linear2.output_matrix = None
34+
35+
36+
def test_streaming_callback_skips_inplace_trim():
37+
inp = create_input("x", 16)
38+
pos = PosEncoding(9) # trig_width 8 == d_head below
39+
torch.manual_seed(0)
40+
out = linear_relu_linear(
41+
inp,
42+
torch.randn(24, 16),
43+
torch.randn(24),
44+
torch.randn(24, 16),
45+
torch.randn(16),
46+
name="chain",
47+
)
48+
49+
fired: list[int] = []
50+
51+
def on_layer_compiled(i, layer):
52+
_null_layer_weights(layer)
53+
fired.append(i)
54+
55+
# trim_heads=True + a streaming callback that nulls weights: the post-loop
56+
# trims must be skipped. Before the fix this raised inside trim_unused_heads
57+
# / trim_unused_slots; it must now complete.
58+
net = forward_compile(
59+
d=64,
60+
d_head=8,
61+
output_node=out,
62+
pos_encoding=pos,
63+
on_layer_compiled=on_layer_compiled,
64+
trim_heads=True,
65+
verbose=False,
66+
device="cpu",
67+
)
68+
69+
assert fired, "streaming callback never fired"
70+
assert net is not None

torchwright/compiler/forward/compile.py

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -945,7 +945,12 @@ def forward_compile(
945945
net.residual_assignment = ra
946946
net.assert_aliases = graph.get_assert_aliases()
947947

948-
if trim_heads:
948+
# Skip in-place weight trimming when streaming: ``on_layer_compiled`` (the
949+
# ONNX streaming path) extracts each layer's weights and NULLs the tensors
950+
# per layer, so the post-loop trims here would slice ``None``. The streamed
951+
# sparse export already drops the unused (zero) heads/slots, making the
952+
# in-memory trim both impossible and redundant in that mode.
953+
if trim_heads and on_layer_compiled is None:
949954
max_heads = d // d_head
950955
for layer in net.layers:
951956
layer.attn.attn.trim_unused_heads()
@@ -987,17 +992,20 @@ def forward_compile(
987992
f"{kv_depth:>10} {cp:>10} {sa:>10}"
988993
)
989994

990-
# Trim trailing unused MLP slots (same idea as head trimming).
991-
for layer in net.layers:
992-
layer.mlp.trim_unused_slots()
993-
if verbose:
994-
mlp_before = d_hidden * len(net.layers)
995-
mlp_after = sum(layer.mlp.d_hidden for layer in net.layers)
996-
mlp_saved = (mlp_before - mlp_after) * (2 * d + 2)
997-
print(
998-
f"\n MLP trimming: {mlp_before - mlp_after}/{mlp_before} "
999-
f"slots trimmed ({mlp_saved:,} params saved)"
1000-
)
995+
# Trim trailing unused MLP slots (same idea as head trimming). Skipped when
996+
# streaming for the same reason as head trimming above (weights already
997+
# extracted + nulled per layer by ``on_layer_compiled``).
998+
if on_layer_compiled is None:
999+
for layer in net.layers:
1000+
layer.mlp.trim_unused_slots()
1001+
if verbose:
1002+
mlp_before = d_hidden * len(net.layers)
1003+
mlp_after = sum(layer.mlp.d_hidden for layer in net.layers)
1004+
mlp_saved = (mlp_before - mlp_after) * (2 * d + 2)
1005+
print(
1006+
f"\n MLP trimming: {mlp_before - mlp_after}/{mlp_before} "
1007+
f"slots trimmed ({mlp_saved:,} params saved)"
1008+
)
10011009

10021010
if device == "auto":
10031011
net.to(get_device(verbose=verbose))

0 commit comments

Comments
 (0)