onnxruntime/tools/python/util/onnx_model_utils.py
Chi Lo b713855a98
Release 1.11.0 cherry pick round 1 (#10915)
* Update to flatbuffers v2.0.0 (#10866)

* Fix Reduced ops pipeline (#10861)

* Fix a couple of issues with the python package tools (#10858)

* Tweaks to the model utils
  * Add handling for a dim_value of -1 when replacing the entire input shape. This occurs in models exported from PaddlePaddle
  * make pytorch helpers accessible in package
  * make QDQ helpers accessible in package

* Fix wrong percentile values returned during calibration (#10847)

* Use numpy.percentile to get the lookup value.

* Use 1.0 as float value rather than integer.

* Add missing cdf parameter for `np.percentile`.

* Use 100. instead of 1.0

* Remove print.

* Update from @yufenglee

* Add support for opset 16 to transpose optimizer. (#10841)

* Add support for opset 16 to transpose optimizer.

Only change required is for GridSample to be added to the layout sensitive ops. The existing handling for layout transpose works with that as the first input and first output are layout sensitive.

Update the optimize to be able to return an error message if it fails.

* Use separate build directories for full and mobile iOS packages. (#10835)

* Address performance issue with abseil flat_hash_table. (#10819)

When returning by value in a cross DLL call, the hash table
even though containing all the entries that are originally there
can not find at least some of them. Reverting to std::unordered_set
pending further investigation.

* Mark end of version 11 C API. (#10803)

* Mark end of version 11 C API

* Add static_assert

* avoid using LocalFree on FormatMessageW buffer (#10796)

* remove local free

* Remove local free from onnxruntime

* don't allocate

* Change to use constexpr to satisfy  CPU build warning

* Integrate C-API tests into Pipelines for release packages (#10794)

* add c-api test for package

* fix bug for running c-api test for package

* refine run application script

* remove redundant code

* include CUDA test

* Remove testing CUDA EP temporarily

* fix bug

* Code refactor

* try to fix YAML bug

* try to fix YAML bug

* try to fix YAML bug

* fix bug for multiple directories in Pipelines

* fix bug

* add comments and fix bug

* Update c-api-noopenmp-packaging-pipelines.yml

* Remove failOnStandardError flag in Pipelines

* Detect runtime CUDA JIT and warn the user (#10781)

* Use cudaMalloc vs cudaDeviceSynchronize and show the total time

* Update convert_onnx_models_to_ort.py to support runtime optimizations. (#10765)

Add runtime optimization support to ONNX -> ORT format conversion script.
Replace `--optimization_level`, `--use_nnapi`, and `--use_coreml` with a new `--optimization_style` option.

* Add multithreading test and put a lock on nvinfer1::createInferRuntime() for TRT EP (#10714)

* Add multithread unit test and put lock on library call

* update code

* remove debug code

* add comment

* add one session multi-threads inference

* Put lock for build engine all the time

* Update naming and comment

* remove unnecessary lock

* Revert "remove unnecessary lock"

This reverts commit 9c2317b1d2273dec0ebdeb52160bc757839e5edc.

* Fix handling of nodes inserted by NHWC transformer. (#10904) (#10925)

* Revert "Upsample support NHWC (#10554)" (#10917)

This reverts commit bd08f11a58.

Co-authored-by: Yufeng Li <liyufeng1987@gmail.com>

* [python API] Change raise import error when `C:\Windows\System32\vcruntime140_1.dll` is not found to warning (#10927)

* remove throw if C:\\Windows\\System32\\vcruntime140_1.dll cannot be found

* Add comments and update warning message

* adding back accidentally removed line

Co-authored-by: gwang0000 <62914304+gwang0000@users.noreply.github.com>

* [js] Create npm packaging pipeline (#10886)

* create npm packaging pipeline

* fix indentations

* Update npm-packaging-pipeline.yml for Azure Pipelines

* Update npm-packaging-pipeline.yml for Azure Pipelines

* Update npm-packaging-pipeline.yml for Azure Pipelines

* react-native-ci as a template

* fix typos

* fix template paths

* add a depencendy

* change a stage name

* set different artifact name for each package

* fix typo

* Update npm-packaging-pipeline.yml for Azure Pipelines

Set a build Id for node npm package as a parameter

* Update npm-packaging-pipeline.yml for Azure Pipelines

Set a build Id for node npm package as a parameter

* Update npm-packaging-pipeline.yml for Azure Pipelines

* Follow up update for python API checking if `vcruntime140_1.dll` is available (#10927) (#10933)

Co-authored-by: Hariharan Seshadri <hasesh@microsoft.com>
Co-authored-by: Scott McKay <skottmckay@gmail.com>
Co-authored-by: Funtowicz Morgan <mfuntowicz@users.noreply.github.com>
Co-authored-by: Edward Chen <18449977+edgchen1@users.noreply.github.com>
Co-authored-by: Dmitri Smirnov <yuslepukhin@users.noreply.github.com>
Co-authored-by: Pranav Sharma <prs@microsoft.com>
Co-authored-by: Ryan Lai <rylai@microsoft.com>
Co-authored-by: Ryan Hill <38674843+RyanUnderhill@users.noreply.github.com>
Co-authored-by: Yi-Hong Lyu <yilyu@microsoft.com>
Co-authored-by: Yufeng Li <liyufeng1987@gmail.com>
Co-authored-by: Guoyu Wang <62914304+gwang-msft@users.noreply.github.com>
Co-authored-by: gwang0000 <62914304+gwang0000@users.noreply.github.com>
Co-authored-by: Sunghoon <35605090+hanbitmyths@users.noreply.github.com>
2022-03-18 11:16:30 -07:00

356 lines
14 KiB
Python

# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.
import logging
import onnx
import onnxruntime as ort
import pathlib
from onnx import version_converter
def iterate_graph_per_node_func(graph, per_node_func, **func_args):
'''
Iterate the graph including subgraphs calling the per_node_func for each node.
:param graph: Graph to iterate
:param per_node_func: Function to call for each node. Signature is fn(node: onnx:NodeProto, **kwargs)
:param func_args: The keyword args to pass through.
'''
for node in graph.node:
per_node_func(node, **func_args)
# recurse into subgraph for control flow nodes (Scan/Loop/If)
for attr in node.attribute:
if attr.HasField('g'):
iterate_graph_per_node_func(attr.g, per_node_func, **func_args)
def iterate_graph_per_graph_func(graph, per_graph_func, **func_args):
'''
Iterate the graph including subgraphs calling the per_graph_func for each Graph.
:param graph: Graph to iterate
:param per_graph_func: Function to call for each graph. Signature is fn(graph: onnx:GraphProto, **kwargs)
:param func_args: The keyword args to pass through.
'''
per_graph_func(graph, **func_args)
for node in graph.node:
# recurse into subgraph for control flow nodes (Scan/Loop/If)
for attr in node.attribute:
if attr.HasField('g'):
iterate_graph_per_graph_func(attr.g, per_graph_func, **func_args)
def get_opsets_imported(model: onnx.ModelProto):
'''
Get the opsets imported by the model
:param model: Model to check.
:return: Map of domain to opset.
'''
opsets = {}
for entry in model.opset_import:
# if empty it's ai.onnx
domain = entry.domain or 'ai.onnx'
opsets[domain] = entry.version
return opsets
def update_onnx_opset(model_path: pathlib.Path, opset: int, out_path: pathlib.Path = None,
logger: logging.Logger = None):
"""
Helper to update the opset of a model using onnx version_converter. Target opset must be greater than current opset.
:param model_path: Path to model to update
:param opset: Opset to update model to
:param out_path: Optional output path for updated model to be saved to.
:param logger: Optional logger for diagnostic output
:returns: Updated onnx.ModelProto
"""
model_path_str = str(model_path.resolve(strict=True))
if logger:
logger.info("Updating %s to opset %d", model_path_str, opset)
model = onnx.load(model_path_str)
new_model = version_converter.convert_version(model, opset)
if out_path:
onnx.save(new_model, str(out_path))
if logger:
logger.info("Saved updated model to %s", out_path)
return new_model
def optimize_model(model_path: pathlib.Path,
output_path: pathlib.Path,
level: ort.GraphOptimizationLevel = ort.GraphOptimizationLevel.ORT_ENABLE_BASIC,
log_level: int = 3):
'''
Optimize an ONNX model using ONNX Runtime to the specified level
:param model_path: Path to ONNX model
:param output_path: Path to save optimized model to.
:param level: onnxruntime.GraphOptimizationLevel to use. Default is ORT_ENABLE_BASIC.
:param log_level: Log level. Defaults to Error (3) so we don't get output about unused initializers being removed.
Warning (2) or Info (1) may be desirable in some scenarios.
'''
so = ort.SessionOptions()
so.optimized_model_filepath = str(output_path.resolve())
so.graph_optimization_level = level
so.log_severity_level = log_level
# create session to optimize. this will write the updated model to output_path
_ = ort.InferenceSession(str(model_path.resolve(strict=True)), so, providers=['CPUExecutionProvider'])
def _replace_symbolic_dim_value(graph: onnx.GraphProto, **kwargs):
param_to_replace = kwargs['dim_param']
value = kwargs['value']
def update_dim_values(value_infos):
for vi in value_infos:
if vi.type.HasField("tensor_type"):
shape = vi.type.tensor_type.shape
if shape:
for dim in shape.dim:
if dim.HasField('dim_param') and dim.dim_param == param_to_replace:
dim.Clear()
dim.dim_value = value
update_dim_values(graph.input)
update_dim_values(graph.output)
update_dim_values(graph.value_info)
def _remove_invalid_dim_values_impl(graph: onnx.GraphProto):
def clear_invalid_values(value):
if value.type.HasField("tensor_type"):
shape = value.type.tensor_type.shape
if shape:
for dim in shape.dim:
if dim.HasField('dim_value') and dim.dim_value < 1:
dim.Clear()
for i in graph.input:
clear_invalid_values(i)
for o in graph.output:
clear_invalid_values(o)
for vi in graph.value_info:
clear_invalid_values(vi)
def remove_invalid_dim_values(graph: onnx.GraphProto):
'''
Iterate the graph and subgraphs, unsetting any dim_value entries that have a value of less than 1.
These are typically erroneously inserted by a converter to represent a dynamic dimension.
:param graph: GraphProto to update
'''
iterate_graph_per_graph_func(graph, _remove_invalid_dim_values_impl)
def make_dim_param_fixed(graph: onnx.GraphProto, param_name: str, value: int):
'''
Iterate all values in the graph, replacing dim_param in a tensor shape with the provided value.
:param graph: GraphProto to update
:param param_name: dim_param to set
:param value: value to use
'''
iterate_graph_per_graph_func(graph, _replace_symbolic_dim_value, dim_param=param_name, value=value)
def make_input_shape_fixed(graph: onnx.GraphProto, input_name: str, fixed_shape: [int]):
'''
Update the named graph input to set shape to the provided value. This can be used to set unknown dims as well
as to replace dim values.
If setting the input shape replaces a dim_param, update any other values in the graph that use the dim_param.
:param graph: Graph to update
:param input_name: Name of graph input to update.
:param fixed_shape: Shape to use.
'''
# remove any invalid dim values first. typically this is a dim_value of -1.
remove_invalid_dim_values(graph)
for i in graph.input:
if i.name == input_name:
if not i.type.HasField("tensor_type"):
raise ValueError(f'Input {input_name} is not a tensor')
# graph inputs are required to have a shape to provide the rank
shape = i.type.tensor_type.shape
if len(shape.dim) != len(fixed_shape):
raise ValueError(
f'Rank mismatch. Existing:{len(shape.dim)} Replacement:{len(fixed_shape)}')
for idx, dim in enumerate(shape.dim):
# check any existing fixed dims match
if dim.HasField('dim_value'):
if dim.dim_value != fixed_shape[idx]:
raise ValueError(
f"Can't replace existing fixed size of {dim.dim_value} with {fixed_shape[idx]} "
f"for dimension {idx + 1}")
elif dim.HasField('dim_param'):
# replacing a dim_param so have to do that through the entire graph
make_dim_param_fixed(graph, dim.dim_param, fixed_shape[idx])
else:
# replacing an unknown dim
dim.Clear()
dim.dim_value = fixed_shape[idx]
return
raise ValueError(f'Input {input_name} was not found in graph inputs. '
f'Valid input names are: {",".join([i.name for i in graph.input])}')
def fix_output_shapes(model: onnx.ModelProto):
'''
Update the output shapesof a model where the input shape/s were made fixed, if possible.
This is mainly to make the model usage clearer if the output shapes can be inferred from the new input shapes.
:param model: Model that had input shapes fixed.
'''
# get a version of the model with shape inferencing info in it. this will provide fixed output shapes if possible.
m2 = onnx.shape_inference.infer_shapes(model)
onnx.checker.check_model(m2)
for idx, o in enumerate(model.graph.output):
if not is_fixed_size_tensor(o):
new_o = m2.graph.output[idx]
if is_fixed_size_tensor(new_o):
o.type.tensor_type.shape.CopyFrom(new_o.type.tensor_type.shape)
def _create_producer_consumer_link(node_to_producers: dict, node_to_consumers: dict,
producer: onnx.NodeProto, consumer: onnx.NodeProto):
'''
Create links between two nodes for a value produced by one and consumed by the other.
:param node_to_producers: Map of NodeProto to set of nodes that produce values the node consumes as inputs.
:param node_to_consumers: Map of NodeProto to set of nodes that consume values the node produces as outputs.
:param producer: Producer node
:param consumer: Consumer node
'''
if consumer not in node_to_producers:
node_to_producers[consumer] = set()
if producer not in node_to_consumers:
node_to_consumers[producer] = set()
# add entry mapping this node to the producer of this input
node_to_producers[consumer].add(producer)
node_to_consumers[producer].add(consumer)
def _map_node_dependencies(graph: onnx.GraphProto, node_to_producers: dict, node_to_consumers: dict):
graph_inputs = set([i.name for i in graph.input])
initializers = set([i.name for i in graph.initializer])
# map of value name to node that creates it. copy parent values but override if values get shadowed
producers = {}
implicit_inputs = set()
def is_local_value(value):
return value in producers or value in initializers or value in graph_inputs
for node in graph.node:
inputs = [i for i in node.input]
for attr in node.attribute:
if attr.HasField('g'):
subgraph_implicit_inputs = _map_node_dependencies(attr.g, node_to_producers, node_to_consumers)
inputs += subgraph_implicit_inputs
for i in inputs:
if not i:
# missing optional input
continue
if is_local_value(i):
if i in producers:
producer = producers[i]
_create_producer_consumer_link(node_to_producers, node_to_consumers, producer, node)
else:
implicit_inputs.add(i)
for o in node.output:
producers[o] = node
return implicit_inputs
def get_producer_consumer_maps(graph: onnx.GraphProto):
'''
Get maps for connections between the node that produces each value and the nodes that consume the value.
Processing includes subgraphs. As the map key is a Node instance from the Graph there should be no ambiguity.
:param graph: Graph to process.
:return: Tuple with two maps.
First is node_to_producers map of a node to set of all nodes producing input it consumes.
Second is node_to_consumers map of a node to set of all nodes consuming output it creates.
e.g. NodeA and NodeB provide inputs to NodeC. NodeC provides input to NodeD
node_to_consumers[NodeA] = set([NodeC])
node_to_consumers[NodeB] = set([NodeC])
node_to_producers[NodeC] = set([NodeA, NodeB])
node_to_consumers[NodeC] = set([NodeD])
node_to_producers[NodeD] = set([NodeC])
'''
# use a hash of the object id for NodeProto.
# we need this for the partitioning checker where we keep maps with nodes as the key.
onnx.NodeProto.__hash__ = lambda self: id(self)
node_to_producers = {} # map of node instance to nodes producing input values it consumes
node_to_consumers = {} # map of node instance to nodes consuming output values it produces
implicit_inputs = _map_node_dependencies(graph, node_to_producers, node_to_consumers)
# top level graph should have no implicit inputs
if implicit_inputs:
raise ValueError('This appears to be an invalid model with missing inputs of '
f'{",".join(sorted(implicit_inputs))}')
return node_to_producers, node_to_consumers
def is_fixed_size_tensor(value: onnx.ValueInfoProto):
'''
Check if value is a tensor with a fixed shape.
:param value: onnx.ValueInfoProto to check
:return: True if value is a tensor, with a shape, where all dimensions have fixed values.
'''
is_fixed = False
if value.type.HasField("tensor_type"):
shape = value.type.tensor_type.shape
if shape:
is_fixed = True # scalar has no dims so set to True and unset if we hit a dim without a valid value
for dim in shape.dim:
if dim.HasField('dim_value') and dim.dim_value > 0:
continue
# anything else means it's a dynamic value
is_fixed = False
break
return is_fixed
def get_optimization_level(level):
'''Convert string to GraphOptimizationLevel.'''
if level == 'disable':
return ort.GraphOptimizationLevel.ORT_DISABLE_ALL
if level == 'basic':
# Constant folding and other optimizations that only use ONNX operators
return ort.GraphOptimizationLevel.ORT_ENABLE_BASIC
if level == 'extended':
# Optimizations using custom operators, excluding NCHWc and NHWC layout optimizers
return ort.GraphOptimizationLevel.ORT_ENABLE_EXTENDED
if level == 'all':
return ort.GraphOptimizationLevel.ORT_ENABLE_ALL
raise ValueError('Invalid optimization level of ' + level)