1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265
|
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.
import pathlib
import unittest
import onnx
from onnx import TensorProto, helper, shape_inference
from ..mobile_helpers.usability_checker import check_shapes
from ..onnx_model_utils import (
fix_output_shapes,
get_producer_consumer_maps,
is_fixed_size_tensor,
make_dim_param_fixed,
make_input_shape_fixed,
)
script_dir = pathlib.Path(__file__).parent
ort_root = script_dir.parents[3]
# example usage from <ort root>/tools/python
# python -m unittest util/test/test_onnx_model_utils.py
# NOTE: at least on Windows you must use that as the working directory for all the imports to be happy
class TestGetProducerConsumerMaps(unittest.TestCase):
@staticmethod
def _create_model():
# create a model with subgraphs and various types of shadowing
body = helper.make_graph(
[
# shadow a1 in main graph.
# LoopAdd_SubgraphOutput should be linked to this and a1 should not be an implicit input
helper.make_node("Add", ["loop_state_in", "iter"], ["a1"], "LoopAdd_Shadows"),
# main_graph_initializer should be handled (implicit input but no producer node)
# graph input 'x' from main graph should also be handled
helper.make_node("Add", ["main_graph_initializer", "x"], ["a2"], "LoopAdd_OuterScopeInitializer"),
# implicit input should be handled - 'z' can be accessed from outside scope
# Add2 in main graph should be implicit input of the Loop node
helper.make_node("Add", ["z", "a1"], ["a3"], "LoopAdd_ImplicitInput"),
# create subgraph output
helper.make_node("Add", ["a2", "a3"], ["loop_state_out"], "LoopAdd_SubgraphOutput"),
],
"Loop_body",
[
helper.make_tensor_value_info("iter", TensorProto.INT64, [1]),
helper.make_tensor_value_info("cond", TensorProto.BOOL, [1]),
helper.make_tensor_value_info("loop_state_in", TensorProto.FLOAT, [1]),
],
[
helper.make_tensor_value_info("cond", TensorProto.BOOL, [1]),
helper.make_tensor_value_info("loop_state_out", TensorProto.FLOAT, [1]),
],
[],
)
# Create the main graph
graph_proto = helper.make_graph(
[
# create 'a1' which is shadowed in the subgraph. node should not be joined to Loop1
helper.make_node("Add", ["x", "y"], ["a1"], "Add1"),
# create 'z' which is an implicit input to subgraph. node should be joined to Loop1
helper.make_node("Add", ["a1", "main_graph_initializer"], ["z"], "Add2"),
# rename 'z' to use as explicit input to Loop
helper.make_node("Identity", ["z"], ["state_var_in"], "RenameZ"),
helper.make_node(
"Loop", ["max_trip_count", "keep_going", "state_var_in"], ["state_var_out"], "Loop1", body=body
),
helper.make_node("Sub", ["a1", "state_var_out"], ["graph_output"], "sub_1"),
],
"Main_graph",
[
helper.make_tensor_value_info("x", TensorProto.FLOAT, [1]),
helper.make_tensor_value_info("y", TensorProto.FLOAT, [1]),
],
[
helper.make_tensor_value_info("graph_output", TensorProto.FLOAT, [1]),
],
[
helper.make_tensor("max_trip_count", TensorProto.INT64, [1], [2]),
helper.make_tensor("main_graph_initializer", TensorProto.FLOAT, [1], [1.0]),
helper.make_tensor("keep_going", TensorProto.BOOL, [1], [True]),
],
)
return helper.make_model(graph_proto)
def test_model_with_subgraph(self):
"""
Test a manually created model that has a subgraph and implicit inputs of all possible types.
"""
model = self._create_model()
node_to_producers, node_to_consumers = get_producer_consumer_maps(model.graph)
main_graph_add_create_a1 = model.graph.node[0]
main_graph_add_create_z = model.graph.node[1]
main_graph_rename_z = model.graph.node[2]
main_graph_loop = model.graph.node[3]
main_graph_sub = model.graph.node[4]
subgraph = main_graph_loop.attribute[0].g
loop_add_shadow = subgraph.node[0]
loop_add_outer_scope_init = subgraph.node[1]
loop_add_implicit_input = subgraph.node[2]
loop_add_subgraph_output = subgraph.node[3]
def node_name(node):
return f"{node.name}:{node.op_type}"
def check_linked(producer, consumer):
self.assertTrue(
producer in node_to_producers[consumer],
f"{node_name(producer)} not in producers for {node_name(consumer)}",
)
self.assertTrue(
consumer in node_to_consumers[producer],
f"{node_name(consumer)} not in consumers for {node_name(producer)}",
)
def check_not_linked(producer, consumer):
self.assertFalse(
producer in node_to_producers[consumer], f"{node_name(producer)} in producers for {node_name(consumer)}"
)
self.assertFalse(
consumer in node_to_consumers[producer],
f"{node_name(consumer)} not in consumers for {node_name(producer)}",
)
check_linked(main_graph_add_create_a1, main_graph_add_create_z)
# a1 in main graph shouldn't be implicit input to loop as it is shadowed
check_not_linked(main_graph_add_create_a1, main_graph_loop)
# z is implicit input
check_linked(main_graph_add_create_z, main_graph_loop)
check_linked(main_graph_rename_z, main_graph_loop)
check_linked(main_graph_loop, main_graph_sub)
# check subgraph
check_linked(loop_add_shadow, loop_add_implicit_input)
check_linked(loop_add_outer_scope_init, loop_add_subgraph_output)
check_linked(loop_add_implicit_input, loop_add_subgraph_output)
class TestDynamicDimReplacement(unittest.TestCase):
def test_replace_symbolic_dim(self):
"""
Update a model with a single symbolic input dimension. After replacement run shape inferencing to verify that
all shapes in the model have fixed sizes.
"""
model_path = (
ort_root / "onnxruntime" / "test" / "testdata" / "CNTK" / "test_LSTM.tanh.bidirectional" / "model.onnx"
)
model = onnx.load_model(str(model_path))
# validate the expected input after inferring shape info
m2 = shape_inference.infer_shapes(model, True)
dynamic_inputs, num_dynamic_values = check_shapes(m2.graph)
self.assertEqual(len(dynamic_inputs), 1)
self.assertEqual(dynamic_inputs[0].name, "Input3")
self.assertGreater(num_dynamic_values, 0)
# update original model
make_dim_param_fixed(model.graph, "None", 4)
# and validate the model no longer has dynamic values
model = shape_inference.infer_shapes(model, True)
dynamic_inputs, num_dynamic_values = check_shapes(model.graph)
self.assertFalse(dynamic_inputs)
self.assertEqual(num_dynamic_values, 0)
def test_replace_input_shape(self):
"""
Replace the entire shape for an input. This can be used when the model has inputs with unknown dimensions
i.e. the dimension has no value and no symbolic name so it's harder to replace.
"""
model_path = ort_root / "onnxruntime" / "test" / "testdata" / "gh_issue_9671.onnx"
model = onnx.load_model(str(model_path))
# validate the expected input after inferring shape info
m2 = shape_inference.infer_shapes(model, True)
dynamic_inputs, num_dynamic_values = check_shapes(m2.graph)
self.assertEqual(len(dynamic_inputs), 3)
self.assertEqual(dynamic_inputs[0].name, "X1")
self.assertEqual(dynamic_inputs[1].name, "X2")
self.assertEqual(dynamic_inputs[2].name, "X3")
self.assertGreater(num_dynamic_values, 0)
# update original model
make_input_shape_fixed(model.graph, "X1", [2, 2, 4])
make_input_shape_fixed(model.graph, "X2", [2, 4])
make_input_shape_fixed(model.graph, "X3", [2, 2, 4])
# and validate the model no longer has dynamic values
model = shape_inference.infer_shapes(model, True)
dynamic_inputs, num_dynamic_values = check_shapes(model.graph)
self.assertFalse(dynamic_inputs)
def test_replace_input_shape_with_dim_params(self):
# replace the input shape where the existing shape also has dim_param entries.
# in this case we should also iterate the rest of the model and replace other instances
# of the dim_param with the new value.
model_path = ort_root / "onnxruntime" / "test" / "testdata" / "fuse_mul_1.onnx"
model = onnx.load_model(str(model_path))
m2 = shape_inference.infer_shapes(model, True)
dynamic_inputs, num_dynamic_values = check_shapes(m2.graph)
self.assertEqual(len(dynamic_inputs), 1)
self.assertEqual(dynamic_inputs[0].name, "X1")
# input as well as other values in model have shape ['D'] so check > 1
self.assertGreater(num_dynamic_values, 1)
# replace X1's shape of ['D'] -> [4]
make_input_shape_fixed(model.graph, "X1", [4])
# validate the model no longer has dynamic values
# we don't run shape_inference here as 'D' is the only dimension in the whole model, and we should have
# replaced every instance of it if _make_input_shape_fixed worked as expected.
model = shape_inference.infer_shapes(model, True)
dynamic_inputs, num_dynamic_values = check_shapes(model.graph)
self.assertFalse(dynamic_inputs)
self.assertEqual(num_dynamic_values, 0)
def test_fix_output_shape(self):
"""
Replace an input shape in a model where that won't update the output shape automatically.
Manually fix the output so the usage of the model is clearer.
"""
model_path = ort_root / "onnxruntime" / "test" / "testdata" / "transform" / "fusion" / "bias_gelu_fusion.onnx"
model = onnx.load_model(str(model_path))
make_input_shape_fixed(model.graph, "A", [2, 2, 3072])
# symbolic dim names in graph inputs don't match graph outputs so they won't have been updated yet
self.assertFalse(is_fixed_size_tensor(model.graph.output[0]))
fix_output_shapes(model)
self.assertTrue(is_fixed_size_tensor(model.graph.output[0]))
def test_invalid_replace_input_shape(self):
model_path = ort_root / "onnxruntime" / "test" / "testdata" / "sklearn_bin_voting_classifier_soft.onnx"
model = onnx.load_model(str(model_path))
# test some invalid usages
self.assertRaisesRegex(
ValueError,
"Rank mismatch. Existing:2 Replacement:3",
make_input_shape_fixed,
model.graph,
"input",
[1, 2, 3],
)
self.assertRaisesRegex(
ValueError,
"Can't replace existing fixed size of 2 with 3 for dimension 2",
make_input_shape_fixed,
model.graph,
"input",
[4, 3],
)
self.assertRaisesRegex(
ValueError, "Input X1 was not found in graph inputs.", make_input_shape_fixed, model.graph, "X1", [2, 3]
)
|