481 lines
17 KiB
Python
481 lines
17 KiB
Python
#
|
|
# 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 copy
|
|
from collections import OrderedDict
|
|
|
|
from polygraphy import mod, util
|
|
from polygraphy.common import TensorMetadata
|
|
from polygraphy.datatype import DataType
|
|
from polygraphy.logger import G_LOGGER, LogMode
|
|
|
|
gs = mod.lazy_import("onnx_graphsurgeon")
|
|
onnx = mod.lazy_import("onnx")
|
|
onnx_numpy_helper = mod.lazy_import("onnx.numpy_helper")
|
|
|
|
|
|
def get_num_nodes(model):
|
|
def _get_num_graph_nodes(graph):
|
|
num_nodes = len(graph.node)
|
|
for node in graph.node:
|
|
for attr in node.attribute:
|
|
if attr.type == onnx.AttributeProto.GRAPH:
|
|
num_nodes += _get_num_graph_nodes(attr.g)
|
|
elif attr.type == onnx.AttributeProto.GRAPHS:
|
|
for subgraph in attr.graphs:
|
|
num_nodes += _get_num_graph_nodes(subgraph)
|
|
return num_nodes
|
|
|
|
return _get_num_graph_nodes(model.graph)
|
|
|
|
|
|
def all_tensor_names(model, include_inputs=None):
|
|
include_inputs = util.default(include_inputs, False)
|
|
|
|
all_outputs = [
|
|
output
|
|
for node in model.graph.node
|
|
if node.op_type != "Constant"
|
|
for output in node.output
|
|
]
|
|
if include_inputs:
|
|
all_outputs += [inp.name for inp in model.graph.input]
|
|
all_outputs = util.unique_list(all_outputs)
|
|
return all_outputs
|
|
|
|
|
|
def _check_has_tensors(model, outputs):
|
|
all_outputs = all_tensor_names(model, include_inputs=True)
|
|
util.check_sequence_contains(
|
|
all_outputs, outputs, name="the model", items_name="outputs", check_extra=False
|
|
)
|
|
|
|
|
|
def mark_outputs(model, outputs):
|
|
# Clear the old outputs
|
|
while model.graph.output:
|
|
model.graph.output.pop()
|
|
|
|
outputs = util.unique_list(outputs)
|
|
_check_has_tensors(model, outputs)
|
|
|
|
value_info_map = {t.name: t for t in model.graph.value_info}
|
|
out_tensors = []
|
|
for output in outputs:
|
|
value_info = value_info_map.get(
|
|
output, onnx.helper.make_empty_tensor_value_info(output)
|
|
)
|
|
out_tensors.append(value_info)
|
|
|
|
G_LOGGER.ultra_verbose(f"Marked output tensors in ONNX model: {out_tensors}")
|
|
model.graph.output.extend(out_tensors)
|
|
return model
|
|
|
|
|
|
def mark_layerwise(model):
|
|
# Add all non-constant node outputs as graph outputs
|
|
model = mark_outputs(model, all_tensor_names(model))
|
|
return model
|
|
|
|
|
|
def unmark_outputs(model, outputs):
|
|
outputs = util.unique_list(outputs)
|
|
_check_has_tensors(model, outputs)
|
|
|
|
cur_outputs = []
|
|
while model.graph.output:
|
|
cur_outputs.append(model.graph.output.pop())
|
|
cur_outputs = list(reversed(cur_outputs)) # Preserve ordering
|
|
|
|
for out in cur_outputs:
|
|
if out.name not in outputs:
|
|
model.graph.output.extend([out])
|
|
|
|
return model
|
|
|
|
|
|
def get_shape(tensor):
|
|
shape = []
|
|
if isinstance(tensor, onnx.TensorProto):
|
|
shape = tensor.dims
|
|
else:
|
|
for dim in tensor.type.tensor_type.shape.dim:
|
|
if dim.HasField("dim_param"):
|
|
shape.append(dim.dim_param)
|
|
elif dim.HasField("dim_value"):
|
|
shape.append(dim.dim_value)
|
|
else:
|
|
shape.append(-1)
|
|
return shape
|
|
|
|
|
|
def get_dtype(tensor):
|
|
if isinstance(tensor, onnx.TensorProto):
|
|
onnx_type = tensor.data_type
|
|
else:
|
|
onnx_type = tensor.type.tensor_type.elem_type
|
|
return DataType.from_dtype(onnx_type, source_module="onnx")
|
|
|
|
|
|
def get_values(tensor):
|
|
try:
|
|
return onnx_numpy_helper.to_array(tensor)
|
|
except Exception as err:
|
|
G_LOGGER.error(
|
|
f"Failed to load weights.\nNote: Error was: {err}", mode=LogMode.ONCE
|
|
)
|
|
return "<error: failed to load weights>"
|
|
|
|
|
|
def get_tensor_metadata(tensors):
|
|
metadata = TensorMetadata()
|
|
for tensor in tensors:
|
|
metadata.add(name=tensor.name, dtype=get_dtype(tensor), shape=get_shape(tensor))
|
|
return metadata
|
|
|
|
|
|
def get_input_metadata(graph):
|
|
# Some "inputs" are actually weights with initalizers, so we need to eliminate those.
|
|
initializer_names = {tensor.name for tensor in graph.initializer}
|
|
input_tensors = [
|
|
tensor for tensor in graph.input if tensor.name not in initializer_names
|
|
]
|
|
return get_tensor_metadata(input_tensors)
|
|
|
|
|
|
def get_output_metadata(graph):
|
|
return get_tensor_metadata(graph.output)
|
|
|
|
|
|
def str_from_onnx(model, show_layers=None, show_attrs=None, show_weights=None):
|
|
"""
|
|
Converts an ONNX Graph to a human-readable representation
|
|
|
|
Args:
|
|
graph (onnx.GraphProto): The onnx graph.
|
|
show_layers (bool): Whether to display per-layer information.
|
|
show_attrs (bool): Whether to display per-layer attributes.
|
|
show_weights (bool): Whether to display the value of weights.
|
|
|
|
Returns:
|
|
str
|
|
"""
|
|
show_layers = util.default(show_layers, False)
|
|
show_attrs = util.default(show_attrs, False)
|
|
show_weights = util.default(show_weights, False)
|
|
|
|
def get_opset():
|
|
default_opset = "Unknown"
|
|
other_opsets = {}
|
|
for info in model.opset_import:
|
|
if not info.domain:
|
|
default_opset = info.version
|
|
else:
|
|
other_opsets[info.domain] = info.version
|
|
return default_opset, other_opsets
|
|
|
|
default_opset, other_opsets = get_opset()
|
|
onnx_str = ""
|
|
onnx_str += f"Name: {model.graph.name} | ONNX Opset: {default_opset}"
|
|
if other_opsets:
|
|
onnx_str += f" | Other Opsets: {other_opsets}"
|
|
onnx_str += "\n\n"
|
|
|
|
onnx_str += str_from_onnx_graph(
|
|
model.graph,
|
|
tensors={},
|
|
show_layers=show_layers,
|
|
show_attrs=show_attrs,
|
|
show_weights=show_weights,
|
|
)
|
|
return onnx_str
|
|
|
|
|
|
def str_from_onnx_graph(
|
|
graph, tensors, show_layers, show_attrs, show_weights, indent_level=0
|
|
):
|
|
input_metadata = get_input_metadata(graph)
|
|
output_metadata = get_output_metadata(graph)
|
|
initializer_metadata = get_tensor_metadata(graph.initializer)
|
|
|
|
# Subgraph inputs should remain separate from each other, hence copy the tensors map
|
|
tensors = copy.copy(tensors)
|
|
tensors.update(get_tensor_metadata(graph.value_info))
|
|
tensors.update(initializer_metadata)
|
|
tensors.update(input_metadata)
|
|
tensors.update(output_metadata)
|
|
|
|
graph_type = "Graph" if indent_level == 0 else "Subgraph"
|
|
|
|
onnx_str = ""
|
|
if show_attrs and graph.doc_string:
|
|
onnx_str += f"---- Docstring ----\n{graph.doc_string}\n\n"
|
|
|
|
onnx_str += (
|
|
f"---- {len(input_metadata)} {graph_type} Input(s) ----\n{input_metadata}\n\n"
|
|
)
|
|
onnx_str += f"---- {len(output_metadata)} {graph_type} Output(s) ----\n{output_metadata}\n\n"
|
|
|
|
onnx_str += f"---- {len(initializer_metadata)} Initializer(s) ----\n"
|
|
if show_weights:
|
|
for init in graph.initializer:
|
|
onnx_str += f"Initializer | {init.name} [dtype={get_dtype(init)}, shape={get_shape(init)}] | Values:\n{util.indent_block(str(get_values(init)))}\n\n"
|
|
if not graph.initializer:
|
|
onnx_str += "{}\n\n"
|
|
elif show_layers:
|
|
onnx_str += str(initializer_metadata)
|
|
onnx_str += "\n\n"
|
|
else:
|
|
onnx_str += "\n"
|
|
|
|
def get_names_and_meta(names):
|
|
names_lst = []
|
|
metadata = TensorMetadata()
|
|
for name in names:
|
|
dtype, shape = tensors.get(name, (None, None))
|
|
if name in initializer_metadata:
|
|
name = f"Initializer | {name}"
|
|
names_lst.append(name)
|
|
metadata.add(name=name, dtype=dtype, shape=shape)
|
|
return names_lst, metadata
|
|
|
|
# Maps values from the AttributeType enum to their string representations, e.g., {1: "FLOAT"}
|
|
ATTR_TYPE_MAPPING = dict(
|
|
zip(
|
|
onnx.AttributeProto.AttributeType.values(),
|
|
onnx.AttributeProto.AttributeType.keys(),
|
|
)
|
|
)
|
|
|
|
# Maps an ONNX attribute to the corresponding Python property
|
|
ONNX_PYTHON_ATTR_MAPPING = {
|
|
"FLOAT": "f",
|
|
"INT": "i",
|
|
"STRING": "s",
|
|
"TENSOR": "t",
|
|
"GRAPH": "g",
|
|
"FLOATS": "floats",
|
|
"INTS": "ints",
|
|
"STRINGS": "strings",
|
|
}
|
|
|
|
def attrs_to_dict(attrs):
|
|
attr_dict = OrderedDict()
|
|
for attr in attrs:
|
|
|
|
def process_attr(attr_str: str):
|
|
processed = getattr(attr, ONNX_PYTHON_ATTR_MAPPING[attr_str])
|
|
if attr_str == "STRING":
|
|
processed = processed.decode()
|
|
elif attr_str == "TENSOR":
|
|
tensor_str = f"Tensor: [dtype={get_dtype(processed)}, shape={get_shape(processed)}]"
|
|
if show_weights:
|
|
tensor_str += " | Values:\n" + util.indent_block(
|
|
str(get_values(processed))
|
|
)
|
|
processed = tensor_str
|
|
elif attr_str == "GRAPH":
|
|
processed = "\n" + str_from_onnx_graph(
|
|
processed,
|
|
tensors,
|
|
indent_level=indent_level + 2,
|
|
show_layers=show_layers,
|
|
show_attrs=show_attrs,
|
|
show_weights=show_weights,
|
|
)
|
|
elif attr_str == "FLOATS" or attr_str == "INTS":
|
|
# Proto hacky list to normal Python list
|
|
processed = [p for p in processed]
|
|
elif attr_str == "STRINGS":
|
|
processed = [p.decode() for p in processed]
|
|
return processed
|
|
|
|
if attr.type in ATTR_TYPE_MAPPING:
|
|
attr_str = ATTR_TYPE_MAPPING[attr.type]
|
|
if attr_str in ONNX_PYTHON_ATTR_MAPPING:
|
|
attr_dict[attr.name] = process_attr(attr_str)
|
|
else:
|
|
G_LOGGER.warning(
|
|
f"Attribute of type {attr_str} is currently unsupported. Skipping attribute."
|
|
)
|
|
else:
|
|
G_LOGGER.warning(
|
|
f"Attribute type: {attr.type} was not recognized. Was the graph generated with a newer IR version than the installed `onnx` package? Skipping attribute."
|
|
)
|
|
return attr_dict
|
|
|
|
onnx_str += f"---- {len(graph.node)} Node(s) ----\n"
|
|
if show_layers:
|
|
for index, node in enumerate(graph.node):
|
|
input_names, input_meta = get_names_and_meta(node.input)
|
|
output_names, output_meta = get_names_and_meta(node.output)
|
|
|
|
onnx_str += util.str_from_layer(
|
|
"Node",
|
|
index,
|
|
node.name,
|
|
node.op_type,
|
|
input_names,
|
|
input_meta,
|
|
output_names,
|
|
output_meta,
|
|
)
|
|
|
|
if show_attrs:
|
|
attrs = attrs_to_dict(node.attribute)
|
|
if attrs:
|
|
onnx_str += util.indent_block("---- Attributes ----") + "\n"
|
|
for key, val in attrs.items():
|
|
attr_str = ""
|
|
if node.name:
|
|
attr_str += f"{node.name}."
|
|
onnx_str += util.indent_block(f"{attr_str}{key} = {val}") + "\n"
|
|
onnx_str += "\n"
|
|
|
|
return util.indent_block(onnx_str, indent_level)
|
|
|
|
|
|
##
|
|
## ONNX-GraphSurgeon utilities
|
|
##
|
|
|
|
|
|
def meta_from_gs_tensors(tensors):
|
|
"""Get TensorMetadata from a list of ONNX-GraphSurgeon tensors"""
|
|
meta = TensorMetadata()
|
|
for tensor in tensors:
|
|
meta.add(tensor.name, tensor.dtype, tensor.shape)
|
|
return meta
|
|
|
|
|
|
def set_shapes_from_layerwise_meta(graph, layerwise_meta):
|
|
"""
|
|
Args:
|
|
graph (gs.Graph): An ONNX graphsurgeon graph.
|
|
layerwise_meta (TensorMetadata): Metadata for tensors in the graph.
|
|
"""
|
|
for tensor in graph.tensors().values():
|
|
if isinstance(tensor, gs.Variable) and tensor.name in layerwise_meta:
|
|
tensor.shape = layerwise_meta[tensor.name].shape
|
|
tensor.dtype = DataType.to_dtype(
|
|
DataType.from_dtype(layerwise_meta[tensor.name].dtype), "onnx"
|
|
)
|
|
|
|
|
|
def lower_constant_nodes(graph):
|
|
"""Converts the outputs of Constant nodes into constant tensors, removing the nodes"""
|
|
remove_nodes = set()
|
|
with graph.node_ids():
|
|
for node in graph.nodes:
|
|
if node.op == "Constant" and "value" in node.attrs:
|
|
node.outputs[0].to_constant(node.attrs["value"].values)
|
|
remove_nodes.add(node.id)
|
|
# Iterate from the end so we don't shift the list under us.
|
|
for node_id in sorted(remove_nodes, reverse=True):
|
|
del graph.nodes[node_id]
|
|
return graph
|
|
|
|
|
|
def get_unbounded_dds_tensors(graph):
|
|
graph.toposort()
|
|
# A dict of operators that might produce a output tensor with unbounded DDS, when the value of the input tensor
|
|
# at the corresponding index is a runtime value. For example, "Range" => "1" means that if the input 1 of the Range
|
|
# operator is a runtime value, e.g. not a const tensor or an initializer, then the Range output tensor size is unbounded.
|
|
dispatcher_dict = {
|
|
"Range": [1], # the limit input of the Range operator
|
|
"Pad": [1], # the pads input of the Pad operator
|
|
"Resize": [3], # the sizes input of the Resize operator
|
|
"Tile": [1], # the repeats input of the Tile operator
|
|
"Expand": [1], # the shape input of the Expand operator
|
|
}
|
|
|
|
# Check if the given operator produces a output tensor with unbounded DDS.
|
|
def check_op(node, const_tensor_set):
|
|
# Check if the operator is inside the dispatcher dict.
|
|
if node.op in dispatcher_dict:
|
|
input_idx_list = dispatcher_dict[node.op]
|
|
for input_idx in input_idx_list:
|
|
if input_idx < len(node.inputs):
|
|
input_tensor = node.inputs[input_idx]
|
|
# Check if the corresponding input tensor is a runtime value and its producer is not Min operator.
|
|
# If a tensor is produced by a Min operator, its upper bound has already been set.
|
|
if (
|
|
input_tensor.name not in const_tensor_set
|
|
and len(input_tensor.inputs) >= 1
|
|
and input_tensor.inputs[0].op != "Min"
|
|
):
|
|
return input_tensor
|
|
return None
|
|
|
|
# Find all constant tensors.
|
|
def get_const_tensors(graph):
|
|
return {
|
|
tensor.name
|
|
for tensor in graph.tensors().values()
|
|
if isinstance(tensor, gs.Constant)
|
|
}
|
|
|
|
# Find all dynamic shape symbols, customers will set upper bounds for these symbols when building the model in TensorRT.
|
|
def get_dynamic_shapes(graph):
|
|
dynamic_shape_set = set()
|
|
for tensor in graph.inputs:
|
|
for shape in tensor.shape:
|
|
if isinstance(shape, str):
|
|
dynamic_shape_set.add(shape)
|
|
return dynamic_shape_set
|
|
|
|
# Find all tensors with unbounded DDS.
|
|
def get_target_tensors(graph):
|
|
# Find dynamic shapes, these shapes should have upper bounds in TensorRT.
|
|
dynamic_shape_set = get_dynamic_shapes(graph)
|
|
|
|
# Find const tensors. For those operators in the dispatch dict, constant inputs will not introduce outputs with unbounded DDS.
|
|
const_tensor_set = get_const_tensors(graph)
|
|
|
|
# Our target is to find those input tensors that cause its consumer nodes generated unbounded outputs.
|
|
# If a tensor has named dimensions that appeared before in its symbolic shape, it means that the shape is *not* data dependent,
|
|
# and so will have an upper bound.
|
|
target_tensor_names = set()
|
|
target_tensor_list = []
|
|
for node in graph.nodes:
|
|
check_node = False
|
|
# Check if the node's output contains a new introduced dynamic shape.
|
|
for tensor in node.outputs:
|
|
# Always check nodes if tensor.shape is None.
|
|
# This happens when the symbolic inference does not work correctly due to some restrictions.
|
|
if tensor.shape is None:
|
|
check_node = True
|
|
else:
|
|
for shape in tensor.shape:
|
|
# If a shape is a dynamic shape, then it is a str.
|
|
# Only check the node that first introduced the dynamic shape.
|
|
if isinstance(shape, str) and shape not in dynamic_shape_set:
|
|
dynamic_shape_set.add(shape)
|
|
check_node = True
|
|
# Check if the node will generate an unbounded output size.
|
|
if check_node:
|
|
target_tensor = check_op(node, const_tensor_set)
|
|
# Avoid duplication.
|
|
if (
|
|
target_tensor is not None
|
|
and target_tensor.name not in target_tensor_names
|
|
):
|
|
target_tensor_names.add(target_tensor.name)
|
|
target_tensor_list.append(target_tensor)
|
|
return target_tensor_list
|
|
|
|
return get_target_tensors(graph)
|