chore: import upstream snapshot with attribution
Docker Image CI / build-ubuntu2004 (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 13:36:55 +08:00
commit c8a779b1bb
1887 changed files with 3245738 additions and 0 deletions
@@ -0,0 +1,2 @@
from polygraphy.backend.base.runner import *
from polygraphy.backend.base.loader import *
@@ -0,0 +1,40 @@
#
# 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.
#
from polygraphy import func, mod
@mod.export()
class BaseLoader:
"""
Base class for Polygraphy Loaders.
"""
# Implementation for ``__call__``. Derived classes should implement this
# method rather than ``__call__``.
def call_impl(self, *args, **kwargs):
raise NotImplementedError("BaseLoader is an abstract class")
@func.constantmethod
def __call__(self, *args, **kwargs):
"""
Invokes the loader by forwarding arguments to ``call_impl``.
Note: ``call_impl`` should *not* be called directly - use this function instead.
"""
__doc__ = self.call_impl.__doc__
return self.call_impl(*args, **kwargs)
@@ -0,0 +1,258 @@
#
# 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
import time
from collections import defaultdict
from polygraphy import config, func, mod, util
from polygraphy.datatype import DataType
from polygraphy.logger import G_LOGGER, LogMode
from polygraphy.backend.base import util as base_util
np = mod.lazy_import("numpy")
@mod.export()
class BaseRunner:
"""
Base class for Polygraphy runners. All runners should override the functions and attributes specified here.
"""
RUNNER_COUNTS = defaultdict(int)
def __init__(self, name=None, prefix=None):
"""
Args:
name (str):
The name to use for this runner.
prefix (str):
The human-readable name prefix to use for this runner.
A runner count and timestamp will be appended to this prefix.
Only used if name is not provided.
"""
prefix = util.default(prefix, "Runner")
if name is None:
count = BaseRunner.RUNNER_COUNTS[prefix]
BaseRunner.RUNNER_COUNTS[prefix] += 1
name = f"{prefix}-N{count}-{time.strftime('%x')}-{time.strftime('%X')}"
self.name = name
self.inference_time = None
self.is_active = False
"""bool: Whether this runner has been activated, either via context manager, or by calling ``activate()``."""
def __enter__(self):
"""
Activate the runner for inference. For example, this may involve allocating CPU or GPU memory.
"""
self.activate()
return self
def __exit__(self, exc_type, exc_value, traceback):
"""
Deactivate the runner. For example, this may involve freeing CPU or GPU memory.
"""
self.deactivate()
# Implementation for runner activation. Derived classes should override this function
# rather than ``activate()``.
def activate_impl(self):
pass
def activate(self):
"""
Activate the runner for inference. For example, this may involve allocating CPU or GPU memory.
Generally, you should use a context manager instead of manually activating and deactivating.
For example:
::
with RunnerType(...) as runner:
runner.infer(...)
"""
if self.is_active:
G_LOGGER.warning(
f"{self.name:35} | Already active; will not activate again. "
"If you really want to activate this runner again, call activate_impl() directly"
)
return
if config.INTERNAL_CORRECTNESS_CHECKS:
self._pre_activate_runner_state = copy.copy(vars(self))
self.activate_impl()
self.is_active = True
def get_input_metadata_impl(self):
"""
Implemenation for `get_input_metadata`. Derived classes should override this function
rather than `get_input_metadata`.
Derived classes may return any kind of data type supported by Polygraphy's DataType
class (e.g. np.dtype, torch.dtype, etc.)
"""
raise NotImplementedError("BaseRunner is an abstract class")
@func.constantmethod
def get_input_metadata(self, use_numpy_dtypes=None):
"""
Returns information about the inputs of the model.
Shapes here may include dynamic dimensions, represented by ``None``.
Must be called only after ``activate()`` and before ``deactivate()``.
Args:
use_numpy_dtypes (bool):
[DEPRECATED] Whether to return NumPy data types instead of Polygraphy ``DataType`` s.
This is provided to retain backwards compatibility. In the future,
this parameter will be removed and Polygraphy ``DataType`` s will
always be returned. These can be converted to NumPy data types by calling the `numpy()` method.
Defaults to True.
Returns:
TensorMetadata: Input names, shapes, and data types.
"""
if not self.is_active:
G_LOGGER.critical(
f"{self.name:35} | Must be activated prior to calling get_input_metadata()"
)
use_numpy_dtypes = util.default(use_numpy_dtypes, True)
meta = self.get_input_metadata_impl()
for name, (dtype, _) in meta.items():
dtype = DataType.from_dtype(dtype)
if use_numpy_dtypes:
mod.warn_deprecated(
"Returning NumPy data types instead of Polygraphy `DataType`s from `get_input_metadata()`",
use_instead=None,
remove_in="0.60.0",
)
meta[name]._dtype = DataType.to_dtype(dtype, "numpy")
return meta
# Implementation for runner inference. Derived classes should override this function
# rather than ``infer()``
# Derived classes should also set the `inference_time` property so that performance metrics are accurate.
def infer_impl(self, feed_dict):
raise NotImplementedError("BaseRunner is an abstract class")
def infer(self, feed_dict, check_inputs=True, *args, **kwargs):
"""
Runs inference using the provided feed_dict.
Must be called only after ``activate()`` and before ``deactivate()``.
NOTE: Some runners may accept additional parameters in infer().
For details on these, see the documentation for their `infer_impl()` methods.
Args:
feed_dict (OrderedDict[str, numpy.ndarray]):
A mapping of input tensor names to corresponding input NumPy arrays.
check_inputs (bool):
Whether to check that the provided ``feed_dict`` includes the expected inputs
with the expected data types and shapes.
Disabling this may improve performance.
Defaults to True.
Attributes:
inference_time (float):
The time required to run inference in seconds.
Returns:
OrderedDict[str, numpy.ndarray]:
A mapping of output tensor names to their corresponding NumPy arrays.
IMPORTANT: Runners may reuse these output buffers. Thus, if you need to save
outputs from multiple inferences, you should make a copy with ``copy.deepcopy(outputs)``.
"""
if not self.is_active:
G_LOGGER.critical(
f"{self.name:35} | Must be activated prior to calling infer()"
)
if check_inputs:
input_metadata = self.get_input_metadata(use_numpy_dtypes=False)
G_LOGGER.verbose(
f"{self.name:35} | Input metadata is: {input_metadata}",
mode=LogMode.ONCE,
)
base_util.check_inputs(feed_dict, input_metadata)
return self.infer_impl(feed_dict, *args, **kwargs)
@func.constantmethod
def last_inference_time(self):
"""
Returns the total inference time in seconds required during the last call to ``infer()``.
Must be called only after ``activate()`` and before ``deactivate()``.
Returns:
float: The time in seconds, or None if runtime was not measured by the runner.
"""
if self.inference_time is None:
msg = f"{self.name:35} | `inference_time` was not set. Inference time will be incorrect! "
msg += "To correctly compare runtimes, please set the `inference_time` attribute in `infer_impl()`"
G_LOGGER.internal_error(msg)
G_LOGGER.warning(msg, mode=LogMode.ONCE)
return None
return self.inference_time
# Implementation for runner deactivation. Derived classes should override this function
# rather than ``deactivate()``.
def deactivate_impl(self):
pass
def deactivate(self):
"""
Deactivate the runner. For example, this may involve freeing CPU or GPU memory.
Generally, you should use a context manager instead of manually activating and deactivating.
For example:
::
with RunnerType(...) as runner:
runner.infer(...)
"""
if not self.is_active:
G_LOGGER.warning(
f"{self.name:35} | Not active; will not deactivate. If you really want to deactivate this runner, call deactivate_impl() directly"
)
return
self.inference_time = None
self.is_active = None
self.deactivate_impl()
self.is_active = False
if config.INTERNAL_CORRECTNESS_CHECKS:
old_state = self._pre_activate_runner_state
del self._pre_activate_runner_state
if old_state != vars(self):
G_LOGGER.internal_error(
f"Runner state was not reset after deactivation. Note:\nOld state: {old_state}\nNew state: {vars(self)}"
)
def __del__(self):
if self.is_active:
# __del__ is not guaranteed to be called, but when it is, this could be a useful warning.
print(
f"[W] {self.name:35} | Was activated but never deactivated. This could cause a memory leak!"
)
@@ -0,0 +1,53 @@
#
# 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.
#
from polygraphy import util
from polygraphy.logger import G_LOGGER
def check_inputs(feed_dict, input_metadata):
"""
Checks the provided `feed_dict` against expected input metadata.
Args:
feed_dict (Dict[str, Union[DeviceView, numpy.ndarray, torch.Tensor]]):
A mapping of input names to arrays.
input_metadata (TensorMetadata):
The expected input metadata.
"""
util.check_sequence_contains(
feed_dict.keys(), input_metadata.keys(), name="input data", items_name="inputs"
)
for name, inp in feed_dict.items():
meta = input_metadata[name]
# The "buffer" might just be a pointer, in which case we can't do any further checks with it, so we skip it.
if isinstance(inp, int):
continue
dtype = util.array.dtype(inp)
if dtype != meta.dtype:
G_LOGGER.critical(
f"Input tensor: {name} | Received unexpected dtype: {dtype}.\nNote: Expected type: {meta.dtype}"
)
shape = util.array.shape(inp)
if not util.is_valid_shape_override(shape, meta.shape):
G_LOGGER.critical(
f"Input tensor: {name} | Received incompatible shape: {shape}.\nNote: Expected a shape compatible with: {meta.shape}"
)
@@ -0,0 +1 @@
from polygraphy.backend.common.loader import *
@@ -0,0 +1,101 @@
#
# 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.
#
from polygraphy import mod, util
from polygraphy.backend.base import BaseLoader
@mod.export(funcify=True)
class BytesFromPath(BaseLoader):
"""
Functor that can load a file in binary mode ('rb').
"""
def __init__(self, path):
"""
Loads a file in binary mode ('rb').
Args:
path (str): The file path.
"""
self._path = path
@util.check_called_by("__call__")
def call_impl(self):
"""
Returns:
bytes: The contents of the file.
"""
return util.load_file(self._path, description="bytes")
@mod.export(funcify=True)
class SaveBytes(BaseLoader):
"""
Functor that can save bytes to a file.
"""
def __init__(self, obj, path):
"""
Saves bytes to a file.
Args:
obj (Union[bytes, Callable() -> bytes]):
The bytes to save or a callable that returns them.
path (str): The file path.
"""
self._bytes = obj
self._path = path
@util.check_called_by("__call__")
def call_impl(self):
"""
Returns:
bytes: The bytes saved.
"""
obj, _ = util.invoke_if_callable(self._bytes)
util.save_file(obj, self._path)
return obj
@mod.export(funcify=True)
class InvokeFromScript(BaseLoader):
"""
Functor that invokes a function from a Python script.
"""
def __init__(self, path, name):
"""
Invokes the specified function from the specified Python script.
If you intend to use the function more than once, you should import
the function using ``polygraphy.mod.import_from_script`` instead.
Args:
path (str): The path to the Python script. The path must include a '.py' extension.
name (str): The name of the function to import and invoke.
"""
self._path = path
self._name = name
@util.check_called_by("__call__")
def call_impl(self, *args, **kwargs):
"""
Returns:
object:
The return value of the imported function.
"""
return mod.import_from_script(self._path, self._name)(*args, **kwargs)
@@ -0,0 +1 @@
from polygraphy.backend.onnx.loader import *
@@ -0,0 +1,988 @@
#
# 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
import os
import sys
import tempfile
from polygraphy import constants, mod, util
from polygraphy.backend.base import BaseLoader
from polygraphy.backend.onnx import util as onnx_util
from polygraphy.datatype import DataType
from polygraphy.logger import G_LOGGER, LogMode
np = mod.lazy_import("numpy")
onnx = mod.lazy_import("onnx>=1.8.1")
onnxrt = mod.lazy_import("onnxruntime>=1.10.0")
onnxmltools = mod.lazy_import(
"onnxmltools==1.11.1", requires=["onnxconverter_common>=1.12.2"]
)
tf = mod.lazy_import("tensorflow<2.0")
tf2onnx = mod.lazy_import("tf2onnx")
tf_util = mod.lazy_import("polygraphy.backend.tf.util", log=False)
gs = mod.lazy_import("onnx_graphsurgeon>=0.3.27")
# ONNX-RT's shape inference also requires "sympy", but it is not reported as a dependency,
# so we work around it by checking for it manually.
onnxrt_symbolic_shape_inference = mod.lazy_import(
"onnxruntime.tools.symbolic_shape_infer>=1.10.0", requires=["sympy"]
)
LARGE_MODEL_THRESHOLD = 512 << 20 # 512 MiB
PROTOBUF_THRESHOLD = 2e9
class BaseLoadOnnxCopy(BaseLoader):
"""
Abstract base class for loaders that require loading an ONNX model and potentially
making a copy.
"""
def __init__(self, model, copy=None):
"""
Args:
model (Union[onnx.ModelProto, Callable() -> onnx.ModelProto]):
An ONNX model or a callable that returns one.
copy (bool): Whether to create a copy of the model first. Defaults to False.
"""
self._model = model
self.copy = util.default(copy, False)
def load(self):
model, _ = util.invoke_if_callable(self._model)
if self.copy:
model = copy.copy(model)
return model
class _GSGraphManager:
"""
Imports an ONNX-GraphSurgeon graph.
If the provided model is already a graph, the graph is not
exported to ONNX.
"""
def __init__(self, model):
self._model = model
def __enter__(self):
model, _ = util.invoke_if_callable(self._model)
self.USE_GS_GRAPH = isinstance(model, gs.Graph)
if self.USE_GS_GRAPH:
self.graph = model.copy()
else:
self.graph = gs_from_onnx(model)
return self
def __exit__(self, exc_type, exc_value, traceback):
if self.USE_GS_GRAPH:
self.retval = self.graph
else:
self.retval = gs.export_onnx(self.graph, do_type_check=False)
@mod.export(funcify=True)
class GsFromOnnx(BaseLoader):
"""
Functor that creates an ONNX-GraphSurgeon graph from an ONNX ModelProto.
"""
def __init__(self, model):
"""
Creates an ONNX-GraphSurgeon graph from an ONNX ModelProto.
Args:
model (Union[onnx.ModelProto, Callable() -> onnx.ModelProto]):
An ONNX model or a callable that returns one.
"""
self._model = model
@util.check_called_by("__call__")
def call_impl(self):
"""
Returns:
onnx_graphsurgeon.Graph: The ONNX-GraphSurgeon representation of the ONNX model
"""
model, _ = util.invoke_if_callable(self._model)
return gs.import_onnx(model)
@mod.export(funcify=True)
class OnnxFromPath(BaseLoader):
"""
Functor that loads an ONNX model from a file.
"""
def __init__(self, path, external_data_dir=None, ignore_external_data=None):
"""
Loads an ONNX model from a file.
Args:
path (str): The path from which to load the model.
external_data_dir (str): The directory where external data for the model is stored.
ignore_external_data (bool):
Whether to ignore any external data and just load the model structure without any weights.
The model will be usable only for purposes that don't require weights, such as extracting
subgraphs or inspecting model structure.
This can be useful in cases where external data is not available.
Defaults to False.
"""
self.path = path
self.external_data_dir = external_data_dir
self.ignore_external_data = util.default(ignore_external_data, False)
@util.check_called_by("__call__")
def call_impl(self):
"""
Returns:
onnx.ModelProto: The ONNX model
"""
G_LOGGER.info(f"Loading model: {self.path}")
# If external_data_dir is not None, we'll load external data ourselves
auto_load_ext_data = (
self.external_data_dir is None and not self.ignore_external_data
)
try:
model = onnx.load(self.path, load_external_data=auto_load_ext_data)
except FileNotFoundError:
if auto_load_ext_data:
G_LOGGER.warning(
"Failed to load model. This could be because external data could not be loaded.\n"
"Hint: If you don't need the model weights, try ignoring external data by setting `ignore_external_data=True` "
"or using the `--ignore-external-data` command-line option."
)
raise
if self.external_data_dir is not None:
G_LOGGER.verbose(f"Loading external data from: {self.external_data_dir}")
onnx.external_data_helper.load_external_data_for_model(
model, self.external_data_dir
)
return model
@mod.export(funcify=True)
class OnnxFromTfGraph(BaseLoader):
"""
Functor that loads a TensorFlow graph and converts it to ONNX using the tf2onnx converter.
"""
def __init__(self, graph, opset=None, optimize=None):
"""
Converts a TensorFlow model into ONNX.
Args:
graph (Union[Tuple[tf.Graph, Sequence[str]], Callable() -> Tuple[tf.Graph, Sequence[str]]]):
A tuple containing a TensorFlow graph and output names or a callable that returns one.
opset (int): The ONNX opset to use during conversion.
optimize (bool): Whether to use tf2onnx's graph optimization pass.
"""
self._graph = graph
self.opset = util.default(opset, 11)
self.optimize = util.default(optimize, True)
@util.check_called_by("__call__")
def call_impl(self):
"""
Returns:
onnx.ModelProto: The ONNX model.
"""
(graph, output_names), _ = util.invoke_if_callable(self._graph)
input_names = list(tf_util.get_input_metadata(graph).keys())
graphdef = graph.as_graph_def()
if self.optimize:
graphdef = tf2onnx.tfonnx.tf_optimize(
input_names, output_names, graph.as_graph_def()
)
with tf.Graph().as_default() as graph, tf.compat.v1.Session(
graph=graph
) as sess:
tf.import_graph_def(graphdef, name="")
onnx_graph = tf2onnx.tfonnx.process_tf_graph(
graph,
input_names=input_names,
output_names=output_names,
opset=self.opset,
)
if self.optimize:
onnx_graph = tf2onnx.optimizer.optimize_graph(onnx_graph)
return onnx_graph.make_model("model")
@mod.export(funcify=True)
class ModifyOutputs(BaseLoadOnnxCopy):
"""
Functor that modifies the outputs of an ONNX model.
"""
def __init__(self, model, outputs=None, exclude_outputs=None, copy=None):
"""
Modifies outputs of an ONNX model.
Args:
model (Union[onnx.ModelProto, Callable() -> onnx.ModelProto]):
An ONNX model or a callable that returns one.
outputs (Sequence[str]):
Names of tensors to mark as outputs. If provided, this will override the
existing model outputs.
If a value of `constants.MARK_ALL` is used instead of a list, all tensors in the network are marked.
exclude_outputs (Sequence[str]):
Names of tensors to exclude as outputs. This can be useful in conjunction with
``outputs=constants.MARK_ALL`` to omit outputs.
copy (bool): Whether to create a copy of the model first. Defaults to False.
"""
super().__init__(model, copy)
self.outputs = outputs
self.exclude_outputs = exclude_outputs
@util.check_called_by("__call__")
def call_impl(self):
"""
Returns:
onnx.ModelProto: The ONNX model with modified outputs.
"""
model = self.load()
if self.outputs == constants.MARK_ALL:
G_LOGGER.verbose("Marking all ONNX tensors as outputs")
model = onnx_util.mark_layerwise(model)
elif self.outputs is not None:
model = onnx_util.mark_outputs(model, self.outputs)
if self.exclude_outputs is not None:
model = onnx_util.unmark_outputs(model, self.exclude_outputs)
return model
@mod.export(funcify=True)
class ConvertToFp16(BaseLoadOnnxCopy):
"""
Functor that converts all floating point tensors in the model to 16-bit precision.
This is *not* needed in order to use TensorRT's fp16 precision, but may be useful for other backends.
"""
def __init__(self, model, copy=None):
"""
Converts all floating point tensors in the model to 16-bit precision.
Args:
model (Union[onnx.ModelProto, Callable() -> onnx.ModelProto]):
An ONNX model or a callable that returns one.
copy (bool): Whether to create a copy of the model first. Defaults to False.
"""
super().__init__(model, copy)
@util.check_called_by("__call__")
def call_impl(self):
"""
Returns:
onnx.ModelProto: The modified ONNX model.
"""
model = self.load()
G_LOGGER.info("Converting float tensors to float16")
model = onnxmltools.utils.float16_converter.convert_float_to_float16(
model, keep_io_types=True, disable_shape_infer=True
)
return model
@mod.export(funcify=True)
class FoldConstants(BaseLoadOnnxCopy):
"""
Functor that folds constants in an ONNX model.
"""
def __init__(
self,
model,
num_passes=None,
do_shape_inference=None,
partitioning=None,
fold_shapes=None,
copy=None,
error_ok=None,
size_threshold=None,
allow_onnxruntime_shape_inference=None,
):
"""
Fold constants in an ONNX model.
Args:
model (Union[onnx.ModelProto, Callable() -> onnx.ModelProto]):
An ONNX model or a callable that returns one.
num_passes (int):
The number of constant folding passes to run.
Sometimes, subgraphs that compute tensor shapes may not be foldable in a single pass.
By default, Polygraphy will automatically determine the number of passes required.
do_shape_inference (bool):
Whether to run shape inference in the model between passes.
This enables the loader to fold `Shape` nodes.
Only effective if `fold_shapes` is True.
Defaults to True.
partitioning (Union[str, None]):
Whether/How to partition the graph so that errors in folding one
part of a model do not affect other parts. Available modes are:
- None: Do not partition the graph. If inference fails, no constants are folded.
- 'basic': Partition the graph. If inference fails in one partition, other partitions will remain unaffected.
- 'recursive': Parition the graph recursively. If inference fails in a partition, the partition will be further partitioned.
Defaults to None.
fold_shapes (bool):
Whether to fold `Shape` nodes in the graph.
This requires shapes to be inferred in the graph, and can only fold
static shapes.
Defaults to True.
copy (bool):
Whether to create a copy of the model first.
Defaults to False.
error_ok (bool):
Whether to suppress errors during constant folding.
If this is set to ``False``, errors will be re-raised.
Defaults to True.
size_threshold (int):
The maximum size threshold, in bytes, for which to fold constants.
Any tensors larger than this value will not be folded.
Set to ``None`` to disable the size threshold and always fold constants.
For example, some models may apply ops like `Tile` or `Expand` to constants, which can
result in very large tensors. Rather than pre-computing those constants and bloating
the model size, it may be desirable to skip folding them and allow them to be computed
at runtime.
Defaults to None.
allow_onnxruntime_shape_inference (bool):
Allow ONNX-Runtime's shape inference to be used if available instead of ONNX's
shape inference utilities. The former may provide performance or memory usage benefits.
Has no effect if ``do_shape_inference`` is False.
Defaults to True.
"""
super().__init__(model, copy)
self.num_passes = num_passes
self.do_shape_inference = util.default(do_shape_inference, True)
self.partitioning = partitioning
self.fold_shapes = util.default(fold_shapes, True)
self.error_ok = util.default(error_ok, True)
self.size_threshold = size_threshold
self.allow_onnxruntime_shape_inference = allow_onnxruntime_shape_inference
@util.check_called_by("__call__")
def call_impl(self):
"""
Returns:
onnx.ModelProto: The new ONNX model with constants folded.
"""
def run_const_fold_pass(model):
graph = gs_from_onnx(model)
del model
graph.fold_constants(
fold_shapes=self.fold_shapes,
partitioning=self.partitioning,
size_threshold=self.size_threshold,
)
model = gs.export_onnx(graph.cleanup(), do_type_check=False)
del graph
if self.fold_shapes and self.do_shape_inference:
model = infer_shapes(
model, allow_onnxruntime=self.allow_onnxruntime_shape_inference
)
return model
# Need to manually trigger the autoinstall this since it's used by ONNX-GS, which does not have an autoinstall mechanism.
mod.autoinstall(onnxrt)
if not onnxrt.is_installed() or not onnxrt.is_importable():
G_LOGGER.error(
f"ONNX-Runtime is not installed, so constant folding may be suboptimal or not work at all.\n"
f"Consider installing ONNX-Runtime: {sys.executable} -m pip install onnxruntime"
)
model = self.load()
prefold_num_nodes = len(model.graph.node)
postfold_num_nodes = -1
index = 0
while (prefold_num_nodes != postfold_num_nodes) and (
self.num_passes is None or index < self.num_passes
):
prefold_num_nodes = onnx_util.get_num_nodes(model)
G_LOGGER.start(f"Folding Constants | Pass {index + 1}")
try:
model = run_const_fold_pass(model)
except Exception as err:
if not self.error_ok:
raise
G_LOGGER.warning(
f"Constant folding pass failed. Skipping subsequent passes.\nNote: Error was:\n{err}"
)
break
else:
postfold_num_nodes = onnx_util.get_num_nodes(model)
index += 1
G_LOGGER.finish(
f"{constants.TAB}Total Nodes | Original: {prefold_num_nodes:5}, "
f"After Folding: {postfold_num_nodes:5} | {prefold_num_nodes - postfold_num_nodes:5} Nodes Folded"
)
return model
@mod.export(funcify=True)
class SetUpperBound(BaseLoadOnnxCopy):
"""
Functor that sets upper bounds for tensors with unbounded DDS in an ONNX model.
Requires that the model has been constant folded and has shapes inferred.
"""
def __init__(
self,
model,
upper_bounds,
copy=None,
):
"""
Set upper bounds for tensors with unbounded DDS in an ONNX model.
Args:
model (Union[onnx.ModelProto, Callable() -> onnx.ModelProto]):
An ONNX model or a callable that returns one.
upper_bounds (Union[int, Dict[str, int]]):
The upper bounds for tensors with unbounded DDS.
If a single integer is provided, it will be used as the default upper bound for all tensors with unbounded DDS.
This can also be provided on a per-tensor basis using a dictionary. In that case, use an empty string ("") as the
key to specify default upper bound for tensors not explicitly listed.
copy (bool):
Whether to create a copy of the model first.
Defaults to False.
"""
super().__init__(model, copy)
self.upper_bounds = upper_bounds
def call_impl(self):
"""
Returns:
onnx.ModelProto: The new ONNX model.
"""
# Set upper bounds for tensors with unbounded DDS in the onnx model.
def set_upper_bound(graph, target_tensor_list):
applied_bounds = {}
for tensor in target_tensor_list:
upper_bound = util.value_or_from_dict(self.upper_bounds, tensor.name)
if upper_bound is None:
continue
# Insert a min operator to set the upper bound for the target tensor.
# A target tensor should always be produced from a single node.
assert len(tensor.inputs) == 1
producer = tensor.inputs[0]
producer_idx = producer.outputs.index(tensor)
tensor_copy = gs.Variable(
tensor.name + "_copy", dtype=tensor.dtype, shape=tensor.shape
)
upper_bound_values = np.array(upper_bound)
if tensor.shape is not None and len(tensor.shape) > 0:
upper_bound_values = np.array([upper_bound] * len(tensor.shape))
tensor_upper_bound = gs.Constant(
tensor.name + "_upper_bound", values=upper_bound_values
)
min_node = gs.Node(
op="Min", inputs=[tensor_copy, tensor_upper_bound], outputs=[tensor]
)
producer.outputs[producer_idx] = tensor_copy
tensor.inputs = [min_node]
graph.nodes.append(min_node)
applied_bounds[tensor.name] = upper_bound
G_LOGGER.info(f"Set tensor upper bounds: {applied_bounds}")
return graph
model = self.load()
graph = gs_from_onnx(model)
target_tensor_list = onnx_util.get_unbounded_dds_tensors(graph)
tensor_map = graph.tensors()
target_names = {tensor.name for tensor in target_tensor_list}
if isinstance(self.upper_bounds, dict):
input_names = set(self.upper_bounds.keys()) - {""}
# Report error when input tensor name is not in the graph.
util.check_sequence_contains(
set(tensor_map.keys()),
input_names,
name="the upper bounds dictionary",
items_name="tensors",
check_extra=False,
)
# Report warning when input tensor is not a unbounded DDS tensor.
util.check_sequence_contains(
set(target_names),
input_names,
name="the upper bounds dictionary",
items_name="tensors",
log_func=G_LOGGER.warning,
check_extra=False,
)
# Still set upper bound for input tensors with bounded shapes.
target_names.update(input_names)
graph = set_upper_bound(graph, [tensor_map[name] for name in target_names])
model = gs.export_onnx(graph.cleanup(), do_type_check=False)
return model
@mod.export(funcify=True)
class InferShapes(BaseLoader):
"""
Functor that runs shape inference on an ONNX model.
"""
def __init__(
self,
model,
error_ok=None,
external_data_dir=None,
save_to_disk_threshold_bytes=None,
allow_onnxruntime=None,
):
"""
Run shape inference on an ONNX model.
Args:
model (Union[onnx.ModelProto, Callable() -> onnx.ModelProto, str, Callable() -> str]):
An ONNX model or a callable that returns one, or a path to a model.
Supports models larger than the 2 GB protobuf limit.
error_ok (bool):
Whether errors during shape inference should be suppressed.
Defaults to True.
external_data_dir (str):
The directory where external data for the model is stored.
Only used if the model is provided via a path rather than a loader.
save_to_disk_threshold_bytes (int):
The size in bytes above which a ModelProto will be serialized to the disk
before running shape inference.
This can be used to work around the 2 GB protobuf limitation.
Defaults to 2 GB.
allow_onnxruntime (bool):
Allow ONNX-Runtime's shape inference to be used if available instead of ONNX's
shape inference utilities. The former may provide performance or memory usage benefits.
Defaults to True.
"""
self._model = model
self.error_ok = util.default(error_ok, True)
self.external_data_dir = external_data_dir
# Subtract a little so we're below the real threshold
self.save_to_disk_threshold_bytes = util.default(
save_to_disk_threshold_bytes, PROTOBUF_THRESHOLD
)
self.allow_onnxruntime = util.default(allow_onnxruntime, True)
def _run_onnx_shape_inference(self, model, external_data_dir):
if isinstance(model, onnx.ModelProto):
MODEL_SIZE = model.ByteSize()
if MODEL_SIZE > LARGE_MODEL_THRESHOLD:
G_LOGGER.warning(
f"Attempting to run shape inference on a large model ({MODEL_SIZE // 1024.0 ** 2} MiB). "
"This may require a large amount of memory.\nIf memory consumption becomes too high, "
"the process may be killed. You may want to try disabling shape inference in that case. ",
mode=LogMode.ONCE,
)
if MODEL_SIZE >= self.save_to_disk_threshold_bytes:
G_LOGGER.warning(
f"Model size ({MODEL_SIZE / 1024.0 ** 2} MiB) exceeds the in-memory size threshold: "
f"{self.save_to_disk_threshold_bytes / 1024.0 ** 2} MiB.\n"
f"The model will be saved to a temporary file before shape inference is run.",
mode=LogMode.ONCE,
)
outdir = tempfile.TemporaryDirectory()
outpath = os.path.join(outdir.name, "tmp_model.onnx")
save_onnx(model, outpath, external_data_path="ext.data")
model = outpath
external_data_dir = outdir.name
if isinstance(model, onnx.ModelProto):
model = onnx.shape_inference.infer_shapes(model)
else:
tmp_path = util.NamedTemporaryFile(
prefix="tmp_polygraphy_", suffix=".onnx"
).name
G_LOGGER.verbose(f"Writing shape-inferred model to: {tmp_path}")
onnx.shape_inference.infer_shapes_path(model, tmp_path)
# In cases where the original model had external data stored in the same directory,
# the external data directory may not be explicitly specified.
# In such cases, we need to use the model's directory as the external data path
# for the newly generated model.
model = onnx_from_path(
tmp_path,
external_data_dir=util.default(
external_data_dir, os.path.dirname(model) or None
),
)
return model
def _run_onnxruntime_shape_inference(self, model, external_data_dir):
if not isinstance(model, onnx.ModelProto):
model = onnx_from_path(model, external_data_dir=external_data_dir)
return onnxrt_symbolic_shape_inference.SymbolicShapeInference.infer_shapes(
model, auto_merge=True
)
@util.check_called_by("__call__")
def call_impl(self):
"""
Returns:
onnx.ModelProto: The new ONNX model with shapes inferred.
"""
model, _ = util.invoke_if_callable(self._model)
external_data_dir = self.external_data_dir
G_LOGGER.verbose("Starting shape inference")
try:
use_onnx_shape_inference = not self.allow_onnxruntime
if self.allow_onnxruntime:
try:
model = self._run_onnxruntime_shape_inference(
model, external_data_dir
)
G_LOGGER.verbose(
"Inferred shapes in the model with `onnxruntime.tools.symbolic_shape_infer`.\n"
"Note: To force Polygraphy to use `onnx.shape_inference` instead, set `allow_onnxruntime=False` or "
"use the `--no-onnxruntime-shape-inference` command-line option.",
mode=LogMode.ONCE,
)
except Exception as err:
use_onnx_shape_inference = True
G_LOGGER.extra_verbose(
f"Error while running `onnxruntime.tools.symbolic_shape_infer`:\n{err}"
)
G_LOGGER.warning(
"Falling back to `onnx.shape_inference` because `onnxruntime.tools.symbolic_shape_infer` either could not be loaded "
"or did not run successfully.\n"
"Note that using ONNX-Runtime for shape inference may be faster and require less memory.\n"
"Consider installing ONNX-Runtime or setting POLYGRAPHY_AUTOINSTALL_DEPS=1 in your environment "
"variables to allow Polygraphy to do so automatically.",
mode=LogMode.ONCE,
)
if use_onnx_shape_inference:
model = self._run_onnx_shape_inference(model, external_data_dir)
except Exception as err:
if not self.error_ok:
raise
G_LOGGER.warning(f"ONNX shape inference exited with an error:\n{err}")
G_LOGGER.internal_error(
f"ONNX shape inference exited with an error:\n{err}"
)
if not isinstance(model, onnx.ModelProto):
model = onnx_from_path(model, external_data_dir=external_data_dir)
else:
G_LOGGER.verbose("Shape inference completed successfully")
return model
@mod.export(funcify=True)
class ExtractSubgraph(BaseLoader):
"""
Functor that extracts a subgraph from an ONNX model.
"""
def __init__(
self, model, input_metadata=None, output_metadata=None, check_meta=None
):
"""
Extracts a subgraph from an ONNX model.
Args:
model (Union[Union[onnx.ModelProto, onnx_graphsurgeon.Graph], Callable() -> Union[onnx.ModelProto, onnx_graphsurgeon.Graph]]):
An ONNX model or ONNX-GraphSurgeon Graph or a callable that returns one.
input_metadata (TensorMetadata):
Metadata for the inputs of the subgraph.
Name, shape, and data type are required.
If not provided, the graph outputs are not modified.
output_metadata (TensorMetadata):
Metadata for the outputs of the subgraph.
Name and data type are required.
If not provided, the graph outputs are not modified.
check_meta (bool):
Whether to check that the provided input and output metadata include
all the expected fields.
Defaults to True.
"""
self._model = model
self.input_metadata = input_metadata
self.output_metadata = output_metadata
self.check_meta = util.default(check_meta, True)
@util.check_called_by("__call__")
def call_impl(self):
"""
Returns:
Union[onnx.ModelProto, onnx_graphsurgeon.Graph]:
The new ONNX model or ONNX-GraphSurgeon Graph.
"""
with _GSGraphManager(self._model) as manager:
graph = manager.graph
TENSOR_MAP = graph.tensors()
def get_tensor(name):
if name not in TENSOR_MAP:
G_LOGGER.critical(f"Tensor: {name} does not exist in the model.")
return TENSOR_MAP[name]
def update_tensor(name, dtype, shape):
tensor = get_tensor(name)
# No need to update constants
if isinstance(tensor, gs.Variable):
tensor.dtype, tensor.shape = (
DataType.to_dtype(DataType.from_dtype(dtype), "onnx")
if dtype is not None
else None
) or tensor.dtype, shape or tensor.shape
return tensor
def check_meta(name, dtype, shape, meta_type, needs_shape=True):
if not self.check_meta:
return
if needs_shape and shape is None:
G_LOGGER.warning(
f"{meta_type} metadata should include shape, but no shape was provided for tensor: {name}"
)
if dtype is None:
G_LOGGER.warning(
f"{meta_type} metadata should include data type, but no data type was provided for tensor: {name}"
)
if self.input_metadata is not None:
graph.inputs.clear()
for name, (dtype, shape) in self.input_metadata.items():
tensor = update_tensor(name, dtype, shape)
check_meta(name, tensor.dtype, tensor.shape, "Input")
tensor.inputs.clear()
graph.inputs.append(tensor)
if self.output_metadata is not None:
graph.outputs.clear()
for name, (dtype, shape) in self.output_metadata.items():
tensor = update_tensor(name, dtype, shape)
check_meta(
name, tensor.dtype, tensor.shape, "Output", needs_shape=False
)
graph.outputs.append(tensor)
graph.cleanup()
tensor_map = graph.tensors()
for tensor in tensor_map.values():
if (
isinstance(tensor, gs.Variable)
and not tensor.inputs
and tensor not in graph.inputs
):
consumer_nodes = [
f"Node: '{node.name}' (Op: {node.op})"
for node in tensor.outputs
]
G_LOGGER.error(
f"Tensor: '{tensor.name}' is a variable tensor consumed by: {consumer_nodes}, "
"but is not produced by a node or marked as a graph input."
f"\nDid you forget to mark a tensor as a graph input? Hint: Try inspecting the resulting model. "
f"\nNote: The resulting model will not be valid!"
)
return manager.retval
@mod.export(funcify=True)
class SaveOnnx(BaseLoader):
"""
Functor that saves an ONNX model to the specified path.
"""
def __init__(
self,
model,
path,
external_data_path=None,
size_threshold=None,
all_tensors_to_one_file=None,
):
"""
Saves an ONNX model to the specified path.
Args:
model (Union[onnx.ModelProto, Callable() -> onnx.ModelProto]):
An ONNX model or a callable that returns one.
path (str): Path at which to write the ONNX model.
external_data_path (str):
Path to save external data.
This is always a relative path; external data is always written to the same
directory as the model.
Set to an empty string to use the default path.
Set to None to disable.
Defaults to None if the model is within the protobuf size threshold and an empty string otherwise.
size_threshold (int):
Tensor size threshold, in bytes, above which tensor data will be
stored in the external file.
Tensors smaller that this threshold will remain in the ONNX file.
Has no effect if external_data_path is not set.
Defaults to 1024.
all_tensors_to_one_file (bool):
Whether to write all tensors to one file when saving external data.
Has no effect if external_data_path is not set.
Defaults to True.
"""
self._model = model
self.path = path
self.external_data_path = external_data_path
self.size_threshold = size_threshold
self.all_tensors_to_one_file = all_tensors_to_one_file
@util.check_called_by("__call__")
def call_impl(self):
"""
Returns:
onnx.ModelProto: The model, after saving it.
"""
model, _ = util.invoke_if_callable(self._model)
G_LOGGER.info(f"Saving ONNX model to: {self.path}")
model_size = model.ByteSize()
if self.external_data_path is None and model_size >= PROTOBUF_THRESHOLD:
external_data_path = ""
G_LOGGER.warning(
f"Model size ({model_size // 1024.0 ** 2} MiB) exceeds protobuf size threshold ({PROTOBUF_THRESHOLD // 1024 ** 2} MiB). "
f"Will save weight data to an external file.\n"
f"To control the location of this file, use the `external_data_path` parameter or the `--external-data-path` command-line option. "
)
else:
external_data_path = self.external_data_path
if external_data_path is not None:
G_LOGGER.verbose(
f"Saving external data for ONNX model to: {external_data_path}"
)
try:
onnx.external_data_helper.convert_model_to_external_data(
model,
location=external_data_path,
all_tensors_to_one_file=util.default(
self.all_tensors_to_one_file, True
),
size_threshold=util.default(self.size_threshold, 1024),
)
except TypeError:
if self.size_threshold is not None:
G_LOGGER.warning(
"This version of onnx does not support size_threshold in convert_model_to_external_data"
)
onnx.external_data_helper.convert_model_to_external_data(
model,
location=external_data_path,
all_tensors_to_one_file=util.default(
self.all_tensors_to_one_file, True
),
)
else:
if self.size_threshold is not None:
G_LOGGER.warning(
"size_threshold is set, but external data path has not been set. "
"No external data will be written."
)
if self.all_tensors_to_one_file is not None:
G_LOGGER.warning(
"all_tensors_to_one_file is set, but external data path has not been set. "
"No external data will be written."
)
util.makedirs(self.path)
onnx.save(model, self.path)
return model
@mod.export(funcify=True)
class BytesFromOnnx(BaseLoader):
"""
Functor that serializes an ONNX model.
"""
def __init__(self, model):
"""
Serializes an ONNX model.
Args:
model (Union[onnx.ModelProto, Callable() -> onnx.ModelProto]):
An ONNX model or a callable that returns one.
"""
self._model = model
@util.check_called_by("__call__")
def call_impl(self):
"""
Returns:
bytes: The serialized model.
"""
model, _ = util.invoke_if_callable(self._model)
return model.SerializeToString()
@mod.export(funcify=True)
class OnnxFromBytes(BaseLoader):
"""
Functor that deserializes an ONNX model.
"""
def __init__(self, serialized_onnx):
"""
Deserializes an ONNX model.
Args:
serialized_onnx (Union[bytes, Callable() -> bytes]):
A serialized ONNX model or a callable that returns one.
"""
self._serialized_onnx = serialized_onnx
@util.check_called_by("__call__")
def call_impl(self):
"""
Returns:
onnx.ModelProto: The ONNX model.
"""
serialized_onnx, _ = util.invoke_if_callable(self._serialized_onnx)
model = onnx.ModelProto()
model.ParseFromString(serialized_onnx)
return model
@@ -0,0 +1 @@
onnx
@@ -0,0 +1,480 @@
#
# 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)
@@ -0,0 +1,2 @@
from polygraphy.backend.onnxrt.loader import *
from polygraphy.backend.onnxrt.runner import *
@@ -0,0 +1,85 @@
#
# 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.
#
from polygraphy import mod, util
from polygraphy.backend.base import BaseLoader
from polygraphy.logger import G_LOGGER
import os
onnxrt = mod.lazy_import("onnxruntime")
@mod.export(funcify=True)
class SessionFromOnnx(BaseLoader):
"""
Functor that builds an ONNX-Runtime inference session.
"""
def __init__(self, model_bytes, providers=None):
"""
Builds an ONNX-Runtime inference session.
Args:
model_bytes (Union[Union[bytes, str], Callable() -> Union[bytes, str]]):
A serialized ONNX model or a path to a model or a callable that returns one of those.
providers (Sequence[str]):
A sequence of execution providers to use in order of priority.
Each element of the sequence may be either an exact match or a case-insensitive partial match
for the execution providers available in ONNX-Runtime. For example, a value of "cpu" would
match the "CPUExecutionProvider".
Defaults to ``["cpu"]``.
"""
self._model_bytes_or_path = model_bytes
self.providers = util.default(providers, ["cpu"])
@util.check_called_by("__call__")
def call_impl(self):
"""
Returns:
onnxruntime.InferenceSession: The inference session.
"""
model_bytes, _ = util.invoke_if_callable(self._model_bytes_or_path)
available_providers = onnxrt.get_available_providers()
providers = []
for prov in self.providers:
matched_prov_name = util.find_str_in_iterable(prov[0] if isinstance(prov, tuple) else prov, available_providers)
matched_prov = (matched_prov_name, prov[1]) if isinstance(prov, tuple) else matched_prov_name
if matched_prov is None:
G_LOGGER.critical(
f"Could not find specified ONNX-Runtime execution provider.\nNote: Requested provider was: {prov}, but available providers are: {available_providers}"
)
providers.append(matched_prov)
G_LOGGER.start(
f"Creating ONNX-Runtime Inference Session with providers: {providers}"
)
# ONNX Runtime tried to bind each thread to a logical CPU, but not all assigned cpu cores are available on some platforms.
# Set the number of threads within each operator and between operators the number of usable CPUs to avoid crash in onnxruntime on those platforms.
options = onnxrt.SessionOptions()
try:
# sched_getaffinity is only available on UNIX platforms
process_cpu_count = len(os.sched_getaffinity(0))
except AttributeError:
process_cpu_count = 1
options.intra_op_num_threads = process_cpu_count
options.inter_op_num_threads = process_cpu_count
return onnxrt.InferenceSession(
model_bytes, providers=providers, sess_options=options
)
@@ -0,0 +1 @@
onnxruntime
@@ -0,0 +1,90 @@
#
# 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 time
from collections import OrderedDict
from polygraphy import mod, util
from polygraphy.backend.base import BaseRunner
from polygraphy.common import TensorMetadata
from polygraphy.datatype import DataType
@mod.export()
class OnnxrtRunner(BaseRunner):
"""
Runs inference using an ONNX-Runtime inference session.
"""
def __init__(self, sess, name=None):
"""
Args:
sess (Union[onnxruntime.InferenceSession, Callable() -> onnxruntime.InferenceSession]):
An ONNX-Runtime inference session or a callable that returns one.
"""
super().__init__(name=name, prefix="onnxrt-runner")
self._sess = sess
@util.check_called_by("activate")
def activate_impl(self):
self.sess, _ = util.invoke_if_callable(self._sess)
@util.check_called_by("get_input_metadata")
def get_input_metadata_impl(self):
meta = TensorMetadata()
for node in self.sess.get_inputs():
meta.add(
node.name,
dtype=DataType.from_dtype(node.type, "onnxruntime"),
shape=node.shape,
)
return meta
@util.check_called_by("infer")
def infer_impl(self, feed_dict):
"""
Implementation for running inference with ONNX-Runtime.
Do not call this method directly - use ``infer()`` instead,
which will forward unrecognized arguments to this method.
Args:
feed_dict (OrderedDict[str, Union[numpy.ndarray, torch.Tensor]]):
A mapping of input tensor names to corresponding input NumPy arrays or PyTorch tensors.
If PyTorch tensors are provided in the feed_dict, then this function
will return the outputs also as PyTorch tensors.
Returns:
OrderedDict[str, Union[numpy.ndarray, torch.Tensor]]:
A mapping of output tensor names to corresponding output NumPy arrays
or PyTorch tensors.
"""
use_torch = any(util.array.is_torch(t) for t in feed_dict.values())
# `to_numpy()`` and `to_torch()` should be zero-copy whenever possible.
feed_dict = {name: util.array.to_numpy(t) for name, t in feed_dict.items()}
start = time.time()
inference_outputs = self.sess.run(None, feed_dict)
end = time.time()
out_dict = OrderedDict()
for node, out in zip(self.sess.get_outputs(), inference_outputs):
out_dict[node.name] = out if not use_torch else util.array.to_torch(out)
self.inference_time = end - start
return out_dict
@util.check_called_by("deactivate")
def deactivate_impl(self):
del self.sess
@@ -0,0 +1,17 @@
#
# Copyright (c) 2021, NVIDIA CORPORATION. All rights reserved.
#
# 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.
#
from polygraphy.backend.pluginref.runner import *
@@ -0,0 +1,102 @@
#
# 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.
#
from polygraphy import mod
from polygraphy.logger import G_LOGGER
np = mod.lazy_import("numpy")
gs = mod.lazy_import("onnx_graphsurgeon")
OP_REGISTRY = {} # Dict[str, Callable]: Maps op names to reference implementations
def register(op):
"""
Registers a function as the reference implementation for a given op.
Args:
op (str): The name of the op for which to register this function.
"""
def register_impl(func):
def wrapped_func(node, intermediate_tensors):
inputs = []
for inp in node.inputs:
if inp.is_empty(): # Optional input
inputs.append(None)
elif isinstance(inp, gs.Constant):
inputs.append(inp.values)
elif inp.name in intermediate_tensors:
inputs.append(intermediate_tensors[inp.name])
else:
G_LOGGER.internal_error(
f"Input: {inp.name} was not found in intermediate tensors and is not a constant.\nNote: Intermediate tensors include: {list(intermediate_tensors.keys())}"
)
outputs = func(node.attrs, *inputs)
if len(outputs) != len(node.outputs):
G_LOGGER.internal_error(
f"{op} reference implementation returned the wrong number of outputs.\nNote: Expected {len(node.outputs)} but recevied {len(outputs)}"
)
return {
out_tensor.name: out for out_tensor, out in zip(node.outputs, outputs)
}
OP_REGISTRY[op] = wrapped_func
return wrapped_func
return register_impl
@register("Identity")
def run_identity(attrs, x):
return [x]
@register("InstanceNormalization")
def run_instancenorm(attrs, x, weights, bias):
epsilon = attrs.get("epsilon", 1.0e-5)
rank = len(x.shape)
axis = tuple(range(2, rank))
mean = np.mean(x, axis=axis, keepdims=True)
var = np.var(x, axis=axis, keepdims=True)
# Weights and bias needs to be broadcasted to shape of X. C dimension should be a wildcard.
broadcast_shape = [-1] + [1] * (rank - 2)
weights = weights.reshape(broadcast_shape)
bias = bias.reshape(broadcast_shape)
res = weights * (x - mean) / np.sqrt(var + epsilon) + bias
return [res]
@register("MeanVarianceNormalization")
def run_meanvarnorm(attrs, x):
epsilon = 1.0e-9
axes = attrs.get("axes", [0, 2, 3])
axes = tuple(axes)
data_mean = np.mean(x, axis=axes, keepdims=True)
data_mean_squared = np.power(data_mean, 2)
data_squared = np.power(x, 2)
data_squared_mean = np.mean(data_squared, axis=axes, keepdims=True)
std = np.sqrt(data_squared_mean - data_mean_squared)
res = (x - data_mean) / (std + epsilon)
return [res]
@@ -0,0 +1,2 @@
numpy
onnx_graphsurgeon
@@ -0,0 +1,83 @@
#
# 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
import time
from collections import OrderedDict
from polygraphy import mod, util
from polygraphy.backend.base import BaseRunner
from polygraphy.backend.pluginref.references import OP_REGISTRY
from polygraphy.logger import G_LOGGER
np = mod.lazy_import("numpy")
onnx_util = mod.lazy_import("polygraphy.backend.onnx.util")
@mod.export()
class PluginRefRunner(BaseRunner):
"""
Runs inference using custom CPU reference implementations
"""
def __init__(self, graph, name=None):
"""
Args:
graph (Union[onnx_graphsurgeon.Graph, Callable() -> onnx_graphsurgeon.Graph]):
An ONNX-GraphSurgeon graph or a callable that returns one.
name (str):
The human-readable name prefix to use for this runner.
A runner count and timestamp will be appended to this prefix.
"""
super().__init__(name=name, prefix="pluginref-runner")
self._graph = graph
@util.check_called_by("activate")
def activate_impl(self):
self.graph, _ = util.invoke_if_callable(self._graph)
@util.check_called_by("get_input_metadata")
def get_input_metadata_impl(self):
return onnx_util.meta_from_gs_tensors(self.graph.inputs)
@util.check_called_by("infer")
def infer_impl(self, feed_dict):
start = time.time()
intermediate_tensors = copy.copy(feed_dict)
for node in self.graph.nodes:
if node.op not in OP_REGISTRY:
G_LOGGER.critical(
f"Op: {node.op} does not have a reference implementation registered!"
)
intermediate_tensors.update(
OP_REGISTRY[node.op](node, intermediate_tensors)
)
outputs = OrderedDict()
for out in self.graph.outputs:
outputs[out.name] = intermediate_tensors[out.name]
end = time.time()
self.inference_time = end - start
return outputs
@util.check_called_by("deactivate")
def deactivate_impl(self):
del self.graph
@@ -0,0 +1 @@
from polygraphy.backend.pyt.runner import *
@@ -0,0 +1 @@
torch
@@ -0,0 +1,81 @@
#
# 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 time
from collections import OrderedDict
from polygraphy import mod, util
from polygraphy.backend.base import BaseRunner
torch = mod.lazy_import("torch>=1.13.0")
@mod.export()
class PytRunner(BaseRunner):
"""
Runs inference using PyTorch.
"""
def __init__(self, model, input_metadata, output_names, name=None):
"""
Args:
model (Union[torch.nn.Module, Callable() -> torch.nn.Module]):
A torch.nn.Module or subclass or a callable that returns one.
input_metadata (TensorMetadata): Mapping of input names to their data types and shapes.
output_names (List[str]):
A list of output names of the model. This information is used by the
Comparator to determine which outputs to compare.
name (str):
The human-readable name prefix to use for this runner.
A runner count and timestamp will be appended to this prefix.
"""
super().__init__(name=name, prefix="pytorch-runner")
self._model = model
self.input_metadata = input_metadata
self.output_names = output_names
@util.check_called_by("activate")
def activate_impl(self):
self.model, _ = util.invoke_if_callable(self._model)
self.model.eval()
@util.check_called_by("get_input_metadata")
def get_input_metadata_impl(self):
return self.input_metadata
@util.check_called_by("infer")
def infer_impl(self, feed_dict):
with torch.no_grad():
inputs = [
torch.from_numpy(val.astype(dtype)).cuda()
for (val, (dtype, _)) in zip(
feed_dict.values(), self.input_metadata.values()
)
]
start = time.time()
outputs = self.model(*inputs)
end = time.time()
out_dict = OrderedDict()
for name, output in zip(self.output_names, outputs):
out_dict[name] = output.cpu().numpy()
return out_dict, end - start
@util.check_called_by("deactivate")
def deactivate_impl(self):
del self.model
@@ -0,0 +1 @@
from polygraphy.backend.tensorrt_rtx.config import *
@@ -0,0 +1,117 @@
#
# SPDX-FileCopyrightText: Copyright (c) 1993-2025 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.
#
from polygraphy import config as polygraphy_config, mod, util
from polygraphy.backend.trt.config import _CreateConfigCommon
from polygraphy.backend.trt.util import inherit_and_extend_docstring
from polygraphy.logger import G_LOGGER
from polygraphy.mod.trt_importer import lazy_import_trt
trt = lazy_import_trt()
@mod.export(funcify=True, func_name="create_config_rtx")
class CreateConfigRTX(_CreateConfigCommon):
"""
Functor that creates an IBuilderConfig with TensorRT-RTX specific features.
"""
@inherit_and_extend_docstring(_CreateConfigCommon.__init__)
def __init__(
self,
use_gpu=None,
compute_capabilities=None,
**kwargs
):
"""
Creates an IBuilderConfig with TensorRT-RTX specific features.
Args:
use_gpu (bool):
Whether to use the current GPU device as target for engine compilation.
Equivalent to setting ComputeCapability.CURRENT. This is mutually exclusive with compute_capabilities.
Defaults to False.
compute_capabilities (List[Tuple[int, int]]):
List of (major, minor) compute capability tuples to target for engine compilation.
This is mutually exclusive with use_gpu. When specified, the engine can only run on devices
with the specified compute capabilities.
Defaults to None.
"""
super().__init__(**kwargs)
self.use_gpu = util.default(use_gpu, False)
self.compute_capabilities = compute_capabilities
if self.use_gpu and self.compute_capabilities:
G_LOGGER.critical("use_gpu and compute_capabilities are mutually exclusive.")
self._validator()
def _validator(self):
"""
Validates initialization parameters for TensorRT-RTX specific features.
"""
if self.use_gpu or self.compute_capabilities is not None:
if not polygraphy_config.USE_TENSORRT_RTX:
G_LOGGER.critical("--compute-capabilities and --use-gpu settings are only supported with USE_TENSORRT_RTX=1.")
# Validate compute capabilities format and availability
if self.compute_capabilities:
for major, minor in self.compute_capabilities:
cap_name = f"SM{major}{minor}"
if not hasattr(trt.ComputeCapability, cap_name):
G_LOGGER.critical(f"Compute capability {major}.{minor} ({cap_name})"
" not supported by this TensorRT-RTX version.")
def _configure_flags(self, builder, network, config):
"""
Validates and configures TensorRT-RTX-specific features.
Args:
builder (trt.Builder): The TensorRT builder
network (trt.INetworkDefinition): The TensorRT network
config (trt.IBuilderConfig): The TensorRT builder config to modify
"""
# Set compute capabilities if specified
if self.use_gpu or self.compute_capabilities is not None:
try:
if self.use_gpu:
# Use current GPU device
config.num_compute_capabilities = 1
config.set_compute_capability(trt.ComputeCapability.CURRENT, 0)
G_LOGGER.info("Using current GPU device for engine compilation (ComputeCapability.CURRENT)")
elif self.compute_capabilities:
# Set specific compute capabilities
config.num_compute_capabilities = len(self.compute_capabilities)
G_LOGGER.info(f"Setting {len(self.compute_capabilities)} target compute capabilities: {self.compute_capabilities}")
for i, (major, minor) in enumerate(self.compute_capabilities):
cap_name = f"SM{major}{minor}"
compute_cap = getattr(trt.ComputeCapability, cap_name)
config.set_compute_capability(compute_cap, i)
except Exception as e:
G_LOGGER.critical(f"Failed to set compute capabilities: {e}. You are likely not using a TensorRT-RTX build.")
@util.check_called_by("__call__")
def call_impl(self, builder, network):
"""
Callable implementation that creates and configures the IBuilderConfig with TensorRT-RTX features.
"""
# Enable all common config options
config = super().call_impl(builder, network)
self._configure_flags(builder, network, config)
return config
@@ -0,0 +1 @@
NOTE: The `tf` backend currently only supports TensorFlow 1.X.
@@ -0,0 +1,37 @@
from polygraphy.backend.tf.loader import *
from polygraphy.backend.tf.runner import *
def register_logger_callback():
from polygraphy.logger import G_LOGGER
def set_tf_logging_level(severity_trie):
import os
from polygraphy import mod
tf = mod.lazy_import("tensorflow<2.0")
if not tf.is_installed() or not tf.is_importable():
return
sev = severity_trie.get(G_LOGGER.module_path(os.path.dirname(__file__)))
if sev > G_LOGGER.WARNING:
tf_sev = tf.compat.v1.logging.ERROR
tf_logging_level = "3"
elif sev > G_LOGGER.INFO:
tf_sev = tf.compat.v1.logging.WARN
tf_logging_level = "2"
elif sev > G_LOGGER.VERBOSE:
tf_sev = tf.compat.v1.logging.INFO
tf_logging_level = "1"
else:
tf_sev = tf.compat.v1.logging.DEBUG
tf_logging_level = "0"
tf.compat.v1.logging.set_verbosity(tf_sev)
os.environ["TF_CPP_MIN_LOG_LEVEL"] = tf_logging_level
G_LOGGER.register_callback(set_tf_logging_level) # Will be registered when this backend is imported.
register_logger_callback()
@@ -0,0 +1,496 @@
#
# 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.
#
# Sets up everything needed to perform inference in TensorFlow.
import os
from polygraphy import constants, mod, util
from polygraphy.backend.base import BaseLoader
from polygraphy.backend.tf import util as tf_util
from polygraphy.logger import G_LOGGER
tf = mod.lazy_import("tensorflow<2.0")
@mod.export(funcify=True)
class OptimizeGraph(BaseLoader):
"""
Functor that freezes a TensorFlow graph, and folds constants.
"""
def __init__(self, graph):
"""
Freezes a TensorFlow graph and folds constants.
Args:
graph (Union[Tuple[tf.Graph, Sequence[str]], Callable() -> Tuple[tf.Graph, Sequence[str]]]):
A tuple containing a TensorFlow graph and output names or a callable that returns one.
"""
self._graph = graph
def constfold(self, graphdef, output_names):
from tensorflow.core.protobuf import (
config_pb2,
meta_graph_pb2,
rewriter_config_pb2,
)
from tensorflow.python.framework import importer, ops
from tensorflow.python.grappler import tf_optimizer
from tensorflow.python.training import saver
graph = ops.Graph()
with graph.as_default():
output_collection = meta_graph_pb2.CollectionDef()
output_list = output_collection.node_list.value
for output in output_names:
output_list.append(output.encode("utf-8"))
importer.import_graph_def(graphdef, name="")
metagraph = saver.export_meta_graph(
graph_def=graph.as_graph_def(add_shapes=True), graph=graph
)
metagraph.collection_def["train_op"].CopyFrom(output_collection)
rewriter_config = rewriter_config_pb2.RewriterConfig()
rewriter_config.optimizers.extend(["constfold"])
rewriter_config.meta_optimizer_iterations = (
rewriter_config_pb2.RewriterConfig.ONE
)
session_config = config_pb2.ConfigProto()
session_config.graph_options.resave_options.CopyFrom(rewriter_config)
return tf_optimizer.OptimizeGraph(session_config, metagraph, graph_id=b"graph")
@util.check_called_by("__call__")
def call_impl(self):
"""
Returns:
Tuple[tf.Graph, Sequence[str]]: The TensorFlow graph, and the names of its outputs.
"""
(graph, output_names), _ = util.invoke_if_callable(self._graph)
with tf.Session(graph=graph) as sess:
sess.run(tf.initializers.global_variables())
sess.run(tf.initializers.local_variables())
graphdef = sess.graph.as_graph_def()
removed = tf.graph_util.remove_training_nodes(graphdef)
G_LOGGER.ultra_verbose(f"Removed nodes: {removed}")
for node in graphdef.node:
if node.op == "RefSwitch":
node.op = "Switch"
for index in range(len(node.input)):
if "moving_" in node.input[index]:
node.input[index] = node.input[index] + "/read"
elif node.op == "AssignSub":
node.op = "Sub"
if "use_locking" in node.attr:
del node.attr["use_locking"]
elif node.op == "AssignAdd":
node.op = "Add"
if "use_locking" in node.attr:
del node.attr["use_locking"]
elif node.op == "Assign":
node.op = "Identity"
if "use_locking" in node.attr:
del node.attr["use_locking"]
if "validate_shape" in node.attr:
del node.attr["validate_shape"]
if len(node.input) == 2:
# input0: ref: Should be from a Variable node. May be uninitialized.
# input1: value: The value to be assigned to the variable.
node.input[0] = node.input[1]
del node.input[1]
# Strip port information from outputs
output_names = [name.split(":")[0] for name in output_names]
output_graph_def = tf.graph_util.convert_variables_to_constants(
sess, graphdef, output_names
)
output_graph_def = self.constfold(output_graph_def, output_names)
return graph_from_frozen(output_graph_def)
@mod.export(funcify=True)
class GraphFromKeras(BaseLoader):
"""
Functor that loads a TensorFlow model from Keras.
"""
def __init__(self, path):
"""
Loads a TensorFlow model from Keras.
Args:
path (Union[str, h5py.File]): A path to the saved model, or the file object.
"""
self.path = path
@util.check_called_by("__call__")
def call_impl(self):
"""
Returns:
Tuple[tf.Graph, Sequence[str]]: The TensorFlow graph, and the names of its outputs.
"""
from tensorflow.python import keras
from tensorflow.python.keras import backend
model = keras.models.load_model(self.path)
graph = backend.get_session().graph
return graph, tf_util.get_graph_output_names(graph)
@mod.export(funcify=True)
class GraphFromFrozen(BaseLoader):
"""
Functor that loads a TensorFlow frozen model.
"""
def __init__(self, path):
"""
Loads a TensorFlow frozen model.
Args:
path (Union[str, tf.Graph, tf.GraphDef]):
A path to the frozen model, or a frozen TensorFlow graph or graphdef.
"""
self.path = path
@util.check_called_by("__call__")
def call_impl(self):
"""
Returns:
Tuple[tf.Graph, Sequence[str]]: The TensorFlow graph, and the names of its outputs.
"""
graph = tf_util.load_graph(self.path)
return graph, tf_util.get_graph_output_names(graph)
@mod.export(funcify=True)
class GraphFromCkpt(BaseLoader):
"""
Functor that loads a TensorFlow model from a checkpoint. Note that in order to use checkpoints,
you must NOT use subprocesses in the Comparator.
"""
def __init__(self, dir, name=None):
"""
Loads a TensorFlow model from a checkpoint.
Args:
dir (str): Path to a directory containing checkpoints.
name (str):
The name of the checkpoint to load, not including the file extension.
For example, to load `model.meta`, the argument would be `model`.
"""
self.dir = dir
self.name = name
@util.check_called_by("__call__")
def call_impl(self):
"""
Returns:
Tuple[tf.Graph, Sequence[str]]: The TensorFlow graph, and the names of its outputs.
"""
# If `name` is not provided, this expects that the directory contains a `checkpoint` file with the contents:
#
# model_checkpoint_path: "model"
# all_model_checkpoint_paths: "model"
#
# where "model" is the checkpoint name
if not os.path.isdir(self.dir):
G_LOGGER.warning(
f"Specified checkpoint directory: {self.dir} does not look like a directory."
)
if self.name is None:
G_LOGGER.verbose(
"Checkpoint name was not explicitly provided, searching for `checkpoint` file"
)
checkpoint = tf.train.get_checkpoint_state(self.dir)
if checkpoint is None:
ckpt_file_contents = '\nmodel_checkpoint_path: "model"\nall_model_checkpoint_paths: "model"\n'
G_LOGGER.critical(
f"Checkpoint directory: {self.dir} does not contain a `checkpoint` file, and the checkpoint name was not provided. Please either create a checkpoint file with the contents:\n{ckpt_file_contents} \nWhere `model` is the name of the checkpoint, or explicitly provide the name with --ckpt, not including file extensions"
)
input_checkpoint = checkpoint.model_checkpoint_path
else:
input_checkpoint = os.path.join(self.dir, self.name)
meta_file = input_checkpoint + ".meta"
with tf.Graph().as_default() as graph, tf.compat.v1.Session(
graph=graph
).as_default() as sess:
saver = tf.compat.v1.train.import_meta_graph(meta_file, clear_devices=True)
saver.restore(sess, input_checkpoint)
return graph, tf_util.get_graph_output_names(graph)
@mod.export(funcify=True)
class UseTfTrt(BaseLoader):
"""
[UNTESTED] Functor that optimizes a TensorFlow model using TF-TRT.
"""
def __init__(
self,
graph,
max_workspace_size=None,
fp16=None,
int8=None,
max_batch_size=None,
is_dynamic_op=False,
minimum_segment_size=None,
):
"""
Optimizes a TensorFlow model using TF-TRT.
Args:
graph (Union[Tuple[tf.Graph, Sequence[str]], Callable() -> Tuple[tf.Graph, Sequence[str]]]):
A tuple containing a TensorFlow graph and output names or a callable that returns one.
max_workspace_size (int): The maximum workspace size.
fp16 (bool): Whether to run in FP16 mode.
max_batch_size (int): The maximum batch size.
"""
self._graph = graph
self.max_workspace_size = util.default(max_workspace_size, 1 << 24)
self.fp16 = util.default(fp16, False)
self.fp8 = util.default(fp8, False)
self.int8 = util.default(int8, False)
self.max_batch_size = util.default(max_batch_size, 1)
self.is_dynamic_op = is_dynamic_op
self.minimum_segment_size = util.default(minimum_segment_size, 3)
@util.check_called_by("__call__")
def call_impl(self):
"""
Returns:
Tuple[tf.Graph, Sequence[str]]: The TensorFlow graph, and the names of its outputs.
"""
from tensorflow.contrib import tensorrt as tf_trt
(graph, output_names), _ = util.invoke_if_callable(self._graph)
precision_mode = "FP16" if self.fp16 else "FP32"
precision_mode = "INT8" if self.int8 else precision_mode
precision_mode = "FP8" if self.fp8 else precision_mode
G_LOGGER.info(
f"For TF-TRT, using outputs={output_names}, max_workspace_size_bytes={self.max_workspace_size}, max_batch_size={self.max_batch_size}, minimum_segment_size={self.minimum_segment_size}, is_dynamic_op={self.is_dynamic_op}, precision_mode={precision_mode}"
)
graphdef = tf_trt.create_inference_graph(
graph.as_graph_def(),
outputs=output_names,
max_workspace_size_bytes=self.max_workspace_size,
max_batch_size=self.max_batch_size,
minimum_segment_size=self.minimum_segment_size,
is_dynamic_op=self.is_dynamic_op,
precision_mode=precision_mode,
)
segment_number = 0
for node in graphdef.node:
if node.op == "TRTEngineOp":
engine = node.attr["serialized_segment"].s
segment_number += 1
G_LOGGER.info(f"Found {segment_number} engines in TFTRT graph")
with tf.Graph().as_default() as graph:
tf.import_graph_def(graphdef, name="")
return graph, tf_util.get_graph_output_names(graph)
@mod.export(funcify=True)
class ModifyGraphOutputs(BaseLoader):
"""
Functor that modifies outputs of a TensorFlow graph.
"""
def __init__(self, graph, outputs=None):
"""
Modifies outputs of a TensorFlow graph.
Args:
graph (Union[Tuple[tf.Graph, Sequence[str]], Callable() -> Tuple[tf.Graph, Sequence[str]]]):
A tuple containing a TensorFlow graph and output names or a callable that returns one.
outputs (List[str]):
Names of output tensors. If provided, this will override the outputs
determined by the loader.
If a value of `constants.MARK_ALL` is used instead of a list, all tensors in the network are marked.
"""
self._graph = graph
self.outputs = outputs
@util.check_called_by("__call__")
def call_impl(self):
"""
Returns:
Tuple[tf.Graph, Sequence[str]]: The TensorFlow graph, and the names of its outputs.
"""
(graph, outputs), _ = util.invoke_if_callable(self._graph)
if self.outputs == constants.MARK_ALL:
outputs = list(tf_util.get_output_metadata(graph, layerwise=True).keys())
elif self.outputs is not None:
outputs = self.outputs
return graph, outputs
@mod.export(funcify=True)
class SaveGraph(BaseLoader):
"""
Functor that writes out artifacts from a TensorFlow graph.
"""
def __init__(self, graph, path=None, tensorboard_dir=None, engine_dir=None):
"""
Writes out artifacts from a TensorFlow Graph.
Args:
graph (Union[Tuple[tf.Graph, Sequence[str]], Callable() -> Tuple[tf.Graph, Sequence[str]]]):
A tuple containing a TensorFlow graph and output names or a callable that returns one.
path (str): Path at which to save the frozen graphdef.
tensorboard_dir (str): The directory in which to write TensorBoard visualizations.
engine_dir (str): The directory in which to save TF-TRT engines,
"""
self._graph = graph
self.path = path
self.tensorboard_dir = tensorboard_dir
self.engine_dir = engine_dir
@util.check_called_by("__call__")
def call_impl(self):
"""
Returns:
Tuple[tf.Graph, Sequence[str]]: The TensorFlow graph, and the names of its outputs.
"""
(graph, outputs), _ = util.invoke_if_callable(self._graph)
if self.path:
util.save_file(graph.as_graph_def().SerializeToString(), dest=self.path)
if self.tensorboard_dir:
G_LOGGER.info(f"Writing tensorboard events to {self.tensorboard_dir}")
train_writer = tf.compat.v1.summary.FileWriter(self.tensorboard_dir)
train_writer.add_graph(graph)
if self.engine_dir is not None:
graphdef = graph.as_graph_def()
segment_number = 0
for node in graphdef.node:
if node.op == "TRTEngineOp":
engine = node.attr["serialized_segment"].s
if self.engine_dir is not None:
util.save_file(
contents=engine,
dest=os.path.join(
self.engine_dir, f"segment-{segment_number}"
),
)
segment_number += 1
return graph, outputs
@mod.export(funcify=True)
class CreateConfig(BaseLoader):
"""
Functor that creates a TensorFlow config.
"""
def __init__(self, gpu_memory_fraction=None, allow_growth=None, use_xla=None):
"""
Creates a TensorFlow config.
Args:
gpu_memory_fraction (float):
The fraction of GPU memory that will be made available to TensorFlow.
This should be a value between 0.0 and 1.0.
allow_growth (bool): Whether to allow GPU memory allocated by TensorFlow to grow.
use_xla (bool): Whether to attempt to enable XLA.
"""
self.gpu_memory_fraction = util.default(gpu_memory_fraction, 0.9)
self.allow_growth = util.default(allow_growth, False)
self.use_xla = util.default(use_xla, False)
@util.check_called_by("__call__")
def call_impl(self):
"""
Returns:
tf.ConfigProto: The TensorFlow config.
"""
# Session configuration
gpu_options = tf.compat.v1.GPUOptions(
per_process_gpu_memory_fraction=self.gpu_memory_fraction,
allow_growth=self.allow_growth,
)
config = tf.compat.v1.ConfigProto(gpu_options=gpu_options)
if self.use_xla:
config.graph_options.optimizer_options.global_jit_level = (
tf.OptimizerOptions.ON_1
)
G_LOGGER.verbose(
f"Using gpu memory fraction: {self.gpu_memory_fraction}, XLA: {self.use_xla}"
)
return config
@mod.export(funcify=True)
class SessionFromGraph(BaseLoader):
"""
Functor that creates a TensorFlow session that can be used for inference.
"""
def __init__(self, graph, config=None):
"""
Creates a TensorFlow session.
Args:
graph (Union[Tuple[tf.Graph, Sequence[str]], Callable() -> Tuple[tf.Graph, Sequence[str]]]):
A tuple containing a TensorFlow graph and output names or a callable that returns one.
config (Union[tf.ConfigProto, Callable() -> tf.ConfigProto]):
A TensorFlow ConfigProto or a callable that returns one.
"""
self.graph = graph
self.config = util.default(config, CreateConfig())
@util.check_called_by("__call__")
def call_impl(self):
"""
Returns:
tf.Session: The TensorFlow session.
"""
config, _ = util.invoke_if_callable(self.config)
(graph, output_names), _ = util.invoke_if_callable(self.graph)
with graph.as_default() as graph, tf.compat.v1.Session(
graph=graph, config=config
).as_default() as sess:
G_LOGGER.verbose(f"Using TensorFlow outputs: {output_names}")
G_LOGGER.extra_verbose("Initializing variables in TensorFlow Graph")
sess.run(tf.compat.v1.initializers.global_variables())
return sess, output_names
@@ -0,0 +1 @@
tensorflow<2.0
@@ -0,0 +1,107 @@
#
# 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.
#
# Sets up everything needed to perform inference in TensorFlow.
import os
import time
from collections import OrderedDict
from polygraphy import mod, util
from polygraphy.backend.base import BaseRunner
from polygraphy.backend.tf import util as tf_util
from polygraphy.logger import G_LOGGER
tf = mod.lazy_import("tensorflow<2.0")
@mod.export()
class TfRunner(BaseRunner):
"""
Runs inference using a TensorFlow session.
"""
def __init__(self, sess, timeline_dir=None, name=None):
"""
Args:
sess (Union[Tuple[tf.Session, Sequence[str]], Callable() -> Tuple[tf.Session, Sequence[str]]]):
A tuple containing a TensorFlow session and output names or a callable that returns one.
timeline_dir (str):
Path to write a TensorFlow timeline.
Note that profiling may affect execution time.
name (str):
The human-readable name prefix to use for this runner.
A runner count and timestamp will be appended to this prefix.
"""
super().__init__(name=name, prefix="tf-runner")
self._sess = sess
self.timeline_dir = timeline_dir
self.num_inferences = 0
self.run_options = None
self.run_metadata = None
if self.timeline_dir is not None:
# Enable profiling
G_LOGGER.warning("Profiling is enabled. This will impact performance")
self.run_options = tf.RunOptions(trace_level=tf.RunOptions.FULL_TRACE)
self.run_metadata = tf.RunMetadata()
@util.check_called_by("activate")
def activate_impl(self):
(self.sess, self.output_names), _ = util.invoke_if_callable(self._sess)
@util.check_called_by("get_input_metadata")
def get_input_metadata_impl(self):
return tf_util.get_input_metadata(self.sess.graph)
@util.check_called_by("infer")
def infer_impl(self, feed_dict):
G_LOGGER.extra_verbose(f"Received feed_dict: {feed_dict}")
start = time.time()
inference_outputs = self.sess.run(
self.output_names,
feed_dict=feed_dict,
options=self.run_options,
run_metadata=self.run_metadata,
)
end = time.time()
out_dict = OrderedDict()
for name, out in zip(self.output_names, inference_outputs):
out_dict[name] = out
self.inference_time = end - start
if self.timeline_dir is not None:
from tensorflow.python.client import timeline
t1 = timeline.Timeline(self.run_metadata.step_stats)
util.save_file(
contents=t1.generate_chrome_trace_format(),
dest=os.path.join(self.timeline_dir, f"run-{self.num_inferences}"),
mode="w",
)
self.num_inferences += 1
return out_dict
@util.check_called_by("deactivate")
def deactivate_impl(self):
self.sess.close()
del (self.sess, self.output_names)
self.num_inferences = 0
@@ -0,0 +1,204 @@
#
# 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.
#
from collections import defaultdict
from polygraphy import mod, util
from polygraphy.common import TensorMetadata
from polygraphy.logger import G_LOGGER
tf = mod.lazy_import("tensorflow<2.0")
def load_graph(path):
"""
Loads a TensorFlow frozen model.
Args:
path (Union[str, tf.Graph, tf.GraphDef]):
A path to the frozen model, or a frozen TensorFlow graph or graphdef.
Returns:
tf.Graph: The TensorFlow graph
"""
if isinstance(path, tf.Graph):
return path
if isinstance(path, str):
graphdef = tf.compat.v1.GraphDef()
import google
try:
graphdef.ParseFromString(util.load_file(path, description="GraphDef"))
except google.protobuf.message.DecodeError:
G_LOGGER.backtrace()
G_LOGGER.critical(
f"Could not import TensorFlow GraphDef from: {path}. Is this a valid TensorFlow model?"
)
elif isinstance(path, tf.compat.v1.GraphDef):
graphdef = path
with tf.Graph().as_default() as graph:
tf.import_graph_def(graphdef, name="")
return graph
def find_nodes_by_ops(graphdef, ops):
ops = set(ops)
return [node for node in graphdef.node if any([op in node.op for op in ops])]
def map_node_outputs(graphdef):
def sanitize_input_name(input_name):
# Strip port information and control symbol
split_input = input_name.split(":")
if len(split_input) > 1:
split_input.pop(-1)
return ":".join(split_input).replace("^", "")
node_outputs = defaultdict(list)
for node in graphdef.node:
for input_name in node.input:
node_outputs[sanitize_input_name(input_name)].append(node)
return node_outputs
def get_tensor_metadata(tensors):
metadata = TensorMetadata()
for tensor in tensors:
try:
shape = [
elem.value if hasattr(elem, "value") else elem for elem in tensor.shape
]
except ValueError:
# Happens when rank is unknown
shape = None
metadata.add(tensor.name, dtype=tensor.dtype.as_numpy_dtype, shape=shape)
return metadata
def get_input_metadata(graph):
input_tensors = []
input_nodes = find_nodes_by_ops(graph.as_graph_def(), ["Placeholder", "FIFOQueue"])
G_LOGGER.verbose(
f"Found input tensors: {[f'{n.name}: {n.op}' for n in input_nodes]}"
)
for node in input_nodes:
input_tensors.append(graph.get_tensor_by_name(node.name + ":0"))
G_LOGGER.verbose(f"Retrieved TensorFlow input_tensors: {input_tensors}")
return get_tensor_metadata(input_tensors)
def get_output_metadata(graph, layerwise=False):
graphdef = graph.as_graph_def()
node_output_map = map_node_outputs(graphdef)
def is_output_node(node):
# Make sure that we're not using hanging nodes as outputs - must have at least one input.
if len(node_output_map[node.name]) != 0 or len(node.input) == 0:
return False
# Tensors with no shape cannot be outputs and TensorFlow doesn't like certain ops as outputs.
EXCLUDE_OPS = [
"Switch",
"FusedBatchNorm",
"Assert",
"NextIteration",
"Enter",
"LoopCond",
"Exit",
"Print",
"Assign",
"NoOp",
"ReadVariableOp",
"VarIsInitializedOp",
"Const",
]
# Additionally, we sometimes need to exclude entire namespaces e.g. while loops.
EXCLUDE_NAMESPACES = ["while", "Assert"]
if any([ex_op in node.op for ex_op in EXCLUDE_OPS]) or any(
[ns in node.name for ns in EXCLUDE_NAMESPACES]
):
G_LOGGER.extra_verbose(
f"Excluding {node.name}, op {node.op} is not a valid output op or is part of an excluded namespace (Note: excluded namespaces: {EXCLUDE_NAMESPACES})"
)
return False
return True
# For layerwise mode, every layer becomes an output.
if layerwise:
output_nodes = list(graphdef.node)
G_LOGGER.verbose(
f"Running in layerwise mode. Marking {len(output_nodes)} layers as potential outputs"
)
else:
output_nodes = [node for node in graphdef.node if is_output_node(node)]
G_LOGGER.extra_verbose(f"Found likely output nodes: {output_nodes}")
output_tensors = []
for node in output_nodes:
tensor_name = node.name + ":0"
try:
tensor = graph.get_tensor_by_name(tensor_name)
output_tensors.append(tensor)
except KeyError:
G_LOGGER.warning(f"Could not import: {tensor_name}. Skipping.")
if len(output_tensors) != len(output_nodes):
G_LOGGER.warning(
f"Excluded {len(output_nodes) - len(output_tensors)} ops that don't seem like outputs. Use -vv/--super-verbose, or set logging verbosity to EXTRA_VERBOSE to view them."
)
G_LOGGER.extra_verbose(
f"Found output op types in graph: {set(tensor.op.type for tensor in output_tensors)}"
)
G_LOGGER.verbose(f"Retrieved TensorFlow output_tensors: {output_tensors}")
return get_tensor_metadata(output_tensors)
def get_graph_output_names(graph):
return list(get_output_metadata(graph).keys())
def str_from_graph(graph, show_layers=None, show_attrs=None, show_weights=None):
show_layers = util.default(show_layers, False)
show_attrs = util.default(show_attrs, False)
show_weights = util.default(show_weights, False)
graph_str = ""
input_metadata = get_input_metadata(graph)
output_metadata = get_output_metadata(graph)
graph_str += f"---- {len(input_metadata)} Graph Inputs ----\n{input_metadata}\n\n"
graph_str += (
f"---- {len(output_metadata)} Graph Outputs ----\n{output_metadata}\n\n"
)
graph_str += f"---- {len(graph.as_graph_def().node)} Nodes ----\n"
if show_layers:
G_LOGGER.warning(
"Displaying layer information is unsupported for TensorFlow graphs. "
"Please use --show layers attrs weights if you would like to see the raw nodes"
)
if show_attrs or show_weights:
for node in graph.as_graph_def().node:
graph_str += str(node) + "\n"
graph_str += "\n"
return util.indent_block(graph_str, level=0)
@@ -0,0 +1,9 @@
from polygraphy.backend.trt.algorithm_selector import *
from polygraphy.backend.trt.calibrator import *
from polygraphy.backend.trt.config import *
from polygraphy.backend.trt.file_reader import *
from polygraphy.backend.trt.loader import *
from polygraphy.backend.trt.profile import *
from polygraphy.mod.trt_importer import *
from polygraphy.backend.trt.runner import *
from polygraphy.backend.trt.util import *
@@ -0,0 +1,422 @@
#
# 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.
#
from polygraphy import func, mod, util, constants
from polygraphy.backend.trt import util as trt_util
from polygraphy.mod.trt_importer import lazy_import_trt
from polygraphy.common.interface import TypedDict
from polygraphy.json import Decoder, Encoder, add_json_methods
from polygraphy.logger import G_LOGGER, LogMode
from typing import Sequence
trt = lazy_import_trt()
##
## Data Structures
##
#
# NOTE: Modifying the structure of the data classes below will break backwards compatiblity
#
def check_is_instance(obj, cls, name):
if not isinstance(obj, cls):
G_LOGGER.critical(
f"'{name}' must be an instance of {cls.__name__}, but is: {obj}."
)
@mod.export()
class TensorInfo:
"""
Tracks information about a tensor, such as format and data type.
"""
@staticmethod
def from_trt(io_info):
"""
Creates a Polygraphy ``TensorInfo`` instance from a TensorRT ``IAlgorithmIOInfo``.
Args:
io_info (trt.IAlgorithmIOInfo): The algorithm I/O information.
Returns:
TensorInfo
"""
return TensorInfo(
io_info.dtype,
tuple(io_info.strides),
# These fields were added in 8.6
util.try_getattr(io_info, "vectorized_dim"),
util.try_getattr(io_info, "components_per_element"),
)
def __init__(self, dtype, strides, vectorized_dim, components_per_element):
"""
Args:
dtype (trt.DataType): The data type.
strides (Sequence[int]): The strides.
vectorized_dim (int): The index of the vectorized dimensions.
components_per_element (int): The number of components per element.
"""
check_is_instance(dtype, trt.DataType, "dtype")
check_is_instance(strides, Sequence, "strides")
if vectorized_dim is not None:
check_is_instance(vectorized_dim, int, "vectorized_dim")
if components_per_element is not None:
check_is_instance(components_per_element, int, "components_per_element")
self.dtype = dtype
self.strides = tuple(strides)
self.vectorized_dim = vectorized_dim
self.components_per_element = components_per_element
def __eq__(self, other):
return self.__dict__ == other.__dict__
def __repr__(self):
return f"TensorInfo({str(self.dtype)}, {self.strides}, {self.vectorized_dim}, {self.components_per_element})"
def __hash__(self):
return hash(
(self.dtype, self.strides, self.vectorized_dim, self.components_per_element)
)
@Encoder.register(TensorInfo)
def encode(tensor_info):
return {
"dtype": str(tensor_info.dtype),
"strides": tensor_info.strides,
"vectorized_dim": tensor_info.vectorized_dim,
"components_per_element": tensor_info.components_per_element,
}
@Decoder.register(TensorInfo)
def decode(dct):
return TensorInfo(
util.getattr_nested(trt, dct["dtype"]),
dct["strides"],
dct["vectorized_dim"],
dct["components_per_element"],
)
@mod.export()
class Algorithm:
"""
Represents a TensorRT algorithm variant, which can be uniquely represented
by an implementation ID, tactic ID, and I/O tensor information.
"""
@staticmethod
def from_trt(context, algorithm):
"""
Creates a Polygraphy ``Algorithm`` instance from a TensorRT
``IAlgorithmContext`` and ``IAlgorithm``.
Args:
context (trt.IAlgorithmContext):
The algorithm context corresponding to the layer.
algorithm (trt.IAlgorithm):
The algorithm variant provided by TensorRT.
Returns:
Algorithm
"""
implementation = algorithm.algorithm_variant.implementation
tactic = algorithm.algorithm_variant.tactic
inputs = tuple(
TensorInfo.from_trt(algorithm.get_algorithm_io_info(i))
for i in range(context.num_inputs)
)
outputs = tuple(
TensorInfo.from_trt(algorithm.get_algorithm_io_info(i))
for i in range(context.num_inputs, context.num_inputs + context.num_outputs)
)
return Algorithm(implementation, tactic, inputs, outputs)
def __init__(self, implementation, tactic, inputs, outputs):
"""
Args:
implementation (int):
The implementation for this Algorithm.
tactic (int):
The tactic for this Algorithm.
inputs (Sequence[TensorInfo]):
A sequence of TensorInfos for each input.
outputs (Sequence[TensorInfo]):
A sequence of TensorInfos for each output.
"""
self.implementation = implementation
self.tactic = tactic
def check_io(lst, name):
for index, io in enumerate(lst):
check_is_instance(io, TensorInfo, f"{name}[{index}]")
check_io(inputs, "inputs")
check_io(outputs, "outputs")
# Use tuples here so the class is hashable.
self.inputs = tuple(inputs)
self.outputs = tuple(outputs)
def __str__(self):
return f"(Implementation: {self.implementation}, Tactic: {self.tactic}) | Inputs: {self.inputs} | Outputs: {self.outputs}"
def __eq__(self, other):
return self.__dict__ == other.__dict__
def __hash__(self):
return hash((self.implementation, self.tactic, self.inputs, self.outputs))
@Encoder.register(Algorithm)
def encode(algo):
return {
"implementation": algo.implementation,
"tactic": algo.tactic,
"inputs": algo.inputs,
"outputs": algo.outputs,
}
@Decoder.register(Algorithm)
def decode(dct):
return Algorithm(
implementation=dct["implementation"],
tactic=dct["tactic"],
inputs=dct["inputs"],
outputs=dct["outputs"],
)
@mod.export()
@add_json_methods("tactic replay file")
class TacticReplayData(TypedDict(lambda: str, lambda: Algorithm)):
"""
Maps layer names to corresponding tactics.
More specifically, it is an ``OrderedDict[str, Algorithm]``.
"""
def add(self, name, algorithm):
"""
Add an entry into the tactic replay data.
Args:
name (str): The name of the layer
algorithm (Algorithm): The algorithm to use for the layer.
Returns:
TacticReplayData: self, to allow for method chaining.
"""
self[name] = algorithm
return self
def __str__(self):
return "\n".join(
[
f"Layer: {name}\n{constants.TAB}Algorithm: {algorithm}"
for (name, algorithm) in self.items()
]
)
@Encoder.register(TacticReplayData)
def encode(replay):
return {"replay": replay.dct}
@Decoder.register(TacticReplayData)
def decode(dct):
return TacticReplayData(dct["replay"])
##
## Algorithm Selectors
##
# Everything is encapsulated in functions so that we don't create a dependency on TensorRT
# when objects from this file are imported.
def get_base_selector_type():
class BaseSelector(trt.IAlgorithmSelector):
def __init__(self, data):
# Must explicitly initialize parent for any trampoline class! Will mysteriously segfault without this.
trt.IAlgorithmSelector.__init__(self)
self.path = None
self.data = TacticReplayData()
if isinstance(data, TacticReplayData):
self.data = data
else:
self.path = data
def select_algorithms(self, context, choices):
return list(range(len(choices)))
return BaseSelector
@mod.deprecate(remove_in="0.50.0", use_instead=None)
@mod.export()
def TacticRecorder(record):
"""
A TensorRT algorithm selector that can record tactics selected by TensorRT.
The generated tactic replay file is specific to network and builder configuration.
Changing either of these may render the tactic replay file unusable.
Args:
record (Union[path, file-like, TacticReplayData]):
A path or file-like object or an empty ``TacticReplayData`` instance.
Tactics will be recorded and stored here.
"""
class TacticRecorderClass(get_base_selector_type()):
def __init__(self):
super().__init__(record)
# The function that constructed this instance
self.make_func = TacticRecorder
@G_LOGGER.log_exception
def report_algorithms(self, contexts, choices):
"""
Records algorithms selected by TensorRT into the provided path or
``TacticReplayData`` instance.
Args:
contexts (List[trt.IAlgorithmContext]):
The list of TensorRT algorithm contexts. Generally, there is one per layer.
choices (List[trt.IAlgorithm]):
A list of selected algorithms for each context.
Returns:
None
"""
for context, choice in zip(contexts, choices):
self.data.add(context.name, Algorithm.from_trt(context, choice))
if self.path is not None:
self.data.save(self.path)
return TacticRecorderClass()
@mod.deprecate(remove_in="0.50.0", use_instead=None)
@mod.export()
def TacticReplayer(replay):
"""
A TensorRT algorithm selector that can replay tactics according to a tactic replay file.
Args:
replay (Union[path, file-like, TacticReplayData]):
A path or file-like object containing a JSON-ified ``TacticReplayData`` instance,
or a ``TacticReplayData`` instance.
"""
class TacticReplayerClass(get_base_selector_type()):
def __init__(self):
super().__init__(replay)
if self.path is not None:
self.data = TacticReplayData.load(self.path)
# The function that constructed this instance
self.make_func = TacticReplayer
@G_LOGGER.log_exception
@func.constantmethod
def select_algorithms(self, context, choices):
"""
Selects an algorithm based on ``self.data`` if possible. Otherwise, returns
default tactics.
Args:
context (trt.IAlgorithmContext):
The TensorRT algorithm context.
choices (List[trt.IAlgorithm]):
A list of TensorRT algorithm choices.
Returns:
List[int]:
The indices of selected tactics. If ``self.data`` includes the layer and
TensorRT provides a matching tactic, this will always be of length 1.
Raises:
PolygraphyException:
If a tactic is set for a layer in ``self.data`` but is not provided by
TensorRT as a choice for that layer.
"""
default_choices = super().select_algorithms(context, choices)
if not self.data: # No replay data, we are in recording mode.
return default_choices
if context.name not in self.data:
G_LOGGER.warning(
f"Layer: {context.name} was not found in the tactic replay. Falling back to default tactics."
)
sep = f"\n{constants.TAB}"
G_LOGGER.warning(
"Has the network changed since the tactic replay file was generated?\n"
f"Note: Layers in the tactic replay are:{sep}{sep.join(self.data.keys())}",
mode=LogMode.ONCE,
)
return default_choices
# Need to find the index of the tactic we want.
to_select = self.data[context.name]
tactic_choices = [Algorithm.from_trt(context, algo) for algo in choices]
if to_select not in tactic_choices:
sep = f"\n{constants.TAB}"
G_LOGGER.critical(
f"Layer: {context.name} | Tactic in replay was not provided by TensorRT as a choice for this layer.\n"
f"Has the network or builder configuration changed since the replay file was generated?\n"
f"Note: Tactic in replay was:{sep}{to_select}\nProvided choices were:{sep}{sep.join(map(str, tactic_choices))}"
)
return [tactic_choices.index(to_select)]
@G_LOGGER.log_exception
@func.constantmethod
def report_algorithms(self, contexts, choices):
"""
Checks if the tactics specified in ``self.data`` were selected and raises an exception
if not.
Raises:
PolygraphyException:
If a tactic specified in ``self.data`` was not selected for a layer.
"""
for context, choice in zip(contexts, choices):
if context.name in self.data:
to_select = self.data[context.name]
selected = Algorithm.from_trt(context, choice)
if to_select != selected:
G_LOGGER.critical(
f"Layer: {context.name} | TensorRT selected a tactic different than the one specified in the tactic replay."
f"\nNote: Tactic in replay was:\n{constants.TAB}{to_select}, but TensorRT selected:\n{constants.TAB}{selected}"
)
return TacticReplayerClass()
@@ -0,0 +1,295 @@
#
# 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 contextlib
import copy
from collections import OrderedDict
from polygraphy import mod, util
from polygraphy.backend.base import util as base_util
from polygraphy.backend.trt import util as trt_util
from polygraphy.mod.trt_importer import lazy_import_trt
from polygraphy.datatype import DataType
from polygraphy.exception import PolygraphyException
from polygraphy.logger import G_LOGGER, LogMode
trt = lazy_import_trt()
np = mod.lazy_import("numpy")
@mod.export()
def Calibrator(
data_loader,
cache=None,
BaseClass=None,
batch_size=None,
quantile=None,
regression_cutoff=None,
algo=None,
):
"""
Supplies calibration data to TensorRT to calibrate the network for INT8 inference.
Args:
data_loader (Sequence[OrderedDict[str, Union[numpy.ndarray, DeviceView, torch.Tensor, int]]]):
A generator or iterable that yields a dictionary that maps input names to NumPy
arrays, Polygraphy DeviceViews, PyTorch tensors, or GPU pointers. If NumPy arrays,
DeviceViews, or PyTorch tensors are provided, the calibrator will check the data types
and shapes if possible to ensure that they match those expected by the model.
In case you don't know details about the inputs ahead of time, you can access the
`input_metadata` property in your data loader, which will be set to a ``TensorMetadata``
instance by Polygraphy APIs like ``CreateConfig`` and ``EngineFromNetwork``.
Note that this does not work for generators or lists.
The number of calibration batches is controlled by the number of items supplied
by the data loader.
cache (Union[str, file-like]):
Path or file-like object to save/load the calibration cache.
By default, the calibration cache is not saved.
BaseClass (type):
The type of calibrator to inherit from.
Defaults to ``trt.IInt8EntropyCalibrator2``.
batch_size (int):
[DEPRECATED] The size of each batch provided by the data loader.
quantile (float):
The quantile to use for ``trt.IInt8LegacyCalibrator``.
Has no effect for other calibrator types.
Defaults to 0.5.
regression_cutoff (float):
The regression cutoff to use for ``trt.IInt8LegacyCalibrator``.
Has no effect for other calibrator types.
Defaults to 0.5.
algo (trt.CalibrationAlgoType):
Calibration algorithm to use for ``trt.IInt8Calibrator``.
Has no effect for other calibrator types.
Defaults to ``trt.CalibrationAlgoType.MINMAX_CALIBRATION``.
"""
BaseClass = util.default(BaseClass, trt.IInt8EntropyCalibrator2)
class CalibratorClass(BaseClass):
"""
Calibrator that supplies calibration data to TensorRT to calibrate the network for INT8 inference.
"""
def __init__(self):
# Must explicitly initialize parent for any trampoline class! Will mysteriously segfault without this.
BaseClass.__init__(self) # type: ignore
self.data_loader = data_loader
self._cache = cache
self.device_buffers = OrderedDict()
self.input_metadata = None
self.reset()
G_LOGGER.verbose(f"Created calibrator [cache={self._cache}]")
self.batch_size = util.default(batch_size, 1)
self.is_polygraphy_calibrator = True
# The function that constructed this instance
self.make_func = Calibrator
def set_input_metadata(self, input_metadata):
"""
Sets the input metadata for the calibrator.
This is passed along to the data loader and is also used for
input data type and shape checks.
NOTE: This generally does not need to be called manually if the calibrator is being used
with Polygraphy's loaders, like ``CreateConfig`` or ``EngineFromNetwork``.
Args:
input_metadata (TensorMetadata):
Mapping of input names to their data types and shapes.
Passed along to the data loader if provided. This is required if
using Polygraphy's included `DataLoader` to provide calibration data,
or if data type and shape checking is desired.
"""
calibration_metadata = copy.copy(input_metadata)
for name, meta_tuple in calibration_metadata.items():
if meta_tuple.dtype not in {
DataType.FLOAT32,
DataType.INT32,
DataType.INT64,
DataType.BOOL,
}:
G_LOGGER.warning(
f"TensorRT requires non-index calibration inputs to be provided in float32. "
f"Input: {name} has datatype: {meta_tuple.dtype}, so will override to float32 in the calibrator's metadata. "
f"If you are using a custom data loader with the calibrator, please ensure that you return a float32 tensor for this input."
)
meta_tuple.dtype = DataType.FLOAT32
self.input_metadata = calibration_metadata
if calibration_metadata is not None:
with contextlib.suppress(AttributeError):
self.data_loader.input_metadata = calibration_metadata
def reset(self):
"""
Reset this calibrator for reuse.
The calibrator will clear any dynamic ranges cached from previous calibration runs, and will
attempt to rewind the data loader (note that generators cannot be rewound).
Typically, this is only required if the same calibrator is used for multiple different networks.
"""
# Attempt to reset data loader
self.data_loader_iter = iter(self.data_loader)
self.num_batches = 0
# Make sure calibrator will check the cache again when reset.
self.cache_contents = None
def get_batch_size(self):
return self.batch_size
def _get_batch_impl(self, names):
try:
buffers = next(self.data_loader_iter)
except StopIteration:
if not self.num_batches:
G_LOGGER.critical(
"Calibrator data loader provided no data.\nPossible reasons for this include:\n(1) data loader "
"has no data to provide\n(2) data loader was a generator, and the calibrator is being "
"used multiple times (generators cannot be rewound)"
)
return None
self.num_batches += 1
if self.input_metadata is not None:
base_util.check_inputs(buffers, self.input_metadata)
ptrs = []
for name in names:
buf = buffers[name]
if isinstance(buf, int):
ptrs.append(buf)
else:
ptrs.append(
trt_util._get_array_on_gpu(buf, name, self.device_buffers)
)
return ptrs
def get_batch(self, names):
ptrs = None
try:
ptrs = self._get_batch_impl(names)
except PolygraphyException:
pass
if ptrs is None:
self.free()
return ptrs
def read_calibration_cache(self):
def load_from_cache():
if self._cache is None or not util.get_file_size(self._cache):
return None
try:
return util.load_file(self._cache, description="calibration cache")
except Exception as err:
G_LOGGER.error(
f"Could not read from calibration cache: {self._cache}\nNote: Error was: {err}"
)
return None
if self.cache_contents is not None:
return self.cache_contents
self.cache_contents = load_from_cache()
if not self.cache_contents:
if self.cache_contents is not None:
G_LOGGER.warning(
"Calibration cache was provided, but is empty. "
"Will regenerate scales by running calibration.",
mode=LogMode.ONCE,
)
self.cache_contents = None
return self.cache_contents
def write_calibration_cache(self, cache):
self.cache_contents = cache.tobytes()
if self._cache is None:
return
try:
util.save_file(
contents=self.cache_contents,
dest=self._cache,
description="calibration cache",
)
except Exception as err:
G_LOGGER.error(
f"Could not write to calibration cache: {self._cache}.\nNote: Error was: {err}"
)
def free(self):
"""
Frees all device buffers associated with this calibrator
"""
for device_buffer in self.device_buffers.values():
device_buffer.free()
def __enter__(self):
return self
def __exit__(self, exc_type, exc_value, traceback):
self.free()
# IInt8LegacyCalibrator methods
if BaseClass == trt.IInt8LegacyCalibrator:
def get_quantile(self):
return util.default(quantile, 0.5)
def get_regression_cutoff(self):
return util.default(regression_cutoff, 0.5)
def read_histogram_cache(self, length):
pass
def write_histogram_cache(self, ptr, length):
pass
# IInt8Calibrator methods
if BaseClass == trt.IInt8Calibrator:
def get_algorithm(self):
return util.default(algo, trt.CalibrationAlgoType.ENTROPY_CALIBRATION_2)
def __repr__(self):
return util.make_repr(
"Calibrator",
data_loader,
cache=cache,
BaseClass=BaseClass,
batch_size=batch_size,
quantile=quantile,
regression_cutoff=regression_cutoff,
algo=algo,
)[0]
return CalibratorClass()
@@ -0,0 +1,657 @@
#
# 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 contextlib
import copy
import re
from polygraphy import config as polygraphy_config, mod, util
from polygraphy.backend.base import BaseLoader
from polygraphy.backend.trt import util as trt_util
from polygraphy.backend.trt.profile import Profile
from polygraphy.backend.trt.util import inherit_and_extend_docstring
from polygraphy.mod.trt_importer import lazy_import_trt
from polygraphy.logger import G_LOGGER
trt = lazy_import_trt()
class _CreateConfigCommon(BaseLoader):
"""
Generic TensorRT IBuilderConfig.
"""
def __init__(
self,
profiles=None,
precision_constraints=None,
load_timing_cache=None,
algorithm_selector=None,
sparse_weights=None,
tactic_sources=None,
restricted=None,
profiling_verbosity=None,
memory_pool_limits=None,
refittable=None,
strip_plan=None,
preview_features=None,
engine_capability=None,
direct_io=None,
builder_optimization_level=None,
hardware_compatibility_level=None,
max_aux_streams=None,
version_compatible=None,
exclude_lean_runtime=None,
quantization_flags=None,
error_on_timing_cache_miss=None,
disable_compilation_cache=None,
progress_monitor=None,
weight_streaming=None,
runtime_platform=None,
tiling_optimization_level=None,
):
"""
Creates an IBuilderConfig that can be used by EngineFromNetwork.
Args:
profiles (List[Profile]):
A list of optimization profiles to add to the configuration. Only needed for
networks with dynamic input shapes. If this is omitted for a network with
dynamic shapes, a default profile is created, where dynamic dimensions are
replaced with Polygraphy's DEFAULT_SHAPE_VALUE (defined in constants.py).
A partially populated profile will be automatically filled using values from ``Profile.fill_defaults()``
See ``Profile`` for details.
precision_constraints (Optional[str]):
If set to "obey", require that layers execute in specified precisions.
If set to "prefer", prefer that layers execute in specified precisions but allow TRT to fall back to
other precisions if no implementation exists for the requested precision.
Otherwise, precision constraints are ignored.
Defaults to None.
load_timing_cache (Union[str, file-like]):
A path or file-like object from which to load a tactic timing cache.
Providing a tactic timing cache can speed up the engine building process.
Caches can be generated while building an engine with, for example, EngineFromNetwork.
If a path is provided, the file will be locked for exclusive access so that other processes
cannot update the cache while it is being read.
If the file specified by the path does not exist, CreateConfig will emit a warning and fall back
to using an empty timing cache.
algorithm_selector (trt.IAlgorithmSelector):
An algorithm selector. Allows the user to control how tactics are selected
instead of letting TensorRT select them automatically.
sparse_weights (bool):
Whether to enable optimizations for sparse weights.
Defaults to False.
tactic_sources (List[trt.TacticSource]):
The tactic sources to enable. This controls which libraries (e.g. cudnn, cublas, etc.)
TensorRT is allowed to load tactics from.
Use an empty list to disable all tactic sources.
Defaults to TensorRT's default tactic sources.
restricted (bool):
Whether to enable safety scope checking in the builder. This will check if the network
and builder configuration are compatible with safety scope.
Defaults to False.
profiling_verbosity (trt.ProfilingVerbosity):
The verbosity of NVTX annotations in the generated engine.
Higher verbosity allows you to determine more information about the engine.
Defaults to ``trt.ProfilingVerbosity.VERBOSE``.
memory_pool_limits (Dict[trt.MemoryPoolType, int]):
Limits for different memory pools.
This should be a mapping of pool types to their respective limits in bytes.
refittable (bool):
Enables the engine to be refitted with new weights after it is built.
Defaults to False.
strip_plan (bool):
Strips the refittable weights from the engine plan file.
Defaults to False.
preview_features (List[trt.PreviewFeature]):
The preview features to enable.
Use an empty list to disable all preview features.
Defaults to TensorRT's default preview features.
engine_capability (trt.EngineCapability):
The engine capability to build for.
Defaults to the default TensorRT engine capability.
direct_io (bool):
Whether to disallow reformatting layers at network input/output tensors with
user-specified formats.
Defaults to False.
builder_optimization_level (int):
The builder optimization level. A higher optimization level allows the optimizer to spend more time
searching for optimization opportunities. The resulting engine may have better performance compared
to an engine built with a lower optimization level.
Refer to the TensorRT API documentation for details.
Defaults to TensorRT's default optimization level.
hardware_compatibility_level (trt.HardwareCompatibilityLevel):
The hardware compatibility level. This allows engines built on one GPU architecture to work on GPUs
of other architectures.
Defaults to TensorRT's default hardware compatibility level.
max_aux_streams (int):
The maximum number of auxiliary streams that TensorRT is allowed to use. If the network contains
operators that can run in parallel, TRT can execute them using auxiliary streams in addition to the
one provided to the IExecutionContext::enqueueV3() call.
The default maximum number of auxiliary streams is determined by the heuristics in TensorRT on
whether enabling multi-stream would improve the performance.
version_compatible (bool):
Whether to build an engine that is version compatible.
exclude_lean_runtime (bool):
Whether to exclude the lean runtime in version compatible engines.
Requires that version compatibility is enabled.
quantization_flags (List[trt.QuantizationFlag]):
The quantization flags to enable.
Use an empty list to disable all quantization flags.
Defaults to TensorRT's default quantization flags.
error_on_timing_cache_miss (bool):
Emit error when a tactic being timed is not present in the timing cache.
This flag has an effect only when IBuilderConfig has an associated ITimingCache.
Defaults to False.
disable_compilation_cache (bool):
Whether to disable caching JIT-compiled code.
Defaults to False.
progress_monitor (trt.IProgressMonitor):
A progress monitor. Allow users to view engine building progress through CLI.
weight_streaming (bool):
TWhether to enable weight streaming for the TensorRT Engine.
runtime_platform (trt.RuntimePlatform):
Describes the intended runtime platform (operating system and CPU architecture) for the execution of the TensorRT engine.
TensorRT provides support for cross-platform engine compatibility when the target runtime platform is different from the build platform.
Defaults to TensorRT's default runtime platform.
tiling_optimization_level (trt.TilingOptimizationLevel):
The tiling optimization level. Setting a higher optimization level allows TensorRT to spend more building time for more tiling strategies.
Defaults to TensorRT's default tiling optimization level. Refer to the TensorRT API documentation for details.
"""
self.profiles = util.default(profiles, [Profile()])
self.precision_constraints = precision_constraints
self.restricted = util.default(restricted, False)
self.refittable = util.default(refittable, False)
self.strip_plan = util.default(strip_plan, False)
self.timing_cache_path = load_timing_cache
self.algorithm_selector = algorithm_selector
self.sparse_weights = util.default(sparse_weights, False)
self.tactic_sources = tactic_sources
self.profiling_verbosity = profiling_verbosity
self.memory_pool_limits = memory_pool_limits
self.preview_features = preview_features
self.engine_capability = engine_capability
self.direct_io = util.default(direct_io, False)
self.builder_optimization_level = builder_optimization_level
self.hardware_compatibility_level = hardware_compatibility_level
self.max_aux_streams = max_aux_streams
self.version_compatible = version_compatible
self.exclude_lean_runtime = exclude_lean_runtime
self.quantization_flags = quantization_flags
self.error_on_timing_cache_miss = util.default(
error_on_timing_cache_miss, False
)
self.disable_compilation_cache = util.default(disable_compilation_cache, False)
self.progress_monitor = progress_monitor
self.weight_streaming = weight_streaming
self.runtime_platform = runtime_platform
self.tiling_optimization_level = tiling_optimization_level
@util.check_called_by("__call__")
def call_impl(self, builder, network):
"""
Args:
builder (trt.Builder):
The TensorRT builder to use to create the configuration.
network (trt.INetworkDefinition):
The TensorRT network for which to create the config. The network is used to
automatically create a default optimization profile if none are provided.
Returns:
trt.IBuilderConfig: The TensorRT builder configuration.
"""
config = builder.create_builder_config()
def try_run(func, name):
try:
return func()
except AttributeError:
trt_util.fail_unavailable(f"{name} in {self.__class__.__name__}")
def try_set_flag(flag_name):
return try_run(
lambda: config.set_flag(getattr(trt.BuilderFlag, flag_name)),
flag_name.lower(),
)
if self.preview_features is not None:
for preview_feature in trt.PreviewFeature.__members__.values():
try_run(
lambda: config.set_preview_feature(
preview_feature, preview_feature in self.preview_features
),
"preview_features",
)
G_LOGGER.verbose("Setting TensorRT Optimization Profiles")
profiles = copy.deepcopy(self.profiles)
for profile in profiles:
# Last profile is used for set_calibration_profile.
calib_profile = profile.fill_defaults(network)
config.add_optimization_profile(calib_profile.to_trt(builder, network))
newline = "\n"
sep = ",\n"
G_LOGGER.info(
f"Configuring with profiles:[\n"
f"{util.indent_block(sep.join([f'Profile {index}:{newline}{util.indent_block(profile)}' for index, profile in enumerate(profiles)]))}\n]"
)
layer_with_precisions = {
layer.name: layer.precision.name
for layer in network
if layer.precision_is_set and not layer.type == trt.LayerType.SHAPE
}
if self.precision_constraints == "obey":
try_set_flag("OBEY_PRECISION_CONSTRAINTS")
elif self.precision_constraints == "prefer":
try_set_flag("PREFER_PRECISION_CONSTRAINTS")
elif layer_with_precisions:
G_LOGGER.warning(
"It looks like some layers in the network have compute precision set, but precision constraints were not enabled. "
"\nPrecision constraints must be set to 'prefer' or 'obey' for layer compute precision to take effect. "
f"\nNote: Layers and their requested precisions were: {layer_with_precisions}"
)
if self.restricted:
try_set_flag("SAFETY_SCOPE")
if self.refittable:
try_set_flag("REFIT")
if self.strip_plan:
try_set_flag("STRIP_PLAN")
if self.direct_io:
try_set_flag("DIRECT_IO")
if self.sparse_weights:
try_set_flag("SPARSE_WEIGHTS")
if self.profiling_verbosity is not None:
def set_profiling_verbosity():
config.profiling_verbosity = self.profiling_verbosity
try_run(set_profiling_verbosity, name="profiling_verbosity")
else:
try:
config.profiling_verbosity = trt.ProfilingVerbosity.DETAILED
except AttributeError:
pass
if self.memory_pool_limits is not None:
for pool_type, pool_size in self.memory_pool_limits.items():
try_run(
lambda: config.set_memory_pool_limit(pool_type, pool_size),
name="memory_pool_limits",
)
if self.tactic_sources is not None:
tactic_sources_flag = 0
for source in self.tactic_sources:
tactic_sources_flag |= 1 << int(source)
try_run(
lambda: config.set_tactic_sources(tactic_sources_flag),
name="tactic_sources",
)
try:
cache = None
if self.timing_cache_path:
try:
with util.LockFile(self.timing_cache_path):
timing_cache_data = util.load_file(
self.timing_cache_path, description="tactic timing cache"
)
cache = config.create_timing_cache(timing_cache_data)
except FileNotFoundError:
G_LOGGER.warning(
"Timing cache file {} not found, falling back to empty timing cache.".format(
self.timing_cache_path
)
)
if cache is None:
# Create an empty timing cache by default so it will be populated during engine build.
# This way, consumers of CreateConfig have the option to use the cache later.
cache = config.create_timing_cache(b"")
except AttributeError:
if self.timing_cache_path:
trt_util.fail_unavailable(f"load_timing_cache in {self.__class__.__name__}")
else:
config.set_timing_cache(cache, ignore_mismatch=False)
if self.algorithm_selector is not None:
def set_algo_selector():
config.algorithm_selector = self.algorithm_selector
try_run(set_algo_selector, name="algorithm_selector")
if not self.timing_cache_path:
G_LOGGER.warning(
"Disabling tactic timing cache because algorithm selector is enabled."
)
try_set_flag("DISABLE_TIMING_CACHE")
if self.engine_capability is not None:
def set_engine_cap():
config.engine_capability = self.engine_capability
try_run(set_engine_cap, "engine_capability")
if self.builder_optimization_level is not None:
def set_builder_optimization_level():
config.builder_optimization_level = self.builder_optimization_level
try_run(set_builder_optimization_level, "builder_optimization_level")
if self.hardware_compatibility_level is not None:
def set_hardware_compatibility_level():
config.hardware_compatibility_level = self.hardware_compatibility_level
try_run(set_hardware_compatibility_level, "hardware_compatibility_level")
if self.version_compatible:
try_set_flag("VERSION_COMPATIBLE")
if self.exclude_lean_runtime:
if not self.version_compatible:
G_LOGGER.critical(
f"Cannot set EXCLUDE_LEAN_RUNTIME if version compatibility is not enabled. "
)
try_set_flag("EXCLUDE_LEAN_RUNTIME")
if self.hardware_compatibility_level is not None or self.version_compatible:
G_LOGGER.info(
"Version or hardware compatibility was enabled. "
"If you are using an ONNX model, please set the NATIVE_INSTANCENORM ONNX parser flag, e.g. `--onnx-flags NATIVE_INSTANCENORM`"
)
if self.max_aux_streams is not None:
def set_max_aux_streams():
config.max_aux_streams = self.max_aux_streams
try_run(set_max_aux_streams, "max_aux_streams")
if self.quantization_flags is not None:
for quantization_flag in trt.QuantizationFlag.__members__.values():
if quantization_flag in self.quantization_flags:
try_run(
lambda: config.set_quantization_flag(quantization_flag),
"quantization_flag",
)
else:
try_run(
lambda: config.clear_quantization_flag(quantization_flag),
"quantization_flag",
)
if self.error_on_timing_cache_miss:
try_set_flag("ERROR_ON_TIMING_CACHE_MISS")
if self.disable_compilation_cache:
try_set_flag("DISABLE_COMPILATION_CACHE")
if self.progress_monitor is not None:
def set_progress_monitor():
config.progress_monitor = self.progress_monitor
try_run(set_progress_monitor, name="progress_monitor")
if self.weight_streaming:
try_set_flag("WEIGHT_STREAMING")
if self.runtime_platform is not None:
def set_runtime_platform():
config.runtime_platform = self.runtime_platform
try_run(set_runtime_platform, "runtime_platform")
if self.tiling_optimization_level is not None:
def set_tiling_optimization_level():
config.tiling_optimization_level = self.tiling_optimization_level
try_run(set_tiling_optimization_level, "tiling_optimization_level")
return config
@mod.export(funcify=True)
class CreateConfig(_CreateConfigCommon):
"""
Functor that creates an IBuilderConfig with TensorRT features.
"""
@inherit_and_extend_docstring(_CreateConfigCommon.__init__)
def __init__(
self,
tf32=None,
fp16=None,
int8=None,
fp8=None,
bf16=None,
calibrator=None,
use_dla=None,
allow_gpu_fallback=None,
**kwargs
):
"""
Creates an IBuilderConfig with TensorRT-specific features.
Args:
tf32 (bool):
Whether to enable TF32 precision. Defaults to False.
fp16 (bool):
Whether to enable FP16 precision. Defaults to False.
int8 (bool):
Whether to enable INT8 precision. Defaults to False.
fp8 (bool):
Whether to enable FP8 precision. Defaults to False.
bf16 (bool):
Whether to enable BF16 precision. Defaults to False.
calibrator (trt.IInt8Calibrator):
An int8 calibrator. Only required in int8 mode when
the network does not have explicit precision. For networks with
dynamic shapes, the last profile provided (or default profile if
no profiles are provided) is used during calibration.
use_dla (bool):
[EXPERIMENTAL] Whether to enable DLA as the default device type.
Defaults to False.
allow_gpu_fallback (bool):
[EXPERIMENTAL] When DLA is enabled, whether to allow layers to fall back to GPU if they cannot be run on DLA.
Has no effect if DLA is not enabled.
Defaults to False.
**kwargs: All other arguments from _CreateConfigCommon.
"""
super().__init__(**kwargs)
self.tf32 = util.default(tf32, False)
self.fp16 = util.default(fp16, False)
self.bf16 = util.default(bf16, False)
self.int8 = util.default(int8, False)
self.fp8 = util.default(fp8, False)
self.calibrator = calibrator
self.use_dla = util.default(use_dla, False)
self.allow_gpu_fallback = util.default(allow_gpu_fallback, False)
if self.calibrator is not None and not self.int8:
G_LOGGER.warning(
"A calibrator was provided to `CreateConfig`, but int8 mode was not enabled. "
"Did you mean to set `int8=True` to enable building with int8 precision?"
)
# Print a message to tell users that TF32 can be enabled to improve perf with minor accuracy differences.
if not self.tf32:
G_LOGGER.info(
"TF32 is disabled by default. Turn on TF32 for better performance with minor accuracy differences."
)
self._validator()
def _validator(self):
"""
Validates initialization parameters for TensorRT-specific features.
"""
# Validate that TensorRT-RTX specific flags are not used in regular TensorRT mode
if polygraphy_config.USE_TENSORRT_RTX:
if self.fp16 or self.int8 or self.bf16 or self.fp8:
G_LOGGER.critical("Precision flags (fp16, int8, bf16, fp8) are not supported with USE_TENSORRT_RTX=1.")
if self.use_dla:
G_LOGGER.critical("DLA is not supported with USE_TENSORRT_RTX=1.")
if self.calibrator is not None:
G_LOGGER.critical("Custom calibrator is not supported with USE_TENSORRT_RTX=1.")
def _configure_flags(self, builder, network, config):
"""
Validates and configures TensorRT-specific features.
Args:
builder (trt.Builder): The TensorRT builder
network (trt.INetworkDefinition): The TensorRT network
config (trt.IBuilderConfig): The TensorRT builder config to modify
"""
def try_run(func, name):
try:
return func()
except AttributeError:
trt_util.fail_unavailable(f"{name} in CreateConfig")
def try_set_flag(flag_name):
return try_run(
lambda: config.set_flag(getattr(trt.BuilderFlag, flag_name)),
flag_name.lower(),
)
# Add precision-related logic
if self.tf32:
try_set_flag("TF32")
else: # TF32 is on by default
with contextlib.suppress(AttributeError):
config.clear_flag(trt.BuilderFlag.TF32)
if self.fp16:
try_set_flag("FP16")
if self.bf16:
try_set_flag("BF16")
if self.fp8:
try_set_flag("FP8")
if self.int8:
try_set_flag("INT8")
if self.int8:
# No Q/DQ layers means that we will need to calibrate.
if not any(
layer.type in [trt.LayerType.QUANTIZE, trt.LayerType.DEQUANTIZE]
for layer in network
):
if self.calibrator is not None:
config.int8_calibrator = self.calibrator
try:
profiles = copy.deepcopy(self.profiles)
calib_profile = profiles[-1].fill_defaults(network)
config.set_calibration_profile(
calib_profile.to_trt(builder, network)
)
G_LOGGER.info(f"Using calibration profile: {calib_profile}")
except AttributeError:
G_LOGGER.extra_verbose(
"Cannot set calibration profile on TensorRT 7.0 and older."
)
trt_util.try_setup_polygraphy_calibrator(
config,
network,
calib_profile=calib_profile.to_trt(builder, network),
)
else:
G_LOGGER.warning(
"Network does not have explicit precision and no calibrator was provided. Please ensure "
"that tensors in the network have dynamic ranges set, or provide a calibrator in order to use int8 mode."
)
if self.use_dla:
config.default_device_type = trt.DeviceType.DLA
config.DLA_core = 0
if self.allow_gpu_fallback:
try_set_flag("GPU_FALLBACK")
@util.check_called_by("__call__")
def call_impl(self, builder, network):
"""
Callable implementation that creates and configures the IBuilderConfig with TensorRT features.
"""
config = super().call_impl(builder, network)
self._configure_flags(builder, network, config)
return config
@mod.export(funcify=True)
class PostprocessConfig(BaseLoader):
"""
[EXPERIMENTAL] Functor that applies a given post-processing function to a TensorRT ``IBuilderConfig``.
"""
def __init__(self, config, func):
"""
Applies a given post-processing function to a TensorRT ``IBuilderConfig``.
Args:
config (Union[trt.IBuilderConfig, Callable[[trt.Builder, trt.INetworkDefinition], trt.IBuilderConfig]):
A TensorRT IBuilderConfig or a callable that accepts a TensorRT builder and network and returns a config.
func (Callable[[trt.Builder, trt.INetworkDefinition, trt.IBuilderConfig], None])
A callable which takes a builder, network, and config parameter and modifies the config in place.
"""
self._config = config
# Sanity-check that the function passed in is callable
if not callable(func):
G_LOGGER.critical(
f"Object {func} (of type {type(func)}) is not a callable."
)
self._func = func
@util.check_called_by("__call__")
def call_impl(self, builder, network):
"""
Args:
builder (trt.Builder):
The TensorRT builder to use to create the configuration.
network (trt.INetworkDefinition):
The TensorRT network for which to create the config. The network is used to
automatically create a default optimization profile if none are provided.
Returns:
trt.IBuilderConfig:
The modified builder configuration.
"""
config, _ = util.invoke_if_callable(self._config, builder, network)
self._func(builder, network, config)
return config
@@ -0,0 +1,94 @@
#
# 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.
#
from pathlib import Path
from polygraphy import mod, util
from polygraphy.mod.trt_importer import lazy_import_trt
from polygraphy.logger import G_LOGGER
trt = lazy_import_trt()
@mod.export()
def FileReader(
filepath,
BaseClass=None,
):
"""
Class that supplies data to TensorRT from a stream. This may help reduce memory usage during deserialization.
Args:
filepath (str):
The path to the serialized file.
"""
BaseClass = util.default(BaseClass, trt.IStreamReader)
class FileReaderClass(BaseClass):
"""
Class that supplies data to TensorRT from a stream. This may help reduce memory usage during deserialization.
"""
def __init__(self):
# Must explicitly initialize parent for any trampoline class! Will mysteriously segfault without this.
BaseClass.__init__(self) # type: ignore
self.filepath = filepath
if not Path(self.filepath).exists():
G_LOGGER.error(f"File at {self.filepath} does not exist!")
self.mode = 'rb'
self.file = open(self.filepath, self.mode)
if not self.file:
G_LOGGER.error(f"Failed to open file at {self.filepath}!")
self.make_func = FileReader
def read(self, size: int) -> bytes:
return self.file.read(size)
def seek(self, offset: int, whence: int = 0) -> int:
"""
Seek to a position in the stream. Required for IStreamReaderV2.
Args:
offset: The offset to seek to
whence: How to interpret the offset (0=absolute, 1=relative to current, 2=relative to end)
Returns:
The new absolute position
"""
return self.file.seek(offset, whence)
def free(self):
if self.file:
self.file.close()
def __enter__(self):
return self
def __exit__(self, exc_type, exc_value, traceback):
self.free()
def __repr__(self):
return util.make_repr(
"FileReader",
self.filepath,
BaseClass=BaseClass,
)[0]
return FileReaderClass()
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,215 @@
#
# 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.
#
from polygraphy import constants, mod, util
from polygraphy.backend.trt import util as trt_util
from polygraphy.common.interface import TypedDict
from polygraphy.logger import G_LOGGER, LogMode
from fnmatch import fnmatch
@mod.export()
class ShapeTuple:
"""
Represents a set of shapes for a single binding in a profile.
"""
def __init__(self, min, opt, max):
"""
Args:
min (Tuple[int]): The minimum shape that the profile will support.
opt (Tuple[int]): The shape for which TensorRT will optimize the engine.
max (Tuple[int]): The maximum shape that the profile will support.
"""
self.min = min
self.opt = opt
self.max = max
def __str__(self):
return f"(min={self.min}, opt={self.opt}, max={self.max})"
def __repr__(self):
return type(self).__name__ + self.__str__()
def __iter__(self):
yield from [self.min, self.opt, self.max]
@mod.export()
class Profile(TypedDict(lambda: str, lambda: ShapeTuple)):
"""
An ordered dictionary that represents a single optimization profile that
can be used to build an engine.
More specifically, it is an ``OrderedDict[str, ShapeTuple]`` which maps binding
names to a set of min/opt/max shapes.
"""
def add(self, name, min, opt, max):
"""
A convenience function to add shapes for a single binding.
Args:
name (str): The name of the binding.
min (Tuple[int]): The minimum shape that the profile will support.
opt (Tuple[int]): The shape for which TensorRT will optimize the engine.
max (Tuple[int]): The maximum shape that the profile will support.
Returns:
Profile:
self, which allows this function to be easily chained to add multiple bindings,
e.g., Profile().add(...).add(...)
"""
self[name] = ShapeTuple(min, opt, max)
return self
def __getitem__(self, key):
"""
Retrieves the shapes registered for a given input name.
Returns:
ShapeTuple:
A named tuple including ``min``, ``opt``, and ``max`` members for the shapes
corresponding to the input.
"""
if key not in self:
G_LOGGER.critical(
f"Binding: {key} does not have shapes set in this profile"
)
return super().__getitem__(key)
def fill_defaults(self, network, default_shape_value=None):
"""
Fill this profile with sane default values for any bindings whose
shapes have not been set explicitly.
Args:
network (trt.INetworkDefinition):
The TensorRT network this profile is meant for.
This will be used to determine model inputs and their shapes.
default_shape_value (int):
The value to use to override dynamic dimensions.
Returns:
Profile: Self
"""
default_shape_value = util.default(
default_shape_value, constants.DEFAULT_SHAPE_VALUE
)
for idx in range(network.num_inputs):
inp = network.get_input(idx)
if any(fnmatch(inp.name, wc) for wc in self):
continue
with G_LOGGER.verbosity(G_LOGGER.CRITICAL): # WAR for spam from TRT
is_shape_tensor = inp.is_shape_tensor
if is_shape_tensor:
rank = inp.shape[0] if len(inp.shape) > 0 else 1
shape = (default_shape_value,) * rank
G_LOGGER.warning(
f"{trt_util.str_from_tensor(inp, is_shape_tensor)} | No values provided; "
f"Will use input values: {shape} for min/opt/max in profile.\n",
mode=LogMode.ONCE,
)
G_LOGGER.warning(
"This will cause the shape-tensor to have static values. If this is incorrect, please "
"set the range of values for this input shape-tensor.",
mode=LogMode.ONCE,
)
else:
shape = util.override_dynamic_shape(inp.shape, default_shape_value)
if shape != inp.shape:
G_LOGGER.warning(
f"{trt_util.str_from_tensor(inp, is_shape_tensor)} | No shapes provided; Will use shape: {shape} for min/opt/max in profile.\n",
mode=LogMode.ONCE,
)
G_LOGGER.warning(
"This will cause the tensor to have a static shape. If this is incorrect, please "
"set the range of shapes for this input tensor.",
mode=LogMode.ONCE,
)
self.add(inp.name, shape, shape, shape)
return self
def to_trt(self, builder, network):
"""
Creates a TensorRT IOptimizationProfile based on the values set in this Profile.
Args:
builder (trt.Builder):
A TensorRT builder. This will be used to construct the IOptimizationProfile.
network (trt.INetworkDefinition):
The TensorRT network the profile applies to.
Returns:
trt.IOptimizationProfile: A TensorRT optimization profile.
"""
trt_profile = builder.create_optimization_profile()
unused_keys = set(self.keys())
inp_names = [network.get_input(idx).name for idx in range(network.num_inputs)]
name_to_key, unmatched_inps = util.match_keys(unused_keys, inp_names)
if unmatched_inps:
G_LOGGER.critical(
f"Invalid inputs were provided to the optimization profile: {set(unmatched_inps)}\n"
f"Note: Inputs available in the TensorRT network are: {set(inp_names)}"
)
for idx in range(network.num_inputs):
inp = network.get_input(idx)
key = name_to_key[inp.name] if inp.name in name_to_key else None
with G_LOGGER.verbosity(): # WAR for spam from TRT
is_shape_tensor = inp.is_shape_tensor
if is_shape_tensor:
if key:
shapes = self[key]
trt_profile.set_shape_input(
inp.name, shapes.min, shapes.opt, shapes.max
)
G_LOGGER.verbose(
f"{trt_util.str_from_tensor(inp, is_shape_tensor)} | Setting input shape-tensor value range to: {shapes}"
)
else:
G_LOGGER.warning(
f"{trt_util.str_from_tensor(inp, is_shape_tensor)} | No values provided. Assuming this is not a dynamic shape-tensor.",
mode=LogMode.ONCE,
)
else:
shapes = self[key if key else inp.name]
trt_profile.set_shape(inp.name, shapes.min, shapes.opt, shapes.max)
G_LOGGER.verbose(
f"{trt_util.str_from_tensor(inp, is_shape_tensor)} | Setting input tensor shapes to: {shapes}"
)
return trt_util.check_profile(trt_profile)
def __repr__(self):
ret = "Profile()"
for name, (min, opt, max) in self.items():
ret += f".add('{name}', min={min}, opt={opt}, max={max})"
return ret
def __str__(self):
elems = []
for name, (min, opt, max) in self.items():
elems.append(f"{name} [min={min}, opt={opt}, max={max}]")
sep = ",\n "
return "{" + sep.join(elems) + "}"
@@ -0,0 +1,2 @@
-i https://pypi.ngc.nvidia.com/
nvidia-tensorrt
@@ -0,0 +1,544 @@
#
# 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 math
import time
import ctypes
from collections import OrderedDict
from polygraphy import config, cuda, mod, util
from polygraphy.backend.base import BaseRunner
from polygraphy.backend.trt import util as trt_util
from polygraphy.mod.trt_importer import lazy_import_trt
from polygraphy.common import FormattedArray
from polygraphy.datatype import DataType
from polygraphy.logger import G_LOGGER
np = mod.lazy_import("numpy")
torch = mod.lazy_import("torch>=1.13.0")
trt = lazy_import_trt()
def _make_debug_listener():
class DebugTensorWriter(trt.IDebugListener):
def __init__(self):
trt.IDebugListener.__init__(self)
self.debug_tensor_outputs = {}
def process_debug_tensor(self, addr, location, type, shape, name, stream):
if type in [util.try_getattr(trt, "fp8"), util.try_getattr(trt, "int4"), util.try_getattr(trt, "fp4"), util.try_getattr(trt, "bfloat16")]:
G_LOGGER.warning(f"Not supported datatype for debug tensor in polygraphy: {type}")
return
cuda.wrapper().stream_synchronize(stream)
datatype = DataType.from_dtype(type)
size = util.volume(shape)
buffer = np.zeros(shape, dtype=DataType.to_dtype(datatype, "numpy"))
buffer = util.array.resize_or_reallocate(buffer, size)
if location == trt.TensorLocation.HOST:
ctypes.memmove(util.array.data_ptr(buffer), addr, size * datatype.itemsize)
else:
cuda.wrapper().memcpy(
dst=util.array.data_ptr(buffer),
src=addr,
nbytes=size * datatype.itemsize,
kind=cuda.MemcpyKind.DeviceToHost,
stream_ptr=stream,
)
cuda.wrapper().stream_synchronize(stream)
self.debug_tensor_outputs[name] = util.array.resize_or_reallocate(buffer, shape)
return DebugTensorWriter()
def _make_output_allocator():
class OutputAllocator(trt.IOutputAllocator):
def __init__(self):
trt.IOutputAllocator.__init__(self)
self.buffers = {}
self.shapes = {}
self.use_torch = False
def reallocate_output(self, tensor_name, memory, size, alignment):
shape = (size,)
if tensor_name not in self.buffers:
self.buffers[tensor_name] = (
cuda.DeviceArray.raw(shape)
if not self.use_torch
else torch.empty(shape, dtype=torch.uint8, device="cuda")
)
else:
self.buffers[tensor_name] = util.array.resize_or_reallocate(self.buffers[tensor_name], shape)
G_LOGGER.extra_verbose(f"Reallocated output tensor: {tensor_name} to: {self.buffers[tensor_name]}")
return util.array.data_ptr(self.buffers[tensor_name])
def notify_shape(self, tensor_name, shape):
self.shapes[tensor_name] = tuple(shape)
def set_use_torch(self, use_torch):
self.use_torch = use_torch
return OutputAllocator()
def _get_array_on_cpu(arr, name, host_buffers, stream, nbytes, use_torch):
"""
Copies the provided array to CPU memory and returns it.
If sufficient CPU memory has not been allocated for the array in
``host_bufffers``, this function will allocate new memory.
If the input is a `torch.Tensor`, then a `torch.Tensor` is returned.
Otherwise, if the input is a `DeviceView`, a `NumPy` array is returned.
Args:
arr (Union[DeviceView, torch.Tensor]): The array.
name (str): The name of the array.
host_buffers (Dict[str, Union[numpy.ndarray, torch.Tensor]]):
A mapping of names to host buffers.
stream (cuda.Stream): The CUDA stream to use.
nbytes (int): The number of bytes to copy. This may be smaller than the size of the GPU memory.
use_torch (bool): Whether to use PyTorch tensors instead of NumPy arrays.
Returns:
Union[numpy.ndarray, torch.Tensor]: The host buffer as a flat array of bytes.
"""
if not util.array.is_on_gpu(arr):
G_LOGGER.internal_error(f"_get_array_on_cpu() should only be called with input arrays on the GPU!")
# The host buffer will always be a "raw" array, i.e. a flat array of bytes.
shape = (nbytes,)
dtype = DataType.UINT8
# If we switch between torch tensors and DeviceViews between inferences, we need to reallocate the host buffer.
if name not in host_buffers or util.array.is_torch(host_buffers[name]) != use_torch:
host_buffers[name] = (
np.empty(shape, dtype=DataType.to_dtype(dtype, "numpy"))
if not use_torch
else torch.empty(shape, dtype=DataType.to_dtype(dtype, "torch"), device="cpu")
)
host_buffers[name] = util.array.resize_or_reallocate(host_buffers[name], shape)
cuda.wrapper().memcpy(
dst=util.array.data_ptr(host_buffers[name]),
src=util.array.data_ptr(arr),
nbytes=nbytes,
kind=cuda.MemcpyKind.DeviceToHost,
stream_ptr=stream.ptr,
)
return host_buffers[name]
@mod.export()
class TrtRunner(BaseRunner):
"""
Runs inference using TensorRT.
Note that runners are not designed for production deployment and should generally
be used only for prototyping, testing, and debugging.
"""
def __init__(
self,
engine,
name: str = None,
optimization_profile: int = None,
allocation_strategy: str = None,
weight_streaming_budget: int = None,
weight_streaming_percent: float = None,
):
"""
Args:
engine (Union[Union[trt.ICudaEngine, trt.IExecutionContext], Callable() -> Union[trt.ICudaEngine, trt.IExecutionContext]]):
A TensorRT engine or execution context or a callable that returns one.
If an engine is provided, the runner will create a context automatically.
name (str):
The human-readable name prefix to use for this runner.
A runner count and timestamp will be appended to this prefix.
optimization_profile (int):
The index of the optimization profile to set each time this runner is activated.
When this is not provided, the profile is not set explicitly and will default to the 0th profile.
You can also change the profile after the runner is active using the ``set_profile()`` method.
allocation_strategy (str):
The way device memory (internal activation and scratch memory) is allocated for the execution context. The value of this argument can be:
- "static": The default value. The execution context will pre-allocate a block of memory that is sufficient for any possible input size across all profiles.
- "profile": Allocate device memory enough for the current profile based on profile max shapes.
- "runtime": Allocate device meomry enough for the current input shapes.
weight_streaming_budget (int):
The amount of GPU memory that TensorRT can use for weights at runtime. It can take on the following values:
None or -2: Disables weight streaming at runtime.
-1: TensorRT will decide the streaming budget automatically.
>= 0: The maximum amount of GPU memory TensorRT is allowed to use for weights in bytes.
weight_streaming_percent (float):
The percentage of weights that TRT will keep on the GPU. It can take on the following values:
None or 100%: Disables weight streaming at runtime.
[0 to 100]: The percentage of weights TRT will stream. 0 will stream the maximum number of weights.
"""
super().__init__(name=name, prefix="trt-runner")
self._engine_or_context = engine
self.optimization_profile = optimization_profile
self.allocation_strategy = allocation_strategy
self.weight_streaming_budget = weight_streaming_budget
self.weight_streaming_percent = weight_streaming_percent
@util.check_called_by("activate")
def activate_impl(self):
engine_or_context, _ = util.invoke_if_callable(self._engine_or_context)
if isinstance(engine_or_context, trt.ICudaEngine):
self.engine = engine_or_context
self._set_weight_streaming_budget()
allocation_strategy = util.default(self.allocation_strategy, "static")
if allocation_strategy == "static":
self.context = self.engine.create_execution_context()
elif allocation_strategy in ["profile", "runtime"]:
# Device memory will be managed by polygraphy
self.context = self.engine.create_execution_context(trt.ExecutionContextAllocationStrategy.USER_MANAGED)
else:
G_LOGGER.critical("Invalid allocation strategy specified.")
if not self.context:
G_LOGGER.critical("Invalid Context. See error log for details.")
elif isinstance(engine_or_context, trt.IExecutionContext):
self.context = engine_or_context
self.engine = self.context.engine
if self.allocation_strategy is not None:
G_LOGGER.warning(
"An allocation strategy was specified. Please ensure the provided execution context uses the same strategy."
)
else:
G_LOGGER.critical(
"Invalid Engine or Context. Please ensure the engine was built correctly. See error log for details."
)
self.device_input_buffers = OrderedDict()
self.host_output_buffers = OrderedDict()
self.stream = cuda.Stream()
self.context_memory_buffer = None
self.output_allocator = _make_output_allocator()
if self.optimization_profile is not None:
self.set_profile(self.optimization_profile)
def set_profile(self, index: int):
"""
Sets the active optimization profile for this runner.
The runner must already be active (see ``__enter__()`` or ``activate()``).
This only applies if your engine was built with multiple
optimization profiles.
In TensorRT 8.0 and newer, the profile will be set asynchronously
using this runner's CUDA stream (``runner.stream``).
By default, the runner uses the first profile (profile 0).
Args:
index (int):
The index of the optimization profile to use.
"""
if not hasattr(self, "context") or self.context is None:
G_LOGGER.critical(f"{self.name:35} | Must be activated prior to calling set_profile()")
try:
self.context.set_optimization_profile_async
except AttributeError:
self.context.active_optimization_profile = index
else:
if not self.context.set_optimization_profile_async(index, self.stream.ptr):
G_LOGGER.critical(f"Failed to set optimization profile to: {index}")
@util.check_called_by("get_input_metadata")
def get_input_metadata_impl(self):
return trt_util.get_metadata_from_engine(self.engine, self.context, mode=trt.TensorIOMode.INPUT)
def _infer_impl(self, feed_dict, copy_outputs_to_host, return_raw_buffers):
def get_io(mode):
for idx in range(self.engine.num_io_tensors):
name = self.engine.get_tensor_name(idx)
if self.engine.get_tensor_mode(name) == mode:
yield name
use_torch = False
for name in get_io(trt.TensorIOMode.INPUT):
# Set up input tensor shapes and copy from host memory if needed
array = feed_dict[name]
if not isinstance(array, FormattedArray):
array = FormattedArray(array, shape=util.array.shape(array))
underlying_array = array.array
use_torch = use_torch or util.array.is_torch(underlying_array)
ptr = None
if self.engine.is_shape_inference_io(name):
if not util.array.is_on_cpu(underlying_array):
G_LOGGER.critical(
f"A {type(underlying_array).__name__} was provided for input: {name}, but since this is a shape tensor, "
"it must reside in host memory. "
)
ptr = util.array.data_ptr(underlying_array)
else:
ptr = trt_util._get_array_on_gpu(underlying_array, name, self.device_input_buffers, self.stream)
# If the format is HWC, make sure array.shape is considered after transposing back to CHW
if trt_util.get_tensor_format(self.engine, self.context, name) == trt.TensorFormat.HWC:
array_shape = trt_util.get_chw_shape_from_hwc(array.shape, self.context.get_tensor_strides(name))
else:
array_shape = array.shape
# Only update the input shape/address if something has changed. Otherwise, we'd be
# doing extra work unnecessarily.
# We retrieve the semantic shape from the FormattedArray, *not* the underlying array.
if self.context.get_tensor_shape(name) != array_shape:
G_LOGGER.ultra_verbose(f"Setting {name} input shape to: {array_shape}")
if not self.context.set_input_shape(name, array_shape):
G_LOGGER.critical(f"For input: {name}, failed to set shape to: {array_shape}")
if self.context.get_tensor_address(name) != ptr:
if not self.context.set_tensor_address(name, ptr):
G_LOGGER.critical(f"For input: {name}, failed to set tensor address to: {ptr}")
try:
self.context.set_all_tensors_debug_state
except AttributeError:
pass
else:
# Set up the debug listener before running inference.
debug_listener = _make_debug_listener()
self.context.set_all_tensors_debug_state(True)
if not self.context.set_debug_listener(debug_listener):
G_LOGGER.critical(f"Failed to set debug listener.")
# Set up the output allocator before running inference.
self.output_allocator.set_use_torch(use_torch and torch.cuda.is_available())
for name in get_io(trt.TensorIOMode.OUTPUT):
if not self.context.set_output_allocator(name, self.output_allocator):
G_LOGGER.critical(f"For output: {name}, failed to set output allocator")
if self.allocation_strategy in ["profile", "runtime"]:
if self.allocation_strategy == "profile":
# Perform per-profile allocation.
size_to_allocate = 0
if config.USE_TENSORRT_RTX or mod.version(trt.__version__) >= mod.version("10.1"):
size_to_allocate = self.engine.get_device_memory_size_for_profile_v2(
self.context.active_optimization_profile
)
else:
size_to_allocate = self.engine.get_device_memory_size_for_profile(
self.context.active_optimization_profile
)
elif self.allocation_strategy == "runtime":
# Perform runtime allocation.
size_to_allocate = self.context.update_device_memory_size_for_shapes()
if self.context_memory_buffer is None:
self.context_memory_buffer = cuda.DeviceArray.raw((size_to_allocate,))
self.context_memory_buffer.resize((size_to_allocate,))
if config.USE_TENSORRT_RTX or mod.version(trt.__version__) >= mod.version("10.1"):
self.context.set_device_memory(self.context_memory_buffer.ptr, self.context_memory_buffer.allocated_nbytes)
else:
self.context.device_memory = self.context_memory_buffer.ptr
if not self.context.execute_async_v3(self.stream.ptr):
G_LOGGER.critical("`execute_async_v3()` failed. Please see the logging output above for details.")
output_buffers = OrderedDict()
for name in get_io(trt.TensorIOMode.OUTPUT):
# If we're dealing with vectorized formats, we need to return a FormattedArray.
# Otherwise, we create a view instead with the correct shape/dtype.
raw_array = self.output_allocator.buffers[name]
shape = self.output_allocator.shapes[name]
# If the format is HWC, make sure the result is shaped accordingly
tensor_format = trt_util.get_tensor_format(self.engine, self.context, name)
if tensor_format == trt.TensorFormat.HWC:
shape = trt_util.get_hwc_shape_from_chw(shape, self.context.get_tensor_strides(name))
using_vectorized_format = tensor_format != trt.TensorFormat.LINEAR and tensor_format != trt.TensorFormat.HWC
should_use_formatted_array = return_raw_buffers or using_vectorized_format
dtype = DataType.from_dtype(self.engine.get_tensor_dtype(name), source_module="tensorrt")
# The memory allocated by the output allocator may be larger than actually required.
# If we're using a vectorized format, then we need to copy the whole thing.
# Otherwise, we can determine how much we actually need.
nbytes = (
util.array.nbytes(raw_array)
if using_vectorized_format
# Some data types have fractional sizes, in which case we round up to the nearest byte.
else int(math.ceil(util.volume(shape) * dtype.itemsize))
)
if copy_outputs_to_host:
raw_array = _get_array_on_cpu(
raw_array,
name,
self.host_output_buffers,
self.stream,
nbytes,
use_torch=use_torch,
)
if should_use_formatted_array:
array = FormattedArray(raw_array, shape=shape)
else:
array = util.array.view(raw_array, dtype, shape)
output_buffers[name] = array
self.stream.synchronize()
try:
self.context.set_all_tensors_debug_state
except AttributeError:
pass
else:
if debug_listener.debug_tensor_outputs:
output_buffers.update(debug_listener.debug_tensor_outputs)
return output_buffers
@util.check_called_by("infer")
def infer_impl(self, feed_dict, copy_outputs_to_host=None, return_raw_buffers=None):
"""
Implementation for running inference with TensorRT.
Do not call this method directly - use ``infer()`` instead,
which will forward unrecognized arguments to this method.
Args:
feed_dict (OrderedDict[str, Union[numpy.ndarray, DeviceView, torch.Tensor]]):
A mapping of input tensor names to corresponding input NumPy arrays,
Polygraphy DeviceViews, or PyTorch tensors.
If PyTorch tensors are provided in the feed_dict, then this function
will return the outputs also as PyTorch tensors.
If the provided inputs already reside in GPU memory, no additional copies are made.
copy_outputs_to_host (bool):
Whether to copy inference outputs back to host memory.
If this is False, PyTorch GPU tensors or Polygraphy DeviceViews
are returned instead of PyTorch CPU tensors or NumPy arrays respectively.
Defaults to True.
Returns:
OrderedDict[str, Union[numpy.ndarray, DeviceView, torch.Tensor]]:
A mapping of output tensor names to corresponding output NumPy arrays,
Polygraphy DeviceViews, or PyTorch tensors.
"""
copy_outputs_to_host = util.default(copy_outputs_to_host, True)
return_raw_buffers = util.default(return_raw_buffers, False)
start = time.time()
output_buffers = self._infer_impl(feed_dict, copy_outputs_to_host, return_raw_buffers)
end = time.time()
self.inference_time = end - start
return output_buffers
@util.check_called_by("deactivate")
def deactivate_impl(self):
[buf.free() for buf in self.device_input_buffers.values()]
if self.context_memory_buffer is not None:
self.context_memory_buffer.free()
self.stream.free()
del (
self.engine,
self.context,
self.device_input_buffers,
self.host_output_buffers,
self.stream,
self.context_memory_buffer,
self.output_allocator,
)
def _set_weight_streaming_budget(self):
# Setup weight streaming if applicable
if self.weight_streaming_budget != None and self.weight_streaming_percent != None:
G_LOGGER.warning(f"Cannot specify the weight streaming budget both in bytes and percentage. Prioritizing the bytes value.")
if self.weight_streaming_budget is not None:
assert self.weight_streaming_budget == -2 or self.weight_streaming_budget == -1 or self.weight_streaming_budget >= 0
if config.USE_TENSORRT_RTX or mod.version(trt.__version__) >= mod.version("10.1"):
self._set_weight_streaming_budget_v2()
else:
self._set_weight_streaming_budget_v1()
def _set_weight_streaming_budget_v1(self):
budget_bytes = None
if self.weight_streaming_budget is not None:
if self.weight_streaming_budget == -2:
budget_bytes = 0
else:
budget_bytes = self.weight_streaming_budget
elif self.weight_streaming_percent is not None:
assert 0 <= self.weight_streaming_percent <= 100
if self.weight_streaming_percent == 0:
budget_bytes = 0 # Disable weight streaming
else:
try:
min_budget = self.engine.minimum_weight_streaming_budget
except AttributeError:
# minimum_weight_streaming_budget is deprecated in TensorRT 10.1 and removed in
# TensorRT RTX 1.0. For the new / V2 path, the minimum budget is 0.
min_budget = 0
max_budget = self.engine.streamable_weights_size
budget_bytes = (1 - self.weight_streaming_percent / 100.0) * (max_budget - min_budget) + min_budget
if budget_bytes is not None:
budget_bytes = int(budget_bytes)
self.engine.weight_streaming_budget = budget_bytes
if self.engine.weight_streaming_budget != budget_bytes:
G_LOGGER.critical(f"Failed to set weight streaming budget to {budget_bytes}!")
if budget_bytes == 0:
G_LOGGER.info(f"Weight streaming is disabled.")
elif budget_bytes == -1:
G_LOGGER.info(f"Weight streaming is enabled with TensorRT automatically determiing the budget.")
else:
G_LOGGER.info(f"Weight streaming is enabled with a memory budget of {budget_bytes} bytes.")
def _set_weight_streaming_budget_v2(self):
budget_bytes = None
if self.weight_streaming_budget is not None:
# use V2 path
assert self.weight_streaming_budget == -2 or self.weight_streaming_budget == -1 or self.weight_streaming_budget >= 0
if self.weight_streaming_budget == -2:
budget_bytes = self.engine.streamable_weights_size
elif self.weight_streaming_budget == -1:
budget_bytes = self.engine.get_weight_streaming_automatic_budget()
else:
budget_bytes = self.weight_streaming_budget
elif self.weight_streaming_percent is not None:
assert 0 <= self.weight_streaming_percent <= 100
if self.weight_streaming_percent == 100:
budget_bytes = self.engine.streamable_weights_size
else:
budget_bytes = self.weight_streaming_percent / 100.0 * (self.engine.streamable_weights_size)
if budget_bytes is not None:
budget_bytes = int(budget_bytes)
self.engine.weight_streaming_budget_v2 = budget_bytes
if self.engine.weight_streaming_budget_v2 != budget_bytes:
G_LOGGER.critical(f"Failed to set weight streaming budget to {budget_bytes}!")
if budget_bytes == self.engine.streamable_weights_size:
G_LOGGER.info(f"Weight streaming is disabled.")
else:
G_LOGGER.info(f"Weight streaming is enabled with a memory budget of {budget_bytes} bytes.")
File diff suppressed because it is too large Load Diff