File: test_onnx_model_utils.py

package info (click to toggle)
onnxruntime 1.23.2%2Bdfsg-6
  • links: PTS, VCS
  • area: main
  • in suites: sid
  • size: 340,756 kB
  • sloc: cpp: 3,222,136; python: 188,267; ansic: 114,318; asm: 37,927; cs: 36,849; java: 10,962; javascript: 6,811; pascal: 4,126; sh: 2,996; xml: 705; objc: 281; makefile: 67
file content (265 lines) | stat: -rw-r--r-- 11,759 bytes parent folder | download | duplicates (3)
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]
        )