Files
wehub-resource-sync c8a779b1bb
Docker Image CI / build-ubuntu2004 (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:36:55 +08:00

2252 lines
77 KiB
Python

#
# SPDX-FileCopyrightText: Copyright (c) 1993-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
import copy
from typing import List
import numpy as np
import onnx
import onnx_graphsurgeon as gs
import pytest
from onnx_graphsurgeon.ir.function import Function
from onnx_graphsurgeon.ir.graph import Graph
from onnx_graphsurgeon.ir.node import Node
from onnx_graphsurgeon.ir.tensor import Constant, LazyValues, Tensor, Variable
from onnx_graphsurgeon.logger import G_LOGGER
from onnx_graphsurgeon.util import misc
from onnx_graphsurgeon.util.exception import OnnxGraphSurgeonException
from onnx_graphsurgeon.util.misc import SynchronizedList
from onnx_models import const_foldable, shape_cast_elision
G_LOGGER.severity = G_LOGGER.ULTRA_VERBOSE
@Graph.register()
def shape(self, inp):
return self.layer(op="Shape", inputs=[inp], outputs=["shape_out"])[0]
@Graph.register()
def cast(self, inp, to):
return self.layer(op="Cast", inputs=[inp], outputs=["cast_out"], attrs={"to": to})[
0
]
@Graph.register()
def constant(self, values):
return self.layer(
op="Constant",
inputs=[],
outputs=["constant_out"],
attrs={"value": Constant("values", values)},
)[0]
@Graph.register()
def identity(self, inp):
out = self.layer(op="Identity", inputs=[inp], outputs=["identity_out"])[0]
out.dtype = inp.dtype
return out
@Graph.register()
def relu(self, inp):
out = self.layer(op="Relu", inputs=[inp], outputs=["relu_out"])[0]
out.dtype = inp.dtype
return out
@Graph.register()
def add(self, a, b, name=None):
outputs = [Variable(name=name)] if name else ["add_out"]
out = self.layer(op="Add", inputs=[a, b], outputs=outputs)[0]
out.dtype = a.dtype or b.dtype
return out
@Graph.register()
def mul(self, a, b, name=None):
outputs = [Variable(name=name)] if name else ["mul_out"]
out = self.layer(op="Mul", inputs=[a, b], outputs=outputs)[0]
out.dtype = a.dtype or b.dtype
return out
@Graph.register()
def less(self, a, b, name=None):
outputs = [Variable(name=name)] if name else ["less_out"]
out = self.layer(op="Less", inputs=[a, b], outputs=outputs)[0]
out.dtype = bool
return out
# A fake op that can be used to ensure things work even when there is an invalid
# node present in the model.
@Graph.register()
def fake(self, inp, name=None):
outputs = [Variable(name=name)] if name else ["fake_out"]
out = self.layer(op="Fake", inputs=[inp], outputs=outputs)[0]
out.dtype = inp.dtype
return out
@gs.Graph.register()
def gather(self, data, indices):
return self.layer(op="Gather", inputs=[data, indices], outputs=["gather_out"])[0]
@gs.Graph.register()
def slice(self, data, starts=None, ends=None, axes=None, steps=None):
inputs = []
for inp in [data, starts, ends, axes, steps]:
if inp is None:
break
inputs.append(inp)
return self.layer(op="Slice", inputs=inputs, outputs=["slice_out"])[0]
@gs.Graph.register()
def nested(self, inp, graph):
return self.layer(
op="Nested", inputs=[inp], outputs=["nested_out"], attrs={"body": graph}
)[0]
@gs.Graph.register()
def if_op(self, cond, then_graph, else_graph):
return self.layer(
op="If",
inputs=[cond],
outputs=["if_out"],
attrs={"then_branch": then_graph, "else_branch": else_graph},
)[0]
@gs.Graph.register()
def tile(self, inp, repeats):
out = self.layer(op="Tile", inputs=[inp, repeats], outputs=["tile_out"])[0]
out.dtype = inp.dtype
return out
@gs.Graph.register()
def dequantize_linear(self, inp, scale, zero_point, axis=1):
out = self.layer(
op="DequantizeLinear",
inputs=[inp, scale, zero_point],
outputs=["dequantize_linear_out"],
attrs={"axis": axis},
)[0]
out.dtype = np.float32
return out
@gs.Graph.register()
def quantize_linear(self, inp, out_scale, out_zero_point, axis=1):
out = self.layer(
op="QuantizeLinear",
inputs=[inp, out_scale, out_zero_point],
outputs=["quantize_linear_out"],
attrs={"axis": axis},
)[0]
out.dtype = np.int8
return out
@gs.Graph.register()
def pad(self, data, pads, constant_value=None):
constant_value = misc.default_value(constant_value, Variable.empty())
out = self.layer(
op="Pad", inputs=[data, pads, constant_value], outputs=["pad_out"]
)[0]
out.dtype = data.dtype
return out
@gs.Graph.register()
def softmax(self, data, axis=None):
attrs = {}
if axis is not None:
attrs["axis"] = axis
out = self.layer(op="Softmax", inputs=[data], outputs=["softmax_out"], attrs=attrs)[
0
]
out.dtype = data.dtype
return out
# Generates a graph where an outer node has no outputs except
# within the subgraph. ONNX-GS should recognize that the node
# is being used, and should not remove it during cleanup().
def make_nested_graph():
inp = Variable("input")
id_out = Variable("id_out")
identity = Node(op="Identity", inputs=[inp], outputs=[id_out])
# Subgraph outputs come from the parent node, but nodes in the subgraph
# can use nodes from the outer graphs too.
subgraph_inputs = [Variable("subgraph_inp")]
subgraph_id_out = Variable("subgraph_id_out")
subgraph_outputs = [Variable("subgraph_out")]
subgraph_identity0 = Node(op="Identity", inputs=[id_out], outputs=[subgraph_id_out])
subgraph_identity1 = Node(
op="Identity", inputs=[subgraph_id_out], outputs=subgraph_outputs
)
subgraph = Graph(
nodes=[subgraph_identity0, subgraph_identity1],
inputs=subgraph_inputs,
outputs=subgraph_outputs,
)
nested_out = Variable("nested_out")
nested_node = Node(
op="Nested", attrs={"body": subgraph}, inputs=[inp], outputs=[nested_out]
)
return Graph(nodes=[identity, nested_node], inputs=[inp], outputs=[nested_out])
@pytest.fixture
def nested_graph():
yield make_nested_graph()
@pytest.fixture
def very_nested_graph():
inner_subgraph_1 = Graph(name="inner_subgraph_1")
inner_subgraph_2 = Graph(name="inner_subgraph_2")
inner_subgraph_3 = Graph(name="inner_subgraph_3")
outer_subgraph_1 = Graph(name="subgraph1")
outer_subgraph_2 = Graph(
name="subgraph2",
nodes=[Node("Add", attrs={"x": inner_subgraph_1, "y": inner_subgraph_2})],
)
outer_subgraph_3 = Graph(
name="subgraph3", nodes=[Node("Add", attrs={"x": inner_subgraph_3, "y": 3.14})]
)
node_1 = Node(
op="Add",
attrs={
"x": outer_subgraph_1,
"y": outer_subgraph_2,
"z": 5,
"w": outer_subgraph_3,
},
)
node_2 = Node(op="Add", attrs={"x": outer_subgraph_3})
return Graph(nodes=[node_1, node_2], name="main_graph")
class TestBasic(object):
def test_generate_name(self):
graph = Graph()
generated_names = set()
existing_names = {
"name_{}".format(i) for i in range(50, 150)
} # names_50 to names_149
num_names = 100
for idx in range(num_names):
generated_names.add(graph._generate_name("name", existing_names))
assert len(generated_names) == num_names # 100 unique generated_names
assert (
len(generated_names.intersection(existing_names)) == 0
) # no generated_names in existing_names
expected_names = {"name_{}".format(i) for i in range(0, 50)}
expected_names.update({"name_{}".format(i) for i in range(150, 200)})
assert (
generated_names == expected_names
) # expect 'names_0' to 'names_49', 'names_150' to 'names_199'
def test_equal(self, nested_graph):
assert nested_graph == nested_graph
def test_equal_inputs_unequal(self):
g0 = make_nested_graph()
g1 = make_nested_graph()
g0.inputs.append(Variable("test"))
assert not (g0 == g1)
def test_equal_outputs_unequal(self):
g0 = make_nested_graph()
g1 = make_nested_graph()
g0.outputs.append(Variable("test"))
assert not (g0 == g1)
def test_equal_nested_unequal(self):
g0 = make_nested_graph()
g1 = make_nested_graph()
# Changing the nested subgraph should make the graphs unequal
g0.nodes[1].inputs[0].name = "subgraph_inp_modified"
assert not (g0 == g1)
def test_subgraphs_not_recursive(self, very_nested_graph):
unrelated_graph = Graph(name="unrelated")
subgraph_names = {subgraph.name for subgraph in very_nested_graph.subgraphs()}
assert subgraph_names == {"subgraph1", "subgraph2", "subgraph3"}
def test_subgraphs_recursive(self, very_nested_graph):
unrelated_graph = Graph(name="unrelated")
subgraph_names = {
subgraph.name for subgraph in very_nested_graph.subgraphs(recursive=True)
}
assert subgraph_names == {
"subgraph1",
"subgraph2",
"subgraph3",
"inner_subgraph_1",
"inner_subgraph_2",
"inner_subgraph_3",
}
class TestRegister(object):
def test_register(self):
@Graph.register()
def fake_add(self, a, b):
return self.layer(op="Add", inputs=[a, b], outputs=["add_out"])
graph = Graph()
[output] = graph.fake_add("a", "b")
assert "add_out" in output.name
assert len(graph.nodes) == 1
assert graph.nodes[-1].op == "Add"
def test_register_opset(self):
@Graph.register(opsets=[11])
def fake_add(self, a, b):
return self.layer(op="Add", inputs=[a, b], outputs=["add_out"])
@Graph.register(opsets=[10])
def fake_add(self, a, b):
return self.layer(op="Add-10", inputs=[a, b], outputs=["add_out"])
graph = Graph()
[output] = graph.fake_add("a", "b")
assert "add_out" in output.name
assert len(graph.nodes) == 1
assert graph.nodes[-1].op == "Add"
graph_opset10 = Graph(opset=10)
[output] = graph_opset10.fake_add("a", "b")
assert "add_out" in output.name
assert len(graph_opset10.nodes) == 1
assert graph_opset10.nodes[-1].op == "Add-10"
def test_register_name_conflict(self):
@Graph.register()
def fake_mul(self, a, b):
return self.layer(
op="Add", domain="domain1", inputs=[a, b], outputs=["mul_out"]
)
func = Function("fake_mul", domain="domain2")
graph = Graph(functions=[func])
graph.fake_mul("a", "b")
assert len(graph.nodes) == 1
assert graph.nodes[0].domain == "domain1"
class TestLayer(object):
def test_layer_default_naming(self):
node1 = Node(
name="onnx_graphsurgeon_node_0", op="Identity"
) # injecting default name
node2 = Node(
name="onnx_graphsurgeon_node_1", op="Identity"
) # injecting default name again
graph = Graph(nodes=[node1, node2])
graph.layer(
op="Identity"
) # new default name should be onnx_graphsurgeon_node_2
assert graph.nodes[-1].name == "onnx_graphsurgeon_node_2"
graph.layer(
op="Identity"
) # new default name should be onnx_graphsurgeon_node_3
assert graph.nodes[-1].name == "onnx_graphsurgeon_node_3"
def test_layer_with_attrs(self):
graph = Graph()
outputs = graph.layer(op="Add", name="node", attrs={"fake_attr": 0})
assert len(graph.nodes) == 1
assert graph.nodes[-1].op == "Add"
assert graph.nodes[-1].name == "node"
assert graph.nodes[-1].attrs["fake_attr"] == 0
def test_layer_with_tensors(self):
x0 = Variable("x0")
x1 = Variable("x1")
y0 = Variable("y0")
y1 = Variable("y1")
graph = Graph()
outputs = graph.layer(op="Fake", inputs=[x0, x1], outputs=[y0, y1])
assert outputs == [y0, y1]
assert len(graph.nodes) == 1
assert graph.nodes[-1].inputs == [x0, x1]
assert graph.nodes[-1].outputs == outputs
def test_layer_with_strings(self):
x0 = "x0"
x1 = "x1"
y0 = "y0"
y1 = "y1"
graph = Graph()
outputs = graph.layer(op="Fake", inputs=[x0, x1], outputs=[y0, y1])
assert len(graph.nodes) == 1
assert [
prefix in tensor.name
for prefix, tensor in zip([x0, x1], graph.nodes[-1].inputs)
]
assert [
prefix in tensor.name
for prefix, tensor in zip([y0, y1], graph.nodes[-1].outputs)
]
assert graph.nodes[-1].outputs == outputs
def test_layer_with_arrays(self):
x0 = np.array([1])
x1 = np.array([1])
y0 = "y0"
y1 = "y1"
graph = Graph()
outputs = graph.layer(op="Fake", inputs=[x0, x1], outputs=[y0, y1])
assert [
prefix in tensor.name
for prefix, tensor in zip([y0, y1], graph.nodes[-1].outputs)
]
assert len(graph.nodes) == 1
assert graph.nodes[-1].inputs[0].values == x0
assert graph.nodes[-1].inputs[1].values == x1
assert graph.nodes[-1].outputs == outputs
def test_layer_with_iterables(self):
x0 = [1]
x1 = (1,)
y0 = "y0"
y1 = "y1"
graph = Graph()
outputs = graph.layer(op="Fake", inputs=[x0, x1], outputs=[y0, y1])
assert [
prefix in tensor.name
for prefix, tensor in zip([y0, y1], graph.nodes[-1].outputs)
]
assert len(graph.nodes) == 1
assert graph.nodes[-1].inputs[0].values == x0
assert graph.nodes[-1].inputs[1].values == x1
assert graph.nodes[-1].outputs == outputs
class TestFunctionCall(object):
def make_graph_and_func(self):
func_output = Variable("test_output", shape=[1, 2, 3], dtype=np.int32)
func = Function(
"TestFunction", inputs=[Variable("test_input")], outputs=[func_output]
)
graph = Graph(functions=[func])
return graph, func_output
def check_outputs_match(self, func_output, node_outputs):
assert len(node_outputs) == 1
output = node_outputs[0]
assert output is not func_output
assert output.name
assert output.shape == func_output.shape
assert output.dtype == func_output.dtype
def test_function_default_outputs(self):
graph, func_output = self.make_graph_and_func()
# No outputs given, but they should be created.
outputs = graph.TestFunction(inputs=["input"])
self.check_outputs_match(func_output, outputs)
def test_function_default_outputs_string_names(self):
graph, func_output = self.make_graph_and_func()
# Output name given, it should be preserved.
outputs = graph.TestFunction(inputs=["input"], outputs=["output"])
self.check_outputs_match(func_output, outputs)
assert outputs[0].name.startswith("output")
def test_function_default_outputs_existing_tensor(self):
graph, _ = self.make_graph_and_func()
# Output tensor provided, it should not be changed.
existing_tensor = Variable("output", shape=[2, 3, 4], dtype=np.float32)
outputs = graph.TestFunction(inputs=["input"], outputs=[existing_tensor])
assert outputs[0] is existing_tensor
def tensors_linear_graph():
inputs = [Variable(name="x")]
intermediate0 = Variable(name="intermediate0")
intermediate1 = Variable(name="intermediate1")
intermediate2 = Variable(name="intermediate2")
outputs = [Variable(name="y")]
tensors = inputs + [intermediate0, intermediate1, intermediate2] + outputs
tensors = {tensor.name: tensor for tensor in tensors}
# Nodes are NOT in topo order.
nodes = [
Node(op="Add", name="Test0", inputs=inputs, outputs=[intermediate0]),
Node(op="Add", name="Test1", inputs=[intermediate0], outputs=[intermediate1]),
Node(op="Add", name="Test2", inputs=[intermediate1], outputs=[intermediate2]),
Node(op="Add", name="Test3", inputs=[intermediate2], outputs=outputs),
]
return Graph(nodes=nodes, inputs=inputs, outputs=outputs), nodes, tensors
class TestTensors(object):
# Calling `tensors()` should not modify tensors in the graph.
def test_tensors_does_not_modify_tensors(self):
graph, _, _ = tensors_linear_graph()
graph_tensors = graph.tensors()
# Generate a new graph to compare against
_, _, tensors = tensors_linear_graph()
assert set(tensors.keys()) == set(graph_tensors.keys())
for name, tensor in tensors.items():
graph_tensor = graph_tensors[name]
assert tensor == graph_tensor
assert tensor.inputs == graph_tensor.inputs
assert tensor.outputs == graph_tensor.outputs
# Check that tensors includes tensors not attached to nodes
def test_tensors_includes_non_node_tensors(self):
X = Constant("X", values=np.ones(shape=(64, 64), dtype=np.float32))
graph = Graph(inputs=[], outputs=[X])
tensor_map = graph.tensors()
assert "X" in tensor_map
assert tensor_map["X"] == X
def test_tensors_check_duplicates(self):
inputs = [Variable(name="x")]
outputs = [Variable(name="x")] # Distinct tensors with the same name
nodes = [
Node(op="Add", name="Test", inputs=inputs, outputs=outputs),
]
graph = Graph(nodes=nodes, inputs=inputs, outputs=outputs)
with pytest.raises(OnnxGraphSurgeonException):
graph.tensors(check_duplicates=True)
def test_tensors_with_duplicates_check_disabled(self):
inputs = [Variable(name="x")]
outputs = [Variable(name="x")] # Distinct tensors with the same name
nodes = [
Node(op="Add", name="Test", inputs=inputs, outputs=outputs),
]
graph = Graph(nodes=nodes, inputs=inputs, outputs=outputs)
# This should *not* throw
graph.tensors(check_duplicates=False)
def toposort_linear_graph():
inputs = [Variable(name="x")]
intermediate0 = Variable(name="intermediate0")
intermediate1 = Variable(name="intermediate1")
intermediate2 = Variable(name="intermediate2")
outputs = [Variable(name="y")]
# Nodes are NOT in topo order.
nodes = [
Node(op="Add", name="Test0", inputs=inputs, outputs=[intermediate0]),
Node(op="Add", name="Test2", inputs=[intermediate1], outputs=[intermediate2]),
Node(op="Add", name="Test3", inputs=[intermediate2], outputs=outputs),
Node(op="Add", name="Test1", inputs=[intermediate0], outputs=[intermediate1]),
]
expected_node_order = [nodes[0], nodes[3], nodes[1], nodes[2]]
return Graph(nodes=nodes, inputs=inputs, outputs=outputs), expected_node_order
# Graph structure:
# x
# |
# Test0
# |
# intermediate0
# | \
# Test1 Test2
# | |
# intermediate1 intermediate2
# \ /
# Test3
# |
# y
def toposort_diamond_graph():
inputs = [Variable(name="x")]
intermediate0 = Variable(name="intermediate0")
intermediate1 = Variable(name="intermediate1")
intermediate2 = Variable(name="intermediate2")
outputs = [Variable(name="y")]
# Nodes are NOT in topo order:
nodes = [
Node(op="Add", name="Test0", inputs=inputs, outputs=[intermediate0]),
# If the end of the diamond shape occurs before the branches, it can trigger a path
# where a false cycle is detected - this test guards against that.
Node(
op="Add",
name="Test3",
inputs=[intermediate1, intermediate2],
outputs=outputs,
),
Node(op="Add", name="Test1", inputs=[intermediate0], outputs=[intermediate1]),
Node(op="Add", name="Test2", inputs=[intermediate0], outputs=[intermediate2]),
]
expected_node_order = [nodes[0], nodes[2], nodes[3], nodes[1]]
return Graph(nodes=nodes, inputs=inputs, outputs=outputs), expected_node_order
# Graph structure:
# x
# |
# Test0 -> out0 (graph output)
# |
# out0
# |
# Test1 -> out1 (graph output)
# |
# out1
# |
# Test2 -> out2 (graph_output)
def toposort_multi_tier_output_graph():
inputs = [Variable(name="x")]
outputs = [Variable(name="out0"), Variable(name="out1"), Variable(name="out2")]
out0, out1, out2 = outputs
nodes = [
Node(op="Add", name="Test2", inputs=[out1], outputs=[out2]),
Node(op="Add", name="Test0", inputs=inputs, outputs=[out0]),
Node(op="Add", name="Test1", inputs=[out0], outputs=[out1]),
]
expected_node_order = [nodes[1], nodes[2], nodes[0]]
return Graph(nodes=nodes, inputs=inputs, outputs=outputs), expected_node_order
# Graph structure:
# x2 x1
# | |
# Test0
# |
# int0 x0
# | /
# Test1
# |
# int1 x3
# | /
# Test2 -> out (graph_output)
def toposort_multi_tier_input_graph():
inputs = [
Variable(name="x0"),
Variable(name="x1"),
Variable(name="x2"),
Variable(name="x3"),
]
int0, int1 = [Variable(name="intermediate0"), Variable(name="intermediate1")]
outputs = [Variable(name="out")]
x0, x1, x2, x3 = inputs
nodes = [
Node(op="Add", name="Test2", inputs=[int1, x3], outputs=outputs),
Node(op="Add", name="Test0", inputs=[x2, x1], outputs=[int0]),
Node(op="Add", name="Test1", inputs=[int0, x0], outputs=[int1]),
]
expected_node_order = [nodes[1], nodes[2], nodes[0]]
return Graph(nodes=nodes, inputs=inputs, outputs=outputs), expected_node_order
# Graph structure:
# x0
# |
# Add
# |
# x1
# |
# Relu
# |
# x2
#
# If:
# Then:
# x1 x2
# | |
# Add
# |
# res
# Else:
# x1 x2
# | |
# Add
# |
# res
# |
# out
#
# In this graph, the subgraph of If implicitly depends on x1/x2 from the outer graph, so the parent If
# node must come after the outer Add/ReLU nodes.
# If we fail to consider such implicit inputs, the If will remain the first node.
def toposort_implicit_subgraph_inputs_graph():
def make_var(name):
return Variable(name, shape=(1, 1), dtype=np.float32)
# Main graph
inputs = [make_var("x0")]
const = Constant(name="const", values=np.array([[1.5]], dtype=np.float32))
cond = Constant(name="cond", values=np.array([True]))
x1, x2 = [make_var("x1"), make_var("x2")]
outputs = [make_var("out")]
# Subgraphs for If
subgraph_outputs = [make_var("res")]
subgraph_nodes = [
Node(op="Add", name="SubgraphTest0", inputs=[x1, x2], outputs=subgraph_outputs)
]
subgraph = Graph(nodes=subgraph_nodes, outputs=subgraph_outputs)
nodes = [
Node(
op="If",
name="Test2",
inputs=[cond],
outputs=outputs,
attrs={"then_branch": subgraph, "else_branch": subgraph},
),
Node(op="Relu", name="Test1", inputs=[x1], outputs=[x2]),
Node(op="Add", name="Test0", inputs=inputs + [const], outputs=[x1]),
]
expected_node_order = [nodes[2], nodes[1], nodes[0]]
return Graph(nodes=nodes, inputs=inputs, outputs=outputs), expected_node_order
TOPOSORT_TEST_CASES = [
toposort_linear_graph,
toposort_diamond_graph,
toposort_multi_tier_output_graph,
toposort_multi_tier_input_graph,
toposort_implicit_subgraph_inputs_graph,
]
class TestToposort(object):
@staticmethod
def make_single_node_function(name: str, dependencies: List[str]) -> Function:
"""
Create a function which uses all the functions given in 'dependencies'.
"""
func = Function(name, inputs=[Variable("input")], outputs=[Variable("output")])
if not dependencies:
return func
intermediate = func.inputs[0]
for i, dep in enumerate(dependencies):
new_intermediate = Variable(f"inter_{i}")
func.nodes.append(
Node(
dep,
domain=Function.DEFAULT_DOMAIN,
inputs=[intermediate],
outputs=[new_intermediate],
)
)
intermediate = new_intermediate
func.outputs = [intermediate]
return func
@pytest.mark.parametrize("toposort_test_case", TOPOSORT_TEST_CASES)
def test_topologically_sort(self, toposort_test_case):
graph, expected_node_order = toposort_test_case()
assert graph.nodes != expected_node_order
graph.toposort()
assert graph.nodes == expected_node_order
@pytest.mark.parametrize("toposort_test_case", TOPOSORT_TEST_CASES)
def test_toposort_nested(self, toposort_test_case):
subgraph, expected_node_order = toposort_test_case()
assert subgraph.nodes != expected_node_order
# Wrap the graph within a subgraph
inp = Variable("input")
id_out = Variable("id_out")
identity = Node(op="Identity", inputs=[inp], outputs=[id_out])
# Make the subgraph take an input from the outer graph node
# If toposort tries to take the node id, it'll fault.
subgraph.nodes[0].inputs.append(id_out)
out = Variable("output")
nested = Node(
op="Nested", inputs=[id_out], outputs=[out], attrs={"subgraph": subgraph}
)
graph = Graph(nodes=[identity, nested], inputs=[inp], outputs=[out])
graph.toposort(recurse_subgraphs=True)
assert subgraph.nodes == expected_node_order
def test_function(self):
graph, expected_node_order = toposort_multi_tier_input_graph()
func = Function(
"Test", nodes=graph.nodes, inputs=graph.inputs, outputs=graph.outputs
)
func.toposort()
assert func.nodes == expected_node_order
def test_function_order(self):
# Check that toposort re-orders functions in topological order.
func1 = self.make_single_node_function("func1", [])
func2 = self.make_single_node_function("func2", ["func1"])
func3 = self.make_single_node_function("func3", ["func2"])
func4 = self.make_single_node_function("func4", ["func3", "func2"])
funcs = [func3, func2, func4, func1]
graph = Graph(functions=funcs)
graph.toposort()
assert graph.functions == [func1, func2, func3, func4]
def test_function_circular_dep_simple(self):
func = self.make_single_node_function("func", ["func"])
graph = Graph(functions=[func])
try:
graph.toposort()
assert False, "Should have raised"
except OnnxGraphSurgeonException:
pass
def test_function_circular_dep_complicated(self):
# Circular dependency [func2 -> func3 -> func4 -> func2]
func1 = self.make_single_node_function("func1", ["func2", "func5"])
func2 = self.make_single_node_function("func2", ["func3"])
func3 = self.make_single_node_function("func3", ["func4", "func5"])
func4 = self.make_single_node_function("func4", ["func2", "func5"])
func5 = self.make_single_node_function("func5", [])
funcs = [func3, func2, func4, func1, func5]
graph = Graph(functions=funcs)
try:
graph.toposort()
assert False, "Should have raised"
except OnnxGraphSurgeonException:
pass
def build_basic_graph():
inputs = [Variable(name="x")]
outputs = [Variable(name="y")]
nodes = [
Node(op="Add", name="Test", inputs=inputs, outputs=outputs),
]
return Graph(nodes=nodes, inputs=inputs, outputs=outputs, ir_version=10)
def build_two_layer_graph():
inputs = [Variable(name="x")]
intermediate_tensor = Variable(name="intermediate")
outputs = [Variable(name="y")]
nodes = [
Node(op="Add", name="Test0", inputs=inputs, outputs=[intermediate_tensor]),
Node(op="Add", name="Test1", inputs=[intermediate_tensor], outputs=outputs),
]
return Graph(nodes=nodes, inputs=inputs, outputs=outputs, ir_version=10)
def build_two_layer_graph_multiple_io():
inputs = [Variable(name="x0"), Variable(name="x1")]
intermediate_tensor = Variable(name="intermediate")
outputs = [Variable(name="y0"), Variable(name="y1")]
nodes = [
Node(op="Add", name="Test0", inputs=inputs, outputs=[intermediate_tensor]),
Node(op="Add", name="Test1", inputs=[intermediate_tensor], outputs=outputs),
]
return Graph(nodes=nodes, inputs=inputs, outputs=outputs, ir_version=10)
def build_function_with_unused_node():
func = Function("Test")
A = Variable("A", dtype=np.float32, shape=(1, 1))
B = Variable("B", dtype=np.float32, shape=(1, 1))
X = Variable("X", dtype=np.float32, shape=(1, 1))
Y = Variable("Y", dtype=np.float32, shape=(1, 1))
func.inputs = [A, X]
func.outputs = [B]
func.nodes = [
Node(op="Sin", inputs=[X], outputs=[Y]), # this node is unused
Node(op="Cos", inputs=[A], outputs=[B]),
]
return func
CLEANUP_TEST_CASES = [
build_basic_graph(),
build_two_layer_graph(),
build_two_layer_graph_multiple_io(),
]
class TestCleanup(object):
@pytest.mark.parametrize("graph", CLEANUP_TEST_CASES)
def test_get_used_node_ids(self, graph):
graph_used_nodes = copy.copy(graph.nodes)
graph_used_tensors = copy.copy(list(graph.tensors().values()))
unused_tensor = Variable(name="Unused")
unused_node = Node(
op="Unused", inputs=[graph.inputs[0]], outputs=[unused_tensor]
)
graph.nodes.append(unused_node)
with graph.node_ids():
used_node_ids, used_tensors = graph._get_used_node_ids()
assert len(used_node_ids) == len(graph.nodes) - 1
assert all([node.id in used_node_ids for node in graph_used_nodes])
assert unused_node.id not in used_node_ids
assert unused_tensor not in used_tensors
assert all(
[used_tensor in used_tensors for used_tensor in graph_used_tensors]
)
def test_multi_tier(self):
graph, _ = toposort_multi_tier_output_graph()
tensor = graph.outputs.pop()
unused_node = tensor.inputs[0]
graph.cleanup() # Should remove just the Test2 node as out1 is still an output.
assert unused_node not in graph.nodes
assert len(graph.nodes) == 2
assert len(graph.outputs) == 2
tensor_map = graph.tensors()
assert tensor.name not in tensor_map
def test_remove_unused_node_outputs(self):
graph, _ = toposort_linear_graph()
graph.toposort()
graph_output = graph.outputs[0]
dummy = Variable("dummy")
# Add unused tensor to a node in the middle of the graph.
# Since it does not contribute to graph outputs, it should be removed.
graph.nodes[1].outputs.append(dummy)
graph.cleanup(remove_unused_node_outputs=True)
assert dummy not in graph.nodes[1].outputs
assert graph.outputs[0] == graph_output # Graoh outputs will never be removed
def test_graph_input_producers(self):
graph, _ = toposort_linear_graph()
tensor_map = graph.tensors()
assert "x" in tensor_map
graph.inputs = [tensor_map["intermediate0"]]
graph.cleanup()
cleaned_tensor_map = graph.tensors()
assert "x" not in cleaned_tensor_map
@pytest.mark.parametrize("remove_unused_graph_inputs", [True, False])
def test_independent_path(self, remove_unused_graph_inputs):
graph, _ = toposort_linear_graph()
# Build out a path totally unrelated to rest of the graph
indep0 = Variable(name="indep0")
indep1 = Variable(name="indep1")
node = Node(op="IndepTest", inputs=[indep0], outputs=[indep1])
graph.nodes.append(node)
graph.inputs.append(indep0)
graph.cleanup(remove_unused_graph_inputs=remove_unused_graph_inputs)
assert indep0 not in graph.inputs or not remove_unused_graph_inputs
assert node not in graph.nodes or not remove_unused_graph_inputs
tensor_map = graph.tensors()
assert indep0.name not in tensor_map or not remove_unused_graph_inputs
assert indep1.name not in tensor_map or not remove_unused_graph_inputs
def test_nested_graph(self, nested_graph):
nested_node = nested_graph.nodes[1]
nested_inp = nested_node.inputs[0]
nested_out = nested_node.outputs[0]
subgraph = nested_node.attrs["body"]
assert "id_out" in nested_graph.tensors()
nested_graph.cleanup(recurse_subgraphs=True)
# Clean up should not remove a tensor whose only output node is a subgraph.
assert "id_out" in nested_graph.tensors()
# Clean up should not modify the nested nodes inputs or outputs
assert nested_node.inputs == [nested_inp]
assert nested_node.outputs == [nested_out]
# Next we'll clean up the subgraph by recursing from the top-level
assert subgraph.nodes
subgraph.outputs.clear()
nested_graph.cleanup(recurse_subgraphs=True)
assert not subgraph.nodes
def test_node_used_only_in_nested_graph(self):
X = Variable("X", dtype=np.float32, shape=(1,))
Y = Variable("Y", dtype=np.float32, shape=(1,))
graph = Graph(inputs=[X, Y])
X_p = graph.identity(
X
) # X_p is only used by the subgraph, not in the outer graph.
subgraph_inp = Variable("subgraph_input", dtype=np.float32, shape=(1,))
subgraph = Graph(inputs=[subgraph_inp])
subgraph.outputs = [subgraph.add(subgraph_inp, X_p)]
graph.outputs = [graph.nested(Y, subgraph)]
graph.cleanup(remove_unused_graph_inputs=True)
assert graph.nodes[0].op == "Identity"
assert graph.nodes[0].inputs == [X]
def test_input_is_output(self):
graph = Graph()
A = Variable("A", dtype=np.float32, shape=(1, 1))
B = Variable("B", dtype=np.float32, shape=(1, 1))
C = graph.add(A, B)
graph.inputs = [A, B]
graph.outputs = [C, B, A] # Out of order w/ respect to Add node inputs
# Graph should remain unchanged after cleanup, including I/O tensors.
graph.cleanup()
assert graph.inputs == [A, B]
assert graph.outputs == [C, B, A]
assert len(graph.nodes) == 1
assert graph.nodes[0].inputs == [A, B]
assert graph.nodes[0].outputs == [C]
def test_function(self):
func = build_function_with_unused_node()
func.cleanup()
assert {i.name for i in func.inputs} == {"A", "X"}
assert {o.name for o in func.outputs} == {"B"}
assert len(func.nodes) == 1
def test_graph_cleans_up_function(self):
graph = Graph()
func = build_function_with_unused_node()
graph.functions.append(func)
# Cleaning up the graph should by default also cleanup the function.
graph.cleanup()
assert {i.name for i in func.inputs} == {"A", "X"}
assert {o.name for o in func.outputs} == {"B"}
assert len(func.nodes) == 1
class TestCopy(object):
def test_basic(self):
graph = Graph(
nodes=[Node(op="Test")],
inputs=[Variable("test")],
outputs=[Variable("test")],
name="test-name",
doc_string="test-docstring",
import_domains=["fake-import-domain"],
opset=-1,
)
new_graph = graph.copy()
assert new_graph == graph
assert new_graph.nodes == graph.nodes
assert new_graph.inputs == graph.inputs
assert new_graph.outputs == graph.outputs
assert new_graph.name == graph.name
assert new_graph.doc_string == graph.doc_string
assert new_graph.import_domains == graph.import_domains
assert new_graph.opset == graph.opset
def test_copy(self):
def make_graph():
graph, _ = toposort_multi_tier_output_graph()
graph.outputs.pop()
# Deep copy should work with empty tensors
graph.nodes[0].inputs.append(Variable.empty())
graph.nodes[0].outputs.append(Variable.empty())
return graph
graph = make_graph()
new_graph = graph.copy()
assert graph == new_graph
# Running cleanup on the first graph should not affect the copy
graph.cleanup()
assert graph != new_graph
assert new_graph == make_graph()
def test_copy_with_subgraph(self, nested_graph):
new_graph = nested_graph.copy()
assert new_graph == nested_graph
new_subgraph = new_graph.nodes[1].attrs["body"]
id_out = new_subgraph.nodes[0].inputs[0]
assert id_out.name == "id_out"
assert len(id_out.inputs) == 1
assert id_out.inputs[0].op == "Identity"
assert id_out.inputs[0].inputs[0].name == "input"
new_subgraph.nodes[0].outputs.clear()
new_subgraph.nodes[1].inputs.clear()
subgraph = nested_graph.nodes[1].attrs["body"]
assert subgraph.nodes[0].outputs
assert subgraph.nodes[1].inputs
new_graph.outputs.clear()
new_graph.cleanup()
assert nested_graph.outputs
assert len(nested_graph.nodes) == 2
assert len(subgraph.nodes) == 2
# If the subgraph has a tensor with the same name as the outer graph,
# the subgraph copy should include a copy of the subgraph tensor, not the outer
# graph tensor.
def test_copy_with_subgraph_dup_tensors(self):
inp = Variable("input", dtype=np.float32, shape=(4, 5))
graph = Graph(inputs=[inp])
# We'll use shape to distinguish inner/outer tensor
subgraph_inp = Variable("input", dtype=np.float32, shape=(1, 2))
subgraph = Graph(inputs=[subgraph_inp])
graph.outputs = [graph.nested(inp, subgraph)]
graph_copy = graph.copy()
assert graph_copy.nodes[0].attrs["body"].inputs[0].shape == (1, 2)
def test_copy_with_subgraph_dup_const_tensors(self):
inp = Constant("input", values=np.ones(dtype=np.float32, shape=(4, 5)))
graph = Graph()
# We'll use shape to distinguish inner/outer tensor
subgraph_inp = Constant("input", values=np.ones(dtype=np.float32, shape=(1, 2)))
subgraph = Graph()
subgraph.outputs = [subgraph.identity(subgraph_inp)]
graph.outputs = [graph.nested(inp, subgraph)]
graph_copy = graph.copy()
assert graph_copy.nodes[0].attrs["body"].nodes[0].inputs[0].shape == (1, 2)
def test_function(self):
func = Function(
"Test",
domain="onnx-gs.test",
nodes=[Node(op="Add")],
inputs=[Variable("input")],
outputs=[Variable("output")],
doc_string="docstring",
opset=15,
import_domains=["test"],
attrs={"attr1": None, "attr2": np.array([1, 2, 3])},
)
func_copy = func.copy()
assert func.name == func_copy.name
assert func.domain == func_copy.domain
assert func.nodes == func_copy.nodes
assert func.inputs == func_copy.inputs
assert func.outputs == func_copy.outputs
assert func.doc_string == func_copy.doc_string
assert func.opset == func_copy.opset
assert func.import_domains == func_copy.import_domains
assert func.attrs["attr1"] == func_copy.attrs["attr1"]
assert np.all(func.attrs["attr2"] == func_copy.attrs["attr2"])
assert func.nodes is not func_copy.nodes
assert func.inputs is not func_copy.inputs
assert func.outputs is not func_copy.outputs
assert func.attrs is not func_copy.attrs
assert func.nodes[0] is not func_copy.nodes[0]
assert func.inputs[0] is not func_copy.inputs[0]
assert func.outputs[0] is not func_copy.outputs[0]
assert func.attrs["attr2"] is not func_copy.attrs["attr2"]
@pytest.fixture
def simple_foldable():
# Graph:
# c = (a + b)
# output = input + c
# Should fold to:
# output = input + c
weights = np.ones(shape=(1, 3), dtype=np.float32)
graph = Graph(ir_version=10)
inp = Variable("input", shape=(1, 3), dtype=np.float32)
c = graph.add(weights, weights, name="c")
out = graph.add(inp, c, name="out")
graph.inputs = [inp]
graph.outputs = [out]
yield graph
@pytest.fixture
def one_hop_foldable():
# Graph:
# c = (a + b)
# e = (c + d)
# output = input + e
# Should fold to:
# output = input + e
weights = np.ones(shape=(1, 3), dtype=np.float32)
graph = Graph(ir_version=10)
inp = Variable("input", shape=(1, 3), dtype=np.float32)
c = graph.add(weights, weights, name="c")
e = graph.add(c, weights, name="e")
out = graph.add(inp, e)
graph.inputs = [inp]
graph.outputs = [out]
yield graph
@pytest.fixture
def foldable_with_invalid_node():
# Graph
# c = (a + b)
# e = fake(d)
# f = (e + c)
# out = inp + f
#
# c should be folded even though e is the output of an
# invalid node.
weights = np.ones(shape=(1, 3), dtype=np.float32)
graph = Graph(ir_version=10)
inp = Variable("input", shape=(1, 3), dtype=np.float32)
c = graph.add(weights, weights, name="c")
e = graph.fake(weights, name="e")
f = graph.add(e, c, name="f")
out = graph.add(inp, f, name="output")
graph.inputs = [inp]
graph.outputs = [out]
yield graph
@pytest.fixture
def foldable_with_local_functions():
dtype = np.float32
counter = 0
def const():
nonlocal counter
counter += 1
return Constant(f"constant_{counter}", np.ones(1, dtype=np.float32))
func_inner = Function("FuncInner", ir_version=10)
func_outer = Function("FuncOuter", ir_version=10)
funcs = [func_inner, func_outer]
# func_inner(x) = x + 1
func_inner.inputs = [Variable("input", dtype=dtype)]
func_inner.outputs = [Variable("output", dtype=dtype)]
func_inner.nodes = [
Node(
"Add",
inputs=[func_inner.inputs[0], const()],
outputs=[func_inner.outputs[0]],
)
]
# func_outer(x) = func_inner(1) * x
func_outer.inputs = [Variable("input", dtype=dtype)]
func_outer.functions = [func_inner]
func_outer_intermediate = func_outer.FuncInner(inputs=[const()])[0]
func_outer.outputs = [func_outer.mul(func_outer.inputs[0], func_outer_intermediate)]
# a = func_inner(input)
# b = func_outer(input)
# c = 1 + 1
# d = a + b
# e = c + d
# output = e + c
graph = Graph(
inputs=[Variable("graph_input", dtype=dtype)], functions=funcs, ir_version=10
)
var0 = graph.FuncInner(inputs=[const()])[0]
var1 = graph.FuncOuter(inputs=[const()])[0]
var2 = graph.add(const(), const())
var3 = graph.add(var0, var1)
var4 = graph.add(var2, var3)
graph.outputs = [graph.add(var2, var4)]
graph.outputs[0].dtype = dtype
yield graph
class TestFoldConstants(object):
@pytest.mark.parametrize("partitioning", [None, "basic", "recursive"])
def test_basic(self, simple_foldable, partitioning):
inp = simple_foldable.inputs[0]
simple_foldable.fold_constants(partitioning=partitioning).cleanup(
remove_unused_graph_inputs=True
)
# Extra node should be removed
assert len(simple_foldable.nodes) == 1
assert simple_foldable.nodes[0].inputs[0] == inp
assert simple_foldable.nodes[0].inputs[1].name == "c"
# Value should be computed correctly
assert np.all(
simple_foldable.nodes[0].inputs[1].values
== np.ones(shape=(1, 3), dtype=np.float32) * 2
)
def test_one_hop(self, one_hop_foldable):
inp = one_hop_foldable.inputs[0]
one_hop_foldable.fold_constants().cleanup()
# Extra nodes should be removed
assert len(one_hop_foldable.nodes) == 1
assert one_hop_foldable.nodes[0].inputs[0] == inp
assert one_hop_foldable.nodes[0].inputs[1].name == "e"
# Value should be computed correctly
assert np.all(
one_hop_foldable.nodes[0].inputs[1].values
== np.ones(shape=(1, 3), dtype=np.float32) * 3
)
def test_with_invalid_nodes(self, foldable_with_invalid_node):
foldable_with_invalid_node.fold_constants(partitioning="recursive").cleanup()
tensor_map = foldable_with_invalid_node.tensors()
assert len(foldable_with_invalid_node.nodes) == 3
assert foldable_with_invalid_node.nodes[0].op == "Fake"
assert foldable_with_invalid_node.nodes[1].op == "Add"
assert foldable_with_invalid_node.nodes[2].op == "Add"
assert np.all(
tensor_map["c"].values == (np.ones(shape=(1, 3), dtype=np.float32) * 2)
)
def test_with_invalid_nodes_no_recursive(self, foldable_with_invalid_node):
# No folding should take place without recursive partitioning
original = foldable_with_invalid_node.copy()
assert foldable_with_invalid_node.fold_constants() == original
def test_no_foldable_constants(self):
inp0 = Variable("input0", shape=(1, 3), dtype=np.float32)
inp1 = Variable("input1", shape=(1, 3), dtype=np.float32)
out = Variable("output", shape=(1, 3), dtype=np.float32)
nodes = [Node("Add", inputs=[inp0, inp1], outputs=[out])]
graph = Graph(nodes=nodes, inputs=[inp0, inp1], outputs=[out], ir_version=10)
graph.fold_constants().cleanup()
assert len(graph.nodes) == 1
assert graph.nodes[0].inputs == [inp0, inp1]
def test_const_node(self):
graph = Graph(ir_version=10)
values = np.ones((1, 3, 3), dtype=np.int64)
graph.outputs = [graph.constant(values=values)]
assert isinstance(graph.outputs[0], Variable)
graph.fold_constants().cleanup()
assert isinstance(graph.outputs[0], Constant)
assert np.all(graph.outputs[0].values == values)
assert not graph.nodes
def test_shape_of_constant_tensor(self):
graph = Graph(ir_version=10)
values = np.ones((1, 3, 3), dtype=np.int64)
const = Constant("const", values=values)
graph.outputs = [graph.shape(const)]
graph.fold_constants().cleanup()
assert not graph.nodes
assert isinstance(graph.outputs[0], Constant)
assert np.all(graph.outputs[0].values == (1, 3, 3))
def test_shape_of_constant_node(self):
graph = Graph(ir_version=10)
values = np.ones((1, 3, 3), dtype=np.int64)
const = graph.constant(values=values)
graph.outputs = [graph.shape(const)]
graph.fold_constants().cleanup()
assert not graph.nodes
assert isinstance(graph.outputs[0], Constant)
assert np.all(graph.outputs[0].values == (1, 3, 3))
# Cannot fold shape nodes if they have dynamically shaped inputs.
def test_shape_of_variable_tensor_dynamic_shape(self):
var = Variable("var", dtype=np.float32, shape=("", -1, 0, 4))
graph = Graph(inputs=[var], ir_version=10)
graph.outputs = [graph.shape(var)]
graph.fold_constants().cleanup()
assert len(graph.nodes) == 1
assert graph.nodes[0].op == "Shape"
assert isinstance(graph.outputs[0], Variable)
def test_shape_of_variable_tensor_static_shape(self):
var = Variable("var", dtype=np.float32, shape=(1, 3, 4))
graph = Graph(inputs=[var], ir_version=10)
graph.inputs = [var]
graph.outputs = [graph.shape(var)]
graph.fold_constants().cleanup()
assert not graph.nodes
assert isinstance(graph.outputs[0], Constant)
assert np.all(graph.outputs[0].values == (1, 3, 4))
def test_shape_of_variable_tensor_multiple_shapes(self):
graph = Graph(ir_version=10)
var = Variable("var", dtype=np.float32, shape=(1, 3, 4))
var2 = Variable("var2", dtype=np.float32, shape=tuple()) # Scalar
graph.inputs = [var, var2]
graph.outputs = [graph.shape(var), graph.identity(var), graph.shape(var2)]
graph.fold_constants().cleanup()
assert len(graph.nodes) == 1
assert graph.nodes[0].op == "Identity"
assert isinstance(graph.outputs[0], Constant)
assert np.all(graph.outputs[0].values == (1, 3, 4))
assert isinstance(graph.outputs[2], Constant)
assert np.all(graph.outputs[2].values == tuple())
def test_shape_of_variable_tensor_static_shape_no_fold(self):
graph = Graph(ir_version=10)
var = Variable("var", dtype=np.float32, shape=(1, 3, 4))
graph.inputs = [var]
graph.outputs = [graph.shape(var)]
graph.fold_constants(fold_shapes=False).cleanup()
assert len(graph.nodes) == 1
assert graph.nodes[0].op == "Shape"
assert isinstance(graph.outputs[0], Variable)
# Constant folding should not cause constant tensors in the model to be loaded.
def test_no_load_constants(self):
graph = gs.import_onnx(const_foldable().load())
new_graph = graph.fold_constants()
def check_no_const_loaded(graph):
num_lazy_constants = 0
for tensor in graph.tensors().values():
if isinstance(tensor, Constant) and isinstance(
tensor._values, LazyValues
):
num_lazy_constants += 1
assert (
num_lazy_constants == 3
) # Graph starts with 3 constants - none should be loaded.
check_no_const_loaded(graph)
check_no_const_loaded(new_graph)
@pytest.mark.parametrize(
"shape, indices",
[
(("batch", 3, "height", "width"), 1), # Scalar indices case
(None, 1), # Shape not inferered case
(("batch", 3, "height", "width"), [1]),
(("batch", 3, "height", 224), [1, 3]),
(("batch", 3, 224, 224), [1, 2, 3]),
],
)
def test_shape_gather(self, shape, indices):
indices = np.array(indices)
inp = Variable("input", dtype=np.float32, shape=shape)
graph = Graph(inputs=[inp], ir_version=10)
inp_shape = graph.shape(inp)
shape_part = graph.gather(inp_shape, indices=indices)
graph.outputs = [
graph.add(shape_part, shape_part),
graph.gather(inp_shape, indices=[0]),
graph.gather(inp_shape, indices=np.array(0)),
]
graph.fold_constants()
if shape is not None:
assert isinstance(graph.outputs[0], Constant)
expected_shape = np.array(shape)[indices].astype(np.int64) * 2
assert np.all(graph.outputs[0].values == expected_shape)
else:
assert isinstance(graph.outputs[0], Variable)
assert isinstance(graph.outputs[1], Variable)
assert isinstance(graph.outputs[2], Variable)
@pytest.mark.parametrize(
"shape, starts, ends, axes, steps, expected",
[
(
("batch", 3, "height", "width"),
1,
2,
0,
1,
[3],
), # Scalar starts/ends case
(("batch", 3, "height", "width"), [1], [2], [0], [1], [3]),
(
("batch", 3, 5, "width"),
[1],
[-1],
[0],
[1],
[3, 5],
), # Negative ends case
(
("batch", 3, 5, 7),
[1],
[2000],
[0],
[1],
[3, 5, 7],
), # Past end, ends case
(("batch", 3, 5, 7), [-2], [4], [0], [1], [5, 7]), # Negative starts case
(("batch", 3, 5, 7), [-2], [4], [1], [1], None), # Non-zero axes case
(("batch", 3, 5, "width"), [-2], [4], [1], [1], None), # Dynamic case
(("batch", 3, 5, 7), [1], [4], [0], [2], [3, 7]), # Non-one steps case
(("batch", 3, 5, 7), [4], [0], [0], [-1], [7, 5, 3]), # Negative steps case
],
)
def test_shape_slice(self, shape, starts, ends, axes, steps, expected):
inp = Variable("input", dtype=np.float32, shape=shape)
graph = Graph(inputs=[inp], ir_version=10)
inp_shape = graph.shape(inp)
graph.outputs = [
graph.slice(
inp_shape,
np.array(starts),
np.array(ends),
axes=np.array(axes),
steps=np.array(steps),
)
]
graph.fold_constants()
if expected:
assert isinstance(graph.outputs[0], Constant)
assert np.all(graph.outputs[0].values == expected)
else:
assert isinstance(graph.outputs[0], Variable)
# In the single input case, we should derive starts/ends/axes/steps from the attributes.
def test_shape_slice_single_input(self):
inp = Variable("input", dtype=np.int64, shape=(5, 6, 3, 2))
graph = Graph(inputs=[inp], ir_version=10)
inp_shape = graph.shape(inp)
graph.outputs = [graph.slice(inp_shape)]
slice_node = graph.outputs[0].inputs[0]
slice_node.attrs = {
"axes": [0],
"starts": [1],
"ends": [3],
"steps": [2],
}
graph.fold_constants()
assert isinstance(graph.outputs[0], Constant)
assert np.all(graph.outputs[0].values == inp.shape[1:3:2])
def test_with_variable_conditional(self):
cond = gs.Variable("cond", dtype=bool, shape=(1,))
X = gs.Variable("X", dtype=np.float32, shape=(1,))
Y = gs.Constant("Y", values=np.ones((1,), dtype=np.float32))
graph = Graph(inputs=[X, cond], ir_version=10)
then_graph = Graph(name="Then", ir_version=10)
then_graph.outputs = [then_graph.add(Y, Y)]
else_graph = Graph(name="Else", ir_version=10)
else_graph.outputs = [else_graph.add(X, else_graph.add(Y, Y))]
graph.outputs = [graph.if_op(cond, then_graph, else_graph)]
graph.fold_constants()
graph.cleanup()
assert len(then_graph.nodes) == 0
assert np.all(then_graph.outputs[0].values == (Y.values * 2))
assert len(else_graph.nodes) == 1
assert isinstance(else_graph.nodes[0].inputs[1], Constant)
assert np.all(else_graph.nodes[0].inputs[1].values == (Y.values * 2))
@pytest.mark.parametrize("cond_value", [True, False])
@pytest.mark.parametrize("flatten", [True, False])
def test_flatten_static_conditional(self, flatten, cond_value):
cond = gs.Constant("cond", values=np.array([cond_value], dtype=bool))
X = gs.Variable("X", dtype=np.float32, shape=(1,))
Y = gs.Variable("Y", dtype=np.float32, shape=(1,))
graph = Graph(inputs=[X, cond], ir_version=10)
then_graph = Graph(name="Then", ir_version=10)
then_graph.outputs = [then_graph.relu(then_graph.add(Y, Y))]
else_graph = Graph(name="Else", ir_version=10)
else_graph.outputs = [else_graph.add(X, else_graph.add(Y, Y))]
if_out = graph.if_op(cond, then_graph, else_graph)
graph.outputs = [if_out]
graph.fold_constants(flatten_subgraphs=flatten)
graph.cleanup()
if flatten:
assert len(graph.nodes) == 2
assert graph.nodes[0].op == "Add"
assert graph.nodes[1].op == "Relu" if cond_value else "Add"
subgraph = then_graph if cond_value else else_graph
# Make sure subgraph intermediate tensors are renamed
assert graph.nodes[0].outputs[0].name == "add_out_0_subg_0_{:}".format(
subgraph.name
)
assert graph.outputs[0].inputs[0] == subgraph.nodes[-1]
assert subgraph.nodes[-1] == graph.nodes[-1]
else:
assert len(graph.nodes) == 1
assert len(graph.nodes) == 1
assert graph.nodes[0].op == "If"
assert graph.outputs[0].inputs[0] == graph.nodes[-1]
assert graph.outputs == [if_out]
def test_const_inp_but_non_foldable_nested_graph(self):
cond = gs.Constant("cond", values=np.array(True))
X = gs.Variable("X", dtype=np.float32, shape=(1,))
graph = Graph(inputs=[X], ir_version=10)
then_graph = Graph(name="Then", ir_version=10)
then_graph.outputs = [then_graph.add(X, X)]
else_graph = Graph(name="Else", ir_version=10)
else_graph.outputs = [else_graph.add(X, else_graph.add(X, X))]
# Even though if_op looks foldable because it has all constant inputs,
# it's not, since its subgraphs depend on variables in the outer scope.
graph.outputs = [graph.if_op(cond, then_graph, else_graph)]
# This should not raise because the `If` node should be excluded from
# constant folding.
graph.fold_constants(error_ok=False, flatten_subgraphs=False).cleanup()
assert graph.nodes[0].op == "If"
assert len(then_graph.nodes) == 1
assert len(else_graph.nodes) == 2
def test_cast_elision(self):
graph = gs.import_onnx(shape_cast_elision().load())
graph.fold_constants().cleanup()
assert not any(node.op == "Cast" for node in graph.nodes)
def test_cast_elision_int64(self):
X = gs.Variable("X", dtype=np.int64, shape=(1,))
graph = Graph(inputs=[X], ir_version=10)
casted_x = graph.cast(X, to=onnx.TensorProto.DataType.FLOAT)
add_out = graph.add(casted_x, casted_x)
graph.outputs = [graph.cast(add_out, to=onnx.TensorProto.DataType.INT64)]
graph.fold_constants().cleanup()
assert graph.nodes[0].op == "Add"
# Make sure we're lowering constant nodes before running cast elision
def test_cast_elision_with_constant_node(self):
inp = gs.Variable("inp", dtype=np.int64, shape=(1,))
graph = Graph(inputs=[inp], ir_version=10)
casted_inp = graph.cast(inp, to=onnx.TensorProto.DataType.FLOAT)
add_out = graph.add(casted_inp, graph.constant(np.array([2], dtype=np.float32)))
casted_out = graph.cast(add_out, to=onnx.TensorProto.DataType.INT64)
casted_out.dtype = np.int64
graph.outputs = [casted_out]
graph.fold_constants().cleanup()
assert [node.op for node in graph.nodes] == ["Add"]
add_const_inp = graph.nodes[0].inputs[1]
assert isinstance(add_const_inp, Constant)
assert (
add_const_inp.dtype == np.int64
) # Should have been casted to match dtype of other inputs.
# For a graph like:
#
# inp
# |
# Cast
# |
# Add
# |
# Cast
# |
# out
#
# 1. We cannot remove the initial `Cast` if it is used outside the `Add` node
# 2. We cannot perform cast elision at all if the original output of the `Add` node is
# used outside the subsequent `Cast` node.
#
@pytest.mark.parametrize("use_as_graph_output", [True, False], ids=["graph", ""])
@pytest.mark.parametrize("use_in_other_node", [True, False], ids=["node", ""])
# Whether to apply the effects of the first two parameters to the input `Cast` node or to the `Add` node.
@pytest.mark.parametrize(
"apply_to_input_cast", [True, False], ids=["input", "output"]
)
def test_cast_elision_multi_use_cast(
self, use_as_graph_output, use_in_other_node, apply_to_input_cast
):
X = gs.Variable("X", dtype=np.int32, shape=(1,))
graph = Graph(inputs=[X], ir_version=10)
casted_x = graph.cast(X, to=onnx.TensorProto.DataType.FLOAT)
add_out = graph.add(casted_x, casted_x)
uncasted_x = graph.cast(add_out, to=onnx.TensorProto.DataType.INT32)
graph.outputs = [uncasted_x]
mutli_use_tensor = casted_x if apply_to_input_cast else add_out
if use_in_other_node:
graph.outputs.append(graph.identity(mutli_use_tensor))
if use_as_graph_output:
graph.outputs.append(mutli_use_tensor)
print(graph)
graph.fold_constants().cleanup()
ops = [node.op for node in graph.nodes]
if use_as_graph_output or use_in_other_node:
if apply_to_input_cast:
assert graph.nodes[1].inputs[0] == X
assert graph.nodes[1].outputs[0] == uncasted_x
assert ops == ["Cast", "Add"] + (
["Identity"] if use_in_other_node else []
)
else:
assert ops == ["Cast", "Add", "Cast"] + (
["Identity"] if use_in_other_node else []
)
else:
assert ops == ["Add"]
@pytest.mark.parametrize(
# If layer1_num_bytes is larger than layer0_num_bytes, then it must be a multiple.
"size_threshold, layer0_num_bytes, layer0_should_fold, layer1_num_bytes, layer1_should_fold",
[
# No size threshold - everything should fold.
(
None,
2,
True,
4,
True,
),
# Monotonically increasing but under size threshold - everything should fold.
(
8,
2,
True,
4,
True,
),
# Increasing then decreasing, but under size threshold - everything should fold.
(
8,
2,
True,
1,
True,
),
# All tensors over size threshold - nothing should fold.
(
1,
2,
False,
4,
False,
),
# Second tensor over size threshold - only first tensor should fold.
(
3,
2,
True,
4,
False,
),
# First tensor over size threshold - second tensor should still fold.
(
3,
4,
False,
2,
True,
),
],
)
@pytest.mark.parametrize("push_into_subgraph", [True, False], ids=["subgraph", ""])
def test_folding_size_threshold(
self,
size_threshold,
layer0_num_bytes,
layer0_should_fold,
layer1_num_bytes,
layer1_should_fold,
push_into_subgraph,
):
graph = Graph(ir_version=10)
shape = (1,)
layer0_repeats = layer0_num_bytes // misc.volume(shape)
layer0 = graph.tile(np.ones(shape, dtype=np.int8), repeats=[layer0_repeats])
layer0.inputs[0].name = "Layer0"
if layer1_num_bytes > layer0_num_bytes:
layer1_repeats = layer1_num_bytes // layer0_num_bytes
layer1 = graph.tile(layer0, repeats=[layer1_repeats])
else:
layer1 = graph.slice(layer0, starts=[0], ends=[layer1_num_bytes])
layer1.inputs[0].name = "Layer1"
graph.outputs = [layer1]
# Make sure size_threshold option is propagated into subgraphs.
if push_into_subgraph:
cond = gs.Variable("cond", dtype=bool, shape=tuple())
outer_graph = Graph(inputs=[cond])
outer_graph.if_op(cond, then_graph=graph, else_graph=graph)
outer_graph.fold_constants(size_threshold=size_threshold)
else:
graph.fold_constants(size_threshold=size_threshold)
# When a tensor is folded, it is disconnected from its producer nodes
assert len(graph.nodes[0].outputs) == (0 if layer0_should_fold else 1)
assert len(graph.nodes[1].outputs) == (0 if layer1_should_fold else 1)
@pytest.mark.parametrize("op", ["Q", "DQ"])
@pytest.mark.parametrize("add_intermediate_layer", [True, False])
def test_no_fold_qdq(self, op, add_intermediate_layer):
dtype = np.float32 if op == "Q" else np.int8
inp = gs.Constant("input", np.ones(shape=(1, 3, 5, 5), dtype=dtype))
graph = Graph(inputs=[inp], opset=13, ir_version=10)
if add_intermediate_layer:
inp = graph.identity(inp)
qdq_func = graph.quantize_linear if op == "Q" else graph.dequantize_linear
graph.outputs = [
qdq_func(inp, 1.2, np.array(0, dtype=np.int8))
] # Arbitrary scale and zero-point
graph.fold_constants().cleanup()
assert len(graph.nodes) == 1
assert (
graph.nodes[0].op == "QuantizeLinear" if op == "Q" else "DequantizeLinear"
)
@pytest.mark.parametrize(
"should_exclude_node_func,expected_node_names",
[
(
lambda node: True,
[
"onnx_graphsurgeon_node_1",
"onnx_graphsurgeon_node_3",
"onnx_graphsurgeon_node_5",
"onnx_graphsurgeon_node_7",
],
),
(
lambda node: node.name == "onnx_graphsurgeon_node_5",
["onnx_graphsurgeon_node_5", "onnx_graphsurgeon_node_7"],
),
(
lambda node: node.op == "Add",
[
"onnx_graphsurgeon_node_1",
"onnx_graphsurgeon_node_3",
"onnx_graphsurgeon_node_5",
"onnx_graphsurgeon_node_7",
],
),
(
lambda node: node.op == "Relu",
[
"onnx_graphsurgeon_node_3",
"onnx_graphsurgeon_node_5",
"onnx_graphsurgeon_node_7",
],
),
],
)
def test_custom_should_exclude_node(
self, should_exclude_node_func, expected_node_names
):
inp = gs.Constant("input", np.ones(shape=(1, 3, 5, 5), dtype=np.float32))
graph = Graph(inputs=[inp], ir_version=10)
add_0 = graph.add(inp, inp) # onnx_graphsurgeon_node_1 -> add_out_0
relu_0 = graph.relu(add_0) # onnx_graphsurgeon_node_3 -> relu_out_2
add_1 = graph.add(relu_0, relu_0) # onnx_graphsurgeon_node_5 -> add_out_4
relu_1 = graph.relu(add_1) # onnx_graphsurgeon_node_7 -> relu_out_6
graph.outputs = [relu_1]
graph.fold_constants(should_exclude_node=should_exclude_node_func).cleanup()
assert [node.name for node in graph.nodes] == expected_node_names
def test_omitted_optional_inputs_ignored(self):
# An omitted optional input will show up as a `Variable` with no name.
# This should *not* prevent us from folding nodes where all other inputs are constants.
data = gs.Constant("data", np.ones(shape=(3, 5, 5), dtype=np.float32))
pads = gs.Constant("pads", np.zeros(shape=(6,), dtype=np.int64))
graph = Graph(ir_version=10)
pad_0 = graph.pad(data, pads, constant_value=None)
graph.outputs = [pad_0]
assert pad_0.inputs[0].inputs[2] == Variable.empty()
assert len(graph.nodes) == 1
graph.fold_constants().cleanup()
assert len(graph.nodes) == 0
assert isinstance(graph.outputs[0], gs.Constant)
def test_function(self, simple_foldable):
func = Function(
"Test",
nodes=simple_foldable.nodes,
inputs=simple_foldable.inputs,
outputs=simple_foldable.outputs,
ir_version=10,
)
assert len(func.nodes) == 2
func.fold_constants().cleanup()
assert len(func.nodes) == 1
def test_function_with_attributes(self):
# Nodes that reference function attributes shouldn't be folded.
input = Variable("input", dtype=np.float32)
func = Function(
"Test", inputs=[input], attrs={"softmax_axis": -1}, ir_version=10
)
x = func.softmax(
np.float32([[1, 2, 3]]), axis=Node.AttributeRef("softmax_axis", int)
)
y = func.softmax(np.float32([[4, 5, 6]]), axis=-1)
z = func.add(x, y)
func.outputs += [func.add(input, z)]
# Only one node should get folded.
assert len(func.nodes) == 4
func.fold_constants().cleanup()
assert len(func.nodes) == 3
def test_functions_inside_functions(self, foldable_with_local_functions):
graph = foldable_with_local_functions
graph.toposort()
graph.fold_constants(error_ok=False)
graph.cleanup()
assert len(graph.inputs) == 1
assert len(graph.outputs) == 1
assert isinstance(graph.outputs[0], Constant)
assert graph.outputs[0].values == 8
def test_function_with_unused_input(self, foldable_with_local_functions):
# Constant folding should still work correctly when a function has unused inputs.
graph = foldable_with_local_functions
func_outer = graph.functions[1]
func_outer.nodes[1].inputs[0] = gs.Constant(
"Constant_99", values=np.array([3], dtype=np.float32)
)
graph.toposort().fold_constants(error_ok=False).cleanup()
assert len(graph.inputs) == 1
assert len(graph.outputs) == 1
assert isinstance(graph.outputs[0], Constant)
assert graph.outputs[0].values == 12
def test_inner_function_with_unused_input(self, foldable_with_local_functions):
graph = foldable_with_local_functions
func_inner = graph.functions[0]
func_inner.nodes[0].inputs[0] = gs.Constant(
"Constant_99", values=np.array([3], dtype=np.float32)
)
graph.toposort().fold_constants(error_ok=False).cleanup()
assert len(graph.inputs) == 1
assert len(graph.outputs) == 1
assert isinstance(graph.outputs[0], Constant)
assert graph.outputs[0].values == 12
def test_function_with_subgraph(self, foldable_with_local_functions):
graph = foldable_with_local_functions
dtype = graph.outputs[0].dtype
func = Function("func_with_subgraph", ir_version=10)
func.inputs = [Variable("input")]
then_graph = Graph(name="Then", ir_version=10)
then_graph.outputs = [then_graph.identity(func.inputs[0])]
else_graph = Graph(name="Else", ir_version=10)
else_graph.functions = graph.functions
else_graph.outputs = else_graph.FuncInner(
inputs=[func.inputs[0]], outputs=["else_out"]
)
cond = func.less(func.inputs[0], Constant("Zero", np.zeros(1, dtype=dtype)))
func.outputs = [func.if_op(cond, then_graph, else_graph)]
graph.functions.append(func)
graph.outputs = graph.func_with_subgraph(
inputs=[graph.outputs[0]], outputs=["new_output"]
)
graph.toposort().fold_constants(error_ok=False).cleanup()
assert len(graph.inputs) == 1
assert len(graph.outputs) == 1
assert isinstance(graph.outputs[0], Constant)
assert graph.outputs[0].values == 9
def test_function_with_complicated_attrs(self):
# Types of attributes we should test:
# 1) Has no default value
# 2) Has default value which is used
# 3) Has default value which is overridden
# 4) Confusing attribute name / reference name mappings
dtype = np.float32
opset = 18
func = Function(
"complicated_func",
inputs=[Variable("input", dtype=dtype)],
opset=opset,
ir_version=10,
)
variables = [Variable(f"var{i}", dtype=dtype) for i in range(5)]
func.nodes.append(
Node(
"ConstantOfShape",
attrs={"value": Node.AttributeRef("ConstantOfShape_value", Tensor)},
inputs=[func.inputs[0]],
outputs=[variables[0]], # shape [2, 3, 4]
)
)
func.nodes.append(
Node(
"ReduceSum",
attrs={"keepdims": Node.AttributeRef("keepdims", int)},
inputs=[
variables[0],
Constant("ReduceSum_axis", np.array([1], dtype=int)),
],
outputs=[variables[1]], # shape [2, 1, 4] when keepDims=True
)
)
func.nodes.append(
Node(
"Flatten",
attrs={"axis": Node.AttributeRef("Flatten_axis", np.int32)},
inputs=[variables[1]],
outputs=[variables[2]], # shape [2, 4] when axis=1
)
)
func.nodes.append(
Node(
"Concat",
attrs={"axis": Node.AttributeRef("axis", np.int64)},
inputs=[
variables[2],
Constant("to_concat", values=np.ones((2, 1), dtype=dtype)),
],
outputs=[variables[3]], # shape [2, 5] when axis=-1
)
)
func.nodes.append(
Node(
"Concat",
attrs={"axis": Node.AttributeRef("Concat_axis", int)},
inputs=[
variables[3],
Constant("to_concat_2", values=2 * np.ones((2, 1), dtype=dtype)),
],
outputs=[variables[4]], # shape [2, 6] when axis=-1
)
)
func.outputs = [variables[4]]
func.attrs = {
"ConstantOfShape_value": None, # no default value
"axis": None, # no default value
"Flatten_axis": 0,
"keepdims": True,
"Concat_axis": -1,
}
graph = Graph(opset=opset, functions=[func], ir_version=10)
input = Constant("shape", values=np.array([2, 3, 4], dtype=int))
output = graph.complicated_func(
inputs=[input],
outputs=["output"],
attrs={
"ConstantOfShape_value": Constant(
"three", values=np.array([3], dtype=dtype)
),
"axis": -1,
"Flatten_axis": 2, # overrides default value
},
)[0]
output.dtype = dtype
graph.inputs = [input]
graph.outputs = [output]
graph.fold_constants(error_ok=False).cleanup()
assert len(graph.inputs) == 1
assert len(graph.outputs) == 1
assert isinstance(graph.outputs[0], Constant)
assert np.all(
graph.outputs[0].values
== np.array([[9, 9, 9, 9, 1, 2], [9, 9, 9, 9, 1, 2]])
)
class TestIO(object):
def test_io_cannot_be_sync_list_on_init(self):
inp = Variable("input0", shape=(1, 3), dtype=np.float32)
out = Variable("input1", shape=(1, 3), dtype=np.float32)
node = Node("Add", inputs=[inp], outputs=[out])
assert isinstance(node.inputs, SynchronizedList)
assert isinstance(node.outputs, SynchronizedList)
graph = Graph(nodes=[node], inputs=node.inputs, outputs=node.outputs)
assert not isinstance(graph.inputs, SynchronizedList)
assert not isinstance(graph.outputs, SynchronizedList)
def test_io_cannot_be_sync_list_on_assign(self):
inp = Variable("input0", shape=(1, 3), dtype=np.float32)
out = Variable("input1", shape=(1, 3), dtype=np.float32)
node = Node("Add", inputs=[inp], outputs=[out])
assert isinstance(node.inputs, SynchronizedList)
assert isinstance(node.outputs, SynchronizedList)
graph = Graph(nodes=[node], inputs=[], outputs=[])
graph.inputs = node.inputs
graph.outputs = node.outputs
assert not isinstance(graph.inputs, SynchronizedList)
assert not isinstance(graph.outputs, SynchronizedList)
class TestFunctionList(object):
def test_shared_function_list(self):
subgraph = Graph()
node = Node("Test", attrs={"test_attr": subgraph})
main_graph = Graph(nodes=[node])
new_func = Function("func")
main_graph.functions.append(new_func)
assert len(main_graph.functions) == 1
assert len(subgraph.functions) == 1
assert subgraph.functions[0] == new_func
def test_set_function_list(self):
subgraph = Graph()
node = Node("Test", attrs={"test_attr": subgraph})
main_graph = Graph(nodes=[node])
new_func = Function("func")
main_graph.functions = [new_func]
assert len(main_graph.functions) == 1
assert len(subgraph.functions) == 1
assert subgraph.functions[0] == new_func
def test_merge_funcs(self):
func1 = Function("func1", domain="domain1")
func2 = Function("func1", domain="domain2")
func3 = Function("func2", domain="domain1")
func4 = Function("func2", domain="domain2")
all_funcs = [func1, func2, func3, func4]
subgraph1 = Graph(functions=[func1, func2])
node1 = Node("Test", attrs={"test_attr": subgraph1})
subgraph2 = Graph(functions=[func2, func3], nodes=[node1])
node2 = Node("Test", attrs={"test_attr": subgraph2})
main_graph = Graph(functions=[func3, func4], nodes=[node2])
assert len(main_graph.functions) == len(all_funcs)
assert len(subgraph1.functions) == len(all_funcs)
assert len(subgraph2.functions) == len(all_funcs)
new_func = Function("func3")
main_graph.functions.append(new_func)
assert new_func in subgraph1.functions
assert new_func in subgraph2.functions