Consolidate ORTModule logging (#16078)

### Consolidate ORTModule logging

There are few improvements for ORTModule loggings:
- All ORTModule logging are used logger that is initialized in
`ortmodule.py`.
- Manage all export logs same way, e.g. use `
_logger.suppress_os_stream_output(log_level=self._debug_options.logging.log_level)`
to control exporting related logs suppressing or not. If any warning or
errors suppressed, `self._warning_log_detected_during_export` will be
set to True, then when we log ORTModule feature matrix, we will also
told users there are logs suppressed.
- Downgrade some warnings. We had some warnings for years, and looks
many models have them by default, no action we actually can take, so
downgrade them to make user logging cleaner.
- PyTorch export requires update of custom export function signature
changes, otherwise, _symbolic_context_handler complains with warnings,
so update custom export function adaption for version >=1.13 PyTorch.
- Add ORTModule feature matrix summary, **this is supposed to be only
places users see our logs by default** (unless they use INFO or
VERBOSE). Features ON/OFF states are shown clearly to them in case they
want to try some features in OFF states. This logs only shows up in rank
0 (if there are multiple rank), the intention is we want user to see a
useful and clean output from ORTModule by default. The outputs shown as
below:



![image](https://github.com/microsoft/onnxruntime/assets/10530022/9c6653ac-50fa-4b2d-ba7f-4d5ce44b25b2)


![image](https://github.com/microsoft/onnxruntime/assets/10530022/10dff5a9-2d46-4646-a4b4-2c515566376e)


- `reinitialize_ortmodule` in util.py is only used by ortmodule.py,
moving it into ortmodule.py, then utils takes no dependency on
`orttraining/orttraining/python/training/ortmodule/_custom_op_symbolic_registry.py`,
then `_custom_op_symbolic_registry.py` can call functions defined in
utils.py (without recursively include).
This commit is contained in:
pengwa 2023-06-01 10:09:12 +08:00 committed by GitHub
parent d19e5c0abb
commit 65b316a138
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 400 additions and 295 deletions

View file

@ -14,9 +14,9 @@ from torch.onnx import symbolic_helper
from onnxruntime.capi._pybind_state import register_miscellaneous_const_input, register_torch_autograd_function
from onnxruntime.training import ortmodule
from . import _logger
from ._custom_op_symbolic_registry import pytorch_type_to_onnx
from ._custom_op_symbolic_registry import pytorch_type_to_onnx, wrap_custom_export_function
from ._fallback import ORTModuleONNXModelException, wrap_exception
from ._utils import get_runtime_pytorch_version
# Some autograd.Function's shouldn't be exported as PythonOp.
# If CheckpointFunction is exported as PythonOp, the checkpointed computation
@ -26,28 +26,6 @@ from ._fallback import ORTModuleONNXModelException, wrap_exception
# at all.
BANNED_AUTOGRAD_FUNCTION_NAMES = frozenset([torch.utils.checkpoint.CheckpointFunction.__name__])
# Mapping from pytorch scalar type to onnx scalar type.
_CAST_PYTORCH_TO_ONNX = {
"Byte": torch.onnx.TensorProtoDataType.UINT8,
"Char": torch.onnx.TensorProtoDataType.INT8,
"Double": torch.onnx.TensorProtoDataType.DOUBLE,
"Float": torch.onnx.TensorProtoDataType.FLOAT,
"Half": torch.onnx.TensorProtoDataType.FLOAT16,
"Int": torch.onnx.TensorProtoDataType.INT32,
"Long": torch.onnx.TensorProtoDataType.INT64,
"Short": torch.onnx.TensorProtoDataType.INT16,
"Bool": torch.onnx.TensorProtoDataType.BOOL,
"ComplexFloat": torch.onnx.TensorProtoDataType.COMPLEX64,
"ComplexDouble": torch.onnx.TensorProtoDataType.COMPLEX128,
"BFloat16": torch.onnx.TensorProtoDataType.BFLOAT16,
# Not yet defined in torch.
# "Float8E4M3FN": torch.onnx.TensorProtoDataType.FLOAT8E4M3FN,
# "Float8E4M3FNUZ": torch.onnx.TensorProtoDataType.FLOAT8E4M3FNUZ,
# "Float8E5M2": torch.onnx.TensorProtoDataType.FLOAT8E5M2,
# "Float8E5M2FNUZ": torch.onnx.TensorProtoDataType.FLOAT8E5M2FNUZ,
"Undefined": torch.onnx.TensorProtoDataType.UNDEFINED,
}
def _full_name(klass):
module = klass.__module__
@ -56,13 +34,6 @@ def _full_name(klass):
return module + "." + klass.__qualname__
def pytorch_type_to_onnx(scalar_type: str) -> torch.onnx.TensorProtoDataType: # noqa: F811
try:
return torch.onnx.JitScalarType.from_name(scalar_type).onnx_type()
except AttributeError:
return _CAST_PYTORCH_TO_ONNX[scalar_type]
def _export_pt_1_10(g, n, *args, **kwargs):
"""
This function exports PythonOp (input: "n") into a graph
@ -83,8 +54,7 @@ def _export_pt_1_10(g, n, *args, **kwargs):
inplace = kwargs["inplace"]
# TODO move to public API once exporter team exposes that
training_mode = None
runtime_pytorch_version = version.parse(torch.__version__.split("+")[0])
if runtime_pytorch_version >= version.parse("1.12"):
if get_runtime_pytorch_version() >= version.parse("1.12"):
# FIXME: using privated modules
from torch.onnx import _globals
@ -228,22 +198,10 @@ def _export_pt_1_10(g, n, *args, **kwargs):
raise wrap_exception(ORTModuleONNXModelException, e) # noqa: B904
# Starting from PyTorch 1.11, there has been a change to symbolic function signature
# in terms of how additional context is accessed. More info at
# https://github.com/pytorch/pytorch/blob/6b02648479d3615fa3260961e24f38dd0f22da94/torch/onnx/symbolic_helper.py#L48
# This code can be cleaned up once support for PyTorch version < 1.11 is dropped.
try:
from torch.onnx import SymbolicContext
def _export(ctx: SymbolicContext, g, *args, **kwargs):
n = ctx.cur_node
return _export_pt_1_10(g, n, *args, **kwargs)
except ImportError:
_export = _export_pt_1_10
_export = wrap_custom_export_function(_export_pt_1_10)
def _post_process_after_export(exported_model, enable_custom_autograd_function, log_level):
def _post_process_after_export(exported_model, enable_custom_autograd_function):
if enable_custom_autograd_function:
return _post_process_enabling_autograd_fallback(exported_model)
@ -253,7 +211,7 @@ def _post_process_after_export(exported_model, enable_custom_autograd_function,
is_pythonop_needed = True
break
if is_pythonop_needed and log_level <= _logger.LogLevel.WARNING:
if is_pythonop_needed:
warnings.warn(
"Detected autograd functions usage in current model, the run will fail \
without enabling '_enable_custom_autograd_function'. Please enable it with: \

View file

@ -3,16 +3,19 @@
# Licensed under the MIT License.
# --------------------------------------------------------------------------
import warnings # noqa: F401
from typing import Callable
import torch
import torch.onnx.symbolic_helper as sym_help
from packaging import version
from packaging.version import Version
from torch.onnx import register_custom_op_symbolic
from torch.onnx.symbolic_helper import _get_tensor_dim_size, _get_tensor_sizes, parse_args
from onnxruntime.training import ortmodule
from ._utils import get_runtime_pytorch_version
# Mapping from pytorch scalar type to onnx scalar type.
_CAST_PYTORCH_TO_ONNX = {
"Byte": torch.onnx.TensorProtoDataType.UINT8,
@ -43,12 +46,34 @@ def pytorch_type_to_onnx(scalar_type: str) -> torch.onnx.TensorProtoDataType:
return _CAST_PYTORCH_TO_ONNX[scalar_type]
def wrap_custom_export_function(original_func):
# Starting from PyTorch 1.11, there has been a change to symbolic function signature
# in terms of how additional context is accessed. More info at
# https://github.com/pytorch/pytorch/blob/6b02648479d3615fa3260961e24f38dd0f22da94/torch/onnx/symbolic_helper.py#L48
# This code can be cleaned up once support for PyTorch version < 1.11 is dropped.
try:
def wrap_custom_export_function(original_func: Callable) -> Callable:
"""This function is to wrap the custom export function to make sure it can be used by different versions of PyTorch.
Args:
original_func: The original custom export function.
Note1:
[PyTorch exporter breaking change] Starting from PyTorch 1.11, there has been a change to symbolic function
signature in terms of how additional context is accessed. More info at
https://github.com/pytorch/pytorch/blob/6b02648479d3615fa3260961e24f38dd0f22da94/torch/onnx/symbolic_helper.py#L48
This code can be cleaned up once support for PyTorch version < 1.11 is dropped.
Note2:
[PyTorch exporter breaking change] Custom export function's first argument is SymbolicContext since 1.11, but
is changed later, and will be deprecated in 1.13 as claimed. So we need to use GraphContext as the first
argument instead.
"""
runtime_pytorch_version = get_runtime_pytorch_version()
if runtime_pytorch_version >= version.parse("1.13"):
from torch.onnx._internal import jit_utils
def _export_with_ctx(graph_context: jit_utils.GraphContext, *args, **kwargs):
return original_func(graph_context, graph_context.original_node, *args, **kwargs)
return _export_with_ctx
elif runtime_pytorch_version >= version.parse("1.11"):
from torch.onnx import SymbolicContext
def _export_with_ctx(ctx: SymbolicContext, graph, *args, **kwargs):
@ -56,7 +81,7 @@ def wrap_custom_export_function(original_func):
return original_func(graph, node, *args, **kwargs)
return _export_with_ctx
except ImportError:
else:
def _export_with_no_ctx(graph, *args, **kwargs):
return original_func(graph, None, *args, **kwargs)

View file

@ -4,8 +4,8 @@
# --------------------------------------------------------------------------
import os
import warnings
from enum import IntFlag
from logging import Logger
from typing import Optional
import torch
@ -67,7 +67,7 @@ class _FallbackManager:
be raised to the user, terminating execution
"""
def __init__(self, pytorch_module: torch.nn.Module, policy: _FallbackPolicy, retry: bool):
def __init__(self, pytorch_module: torch.nn.Module, policy: _FallbackPolicy, retry: bool, logger: Logger):
self._original_module = pytorch_module
# Read policy from environment variable for testing purposes
@ -103,6 +103,7 @@ class _FallbackManager:
self.retry = retry
self._exception = None
self._raised_fallback_exception = False
self._logger = logger
def handle_exception(
self, exception: Exception, log_level: _logger.LogLevel, override_policy: Optional[_FallbackPolicy] = None
@ -132,19 +133,15 @@ class _FallbackManager:
and type(exception) in self._policy_exception_map[policy.value]
)
):
if log_level <= _logger.LogLevel.INFO:
warnings.warn(f"Fallback for policy {policy.name} is pending.", UserWarning)
self._logger.info(f"Fallback for policy {policy.name} is pending.")
# ORTModuleInitException exceptions do not call `fallback()` through `GraphExecutionManager`,
# Instead, it fallbacks to PyTorch implicitly through `ORTModule._torch_module = TorchModulePytorch(module)`
if log_level <= _logger.LogLevel.WARNING and policy == _FallbackPolicy.FALLBACK_BAD_INITIALIZATION:
warnings.warn(
(
f"Fallback to PyTorch due to exception {type(exception)} was triggered. "
"Report this issue with a minimal repro at https://www.github.com/microsoft/onnxruntime. "
f"See details below:\n\n{_utils.get_exception_as_string(exception)}"
),
UserWarning,
if policy == _FallbackPolicy.FALLBACK_BAD_INITIALIZATION:
self._logger.warning(
f"Fallback to PyTorch due to exception {type(exception)} was triggered. "
"Report this issue with a minimal repro at https://www.github.com/microsoft/onnxruntime. "
f"See details below:\n\n{_utils.get_exception_as_string(exception)}"
)
self._exception = exception
@ -177,18 +174,15 @@ class _FallbackManager:
exception_string = _utils.get_exception_as_string(self._exception)
# This warning will not be raised again if retry is not enabled
warnings.warn(
(
"Fallback to PyTorch due to exception {} was triggered. "
"Report this issue with a minimal repro at https://www.github.com/microsoft/onnxruntime. "
"See details below:\n\n{}".format(exception_type, exception_string)
),
UserWarning,
self._logger.warning(
"Fallback to PyTorch due to exception {} was triggered. "
"Report this issue with a minimal repro at https://www.github.com/microsoft/onnxruntime. "
"See details below:\n\n{}".format(exception_type, exception_string)
)
self._raised_fallback_exception = True
# Pending fallbacks are resetted to enforce retries
# Pending fallbacks are reset to enforce retries
if self.retry:
self._raised_fallback_exception = False
self._exception = None

View file

@ -8,7 +8,6 @@ import inspect
import io
import logging
import os
import warnings
from abc import ABC, abstractmethod # noqa: F401
from enum import IntFlag
from functools import reduce
@ -30,6 +29,7 @@ from ._fallback import (
ORTModuleONNXModelException,
ORTModuleTorchModelException,
_FallbackManager,
_FallbackPolicy,
wrap_exception,
)
from ._gradient_accumulation_manager import GradientAccumulationManager
@ -38,8 +38,6 @@ from ._io import _FlattenedModule, _InputInfo, _ModelInputOutputSchemaType
from .debug_options import DebugOptions, LogLevel
from .torch_cpp_extensions.cpu.aten_op_executor import load_aten_op_executor_cpp_extension
logger = logging.getLogger(__name__)
class _RunStateInfo:
def __init__(self, state, output_info: List[Tuple[torch.Size, torch.device, torch.dtype]]):
@ -74,7 +72,13 @@ class _SkipCheck(IntFlag):
class GraphExecutionManager(GraphExecutionInterface):
def __init__(self, module: _FlattenedModule, debug_options: DebugOptions, fallback_manager: _FallbackManager):
def __init__(
self,
module: _FlattenedModule,
debug_options: DebugOptions,
fallback_manager: _FallbackManager,
logger: logging.Logger,
):
"""Manages construction and execution of ONNX graphs"""
super().__init__(module._original_module)
@ -82,7 +86,8 @@ class GraphExecutionManager(GraphExecutionInterface):
# IMPORTANT: Debug and Fallback must the configured first
self._debug_options = debug_options
self._fallback_manager = fallback_manager
logger.setLevel(_logger.ortmodule_loglevel_to_python_loglevel(self._debug_options.logging.log_level))
self._logger = logger
# Original and flattened (transformed) output module
self._flattened_module = module
@ -119,7 +124,7 @@ class GraphExecutionManager(GraphExecutionInterface):
self._first_skip_check_warning = True
# Inspect embedding input index sparsity.
self._rt_inspector = _runtime_inspector.RuntimeInspector(logger)
self._rt_inspector = _runtime_inspector.RuntimeInspector(self._logger)
# Graph transformer config
# Specify cast propagation strategy. Currently, three strategies are available, NONE, INSERT-AND-REDUCE and FLOOD-FILL
@ -160,6 +165,8 @@ class GraphExecutionManager(GraphExecutionInterface):
# Input and output infos (including schema) for exported model.
self._input_info: Optional[_InputInfo] = None
self._module_output_schema: Optional[_ModelInputOutputSchemaType] = None
self._warning_log_detected_during_export = False
self._export_duration_in_ms = 0
# Device where the model is placed.
self._device: Optional[torch.device] = _utils.get_device_from_module(module)
@ -172,8 +179,7 @@ class GraphExecutionManager(GraphExecutionInterface):
# TODO: remove after PyTorch ONNX exporter supports VAR_KEYWORD parameters.
for input_parameter in self._module_parameters:
if input_parameter.kind == inspect.Parameter.VAR_KEYWORD:
if self._debug_options.logging.log_level <= LogLevel.WARNING:
logger.warning("The model's forward method has **kwargs parameter which has EXPERIMENTAL support!")
self._logger.info("The model's forward method has **kwargs parameter which has EXPERIMENTAL support!")
self.is_rocm_pytorch = bool(torch.version.hip is not None and ROCM_HOME is not None)
@ -200,6 +206,8 @@ class GraphExecutionManager(GraphExecutionInterface):
self._print_input_density = ortmodule._defined_from_envvar("ORTMODULE_PRINT_INPUT_DENSITY", 0, warn=True) == 1
self._enable_memory_optimizer = ortmodule._defined_from_envvar("ORTMODULE_MEMORY_OPT_CONFIG", "", warn=True)
# Flag to re-export the model due to attribute change on the original module.
# Re-export will be avoided if _skip_check is enabled.
self._original_model_has_changed = False
@ -207,6 +215,21 @@ class GraphExecutionManager(GraphExecutionInterface):
# Load ATen operator executor extension.
load_aten_op_executor_cpp_extension()
self._feature_map: List[List[str]] = [
["ATen Executor", "ON", "Dispatch ATen operators to ORT's ATen executor"],
[
"Cast Propagation",
"ON" if self._propagate_cast_ops_level > 0 else "OFF",
f"Level {self._propagate_cast_ops_level} enabled",
],
["Custom Function", "ON", "Support custom torch.autograd.Function export and execution"],
[
"Memory Optimizer",
"ON" if self._enable_memory_optimizer else "OFF",
"Enable with env ORTMODULE_MEMORY_OPT_CONFIG=<config>",
],
]
def _get_torch_gpu_allocator_function_addresses(self):
if self._use_external_gpu_allocator and torch.cuda.is_available():
# CPP extension to get torch GPU allocator's alloc and free function addresses
@ -256,8 +279,7 @@ class GraphExecutionManager(GraphExecutionInterface):
"""Creates and returns the session configuration to be used for the ExecutionAgent"""
if _are_deterministic_algorithms_enabled():
if self._debug_options.logging.log_level <= _logger.LogLevel.INFO:
logger.warning("ORTModule's determinism will be enabled because PyTorch's determinism is enabled.")
self._logger.info("ORTModule's determinism will be enabled because PyTorch's determinism is enabled.")
providers = None
provider_options = None
@ -270,7 +292,7 @@ class GraphExecutionManager(GraphExecutionInterface):
# Set Conv algo search mode to HEURISTIC by default, which is the same as PyTorch's default setting.
conv_algo_search = ortmodule._defined_from_envvar("ORTMODULE_CONV_ALGO_SEARCH", "HEURISTIC", warn=True)
if conv_algo_search not in ["HEURISTIC", "EXHAUSTIVE"]:
warnings.warn("Invalid value of env CONV_ALGO_SEARCH. Must be HEURISTIC or EXHAUSTIVE.")
self._logger.warning("Invalid value of env CONV_ALGO_SEARCH. Must be HEURISTIC or EXHAUSTIVE.")
conv_algo_search = "HEURISTIC"
provider_option_map["cudnn_conv_algo_search"] = conv_algo_search
provider_option_map["cudnn_conv_use_max_workspace"] = "1"
@ -331,7 +353,7 @@ class GraphExecutionManager(GraphExecutionInterface):
# e.g., some sympy functions in symbolic_shape_infer will change Python's random state.
random_states = _utils.get_random_states()
schema = _io._extract_schema({"args": copy.copy(inputs), "kwargs": copy.copy(kwargs)})
schema = _io._extract_schema({"args": copy.copy(inputs), "kwargs": copy.copy(kwargs)}, self._logger)
if (
self._onnx_models.exported_model
and schema == self._input_info.schema
@ -365,72 +387,84 @@ class GraphExecutionManager(GraphExecutionInterface):
TODO: How to support dynamic axes? Dimensions are determined by samples
"""
with _logger.suppress_os_stream_output(log_level=self._debug_options.logging.log_level) as suppress_output:
from datetime import datetime
# Setup dynamic axes for onnx model
self._input_info = _io.parse_inputs_for_onnx_export(self._module_parameters, None, input_schema, inputs, kwargs)
(
output_names,
output_dynamic_axes,
self._module_output_schema,
) = _io.parse_outputs_for_onnx_export_and_extract_schema(
self._original_module, inputs, kwargs, self._debug_options.logging.log_level
)
self._input_info.dynamic_axes.update(output_dynamic_axes)
start = datetime.now()
# FlattenedModule needs _InputInfo to expand user input from *args to *args + **kwargs
self._flattened_module._input_info = self._input_info
# Export torch.nn.Module to ONNX
f = io.BytesIO()
# Deepcopy inputs, since input values may change after model run.
# NOTE: Inputs may contain tensors that have attributes preventing their deepcopy (example grad_fn).
# Therefore, deepcopy only the data component of the input tensors for export.
sample_inputs_copy, sample_kwargs_copy = _io.deepcopy_model_input(*inputs, **kwargs)
# NOTE: Flattening the input will change the 'input schema', resulting in a re-export
sample_inputs_as_tuple = tuple(self._input_info.flatten(sample_inputs_copy, sample_kwargs_copy, self._device))
# Ops behaving differently under train/eval mode need to be exported with the
# correct training flag to reflect the expected behavior.
# For example, the Dropout node in a model is dropped under eval mode.
assert self._export_mode is not None, "Please use a concrete instance of ExecutionManager"
try:
with torch.no_grad(), _logger.suppress_os_stream_output(log_level=self._debug_options.logging.log_level):
required_export_kwargs = {
"input_names": self._input_info.names,
"output_names": output_names,
"opset_version": ortmodule.ONNX_OPSET_VERSION,
"do_constant_folding": False,
"training": self._export_mode,
"dynamic_axes": self._input_info.dynamic_axes,
"verbose": self._debug_options.logging.log_level < LogLevel.WARNING,
"export_params": False,
"keep_initializers_as_inputs": True,
}
invalid_args = self._export_extra_kwargs.keys() & required_export_kwargs.keys()
assert (
len(invalid_args) == 0
), f"The following PyTorch exporter arguments cannot be specified: '{invalid_args}'."
torch.onnx.export(
self._flattened_module,
sample_inputs_as_tuple,
f,
**required_export_kwargs,
**self._export_extra_kwargs,
)
except Exception as e:
raise wrap_exception( # noqa: B904
ORTModuleONNXModelException,
RuntimeError(
f"There was an error while exporting the PyTorch model to ONNX: "
f"\n\n{_utils.get_exception_as_string(e)}"
),
# Setup dynamic axes for onnx model
self._input_info = _io.parse_inputs_for_onnx_export(
self._module_parameters, None, input_schema, inputs, kwargs
)
exported_model = onnx.load_model_from_string(f.getvalue())
(
output_names,
output_dynamic_axes,
self._module_output_schema,
) = _io.parse_outputs_for_onnx_export_and_extract_schema(
self._original_module, inputs, kwargs, self._logger
)
self._input_info.dynamic_axes.update(output_dynamic_axes)
exported_model = _post_process_after_export(
exported_model, self._enable_custom_autograd_function, self._debug_options.logging.log_level
)
# FlattenedModule needs _InputInfo to expand user input from *args to *args + **kwargs
self._flattened_module._input_info = self._input_info
# Export torch.nn.Module to ONNX
f = io.BytesIO()
# Deepcopy inputs, since input values may change after model run.
# NOTE: Inputs may contain tensors that have attributes preventing their deepcopy (example grad_fn).
# Therefore, deepcopy only the data component of the input tensors for export.
sample_inputs_copy, sample_kwargs_copy = _io.deepcopy_model_input(*inputs, **kwargs)
# NOTE: Flattening the input will change the 'input schema', resulting in a re-export
sample_inputs_as_tuple = tuple(
self._input_info.flatten(sample_inputs_copy, sample_kwargs_copy, self._device)
)
# Ops behaving differently under train/eval mode need to be exported with the
# correct training flag to reflect the expected behavior.
# For example, the Dropout node in a model is dropped under eval mode.
assert self._export_mode is not None, "Please use a concrete instance of ExecutionManager"
try:
with torch.no_grad():
required_export_kwargs = {
"input_names": self._input_info.names,
"output_names": output_names,
"opset_version": ortmodule.ONNX_OPSET_VERSION,
"do_constant_folding": False,
"training": self._export_mode,
"dynamic_axes": self._input_info.dynamic_axes,
"verbose": self._debug_options.logging.log_level < LogLevel.WARNING,
"export_params": False,
"keep_initializers_as_inputs": True,
}
invalid_args = self._export_extra_kwargs.keys() & required_export_kwargs.keys()
assert (
len(invalid_args) == 0
), f"The following PyTorch exporter arguments cannot be specified: '{invalid_args}'."
torch.onnx.export(
self._flattened_module,
sample_inputs_as_tuple,
f,
**required_export_kwargs,
**self._export_extra_kwargs,
)
except Exception as e:
raise wrap_exception( # noqa: B904
ORTModuleONNXModelException,
RuntimeError(
f"There was an error while exporting the PyTorch model to ONNX: "
f"\n\n{_utils.get_exception_as_string(e)}"
),
)
exported_model = onnx.load_model_from_string(f.getvalue())
exported_model = _post_process_after_export(exported_model, self._enable_custom_autograd_function)
if suppress_output.tell() > 0:
self._warning_log_detected_during_export = True
end = datetime.now()
self._export_duration_in_ms = (end - start).total_seconds() * 1000
return exported_model
@ -532,14 +566,27 @@ class GraphExecutionManager(GraphExecutionInterface):
Based on runtime inspection, enable conditional optimizations if applicable.
Input sparsity-based optimization workflows:
1. Input density observer is enabled when label sparsity optimization is enabled.
1. Input density observer is enabled if the sparse optimizer is ON or user wants to print input density.
2. Input density observer inspects input tensors and returns sparsity results.
3. If label or embedding input sparsity is found in sparsity results, graph transformer config is updated to
enable sparsity-based optimization.
"""
# Enable data sparsity inspection if label sparsity optimization is enabled or user wants to print input density.
self._feature_map.extend(
[
[
"Compute Optimizer",
"ON" if self._enable_compute_optimizer else "OFF",
"Enable/Disable with env ORTMODULE_ENABLE_COMPUTE_OPTIMIZER=1/0",
],
[
" -FLOPReduction",
"ON" if self._enable_compute_optimizer else "OFF",
"Enable/Disable with env ORTMODULE_ENABLE_COMPUTE_OPTIMIZER=1/0",
],
]
)
# Enable data sparsity inspection if sparse optimizer is ON or user wants to print input density.
if self._enable_sparse_optimizer or self._print_input_density:
self._rt_inspector.enable_input_inspector(
self._onnx_models.exported_model, self._graph_builder.get_graph_info().user_input_names
@ -563,14 +610,64 @@ class GraphExecutionManager(GraphExecutionInterface):
# Enable sparsity-based optimization when applicable.
if len(label_sparsity_results) > 0:
graph_transformer_config.sparse_label_input_names = label_sparsity_results
logger.info(f"Label sparsity based optimization is on for {label_sparsity_results}")
graph_transformer_config.sparse_label_input_names = list(label_sparsity_results.keys())
self._logger.info("Label sparsity-based optimization is ON for %s", label_sparsity_results)
sparsity_stat_str = ",".join([f"{k}:{v:.0f}%" for k, v in label_sparsity_results.items()])
self._feature_map.append(
[
" -LabelSparsityOpt",
"ON",
f"Input density: {sparsity_stat_str}, switch: ORTMODULE_ENABLE_SPARSE_OPTIMIZER=1/0",
]
)
if len(embed_sparsity_results) > 0:
graph_transformer_config.sparse_embedding_input_names = embed_sparsity_results
logger.info(f"Embedding sparsity based optimization is on for {embed_sparsity_results}")
graph_transformer_config.sparse_embedding_input_names = list(embed_sparsity_results.keys())
self._logger.info("Embedding sparsity-based optimization is ON for %s", embed_sparsity_results)
sparsity_stat_str = ",".join([f"{k}:{v:.0f}%" for k, v in embed_sparsity_results.items()])
self._feature_map.append(
[
" -EmbedSparsityOpt",
"ON",
f"Input density: {sparsity_stat_str}, switch: ORTMODULE_ENABLE_SPARSE_OPTIMIZER=1/0",
]
)
# If users don't want to print input density, disable the input density observer to avoid overhead
# when looping through inputs during training.
if not self._print_input_density:
self._rt_inspector.disable_input_inspector()
def _log_feature_stats(self):
rank = 0
if torch.distributed.is_initialized():
rank = torch.distributed.get_rank()
if rank != 0:
return
self._feature_map.append(
[
"Auto Fallback",
"ON" if self._fallback_manager.policy is not _FallbackPolicy.FALLBACK_DISABLE else "OFF",
"Fallback to PyTorch when encountering unsupported ops",
]
)
mode = "training" if self._export_mode == torch.onnx.TrainingMode.TRAINING else "inference"
mode = f"{_logger.LogColor.UNDERLINE}{mode}{_logger.LogColor.ENDC}"
stat = f"\n\n{_logger.LogColor.HEADER}***** ONNX Runtime Training (ORTModule) is accelerating your model *****{_logger.LogColor.ENDC}\n\n"
stat += f"ORTModule is enabled with following features ON/OFF for [{mode}] mode:\n\n"
for feature_tuple in self._feature_map:
stat += f"{feature_tuple[0]:<20}:\t{feature_tuple[1]:<10}:\t{feature_tuple[2]:<80}\n"
# If anything was captured in fo, raise a single user warning letting users know that there was
# any warning or error that was raised
stat += f"\n{_logger.LogColor.WARNING}There were one or more warnings or errors raised while exporting the PyTorch model.\n"
stat += f"Please enable INFO level logging with DebugOptions to view all warnings and errors.{_logger.LogColor.ENDC}\n\n"
stat += f"Export duration: {self._export_duration_in_ms:.0f} milliseconds\n"
stat += f"Versions: ONNX Runtime - {onnxruntime.__version__}, ONNX - {onnx.__version__}\n\n"
stat += f"{_logger.LogColor.HEADER}************************************************************************{_logger.LogColor.ENDC}\n\n"
self._logger.warning(stat)

View file

@ -3,6 +3,7 @@
# Licensed under the MIT License.
# --------------------------------------------------------------------------
from logging import Logger
from typing import Union
from ._fallback import _FallbackManager
@ -13,9 +14,11 @@ from .debug_options import DebugOptions
class GraphExecutionManagerFactory:
def __init__(self, module: _FlattenedModule, debug_options: DebugOptions, fallback_manager: _FallbackManager):
self._training_manager = TrainingManager(module, debug_options, fallback_manager)
self._inference_manager = InferenceManager(module, debug_options, fallback_manager)
def __init__(
self, module: _FlattenedModule, debug_options: DebugOptions, fallback_manager: _FallbackManager, logger: Logger
):
self._training_manager = TrainingManager(module, debug_options, fallback_manager, logger)
self._inference_manager = InferenceManager(module, debug_options, fallback_manager, logger)
def __call__(self, is_training) -> Union[InferenceManager, TrainingManager]:
if is_training:

View file

@ -3,7 +3,7 @@
# Licensed under the MIT License.
# --------------------------------------------------------------------------
import warnings
from logging import Logger
from typing import Tuple
import onnx
@ -11,7 +11,7 @@ import torch
from onnxruntime.capi import _pybind_state as C
from . import _are_deterministic_algorithms_enabled, _io, _logger, _use_deterministic_algorithms, _utils
from . import _are_deterministic_algorithms_enabled, _io, _use_deterministic_algorithms, _utils
from ._execution_agent import InferenceAgent
from ._fallback import ORTModuleFallbackException, _FallbackManager, _FallbackPolicy
from ._graph_execution_manager import GraphExecutionManager, _RunStateInfo, _SkipCheck
@ -24,8 +24,8 @@ class InferenceManager(GraphExecutionManager):
InferenceManager is responsible for building and running the forward graph of the inference model
"""
def __init__(self, model, debug_options: DebugOptions, fallback_manager: _FallbackManager):
super().__init__(model, debug_options, fallback_manager)
def __init__(self, model, debug_options: DebugOptions, fallback_manager: _FallbackManager, logger: Logger):
super().__init__(model, debug_options, fallback_manager, logger)
self._export_mode = torch.onnx.TrainingMode.EVAL
@staticmethod
@ -92,18 +92,14 @@ class InferenceManager(GraphExecutionManager):
try:
# Issue at most one warning message about fast path
if (
self._first_skip_check_warning is True
and self._skip_check.is_disabled() is False
and self._debug_options.logging.log_level <= _logger.LogLevel.WARNING
):
if self._first_skip_check_warning is True and self._skip_check.is_disabled() is False:
self._first_skip_check_warning = False
warnings.warn(
f"Fast path enabled - skipping checks."
f"rebuild gradient graph: {self._skip_check.is_set(_SkipCheck.SKIP_CHECK_BUILD_GRADIENT)},"
f"execution agent recreation: {self._skip_check.is_set(_SkipCheck.SKIP_CHECK_EXECUTION_AGENT)},"
f"device check: {self._skip_check.is_set(_SkipCheck.SKIP_CHECK_DEVICE)}",
UserWarning,
self._logger.warning(
"Fast path enabled - skipping checks. rebuild gradient graph: %s, execution agent recreation: %s, "
"device check: %s",
self._skip_check.is_set(_SkipCheck.SKIP_CHECK_BUILD_GRADIENT),
self._skip_check.is_set(_SkipCheck.SKIP_CHECK_EXECUTION_AGENT),
self._skip_check.is_set(_SkipCheck.SKIP_CHECK_DEVICE),
)
# If exporting module to ONNX for the first time, this skip check will not take effect.
@ -128,6 +124,8 @@ class InferenceManager(GraphExecutionManager):
# Build the gradient graph
self._build_graph(graph_transformer_config)
self._log_feature_stats()
# If creating the execution agent for the first time, this skip check will not take effect.
# It will only take effect on subsequent forward calls.
create_execution_session = False

View file

@ -6,15 +6,14 @@
import copy
import gc
import inspect
import warnings
from collections import abc
from collections import OrderedDict, abc
from logging import Logger
from typing import Dict, Iterator, List, Mapping, Optional, Sequence, Tuple, Union
import onnx
import torch
from ._fallback import ORTModuleIOError, ORTModuleONNXModelException, wrap_exception
from ._logger import LogLevel, suppress_os_stream_output
from ._runtime_inspector import RuntimeInspector
from ._utils import warn_of_constant_inputs
@ -285,8 +284,8 @@ def _combine_input_buffers_initializers(
_expand_inputs(kwargs, flattened_kwargs_inputs)
buffer_names_dict = {buffer_name: inp for buffer_name, inp in named_buffer}
result = []
embed_sparsity_results = []
label_sparsity_results = []
embed_sparsity_results = OrderedDict()
label_sparsity_results = OrderedDict()
for input_idx, name in enumerate(onnx_input_names):
inp = None
@ -319,12 +318,12 @@ def _combine_input_buffers_initializers(
if _PrimitiveType.is_primitive_type(inp):
inp = _PrimitiveType.get_tensor(inp, device)
found, embedding_is_sparse, label_is_sparse = rt_inspector.inspect_input(name, inp)
found, embedding_density, label_density = rt_inspector.inspect_input(name, inp)
if found:
if embedding_is_sparse is True:
embed_sparsity_results.append(name)
if label_is_sparse is True:
label_sparsity_results.append(name)
if embedding_density < 100:
embed_sparsity_results[name] = embedding_density
if label_density < 100:
label_sparsity_results[name] = label_density
result.append(inp)
else:
raise wrap_exception(
@ -405,7 +404,7 @@ def unflatten_user_output(output_schema: Optional[_ModelInputOutputSchemaType],
return user_output
def _extract_schema(data: _ModelInputOutputType) -> _ModelInputOutputSchemaType:
def _extract_schema(data: _ModelInputOutputType, logger: Logger) -> _ModelInputOutputSchemaType:
"""Extract the data schema by replacing every torch.Tensor value with _TensorStub.
Depth first traversal to iterate over the data:
@ -413,7 +412,6 @@ def _extract_schema(data: _ModelInputOutputType) -> _ModelInputOutputSchemaType:
> Replace None/str typed data with itself
> Recreate tensor from data for other primitive types, and replace them with a stub.
TODO(pengwa): use a unified logger and log level control.
Examples:
Example 1, list:
data = [torch.tensor(1), torch.tensor(2)]
@ -452,11 +450,11 @@ def _extract_schema(data: _ModelInputOutputType) -> _ModelInputOutputSchemaType:
if data is None:
return data
elif isinstance(data, str):
warn_of_constant_inputs(data)
warn_of_constant_inputs(data, logger)
return data
elif _PrimitiveType.is_primitive_type(data):
if isinstance(data, bool):
warn_of_constant_inputs(data)
warn_of_constant_inputs(data, logger)
return _TensorStub(dtype=_PrimitiveType.get_primitive_dtype(data), shape_dims=0)
# Depth first traversal to iterate over the data to replace every tensor with a stub
elif isinstance(data, torch.Tensor):
@ -467,7 +465,7 @@ def _extract_schema(data: _ModelInputOutputType) -> _ModelInputOutputSchemaType:
stubbed_schema: Optional[_ModelInputOutputSchemaType] = None
if isinstance(data, abc.Sequence):
sequence_type = type(data)
stubbed_schema = [_extract_schema(val) for val in data]
stubbed_schema = [_extract_schema(val, logger) for val in data]
try:
# namedtuple can be created by passing the list sequence to method _make
stubbed_schema = sequence_type._make(stubbed_schema)
@ -476,7 +474,7 @@ def _extract_schema(data: _ModelInputOutputType) -> _ModelInputOutputSchemaType:
stubbed_schema = sequence_type(stubbed_schema)
elif isinstance(data, abc.Mapping):
dict_type = type(data)
stubbed_schema = {key: _extract_schema(data[key]) for key in data}
stubbed_schema = {key: _extract_schema(data[key], logger) for key in data}
stubbed_schema = dict_type(**stubbed_schema)
else:
raise wrap_exception(
@ -714,13 +712,13 @@ def parse_inputs_for_onnx_export(
def parse_outputs_for_onnx_export_and_extract_schema(
module, args: Sequence[_ModelInputOutputType], kwargs: Mapping[str, _ModelInputOutputType], log_level: LogLevel
module, args: Sequence[_ModelInputOutputType], kwargs: Mapping[str, _ModelInputOutputType], logger: Logger
):
# Perform a forward call to grab outputs
output_names = None
output_dynamic_axes = None
is_deepcopy = False
with torch.no_grad(), suppress_os_stream_output(log_level=log_level):
with torch.no_grad():
# Deepcopy inputs, since input values may change after model run.
sample_args_copy, sample_kwargs_copy = deepcopy_model_input(*args, **kwargs)
try:
@ -729,7 +727,7 @@ def parse_outputs_for_onnx_export_and_extract_schema(
is_deepcopy = True
except Exception:
model_copy = module
warnings.warn(
logger.warning(
"This model cannot be deep copied (or pickled), "
"which is a required step for stateful models to be properly exported to ONNX."
" Compute will continue, but unexpected results may occur!"
@ -740,7 +738,7 @@ def parse_outputs_for_onnx_export_and_extract_schema(
# Parse the output and extract the output_names and output_dynamic_axes to be used for onnx export
output_names, output_dynamic_axes = _parse_outputs_and_extract_names_and_dynamic_axes(sample_outputs)
output_schema = _extract_schema(sample_outputs)
output_schema = _extract_schema(sample_outputs, logger)
if is_deepcopy:
del model_copy
gc.collect()

View file

@ -6,7 +6,6 @@
import io
import logging
import sys
import warnings
from contextlib import contextmanager
from enum import IntEnum
from typing import Dict, List
@ -42,22 +41,13 @@ def suppress_os_stream_output(suppress_stdout=True, suppress_stderr=True, log_le
sys.stdout = fo
if suppress_stderr and suppress_logs:
sys.stderr = fo
yield
yield fo
finally:
if suppress_stdout:
sys.stdout = stdout
if suppress_stderr:
sys.stderr = stderr
if fo.tell() > 0 and suppress_logs:
# If anything was captured in fo, raise a single user warning letting users know that there was
# some warning or error that was raised
warnings.warn(
"There were one or more warnings or errors raised while exporting the PyTorch "
"model. Please enable INFO level logging to view all warnings and errors.",
UserWarning,
)
ORTMODULE_LOG_LEVEL_MAP: Dict[LogLevel, List[int]] = {
LogLevel.VERBOSE: [Severity.VERBOSE, logging.DEBUG],
@ -74,3 +64,15 @@ def ortmodule_loglevel_to_onnxruntime_c_loglevel(loglevel: LogLevel) -> int:
def ortmodule_loglevel_to_python_loglevel(loglevel: LogLevel) -> int:
return ORTMODULE_LOG_LEVEL_MAP.get(loglevel, [Severity.WARNING, logging.WARNING])[1]
class LogColor:
HEADER = "\033[95m"
BLUE = "\033[94m"
CYAN = "\033[96m"
GREEN = "\033[92m"
WARNING = "\033[93m"
RED = "\033[91m"
ENDC = "\033[0m"
BOLD = "\033[1m"
UNDERLINE = "\033[4m"

View file

@ -40,7 +40,7 @@ class RuntimeInspector:
return self.input_density_ob.initialize(model, user_input_names)
def inspect_input(self, input_name, input_data) -> Tuple[bool, bool, bool]:
def inspect_input(self, input_name, input_data) -> Tuple[bool, float, float]:
"""
Inspect input data and print statistics.
@ -51,13 +51,13 @@ class RuntimeInspector:
Returns:
found: Whether the input name is found in `_embedding_graph_input_to_padding_idx_map` and
`_loss_label_graph_input_to_ignore_idx_map`.
embedding_is_sparse: Whether the input is sparse.
label_is_sparse: Whether the input is sparse.
embed_input_density: Density for the inspected embedding input if found to be True; otherwise, return 100.
label_input_density: Density for the inspected label input if found to be True; otherwise, return 100.
"""
if self.input_density_ob is not None:
return self.input_density_ob.inspect_from_input_data(input_name, input_data)
return (False, False, False)
return (False, 100, 100)
def disable_input_inspector(self) -> None:
"""Disable input density inspector."""
@ -282,7 +282,7 @@ class InputDensityObserver:
[ignore_index, label_preprocess_func]
)
def inspect_from_input_data(self, name: str, inp) -> Tuple[bool, bool, bool]:
def inspect_from_input_data(self, name: str, inp) -> Tuple[bool, float, float]:
"""
Inspect input data and print statistics.
@ -292,15 +292,15 @@ class InputDensityObserver:
Returns:
found: Whether the input name is found in `_embedding_graph_input_to_padding_idx_map` and
`_loss_label_graph_input_to_ignore_idx_map`.
embedding_is_sparse: Whether the input is sparse.
label_is_sparse: Whether the input is sparse.
embed_input_density: Density for the inspected embedding input if found to be True; otherwise, return 100.
label_input_density: Density for the inspected label input if found to be True; otherwise, return 100.
"""
if not self._is_initialized:
return (False, False, False)
return (False, 100, 100)
try:
data = inp.clone()
found, embedding_is_sparse, label_is_sparse = self._inspect_embed_label_input(name, data)
found, embed_input_density, label_input_density = self._inspect_embed_label_input(name, data)
if found:
self._current_step += 1
@ -308,16 +308,15 @@ class InputDensityObserver:
self._last_step = self._current_step
self._print_embed_label_stats()
return (found, embedding_is_sparse, label_is_sparse)
return (found, embed_input_density, label_input_density)
except Exception as e:
self._logger.warning(f"Failed to inspect input {name} due to {e}", UserWarning)
return (False, False, False)
return (False, 100, 100)
def _inspect_embed_label_input(self, name, data):
found = False
embedding_is_sparse = False
label_is_sparse = False
min_embed_density = 100
min_label_density = 100
if (
len(self._embedding_graph_input_to_padding_idx_map) > 0
and name in self._embedding_graph_input_to_padding_idx_map
@ -330,7 +329,8 @@ class InputDensityObserver:
valid_token_per_batch = str(torch.count_nonzero(data - padding_idx, dim=1).tolist())
total_token = data.numel()
embed_density = float(valid_token) / float(total_token) * 100
embedding_is_sparse = embedding_is_sparse or (embed_density < 90)
if embed_density < 90:
min_embed_density = min(min_embed_density, embed_density)
self._stats.append(
[
self._current_step,
@ -355,7 +355,8 @@ class InputDensityObserver:
valid_token = torch.count_nonzero(data_preprocessed - ignore_index)
total_token = data_preprocessed.numel()
label_density = float(valid_token) / float(total_token) * 100
label_is_sparse = label_is_sparse or (label_density < 90)
if label_density < 90:
min_label_density = min(min_label_density, label_density)
self._stats.append(
[
self._current_step,
@ -370,7 +371,7 @@ class InputDensityObserver:
)
found = True
return found, embedding_is_sparse, label_is_sparse
return found, min_embed_density, min_label_density
def _print_embed_label_stats(self):
if len(self._stats) > 0:

View file

@ -2,13 +2,15 @@
# Licensed under the MIT License.
# _torch_module_factory.py
from logging import Logger
from ._fallback import _FallbackManager
from ._torch_module_ort import TorchModuleORT
from .debug_options import DebugOptions
class TorchModuleFactory:
def __call__(self, module, debug_options: DebugOptions, fallback_manager: _FallbackManager):
def __call__(self, module, debug_options: DebugOptions, fallback_manager: _FallbackManager, logger: Logger):
"""Creates a TorchModule instance based on the input module."""
return TorchModuleORT(module, debug_options, fallback_manager)
return TorchModuleORT(module, debug_options, fallback_manager, logger)

View file

@ -3,6 +3,7 @@
# _torch_module_ort.py
from collections import OrderedDict
from logging import Logger
from typing import Callable, Iterator, Optional, Tuple, TypeVar
import torch
@ -17,13 +18,17 @@ T = TypeVar("T", bound="torch.nn.Module")
class TorchModuleORT(TorchModuleInterface):
def __init__(self, module: torch.nn.Module, debug_options: DebugOptions, fallback_manager: _FallbackManager):
def __init__(
self, module: torch.nn.Module, debug_options: DebugOptions, fallback_manager: _FallbackManager, logger: Logger
):
super().__init__(module)
self._flattened_module = _io._FlattenedModule(module)
_utils.patch_torch_module_ort_forward_method(self)
self._execution_manager = GraphExecutionManagerFactory(self._flattened_module, debug_options, fallback_manager)
self._execution_manager = GraphExecutionManagerFactory(
self._flattened_module, debug_options, fallback_manager, logger
)
def _apply(self, fn):
"""Override original method to delegate execution to the flattened PyTorch user module"""

View file

@ -3,7 +3,7 @@
# Licensed under the MIT License.
# --------------------------------------------------------------------------
import warnings
from logging import Logger
from typing import Tuple
import onnx
@ -12,7 +12,7 @@ import torch
from onnxruntime.capi import _pybind_state as C
from onnxruntime.capi.onnxruntime_inference_collection import get_ort_device_type
from . import _are_deterministic_algorithms_enabled, _io, _logger, _use_deterministic_algorithms, _utils
from . import _are_deterministic_algorithms_enabled, _io, _use_deterministic_algorithms, _utils
from ._execution_agent import TrainingAgent
from ._fallback import ORTModuleFallbackException, _FallbackManager, _FallbackPolicy
from ._gradient_accumulation_manager import GradientAccumulationManager
@ -27,8 +27,10 @@ class TrainingManager(GraphExecutionManager):
TrainingManager is responsible for building and running the forward and backward graph of the training model.
"""
def __init__(self, model: _FlattenedModule, debug_options: DebugOptions, fallback_manager: _FallbackManager):
super().__init__(model, debug_options, fallback_manager)
def __init__(
self, model: _FlattenedModule, debug_options: DebugOptions, fallback_manager: _FallbackManager, logger: Logger
):
super().__init__(model, debug_options, fallback_manager, logger)
self._export_mode = torch.onnx.TrainingMode.TRAINING
self._forward_class = self._create_autofunction_class()
@ -215,19 +217,14 @@ class TrainingManager(GraphExecutionManager):
return self._fallback_manager.fallback(self._debug_options.logging.log_level, *inputs, **kwargs)
try:
if (
self._first_skip_check_warning is True
and self._skip_check.is_disabled() is False
and self._debug_options.logging.log_level <= _logger.LogLevel.WARNING
):
if self._first_skip_check_warning is True and self._skip_check.is_disabled() is False:
# Only change this after the firs time a warning is issued.
self._first_skip_check_warning = False
warnings.warn(
f"Fast path enabled - skipping checks."
f" Rebuild graph: {self._skip_check.is_set(_SkipCheck.SKIP_CHECK_BUILD_GRADIENT)},"
f" Execution agent: {self._skip_check.is_set(_SkipCheck.SKIP_CHECK_EXECUTION_AGENT)},"
f" Device check: {self._skip_check.is_set(_SkipCheck.SKIP_CHECK_DEVICE)}",
UserWarning,
self._logger.info(
"Fast path enabled - skipping checks.Rebuild graph: %s, Execution agent: %s, Device check: %s",
self._skip_check.is_set(_SkipCheck.SKIP_CHECK_BUILD_GRADIENT),
self._skip_check.is_set(_SkipCheck.SKIP_CHECK_EXECUTION_AGENT),
self._skip_check.is_set(_SkipCheck.SKIP_CHECK_DEVICE),
)
# If exporting module to ONNX for the first time, this skip check will not take effect.
@ -263,6 +260,8 @@ class TrainingManager(GraphExecutionManager):
# Build the gradient graph
self._build_graph(graph_transformer_config)
self._log_feature_stats()
# If creating the execution agent for the first time, this skip check will not take effect.
# It will only take effect on subsequent forward calls.
create_execution_session = False

View file

@ -7,11 +7,11 @@
import copy
import functools
import inspect
import logging
import os
import random
import traceback
import types
import warnings
from typing import List, Optional, Tuple, Union
import numpy as np
@ -22,11 +22,8 @@ from torch.utils.dlpack import to_dlpack
from onnxruntime.capi import _pybind_state as C
from onnxruntime.capi.onnxruntime_inference_collection import OrtValue
from onnxruntime.tools import pytorch_export_contrib_ops
from . import _onnx_models
from ._custom_gradient_registry import CustomGradientRegistry
from ._custom_op_symbolic_registry import CustomOpSymbolicRegistry
from ._fallback_exceptions import ORTModuleDeviceException, ORTModuleIOError, wrap_exception
from ._torch_module_pytorch import TorchModulePytorch
from .torch_cpp_extensions.cpu.aten_op_executor import load_aten_op_executor_cpp_extension
@ -215,7 +212,9 @@ def _create_iobinding(io_binding, inputs, model, device: torch.device):
io_binding.bind_output(value_info.name, device.type, device_id=device_id)
def check_for_name_collisions_and_bind_methods_to_ortmodule(ortmodule: torch.nn.Module, user_module: torch.nn.Module):
def check_for_name_collisions_and_bind_methods_to_ortmodule(
ortmodule: torch.nn.Module, user_module: torch.nn.Module, logger: logging.Logger
):
"""Warns if there are any common attributes between the user's model and ORTModule and binds user methods to ORTModule
If there are methods defined on the user's model that ORTModule does not recognize (custom methods),
@ -255,7 +254,7 @@ def check_for_name_collisions_and_bind_methods_to_ortmodule(ortmodule: torch.nn.
# This is a user defined/overridden method. Check for collisions.
if attribute_name in ortmodule_attributes:
# This is a user defined method, issue a warning.
warnings.warn(
logger.warning(
f"User Module's attribute name {attribute_name} collides with ORTModule's attribute name. "
"User Module's method may not be called upon invocation through ORTModule."
)
@ -269,7 +268,7 @@ def check_for_name_collisions_and_bind_methods_to_ortmodule(ortmodule: torch.nn.
if attribute_name not in torch_module_attributes and attribute_name in ortmodule_attributes:
# This is a user defined attribute that collides with ORTModule
if attribute_name in ortmodule_attributes:
warnings.warn(
logger.warning(
f"User Module's attribute name {attribute_name} collides with ORTModule's attribute name. "
"User Module's attribute may not be returned when trying to retrieve the attribute through ORTModule."
)
@ -345,8 +344,8 @@ def switch_backend_to_pytorch(ortmodule, pytorch_module):
ortmodule.forward = pytorch_module.forward
def warn_of_constant_inputs(data):
warnings.warn(
def warn_of_constant_inputs(data, logger: logging.Logger):
logger.info(
f"Received input of type {type(data)} which may be treated as a constant by ORT by default."
" Please consider moving constant arguments to the model constructor."
)
@ -389,19 +388,6 @@ def patch_ortmodule_forward_method(ortmodule):
functools.update_wrapper(ortmodule.forward.__func__, ortmodule._torch_module.forward.__func__)
def reinitialize_ortmodule(ortmodule):
# Re-register contrib OPs
pytorch_export_contrib_ops.register()
CustomOpSymbolicRegistry.register_all()
CustomGradientRegistry.register_all()
# Re-initialize the ORTModule forward method
patch_ortmodule_forward_method(ortmodule)
# Re-bind users custom methods to ORTModule
check_for_name_collisions_and_bind_methods_to_ortmodule(ortmodule, ortmodule.module)
def reinitialize_torch_module_ort(torch_module):
# Re-initialize the forward method
patch_torch_module_ort_forward_method(torch_module)
@ -427,3 +413,9 @@ def reinitialize_graph_execution_manager(graph_execution_manager):
def reinitialize_training_manager(training_manager):
# Redefine training managers forward_class
training_manager._forward_class = training_manager._create_autofunction_class()
def get_runtime_pytorch_version():
from packaging import version
return version.parse(torch.__version__.split("+")[0])

View file

@ -4,6 +4,7 @@
# --------------------------------------------------------------------------
# isort: skip_file
# Import ordering is important in this module to aviod circular dependencies
import logging
from ._torch_module_factory import TorchModuleFactory
from ._torch_module_ort import TorchModuleORT
from ._custom_op_symbolic_registry import CustomOpSymbolicRegistry
@ -11,6 +12,7 @@ from ._custom_gradient_registry import CustomGradientRegistry
from . import _utils
from .debug_options import DebugOptions
from ._fallback import _FallbackManager, _FallbackPolicy, ORTModuleFallbackException
from ._logger import ortmodule_loglevel_to_python_loglevel
from onnxruntime.training import ortmodule
from onnxruntime.tools import pytorch_export_contrib_ops
@ -51,9 +53,15 @@ class ORTModule(torch.nn.Module):
if not debug_options:
debug_options = DebugOptions()
self._logger = logging.getLogger(__name__)
self._logger.setLevel(ortmodule_loglevel_to_python_loglevel(debug_options.logging.log_level))
# Fallback settings
self._fallback_manager = _FallbackManager(
pytorch_module=module, policy=ortmodule.ORTMODULE_FALLBACK_POLICY, retry=ortmodule.ORTMODULE_FALLBACK_RETRY
pytorch_module=module,
policy=ortmodule.ORTMODULE_FALLBACK_POLICY,
retry=ortmodule.ORTMODULE_FALLBACK_RETRY,
logger=self._logger,
)
try:
@ -63,7 +71,7 @@ class ORTModule(torch.nn.Module):
super().__init__()
self._torch_module = TorchModuleFactory()(module, debug_options, self._fallback_manager)
self._torch_module = TorchModuleFactory()(module, debug_options, self._fallback_manager, self._logger)
_utils.patch_ortmodule_forward_method(self)
@ -75,7 +83,7 @@ class ORTModule(torch.nn.Module):
# Warn user if there are name collisions between user model's and ORTModule attributes
# And if there are custom methods defined on the user's model, copy and bind them to
# ORTModule.
_utils.check_for_name_collisions_and_bind_methods_to_ortmodule(self, module)
_utils.check_for_name_collisions_and_bind_methods_to_ortmodule(self, module, self._logger)
except ORTModuleFallbackException as e:
# Although backend is switched to PyTorch here,
@ -310,4 +318,13 @@ class ORTModule(torch.nn.Module):
def __setstate__(self, state):
self.__dict__.update(state)
_utils.reinitialize_ortmodule(self)
# Re-register contrib OPs
pytorch_export_contrib_ops.register()
CustomOpSymbolicRegistry.register_all()
CustomGradientRegistry.register_all()
# Re-initialize the ORTModule forward method
_utils.patch_ortmodule_forward_method(self)
# Re-bind users custom methods to ORTModule
_utils.check_for_name_collisions_and_bind_methods_to_ortmodule(self, self.module, self._logger)

View file

@ -4214,18 +4214,20 @@ def test_hf_save_pretrained():
assert p1.data.ne(p2.data).sum() == 0
def test_ortmodule_string_inputs_are_ignored():
def test_ortmodule_string_inputs_are_ignored(caplog):
pt_model = MyStrNet()
ort_model = ORTModule(copy.deepcopy(pt_model))
ort_model = ORTModule(copy.deepcopy(pt_model), DebugOptions(log_level=LogLevel.INFO))
x = torch.randn(1, 2)
with pytest.warns(UserWarning) as warning_record:
out = ort_model(x, "hello")
out = ort_model(x, "hello")
assert (
"Received input of type <class 'str'> which may be treated as a constant by ORT by default."
in warning_record[1].message.args[0]
)
target_str = "Received input of type <class 'str'> which may be treated as a constant by ORT by default."
found_target_str = False
for record in caplog.records:
if target_str in record.message:
found_target_str = True
assert found_target_str
_test_helpers.assert_values_are_close(out, x + 1)
@ -4803,8 +4805,7 @@ def test_ortmodule_setattr_signals_model_changed():
del os.environ["ORTMODULE_SKIPCHECK_POLICY"]
@pytest.mark.skipif(Version(torch.__version__) < Version("1.13.0"), reason="PyTorch 1.12 incompatible")
def test_ortmodule_attribute_name_collision_warning():
def test_ortmodule_attribute_name_collision_warning(caplog):
class UserNet(torch.nn.Module):
def __init__(self):
super().__init__()
@ -4819,18 +4820,14 @@ def test_ortmodule_attribute_name_collision_warning():
device = "cuda"
pt_model = UserNet().to(device)
with pytest.warns(UserWarning) as warning_record:
ORTModule(pt_model)
# FutureWarning('The first argument to symbolic functions is deprecated in 1.13 and will be removed in the future.
# Please annotate treat the first argument (g) as GraphContext and use context information from the object
# instead.')
# TODO(bmeswani): Check with the exporter team as to what this might mean for ortmodule.
# For ROCm EP, the log above appears twice.
assert len(warning_record) == 3 or len(warning_record) == 4
ORTModule(pt_model)
warning_record = [record.message for record in caplog.records if record.levelname == "WARNING"]
assert "_torch_module collides with ORTModule's attribute name." in warning_record[-2].message.args[0]
assert "load_state_dict collides with ORTModule's attribute name." in warning_record[-1].message.args[0]
assert len(warning_record) == 2
assert "_torch_module collides with ORTModule's attribute name." in warning_record[-2]
assert "load_state_dict collides with ORTModule's attribute name." in warning_record[-1]
def test_ortmodule_ortmodule_method_attribute_copy():
@ -5733,10 +5730,10 @@ def test_runtime_inspector_label_and_embed_sparsity_detection(embed_is_sparse, l
found_embed_is_sparse = False
found_label_is_sparse = False
for record in caplog.records:
if "Label sparsity based optimization is on for" in record.getMessage():
if "Label sparsity-based optimization is ON for" in record.getMessage():
found_label_is_sparse = True
if "Embedding sparsity based optimization is on for" in record.getMessage():
if "Embedding sparsity-based optimization is ON for" in record.getMessage():
found_embed_is_sparse = True
if label_is_sparse:

View file

@ -533,7 +533,7 @@ def test_ortmodule_fallback_onnx_model__missing_op(is_training, fallback_enabled
@pytest.mark.parametrize("is_training,persist_fallback", list(itertools.product([True, False], repeat=2)))
def test_ortmodule_fallback_warn_message(is_training, persist_fallback):
def test_ortmodule_fallback_warn_message(is_training, persist_fallback, caplog):
# is_training: True for torch.nn.Module training model, eval mode otherwise
policy = "FALLBACK_UNSUPPORTED_DEVICE"
@ -555,18 +555,35 @@ def test_ortmodule_fallback_warn_message(is_training, persist_fallback):
inputs = torch.randn(N, D_in, device=data_device)
for i in range(3):
with pytest.raises(RuntimeError), pytest.warns(UserWarning) as warning_record:
# The run MUST fail no matter with ORT or PyTorch, so we catch the RuntimeError here in case it breaks the test.
# Be noted, the logs will be caught by caplog.
with pytest.raises(RuntimeError):
ort_model(inputs)
# For retries, the warn message will always be logged
if not persist_fallback:
assert "Fallback to PyTorch due to exception" in str(warning_record[0].message.args[0])
if i == 0:
# For the first time, run ORTModule, feature map is logged as warning
# And the fallback warning is logged.
assert len(caplog.records) == 2
else:
# For the other time, only the fallback warning is logged.
assert len(caplog.records) == 1
assert "Fallback to PyTorch due to exception" in caplog.records[-1].message
caplog.clear()
continue
# If `retries` is not enabled, only log the warn message once
if i == 0:
assert "Fallback to PyTorch due to exception" in str(warning_record[0].message.args[0])
# For the first time, run ORTModule, feature map is logged as warning
# And the fallback warning is logged.
assert len(caplog.records) == 2
assert "Fallback to PyTorch due to exception" in caplog.records[1].message
caplog.clear()
else:
assert warning_record.list == []
# For the other time, no fallback warning will be logged because
# we are running with PyTorch.
assert len(caplog.records) == 0
caplog.clear()
del os.environ["ORTMODULE_SKIPCHECK_POLICY"]