#!/usr/bin/env python3 # # 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. # """ Replaces a subgraph in an onnx model with a plugin. """ from polygraphy import mod from polygraphy.logger import G_LOGGER from polygraphy.tools import Tool from polygraphy.tools.args import ( DataLoaderArgs, OnnxLoadArgs, ModelArgs, OnnxInferShapesArgs, ) import os # Your tool should lazily import any external dependencies. By doing so, # we avoid creating hard dependencies on other packages. # Additionally, this allows Polygraphy to automatically install required packages # as they are needed, instead of requiring the user to do so up front. gs = mod.lazy_import("onnx_graphsurgeon>=0.5.0") onnx = mod.lazy_import("onnx") yaml = mod.lazy_import("yaml", pkg_name="pyyaml") common_backend = mod.lazy_import("polygraphy.backend.common") def default_replace_with_plugin(graph, input_tensors: list, output_tensors: list, attrs=None, op=None): """ replaces a subgraph (set of nodes) with a single plugin node default method to be used when the plugin does not specify a custom replacement method """ def issubset_unhashable(list_a: list, list_b: list) -> bool: """ Return whether list_a is a subset (or equal to) list_b The objects in list_a and list_b are unhashable, otherwise set(list_a) <= set(list_b) is enough """ return len(list_a) <= len(list_b) and all(a in list_b for a in list_a) # Disconnect those output nodes of the input tensors whose inputs are a subset of the input tensors for in_tensor in input_tensors: to_remove_nodes = [] for out_node in in_tensor.outputs: if issubset_unhashable(out_node.inputs, input_tensors): to_remove_nodes.append(out_node) for node in to_remove_nodes: in_tensor.outputs.remove(node) # Disconnet input nodes of all output tensors for out_tensor in output_tensors: to_remove_nodes = [] for in_node in out_tensor.inputs: to_remove_nodes.append(in_node) for node in to_remove_nodes: out_tensor.inputs.remove(node) # Insert the new node new_node = graph.layer(op=op, inputs=input_tensors, outputs=output_tensors, attrs=attrs) graph.cleanup().toposort() return new_node[0].inputs[0] class Replace(Tool): # Polygraphy will use the docstring of the tool child class to generate # the summary for the command-line help output. """ Replace a subgraph in an onnx model with a plugin. """ GRAPH_PATTERN_FILE_NAME="pattern.py" def __init__(self): super().__init__(name="replace") def get_subscriptions_impl(self): return [ ModelArgs(model_opt_required=True), OnnxInferShapesArgs(), OnnxLoadArgs(), DataLoaderArgs(), ] def add_parser_args_impl(self, parser): parser.add_argument("--plugin-dir", help="Plugin directory.", required=True) parser.add_argument( "-o", "--output", help="Where to save the modified model", required=False ) parser.add_argument("--config", help="location of config.yaml.") def run_impl(self, args): self.replace_plugin( model_file=args.model_file, plugin_dir=args.plugin_dir, output=args.output, config=args.config ) def replace_plugin(self, model_file, plugin_dir, output=None, config=None): graph = gs.import_onnx(self.arg_groups[OnnxLoadArgs].load_onnx()) if self.arg_groups else gs.import_onnx(onnx.load(model_file)) tensor_map = graph.tensors() config_yaml = config or os.path.join(os.path.dirname(model_file), "config.yaml") plugin_dir = os.path.abspath(plugin_dir) with open(config_yaml, "r") as stream: in_yaml = yaml.safe_load_all(stream) for plugin in in_yaml: plugin_name = plugin["name"] plugin_op = plugin["op"] G_LOGGER.ultra_verbose(f"replacing {plugin_name}...") plugin_pattern_loc = os.path.join(plugin_dir, plugin_name, self.GRAPH_PATTERN_FILE_NAME) # if the plugin provides a custom replacement method, use that replace_fn = default_replace_with_plugin try: replace_fn = mod.import_from_script( plugin_pattern_loc, "replace_with_plugin" ) except: pass replace_cnt = 0 for instance in plugin["instances"]: attrs = instance.get("attributes", None) if replace_fn( graph=graph, input_tensors=[tensor_map[ip_tensor_name] for ip_tensor_name in instance["inputs"]], output_tensors=[tensor_map[op_tensor_name] for op_tensor_name in instance["outputs"]], attrs=attrs, op=plugin_op ): replace_cnt += 1 G_LOGGER.info(f"replaced {replace_cnt} instances of {plugin_name} plugin") if replace_cnt != len(plugin['instances']): G_LOGGER.warning(f"Warning: not all instances of {plugin_name} were replaced!") output_onnx = output or os.path.join(os.path.dirname(model_file), "replaced.onnx") onnx.save(gs.export_onnx(graph), output_onnx)