# # 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 logging import numpy as np import onnx_graphsurgeon as gs logging.basicConfig(level=logging.INFO) logging.getLogger("ModelHelper").setLevel(logging.INFO) log = logging.getLogger("ModelHelper") @gs.Graph.register() def op_with_const(self, op, name, input, value): """ Add an operation with constant to the graph which will operate on the input tensor with the value(s) given. :param op: The ONNX operation to perform, i.e. "Add" or "Mul". :param input: The tensor to operate on. :param value: The value array to operate with. :param name: The name to use for the node. """ input_tensor = input if type(input) is gs.Variable else input[0] log.debug("Created {} node '{}': {}".format(op, name, value.squeeze())) const = gs.Constant(name="{}_value:0".format(name), values=value) return self.layer( name=name, op=op, inputs=[input_tensor, const], outputs=[name + ":0"] ) @gs.Graph.register() def matmul(self, name, input, value): """ Add MatMul operation to the graph which will operate on the input tensor with the value(s) given. :param input: The tensor to operate on. :param value: The linear transformation matrix to operate with. :param name: The name to use for the node. """ input_tensor = input if type(input) is gs.Variable else input[0] log.debug("Created {} node '{}': {}".format("MatMul", name, value.squeeze())) const = gs.Constant(name="{}_value:0".format(name), values=value) return self.layer( name=name, op="MatMul", inputs=[input_tensor, const], outputs=[name + ":0"] ) @gs.Graph.register() def clip(self, name, input, clip_min, clip_max): """ Add Clip operation to the graph which will operate on the input tensor with the value(s) given. :param input: The tensor to operate on. :param name: The name to use for the node. :param clip_min: Minimum value to include, less is clipped. :param clip_max: Maximum value to include, more is clipped. """ input_tensor = input if type(input) is gs.Variable else input[0] log.debug("Created {} node '{}".format("Clip", name)) const_min = gs.Constant( name="{}_value:0".format(name), values=np.asarray([clip_min], dtype=np.float32) ) const_max = gs.Constant( name="{}_value:1".format(name), values=np.asarray([clip_max], dtype=np.float32) ) return self.layer( name=name, op="Clip", inputs=[input_tensor, const_min, const_max], outputs=[name + ":0"], ) @gs.Graph.register() def slice(self, name, input, starts, ends, axes): """ Add Slice operation to the graph which will operate on the input tensor with the value(s) given. :param op: The ONNX operation to perform, i.e. "Add" or "Mul". :param input: The tensor to operate on. :param name: The name to use for the node. :param starts: Value at which Slice starts. :param ends: Value at which Slice ends. :param axes: Axes on which Slice operation should be performed. """ input_tensor = input if type(input) is gs.Variable else input[0] log.debug("Created {} node '{}".format("Slice", name)) const_start = gs.Constant( name="{}_value:0".format(name), values=np.asarray([starts], dtype=np.int64) ) const_end = gs.Constant( name="{}_value:1".format(name), values=np.asarray([ends], dtype=np.int64) ) const_axes = gs.Constant( name="{}_value:2".format(name), values=np.asarray([axes], dtype=np.int64) ) return self.layer( name=name, op="Slice", inputs=[input_tensor, const_start, const_end, const_axes], outputs=[name + ":0"], ) @gs.Graph.register() def unsqueeze(self, name, input, axes=[3]): """ Adds to the graph an Unsqueeze node for the given axes and to the given input. :param self: The gs.Graph object being extended. :param name: The name to use for the node. :param input: The tensor to be "unsqueezed". :param axes: A list of axes on which to add the new dimension(s). :return: The first output tensor, to allow chained graph construction. """ input_tensor = input if type(input) is gs.Variable else input[0] log.debug("Created Unsqueeze node '{}': {}".format(name, axes)) return self.layer( name=name, op="Unsqueeze", inputs=[input_tensor], outputs=[name + ":0"], attrs={"axes": axes}, ) @gs.Graph.register() def squeeze(self, name, input, axes=[2]): """ Adds to the graph an Squeeze node for the given axes and to the given input. :param self: The gs.Graph object being extended. :param name: The name to use for the node. :param input: The tensor to be "squeezed". :param axes: A list of axes on which to remove a dimension(s). :return: The first output tensor, to allow chained graph construction. """ input_tensor = input if type(input) is gs.Variable else input[0] log.debug("Created Squeeze node '{}': {}".format(name, axes)) return self.layer( name=name, op="Squeeze", inputs=[input_tensor], outputs=[name + ":0"], attrs={"axes": axes}, ) @gs.Graph.register() def gather(self, name, data, indices, axes=0): """ Adds to the graph a Gather node for the given axes and to the given input. :param self: The gs.Graph object being extended. :param name: The name to use for the node. :param data: Data from which to gather specific tensors. :param indices: Indices by which to gather data tensors. :param axes: A list of axes on which to perform gather operation """ data_tensor = data if type(data) is gs.Variable else data[0] indices_tensor = indices if type(indices) is gs.Variable else indices[0] log.debug("Created Gather node '{}': {}".format(name, axes)) return self.layer( name=name, op="Gather", inputs=[data_tensor, indices_tensor], outputs=[name + ":0"], attrs={"axes": axes}, ) @gs.Graph.register() def transpose(self, name, input, perm): """ Adds to the graph a Transpose node for the given axes permutation and to the given input. :param self: The gs.Graph object being extended. :param name: The name to use for the node. :param input: The tensor to be transposed. :param perm: A list of axes defining their order after transposing occurs. :return: The first output tensor, to allow chained graph construction. """ input_tensor = input if type(input) is gs.Variable else input[0] log.debug("Created Transpose node '{}': {}".format(name, perm)) return self.layer( name=name, op="Transpose", inputs=[input_tensor], outputs=[name + ":0"], attrs={"perm": perm}, ) @gs.Graph.register() def sigmoid(self, name, input): """ Adds to the graph a Sigmoid node for the given input. :param self: The gs.Graph object being extended. :param name: The name to use for the node. :param input: The tensor to be applied to. :return: The first output tensor, to allow chained graph construction. """ input_tensor = input if type(input) is gs.Variable else input[0] log.debug("Created Sigmoid node '{}'".format(name)) return self.layer( name=name, op="Sigmoid", inputs=[input_tensor], outputs=[name + ":0"] ) @gs.Graph.register() def plugin(self, op, name, inputs: list, outputs: list, attrs): """ Adds to the graph a TensorRT plugin node with the given name, inputs and outputs. The attrs dictionary holds attributes to be added to the plugin node. :param self: The gs.Graph object being extended. :param op: The registered name for the TensorRT plugin. :param name: The name to use for the node. :param inputs: The list of tensors to use an inputs. :param outputs: The list of tensors to use as outputs. :param attrs: The dictionary to use as attributes. :return: The first output tensor, to allow chained graph construction. """ log.debug("Created TRT Plugin node '{}': {}".format(name, attrs)) return self.layer(op=op, name=name, inputs=inputs, outputs=outputs, attrs=attrs) @gs.Graph.register() def find_node_by_op(self, op): """ Finds the first node in the graph with the given operation name. :param self: The gs.Graph object being extended. :param op: The operation name to search for. :return: The first node matching that performs that op. """ for node in self.nodes: if node.op == op: return node return None @gs.Graph.register() def find_node_by_op_name(self, op, name): """ Finds the first node in the graph with the given operation name. :param self: The gs.Graph object being extended. :param op: The operation name to search for. :param name: Selected node name. :return: The first node matching that performs that op. """ for node in self.nodes: if node.op == op and node.name == name: return node return None @gs.Graph.register() def find_node_by_op_input_output_name( self, op, input_name, output_name, input_pos=0, output_pos=0 ): """ Finds the first node in the graph with the given operation name. :param self: The gs.Graph object being extended. :param op: The operation name to search for. :param input_pos: Which input to consider, default is 0. :param output_pos: Which output to consider, default is 0. :param input_name: Selected input's name. :param output_name: Selected output's name. :return: The first node matching that performs that op. """ for node in self.nodes: if ( node.op == op and node.inputs[input_pos].name == input_name and node.outputs[output_pos].name == output_name ): return node return None @gs.Graph.register() def find_descendant_by_op(self, node, op, depth=10): """ Starting from the given node, finds a node lower in the graph matching the given operation name. This is not an exhaustive graph search. In order to graph search bfs is used, so runtime complexity is O(V+E). :param self: The gs.Graph object being extended. :param node: The node to start searching from. :param op: The operation name to search for. :param depth: Stop searching after traversing these many nodes. :return: The first descendant node matching that performs that op. """ queue = [] for i in range(depth): queue.append(node.o()) while queue: node = queue.pop(0) if node.op == op: return node for child in node.outputs[0].outputs: queue.append(child) return None @gs.Graph.register() def find_ancestor_by_op(self, node, op, depth=10): """ Starting from the given node, finds a node higher in the graph matching the given operation name. This is not an exhaustive graph search. In order to graph search bfs is used, so runtime complexity is O(V+E). :param self: The gs.Graph object being extended. :param node: The node to start searching from. :param op: The operation name to search for. :param depth: Stop searching after traversing these many nodes. :return: The first ancestor node matching that performs that op. """ queue = [] for i in range(depth): queue.append(node.i()) while queue: node = queue.pop(0) if node.op == op: return node for child in node.inputs[-1].inputs: queue.append(child) return None