Skip to content

Commit 2841ccd

Browse files
committed
Add transformer block graph surgery example
Signed-off-by: Fahad Alghanim <163377666+KOKOSde@users.noreply.github.com>
1 parent 5302b28 commit 2841ccd

4 files changed

Lines changed: 235 additions & 0 deletions

File tree

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# Transformer Block Surgery
2+
3+
## Introduction
4+
5+
Transformer-style ONNX graphs often contain shape-only or bookkeeping operators around
6+
attention blocks. This example shows how to use ONNX GraphSurgeon for conservative graph
7+
surgery on a small transformer-like block while keeping the model in standard ONNX
8+
operators.
9+
10+
The example performs two local rewrites:
11+
12+
- Remove `Identity` nodes by rewiring their consumers.
13+
- Cancel adjacent `Transpose` nodes when their permutations compose to the identity permutation.
14+
15+
These cleanups are intentionally small and semantics-preserving. They can make generated
16+
transformer graphs easier to inspect in Netron and prepare for downstream tooling without
17+
introducing custom fused operators.
18+
19+
## Running the example
20+
21+
1. Generate a transformer-like ONNX model:
22+
```bash
23+
python3 generate.py
24+
```
25+
26+
2. Remove no-op graph structure:
27+
```bash
28+
python3 surgeon.py --input model.onnx --output cleaned.onnx
29+
```
30+
31+
The generated model contains a residual projection with an `Identity` and a canceling
32+
`Transpose` pair. The surgery pass exports a checked ONNX model after cleanup and
33+
topological sorting.
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
#!/usr/bin/env python3
2+
#
3+
# SPDX-FileCopyrightText: Copyright (c) 1993-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
4+
# SPDX-License-Identifier: Apache-2.0
5+
#
6+
# Licensed under the Apache License, Version 2.0 (the "License");
7+
# you may not use this file except in compliance with the License.
8+
# You may obtain a copy of the License at
9+
#
10+
# http://www.apache.org/licenses/LICENSE-2.0
11+
#
12+
# Unless required by applicable law or agreed to in writing, software
13+
# distributed under the License is distributed on an "AS IS" BASIS,
14+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
# See the License for the specific language governing permissions and
16+
# limitations under the License.
17+
#
18+
19+
import numpy as np
20+
import onnx
21+
import onnx_graphsurgeon as gs
22+
23+
24+
hidden_size = 8
25+
num_heads = 2
26+
head_dim = hidden_size // num_heads
27+
sequence_length = 4
28+
29+
tokens = gs.Variable("tokens", dtype=np.float32, shape=(1, sequence_length, hidden_size))
30+
identity_out = gs.Variable(
31+
"identity_out", dtype=np.float32, shape=(1, sequence_length, hidden_size)
32+
)
33+
split_shape = gs.Constant(
34+
"split_shape", values=np.array([1, sequence_length, num_heads, head_dim], dtype=np.int64)
35+
)
36+
merged_shape = gs.Constant(
37+
"merged_shape", values=np.array([1, sequence_length, hidden_size], dtype=np.int64)
38+
)
39+
split_heads = gs.Variable(
40+
"split_heads", dtype=np.float32, shape=(1, sequence_length, num_heads, head_dim)
41+
)
42+
heads_first = gs.Variable(
43+
"heads_first", dtype=np.float32, shape=(1, num_heads, sequence_length, head_dim)
44+
)
45+
tokens_again = gs.Variable(
46+
"tokens_again", dtype=np.float32, shape=(1, sequence_length, num_heads, head_dim)
47+
)
48+
merged = gs.Variable("merged", dtype=np.float32, shape=(1, sequence_length, hidden_size))
49+
50+
weights = gs.Constant(
51+
"projection_weight",
52+
values=np.linspace(-0.5, 0.5, hidden_size * hidden_size, dtype=np.float32).reshape(
53+
hidden_size, hidden_size
54+
),
55+
)
56+
bias = gs.Constant(
57+
"projection_bias",
58+
values=np.linspace(-0.1, 0.1, hidden_size, dtype=np.float32),
59+
)
60+
projected = gs.Variable("projected", dtype=np.float32, shape=(1, sequence_length, hidden_size))
61+
output = gs.Variable("output", dtype=np.float32, shape=(1, sequence_length, hidden_size))
62+
63+
nodes = [
64+
gs.Node("Identity", inputs=[tokens], outputs=[identity_out]),
65+
gs.Node("Reshape", inputs=[identity_out, split_shape], outputs=[split_heads]),
66+
gs.Node(
67+
"Transpose",
68+
attrs={"perm": [0, 2, 1, 3]},
69+
inputs=[split_heads],
70+
outputs=[heads_first],
71+
),
72+
gs.Node(
73+
"Transpose",
74+
attrs={"perm": [0, 2, 1, 3]},
75+
inputs=[heads_first],
76+
outputs=[tokens_again],
77+
),
78+
gs.Node("Reshape", inputs=[tokens_again, merged_shape], outputs=[merged]),
79+
gs.Node("MatMul", inputs=[merged, weights], outputs=[projected]),
80+
gs.Node("Add", inputs=[projected, bias], outputs=[output]),
81+
]
82+
83+
graph = gs.Graph(nodes=nodes, inputs=[tokens], outputs=[output], opset=18)
84+
model = gs.export_onnx(graph.cleanup().toposort())
85+
onnx.checker.check_model(model)
86+
onnx.save(model, "model.onnx")
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
#!/usr/bin/env python3
2+
#
3+
# SPDX-FileCopyrightText: Copyright (c) 1993-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
4+
# SPDX-License-Identifier: Apache-2.0
5+
#
6+
# Licensed under the Apache License, Version 2.0 (the "License");
7+
# you may not use this file except in compliance with the License.
8+
# You may obtain a copy of the License at
9+
#
10+
# http://www.apache.org/licenses/LICENSE-2.0
11+
#
12+
# Unless required by applicable law or agreed to in writing, software
13+
# distributed under the License is distributed on an "AS IS" BASIS,
14+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
# See the License for the specific language governing permissions and
16+
# limitations under the License.
17+
#
18+
19+
import argparse
20+
from typing import Sequence
21+
22+
import onnx
23+
import onnx_graphsurgeon as gs
24+
25+
26+
def replace_tensor(graph: gs.Graph, old: gs.Tensor, new: gs.Tensor) -> None:
27+
for node in graph.nodes:
28+
node.inputs = [new if tensor is old else tensor for tensor in node.inputs]
29+
graph.outputs = [new if tensor is old else tensor for tensor in graph.outputs]
30+
31+
32+
def remove_identity_nodes(graph: gs.Graph) -> int:
33+
removed = 0
34+
for node in graph.nodes:
35+
if node.op != "Identity" or len(node.inputs) != 1 or len(node.outputs) != 1:
36+
continue
37+
38+
replace_tensor(graph, node.outputs[0], node.inputs[0])
39+
node.inputs.clear()
40+
node.outputs.clear()
41+
removed += 1
42+
43+
return removed
44+
45+
46+
def compose_permutations(first: Sequence[int], second: Sequence[int]) -> list[int]:
47+
return [first[index] for index in second]
48+
49+
50+
def cancel_transpose_pairs(graph: gs.Graph) -> int:
51+
removed_pairs = 0
52+
53+
for node in graph.nodes:
54+
if node.op != "Transpose" or len(node.inputs) != 1 or len(node.outputs) != 1:
55+
continue
56+
57+
consumers = list(node.outputs[0].outputs)
58+
if len(consumers) != 1:
59+
continue
60+
61+
next_node = consumers[0]
62+
if (
63+
next_node.op != "Transpose"
64+
or len(next_node.inputs) != 1
65+
or len(next_node.outputs) != 1
66+
):
67+
continue
68+
69+
first_perm = node.attrs.get("perm")
70+
second_perm = next_node.attrs.get("perm")
71+
if not isinstance(first_perm, list) or not isinstance(second_perm, list):
72+
continue
73+
if len(first_perm) != len(second_perm):
74+
continue
75+
76+
composed = compose_permutations(first_perm, second_perm)
77+
if composed != list(range(len(composed))):
78+
continue
79+
80+
replace_tensor(graph, next_node.outputs[0], node.inputs[0])
81+
node.inputs.clear()
82+
node.outputs.clear()
83+
next_node.inputs.clear()
84+
next_node.outputs.clear()
85+
removed_pairs += 1
86+
87+
return removed_pairs
88+
89+
90+
def main() -> None:
91+
parser = argparse.ArgumentParser(
92+
description="Apply conservative ONNX GraphSurgeon rewrites to a transformer-like block."
93+
)
94+
parser.add_argument("--input", required=True, help="Path to the input ONNX model")
95+
parser.add_argument("--output", required=True, help="Path for the cleaned ONNX model")
96+
args = parser.parse_args()
97+
98+
graph = gs.import_onnx(onnx.load(args.input))
99+
removed_identities = remove_identity_nodes(graph)
100+
removed_transpose_pairs = cancel_transpose_pairs(graph)
101+
102+
model = gs.export_onnx(graph.cleanup().toposort())
103+
onnx.checker.check_model(model)
104+
onnx.save(model, args.output)
105+
106+
print(f"Removed Identity nodes: {removed_identities}")
107+
print(f"Removed Transpose pairs: {removed_transpose_pairs}")
108+
print(f"Wrote cleaned model: {args.output}")
109+
110+
111+
if __name__ == "__main__":
112+
main()

tools/onnx-graphsurgeon/tests/test_examples.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,10 @@ def __init__(self, name, infer=True):
5656
"12_using_numpy_unsupported_dtypes",
5757
[Artifact("test_conv_bf16.onnx", infer=False)],
5858
),
59+
(
60+
"13_transformer_block_surgery",
61+
[Artifact("model.onnx"), Artifact("cleaned.onnx")],
62+
),
5963
]
6064

6165

0 commit comments

Comments
 (0)