Files
wehub-resource-sync c8a779b1bb
Docker Image CI / build-ubuntu2004 (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:36:55 +08:00

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)