Initial import: grid-bot — grid trading bot for BTC-USDT on Cifra Markets
This commit is contained in:
@@ -0,0 +1,39 @@
|
||||
"""Utility to lazily import modules."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib
|
||||
from typing import Any, TYPE_CHECKING
|
||||
|
||||
|
||||
class _LazyModule:
|
||||
"""Lazily import a module."""
|
||||
|
||||
def __init__(self, module_name: str) -> None:
|
||||
self._name = module_name
|
||||
self._module: Any = None
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<lazy module '{self._name}'>"
|
||||
|
||||
def __getattr__(self, attr: str) -> object:
|
||||
if self._module is None:
|
||||
self._module = importlib.import_module(".", self._name)
|
||||
return getattr(self._module, attr)
|
||||
|
||||
|
||||
# Import the following modules during type checking to enable code intelligence features,
|
||||
# such as auto-completion in tools like pylance, even when these modules are not explicitly
|
||||
# imported in user code.
|
||||
# NOTE: Add additional used imports here.
|
||||
if TYPE_CHECKING:
|
||||
import onnx
|
||||
import onnx_ir # type: ignore[import-untyped, import-not-found]
|
||||
import onnxscript
|
||||
import onnxscript._framework_apis.torch_2_11 as onnxscript_apis
|
||||
|
||||
else:
|
||||
onnx = _LazyModule("onnx")
|
||||
onnx_ir = _LazyModule("onnx_ir")
|
||||
onnxscript = _LazyModule("onnxscript")
|
||||
onnxscript_apis = _LazyModule("onnxscript._framework_apis.torch_2_11")
|
||||
@@ -0,0 +1,271 @@
|
||||
"""Compatibility analyzer for PyTorch models."""
|
||||
|
||||
# mypy: allow-untyped-defs
|
||||
# flake8: noqa: B950 We do not need flake8 as it complains line length
|
||||
from __future__ import annotations
|
||||
|
||||
import dataclasses
|
||||
import operator
|
||||
import textwrap
|
||||
import traceback
|
||||
from collections import defaultdict
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import torch
|
||||
import torch._export.serde.schema
|
||||
from torch.export import graph_signature
|
||||
from torch.onnx._internal.exporter import _dispatching, _registration
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import torch.fx
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class ModelInfo:
|
||||
"""Information about the model."""
|
||||
|
||||
parameter_count: defaultdict[torch.dtype, int] = dataclasses.field(
|
||||
default_factory=lambda: defaultdict(int)
|
||||
)
|
||||
buffer_count: defaultdict[torch.dtype, int] = dataclasses.field(
|
||||
default_factory=lambda: defaultdict(int)
|
||||
)
|
||||
fx_node_count: int = 0
|
||||
fx_node_op_count: defaultdict[str, int] = dataclasses.field(
|
||||
default_factory=lambda: defaultdict(int)
|
||||
)
|
||||
fx_node_target_count: defaultdict[str, int] = dataclasses.field(
|
||||
default_factory=lambda: defaultdict(int)
|
||||
)
|
||||
dispatch_failures: list[tuple[torch.fx.Node, str]] = dataclasses.field(
|
||||
default_factory=list
|
||||
)
|
||||
inputs: dict[str, torch._export.serde.schema.TensorMeta] = dataclasses.field(
|
||||
default_factory=dict
|
||||
)
|
||||
outputs: dict[str, torch._export.serde.schema.TensorMeta] = dataclasses.field(
|
||||
default_factory=dict
|
||||
)
|
||||
|
||||
|
||||
def _count_weights(
|
||||
exported_program: torch.export.ExportedProgram,
|
||||
) -> tuple[defaultdict[torch.dtype, int], defaultdict[torch.dtype, int]]:
|
||||
"""Count the size of the parameters in the exported program."""
|
||||
|
||||
parameter_count: defaultdict[torch.dtype, int] = defaultdict(int)
|
||||
buffer_count: defaultdict[torch.dtype, int] = defaultdict(int)
|
||||
for parameter in exported_program.parameters():
|
||||
dtype = parameter.dtype
|
||||
parameter_count[dtype] += parameter.numel()
|
||||
|
||||
for buffer in exported_program.buffers():
|
||||
dtype = buffer.dtype
|
||||
buffer_count[dtype] += buffer.numel()
|
||||
|
||||
return parameter_count, buffer_count
|
||||
|
||||
|
||||
def _format_model_info(model_info: ModelInfo) -> str:
|
||||
"""Format the information about the model."""
|
||||
lines = [
|
||||
textwrap.dedent(
|
||||
f"""\
|
||||
PyTorch ONNX Conversion Analysis
|
||||
|
||||
## Model Information
|
||||
|
||||
The model has {sum(model_info.parameter_count.values())} parameters and {sum(model_info.buffer_count.values())} buffers (non-trainable parameters).
|
||||
Number of parameters per dtype:
|
||||
```python
|
||||
{model_info.parameter_count}
|
||||
```
|
||||
Number of buffers per dtype:
|
||||
```python
|
||||
{model_info.buffer_count}
|
||||
```
|
||||
"""
|
||||
),
|
||||
"Inputs:",
|
||||
*[f"- `{name}`: `{meta}`" for name, meta in model_info.inputs.items()],
|
||||
"",
|
||||
"Outputs:",
|
||||
*[f"- `{name}`: `{meta}`" for name, meta in model_info.outputs.items()],
|
||||
"",
|
||||
f"The FX graph has {model_info.fx_node_count} nodes in total. Number of FX nodes per op:",
|
||||
]
|
||||
for op, count in model_info.fx_node_op_count.items():
|
||||
lines.append(f"- `{op}`: {count}")
|
||||
lines.append("\n")
|
||||
lines.append("Of the call_function nodes, the counts of operators used are:\n")
|
||||
sorted_targets = sorted(
|
||||
model_info.fx_node_target_count.items(),
|
||||
key=operator.itemgetter(1),
|
||||
reverse=True,
|
||||
)
|
||||
for target, count in sorted_targets:
|
||||
lines.append(f"- `{target}`: {count}")
|
||||
|
||||
lines.append("")
|
||||
lines.append("## ONNX Conversion Information")
|
||||
lines.append("")
|
||||
|
||||
if model_info.dispatch_failures:
|
||||
lines.append(
|
||||
"The model contains operators the dispatcher could not find registered ONNX decompositions for. "
|
||||
"This may be due to missing implementations, decompositions not registered "
|
||||
"correctly, or a bug in the dispatcher."
|
||||
)
|
||||
lines.append("")
|
||||
lines.append("Errors grouped by operator:\n")
|
||||
|
||||
target_to_nodes = defaultdict(list)
|
||||
for node, _ in model_info.dispatch_failures:
|
||||
target_to_nodes[str(node.target)].append(node)
|
||||
|
||||
target_to_messages = {}
|
||||
for node, message in model_info.dispatch_failures:
|
||||
if str(node.target) not in target_to_messages:
|
||||
target_to_messages[str(node.target)] = message
|
||||
|
||||
for target, nodes in sorted(
|
||||
target_to_nodes.items(), key=operator.itemgetter(0), reverse=True
|
||||
):
|
||||
message = textwrap.indent(
|
||||
f"{target_to_messages[target]}. Example node: `{nodes[0].format_node()}`. All nodes: `{nodes}`",
|
||||
" ",
|
||||
)
|
||||
lines.append(f"- `{target}`: {message}")
|
||||
else:
|
||||
lines.append("All operators in the model have registered ONNX decompositions.")
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def _get_io_specs(exported_program: torch.export.ExportedProgram) -> tuple[dict, dict]:
|
||||
"""Get the input and output specs of the exported program."""
|
||||
|
||||
nodes: dict[str, torch.fx.Node] = {
|
||||
node.name: node for node in exported_program.graph.nodes
|
||||
}
|
||||
user_inputs = [
|
||||
spec
|
||||
for spec in exported_program.graph_signature.input_specs
|
||||
if spec.kind == graph_signature.InputKind.USER_INPUT
|
||||
]
|
||||
user_outputs = [
|
||||
spec
|
||||
for spec in exported_program.graph_signature.output_specs
|
||||
if spec.kind == graph_signature.OutputKind.USER_OUTPUT
|
||||
]
|
||||
inputs: dict[str, torch._export.serde.schema.TensorMeta | str] = {}
|
||||
outputs: dict[str, torch._export.serde.schema.TensorMeta | str] = {}
|
||||
for spec in user_inputs:
|
||||
inputs = _log_spec_into_io_specs(spec, nodes, inputs)
|
||||
for spec in user_outputs:
|
||||
outputs = _log_spec_into_io_specs(spec, nodes, outputs)
|
||||
return inputs, outputs
|
||||
|
||||
|
||||
def _log_spec_into_io_specs(
|
||||
spec: graph_signature.InputSpec,
|
||||
nodes: dict[str, torch.fx.Node],
|
||||
inputs_or_outputs: dict[str, torch._export.serde.schema.TensorMeta | str],
|
||||
) -> dict[str, torch._export.serde.schema.TensorMeta | str]:
|
||||
# If dynamic is set to a constant input, it becomes a
|
||||
# symbolic argument, which is not a tensor.
|
||||
if isinstance(spec.arg, graph_signature.ConstantArgument):
|
||||
# Constant input does not have tensor_meta.
|
||||
return inputs_or_outputs
|
||||
# Symbolic arguments are not tensors, so it does not have tensor_meta,
|
||||
# but we need to provide a string representation for them to inform users.
|
||||
name = spec.arg.name
|
||||
if isinstance(
|
||||
spec.arg,
|
||||
(
|
||||
graph_signature.SymIntArgument,
|
||||
graph_signature.SymFloatArgument,
|
||||
graph_signature.SymBoolArgument,
|
||||
),
|
||||
):
|
||||
argument_to_str: dict[type[graph_signature.ArgumentSpec], str] = {
|
||||
graph_signature.SymIntArgument: "SymInt",
|
||||
graph_signature.SymFloatArgument: "SymFloat",
|
||||
graph_signature.SymBoolArgument: "SymBool",
|
||||
}
|
||||
inputs_or_outputs[name] = argument_to_str[type(spec.arg)]
|
||||
return inputs_or_outputs
|
||||
# FIXME: tensor_meta is None sometimes when the exported program still knows the shape/type
|
||||
inputs_or_outputs[name] = nodes[name].meta["tensor_meta"]
|
||||
return inputs_or_outputs
|
||||
|
||||
|
||||
def _count_fx_targets(
|
||||
exported_program: torch.export.ExportedProgram,
|
||||
) -> defaultdict[str, int]:
|
||||
"""Count the number of targets for each node in the exported program."""
|
||||
fx_node_target_count: defaultdict[str, int] = defaultdict(int)
|
||||
for node in exported_program.graph.nodes:
|
||||
if node.op == "call_function":
|
||||
fx_node_target_count[str(node.target)] += 1
|
||||
return fx_node_target_count
|
||||
|
||||
|
||||
def analyze(
|
||||
exported_program: torch.export.ExportedProgram,
|
||||
registry: _registration.ONNXRegistry | None = None,
|
||||
file=None,
|
||||
) -> None:
|
||||
"""Analyze the compatibility of the exported program."""
|
||||
# Get basic information about the model
|
||||
model_info = ModelInfo()
|
||||
model_info.parameter_count, model_info.buffer_count = _count_weights(
|
||||
exported_program
|
||||
)
|
||||
model_info.fx_node_count = len(exported_program.graph.nodes)
|
||||
model_info.fx_node_target_count = _count_fx_targets(exported_program)
|
||||
inputs, outputs = _get_io_specs(exported_program)
|
||||
model_info.inputs = inputs
|
||||
model_info.outputs = outputs
|
||||
|
||||
if registry is None:
|
||||
registry = _registration.ONNXRegistry.from_torchlib()
|
||||
|
||||
# Try to find ops for every node in the graph
|
||||
for node in exported_program.graph.nodes:
|
||||
model_info.fx_node_op_count[node.op] += 1
|
||||
if node.op == "call_function":
|
||||
try:
|
||||
onnx_function, message = _dispatching.dispatch(node, registry)
|
||||
except Exception as e:
|
||||
message = "Critical Error in dispatcher:\n"
|
||||
formatted_exception = "\n".join(
|
||||
traceback.format_exception(type(e), e, e.__traceback__)
|
||||
)
|
||||
message += f"```pytb\n{formatted_exception}\n```\n"
|
||||
onnx_function = None
|
||||
if onnx_function is None:
|
||||
model_info.dispatch_failures.append((node, message))
|
||||
|
||||
# Print the results
|
||||
report = _format_model_info(model_info)
|
||||
print(report, file=file, flush=True)
|
||||
|
||||
|
||||
def compare_ops(
|
||||
program_a: torch.export.ExportedProgram, program_b: torch.export.ExportedProgram
|
||||
) -> tuple[set[str], set[str]]:
|
||||
"""Compare and get unique ops in two exported programs.
|
||||
|
||||
Args:
|
||||
program_a: The first exported program.
|
||||
program_b: The second exported program.
|
||||
|
||||
Returns:
|
||||
A tuple of two sets, where the first set contains the unique ops in the first program
|
||||
and the second set contains the unique ops in the second program.
|
||||
"""
|
||||
program_a_ops = set(_count_fx_targets(program_a))
|
||||
program_b_ops = set(_count_fx_targets(program_b))
|
||||
return program_a_ops - program_b_ops, program_b_ops - program_a_ops
|
||||
@@ -0,0 +1,734 @@
|
||||
"""NOTES:
|
||||
|
||||
We need a typing module that will handling Python to ONNX type promotion for use.
|
||||
For example, if we have torch.ops.aten.add(Tensor, 1.0), we need to promote 1.0
|
||||
to the same type as Tensor. The same thing needs to work for
|
||||
torch.ops.aten.add(1.0, Tensor) as well, which means we need a mechanism to`
|
||||
"""
|
||||
|
||||
# mypy: allow-untyped-defs
|
||||
# mypy: disable-error-code=union-attr
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import inspect
|
||||
import logging
|
||||
from collections.abc import Iterable, Mapping, Sequence
|
||||
from typing import Any, TYPE_CHECKING
|
||||
|
||||
from onnxscript import evaluator
|
||||
|
||||
import torch
|
||||
from torch.onnx._internal._lazy_import import onnx_ir as ir, onnxscript
|
||||
from torch.onnx._internal.exporter import _errors, _schemas, _tensors
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import onnx
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
ValidAttributeType = (
|
||||
ir.TensorProtocol
|
||||
| int
|
||||
| float
|
||||
| bool
|
||||
| str
|
||||
| Sequence[int]
|
||||
| Sequence[float]
|
||||
| None
|
||||
)
|
||||
|
||||
AllowedArgType = ir.Value | Sequence[ir.Value | ValidAttributeType] | ValidAttributeType
|
||||
|
||||
|
||||
# Logic for adapting inputs from general Python or PyTorch inputs to ONNX ir.Value
|
||||
def _construct_named_inputs_and_attrs(
|
||||
signature: ir.schemas.OpSignature,
|
||||
args: Sequence[AllowedArgType],
|
||||
kwargs: Mapping[str, AllowedArgType],
|
||||
) -> tuple[dict[str, AllowedArgType], dict[str, ValidAttributeType]]:
|
||||
"""Construct two mappings: name to inputs and named to attributes based on the signature and args/kwargs.
|
||||
|
||||
This function uses the OpSignature to determine which argument in args and kwargs corresponds to
|
||||
which parameter in the signature. ONNX node inputs are stored in named_inputs, and attributes are
|
||||
stored in named_attrs. If an _optional input_ is not provided, it is filled with None.
|
||||
|
||||
Args:
|
||||
signature: The OpSignature for the node.
|
||||
args: The positional arguments for the node.
|
||||
kwargs: The keyword arguments for the node.
|
||||
|
||||
Returns:
|
||||
A tuple of two mappings: named_inputs and named_attrs.
|
||||
|
||||
Raises:
|
||||
ValueError: If a required parameter is not provided.
|
||||
"""
|
||||
# 1. Construct the (named_inputs, named_attrs) mapping based on (args, kwargs) and the signature.
|
||||
# a. Loop over all parameters in the signature and args together
|
||||
# b. Depending on param.is_input, Record named_inputs[param.name] = arg or named_attrs[param.name] = arg
|
||||
# c. Handle kwargs as well
|
||||
# d. Fill in None if the input is not provided
|
||||
named_inputs: dict[str, Any] = {}
|
||||
named_attrs: dict[str, Any] = {}
|
||||
reversed_args_stack = list(reversed(args))
|
||||
for param in signature.params:
|
||||
if isinstance(param, ir.schemas.Parameter):
|
||||
# Handle inputs
|
||||
if reversed_args_stack:
|
||||
# First exhaust the positional arguments
|
||||
if param.variadic:
|
||||
# Handle variadic arguments
|
||||
named_inputs[param.name] = tuple(args)
|
||||
reversed_args_stack.clear()
|
||||
else:
|
||||
named_inputs[param.name] = reversed_args_stack.pop() # type: ignore[assignment]
|
||||
elif param.name in kwargs:
|
||||
named_inputs[param.name] = kwargs[param.name] # type: ignore[assignment]
|
||||
elif param.required:
|
||||
raise ValueError(
|
||||
f"Required parameter '{param.name}' is not provided. "
|
||||
f"Signature: {signature}. Args: {args}. Kwargs: {kwargs}."
|
||||
)
|
||||
else:
|
||||
logger.debug(
|
||||
"Optional parameter '%s' is not provided. Added as None. Signature: %s",
|
||||
param.name,
|
||||
signature,
|
||||
)
|
||||
named_inputs[param.name] = None # type: ignore[assignment]
|
||||
else:
|
||||
# Handle attributes
|
||||
attribute: ValidAttributeType | ir.Attr
|
||||
if not isinstance(param, ir.schemas.AttributeParameter):
|
||||
raise AssertionError(f"Expected AttributeParameter, got {type(param)}")
|
||||
if reversed_args_stack:
|
||||
# First exhaust the positional arguments
|
||||
attribute = reversed_args_stack.pop() # type: ignore[assignment]
|
||||
elif param.name in kwargs:
|
||||
attribute = kwargs[param.name] # type: ignore[assignment]
|
||||
elif param.default is not None:
|
||||
attribute = param.default
|
||||
else:
|
||||
attribute = None
|
||||
|
||||
if attribute is None:
|
||||
if param.required:
|
||||
raise ValueError(
|
||||
f"Required attribute '{param.name}' is not provided. "
|
||||
f"Signature: {signature}. Args: {args}. Kwargs: {kwargs}."
|
||||
)
|
||||
else:
|
||||
logger.debug(
|
||||
"Optional attribute '%s' is None. Dropped. Signature: %s",
|
||||
param.name,
|
||||
signature,
|
||||
)
|
||||
continue
|
||||
|
||||
if isinstance(attribute, ir.Attr):
|
||||
# Turn the attribute from an default value into an actual parameter for the node
|
||||
attr_copied = copy.copy(attribute)
|
||||
# Make sure the name is the same as the parameter name and not the name of the default parameter
|
||||
attr_copied.name = param.name
|
||||
attribute = attr_copied
|
||||
|
||||
if isinstance(attribute, int) and param.type == ir.AttributeType.FLOAT:
|
||||
# Convert the attribute to float if needed. This happens in PyTorch
|
||||
# where an attribute marked as float can be passed as an int.
|
||||
attribute = float(attribute)
|
||||
named_attrs[param.name] = attribute
|
||||
return named_inputs, named_attrs # type: ignore[return-value]
|
||||
|
||||
|
||||
def _resolve_parameter_dtypes(
|
||||
signature: ir.schemas.OpSignature, named_inputs: Mapping[str, AllowedArgType]
|
||||
) -> Mapping[ir.schemas.TypeConstraintParam, ir.TypeProtocol]:
|
||||
"""Determine which parameter takes which type.
|
||||
|
||||
Handle non-tensor input corner cases and type promotion.
|
||||
|
||||
Requires:
|
||||
All ir.Value in name_inputs should have type set. Their type should be
|
||||
compatible with the type_constraint of the corresponding parameter in the signature.
|
||||
|
||||
Args:
|
||||
signature: The OpSignature for the node.
|
||||
named_inputs: The mapping of parameter names to their arguments.
|
||||
|
||||
Returns:
|
||||
A mapping of Constraint names to ir.TypeProtocol.
|
||||
"""
|
||||
# a. Create type_binding: dict[str, ir.TypeProtocol]
|
||||
# b. Iterate over all named_inputs
|
||||
# b0. Find the corresponding parameter in the signature
|
||||
# b1. If the argument is a Python constant, skip.
|
||||
# b2. If the argument is a ir.Value, Bind {constraint: arg.type}.
|
||||
type_binding = {}
|
||||
for name, arg in named_inputs.items():
|
||||
param = signature.params_map[name]
|
||||
if not isinstance(param, ir.schemas.Parameter):
|
||||
raise AssertionError(f"Expected Parameter, got {type(param)}")
|
||||
if isinstance(arg, (int, float, bool, str, Sequence, torch.Tensor)):
|
||||
# Skip the Python constants because we do not know what dtype they should take yet
|
||||
continue
|
||||
elif isinstance(arg, ir.Value):
|
||||
if arg.type is None:
|
||||
# Skip the ir.Value if the type is not set
|
||||
continue
|
||||
# NOTE: We assume arg.type is compatible with the type_constraint
|
||||
if arg.type is None:
|
||||
raise AssertionError(f"Expected type to be set for {arg}")
|
||||
# TODO(justinchuby): Implement type promotion logic here.
|
||||
type_binding[param.type_constraint] = arg.type
|
||||
return type_binding
|
||||
|
||||
|
||||
def _determine_input_dtype(
|
||||
param: ir.schemas.Parameter,
|
||||
arg: AllowedArgType,
|
||||
type_binding: Mapping[ir.schemas.TypeConstraintParam, ir.TypeProtocol],
|
||||
) -> ir.DataType:
|
||||
"""Determine the dtype of the input that is a mix of Python constants and ir.Value."""
|
||||
if param.type_constraint in type_binding:
|
||||
# A known dtype is available because it was resolved
|
||||
return type_binding[param.type_constraint].dtype
|
||||
if len(param.type_constraint.allowed_types) == 1:
|
||||
# Only one type is allowed by the type constraint
|
||||
return next(iter(param.type_constraint.allowed_types)).dtype
|
||||
|
||||
# No dtype information available. Infer from the Python constant or (in the Sequence case)
|
||||
# from a mix of Python constants and ir.Value
|
||||
if isinstance(arg, bool):
|
||||
return ir.DataType.BOOL
|
||||
if isinstance(arg, float):
|
||||
return ir.DataType.FLOAT
|
||||
if isinstance(arg, int):
|
||||
return ir.DataType.INT64
|
||||
if isinstance(arg, str):
|
||||
return ir.DataType.STRING
|
||||
if isinstance(arg, (ir.Tensor, ir.TensorProtocol)):
|
||||
return arg.dtype
|
||||
if isinstance(arg, complex):
|
||||
return ir.DataType.FLOAT
|
||||
if arg is None:
|
||||
return ir.DataType.UNDEFINED
|
||||
|
||||
# Handle sequences
|
||||
if isinstance(arg, (tuple, list)):
|
||||
if len(arg) == 0:
|
||||
# Special case: Treat empty sequence as INT64 as they are typically used for shape
|
||||
return ir.DataType.INT64
|
||||
|
||||
# Try to obtain the dtype from one of the values
|
||||
for val in arg:
|
||||
if isinstance(val, ir.Value) and val.dtype is not None:
|
||||
return val.dtype
|
||||
|
||||
if any(isinstance(val, float) for val in arg):
|
||||
# If any float is present, the dtype is float
|
||||
return ir.DataType.FLOAT
|
||||
elif any(isinstance(val, int) for val in arg):
|
||||
# Otherwise if any int is present, the dtype is int
|
||||
return ir.DataType.INT64
|
||||
|
||||
raise ValueError(
|
||||
f"Could not determine the dtype for the input '{param.name}'. "
|
||||
f"param={param}, arg={arg}, param_type_constraint={param.type_constraint}, "
|
||||
f"type_binding={type_binding}"
|
||||
)
|
||||
|
||||
|
||||
def _allowed_types_are_sequence_types(allowed_types: Iterable[ir.TypeProtocol]) -> bool:
|
||||
"""Check if all allowed types are Sequence types."""
|
||||
return all(isinstance(t, ir.SequenceType) for t in allowed_types)
|
||||
|
||||
|
||||
def _get_or_create_constant(
|
||||
constant_farm: dict[
|
||||
tuple[
|
||||
bool
|
||||
| int
|
||||
| float
|
||||
| str
|
||||
| tuple[int, ...]
|
||||
| tuple[float, ...]
|
||||
| tuple[bool, ...],
|
||||
ir.DataType,
|
||||
],
|
||||
ir.Value,
|
||||
],
|
||||
arg: bool
|
||||
| int
|
||||
| float
|
||||
| str
|
||||
| tuple[int, ...]
|
||||
| tuple[float, ...]
|
||||
| tuple[bool, ...]
|
||||
| list[int]
|
||||
| list[float]
|
||||
| list[bool],
|
||||
dtype: ir.DataType,
|
||||
opset: onnxscript.values.Opset,
|
||||
) -> ir.Value:
|
||||
# float representation of complex numbers
|
||||
if isinstance(arg, complex):
|
||||
# Convert the complex number to a float
|
||||
arg = (arg.real, arg.imag)
|
||||
|
||||
if isinstance(arg, list):
|
||||
# Make the arg hashable
|
||||
# pyrefly: ignore [bad-argument-type]
|
||||
arg = tuple(arg)
|
||||
|
||||
constant_value = constant_farm.get((arg, dtype)) # type: ignore[arg-type]
|
||||
if constant_value is None:
|
||||
constant_tensor = ir.tensor(value=arg, dtype=dtype)
|
||||
constant_value = opset.Constant(value=constant_tensor)
|
||||
constant_farm[(arg, dtype)] = constant_value # type: ignore[arg-type,index]
|
||||
return constant_value # type: ignore[return-value]
|
||||
|
||||
|
||||
def _process_python_constants(
|
||||
signature: ir.schemas.OpSignature,
|
||||
named_inputs: dict[str, AllowedArgType],
|
||||
type_binding: Mapping[ir.schemas.TypeConstraintParam, ir.TypeProtocol],
|
||||
constant_farm: dict[
|
||||
tuple[
|
||||
bool | int | float | str | tuple[int, ...] | tuple[float, ...],
|
||||
ir.DataType,
|
||||
],
|
||||
ir.Value,
|
||||
],
|
||||
opset: onnxscript.values.Opset,
|
||||
) -> dict[str, ir.Value | None]:
|
||||
"""Convert Python constants to Constant nodes and list to Sequence nodes based on the dtype information.
|
||||
|
||||
The added constants will be replacing values in named_inputs in place.
|
||||
|
||||
Args:
|
||||
signature: The OpSignature for the node.
|
||||
named_inputs: The mapping of parameter names to their arguments.
|
||||
type_binding: A mapping of Constraint names to ir.DataType.
|
||||
constant_farm: A dictionary of {(py_value, ir.DataType): ir.Value} to store the deduplicated constants.
|
||||
opset: The Opset to use for creating Constant nodes.
|
||||
|
||||
Returns:
|
||||
A mapping of parameter names to Python constants converted to constant Nodes.
|
||||
"""
|
||||
# 3. Convert Python constants to Constant nodes based on the dtype information;
|
||||
# construct sequences
|
||||
# a. Iterate over all parameters in the signature the second time
|
||||
# b. If the parameter is in to_resolve_type:
|
||||
# - If param.constraint in type_binding,
|
||||
# Get the constant from constant_farm (deduplicated);
|
||||
# otherwise set named_inputs[param.name] = Constant(value, dtype=type_binding[param.constraint])
|
||||
# - Otherwise, set named_inputs[param.name] = Constant(value)
|
||||
for name, arg in named_inputs.items():
|
||||
param = signature.params_map[name]
|
||||
if not isinstance(param, ir.schemas.Parameter):
|
||||
raise AssertionError(f"Expected Parameter, got {type(param)}")
|
||||
|
||||
if isinstance(arg, ir.Value):
|
||||
# TODO(justinchuby): Cast the ir.Value here if needed
|
||||
continue
|
||||
|
||||
if (
|
||||
isinstance(arg, Sequence)
|
||||
and len(arg) > 0
|
||||
and any(isinstance(val, ir.Value) for val in arg)
|
||||
):
|
||||
# Skip the sequence of ir.Value. This is a variadic input or a Sequence input
|
||||
# It will be handled by _process_python_sequences
|
||||
continue
|
||||
if param.variadic:
|
||||
# Handled by _process_python_sequences
|
||||
continue
|
||||
if _allowed_types_are_sequence_types(param.type_constraint.allowed_types):
|
||||
# Handled by _process_python_sequences
|
||||
continue
|
||||
|
||||
dtype = _determine_input_dtype(param, arg, type_binding)
|
||||
|
||||
if arg is None:
|
||||
constant_value = None
|
||||
elif isinstance(arg, (ir.Tensor, ir.TensorProtocol)):
|
||||
constant_value = opset.Constant(value=arg)
|
||||
else:
|
||||
# Deduplicate the constants
|
||||
constant_value = _get_or_create_constant(constant_farm, arg, dtype, opset) # type: ignore[arg-type]
|
||||
|
||||
named_inputs[param.name] = constant_value
|
||||
return named_inputs # type: ignore[return-value]
|
||||
|
||||
|
||||
def _reshape_to_1d_tensor(opset: onnxscript.values.Opset, arg: ir.Value) -> ir.Value:
|
||||
"""Reshape the input to a 1D tensor."""
|
||||
|
||||
return opset.Reshape(
|
||||
arg, opset.Constant(value=ir.tensor([-1], dtype=ir.DataType.INT64))
|
||||
)
|
||||
|
||||
|
||||
def _process_python_sequences(
|
||||
signature: ir.schemas.OpSignature,
|
||||
named_inputs: dict[str, AllowedArgType],
|
||||
type_binding: Mapping[ir.schemas.TypeConstraintParam, ir.TypeProtocol],
|
||||
constant_farm: dict[
|
||||
tuple[
|
||||
bool
|
||||
| int
|
||||
| float
|
||||
| str
|
||||
| ir.TensorProtocol
|
||||
| tuple[bool, ...]
|
||||
| tuple[int, ...]
|
||||
| tuple[float, ...],
|
||||
ir.DataType,
|
||||
],
|
||||
ir.Value,
|
||||
],
|
||||
opset: onnxscript.values.Opset,
|
||||
):
|
||||
"""Handle three types of sequences.
|
||||
|
||||
1. Variadic inputs
|
||||
2. Sequence input of ir.Value,
|
||||
3. Sequence of Python constants that contains ir.Value
|
||||
"""
|
||||
for name, arg in named_inputs.items():
|
||||
param = signature.params_map[name]
|
||||
if not isinstance(param, ir.schemas.Parameter):
|
||||
raise AssertionError(f"Expected Parameter, got {type(param)}")
|
||||
|
||||
if not isinstance(arg, (tuple, list)):
|
||||
continue
|
||||
|
||||
if len(arg) == 0:
|
||||
# Skip empty sequences
|
||||
continue
|
||||
|
||||
# 1. Sequence input of ir.Value
|
||||
if _allowed_types_are_sequence_types(param.type_constraint.allowed_types):
|
||||
# Turn the list into a Sequence node
|
||||
# Constant op creation will be handled by the variadic case below when calling
|
||||
# the SequenceConstruct op.
|
||||
named_inputs[name] = opset.SequenceConstruct(*arg)
|
||||
continue
|
||||
|
||||
# 2. Variadic inputs
|
||||
# NOTE: Variadic operators like Max can be called with mixed ir.Value and Python constants
|
||||
# like `Max(0, ir.Value())`
|
||||
# We need to convert the Python constants to Constant nodes
|
||||
if param.variadic:
|
||||
if all(isinstance(val, ir.Value) for val in arg):
|
||||
# Skip the variadic input if all values are ir.Value
|
||||
continue
|
||||
|
||||
dtype = _determine_input_dtype(param, arg, type_binding)
|
||||
new_args = []
|
||||
for val in arg:
|
||||
if isinstance(val, ir.Value):
|
||||
new_args.append(val)
|
||||
else:
|
||||
constant_tensor = ir.tensor(value=val, dtype=dtype) # type: ignore[arg-type]
|
||||
constant_value = opset.Constant(value=constant_tensor)
|
||||
new_args.append(constant_value)
|
||||
named_inputs[name] = new_args
|
||||
continue
|
||||
else:
|
||||
# 3. Concat the list as a single input
|
||||
# E.g. [Value, 42] should be converted to op.Concat(Value, Constant(42))
|
||||
# when the expected input type is INT64
|
||||
# We assume this only happens for 0D cases
|
||||
if all(isinstance(val, ir.Value) for val in arg):
|
||||
expanded_args = [_reshape_to_1d_tensor(opset, val) for val in arg]
|
||||
named_inputs[name] = opset.Concat(*expanded_args, axis=0)
|
||||
continue
|
||||
|
||||
dtype = _determine_input_dtype(param, arg, type_binding)
|
||||
new_args = []
|
||||
for val in arg:
|
||||
if isinstance(val, ir.Value):
|
||||
new_args.append(_reshape_to_1d_tensor(opset, val))
|
||||
elif val is None:
|
||||
# Skip None values
|
||||
continue
|
||||
elif isinstance(val, (ir.Tensor, ir.TensorProtocol)):
|
||||
new_args.append(
|
||||
_reshape_to_1d_tensor(opset, opset.Constant(value=val))
|
||||
)
|
||||
else:
|
||||
# Turn the Python constant into 1D tensor for the constant
|
||||
if not isinstance(val, (bool, int, float)):
|
||||
raise AssertionError(f"Expected int or float, got {type(val)}")
|
||||
new_args.append(
|
||||
_get_or_create_constant(constant_farm, [val], dtype, opset) # type: ignore[arg-type]
|
||||
)
|
||||
named_inputs[name] = opset.Concat(*new_args, axis=0)
|
||||
continue
|
||||
return named_inputs
|
||||
|
||||
|
||||
def _determine_output_number(
|
||||
signature: ir.schemas.OpSignature, named_attrs: Mapping[str, ValidAttributeType]
|
||||
) -> int:
|
||||
"""Determine the number of outputs for the node with heuristics."""
|
||||
if signature.domain == "":
|
||||
if signature.name == "BatchNormalization":
|
||||
if not named_attrs.get("training_mode", 0):
|
||||
return 1
|
||||
if signature.name == "Split":
|
||||
num_outputs = named_attrs.get("num_outputs")
|
||||
if num_outputs is not None and isinstance(num_outputs, int):
|
||||
return num_outputs
|
||||
else:
|
||||
raise ValueError(
|
||||
"Could not determine the number of outputs for Split. "
|
||||
"num_outputs must be provided"
|
||||
)
|
||||
return len(signature.outputs)
|
||||
|
||||
|
||||
def _construct_node(
|
||||
signature: ir.schemas.OpSignature,
|
||||
named_inputs: Mapping[str, ir.Value | None],
|
||||
named_attrs: Mapping[str, ValidAttributeType],
|
||||
opset: onnxscript.values.Opset,
|
||||
num_outputs: int,
|
||||
) -> ir.Node:
|
||||
"""Construct the node with the inputs and attributes.
|
||||
|
||||
Variadic inputs are flattened.
|
||||
|
||||
Args:
|
||||
signature: The OpSignature for the node.
|
||||
named_inputs: The mapping of parameter names to their arguments. When we
|
||||
do not have the schema of an operator, we do not know the names of
|
||||
the inputs, in which case the names can be anything because they
|
||||
are not used in this function. The data structure is passed in for
|
||||
consistency with the other functions.
|
||||
named_attrs: The mapping of attribute names to their values.
|
||||
num_outputs: The number of outputs for the node.
|
||||
"""
|
||||
inputs: list[ir.Value | None] = []
|
||||
# Flatten variadic inputs
|
||||
for value in named_inputs.values():
|
||||
if isinstance(value, Sequence):
|
||||
inputs.extend(value)
|
||||
else:
|
||||
inputs.append(value)
|
||||
|
||||
# If final inputs are None, strip them from the node inputs
|
||||
for input in reversed(inputs):
|
||||
if input is not None:
|
||||
break
|
||||
inputs.pop()
|
||||
|
||||
# Construct and filter out None attributes
|
||||
attributes = [
|
||||
attr
|
||||
for attr in ir.convenience.convert_attributes(named_attrs)
|
||||
if attr.value is not None
|
||||
]
|
||||
outputs = [_tensors.SymbolicTensor(opset) for _ in range(num_outputs)]
|
||||
return ir.Node(
|
||||
signature.domain,
|
||||
signature.name,
|
||||
inputs=inputs,
|
||||
attributes=attributes,
|
||||
outputs=outputs,
|
||||
version=signature.since_version,
|
||||
)
|
||||
|
||||
|
||||
class OpRecorder(evaluator.Evaluator):
|
||||
"""An onnxscript Evaluator that captures the graph into ONNX IR."""
|
||||
|
||||
def __init__(
|
||||
self, opset: onnxscript.values.Opset, constant_farm: dict[Any, ir.Value]
|
||||
) -> None:
|
||||
self.nodes: list[ir.Node] = []
|
||||
self.opset = opset
|
||||
self.functions: dict[
|
||||
ir.OperatorIdentifier, onnxscript.OnnxFunction | ir.Function
|
||||
] = {}
|
||||
self.constant_farm = constant_farm
|
||||
|
||||
def _call_op(
|
||||
self,
|
||||
op_signature: ir.schemas.OpSignature,
|
||||
named_inputs: dict[str, AllowedArgType],
|
||||
named_attrs: dict[str, ValidAttributeType],
|
||||
num_outputs: int,
|
||||
) -> Sequence[_tensors.SymbolicTensor]:
|
||||
"""Record nodes for the given opschema and arguments.
|
||||
|
||||
Args:
|
||||
op_signature: The OpSchema containing the node signature.
|
||||
named_inputs: The mapping of parameter names to their arguments.
|
||||
named_attrs: The mapping of attribute names to their values.
|
||||
"""
|
||||
type_binding = _resolve_parameter_dtypes(op_signature, named_inputs)
|
||||
try:
|
||||
converted_named_inputs = _process_python_constants(
|
||||
op_signature, named_inputs, type_binding, self.constant_farm, self.opset
|
||||
)
|
||||
converted_named_inputs = _process_python_sequences(
|
||||
op_signature,
|
||||
converted_named_inputs, # type: ignore[arg-type]
|
||||
type_binding,
|
||||
self.constant_farm,
|
||||
self.opset,
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
raise _errors.GraphConstructionError(
|
||||
f"Error processing Python constants for operator '{op_signature.domain}::{op_signature.name}'. "
|
||||
f"named_inputs={named_inputs}, named_attrs={named_attrs}, opset={self.opset}, op_signature={op_signature}."
|
||||
) from e
|
||||
|
||||
try:
|
||||
self.nodes.append(
|
||||
node := _construct_node(
|
||||
op_signature,
|
||||
converted_named_inputs,
|
||||
named_attrs,
|
||||
self.opset,
|
||||
num_outputs,
|
||||
)
|
||||
)
|
||||
except Exception as e:
|
||||
raise _errors.GraphConstructionError(
|
||||
f"Error constructing node for operator '{op_signature.domain}::{op_signature.name}'. "
|
||||
f"named_inputs={named_inputs}, converted_named_inputs={converted_named_inputs}, "
|
||||
f"named_attrs={named_attrs}, opset={self.opset}, op_signature={op_signature}."
|
||||
) from e
|
||||
return node.outputs # type: ignore[return-value]
|
||||
|
||||
def eval(
|
||||
self,
|
||||
schema: onnx.defs.OpSchema,
|
||||
args: Sequence[AllowedArgType], # type: ignore[override]
|
||||
kwargs: Mapping[str, AllowedArgType],
|
||||
) -> _tensors.SymbolicTensor | Sequence[_tensors.SymbolicTensor]:
|
||||
try:
|
||||
op_signature = ir.schemas.OpSignature.from_op_schema(schema)
|
||||
named_inputs, named_attrs = _construct_named_inputs_and_attrs(
|
||||
op_signature, args, kwargs
|
||||
)
|
||||
# TODO(justinchuby): Handle cast
|
||||
if schema.name == "CastLike":
|
||||
if len(named_inputs) != 2:
|
||||
raise AssertionError(f"Expected 2 inputs, got {len(named_inputs)}")
|
||||
# Skip CastLike if the input and output types are the same
|
||||
src_input = named_inputs["input"]
|
||||
target_type = named_inputs["target_type"]
|
||||
|
||||
if (
|
||||
isinstance(src_input, ir.Value)
|
||||
and isinstance(target_type, ir.Value)
|
||||
and src_input.dtype is not None
|
||||
and target_type.dtype is not None
|
||||
):
|
||||
# dtypes are available
|
||||
if src_input.dtype == target_type.dtype:
|
||||
# Same type. No cast needed
|
||||
return src_input # type: ignore[return-value]
|
||||
else:
|
||||
# Create a Cast node
|
||||
return self.opset.Cast(src_input, to=target_type.dtype) # type: ignore[union-attr,return-value]
|
||||
|
||||
num_outputs = _determine_output_number(op_signature, named_attrs)
|
||||
outputs = self._call_op(
|
||||
op_signature, named_inputs, named_attrs, num_outputs
|
||||
)
|
||||
if len(outputs) == 1:
|
||||
return outputs[0]
|
||||
return outputs
|
||||
except Exception as e:
|
||||
raise _errors.GraphConstructionError(
|
||||
f"Error calling operator '{schema.name}' with args {args} and kwargs {kwargs}."
|
||||
) from e
|
||||
|
||||
def eval_function( # type: ignore[override]
|
||||
self,
|
||||
function: onnxscript.OnnxFunction,
|
||||
args: Sequence[AllowedArgType],
|
||||
kwargs: Mapping[str, AllowedArgType],
|
||||
) -> _tensors.SymbolicTensor | Sequence[_tensors.SymbolicTensor] | bool | int:
|
||||
try:
|
||||
# NOTE: signature should be written to function in the registration process
|
||||
if hasattr(function, "_pt_onnx_signature"):
|
||||
op_signature = function._pt_onnx_signature # type: ignore[attr-defined]
|
||||
else:
|
||||
op_signature = _schemas.op_signature_from_function(
|
||||
function,
|
||||
function.function_ir.domain,
|
||||
function.name,
|
||||
since_version=function.opset.version,
|
||||
)
|
||||
function._pt_onnx_signature = op_signature # type: ignore[attr-defined]
|
||||
|
||||
named_inputs, named_attrs = _construct_named_inputs_and_attrs(
|
||||
op_signature, args, kwargs
|
||||
)
|
||||
|
||||
# TODO(after torchlib migration): Remove traceable function handling
|
||||
# NOTE: We need to call traceable functions after the _construct_named_inputs_and_attrs
|
||||
# call because it will filter out the unexpected kwargs for us.
|
||||
if function.traceable:
|
||||
# Trace the function call instead of adding the function as a node
|
||||
# Turn the ir.Attr objects into Python constants first
|
||||
named_attrs = {
|
||||
name: attr.value if isinstance(attr, ir.Attr) else attr
|
||||
for name, attr in named_attrs.items()
|
||||
}
|
||||
|
||||
# Use the type binding to resolve the dtypes of the inputs, and
|
||||
# convert Python constants to Constant nodes
|
||||
type_binding = _resolve_parameter_dtypes(op_signature, named_inputs)
|
||||
try:
|
||||
# _process_python_sequences is not here because we want to preserve python list
|
||||
# properties for the function call
|
||||
converted_named_inputs = _process_python_constants(
|
||||
op_signature,
|
||||
named_inputs,
|
||||
type_binding,
|
||||
self.constant_farm,
|
||||
self.opset,
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
raise _errors.GraphConstructionError(
|
||||
f"Error processing Python constants for operator '{op_signature.domain}::{op_signature.name}'. "
|
||||
f"named_inputs={named_inputs}, named_attrs={named_attrs}, opset={self.opset}, op_signature={op_signature}."
|
||||
) from e
|
||||
|
||||
return function.function(**converted_named_inputs, **named_attrs)
|
||||
|
||||
outputs = self._call_op(
|
||||
op_signature,
|
||||
named_inputs,
|
||||
named_attrs,
|
||||
len(op_signature.outputs),
|
||||
)
|
||||
|
||||
self.functions[(function.function_ir.domain, function.name, "")] = function
|
||||
if len(outputs) == 1:
|
||||
return outputs[0]
|
||||
return outputs
|
||||
except Exception as e:
|
||||
try:
|
||||
source_file = inspect.getsourcefile(function.function)
|
||||
_, lineno = inspect.getsourcelines(function.function)
|
||||
except Exception:
|
||||
source_file = lineno = None
|
||||
raise _errors.GraphConstructionError(
|
||||
f"Error calling function '{function.name}' with args {args} and kwargs {kwargs}."
|
||||
+ f" The function is defined at '{source_file}:{lineno}'."
|
||||
if source_file
|
||||
else ""
|
||||
) from e
|
||||
+304
@@ -0,0 +1,304 @@
|
||||
"""Strategies for capturing ExportedPrograms."""
|
||||
|
||||
# mypy: allow-untyped-defs
|
||||
from __future__ import annotations
|
||||
|
||||
import abc
|
||||
import contextlib
|
||||
import dataclasses
|
||||
import datetime
|
||||
import logging
|
||||
import pathlib
|
||||
from typing import Any, TYPE_CHECKING
|
||||
|
||||
import torch
|
||||
from torch.onnx import _flags
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import os
|
||||
from collections.abc import Callable
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _verbose_printer(verbose: bool | None) -> Callable[..., None]:
|
||||
"""Prints messages based on `verbose`."""
|
||||
if verbose is False:
|
||||
return lambda *_, **__: None
|
||||
|
||||
return lambda *args, **kwargs: print("[torch.onnx]", *args, **kwargs)
|
||||
|
||||
|
||||
def _take_first_line(text: str) -> str:
|
||||
"""Take the first line of a text."""
|
||||
lines = text.split("\n", maxsplit=1)
|
||||
first_line = lines[0]
|
||||
if len(lines) > 1:
|
||||
first_line += "[...]"
|
||||
return first_line
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def _patch_dynamo_unsupported_functions():
|
||||
"""Patch PyTorch to bypass some functions torch.export.export does not support."""
|
||||
# TODO: Remove the patches once dynamo supports these functions.
|
||||
import torch.jit
|
||||
|
||||
# Replace torch.jit.isinstance with isinstance
|
||||
jit_isinstance = torch.jit.isinstance
|
||||
# pyrefly: ignore [bad-assignment]
|
||||
torch.jit.isinstance = isinstance
|
||||
logger.info("Replaced torch.jit.isinstance with isinstance to allow dynamo tracing")
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
torch.jit.isinstance = jit_isinstance
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class Result:
|
||||
exported_program: torch.export.ExportedProgram | None
|
||||
strategy: str
|
||||
exception: Exception | None = None
|
||||
|
||||
@property
|
||||
def success(self) -> bool:
|
||||
"""Whether the capture was successful.
|
||||
|
||||
An exception can still be recorded even if the capture was successful. In
|
||||
this case the exception is informational only. For example, draft_export
|
||||
can record an exception if there are warnings during the export. The exceptions
|
||||
will go into the onnx export report when report=True.
|
||||
"""
|
||||
return self.exported_program is not None
|
||||
|
||||
|
||||
class CaptureStrategy(abc.ABC):
|
||||
"""Strategy for capturing a module as ExportedProgram.
|
||||
|
||||
To use a strategy, create an instance and call it with the model, args, kwargs, and dynamic_shapes.
|
||||
Example::
|
||||
|
||||
strategy = TorchExportNonStrictStrategy(verbose=True)
|
||||
result = strategy(model, args, kwargs, dynamic_shapes)
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
verbose: bool = False,
|
||||
dump: bool = False,
|
||||
artifacts_dir: str | os.PathLike = ".",
|
||||
timestamp: str | None = None,
|
||||
) -> None:
|
||||
"""Initialize the strategy.
|
||||
|
||||
Args:
|
||||
verbose: Whether to print verbose messages.
|
||||
dump: Whether to dump the intermediate artifacts to a file.
|
||||
"""
|
||||
self._verbose_print = _verbose_printer(verbose)
|
||||
self._dump = dump
|
||||
self._artifacts_dir = pathlib.Path(artifacts_dir)
|
||||
self._timestamp = timestamp or datetime.datetime.now().strftime(
|
||||
"%Y-%m-%d_%H-%M-%S-%f"
|
||||
)
|
||||
self._exception: Exception | None = None
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
model: torch.nn.Module | torch.jit.ScriptFunction,
|
||||
args: tuple[Any, ...],
|
||||
kwargs: dict[str, Any] | None,
|
||||
dynamic_shapes,
|
||||
) -> Result:
|
||||
self._enter(model)
|
||||
if kwargs is None:
|
||||
kwargs = {}
|
||||
try:
|
||||
exported_program = self._capture(model, args, kwargs, dynamic_shapes)
|
||||
except Exception as e:
|
||||
self._failure(model, e)
|
||||
return Result(
|
||||
exported_program=None,
|
||||
strategy=self.__class__.__name__,
|
||||
exception=e,
|
||||
)
|
||||
self._success(model)
|
||||
return Result(
|
||||
exported_program,
|
||||
strategy=self.__class__.__name__,
|
||||
exception=self._exception,
|
||||
)
|
||||
|
||||
@abc.abstractmethod
|
||||
def _capture(
|
||||
self, model, args, kwargs, dynamic_shapes
|
||||
) -> torch.export.ExportedProgram:
|
||||
raise NotImplementedError
|
||||
|
||||
def _enter(self, model: torch.nn.Module | torch.jit.ScriptFunction) -> None:
|
||||
return
|
||||
|
||||
def _success(self, model: torch.nn.Module | torch.jit.ScriptFunction) -> None:
|
||||
return
|
||||
|
||||
def _failure(
|
||||
self, model: torch.nn.Module | torch.jit.ScriptFunction, e: Exception
|
||||
) -> None:
|
||||
return
|
||||
|
||||
|
||||
class TorchExportStrictStrategy(CaptureStrategy):
|
||||
def _capture(
|
||||
self, model, args, kwargs, dynamic_shapes
|
||||
) -> torch.export.ExportedProgram:
|
||||
with (
|
||||
_patch_dynamo_unsupported_functions(),
|
||||
# Support the dynamism with 0/1 input dim
|
||||
torch.fx.experimental._config.patch(backed_size_oblivious=True), # type: ignore[attr-defined]
|
||||
):
|
||||
try:
|
||||
return torch.export.export(
|
||||
model,
|
||||
args,
|
||||
kwargs=kwargs,
|
||||
dynamic_shapes=dynamic_shapes,
|
||||
strict=True,
|
||||
prefer_deferred_runtime_asserts_over_guards=_flags.PREFER_DEFERRED_RUNTIME_ASSERTS_OVER_GUARDS,
|
||||
)
|
||||
except torch._dynamo.exc.UserError as exc:
|
||||
# Refine the dynamic shapes based on the suggested fixes.
|
||||
try:
|
||||
new_shapes = torch.export.dynamic_shapes.refine_dynamic_shapes_from_suggested_fixes(
|
||||
exc.msg, dynamic_shapes
|
||||
)
|
||||
except Exception:
|
||||
# If the dynamic shapes cannot be refined, re-raise the exception.
|
||||
raise exc from None
|
||||
return torch.export.export(
|
||||
model,
|
||||
args,
|
||||
kwargs=kwargs,
|
||||
dynamic_shapes=new_shapes,
|
||||
strict=True,
|
||||
prefer_deferred_runtime_asserts_over_guards=_flags.PREFER_DEFERRED_RUNTIME_ASSERTS_OVER_GUARDS,
|
||||
)
|
||||
|
||||
def _enter(self, model) -> None:
|
||||
model_repr = _take_first_line(repr(model))
|
||||
self._verbose_print(
|
||||
f"Obtain model graph for `{model_repr}` with `torch.export.export(..., strict=True)`..."
|
||||
)
|
||||
|
||||
def _success(self, model) -> None:
|
||||
model_repr = _take_first_line(repr(model))
|
||||
self._verbose_print(
|
||||
f"Obtain model graph for `{model_repr}` with `torch.export.export(..., strict=True)`... ✅"
|
||||
)
|
||||
|
||||
def _failure(self, model, e) -> None:
|
||||
del e # Unused
|
||||
model_repr = _take_first_line(repr(model))
|
||||
self._verbose_print(
|
||||
f"Obtain model graph for `{model_repr}` with `torch.export.export(..., strict=True)`... ❌"
|
||||
)
|
||||
|
||||
|
||||
class TorchExportNonStrictStrategy(CaptureStrategy):
|
||||
def _capture(
|
||||
self, model, args, kwargs, dynamic_shapes
|
||||
) -> torch.export.ExportedProgram:
|
||||
with (
|
||||
# Support the dynamism with 0/1 input dim
|
||||
torch.fx.experimental._config.patch(backed_size_oblivious=True), # type: ignore[attr-defined]
|
||||
):
|
||||
try:
|
||||
return torch.export.export(
|
||||
model,
|
||||
args,
|
||||
kwargs=kwargs,
|
||||
dynamic_shapes=dynamic_shapes,
|
||||
strict=False,
|
||||
prefer_deferred_runtime_asserts_over_guards=_flags.PREFER_DEFERRED_RUNTIME_ASSERTS_OVER_GUARDS,
|
||||
)
|
||||
except torch._dynamo.exc.UserError as exc:
|
||||
# Refine the dynamic shapes based on the suggested fixes.
|
||||
try:
|
||||
new_shapes = torch.export.dynamic_shapes.refine_dynamic_shapes_from_suggested_fixes(
|
||||
exc.msg, dynamic_shapes
|
||||
)
|
||||
except Exception:
|
||||
# If the dynamic shapes cannot be refined, re-raise the exception.
|
||||
raise exc from None
|
||||
return torch.export.export(
|
||||
model,
|
||||
args,
|
||||
kwargs=kwargs,
|
||||
dynamic_shapes=new_shapes,
|
||||
strict=False,
|
||||
prefer_deferred_runtime_asserts_over_guards=_flags.PREFER_DEFERRED_RUNTIME_ASSERTS_OVER_GUARDS,
|
||||
)
|
||||
|
||||
def _enter(self, model) -> None:
|
||||
model_repr = _take_first_line(repr(model))
|
||||
self._verbose_print(
|
||||
f"Obtain model graph for `{model_repr}` with `torch.export.export(..., strict=False)`..."
|
||||
)
|
||||
|
||||
def _success(self, model) -> None:
|
||||
model_repr = _take_first_line(repr(model))
|
||||
self._verbose_print(
|
||||
f"Obtain model graph for `{model_repr}` with `torch.export.export(..., strict=False)`... ✅"
|
||||
)
|
||||
|
||||
def _failure(self, model, e) -> None:
|
||||
del e # Unused
|
||||
model_repr = _take_first_line(repr(model))
|
||||
self._verbose_print(
|
||||
f"Obtain model graph for `{model_repr}` with `torch.export.export(..., strict=False)`... ❌"
|
||||
)
|
||||
|
||||
|
||||
class TorchExportDraftExportStrategy(CaptureStrategy):
|
||||
def _capture(
|
||||
self, model, args, kwargs, dynamic_shapes
|
||||
) -> torch.export.ExportedProgram:
|
||||
ep = torch.export.draft_export(
|
||||
model, args, kwargs=kwargs, dynamic_shapes=dynamic_shapes
|
||||
)
|
||||
report = ep._report # type: ignore[attr-defined]
|
||||
if not report.successful():
|
||||
self._exception = RuntimeError(str(report))
|
||||
self._verbose_print(f"Draft Export report:\n{report}")
|
||||
return ep
|
||||
|
||||
def _enter(self, model) -> None:
|
||||
model_repr = _take_first_line(repr(model))
|
||||
self._verbose_print(
|
||||
f"Obtain model graph for `{model_repr}` with `torch.export.draft_export`..."
|
||||
)
|
||||
|
||||
def _success(self, model) -> None:
|
||||
model_repr = _take_first_line(repr(model))
|
||||
self._verbose_print(
|
||||
f"Obtain model graph for `{model_repr}` with `torch.export.draft_export`... ✅"
|
||||
)
|
||||
|
||||
def _failure(self, model, e) -> None:
|
||||
del e # Unused
|
||||
model_repr = _take_first_line(repr(model))
|
||||
self._verbose_print(
|
||||
f"Obtain model graph for `{model_repr}` with `torch.export.draft_export`... ❌"
|
||||
)
|
||||
|
||||
|
||||
CAPTURE_STRATEGIES: tuple[type[CaptureStrategy], ...] = (
|
||||
TorchExportNonStrictStrategy, # strict=False is preferred over strict=True because it does not have dynamo issues
|
||||
TorchExportStrictStrategy,
|
||||
)
|
||||
|
||||
if _flags.ENABLE_DRAFT_EXPORT:
|
||||
CAPTURE_STRATEGIES = (*CAPTURE_STRATEGIES, TorchExportDraftExportStrategy)
|
||||
@@ -0,0 +1,202 @@
|
||||
"""Compatibility functions for the torch.onnx.export API."""
|
||||
|
||||
# mypy: allow-untyped-defs
|
||||
# mypy: disable-error-code=attr-defined
|
||||
from __future__ import annotations
|
||||
|
||||
import io
|
||||
import logging
|
||||
import warnings
|
||||
from collections.abc import Callable, Mapping, Sequence
|
||||
from typing import Any, TYPE_CHECKING
|
||||
|
||||
import torch
|
||||
from torch.onnx import _constants as onnx_constants
|
||||
from torch.onnx._internal._lazy_import import onnx
|
||||
from torch.onnx._internal.exporter import (
|
||||
_constants,
|
||||
_core,
|
||||
_dynamic_shapes,
|
||||
_exportable_module,
|
||||
_onnx_program,
|
||||
_registration,
|
||||
)
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import os
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _get_torch_export_args(
|
||||
args: tuple[Any, ...],
|
||||
kwargs: dict[str, Any] | None,
|
||||
) -> tuple[tuple[Any, ...], dict[str, Any] | None]:
|
||||
"""Obtain the arguments for torch.onnx.export from the model and the input arguments."""
|
||||
if not kwargs and args and isinstance(args[-1], dict):
|
||||
kwargs = args[-1]
|
||||
args = args[:-1]
|
||||
return args, kwargs
|
||||
|
||||
|
||||
def export_compat(
|
||||
model: torch.nn.Module
|
||||
| torch.export.ExportedProgram
|
||||
| torch.jit.ScriptModule
|
||||
| torch.jit.ScriptFunction,
|
||||
args: tuple[Any, ...],
|
||||
f: str | os.PathLike | None = None,
|
||||
*,
|
||||
kwargs: dict[str, Any] | None = None,
|
||||
export_params: bool = True,
|
||||
verbose: bool | None = None,
|
||||
input_names: Sequence[str] | None = None,
|
||||
output_names: Sequence[str] | None = None,
|
||||
opset_version: int | None = onnx_constants.ONNX_DEFAULT_OPSET,
|
||||
custom_translation_table: dict[Callable, Callable] | None = None,
|
||||
dynamic_axes: Mapping[str, Mapping[int, str]]
|
||||
| Mapping[str, Sequence[int]]
|
||||
| None = None,
|
||||
dynamic_shapes: dict[str, Any] | tuple[Any, ...] | list[Any] | None = None,
|
||||
keep_initializers_as_inputs: bool = False,
|
||||
external_data: bool = True,
|
||||
report: bool = False,
|
||||
optimize: bool = True,
|
||||
verify: bool = False,
|
||||
profile: bool = False,
|
||||
dump_exported_program: bool = False,
|
||||
artifacts_dir: str | os.PathLike = ".",
|
||||
) -> _onnx_program.ONNXProgram:
|
||||
if opset_version is None:
|
||||
opset_version = onnx_constants.ONNX_DEFAULT_OPSET
|
||||
|
||||
if isinstance(model, torch.nn.Module):
|
||||
if model.training:
|
||||
warnings.warn(
|
||||
"Exporting a model while it is in training mode. "
|
||||
"Please ensure that this is intended, as it may lead to "
|
||||
"different behavior during inference. "
|
||||
"Calling model.eval() before export is recommended.",
|
||||
UserWarning,
|
||||
stacklevel=3,
|
||||
)
|
||||
|
||||
if isinstance(model, _exportable_module.ExportableModule):
|
||||
# Skip argument extraction if args or kwargs are provided
|
||||
if not args and not kwargs:
|
||||
args, kwargs = model.example_arguments()
|
||||
if input_names is None:
|
||||
input_names = model.input_names()
|
||||
if output_names is None:
|
||||
output_names = model.output_names()
|
||||
if dynamic_shapes is None:
|
||||
dynamic_shapes = model.dynamic_shapes()
|
||||
|
||||
if isinstance(model, torch.export.ExportedProgram):
|
||||
# We know the model is already exported program, so the args, kwargs, and dynamic_shapes
|
||||
# are not used
|
||||
dynamic_shapes = dynamic_shapes or {}
|
||||
else:
|
||||
args, kwargs = _get_torch_export_args(args, kwargs)
|
||||
if dynamic_shapes is None and dynamic_axes is not None:
|
||||
warnings.warn(
|
||||
"# 'dynamic_axes' is not recommended when dynamo=True, "
|
||||
"and may lead to 'torch._dynamo.exc.UserError: Constraints violated.' "
|
||||
"Supply the 'dynamic_shapes' argument instead if export is unsuccessful.",
|
||||
UserWarning,
|
||||
stacklevel=3,
|
||||
)
|
||||
try:
|
||||
dynamic_shapes, args, kwargs = (
|
||||
_dynamic_shapes.from_dynamic_axes_to_dynamic_shapes(
|
||||
model,
|
||||
args,
|
||||
kwargs,
|
||||
dynamic_axes=dynamic_axes,
|
||||
input_names=input_names,
|
||||
output_names=set(output_names or ()),
|
||||
)
|
||||
)
|
||||
except Exception as e:
|
||||
raise RuntimeError(
|
||||
"# Failed to convert 'dynamic_axes' to 'dynamic_shapes'. "
|
||||
"Please provide 'dynamic_shapes' directly. "
|
||||
"Refer to the documentation for 'torch.export.export' for more information on dynamic shapes."
|
||||
) from e
|
||||
|
||||
dynamic_shapes_with_export_dim, need_axis_mapping = (
|
||||
_dynamic_shapes.convert_str_to_export_dim(dynamic_shapes)
|
||||
)
|
||||
|
||||
if opset_version < _constants.TORCHLIB_OPSET:
|
||||
logger.warning(
|
||||
"Setting ONNX exporter to use operator set version %s because "
|
||||
"the requested opset_version %s is a lower version than we have implementations for. "
|
||||
"Automatic version conversion will be performed, which may not be successful "
|
||||
"at converting to the requested version. If version conversion is unsuccessful, "
|
||||
"the opset version of the exported model will be kept at %s. "
|
||||
"Please consider setting opset_version >=%s to leverage latest ONNX features",
|
||||
_constants.TORCHLIB_OPSET,
|
||||
opset_version,
|
||||
_constants.TORCHLIB_OPSET,
|
||||
_constants.TORCHLIB_OPSET,
|
||||
)
|
||||
registry_opset_version = _constants.TORCHLIB_OPSET
|
||||
else:
|
||||
registry_opset_version = opset_version
|
||||
|
||||
registry = _registration.ONNXRegistry().from_torchlib(
|
||||
opset_version=registry_opset_version
|
||||
)
|
||||
if custom_translation_table is not None:
|
||||
for torch_op, onnx_op in custom_translation_table.items():
|
||||
# TODO(justinchuby): Support complex inputs with annotations
|
||||
if isinstance(onnx_op, Sequence):
|
||||
raise TypeError(
|
||||
"The value in custom_translation_table should be a single callable, not a sequence"
|
||||
)
|
||||
registry.register_op(torch_op, onnx_op, is_complex=False)
|
||||
|
||||
onnx_program = _core.export(
|
||||
model,
|
||||
args,
|
||||
kwargs,
|
||||
registry=registry,
|
||||
dynamic_shapes=dynamic_shapes_with_export_dim,
|
||||
input_names=input_names,
|
||||
output_names=output_names,
|
||||
profile=profile,
|
||||
report=report,
|
||||
verify=verify,
|
||||
dump_exported_program=dump_exported_program,
|
||||
artifacts_dir=artifacts_dir,
|
||||
verbose=verbose,
|
||||
optimize=optimize,
|
||||
opset_version=opset_version,
|
||||
)
|
||||
|
||||
if need_axis_mapping and dynamic_shapes is not None:
|
||||
onnx_program._rename_dynamic_axes(dynamic_shapes)
|
||||
|
||||
if f is not None:
|
||||
if isinstance(f, io.BytesIO):
|
||||
# For legacy export compatibility, we allow f to be a BytesIO object.
|
||||
# This is not explicitly supported but we may need to maintain the
|
||||
# behavior indefinitely.
|
||||
warnings.warn(
|
||||
"Saving ONNX model to a BytesIO object is deprecated. "
|
||||
"Please use a file path instead.",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
onnx.save(onnx_program.model_proto, f)
|
||||
else:
|
||||
onnx_program.save(
|
||||
f,
|
||||
include_initializers=export_params,
|
||||
keep_initializers_as_inputs=keep_initializers_as_inputs,
|
||||
external_data=external_data,
|
||||
)
|
||||
|
||||
return onnx_program
|
||||
@@ -0,0 +1,7 @@
|
||||
# ir_version used for the ONNX file. See https://github.com/onnx/onnx/blob/main/docs/IR.md#onnx-versioning
|
||||
ONNX_IR_VERSION = 10
|
||||
# The opset version torchlib is implemented with. Update this number when updating torchlib
|
||||
TORCHLIB_OPSET = 18
|
||||
TORCHLIB_DOMAIN = "pkg.torch.onnx"
|
||||
# Domain used for functions translated from subgraphs
|
||||
LOCAL_FUNCTION_DOMAIN = "pkg.torch.__subgraph__"
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,77 @@
|
||||
# mypy: allow-untyped-defs
|
||||
from __future__ import annotations
|
||||
|
||||
import itertools
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import torch
|
||||
import torch._ops
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Callable
|
||||
|
||||
from torch.onnx._internal.exporter import _registration
|
||||
|
||||
|
||||
def get_onnx_implemented_overloads(
|
||||
registry: _registration.ONNXRegistry,
|
||||
) -> list[_registration.TorchOp]:
|
||||
"""
|
||||
Creates a set of OperatorBase and Callable objects that represent ONNX-supported PyTorch operations.
|
||||
|
||||
Args:
|
||||
registry: The ONNX registry for PyTorch.
|
||||
|
||||
Returns:
|
||||
A collection of OperatorBase and Callable objects representing ONNX-supported PyTorch operations.
|
||||
"""
|
||||
registered_ops: list[_registration.TorchOp] = []
|
||||
for onnx_decomp_meta in registry.functions.values():
|
||||
if len(onnx_decomp_meta) == 0:
|
||||
raise AssertionError("onnx_decomp_meta must not be empty")
|
||||
# Different OnnxDecompMeta for the same TorchOp should
|
||||
# have the same fx_target.
|
||||
fx_target = onnx_decomp_meta[0].fx_target
|
||||
registered_ops.append(fx_target)
|
||||
return registered_ops
|
||||
|
||||
|
||||
def create_onnx_friendly_decomposition_table(
|
||||
onnx_registered_ops: set[_registration.TorchOp],
|
||||
) -> dict[_registration.TorchOp, Callable]:
|
||||
"""
|
||||
This function creates a dictionary of op overloads and their decomposition functions
|
||||
for ops that do not have ONNX symbolic functions. If an op already has an ONNX symbolic function,
|
||||
its decomposition function is excluded from the table. The decomposition table is a subset of PyTorch's
|
||||
built-in aten-to-aten decomposition.
|
||||
|
||||
Args:
|
||||
onnx_registered_ops: All ops that have an ONNX decomposition implemented.
|
||||
|
||||
Returns:
|
||||
Dict[torch._ops.OperatorBase, Callable]: A dictionary that maps op overloads to their corresponding
|
||||
decomposition functions.
|
||||
"""
|
||||
decomposition_table: dict[_registration.TorchOp, Callable] = {}
|
||||
|
||||
for op_overload, decomp_fn in itertools.chain(
|
||||
torch.export.default_decompositions().items(), # type: ignore[attr-defined]
|
||||
torch._decomp.decomposition_table.items(), # type: ignore[attr-defined]
|
||||
):
|
||||
# Skip decomposition for op_overload as long as that op_overload has a corresponding ONNX
|
||||
# symbolic function.
|
||||
# NOTE: Do not skip torch._refs decomps. They are fine because otherwise the model is
|
||||
# not exportable anyways.
|
||||
if op_overload in onnx_registered_ops:
|
||||
continue
|
||||
# If it is HOP, we filter those out as well.
|
||||
if not hasattr(op_overload, "_schema"):
|
||||
continue
|
||||
# NOTE: torch._decomp.decomposition_table covers more ops
|
||||
# than torch.export.default_decompositions, but the latter is
|
||||
# more critical to torch.onnx.export.
|
||||
if op_overload in decomposition_table:
|
||||
continue
|
||||
decomposition_table[op_overload] = decomp_fn
|
||||
return decomposition_table
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
# mypy: allow-untyped-defs
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import torch
|
||||
import torch.fx
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Callable
|
||||
|
||||
from torch.onnx._internal.exporter import _registration
|
||||
|
||||
|
||||
def _arg_has_complex_dtype(arg) -> bool:
|
||||
"""Check if the node has complex dtype recursively."""
|
||||
if (
|
||||
isinstance(arg, torch.fx.Node)
|
||||
and "val" in arg.meta
|
||||
and isinstance(arg.meta["val"], torch.Tensor)
|
||||
and torch.is_complex(arg.meta["val"])
|
||||
):
|
||||
return True
|
||||
elif isinstance(arg, list):
|
||||
return any(_arg_has_complex_dtype(item) for item in arg)
|
||||
return False
|
||||
|
||||
|
||||
def dispatch(
|
||||
node: torch.fx.Node, registry: _registration.ONNXRegistry
|
||||
) -> tuple[Callable | None, str]:
|
||||
"""Dispatch a node to an ONNX function based on the node's target and the ONNX registry.
|
||||
|
||||
Args:
|
||||
node: The node to dispatch.
|
||||
registry: The ONNX registry to use for dispatching.
|
||||
|
||||
Returns:
|
||||
A tuple containing the matched ONNX function and a string describing the reason for failure or success.
|
||||
"""
|
||||
decomp_metas = registry.get_decomps(node.target) # type: ignore[arg-type]
|
||||
# Determine if the node has complex inputs.
|
||||
is_complex = any(_arg_has_complex_dtype(arg) for arg in node.args) or any(
|
||||
_arg_has_complex_dtype(arg) for arg in node.kwargs.values()
|
||||
)
|
||||
if is_complex:
|
||||
decomp_metas = [decomp for decomp in decomp_metas if decomp.is_complex]
|
||||
if not decomp_metas:
|
||||
return None, "No decompositions registered for the complex-valued input"
|
||||
else:
|
||||
decomp_metas = [decomp for decomp in decomp_metas if not decomp.is_complex]
|
||||
if not decomp_metas:
|
||||
return None, "No decompositions registered for the real-valued input"
|
||||
|
||||
# NOTE: Complex overload type matching logic has been removed to keep this simple
|
||||
# There should no longer be overloads (for the same opset version) in torchlib anymore
|
||||
return (decomp_metas[0].onnx_function, "The first implementation is used")
|
||||
+343
@@ -0,0 +1,343 @@
|
||||
"""Compatibility functions for the torch.onnx.export API."""
|
||||
|
||||
# mypy: allow-untyped-defs
|
||||
from __future__ import annotations
|
||||
|
||||
import inspect
|
||||
import warnings
|
||||
from typing import Any, TYPE_CHECKING
|
||||
|
||||
import torch
|
||||
from torch.export.dynamic_shapes import _DimHint, Dim
|
||||
from torch.onnx._internal._lazy_import import onnx_ir as ir
|
||||
from torch.utils import _pytree
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Sequence
|
||||
|
||||
|
||||
def from_dynamic_axes_to_dynamic_shapes(
|
||||
model,
|
||||
args: tuple[Any, ...],
|
||||
kwargs: dict[str, Any] | None,
|
||||
*,
|
||||
dynamic_axes=None,
|
||||
output_names: set[str],
|
||||
input_names: Sequence[str] | None = None,
|
||||
) -> tuple[dict[str, Any | None] | None, tuple[Any, ...], dict[str, Any] | None]:
|
||||
"""
|
||||
Converts dynamic_axes into dynamic_shapes by wrapping the axis names with ``torch.export.Dim.DYNAMIC``.
|
||||
|
||||
dynamic_axes examples:
|
||||
(1) dynamic_axes = {"x": {0: "my_custom_axis_name_1"}, "y": {1: "my_custom_axis_name_2"}}
|
||||
(2) dynamic_axes = {"x": [0], "y": [1]}
|
||||
|
||||
these will be converted to dynamic_shapes respectively:
|
||||
(1) dynamic_shapes = {"x": {0: Dim.DYNAMIC}, "y": {1: Dim.DYNAMIC}}
|
||||
(2) dynamic_shapes = {"x": {0: Dim.DYNAMIC}, "y": {1: Dim.DYNAMIC}}
|
||||
|
||||
Detail on Dim.DYNAMIC: `#133620 <https://github.com/pytorch/pytorch/pull/133620>`_
|
||||
"""
|
||||
|
||||
warnings.warn(
|
||||
"from_dynamic_axes_to_dynamic_shapes is deprecated and will be removed in a future release. "
|
||||
"This function converts 'dynamic_axes' format (including custom axis names) to 'dynamic_shapes' format. "
|
||||
"Instead of relying on this conversion, provide 'dynamic_shapes' directly with custom names.",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
|
||||
# https://github.com/pytorch/pytorch/pull/128371
|
||||
# 1. The function does not need to provide dynamic_shapes to torch.export.export
|
||||
if dynamic_axes is None:
|
||||
return None, args, kwargs
|
||||
|
||||
if input_names is None:
|
||||
input_names = []
|
||||
|
||||
if kwargs is None:
|
||||
kwargs = {}
|
||||
|
||||
dynamic_shapes: dict[str, Any | None] = {}
|
||||
for input_name, axes in dynamic_axes.items():
|
||||
# NOTE: torch.export.Dim.DYNAMIC does its best to infer the min and max values
|
||||
# from the model, but it's not guaranteed to be dynamic.
|
||||
if input_name in output_names:
|
||||
# output names are not needed for dynamic_shapes
|
||||
continue
|
||||
if isinstance(axes, dict):
|
||||
if any(not isinstance(k, int) for k in axes):
|
||||
raise ValueError(
|
||||
"The axis in dynamic_axes must be in the form of: dict[int, str] or list[int]."
|
||||
)
|
||||
# str will be converted to Dim.DYNAMIC in convert_str_to_export_dim
|
||||
dynamic_shapes[input_name] = axes
|
||||
elif isinstance(axes, list):
|
||||
if any(not isinstance(k, int) for k in axes):
|
||||
raise ValueError(
|
||||
"The axis in dynamic_axes must be in the form of: dict[int, str] or list[int]."
|
||||
)
|
||||
dynamic_shapes[input_name] = dict.fromkeys(axes, torch.export.Dim.DYNAMIC)
|
||||
elif axes is None:
|
||||
dynamic_shapes[input_name] = None
|
||||
else:
|
||||
raise ValueError(
|
||||
"Unsupported dynamic_axes format. Please provide a dict or a list."
|
||||
)
|
||||
|
||||
for input_name in input_names:
|
||||
if input_name not in dynamic_shapes:
|
||||
dynamic_shapes[input_name] = None
|
||||
|
||||
# Order the inputs according to the signature of the model
|
||||
sig = _signature(model)
|
||||
inputs = []
|
||||
for idx, param_name in enumerate(sig.parameters):
|
||||
if idx < len(args):
|
||||
inputs.append(args[idx])
|
||||
elif param_name in kwargs:
|
||||
inputs.append(kwargs[param_name])
|
||||
|
||||
# We need tree structure to represent dynamic_shapes
|
||||
dynamic_shapes = _unflatten_dynamic_shapes_with_inputs_tree(inputs, dynamic_shapes)
|
||||
|
||||
# Since the dynamic_shapes are now in the order of the model parameters,
|
||||
# we need to convert args and kwargs to the order of the model parameters.
|
||||
return dynamic_shapes, tuple(inputs), {}
|
||||
|
||||
|
||||
def from_dynamic_shapes_to_dynamic_axes(
|
||||
dynamic_shapes: dict[str, Any] | tuple[Any, ...] | list[Any],
|
||||
input_names: Sequence[str],
|
||||
exception: Exception,
|
||||
) -> dict[str, Any] | None:
|
||||
"""
|
||||
Converts dynamic_shapes into dynamic_axes by removing torch.export.Dim wrapping
|
||||
and converting to list or dict form based on whether dimension names are present.
|
||||
|
||||
dynamic_shapes examples:
|
||||
(1) dynamic_shapes = {"x": {0: Dim("my_custom_axis_name_1")}, "y": {1: Dim("my_custom_axis_name_2")}}
|
||||
(2) dynamic_shapes = ({0: Dim("my_custom_axis_name_1"}, {1: Dim("my_custom_axis_name_2")})
|
||||
|
||||
these will be converted to dynamic_axes respectively:
|
||||
(1) dynamic_axes = {"x": [0], "y": [1]}
|
||||
(2) dynamic_axes = {"x": [0], "y": [1]}
|
||||
|
||||
NOTE: If the model input is nested, so is the dynamic_shapes, we need to flatten the dynamic_shapes,
|
||||
and then assign the axes to the input names in the order they are provided.
|
||||
|
||||
NOTE: input_names are used to assign the axes to the correct input names. If the input names are not
|
||||
provided, or less than the dynamic inputs/axes, it raises an error.
|
||||
"""
|
||||
|
||||
flat_dynamic_shapes, _ = _flatten_dynamic_shapes_to_axes(dynamic_shapes)
|
||||
|
||||
if len(input_names) < len(flat_dynamic_shapes):
|
||||
raise ValueError(
|
||||
"To construct dynamic_axes from dynamic_shapes, "
|
||||
f"number of input names ({len(input_names)}) should be greater than or equal to "
|
||||
f"the number of graph inputs(flat) ({len(flat_dynamic_shapes)})"
|
||||
) from exception
|
||||
|
||||
dynamic_axes: dict[str, list[int]] = {}
|
||||
# input names are assigned in order
|
||||
for input_name, axes in zip(input_names, flat_dynamic_shapes):
|
||||
if axes is None:
|
||||
continue
|
||||
|
||||
converted_axes: list[int] = []
|
||||
if isinstance(axes, dict):
|
||||
for axis, dim in axes.items():
|
||||
if dim is None:
|
||||
continue
|
||||
converted_axes.append(axis)
|
||||
dynamic_axes[input_name] = converted_axes
|
||||
elif isinstance(axes, (list, tuple)):
|
||||
for idx, dim in enumerate(axes):
|
||||
if dim is None:
|
||||
continue
|
||||
converted_axes.append(idx)
|
||||
dynamic_axes[input_name] = converted_axes
|
||||
return dynamic_axes
|
||||
|
||||
|
||||
def _any_str_or_dim_in_dynamic_shapes(
|
||||
dynamic_shapes: dict[str, Any] | tuple[Any, ...] | list[Any],
|
||||
) -> bool:
|
||||
"""Check if there is any string or Dim in the dynamic_shapes."""
|
||||
flat_dynamic_shapes, _ = _flatten_dynamic_shapes_to_axes(dynamic_shapes)
|
||||
# This indicates the dynamic_shapes includes something we don't support in axes, and it's flattened
|
||||
# to itself. Otherwise, flat_dynamic_shapes should be a list of dict/list/tuple (or None).
|
||||
if any(
|
||||
not isinstance(axes, (dict, list, tuple)) and axes is not None
|
||||
for axes in flat_dynamic_shapes
|
||||
):
|
||||
return False
|
||||
# both str and Dim can provide custom names
|
||||
for axes in flat_dynamic_shapes:
|
||||
if isinstance(axes, dict):
|
||||
for dim in axes.values():
|
||||
if isinstance(dim, (str, Dim)):
|
||||
return True
|
||||
elif isinstance(axes, (list, tuple)):
|
||||
for dim in axes:
|
||||
if isinstance(dim, (str, Dim)):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def convert_str_to_export_dim(
|
||||
dynamic_shapes: dict[str, Any] | tuple[Any, ...] | list[Any] | None,
|
||||
) -> tuple[dict[str, Any] | tuple[Any, ...] | list[Any] | None, bool]:
|
||||
# 1. If there is no string in dynamic_shapes, we do not touch dynamic_shapes
|
||||
if dynamic_shapes is None or not _any_str_or_dim_in_dynamic_shapes(dynamic_shapes):
|
||||
return dynamic_shapes, False
|
||||
# 2. Convert "name" to Dim.DYNAMIC with flattening and identify if there is any string
|
||||
# to be replaced with Dim.DYNAMIC, and then unflatten it back to the original structure.
|
||||
# for example: {"y": {0: "dim_0"}, "x": {1: "dim_1"}}
|
||||
# to {"y": {0: Dim.DYNAMIC}, "x": {1: Dim.DYNAMIC}}
|
||||
dynamic_shapes_with_export_dim: list[
|
||||
list[Dim | _DimHint | None] | dict[int, Dim | _DimHint | None] | None
|
||||
] = []
|
||||
flat_dynamic_shapes, tree_structure = _flatten_dynamic_shapes_to_axes(
|
||||
dynamic_shapes
|
||||
)
|
||||
for axes in flat_dynamic_shapes:
|
||||
if axes is None:
|
||||
dynamic_shapes_with_export_dim.append(None)
|
||||
elif isinstance(axes, dict):
|
||||
converted_axes_dict: dict[int, Dim | _DimHint | None] = {}
|
||||
for axis, dim in axes.items():
|
||||
if isinstance(dim, str):
|
||||
converted_axes_dict[axis] = torch.export.Dim.DYNAMIC
|
||||
else:
|
||||
converted_axes_dict[axis] = dim
|
||||
dynamic_shapes_with_export_dim.append(converted_axes_dict)
|
||||
elif isinstance(axes, (list, tuple)):
|
||||
converted_axes_list: list[Dim | _DimHint | None] = []
|
||||
for dim in axes:
|
||||
if isinstance(dim, str):
|
||||
converted_axes_list.append(torch.export.Dim.DYNAMIC)
|
||||
else:
|
||||
converted_axes_list.append(dim)
|
||||
dynamic_shapes_with_export_dim.append(converted_axes_list)
|
||||
|
||||
dynamic_shapes_with_export_dim = _pytree.tree_unflatten(
|
||||
dynamic_shapes_with_export_dim, tree_structure
|
||||
)
|
||||
return (
|
||||
dynamic_shapes_with_export_dim,
|
||||
True,
|
||||
)
|
||||
|
||||
|
||||
def create_rename_mapping(
|
||||
inputs, dynamic_shapes: dict[str, Any] | tuple[Any, ...] | list[Any]
|
||||
) -> dict[str, str]:
|
||||
"""Create a mapping from old names to new names for dynamic axes."""
|
||||
|
||||
# NOTE: There's no need to handle cases where kwargs are out of order with the model signature,
|
||||
# as torch.export.export supports dynamism only when kwargs and dynamic_shapes are provided in order.
|
||||
# Reference: https://github.com/pytorch/pytorch/blob/49082f9dba3b79a344cb03652972ddbe7c3729cc/torch/export/_trace.py#L2034
|
||||
|
||||
flat_dynamic_shapes, _ = _flatten_dynamic_shapes_to_axes(dynamic_shapes)
|
||||
if len(inputs) != len(flat_dynamic_shapes):
|
||||
warnings.warn(
|
||||
"# ONNX model has different number of inputs than the flatten dynamic_shapes. "
|
||||
"The dynamic axes will not be renamed.",
|
||||
UserWarning,
|
||||
stacklevel=3,
|
||||
)
|
||||
return {}
|
||||
rename_mapping: dict[str, str] = {}
|
||||
# NOTE: We assume that the flat_dynamic_shapes is in the same order as the inputs
|
||||
# When the axis is static, or it connects to _DimHint in dynamic shapes, we skip renaming
|
||||
for idx, axes in enumerate(flat_dynamic_shapes):
|
||||
input = inputs[idx]
|
||||
if isinstance(axes, dict):
|
||||
for dim, axis in axes.items():
|
||||
if not isinstance(input.shape[dim], ir.SymbolicDim):
|
||||
continue
|
||||
old_name = input.shape[dim].value
|
||||
if old_name is None:
|
||||
continue
|
||||
# _DimHint, int and None exists in dynamic shapes, we skip renaming
|
||||
if isinstance(axis, (_DimHint, int)) or axis is None:
|
||||
continue
|
||||
# NOTE: ExportedProgram could give the axes the same name if they share
|
||||
# the same shape constraints.
|
||||
custom_name = _get_custom_axis_name(axis)
|
||||
if input.shape[dim].value in rename_mapping:
|
||||
warnings.warn(
|
||||
f"# The axis name: {custom_name} will not be used, since it shares "
|
||||
f"the same shape constraints with another axis: {rename_mapping[input.shape[dim].value]}.",
|
||||
stacklevel=2,
|
||||
)
|
||||
continue
|
||||
rename_mapping[input.shape[dim].value] = custom_name
|
||||
elif isinstance(axes, (list, tuple)):
|
||||
for dim, axis in enumerate(axes):
|
||||
if not isinstance(input.shape[dim], ir.SymbolicDim):
|
||||
continue
|
||||
old_name = input.shape[dim].value
|
||||
if old_name is None:
|
||||
continue
|
||||
# _DimHint, int and None exists in dynamic shapes, we skip renaming
|
||||
if isinstance(axis, (_DimHint, int)) or axis is None:
|
||||
continue
|
||||
# NOTE: ExportedProgram could give the axes the same name if they share
|
||||
# the same shape constraints.
|
||||
custom_name = _get_custom_axis_name(axis)
|
||||
if input.shape[dim].value in rename_mapping:
|
||||
warnings.warn(
|
||||
f"# The axis name: {custom_name} will not be used, since it shares "
|
||||
f"the same shape constraints with another axis: {rename_mapping[input.shape[dim].value]}.",
|
||||
UserWarning,
|
||||
stacklevel=3,
|
||||
)
|
||||
continue
|
||||
rename_mapping[input.shape[dim].value] = _get_custom_axis_name(axis)
|
||||
return rename_mapping
|
||||
|
||||
|
||||
def _get_custom_axis_name(axis: Dim | str) -> str:
|
||||
"""Get the custom axis name from a torch.export.Dim."""
|
||||
if isinstance(axis, Dim):
|
||||
return axis.__name__
|
||||
return axis
|
||||
|
||||
|
||||
def _unflatten_dynamic_shapes_with_inputs_tree(
|
||||
inputs: list[Any],
|
||||
dynamic_shapes: dict[str, Any],
|
||||
) -> dict[str, Any | None]:
|
||||
_, tree_structure = _pytree.tree_flatten(inputs)
|
||||
return _pytree.tree_unflatten(dynamic_shapes.values(), tree_structure)
|
||||
|
||||
|
||||
def _flatten_dynamic_shapes_to_axes(
|
||||
dynamic_shapes: dict[str, Any | None] | tuple[Any, ...] | list[Any],
|
||||
) -> tuple[list[Any], _pytree.TreeSpec]:
|
||||
# If it's a dict/list/tuple with torch.export.Dim, we consider it's an axis to dim mapping
|
||||
def is_axes(x) -> bool:
|
||||
return (
|
||||
isinstance(x, dict)
|
||||
and all(
|
||||
isinstance(k, int)
|
||||
and (v is None or isinstance(v, (Dim, _DimHint, str, int)))
|
||||
for k, v in x.items()
|
||||
)
|
||||
) or (
|
||||
isinstance(x, (list, tuple))
|
||||
and all(v is None or isinstance(v, (Dim, _DimHint, str, int)) for v in x)
|
||||
)
|
||||
|
||||
return _pytree.tree_flatten(dynamic_shapes, is_leaf=is_axes)
|
||||
|
||||
|
||||
def _signature(model) -> inspect.Signature:
|
||||
should_be_callable = getattr(model, "forward", model)
|
||||
if callable(should_be_callable):
|
||||
return inspect.signature(should_be_callable)
|
||||
raise ValueError("model has no forward method and is not callable")
|
||||
@@ -0,0 +1,21 @@
|
||||
"""Error classes for the ONNX exporter."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import torch.onnx.errors
|
||||
|
||||
|
||||
class TorchExportError(torch.onnx.errors.OnnxExporterError):
|
||||
"""Error during graph capturing using torch.export."""
|
||||
|
||||
|
||||
class ConversionError(torch.onnx.errors.OnnxExporterError):
|
||||
"""Error during ExportedProgram to ONNX conversion."""
|
||||
|
||||
|
||||
class DispatchError(ConversionError):
|
||||
"""Error during ONNX Function dispatching."""
|
||||
|
||||
|
||||
class GraphConstructionError(ConversionError):
|
||||
"""Error during ONNX graph construction."""
|
||||
+196
@@ -0,0 +1,196 @@
|
||||
"""Abstract interface for ONNX exportable modules."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import abc
|
||||
from typing import Any, TYPE_CHECKING
|
||||
|
||||
import torch
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Sequence
|
||||
|
||||
|
||||
class ExportableModule(torch.nn.Module, abc.ABC):
|
||||
"""Abstract interface for ONNX exportable modules.
|
||||
|
||||
Inherit from this class and implement the defined abstract methods
|
||||
to create a module that can be exported to ONNX format.
|
||||
|
||||
Example::
|
||||
|
||||
class Model(torch.nn.Module):
|
||||
def forward(self, x):
|
||||
return x * 2
|
||||
|
||||
|
||||
class MyExportableModule(torch.onnx.ExportableModule):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.model = Model()
|
||||
|
||||
def forward(self, x):
|
||||
return self.model(x)
|
||||
|
||||
def example_arguments(self):
|
||||
return (torch.randn(2, 3, 224, 224),), None
|
||||
|
||||
def input_names(self):
|
||||
return ("input",)
|
||||
|
||||
def output_names(self):
|
||||
return ("output",)
|
||||
|
||||
def dynamic_shapes(self):
|
||||
return ({0: "batch_size"},)
|
||||
|
||||
|
||||
exportable_module = MyExportableModule()
|
||||
onnx_program = exportable_module.to_onnx()
|
||||
# The model can also be supplied directly to torch.onnx.export
|
||||
onnx_program = torch.onnx.export(exportable_module)
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def example_arguments(self) -> tuple[tuple[Any], dict[str, Any] | None]:
|
||||
"""Return example arguments for the model's forward method.
|
||||
|
||||
This method must be implemented by subclasses to provide sample inputs
|
||||
that can be used for tracing, testing, and ONNX export. The returned
|
||||
arguments should be representative of the expected input shapes and types
|
||||
during inference.
|
||||
|
||||
Example::
|
||||
|
||||
def example_arguments(self):
|
||||
# For a model expecting a single tensor input
|
||||
return (torch.randn(1, 3, 224, 224),), None
|
||||
|
||||
|
||||
def example_arguments(self):
|
||||
# For a model with multiple inputs and keyword arguments
|
||||
return (torch.randn(1, 3, 224, 224), torch.randn(1, 512)), {
|
||||
"temperature": 1.0
|
||||
}
|
||||
|
||||
Returns:
|
||||
A tuple containing:
|
||||
|
||||
- A tuple of positional arguments to pass to the forward method
|
||||
- A dictionary of keyword arguments (or None if no kwargs are needed)
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def dynamic_shapes(self) -> Any:
|
||||
"""Return dynamic shape specifications for the model's inputs.
|
||||
|
||||
Override this method to specify which dimensions of the input tensors
|
||||
should be treated as dynamic during ONNX export. This allows the exported
|
||||
model to accept inputs with varying sizes along the specified dimensions.
|
||||
|
||||
Example::
|
||||
|
||||
def dynamic_shapes(self):
|
||||
# Specify batch dimension as dynamic for input named 'x'
|
||||
return {"x": {0: "batch_size"}}
|
||||
|
||||
|
||||
def dynamic_shapes(self):
|
||||
# Multiple dynamic dimensions
|
||||
return {
|
||||
"input": {0: "batch_size", 2: "height", 3: "width"},
|
||||
"mask": {0: "batch_size"},
|
||||
}
|
||||
|
||||
Note:
|
||||
The default implementation returns None, indicating all dimensions are static.
|
||||
|
||||
Returns:
|
||||
Dynamic shape specification compatible with ``torch.export.export``.
|
||||
Return None if all input dimensions should be static. The format can be:
|
||||
|
||||
- A dictionary mapping input names to dimension specifications
|
||||
- A tuple/list of dimension specifications corresponding to inputs
|
||||
- Any format accepted by the ``dynamic_shapes`` parameter of ``torch.export.export``
|
||||
"""
|
||||
return None
|
||||
|
||||
def input_names(self) -> Sequence[str] | None:
|
||||
"""Return names for the model's input tensors.
|
||||
|
||||
Override this method to provide custom names for the input tensors in the
|
||||
exported ONNX model. These names will be used as identifiers in the ONNX
|
||||
graph and can be useful for debugging and model inspection.
|
||||
|
||||
Example::
|
||||
|
||||
def input_names(self):
|
||||
return ["image", "mask"]
|
||||
|
||||
|
||||
def input_names(self):
|
||||
# For a single input
|
||||
return ["input_tensor"]
|
||||
|
||||
Note:
|
||||
The default implementation returns None, which results in auto-generated names.
|
||||
|
||||
Returns:
|
||||
A sequence of strings representing input names, or None to use default names.
|
||||
The number of names should match the number of positional arguments in the
|
||||
forward method.
|
||||
"""
|
||||
return None
|
||||
|
||||
def output_names(self) -> Sequence[str] | None:
|
||||
"""Return names for the model's output tensors.
|
||||
|
||||
Override this method to provide custom names for the output tensors in the
|
||||
exported ONNX model. These names will be used as identifiers in the ONNX
|
||||
graph and can be useful for debugging and model inspection.
|
||||
|
||||
Example::
|
||||
|
||||
def output_names(self):
|
||||
return ["logits", "probabilities"]
|
||||
|
||||
|
||||
def output_names(self):
|
||||
# For a single output
|
||||
return ["prediction"]
|
||||
|
||||
Note:
|
||||
The default implementation returns None, which results in auto-generated names.
|
||||
|
||||
Returns:
|
||||
A sequence of strings representing output names, or None to use default names.
|
||||
The number of names should match the number of outputs from the forward method.
|
||||
For models returning multiple outputs, provide a name for each output.
|
||||
"""
|
||||
return None
|
||||
|
||||
def to_onnx(self, **kwargs: Any) -> torch.onnx.ONNXProgram:
|
||||
"""Export the module to ONNX format.
|
||||
|
||||
This method provides a convenient wrapper around ``torch.onnx.export`` that
|
||||
automatically uses the example arguments, dynamic shapes, and input/output
|
||||
names defined by the module. Additional export options can be specified via
|
||||
keyword arguments.
|
||||
|
||||
See Also: ``torch.onnx.export`` for complete documentation of export options.
|
||||
|
||||
Args:
|
||||
**kwargs: Additional keyword arguments to pass to ``torch.onnx.export``.
|
||||
Common options include:
|
||||
|
||||
- ``opset_version`` (int): The ONNX opset version to target
|
||||
- ``optimize`` (bool): Whether to apply optimizations to the exported model
|
||||
|
||||
Returns:
|
||||
An ONNXProgram object containing the exported model and metadata.
|
||||
"""
|
||||
result = torch.onnx.export(self, **kwargs)
|
||||
if result is None:
|
||||
raise AssertionError("result must be non-None")
|
||||
return result
|
||||
@@ -0,0 +1,32 @@
|
||||
"""Internal flags for ONNX export."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import functools
|
||||
from typing import TYPE_CHECKING, TypeVar
|
||||
from typing_extensions import ParamSpec
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Callable
|
||||
|
||||
|
||||
_is_onnx_exporting = False
|
||||
|
||||
# Use ParamSpec to preserve parameter types instead of erasing to Any
|
||||
_P = ParamSpec("_P")
|
||||
_R = TypeVar("_R")
|
||||
|
||||
|
||||
def set_onnx_exporting_flag(func: Callable[_P, _R]) -> Callable[_P, _R]:
|
||||
@functools.wraps(func)
|
||||
def wrapper(*args: _P.args, **kwargs: _P.kwargs) -> _R:
|
||||
global _is_onnx_exporting
|
||||
_is_onnx_exporting = True
|
||||
try:
|
||||
return func(*args, **kwargs)
|
||||
finally:
|
||||
# Ensure it resets even if an exception occurs
|
||||
_is_onnx_exporting = False
|
||||
|
||||
return wrapper
|
||||
@@ -0,0 +1,46 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import torch
|
||||
import torch.export
|
||||
import torch.fx
|
||||
from torch.onnx._internal.exporter import _decomp, _registration
|
||||
from torch.onnx._internal.fx import passes
|
||||
|
||||
|
||||
def decompose_with_registry(
|
||||
exported_program: torch.export.ExportedProgram, registry: _registration.ONNXRegistry
|
||||
) -> torch.export.ExportedProgram:
|
||||
"""Decompose the exported program with the given registry.
|
||||
|
||||
This function is needed so it shows clearly on the profiler results.
|
||||
"""
|
||||
onnx_registered_ops = set(_decomp.get_onnx_implemented_overloads(registry))
|
||||
decomp_table = _decomp.create_onnx_friendly_decomposition_table(onnx_registered_ops)
|
||||
return exported_program.run_decompositions(decomp_table)
|
||||
|
||||
|
||||
def insert_type_promotion_nodes(
|
||||
graph_module: torch.fx.GraphModule,
|
||||
) -> None:
|
||||
"""Inplace pass to insert explicit type promotion nodes, recursively through nested modules."""
|
||||
for module in graph_module.modules():
|
||||
if not isinstance(module, torch.fx.GraphModule):
|
||||
raise AssertionError(f"Expected GraphModule, got {type(module)}")
|
||||
passes.InsertTypePromotion(module).run()
|
||||
|
||||
|
||||
def remove_assertion_nodes(graph_module: torch.fx.GraphModule) -> torch.fx.GraphModule:
|
||||
"""Remove all assertion and check nodes from the FX graph"""
|
||||
aten_assertion_targets = {
|
||||
torch.ops.aten.sym_constrain_range_for_size.default,
|
||||
torch.ops.aten._assert_async.default,
|
||||
torch.ops.aten._assert_async.msg,
|
||||
torch.ops.aten._assert_scalar.default,
|
||||
torch.ops.aten._assert_tensor_metadata.default,
|
||||
}
|
||||
for gm in graph_module.modules():
|
||||
for node in gm.graph.nodes: # type: ignore[union-attr]
|
||||
if node.op == "call_function" and node.target in aten_assertion_targets:
|
||||
gm.graph.erase_node(node) # type: ignore[operator, union-attr]
|
||||
gm.recompile() # type: ignore[operator]
|
||||
return graph_module
|
||||
+1224
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,166 @@
|
||||
# mypy: allow-untyped-defs
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import re
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from torch.onnx._internal._lazy_import import onnx_ir as ir
|
||||
from torch.onnx._internal.exporter import _constants
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Sequence
|
||||
|
||||
|
||||
# The opset domain for ONNX operators
|
||||
_ONNX_DOMAIN = ""
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def rename_inputs(model: ir.Model, new_names: Sequence[str]) -> None:
|
||||
unique_names = frozenset(new_names)
|
||||
if len(unique_names) != len(new_names):
|
||||
seen = set()
|
||||
duplicates = []
|
||||
for name in new_names:
|
||||
if name in seen:
|
||||
duplicates.append(name)
|
||||
seen.add(name)
|
||||
raise ValueError(f"Input names cannot be duplicated: {duplicates}")
|
||||
|
||||
for input, new_name in zip(model.graph.inputs, new_names):
|
||||
input.metadata_props["pkg.torch.onnx.original_node_name"] = str(input.name)
|
||||
input.name = new_name
|
||||
|
||||
|
||||
def rename_outputs(model: ir.Model, new_names: Sequence[str]) -> None:
|
||||
unique_names = frozenset(new_names)
|
||||
if len(unique_names) != len(new_names):
|
||||
seen = set()
|
||||
duplicates = []
|
||||
for name in new_names:
|
||||
if name in seen:
|
||||
duplicates.append(name)
|
||||
seen.add(name)
|
||||
raise ValueError(f"Output names cannot be duplicated: {duplicates}")
|
||||
|
||||
for output, new_name in zip(model.graph.outputs, new_names):
|
||||
output.metadata_props["pkg.torch.onnx.original_node_name"] = str(output.name)
|
||||
output.name = new_name
|
||||
|
||||
|
||||
def _all_values(model: ir.Model):
|
||||
"""Yield all values in a model."""
|
||||
# Yield all values in the model
|
||||
yield from model.graph.inputs
|
||||
yield from model.graph.initializers.values()
|
||||
for node in ir.traversal.RecursiveGraphIterator(model.graph):
|
||||
yield from node.outputs
|
||||
# Yield all values in functions
|
||||
for function in model.functions.values():
|
||||
yield from function.inputs
|
||||
for node in ir.traversal.RecursiveGraphIterator(function):
|
||||
yield from node.outputs
|
||||
|
||||
|
||||
def _replace_names(shape_expr: str, rename_mapping: dict[str, str]) -> str:
|
||||
"""Replace all known names in a shape expression with new names."""
|
||||
for old_name, new_name in rename_mapping.items():
|
||||
shape_expr = re.sub(
|
||||
rf"(?<!\w){re.escape(old_name)}(?!\w)", new_name, shape_expr
|
||||
)
|
||||
return shape_expr
|
||||
|
||||
|
||||
def rename_axis(
|
||||
model: ir.Model, rename_mapping: dict[str | ir.SymbolicDim, str]
|
||||
) -> None:
|
||||
"""Rename dynamic axes in a model according to the specified dynamic_axes names."""
|
||||
|
||||
# Create a mapping from string to string for easier replacement
|
||||
string_mapping: dict[str, str] = {}
|
||||
for key, value in tuple(rename_mapping.items()):
|
||||
if isinstance(key, ir.SymbolicDim):
|
||||
if isinstance(key.value, str):
|
||||
string_mapping[key.value] = value
|
||||
else:
|
||||
raise ValueError(
|
||||
f"Invalid SymbolicDim value in rename_mapping: {key.value!r}. "
|
||||
"Expected str."
|
||||
)
|
||||
elif isinstance(key, str):
|
||||
string_mapping[key] = value
|
||||
else:
|
||||
raise ValueError(
|
||||
f"Invalid key type in rename_mapping: {type(key)}({key!r}). Expected "
|
||||
"str or ir.SymbolicDim."
|
||||
)
|
||||
|
||||
# NOTE: Mapping needs to be sorted by length because the shape expression
|
||||
# could have multiple ways to be expressed, for example,
|
||||
# {"s1": sequence_length, "s11": "past_sequence_length", "s1 + s11": "masked_sequence_length"}
|
||||
# We prefer the replacement starts from the longest match.
|
||||
sorted_rename_mapping = dict(
|
||||
sorted(string_mapping.items(), key=lambda item: len(item[0]), reverse=True)
|
||||
)
|
||||
for value in _all_values(model):
|
||||
if value.shape is None:
|
||||
continue
|
||||
new_shape = []
|
||||
changed = False
|
||||
for dim in value.shape:
|
||||
if not isinstance(dim, ir.SymbolicDim):
|
||||
new_shape.append(dim)
|
||||
continue
|
||||
dim_name = dim.value
|
||||
if dim_name in sorted_rename_mapping:
|
||||
new_shape.append(sorted_rename_mapping[dim_name])
|
||||
changed = True
|
||||
elif dim_name is not None:
|
||||
# For example: "2*s1", "s1+1", "s1-1", "s1*s2", "s1/s2"
|
||||
new_name = _replace_names(dim_name, sorted_rename_mapping)
|
||||
new_shape.append(new_name)
|
||||
if new_name != dim_name:
|
||||
changed = True
|
||||
else:
|
||||
new_shape.append(None)
|
||||
if changed:
|
||||
value.shape = ir.Shape(new_shape)
|
||||
|
||||
|
||||
def _maybe_set_opset_version(
|
||||
opset_imports: dict[str, int], domain: str, version: int | None
|
||||
) -> None:
|
||||
"""Set the opset version for the domain."""
|
||||
if domain in opset_imports and opset_imports[domain] != 1:
|
||||
# Already set
|
||||
return
|
||||
if domain == _ONNX_DOMAIN:
|
||||
opset_imports[domain] = _constants.TORCHLIB_OPSET
|
||||
return
|
||||
if version is None:
|
||||
# We don't know the opset version, so set it to 1
|
||||
# This is valid for the custom function domains like "pkg.torch.__subgraph__"
|
||||
opset_imports[domain] = 1
|
||||
return
|
||||
# Set the known opset version for the domain
|
||||
opset_imports[domain] = version
|
||||
|
||||
|
||||
def add_opset_imports(model: ir.Model) -> None:
|
||||
"""Collect all opsets used and add opset imports to the model and functions."""
|
||||
for node in ir.traversal.RecursiveGraphIterator(model.graph):
|
||||
domain = node.domain
|
||||
_maybe_set_opset_version(model.opset_imports, domain, node.version)
|
||||
|
||||
for function in model.functions.values():
|
||||
for node in ir.traversal.RecursiveGraphIterator(function):
|
||||
domain = node.domain
|
||||
_maybe_set_opset_version(function.opset_imports, domain, node.version)
|
||||
for domain, version in function.opset_imports.items():
|
||||
# Add all opsets used in the function to the model, because ONNX Runtime
|
||||
# does not handle adding the opset imports to the model after inlining during inference.
|
||||
# This should happen after all opsets are collected for the function from its nodes.
|
||||
_maybe_set_opset_version(model.opset_imports, domain, version)
|
||||
@@ -0,0 +1,65 @@
|
||||
"""Isolated calls to methods that may segfault."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import multiprocessing
|
||||
import os
|
||||
import warnings
|
||||
from typing import Any, TYPE_CHECKING, TypeVar
|
||||
from typing_extensions import ParamSpec, TypeVarTuple, Unpack
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Callable
|
||||
|
||||
|
||||
_P = ParamSpec("_P")
|
||||
_R = TypeVar("_R")
|
||||
_Ts = TypeVarTuple("_Ts")
|
||||
|
||||
_IS_WINDOWS = os.name == "nt"
|
||||
|
||||
|
||||
def _call_function_and_return_exception(
|
||||
func: Callable[[Unpack[_Ts]], _R], args: tuple[Unpack[_Ts]], kwargs: dict[str, Any]
|
||||
) -> _R | Exception:
|
||||
"""Call function and return a exception if there is one."""
|
||||
|
||||
try:
|
||||
return func(*args, **kwargs)
|
||||
except Exception as e:
|
||||
return e
|
||||
|
||||
|
||||
def safe_call(func: Callable[_P, _R], *args: _P.args, **kwargs: _P.kwargs) -> _R:
|
||||
"""Call a function in a separate process.
|
||||
|
||||
Args:
|
||||
func: The function to call.
|
||||
args: The positional arguments to pass to the function.
|
||||
kwargs: The keyword arguments to pass to the function.
|
||||
|
||||
Returns:
|
||||
The return value of the function.
|
||||
|
||||
Raises:
|
||||
Exception: If the function raised an exception.
|
||||
"""
|
||||
if _IS_WINDOWS:
|
||||
# On Windows, we cannot create a new process with fork.
|
||||
warnings.warn(
|
||||
f"A new process is not created for {func} on Windows.", stacklevel=1
|
||||
)
|
||||
return func(*args, **kwargs)
|
||||
|
||||
with multiprocessing.get_context("fork").Pool(1) as pool:
|
||||
# It is important to fork a process here to prevent the main logic from
|
||||
# running again when the user does not place it under a `if __name__ == "__main__":`
|
||||
# block.
|
||||
result = pool.apply_async(
|
||||
_call_function_and_return_exception, (func, args, kwargs)
|
||||
)
|
||||
result = result.get(timeout=5)
|
||||
if isinstance(result, Exception):
|
||||
raise result
|
||||
return result
|
||||
+521
@@ -0,0 +1,521 @@
|
||||
# mypy: allow-untyped-defs
|
||||
# mypy: disable-error-code="attr-defined,name-defined"
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
__all__ = ["ONNXProgram"]
|
||||
|
||||
import contextlib
|
||||
import copy
|
||||
import gc
|
||||
import logging
|
||||
import os
|
||||
import tempfile
|
||||
import textwrap
|
||||
import warnings
|
||||
from collections.abc import Callable, Sequence
|
||||
from typing import Any, TYPE_CHECKING
|
||||
|
||||
import torch
|
||||
from torch.onnx._internal._lazy_import import onnx, onnx_ir as ir, onnxscript_apis
|
||||
from torch.onnx._internal.exporter import _dynamic_shapes, _ir_passes
|
||||
from torch.utils import _pytree
|
||||
|
||||
|
||||
# NOTE: DO NOT import module from torch.onnx._internal to this module in the global scope
|
||||
# because ONNXProgram is exposed to the public API
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import numpy as np
|
||||
import onnxruntime as ort
|
||||
|
||||
_LARGE_MODEL_THRESHOLD = 1536 * 1024 * 1024 # 1536MB
|
||||
_NP_UNSUPPORTED_DTYPES_8BIT = frozenset(
|
||||
{
|
||||
torch.float8_e4m3fn,
|
||||
torch.float8_e4m3fnuz,
|
||||
torch.float8_e5m2,
|
||||
torch.float8_e5m2fnuz,
|
||||
}
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _ort_session_initializer(model: str | bytes) -> ort.InferenceSession:
|
||||
"""Initialize an ONNX Runtime inference session with the specified model."""
|
||||
import onnxruntime as ort
|
||||
|
||||
session_options = ort.SessionOptions()
|
||||
session_options.log_severity_level = 3 # 3: Error
|
||||
possible_providers = (
|
||||
"CUDAExecutionProvider",
|
||||
"CPUExecutionProvider",
|
||||
)
|
||||
available_providers = set(ort.get_available_providers())
|
||||
providers = [
|
||||
provider for provider in possible_providers if provider in available_providers
|
||||
]
|
||||
return ort.InferenceSession(
|
||||
model, providers=providers, sess_options=session_options
|
||||
)
|
||||
|
||||
|
||||
def _count_initializer_size(graph: ir.Graph) -> int:
|
||||
"""Count the total size of the initializers in bytes."""
|
||||
return sum(
|
||||
v.const_value.nbytes
|
||||
for v in graph.initializers.values()
|
||||
if v.const_value is not None
|
||||
)
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def _set_graph_outputs(
|
||||
graph: ir.Graph,
|
||||
outputs: list[ir.Value],
|
||||
):
|
||||
"""Temporarily set the outputs of the graph.
|
||||
|
||||
Args:
|
||||
graph: The graph to set the outputs for.
|
||||
outputs: The outputs to set.
|
||||
"""
|
||||
original_outputs = list(graph.outputs)
|
||||
graph.outputs.clear()
|
||||
graph.outputs.extend(outputs)
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
graph.outputs.clear()
|
||||
graph.outputs.extend(original_outputs)
|
||||
|
||||
|
||||
def _create_value_mapping(graph: ir.Graph) -> dict[str, ir.Value]:
|
||||
"""Return a dictionary mapping names to values in the graph.
|
||||
|
||||
The mapping does not include values from subgraphs.
|
||||
|
||||
Args:
|
||||
graph: The graph to extract the mapping from.
|
||||
|
||||
Returns:
|
||||
A dictionary mapping names to values.
|
||||
"""
|
||||
values: dict[str, ir.Value] = {}
|
||||
values.update(graph.initializers)
|
||||
# The names of the values can be None or "", which we need to exclude
|
||||
for input in graph.inputs:
|
||||
if not input.name:
|
||||
continue
|
||||
values[input.name] = input
|
||||
for node in graph:
|
||||
for value in node.outputs:
|
||||
if not value.name:
|
||||
continue
|
||||
values[value.name] = value
|
||||
return values
|
||||
|
||||
|
||||
def _to_numpy_array(input: torch.Tensor | int | float | str | bool) -> np.ndarray:
|
||||
if isinstance(input, (int, float, str, bool)):
|
||||
return ir.tensor(input).numpy()
|
||||
|
||||
from torch.onnx._internal.exporter import _core
|
||||
|
||||
return _core.TorchTensor(input).numpy()
|
||||
|
||||
|
||||
def _from_numpy_array(array: np.ndarray) -> torch.Tensor:
|
||||
"""Convert a NumPy array to a PyTorch tensor."""
|
||||
import ml_dtypes # type: ignore[import-not-found]
|
||||
import numpy as np
|
||||
|
||||
if array.dtype == ml_dtypes.bfloat16:
|
||||
return torch.from_numpy(array.view(np.uint16)).view(torch.bfloat16)
|
||||
if array.dtype == ml_dtypes.float8_e4m3fn:
|
||||
return torch.from_numpy(array.view(np.uint8)).view(torch.float8_e4m3fn)
|
||||
if array.dtype == ml_dtypes.float8_e4m3fnuz:
|
||||
return torch.from_numpy(array.view(np.uint8)).view(torch.float8_e4m3fnuz)
|
||||
if array.dtype == ml_dtypes.float8_e5m2:
|
||||
return torch.from_numpy(array.view(np.uint8)).view(torch.float8_e5m2)
|
||||
if array.dtype == ml_dtypes.float8_e5m2fnuz:
|
||||
return torch.from_numpy(array.view(np.uint8)).view(torch.float8_e5m2fnuz)
|
||||
return torch.from_numpy(array)
|
||||
|
||||
|
||||
def _to_ort_value(input: torch.Tensor | int | float | str | bool) -> ort.OrtValue:
|
||||
"""Convert a PyTorch tensor to an ONNX Runtime OrtValue."""
|
||||
import numpy as np
|
||||
import onnxruntime as ort
|
||||
|
||||
from torch.onnx._internal.exporter import _core
|
||||
|
||||
if isinstance(input, (int, float, str, bool)):
|
||||
# Convert scalar values to OrtValue
|
||||
dtype_mapping = {
|
||||
int: np.int64,
|
||||
float: np.float32,
|
||||
}
|
||||
# pyrefly: ignore [bad-argument-type, no-matching-overload]
|
||||
dtype = dtype_mapping.get(type(input))
|
||||
return ort.OrtValue.ortvalue_from_numpy(np.array(input, dtype=dtype))
|
||||
|
||||
if input.dtype == torch.bfloat16 or input.dtype in _NP_UNSUPPORTED_DTYPES_8BIT:
|
||||
if hasattr(ort.OrtValue, "ortvalue_from_numpy_with_onnx_type"):
|
||||
# This requires ONNX Runtime 1.21 or newer
|
||||
if input.dtype == torch.bfloat16:
|
||||
uint_type = torch.uint16
|
||||
else:
|
||||
uint_type = torch.uint8
|
||||
onnx_type = _core.torch_dtype_to_onnx_dtype(input.dtype)
|
||||
# Make tensor contiguous to ensure view() works
|
||||
input = input.contiguous()
|
||||
return ort.OrtValue.ortvalue_from_numpy_with_onnx_type(
|
||||
input.view(uint_type).numpy(force=True), onnx_element_type=onnx_type
|
||||
)
|
||||
raise RuntimeError(
|
||||
f"Failed to convert tensor of type '{input.dtype}' to OrtValue. "
|
||||
"Please ensure that ONNX Runtime is built with DLPack support or is the latest version"
|
||||
)
|
||||
# TODO(#151064): Use dlpack when ORT properly supports it
|
||||
return ort.OrtValue.ortvalue_from_numpy(input.numpy(force=True))
|
||||
|
||||
|
||||
def _from_ort_value(value: ort.OrtValue) -> torch.Tensor:
|
||||
if value.element_type() in (
|
||||
ir.DataType.BFLOAT16,
|
||||
ir.DataType.FLOAT8E4M3FN,
|
||||
ir.DataType.FLOAT8E4M3FNUZ,
|
||||
ir.DataType.FLOAT8E5M2,
|
||||
ir.DataType.FLOAT8E5M2FNUZ,
|
||||
):
|
||||
# This requires ONNX Runtime 1.21 or newer
|
||||
try:
|
||||
return torch.from_dlpack(value._get_c_value())
|
||||
except Exception as e:
|
||||
raise RuntimeError(
|
||||
"Failed to convert OrtValue to torch.Tensor. "
|
||||
"Please ensure that ONNX Runtime is built with DLPack support or is the latest version"
|
||||
) from e
|
||||
return torch.from_numpy(value.numpy())
|
||||
|
||||
|
||||
class ONNXProgram:
|
||||
"""A class to represent an ONNX program that is callable with torch tensors.
|
||||
|
||||
Attributes:
|
||||
model: The ONNX model as an ONNX IR model object.
|
||||
exported_program: The exported program that produced the ONNX model.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self, model: ir.Model, exported_program: torch.export.ExportedProgram | None
|
||||
) -> None:
|
||||
"""Initialize the ONNX program with the specified model and exported program.
|
||||
Args:
|
||||
model: The ONNX model.
|
||||
exported_program: The exported program that produced the ONNX model. Optional.
|
||||
"""
|
||||
self.model: ir.Model = model
|
||||
self.exported_program = exported_program
|
||||
self._inference_session: ort.InferenceSession | None = None
|
||||
self._tempdir: tempfile.TemporaryDirectory | None = None
|
||||
# Strategy used to capture the exported program
|
||||
self._capture_strategy: str | None = None
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"""\
|
||||
ONNXProgram(
|
||||
model=
|
||||
{textwrap.indent(str(self.model), " " * 8)}
|
||||
,
|
||||
exported_program=
|
||||
{textwrap.indent(str(self.exported_program), " " * 8)}
|
||||
)
|
||||
"""
|
||||
|
||||
def __call__(self, *args, **kwargs) -> Sequence[torch.Tensor]:
|
||||
"""Run the ONNX model with the same arguments you would provide to the GraphModule."""
|
||||
import onnxruntime as ort
|
||||
|
||||
flatten_args = _process_args(args, kwargs)
|
||||
|
||||
if self._inference_session is None:
|
||||
self.initialize_inference_session()
|
||||
|
||||
if self._inference_session is None:
|
||||
raise AssertionError("_inference_session must be non-None")
|
||||
|
||||
ort_input = {
|
||||
k.name: _to_ort_value(v)
|
||||
for k, v in zip(self.model.graph.inputs, flatten_args)
|
||||
}
|
||||
run_options = ort.RunOptions()
|
||||
run_options.log_severity_level = 3 # 3: Error
|
||||
logger.debug("Running the inference session with %s arguments.", len(ort_input))
|
||||
outputs = self._inference_session.run_with_ort_values(
|
||||
None, ort_input, run_options=run_options
|
||||
)
|
||||
logger.debug("Inference session run completed.")
|
||||
return tuple(_from_ort_value(output) for output in outputs)
|
||||
|
||||
def call_reference(self, *args, **kwargs) -> Sequence[torch.Tensor]:
|
||||
"""Run the ONNX model using the reference backend."""
|
||||
import onnx.reference
|
||||
|
||||
evaluator = onnx.reference.ReferenceEvaluator(self.model_proto)
|
||||
|
||||
flatten_args = _process_args(args, kwargs)
|
||||
ref_input = {
|
||||
k.name: _to_numpy_array(v)
|
||||
for k, v in zip(self.model.graph.inputs, flatten_args)
|
||||
}
|
||||
outputs = evaluator.run(None, ref_input) # type: ignore[arg-type]
|
||||
if not isinstance(outputs, Sequence):
|
||||
raise AssertionError(f"Expected Sequence, got {type(outputs)}")
|
||||
return tuple(_from_numpy_array(output) for output in outputs)
|
||||
|
||||
def compute_values(
|
||||
self, value_names: Sequence[str], args=(), kwargs=None
|
||||
) -> Sequence[torch.Tensor]:
|
||||
"""Compute the values of the specified names in the ONNX model.
|
||||
|
||||
This method is used to compute the values of the specified names in the ONNX model.
|
||||
The values are returned as a dictionary mapping names to tensors.
|
||||
|
||||
Args:
|
||||
value_names: The names of the values to compute.
|
||||
|
||||
Returns:
|
||||
A dictionary mapping names to tensors.
|
||||
"""
|
||||
if kwargs is None:
|
||||
kwargs = {}
|
||||
self.release()
|
||||
values = _create_value_mapping(self.model.graph)
|
||||
for name in value_names:
|
||||
if name not in values:
|
||||
raise ValueError(
|
||||
f"Value '{name}' not found in the model. "
|
||||
"Please provide a valid value name."
|
||||
)
|
||||
temporary_outputs = [values[name] for name in value_names]
|
||||
with _set_graph_outputs(self.model.graph, temporary_outputs):
|
||||
try:
|
||||
result = self(*args, **kwargs)
|
||||
finally:
|
||||
self.release()
|
||||
return result
|
||||
|
||||
@property
|
||||
def model_proto(self) -> onnx.ModelProto:
|
||||
"""Return the ONNX ``ModelProto`` object."""
|
||||
return ir.serde.serialize_model(self.model)
|
||||
|
||||
def optimize(self) -> None:
|
||||
"""Optimize the ONNX model.
|
||||
|
||||
This method optimizes the ONNX model by performing constant folding and
|
||||
eliminating redundancies in the graph. The optimization is done in-place.
|
||||
"""
|
||||
self.model = onnxscript_apis.optimize(self.model)
|
||||
|
||||
def save(
|
||||
self,
|
||||
destination: str | os.PathLike,
|
||||
*,
|
||||
include_initializers: bool = True,
|
||||
keep_initializers_as_inputs: bool = False,
|
||||
external_data: bool | None = None,
|
||||
) -> None:
|
||||
"""Save the ONNX model to the specified destination.
|
||||
|
||||
When ``external_data`` is ``True`` or the model is larger than 2GB,
|
||||
the weights are saved as external data in a separate file.
|
||||
|
||||
Initializer (model weights) serialization behaviors:
|
||||
|
||||
* ``include_initializers=True``, ``keep_initializers_as_inputs=False`` (default):
|
||||
The initializers are included in the saved model.
|
||||
* ``include_initializers=True``, ``keep_initializers_as_inputs=True``:
|
||||
The initializers are included in the saved model and kept as model inputs.
|
||||
Choose this option if you want the ability to override the model weights
|
||||
during inference.
|
||||
* ``include_initializers=False``, ``keep_initializers_as_inputs=False``:
|
||||
The initializers are not included in the saved model and are not listed
|
||||
as model inputs. Choose this option if you want to attach the initializers
|
||||
to the ONNX model in a separate, post-processing, step.
|
||||
* ``include_initializers=False``, ``keep_initializers_as_inputs=True``:
|
||||
The initializers are not included in the saved model but are listed as model
|
||||
inputs. Choose this option if you want to supply the initializers during
|
||||
inference and want to minimize the size of the saved model.
|
||||
|
||||
Args:
|
||||
destination: The path to save the ONNX model to.
|
||||
include_initializers: Whether to include the initializers in the saved model.
|
||||
keep_initializers_as_inputs: Whether to keep the initializers as inputs in the saved model.
|
||||
If `True`, the initializers are added as inputs to the model which means they can be overwritten.
|
||||
by providing the initializers as model inputs.
|
||||
external_data: Whether to save the weights as external data in a separate file.
|
||||
|
||||
Raises:
|
||||
TypeError: If ``external_data`` is ``True`` and ``destination`` is not a file path.
|
||||
"""
|
||||
original_initializers = copy.copy(self.model.graph.initializers)
|
||||
original_inputs = copy.copy(self.model.graph.inputs)
|
||||
|
||||
# Adjust the model based on options
|
||||
if not include_initializers:
|
||||
self.model.graph.initializers.clear()
|
||||
if keep_initializers_as_inputs:
|
||||
self.model.graph.inputs.extend(original_initializers.values()) # type: ignore[arg-type]
|
||||
|
||||
try:
|
||||
# Save the model to disk
|
||||
if (
|
||||
external_data
|
||||
or _count_initializer_size(self.model.graph) > _LARGE_MODEL_THRESHOLD
|
||||
):
|
||||
onnxscript_apis.save_model_with_external_data(self.model, destination)
|
||||
else:
|
||||
ir.save(self.model, destination)
|
||||
finally:
|
||||
# Revert the changes to the model
|
||||
if not include_initializers:
|
||||
self.model.graph.initializers.update(original_initializers)
|
||||
if keep_initializers_as_inputs:
|
||||
self.model.graph.inputs.clear()
|
||||
self.model.graph.inputs.extend(original_inputs)
|
||||
|
||||
def apply_weights(self, state_dict: dict[str, torch.Tensor]) -> None:
|
||||
"""Apply the weights from the specified state dict to the ONNX model.
|
||||
|
||||
Use this method to replace FakeTensors or other weights.
|
||||
|
||||
Args:
|
||||
state_dict: The state dict containing the weights to apply to the ONNX model.
|
||||
"""
|
||||
from torch.onnx._internal.exporter import _core
|
||||
|
||||
for name, tensor in state_dict.items():
|
||||
if name in self.model.graph.initializers:
|
||||
self.model.graph.initializers[name].const_value = _core.TorchTensor(
|
||||
tensor, name
|
||||
)
|
||||
else:
|
||||
warnings.warn(
|
||||
f"Weight '{name}' not found in the model. Skipped applying.",
|
||||
category=torch.onnx.errors.OnnxExporterWarning,
|
||||
stacklevel=1,
|
||||
)
|
||||
|
||||
def initialize_inference_session(
|
||||
self,
|
||||
initializer: Callable[
|
||||
[str | bytes], ort.InferenceSession
|
||||
] = _ort_session_initializer,
|
||||
) -> None:
|
||||
"""Initialize the ONNX Runtime inference session.
|
||||
|
||||
Args:
|
||||
initializer: The function to initialize the ONNX Runtime inference
|
||||
session with the specified model. By default, it uses the
|
||||
:func:`_ort_session_initializer` function.
|
||||
"""
|
||||
# TODO(justinchuby): Allow different inference options
|
||||
logger.debug("Initializing the inference session.")
|
||||
if (
|
||||
byte_size := _count_initializer_size(self.model.graph)
|
||||
) > _LARGE_MODEL_THRESHOLD:
|
||||
logger.debug("The model initializers is larger than 1.5GB (%s).", byte_size)
|
||||
# Save the model to a temporary file if too large
|
||||
self._tempdir = tempfile.TemporaryDirectory(ignore_cleanup_errors=True)
|
||||
model_path = os.path.join(self._tempdir.name, "model.onnx")
|
||||
self.save(model_path, external_data=True)
|
||||
model = model_path
|
||||
else:
|
||||
model = self.model_proto.SerializeToString() # type: ignore[assignment]
|
||||
|
||||
self._inference_session = initializer(model)
|
||||
logger.debug("Inference session initialized.")
|
||||
|
||||
def release(self) -> None:
|
||||
"""Release the inference session.
|
||||
|
||||
You may call this method to release the resources used by the inference session.
|
||||
"""
|
||||
# Release the inference session first so that the model file can be deleted
|
||||
if self._inference_session is not None:
|
||||
self._inference_session = None
|
||||
gc.collect()
|
||||
if self._tempdir is not None:
|
||||
self._tempdir.cleanup()
|
||||
self._tempdir = None
|
||||
|
||||
def rename_axes(self, rename_mapping: dict[str | ir.SymbolicDim, str]) -> None:
|
||||
"""Rename axes in a model according to the specified rename mapping.
|
||||
|
||||
Example::
|
||||
|
||||
batch = onnx_program.model.graph.inputs[0].shape[0]
|
||||
seq_len = onnx_program.model.graph.inputs[0].shape[2]
|
||||
rename_mapping = {
|
||||
batch: "batch",
|
||||
seq_len: "seq_len",
|
||||
}
|
||||
onnx_program.rename_axes(rename_mapping)
|
||||
|
||||
Args:
|
||||
rename_mapping: A dictionary mapping old axes to new axis names.
|
||||
Keys can be either:
|
||||
|
||||
* String axis names (e.g., "s1", "s2")
|
||||
* SymbolicDim objects obtained from the model
|
||||
(e.g., onnx_program.model.graph.inputs[0].shape[0])
|
||||
|
||||
Values must be strings representing the new axis names.
|
||||
"""
|
||||
_ir_passes.rename_axis(self.model, rename_mapping)
|
||||
|
||||
def _rename_dynamic_axes(
|
||||
self,
|
||||
dynamic_shapes: dict[str, Any] | tuple[Any, ...] | list[Any],
|
||||
) -> None:
|
||||
"""Rename dynamic axes in a model according to the specified dynamic_axes names."""
|
||||
rename_mapping = _dynamic_shapes.create_rename_mapping(
|
||||
self.model.graph.inputs, dynamic_shapes
|
||||
)
|
||||
_ir_passes.rename_axis(self.model, rename_mapping)
|
||||
|
||||
|
||||
def _process_args(args, kwargs) -> tuple[torch.Tensor, ...]:
|
||||
"""Process input arguments for the ONNX model."""
|
||||
args = _flatten_inputs(args, kwargs)
|
||||
args = _remove_none_from_inputs(args)
|
||||
args = _convert_complex_to_real_representation(args)
|
||||
return args
|
||||
|
||||
|
||||
def _flatten_inputs(model_args, model_kwargs):
|
||||
flattened_args, _ = _pytree.tree_flatten((model_args, model_kwargs))
|
||||
return flattened_args
|
||||
|
||||
|
||||
def _remove_none_from_inputs(model_args):
|
||||
return tuple(arg for arg in model_args if arg is not None)
|
||||
|
||||
|
||||
def _convert_complex_to_real_representation(model_args):
|
||||
"""Convert complex dtype tensors to real representation tensors.
|
||||
|
||||
ONNX does not support complex dtype tensors. Thus, we convert complex dtype tensors
|
||||
to real representation tensors (i.e., float dtype tensors with an extra dimension
|
||||
representing the real and imaginary parts of the complex number).
|
||||
"""
|
||||
return tuple(
|
||||
torch.view_as_real(arg.resolve_conj())
|
||||
if isinstance(arg, torch.Tensor) and arg.is_complex()
|
||||
else arg
|
||||
for arg in model_args
|
||||
)
|
||||
+303
@@ -0,0 +1,303 @@
|
||||
"""Module for handling ATen to ONNX functions registration.
|
||||
|
||||
https://github.com/pytorch/pytorch/blob/6aa5bb1a76dee8112f1a9e7c194c790b5cdc6462/torch/onnx/_internal/fx/registration.py
|
||||
"""
|
||||
|
||||
# NOTE: Why do we need a different registry than the one in torchlib?
|
||||
# The registry in torchlib is used to register functions that are already implemented in
|
||||
# torchlib, and is designed to be a static singleton. It does not take into account custom ops or different
|
||||
# opsets etc. The registry implemented for the exporter is designed to be modifiable at
|
||||
# export time by users, and is designed with dispatching in mind.
|
||||
|
||||
# mypy: allow-untyped-defs
|
||||
from __future__ import annotations
|
||||
|
||||
import dataclasses
|
||||
import importlib.util
|
||||
import logging
|
||||
import math
|
||||
import operator
|
||||
import types
|
||||
from collections.abc import Callable
|
||||
from typing import Literal, TypeAlias
|
||||
|
||||
import torch
|
||||
import torch._ops
|
||||
from torch.onnx._internal._lazy_import import onnx_ir as ir, onnxscript, onnxscript_apis
|
||||
from torch.onnx._internal.exporter import _constants, _schemas
|
||||
from torch.onnx._internal.exporter._torchlib import _torchlib_registry
|
||||
|
||||
|
||||
TorchOp: TypeAlias = torch._ops.OpOverload | types.BuiltinFunctionType | Callable
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class OnnxDecompMeta:
|
||||
"""A wrapper of onnx-script function with additional metadata.
|
||||
|
||||
onnx_function: The onnx-script function from torchlib.
|
||||
fx_target: The PyTorch node callable target.
|
||||
signature: The ONNX signature of the function. When None, the signature is inferred.
|
||||
is_custom: Whether the function is a custom function.
|
||||
is_complex: Whether the function is a function that handles complex valued inputs.
|
||||
opset_introduced:
|
||||
The ONNX opset version in which the function was introduced.
|
||||
Its specifies the minimum ONNX opset version required to use the function.
|
||||
device: The device the function is registered to. If None, it is registered to all devices.
|
||||
skip_signature_inference: Whether to skip signature inference for the function.
|
||||
"""
|
||||
|
||||
onnx_function: Callable
|
||||
fx_target: TorchOp
|
||||
signature: ir.schemas.OpSignature | None
|
||||
is_custom: bool = False
|
||||
is_complex: bool = False
|
||||
opset_introduced: int = 18
|
||||
device: Literal["cuda", "cpu"] | str | None = None # noqa: PYI051
|
||||
skip_signature_inference: bool = False
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
if self.signature is None and not self.skip_signature_inference:
|
||||
try:
|
||||
if isinstance(self.onnx_function, onnxscript.OnnxFunction):
|
||||
signature = _schemas.op_signature_from_function(
|
||||
self.onnx_function,
|
||||
self.onnx_function.function_ir.domain,
|
||||
self.onnx_function.name,
|
||||
since_version=self.onnx_function.opset.version,
|
||||
)
|
||||
else:
|
||||
signature = _schemas.op_signature_from_function(
|
||||
self.onnx_function, "__traced", self.onnx_function.__name__
|
||||
)
|
||||
except Exception as e:
|
||||
# Log an warning if the op is custom. Raise exception for builtin ops.
|
||||
if not self.is_custom:
|
||||
raise
|
||||
else:
|
||||
# When the function is targeting an HOP, for example, it will accept
|
||||
# functions as arguments and fail to generate an ONNX signature.
|
||||
# In this case we set signature to None and dispatch to this function always.
|
||||
logger.warning( # noqa: G200
|
||||
"Failed to infer the signature for function '%s' because '%s'"
|
||||
"All nodes targeting `%s` will be dispatched to this function",
|
||||
self.onnx_function,
|
||||
e,
|
||||
self.fx_target,
|
||||
)
|
||||
else:
|
||||
self.signature = signature
|
||||
self.onnx_function._pt_onnx_signature = signature # type: ignore[attr-defined]
|
||||
|
||||
|
||||
def _get_overload(qualified_name: str) -> torch._ops.OpOverload | None:
|
||||
"""Obtain the torch op from <namespace>::<op_name>[.<overload>]"""
|
||||
# TODO(justinchuby): Handle arbitrary custom ops
|
||||
namespace, opname_overload = qualified_name.split("::")
|
||||
op_name, *maybe_overload = opname_overload.split(".", 1)
|
||||
if namespace == "_operator":
|
||||
# Builtin functions
|
||||
return getattr(operator, op_name)
|
||||
if namespace == "math":
|
||||
return getattr(math, op_name)
|
||||
if namespace == "torchvision":
|
||||
if importlib.util.find_spec("torchvision") is None:
|
||||
logger.warning("torchvision is not installed. Skipping %s", qualified_name)
|
||||
return None
|
||||
try:
|
||||
op_packet = getattr(getattr(torch.ops, namespace), op_name)
|
||||
if maybe_overload:
|
||||
overload = maybe_overload[0]
|
||||
elif "default" in op_packet._overload_names or "" in op_packet._overload_names:
|
||||
# Has a default overload
|
||||
overload = "default"
|
||||
else:
|
||||
logger.warning(
|
||||
"'%s' does not have a 'default' overload. This could be an error in specifying the op name. Ignoring.",
|
||||
qualified_name,
|
||||
stacklevel=1,
|
||||
)
|
||||
return None
|
||||
|
||||
return getattr(op_packet, overload) # type: ignore[call-overload]
|
||||
except AttributeError:
|
||||
if qualified_name.endswith("getitem"):
|
||||
# This is a special case where we registered the function incorrectly,
|
||||
# but for BC reasons (pt<=2.4) we need to keep it.
|
||||
return None
|
||||
logger.info("'%s' is not found in this version of PyTorch.", qualified_name)
|
||||
return None
|
||||
except Exception:
|
||||
logger.exception("Failed to find torch op '%s'", qualified_name)
|
||||
return None
|
||||
|
||||
|
||||
class ONNXRegistry:
|
||||
"""Registry for ONNX functions.
|
||||
|
||||
The registry maintains a mapping from qualified names to symbolic functions under a
|
||||
fixed opset version. It supports registering custom onnx-script functions and for
|
||||
dispatcher to dispatch calls to the appropriate function.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""Initializes the registry"""
|
||||
self._opset_version = _constants.TORCHLIB_OPSET
|
||||
self.functions: dict[TorchOp | str, list[OnnxDecompMeta]] = {}
|
||||
|
||||
@property
|
||||
def opset_version(self) -> int:
|
||||
"""The ONNX opset version the exporter should target."""
|
||||
return self._opset_version
|
||||
|
||||
@classmethod
|
||||
def from_torchlib(cls, opset_version=_constants.TORCHLIB_OPSET) -> ONNXRegistry:
|
||||
"""Populates the registry with ATen functions from torchlib.
|
||||
|
||||
Args:
|
||||
torchlib_registry: The torchlib registry to use for populating the registry.
|
||||
"""
|
||||
registry = cls()
|
||||
registry._opset_version = opset_version
|
||||
for meta in _torchlib_registry.get_torchlib_ops():
|
||||
registry._register(meta.fx_target, meta)
|
||||
|
||||
# TODO(justinchuby): Remove this once torchlib is migrated to PyTorch
|
||||
torchlib_ops = onnxscript_apis.get_torchlib_ops()
|
||||
|
||||
for torchlib_meta in torchlib_ops:
|
||||
qualified_name = torchlib_meta.qualified_name
|
||||
overload_func = torchlib_meta.function
|
||||
try:
|
||||
# NOTE: This is heavily guarded with try-except because we don't want
|
||||
# to fail the entire registry population if one function fails.
|
||||
target = _get_overload(qualified_name)
|
||||
if target is None:
|
||||
continue
|
||||
|
||||
meta = OnnxDecompMeta(
|
||||
onnx_function=overload_func,
|
||||
fx_target=target,
|
||||
signature=None,
|
||||
is_custom=False,
|
||||
is_complex=torchlib_meta.is_complex,
|
||||
)
|
||||
registry._register(target, meta)
|
||||
except Exception:
|
||||
logger.exception("Failed to register '%s'. Skipped", qualified_name)
|
||||
continue
|
||||
|
||||
registry._cleanup_registry_based_on_opset_version()
|
||||
return registry
|
||||
|
||||
def _register(
|
||||
self,
|
||||
target: TorchOp,
|
||||
onnx_decomposition: OnnxDecompMeta,
|
||||
) -> None:
|
||||
"""Registers a OnnxDecompMeta to an operator.
|
||||
|
||||
Args:
|
||||
target: The PyTorch node callable target.
|
||||
onnx_decomposition: The OnnxDecompMeta to register.
|
||||
"""
|
||||
target_or_name: str | TorchOp
|
||||
if isinstance(target, torch._ops.OpOverload):
|
||||
# Get the qualified name of the aten op because torch._ops.OpOverload lookup in
|
||||
# a dictionary is unreliable for some reason.
|
||||
target_or_name = target.name()
|
||||
else:
|
||||
target_or_name = target
|
||||
if onnx_decomposition.is_custom:
|
||||
self.functions.setdefault(target_or_name, []).insert(0, onnx_decomposition)
|
||||
else:
|
||||
self.functions.setdefault(target_or_name, []).append(onnx_decomposition)
|
||||
|
||||
def register_op(
|
||||
self,
|
||||
target: TorchOp,
|
||||
function: Callable,
|
||||
is_complex: bool = False,
|
||||
) -> None:
|
||||
"""Registers a custom operator: torch.ops.<namespace>.<op_name>.<overload>.
|
||||
|
||||
Args:
|
||||
target: The PyTorch node callable target.
|
||||
function: The onnx-script function to register.
|
||||
is_complex: Whether the function is a function that handles complex valued inputs.
|
||||
"""
|
||||
if isinstance(target, torch._ops.OpOverloadPacket):
|
||||
raise TypeError(
|
||||
f"Target '{target}' should be provided as an OpOverload instead of an "
|
||||
"OpOverloadPacket. You can get the default overload with "
|
||||
"<op>.default"
|
||||
)
|
||||
|
||||
self._register(
|
||||
target,
|
||||
OnnxDecompMeta(
|
||||
onnx_function=function,
|
||||
fx_target=target,
|
||||
signature=None,
|
||||
is_custom=True,
|
||||
is_complex=is_complex,
|
||||
),
|
||||
)
|
||||
|
||||
def get_decomps(self, target: TorchOp) -> list[OnnxDecompMeta]:
|
||||
"""Returns a list of OnnxDecompMeta for the given op: torch.ops.<namespace>.<op_name>.<overload>.
|
||||
|
||||
The list is ordered by the time of registration. The custom operators should come
|
||||
first in the list.
|
||||
|
||||
Args:
|
||||
target: The PyTorch node callable target.
|
||||
Returns:
|
||||
A list of OnnxDecompMeta corresponding to the given name, or None if
|
||||
the name is not in the registry.
|
||||
"""
|
||||
target_or_name: str | TorchOp
|
||||
if isinstance(target, torch._ops.OpOverload):
|
||||
# Get the qualified name of the aten op because torch._ops.OpOverload lookup in
|
||||
# a dictionary is unreliable for some reason.
|
||||
target_or_name = target.name()
|
||||
else:
|
||||
target_or_name = target
|
||||
decomps = self.functions.get(target_or_name, [])
|
||||
return sorted(decomps, key=lambda x: x.is_custom, reverse=True)
|
||||
|
||||
def is_registered(self, target: TorchOp) -> bool:
|
||||
"""Returns whether the given op is registered: torch.ops.<namespace>.<op_name>.<overload>.
|
||||
|
||||
Args:
|
||||
target: The PyTorch node callable target.
|
||||
|
||||
Returns:
|
||||
True if the given op is registered, otherwise False.
|
||||
"""
|
||||
return bool(self.get_decomps(target))
|
||||
|
||||
def _cleanup_registry_based_on_opset_version(self) -> None:
|
||||
"""Pick the implementation with the highest opset version valid until the current opset version."""
|
||||
cleaned_functions = {}
|
||||
for target_or_name, decomps in self.functions.items():
|
||||
# Filter decompositions to only include those with opset_introduced <= opset_version
|
||||
decomps = [d for d in decomps if d.opset_introduced <= self.opset_version]
|
||||
|
||||
# Keep only the decomposition with the highest opset_introduced
|
||||
if decomps:
|
||||
# Find the maximum opset_introduced
|
||||
max_opset = max(d.opset_introduced for d in decomps)
|
||||
|
||||
# Keep all decompositions with the maximum opset_introduced
|
||||
cleaned_functions[target_or_name] = [
|
||||
d for d in decomps if d.opset_introduced == max_opset
|
||||
]
|
||||
|
||||
self.functions = cleaned_functions
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"{self.__class__.__name__}(functions={self.functions})"
|
||||
@@ -0,0 +1,207 @@
|
||||
# mypy: allow-untyped-defs
|
||||
from __future__ import annotations
|
||||
|
||||
import dataclasses
|
||||
import re
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from torch.onnx._internal.exporter import _analysis, _registration, _verification
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import os
|
||||
|
||||
import onnx_ir as ir # type: ignore[import-untyped]
|
||||
|
||||
import torch
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class ExportStatus:
|
||||
# Whether torch.export.export(..., strict=True) succeeds
|
||||
torch_export_strict: bool | None = None
|
||||
# Whether torch.export.export(..., strict=False) succeeds
|
||||
torch_export_non_strict: bool | None = None
|
||||
# Whether torch.export.draft_export() succeeds
|
||||
torch_export_draft_export: bool | None = None
|
||||
# Whether decomposition succeeds
|
||||
decomposition: bool | None = None
|
||||
# Whether ONNX translation succeeds
|
||||
onnx_translation: bool | None = None
|
||||
# Whether ONNX model passes onnx.checker.check_model
|
||||
onnx_checker: bool | None = None
|
||||
# Whether ONNX model runs successfully with ONNX Runtime
|
||||
onnx_runtime: bool | None = None
|
||||
# Whether the output of the ONNX model is accurate
|
||||
output_accuracy: bool | None = None
|
||||
|
||||
|
||||
def _status_emoji(status: bool | None) -> str:
|
||||
if status is None:
|
||||
return "⚪"
|
||||
return "✅" if status else "❌"
|
||||
|
||||
|
||||
def _format_export_status(status: ExportStatus) -> str:
|
||||
return (
|
||||
f"```\n"
|
||||
f"{_status_emoji(status.torch_export_non_strict)} Obtain model graph with `torch.export.export(..., strict=False)`\n"
|
||||
f"{_status_emoji(status.torch_export_strict)} Obtain model graph with `torch.export.export(..., strict=True)`\n"
|
||||
f"{_status_emoji(status.torch_export_draft_export)} Obtain model graph with `torch.export.draft_export`\n"
|
||||
f"{_status_emoji(status.decomposition)} Decompose operators for ONNX compatibility\n"
|
||||
f"{_status_emoji(status.onnx_translation)} Translate the graph into ONNX\n"
|
||||
f"{_status_emoji(status.onnx_checker)} Run `onnx.checker` on the ONNX model\n"
|
||||
f"{_status_emoji(status.onnx_runtime)} Execute the model with ONNX Runtime\n"
|
||||
f"{_status_emoji(status.output_accuracy)} Validate model output accuracy\n"
|
||||
f"```\n\n"
|
||||
)
|
||||
|
||||
|
||||
def _strip_color_from_string(text: str) -> str:
|
||||
# This regular expression matches ANSI escape codes
|
||||
# https://github.com/pytorch/pytorch/blob/9554a9af8788c57e1c5222c39076a5afcf0998ae/torch/_dynamo/utils.py#L2785-L2788
|
||||
ansi_escape = re.compile(r"\x1B[@-_][0-?]*[ -/]*[@-~]")
|
||||
return ansi_escape.sub("", text)
|
||||
|
||||
|
||||
def _format_exported_program(exported_program: torch.export.ExportedProgram) -> str:
|
||||
# Adapted from https://github.com/pytorch/pytorch/pull/128476
|
||||
# to remove colors
|
||||
# Even though we can call graph_module.print_readable directly, since the
|
||||
# colored option was added only recently, we can't guarantee that the
|
||||
# version of PyTorch used by the user has this option. Therefore, we
|
||||
# still call str(ExportedProgram)
|
||||
text = f"```python\n{_strip_color_from_string(str(exported_program))}\n```\n\n"
|
||||
return text
|
||||
|
||||
|
||||
def construct_report_file_name(timestamp: str, status: ExportStatus) -> str:
|
||||
# Status could be None. So we need to check for False explicitly.
|
||||
if not (
|
||||
status.torch_export_non_strict
|
||||
or status.torch_export_strict
|
||||
or status.torch_export_draft_export
|
||||
):
|
||||
# All strategies failed
|
||||
postfix = "pt_export"
|
||||
elif status.decomposition is False:
|
||||
postfix = "decomp"
|
||||
elif status.onnx_translation is False:
|
||||
postfix = "conversion"
|
||||
elif status.onnx_checker is False:
|
||||
postfix = "checker"
|
||||
elif status.onnx_runtime is False:
|
||||
postfix = "runtime"
|
||||
elif status.output_accuracy is False:
|
||||
postfix = "accuracy"
|
||||
elif (
|
||||
status.torch_export_strict is False
|
||||
or status.torch_export_non_strict is False
|
||||
or status.torch_export_draft_export is False
|
||||
):
|
||||
# Some strategies failed
|
||||
postfix = "strategies"
|
||||
else:
|
||||
postfix = "success"
|
||||
return f"onnx_export_{timestamp}_{postfix}.md"
|
||||
|
||||
|
||||
def format_decomp_comparison(
|
||||
pre_decomp_unique_ops: set[str],
|
||||
post_decomp_unique_ops: set[str],
|
||||
) -> str:
|
||||
"""Format the decomposition comparison result.
|
||||
|
||||
Args:
|
||||
unique_ops_in_a: The unique ops in the first program.
|
||||
unique_ops_in_b: The unique ops in the second program.
|
||||
|
||||
Returns:
|
||||
The formatted comparison result.
|
||||
"""
|
||||
return (
|
||||
f"Ops exist only in the ExportedProgram before decomposition: `{sorted(pre_decomp_unique_ops)}`\n\n"
|
||||
f"Ops exist only in the ExportedProgram after decomposition: `{sorted(post_decomp_unique_ops)}`\n"
|
||||
)
|
||||
|
||||
|
||||
def format_verification_infos(
|
||||
verification_infos: list[_verification.VerificationInfo],
|
||||
) -> str:
|
||||
"""Format the verification result.
|
||||
|
||||
Args:
|
||||
verification_infos: The verification result.
|
||||
|
||||
Returns:
|
||||
The formatted verification result.
|
||||
"""
|
||||
return "\n".join(
|
||||
f"`{info.name}`: `max_abs_diff={info.max_abs_diff:e}`, `max_rel_diff={info.max_rel_diff:e}`, "
|
||||
f"`abs_diff_hist={info.abs_diff_hist}`, `rel_diff_hist={info.rel_diff_hist}`"
|
||||
for info in verification_infos
|
||||
)
|
||||
|
||||
|
||||
def create_torch_export_error_report(
|
||||
filename: str | os.PathLike,
|
||||
formatted_traceback: str,
|
||||
*,
|
||||
export_status: ExportStatus,
|
||||
profile_result: str | None,
|
||||
) -> None:
|
||||
with open(filename, "w", encoding="utf-8") as f:
|
||||
f.write("# PyTorch ONNX Conversion Error Report\n\n")
|
||||
f.write(_format_export_status(export_status))
|
||||
f.write("Error message:\n\n")
|
||||
f.write("```pytb\n")
|
||||
f.write(formatted_traceback)
|
||||
f.write("```\n\n")
|
||||
if profile_result is not None:
|
||||
f.write("## Profiling result\n\n")
|
||||
f.write("```\n")
|
||||
f.write(profile_result)
|
||||
f.write("```\n")
|
||||
|
||||
|
||||
def create_onnx_export_report(
|
||||
filename: str | os.PathLike,
|
||||
formatted_traceback: str,
|
||||
program: torch.export.ExportedProgram,
|
||||
*,
|
||||
decomp_comparison: str | None = None,
|
||||
export_status: ExportStatus,
|
||||
profile_result: str | None,
|
||||
model: ir.Model | None = None,
|
||||
registry: _registration.ONNXRegistry | None = None,
|
||||
verification_result: str | None = None,
|
||||
) -> None:
|
||||
with open(filename, "w", encoding="utf-8") as f:
|
||||
f.write("# PyTorch ONNX Conversion Report\n\n")
|
||||
f.write(_format_export_status(export_status))
|
||||
f.write("## Error messages\n\n")
|
||||
f.write("```pytb\n")
|
||||
f.write(formatted_traceback)
|
||||
f.write("\n```\n\n")
|
||||
f.write("## Exported program\n\n")
|
||||
f.write(_format_exported_program(program))
|
||||
if model is not None:
|
||||
f.write("## ONNX model\n\n")
|
||||
f.write("```python\n")
|
||||
f.write(str(model))
|
||||
f.write("\n```\n\n")
|
||||
f.write("## Analysis\n\n")
|
||||
_analysis.analyze(program, file=f, registry=registry)
|
||||
if decomp_comparison is not None:
|
||||
f.write("\n## Decomposition comparison\n\n")
|
||||
f.write(decomp_comparison)
|
||||
f.write("\n")
|
||||
if verification_result is not None:
|
||||
f.write("\n## Verification results\n\n")
|
||||
f.write(verification_result)
|
||||
f.write("\n")
|
||||
if profile_result is not None:
|
||||
f.write("\n## Profiling result\n\n")
|
||||
f.write("```\n")
|
||||
f.write(profile_result)
|
||||
f.write("```\n")
|
||||
@@ -0,0 +1,332 @@
|
||||
# mypy: allow-untyped-defs
|
||||
"""Helpers for constructing ONNX operator signatures from Python functions."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import collections.abc
|
||||
import inspect
|
||||
import logging
|
||||
import types
|
||||
import typing
|
||||
from collections.abc import Sequence
|
||||
from typing import Any, Optional, TypeVar, Union
|
||||
|
||||
from torch.onnx._internal._lazy_import import onnx_ir as ir, onnxscript
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# Map from python type to corresponding ONNX AttributeProto type
|
||||
_PY_TYPE_TO_ATTR_TYPE = {
|
||||
float: ir.AttributeType.FLOAT,
|
||||
int: ir.AttributeType.INT,
|
||||
str: ir.AttributeType.STRING,
|
||||
bool: ir.AttributeType.INT,
|
||||
ir.Tensor: ir.AttributeType.TENSOR,
|
||||
ir.TensorProtocol: ir.AttributeType.TENSOR,
|
||||
ir.Graph: ir.AttributeType.GRAPH,
|
||||
ir.GraphProtocol: ir.AttributeType.GRAPH,
|
||||
}
|
||||
|
||||
# Map from python type to corresponding ONNX AttributeProto type,
|
||||
# for repeated (i.e., list of) values
|
||||
_LIST_TYPE_TO_ATTR_TYPE = {
|
||||
float: ir.AttributeType.FLOATS,
|
||||
int: ir.AttributeType.INTS,
|
||||
str: ir.AttributeType.STRINGS,
|
||||
bool: ir.AttributeType.INTS,
|
||||
ir.Tensor: ir.AttributeType.TENSORS,
|
||||
ir.TensorProtocol: ir.AttributeType.TENSORS,
|
||||
ir.Graph: ir.AttributeType.GRAPHS,
|
||||
ir.GraphProtocol: ir.AttributeType.GRAPHS,
|
||||
}
|
||||
|
||||
_ALL_VALUE_TYPES = (
|
||||
{ir.TensorType(dtype) for dtype in ir.DataType}
|
||||
| {ir.SequenceType(ir.TensorType(dtype)) for dtype in ir.DataType}
|
||||
| {ir.OptionalType(ir.TensorType(dtype)) for dtype in ir.DataType}
|
||||
)
|
||||
|
||||
# TypeAnnotationValue represents the (value of) valid type-annotations recognized
|
||||
# by ONNX Script. Currently, it supports
|
||||
# - float, int, str (primitive attribute types)
|
||||
# - Sequence[float], Sequence[int], Sequence[str] (attribute types)
|
||||
# - Tensor types
|
||||
# - Sequence[Tensor] types
|
||||
# - Union of above 2
|
||||
# - TypeVars with above bounds
|
||||
# - Above types with annotation attached
|
||||
TypeAnnotationValue = Any
|
||||
|
||||
|
||||
def _is_optional(type_: type) -> bool:
|
||||
"""Returns whether a type_ is an Optional."""
|
||||
origin_type = typing.get_origin(type_)
|
||||
if origin_type is Union and type(None) in typing.get_args(type_):
|
||||
# Python < 3.10
|
||||
return True
|
||||
if origin_type is Optional:
|
||||
# Python >= 3.10
|
||||
return True
|
||||
if (
|
||||
hasattr(types, "UnionType")
|
||||
and origin_type is types.UnionType
|
||||
and type(None) in typing.get_args(type_)
|
||||
):
|
||||
# Python >= 3.10
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _get_attr_type(type_: type) -> ir.AttributeType:
|
||||
"""Obtain the type of the attribute from a Python class."""
|
||||
try:
|
||||
if type_ in _PY_TYPE_TO_ATTR_TYPE:
|
||||
return _PY_TYPE_TO_ATTR_TYPE[type_]
|
||||
origin_type = typing.get_origin(type_)
|
||||
if origin_type is None:
|
||||
return ir.AttributeType.UNDEFINED
|
||||
if origin_type in (
|
||||
collections.abc.Sequence,
|
||||
Sequence,
|
||||
list,
|
||||
list,
|
||||
tuple,
|
||||
tuple,
|
||||
):
|
||||
inner_type = typing.get_args(type_)[0]
|
||||
if inner_type in _LIST_TYPE_TO_ATTR_TYPE:
|
||||
return _LIST_TYPE_TO_ATTR_TYPE[inner_type]
|
||||
except TypeError:
|
||||
logger.warning("TypeError when checking %s.", type_, exc_info=True)
|
||||
return ir.AttributeType.UNDEFINED
|
||||
|
||||
|
||||
def _get_type_constraint_name(type_: TypeAnnotationValue) -> str | None:
|
||||
"""Returns the name of the type constraint for a given type annotation.
|
||||
|
||||
Args:
|
||||
type_: A Python type.
|
||||
|
||||
Returns:
|
||||
The name of the type constraint if it is a TypeVar.
|
||||
- Prefixes the name with "Sequence_" if the type annotation is a Sequence[].
|
||||
"""
|
||||
if isinstance(type_, TypeVar):
|
||||
return type_.__name__
|
||||
if _is_optional(type_):
|
||||
subtypes = typing.get_args(type_)
|
||||
for subtype in subtypes:
|
||||
if subtype is type(None):
|
||||
continue
|
||||
type_param_name = _get_type_constraint_name(subtype)
|
||||
return type_param_name if type_param_name else None
|
||||
origin_type = typing.get_origin(type_)
|
||||
if isinstance(origin_type, type) and issubclass(origin_type, Sequence):
|
||||
subtypes = typing.get_args(type_)
|
||||
type_param_name = _get_type_constraint_name(subtypes[0])
|
||||
return f"Sequence_{type_param_name}" if type_param_name else None
|
||||
return None
|
||||
|
||||
|
||||
def _get_allowed_types_from_type_annotation(
|
||||
type_: TypeAnnotationValue,
|
||||
) -> set[ir.TypeProtocol]:
|
||||
"""Obtain the allowed types from a type annotation."""
|
||||
if type_ is onnxscript.onnx_types.TensorType:
|
||||
# Any tensor type
|
||||
return {ir.TensorType(dtype) for dtype in ir.DataType}
|
||||
|
||||
allowed_types: set[ir.TypeProtocol]
|
||||
|
||||
if isinstance(type_, TypeVar):
|
||||
allowed_types = set()
|
||||
if constraints := type_.__constraints__:
|
||||
for constraint in constraints:
|
||||
allowed_types.update(
|
||||
_get_allowed_types_from_type_annotation(constraint)
|
||||
)
|
||||
else:
|
||||
bound = type_.__bound__
|
||||
if bound is None:
|
||||
allowed_types = _ALL_VALUE_TYPES # type: ignore[assignment]
|
||||
else:
|
||||
allowed_types.update(_get_allowed_types_from_type_annotation(bound))
|
||||
return allowed_types
|
||||
if hasattr(type_, "dtype"):
|
||||
# A single tensor type like INT64, FLOAT, etc.
|
||||
return {ir.TensorType(ir.DataType(type_.dtype))}
|
||||
if _is_optional(type_):
|
||||
allowed_types = set()
|
||||
subtypes = typing.get_args(type_)
|
||||
for subtype in subtypes:
|
||||
if subtype is type(None):
|
||||
continue
|
||||
allowed_types.update(_get_allowed_types_from_type_annotation(subtype))
|
||||
# NOTE: We do not consider dynamic optional types like optional(float) because they are not very useful.
|
||||
return allowed_types
|
||||
|
||||
origin_type = typing.get_origin(type_)
|
||||
if origin_type is Union:
|
||||
allowed_types = set()
|
||||
subtypes = typing.get_args(type_)
|
||||
for subtype in subtypes:
|
||||
if subtype is type(None):
|
||||
raise AssertionError(
|
||||
"Union should not contain None type because it is handled by _is_optional."
|
||||
)
|
||||
allowed_types.update(_get_allowed_types_from_type_annotation(subtype))
|
||||
return allowed_types
|
||||
|
||||
if isinstance(origin_type, type) and issubclass(origin_type, Sequence):
|
||||
subtypes = typing.get_args(type_)
|
||||
return {
|
||||
ir.SequenceType(t)
|
||||
for t in _get_allowed_types_from_type_annotation(subtypes[0])
|
||||
}
|
||||
|
||||
# Allow everything by default
|
||||
return _ALL_VALUE_TYPES # type: ignore[return-value]
|
||||
|
||||
|
||||
def op_signature_from_function(
|
||||
func,
|
||||
domain: str,
|
||||
name: str | None = None,
|
||||
overload: str = "",
|
||||
*,
|
||||
since_version: int = 1,
|
||||
) -> ir.schemas.OpSignature:
|
||||
"""Produce an OpSignature from a function using type annotation."""
|
||||
|
||||
py_signature = inspect.signature(func)
|
||||
# Not using inspect.get_annotations because typing.get_type_hints seems to handle more cases
|
||||
# https://github.com/python/cpython/issues/102405
|
||||
type_hints = typing.get_type_hints(func)
|
||||
|
||||
params: list[ir.schemas.Parameter | ir.schemas.AttributeParameter] = []
|
||||
# Create a mapping from type to a unique name
|
||||
type_constraints: dict[str, ir.schemas.TypeConstraintParam] = {}
|
||||
|
||||
for param in py_signature.parameters.values():
|
||||
if param.name not in type_hints:
|
||||
logger.debug(
|
||||
"Missing annotation for parameter '%s' from %s. Treating as an Input.",
|
||||
param.name,
|
||||
py_signature,
|
||||
)
|
||||
type_constraint = ir.schemas.TypeConstraintParam.any_value(
|
||||
f"T_{param.name}"
|
||||
)
|
||||
type_constraints[param.name] = type_constraint
|
||||
kwargs: dict[str, Any] = {}
|
||||
if param.default is not inspect.Parameter.empty:
|
||||
kwargs["default"] = param.default
|
||||
params.append(
|
||||
ir.schemas.Parameter(
|
||||
name=param.name,
|
||||
type_constraint=type_constraint,
|
||||
required=param.default is inspect.Parameter.empty,
|
||||
# TODO: Handle variadic
|
||||
variadic=False,
|
||||
**kwargs,
|
||||
)
|
||||
)
|
||||
else:
|
||||
type_ = type_hints[param.name]
|
||||
if (attr_type := _get_attr_type(type_)) != ir.AttributeType.UNDEFINED:
|
||||
# Construct the default attribute
|
||||
if param.default is not inspect.Parameter.empty:
|
||||
# TODO: Use ir_convenience instead to handle int as float
|
||||
default = ir.Attr(param.name, attr_type, param.default)
|
||||
else:
|
||||
default = None
|
||||
params.append(
|
||||
ir.schemas.AttributeParameter(
|
||||
name=param.name,
|
||||
type=attr_type,
|
||||
required=param.default is inspect.Parameter.empty,
|
||||
default=default,
|
||||
)
|
||||
)
|
||||
else:
|
||||
# Obtain the type constraint from the type annotation
|
||||
|
||||
# 1. Get a type constraint name from the type annotation
|
||||
# If the type annotation is a TypeVar or Optional[TypeVar], get its name
|
||||
# Otherwise, name it T_{param.name}
|
||||
type_constraint_name = _get_type_constraint_name(type_)
|
||||
if type_constraint_name is None:
|
||||
type_constraint_name = f"T_{param.name}"
|
||||
|
||||
# 2. If the type constraint param is already initialized, use it
|
||||
if type_constraint_name in type_constraints:
|
||||
type_constraint = type_constraints[type_constraint_name]
|
||||
else:
|
||||
# 3. Otherwise, create a new TypeConstraintParam
|
||||
type_constraint = ir.schemas.TypeConstraintParam(
|
||||
name=type_constraint_name,
|
||||
allowed_types=_get_allowed_types_from_type_annotation(type_),
|
||||
)
|
||||
type_constraints[type_constraint_name] = type_constraint
|
||||
# 4. Create Parameter
|
||||
kwargs: dict[str, Any] = {}
|
||||
if param.default is not inspect.Parameter.empty:
|
||||
kwargs["default"] = param.default
|
||||
params.append(
|
||||
ir.schemas.Parameter(
|
||||
name=param.name,
|
||||
type_constraint=type_constraint,
|
||||
required=param.default is inspect.Parameter.empty,
|
||||
# TODO: Handle variadic
|
||||
variadic=False,
|
||||
**kwargs,
|
||||
)
|
||||
)
|
||||
|
||||
return_type = type_hints.get("return")
|
||||
|
||||
outputs = []
|
||||
if return_type is None:
|
||||
# No returns
|
||||
pass
|
||||
else:
|
||||
if typing.get_origin(return_type) is tuple:
|
||||
# Multiple returns
|
||||
return_types = typing.get_args(return_type)
|
||||
else:
|
||||
return_types = [return_type] # type: ignore[assignment]
|
||||
|
||||
for i, return_type_i in enumerate(return_types):
|
||||
if (
|
||||
return_param_name := _get_type_constraint_name(return_type_i)
|
||||
) in type_constraints:
|
||||
# pyrefly: ignore [bad-index]
|
||||
type_constraint = type_constraints[return_param_name]
|
||||
else:
|
||||
return_param_name = f"TReturn{i}"
|
||||
type_constraint = ir.schemas.TypeConstraintParam(
|
||||
name=return_param_name,
|
||||
allowed_types=_get_allowed_types_from_type_annotation(
|
||||
return_type_i
|
||||
),
|
||||
)
|
||||
type_constraints[return_param_name] = type_constraint
|
||||
outputs.append(
|
||||
ir.schemas.Parameter(
|
||||
name=return_param_name,
|
||||
type_constraint=type_constraint,
|
||||
required=True,
|
||||
variadic=False,
|
||||
)
|
||||
)
|
||||
|
||||
return ir.schemas.OpSignature(
|
||||
domain=domain,
|
||||
name=name or func.__name__,
|
||||
overload=overload,
|
||||
params=params,
|
||||
outputs=outputs,
|
||||
since_version=since_version,
|
||||
)
|
||||
@@ -0,0 +1,100 @@
|
||||
"""Subclass of ir.Value that supports Python operators."""
|
||||
|
||||
# mypy: allow-untyped-defs
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from torch.onnx._internal._lazy_import import onnx_ir as ir
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import onnxscript
|
||||
|
||||
|
||||
class SymbolicTensor(ir.Value):
|
||||
"""A subclass of ir.Value that supports Python operators."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
opset: onnxscript.values.Opset,
|
||||
name: str | None = None,
|
||||
shape: ir.Shape | None = None,
|
||||
type: ir.TypeProtocol | None = None,
|
||||
doc_string: str | None = None,
|
||||
const_value: ir.TensorProtocol | None = None,
|
||||
) -> None:
|
||||
super().__init__(
|
||||
name=name,
|
||||
shape=shape,
|
||||
type=type,
|
||||
doc_string=doc_string,
|
||||
const_value=const_value,
|
||||
)
|
||||
self._opset = opset
|
||||
|
||||
@property
|
||||
def rank(self) -> int | None:
|
||||
if self.shape is None:
|
||||
return None
|
||||
return len(self.shape)
|
||||
|
||||
# TODO: Implement indexing
|
||||
|
||||
def __mod__(self, other):
|
||||
if self.dtype in {
|
||||
ir.DataType.FLOAT,
|
||||
ir.DataType.DOUBLE,
|
||||
ir.DataType.FLOAT16,
|
||||
ir.DataType.BFLOAT16,
|
||||
}:
|
||||
return self._opset.Mod(self, other, fmod=1)
|
||||
return self._opset.Mod(self, other)
|
||||
|
||||
def __ne__(self, other):
|
||||
return self._opset.Not(self._opset.Equal(self, other))
|
||||
|
||||
def __neg__(self):
|
||||
return self._opset.Neg(self)
|
||||
|
||||
def __add__(self, other):
|
||||
return self._opset.Add(self, other)
|
||||
|
||||
def __radd__(self, other):
|
||||
return self._opset.Add(other, self)
|
||||
|
||||
def __rand__(self, other):
|
||||
return self._opset.And(other, self)
|
||||
|
||||
def __mul__(self, other):
|
||||
return self._opset.Mul(self, other)
|
||||
|
||||
def __rmul__(self, other):
|
||||
return self._opset.Mul(other, self)
|
||||
|
||||
def __matmul__(self, other):
|
||||
return self._opset.MatMul(self, other)
|
||||
|
||||
def __pow__(self, other):
|
||||
return self._opset.Pow(self, other)
|
||||
|
||||
def __sub__(self, other):
|
||||
return self._opset.Sub(self, other)
|
||||
|
||||
def __rsub__(self, other):
|
||||
return self._opset.Sub(other, self)
|
||||
|
||||
def __truediv__(self, other):
|
||||
return self._opset.Div(self, other)
|
||||
|
||||
def __lt__(self, other):
|
||||
return self._opset.Less(self, other)
|
||||
|
||||
def __le__(self, other):
|
||||
return self._opset.LessOrEqual(self, other)
|
||||
|
||||
def __ge__(self, other):
|
||||
return self._opset.GreaterOrEqual(self, other)
|
||||
|
||||
def __gt__(self, other):
|
||||
return self._opset.Greater(self, other)
|
||||
@@ -0,0 +1,102 @@
|
||||
"""Test utilities for ONNX export."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
__all__ = ["assert_onnx_program"]
|
||||
|
||||
from typing import Any, Literal, TYPE_CHECKING
|
||||
|
||||
import torch
|
||||
from torch.utils import _pytree
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from torch.onnx._internal.exporter import _onnx_program
|
||||
|
||||
|
||||
def assert_onnx_program(
|
||||
program: _onnx_program.ONNXProgram,
|
||||
*,
|
||||
rtol: float | None = None,
|
||||
atol: float | None = None,
|
||||
args: tuple[Any, ...] | None = None,
|
||||
kwargs: dict[str, Any] | None = None,
|
||||
strategy: str | None = "TorchExportNonStrictStrategy",
|
||||
backend: Literal["onnxruntime", "reference"] = "onnxruntime",
|
||||
) -> None:
|
||||
"""Assert that the ONNX model produces the same output as the PyTorch ExportedProgram.
|
||||
|
||||
Args:
|
||||
program: The ``ONNXProgram`` to verify.
|
||||
rtol: Relative tolerance.
|
||||
atol: Absolute tolerance.
|
||||
args: The positional arguments to pass to the program.
|
||||
If None, the default example inputs in the ExportedProgram will be used.
|
||||
kwargs: The keyword arguments to pass to the program.
|
||||
If None, the default example inputs in the ExportedProgram will be used.
|
||||
strategy: Assert the capture strategy used to export the program. Values can be
|
||||
class names like "TorchExportNonStrictStrategy".
|
||||
If None, the strategy is not asserted.
|
||||
backend: The backend to use for evaluating the ONNX program.
|
||||
Supported values are "onnxruntime" and "reference".
|
||||
"""
|
||||
if strategy is not None:
|
||||
if program._capture_strategy != strategy:
|
||||
raise ValueError(
|
||||
f"Expected strategy '{strategy}' is used to capture the exported program, "
|
||||
f"but got '{program._capture_strategy}'."
|
||||
)
|
||||
exported_program = program.exported_program
|
||||
if exported_program is None:
|
||||
raise ValueError(
|
||||
"The ONNXProgram does not contain an ExportedProgram. "
|
||||
"To verify the ONNX program, initialize ONNXProgram with an ExportedProgram, "
|
||||
"or assign the ExportedProgram to the ONNXProgram.exported_program attribute."
|
||||
)
|
||||
if args is None and kwargs is None:
|
||||
# User did not provide example inputs, use the default example inputs
|
||||
if exported_program.example_inputs is None:
|
||||
raise ValueError(
|
||||
"No example inputs provided and the exported_program does not contain example inputs. "
|
||||
"Please provide arguments to verify the ONNX program."
|
||||
)
|
||||
args, kwargs = exported_program.example_inputs
|
||||
if args is None:
|
||||
args = ()
|
||||
if kwargs is None:
|
||||
kwargs = {}
|
||||
torch_module = exported_program.module()
|
||||
torch_outputs, _ = _pytree.tree_flatten(torch_module(*args, **kwargs))
|
||||
# ONNX outputs are always real, so we need to convert torch complex outputs to real representations
|
||||
torch_outputs_adapted = []
|
||||
for output in torch_outputs:
|
||||
# ONNX graph does not support None outputs, so we skip them
|
||||
if output is None:
|
||||
continue
|
||||
if not isinstance(output, torch.Tensor):
|
||||
torch_outputs_adapted.append(torch.tensor(output))
|
||||
elif torch.is_complex(output):
|
||||
torch_outputs_adapted.append(torch.view_as_real(output))
|
||||
else:
|
||||
torch_outputs_adapted.append(output)
|
||||
|
||||
# Obtain the ONNX outputs using the specified backend
|
||||
if backend == "onnxruntime":
|
||||
onnx_outputs = program(*args, **kwargs)
|
||||
elif backend == "reference":
|
||||
onnx_outputs = program.call_reference(*args, **kwargs)
|
||||
else:
|
||||
raise ValueError(
|
||||
f"Unsupported backend '{backend}'. Supported backends are 'onnxruntime' and 'reference'."
|
||||
)
|
||||
|
||||
# TODO(justinchuby): Include output names in the error message
|
||||
torch.testing.assert_close(
|
||||
tuple(onnx_outputs),
|
||||
tuple(torch_outputs_adapted),
|
||||
rtol=rtol,
|
||||
atol=atol,
|
||||
equal_nan=True,
|
||||
check_device=False,
|
||||
)
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
"""Typings for function definitions."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TypeVar, Union
|
||||
|
||||
from onnxscript import (
|
||||
BFLOAT16,
|
||||
BOOL,
|
||||
COMPLEX128,
|
||||
COMPLEX64,
|
||||
DOUBLE,
|
||||
FLOAT,
|
||||
FLOAT16,
|
||||
INT16,
|
||||
INT32,
|
||||
INT64,
|
||||
INT8,
|
||||
STRING,
|
||||
UINT8,
|
||||
)
|
||||
|
||||
|
||||
# NOTE: We do not care about unsigned types beyond UINT8 because PyTorch does not us them.
|
||||
# More detail can be found: https://pytorch.org/docs/stable/tensors.html
|
||||
|
||||
TensorType = Union[ # noqa: UP007
|
||||
BFLOAT16,
|
||||
BOOL,
|
||||
COMPLEX64,
|
||||
COMPLEX128,
|
||||
DOUBLE,
|
||||
FLOAT,
|
||||
FLOAT16,
|
||||
INT8,
|
||||
INT16,
|
||||
INT32,
|
||||
INT64,
|
||||
UINT8,
|
||||
]
|
||||
_FloatType = Union[FLOAT16, FLOAT, DOUBLE, BFLOAT16] # noqa: UP007
|
||||
IntType = Union[INT8, INT16, INT32, INT64] # noqa: UP007
|
||||
RealType = Union[ # noqa: UP007
|
||||
BFLOAT16,
|
||||
FLOAT16,
|
||||
FLOAT,
|
||||
DOUBLE,
|
||||
INT8,
|
||||
INT16,
|
||||
INT32,
|
||||
INT64,
|
||||
]
|
||||
|
||||
TTensor = TypeVar("TTensor", bound=TensorType)
|
||||
# Duplicate TTensor for inputs/outputs that accept the same set of types as TTensor
|
||||
# but do not constrain the type to be the same as the other inputs/outputs
|
||||
TTensor2 = TypeVar("TTensor2", bound=TensorType)
|
||||
TTensorOrString = TypeVar("TTensorOrString", bound=Union[TensorType, STRING]) # noqa: UP007
|
||||
TFloat = TypeVar("TFloat", bound=_FloatType)
|
||||
TFloatOrUInt8 = TypeVar(
|
||||
"TFloatOrUInt8",
|
||||
bound=Union[FLOAT, FLOAT16, DOUBLE, INT8, UINT8], # noqa: UP007
|
||||
)
|
||||
TInt = TypeVar("TInt", bound=IntType)
|
||||
TReal = TypeVar("TReal", bound=RealType)
|
||||
TRealUnlessInt16OrInt8 = TypeVar(
|
||||
"TRealUnlessInt16OrInt8",
|
||||
bound=Union[FLOAT16, FLOAT, DOUBLE, BFLOAT16, INT32, INT64], # noqa: UP007
|
||||
)
|
||||
TRealUnlessFloat16OrInt8 = TypeVar(
|
||||
"TRealUnlessFloat16OrInt8",
|
||||
bound=Union[DOUBLE, FLOAT, INT16, INT32, INT64], # noqa: UP007
|
||||
)
|
||||
TRealOrUInt8 = TypeVar("TRealOrUInt8", bound=Union[RealType, UINT8]) # noqa: UP007
|
||||
TFloatHighPrecision = TypeVar("TFloatHighPrecision", bound=Union[FLOAT, DOUBLE]) # noqa: UP007
|
||||
+95
@@ -0,0 +1,95 @@
|
||||
"""Registry for aten functions."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
__all__ = ["onnx_impl", "get_torchlib_ops"]
|
||||
|
||||
import logging
|
||||
from collections.abc import Callable, Sequence
|
||||
from typing import Any, TypeVar
|
||||
from typing_extensions import ParamSpec
|
||||
|
||||
import onnxscript
|
||||
|
||||
import torch
|
||||
from torch.onnx._internal.exporter import _constants, _registration
|
||||
|
||||
|
||||
# Use ParamSpec for better type preservation instead of bound Callable TypeVar
|
||||
_P = ParamSpec("_P")
|
||||
_R = TypeVar("_R")
|
||||
|
||||
logger = logging.getLogger("__name__")
|
||||
|
||||
|
||||
_registry: list[_registration.OnnxDecompMeta] = []
|
||||
|
||||
|
||||
def onnx_impl(
|
||||
target: _registration.TorchOp | tuple[_registration.TorchOp, ...],
|
||||
*,
|
||||
trace_only: bool = False,
|
||||
complex: bool = False,
|
||||
opset_introduced: int = 18,
|
||||
no_compile: bool = False,
|
||||
private: bool = False,
|
||||
) -> Callable[[Callable[_P, _R]], Callable[_P, _R]]:
|
||||
"""Register an ONNX implementation of a torch op."""
|
||||
|
||||
if isinstance(target, torch._ops.OpOverloadPacket):
|
||||
raise TypeError(
|
||||
f"Target '{target}' should be provided as an OpOverload instead of an "
|
||||
"OpOverloadPacket. You can get the default overload with "
|
||||
"<op>.default"
|
||||
)
|
||||
|
||||
def wrapper(
|
||||
func: Callable[_P, _R],
|
||||
) -> Callable[_P, _R]:
|
||||
processed_func: Any
|
||||
if no_compile:
|
||||
processed_func = func
|
||||
else:
|
||||
torchlib_opset = onnxscript.values.Opset(
|
||||
domain=_constants.TORCHLIB_DOMAIN, version=1
|
||||
)
|
||||
|
||||
if not trace_only:
|
||||
# Compile the function
|
||||
processed_func = onnxscript.script(opset=torchlib_opset)(func)
|
||||
else:
|
||||
processed_func = onnxscript.TracedOnnxFunction(torchlib_opset, func)
|
||||
|
||||
if not private:
|
||||
# TODO(justinchuby): Simplify the logic and remove the private attribute
|
||||
# Skip registration if private
|
||||
if not isinstance(target, Sequence):
|
||||
targets = (target,)
|
||||
else:
|
||||
targets = target # type: ignore[assignment]
|
||||
|
||||
for t in targets:
|
||||
_registry.append(
|
||||
_registration.OnnxDecompMeta(
|
||||
onnx_function=processed_func,
|
||||
fx_target=t,
|
||||
signature=None,
|
||||
is_complex=complex,
|
||||
opset_introduced=opset_introduced,
|
||||
skip_signature_inference=no_compile,
|
||||
)
|
||||
)
|
||||
return processed_func # type: ignore[return-value]
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
def get_torchlib_ops() -> tuple[_registration.OnnxDecompMeta, ...]:
|
||||
# Trigger op registration
|
||||
from torch.onnx._internal.exporter._torchlib import ops
|
||||
|
||||
del ops
|
||||
if len(_registry) == 0:
|
||||
raise AssertionError("_registry must not be empty")
|
||||
return tuple(_registry)
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
__all__ = ["core", "hop", "nn", "symbolic", "symops"]
|
||||
|
||||
from torch.onnx._internal.exporter._torchlib.ops import core, hop, nn, symbolic, symops
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
"""torch.ops.aten operators under the `core` module."""
|
||||
# mypy: disable-error-code="misc,arg-type,type-arg,valid-type,assignment,return-value,type-var,operator,no-untyped-def,index"
|
||||
# pyrefly: ignore-errors
|
||||
# ruff: noqa: TCH001,TCH002
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import operator
|
||||
|
||||
from onnxscript.onnx_opset import opset18 as op
|
||||
|
||||
import torch
|
||||
from torch.onnx._internal.exporter._torchlib._tensor_typing import TReal, TRealOrUInt8
|
||||
from torch.onnx._internal.exporter._torchlib._torchlib_registry import onnx_impl
|
||||
|
||||
|
||||
aten = torch.ops.aten
|
||||
|
||||
|
||||
@onnx_impl((aten.abs.default, operator.abs), trace_only=True)
|
||||
def aten_abs(self: TRealOrUInt8) -> TRealOrUInt8:
|
||||
"""abs(Tensor self) -> Tensor"""
|
||||
|
||||
return op.Abs(self)
|
||||
|
||||
|
||||
@onnx_impl(aten.abs.default, complex=True, trace_only=True)
|
||||
def aten_abs_complex(self: TRealOrUInt8) -> TRealOrUInt8:
|
||||
"""abs(Tensor self) -> Tensor"""
|
||||
|
||||
return op.ReduceL2(self, [-1], keepdims=False)
|
||||
|
||||
|
||||
@onnx_impl((aten.add.Tensor, aten.add.Scalar, operator.add), trace_only=True)
|
||||
def aten_add(self: TReal, other: TReal, alpha: float = 1.0) -> TReal:
|
||||
"""add.Tensor(Tensor self, Tensor other, *, Scalar alpha=1) -> Tensor"""
|
||||
if alpha != 1.0:
|
||||
alpha = op.CastLike(alpha, other)
|
||||
other = op.Mul(other, alpha)
|
||||
return op.Add(self, other)
|
||||
|
||||
|
||||
@onnx_impl((aten.add.Tensor, aten.add.Scalar), trace_only=True, complex=True)
|
||||
def aten_add_complex(self: TReal, other: TReal, alpha: float = 1.0) -> TReal:
|
||||
"""add.Tensor(Tensor self, Tensor other, *, Scalar alpha=1) -> Tensor"""
|
||||
|
||||
return aten_add(self, other, alpha=alpha)
|
||||
+370
@@ -0,0 +1,370 @@
|
||||
"""Implementation for higher-order operators."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import torch
|
||||
from torch.onnx._internal._lazy_import import onnx_ir as ir
|
||||
from torch.onnx._internal.exporter import _core
|
||||
from torch.onnx._internal.exporter._torchlib._torchlib_registry import onnx_impl
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Sequence
|
||||
|
||||
|
||||
def call_op(
|
||||
op_type: str,
|
||||
*args: ir.Value | None,
|
||||
_num_outputs: int = 1,
|
||||
_domain: str = "",
|
||||
**kwargs: int | float | str | bool | ir.Graph | ir.TensorProtocol | Sequence[int],
|
||||
) -> Sequence[ir.Value]:
|
||||
"""Call an operator with the given arguments and keyword arguments.
|
||||
|
||||
Arguments are always inputs, while keyword arguments are attributes.
|
||||
"""
|
||||
# This is a wrapper around the IR node creation that hooks into the _builder.OpRecorder
|
||||
# tracer so that all nodes created are recorded the same way as if we were to use
|
||||
# onnxscript ops directly.
|
||||
from onnxscript.ir import convenience as ir_convenience
|
||||
|
||||
if _core.current_tracer is None:
|
||||
raise AssertionError("current_tracer must be non-None")
|
||||
tracer = _core.current_tracer
|
||||
|
||||
inputs = list(args)
|
||||
|
||||
# If final inputs are None, strip them from the node inputs
|
||||
for input in reversed(inputs):
|
||||
if input is not None:
|
||||
break
|
||||
inputs.pop()
|
||||
|
||||
# Construct and filter out None attributes
|
||||
attributes = [
|
||||
attr
|
||||
for attr in ir_convenience.convert_attributes(kwargs)
|
||||
if attr.value is not None # type: ignore[union-attr]
|
||||
]
|
||||
tracer.nodes.append(
|
||||
node := ir.Node(
|
||||
_domain,
|
||||
op_type,
|
||||
inputs=inputs,
|
||||
attributes=attributes,
|
||||
num_outputs=_num_outputs,
|
||||
version=tracer.opset.version,
|
||||
)
|
||||
)
|
||||
return node.outputs
|
||||
|
||||
|
||||
@onnx_impl(torch.ops.higher_order.cond, no_compile=True)
|
||||
def higher_order_cond(
|
||||
cond: ir.Value,
|
||||
true_func: ir.Function,
|
||||
false_func: ir.Function,
|
||||
inputs: Sequence[ir.Value],
|
||||
) -> Sequence[ir.Value]:
|
||||
then_node = ir.Node(
|
||||
true_func.domain, true_func.name, inputs, num_outputs=len(true_func.outputs)
|
||||
)
|
||||
else_node = ir.Node(
|
||||
false_func.domain, false_func.name, inputs, num_outputs=len(false_func.outputs)
|
||||
)
|
||||
|
||||
# ONNX Runtime complains about duplicate output names if we don't rename them.
|
||||
# But the doesn't seem to be an actual violation of SSA form without renaming.
|
||||
for func_out, out in zip(true_func.outputs, then_node.outputs):
|
||||
out.name = f"{func_out.name}_{true_func.name}"
|
||||
for func_out, out in zip(false_func.outputs, else_node.outputs):
|
||||
out.name = f"{func_out.name}_{false_func.name}"
|
||||
|
||||
return call_op(
|
||||
"If",
|
||||
cond,
|
||||
_num_outputs=len(true_func.outputs),
|
||||
then_branch=ir.Graph(
|
||||
(), then_node.outputs, nodes=[then_node], name=true_func.name
|
||||
),
|
||||
else_branch=ir.Graph(
|
||||
(), else_node.outputs, nodes=[else_node], name=false_func.name
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@onnx_impl(torch.ops.higher_order.scan, no_compile=True)
|
||||
def higher_order_scan(
|
||||
body_func: ir.Function,
|
||||
scan_inits: Sequence[ir.Value],
|
||||
scan_inputs: Sequence[ir.Value],
|
||||
additional_inputs: Sequence[ir.Value] | None,
|
||||
reverse: bool = False,
|
||||
) -> Sequence[ir.Value]:
|
||||
"""https://github.com/pytorch/pytorch/blob/66ac724b56e6c37a534f3e066423ef2f41d7477f/torch/_higher_order_ops/scan.py#L109"""
|
||||
subgraph_inputs = [
|
||||
*[
|
||||
ir.Value(
|
||||
name=f"{inp.name}_{body_func.name}__subgraph_in",
|
||||
shape=inp.shape,
|
||||
type=ir.TensorType(inp.dtype), # type: ignore[arg-type]
|
||||
)
|
||||
for inp in scan_inits
|
||||
],
|
||||
*[
|
||||
ir.Value(
|
||||
name=f"{inp.name}_{body_func.name}__subgraph_in",
|
||||
# The iterated element passed to the body subgraph does not have a sequence axis.
|
||||
# It will have a rank one less than the rank of the corresponding scan_input.
|
||||
shape=ir.Shape(inp.shape[1:]), # type: ignore[index]
|
||||
type=ir.TensorType(inp.dtype), # type: ignore[arg-type]
|
||||
)
|
||||
for inp in scan_inputs
|
||||
],
|
||||
]
|
||||
# The one and only node in the Scan subgraph that calls the body_func
|
||||
body_node = ir.Node(
|
||||
body_func.domain,
|
||||
body_func.name,
|
||||
[
|
||||
*subgraph_inputs,
|
||||
*(additional_inputs or []),
|
||||
],
|
||||
num_outputs=len(body_func.outputs),
|
||||
)
|
||||
|
||||
# ONNX Runtime complains about duplicate output names if we don't rename them.
|
||||
# But the doesn't seem to be an actual violation of SSA form without renaming.
|
||||
for func_out, out in zip(body_func.outputs, body_node.outputs):
|
||||
out.name = f"{func_out.name}_{body_func.name}"
|
||||
|
||||
n_outputs = len(body_func.outputs) - len(scan_inits)
|
||||
return call_op(
|
||||
"Scan",
|
||||
*scan_inits,
|
||||
*scan_inputs,
|
||||
_num_outputs=len(body_func.outputs),
|
||||
body=ir.Graph(
|
||||
subgraph_inputs,
|
||||
body_node.outputs,
|
||||
nodes=[body_node],
|
||||
name=body_func.name,
|
||||
),
|
||||
num_scan_inputs=len(scan_inputs),
|
||||
scan_input_directions=[(1 if reverse else 0) for _ in scan_inputs],
|
||||
scan_output_directions=[(1 if reverse else 0) for _ in range(n_outputs)],
|
||||
)
|
||||
|
||||
|
||||
@onnx_impl(torch.ops.higher_order.while_loop, no_compile=True)
|
||||
def higher_order_while_loop(
|
||||
cond_func: ir.Function,
|
||||
body_func: ir.Function,
|
||||
carried_inputs: Sequence[ir.Value | int | float],
|
||||
additional_inputs: Sequence[ir.Value],
|
||||
) -> Sequence[ir.Value]:
|
||||
"""Implementation of while_loop using ONNX Loop operator.
|
||||
|
||||
The ONNX Loop operator implements a generic looping construct with the signature:
|
||||
Loop(M, cond, v_initial) -> (v_final_and_scan_outputs)
|
||||
|
||||
For while_loop, we use:
|
||||
- M: None (no trip count limit)
|
||||
- cond: initial condition value
|
||||
- v_initial: carried_inputs (loop-carried dependencies)
|
||||
|
||||
The body subgraph takes:
|
||||
- iteration_num (int): current iteration number
|
||||
- condition_in (bool): loop continuation condition from previous iteration
|
||||
- loop_carried_dependencies: the carried values
|
||||
- additional_inputs: any additional inputs (constants/parameters)
|
||||
|
||||
The body subgraph returns:
|
||||
- condition_out (bool): whether to continue looping
|
||||
- loop_carried_dependencies: updated carried values
|
||||
"""
|
||||
|
||||
# Create subgraph inputs for the Loop body
|
||||
# ONNX Loop body signature: (iter_num, cond_in, loop_carried_deps..., additional_inputs...)
|
||||
|
||||
# Start subgraph construction
|
||||
subgraph_carried_inputs = []
|
||||
|
||||
for i, inp in enumerate(carried_inputs):
|
||||
if isinstance(inp, ir.Value):
|
||||
subgraph_carried_inputs.append(
|
||||
ir.Value(
|
||||
name=f"{inp.name}_{body_func.name}__subgraph_in",
|
||||
shape=inp.shape,
|
||||
type=ir.TensorType(inp.dtype), # type: ignore[arg-type]
|
||||
)
|
||||
)
|
||||
elif isinstance(inp, int):
|
||||
subgraph_carried_inputs.append(
|
||||
ir.Value(
|
||||
name=f"carried_input_{i}_{body_func.name}__subgraph_in",
|
||||
shape=ir.Shape([]),
|
||||
type=ir.TensorType(ir.DataType.INT64),
|
||||
)
|
||||
)
|
||||
elif isinstance(inp, float):
|
||||
subgraph_carried_inputs.append(
|
||||
ir.Value(
|
||||
name=f"carried_input_{i}_{body_func.name}__subgraph_in",
|
||||
shape=ir.Shape([]),
|
||||
type=ir.TensorType(ir.DataType.FLOAT),
|
||||
)
|
||||
)
|
||||
else:
|
||||
raise NotImplementedError(
|
||||
f"Unsupported type for carried input: {type(inp)} ({inp}). "
|
||||
"Expected ir.Value, int, or float."
|
||||
)
|
||||
|
||||
subgraph_inputs = [
|
||||
# Iteration number (int scalar, unused)
|
||||
ir.Value(
|
||||
name=f"iter_num_{body_func.name}",
|
||||
shape=ir.Shape([]),
|
||||
type=ir.TensorType(ir.DataType.INT64),
|
||||
),
|
||||
# Condition input (bool scalar, unused)
|
||||
ir.Value(
|
||||
name=f"cond_in_{body_func.name}",
|
||||
shape=ir.Shape([]),
|
||||
type=ir.TensorType(ir.DataType.BOOL),
|
||||
),
|
||||
# Loop-carried dependencies
|
||||
*subgraph_carried_inputs,
|
||||
]
|
||||
|
||||
# Create the combined body function that handles both condition and body logic
|
||||
# First, call the body function with the same inputs
|
||||
body_node = ir.Node(
|
||||
body_func.domain,
|
||||
body_func.name,
|
||||
[
|
||||
*subgraph_carried_inputs, # carried inputs
|
||||
*additional_inputs,
|
||||
],
|
||||
num_outputs=len(body_func.outputs), # carried inputs
|
||||
)
|
||||
|
||||
# Then call the condition function with carried inputs + additional inputs
|
||||
cond_node = ir.Node(
|
||||
cond_func.domain,
|
||||
cond_func.name,
|
||||
[
|
||||
*body_node.outputs, # updated carried inputs from body
|
||||
*additional_inputs,
|
||||
],
|
||||
num_outputs=len(cond_func.outputs),
|
||||
)
|
||||
|
||||
if len(cond_func.outputs) != 1:
|
||||
raise AssertionError("Condition function must return a single boolean value.")
|
||||
|
||||
# ONNX Runtime complains about duplicate output names if we don't rename them
|
||||
for func_out, out in zip(body_func.outputs, body_node.outputs):
|
||||
out.name = f"{func_out.name}_{body_func.name}"
|
||||
for func_out, out in zip(cond_func.outputs, cond_node.outputs):
|
||||
out.name = f"{func_out.name}_{cond_func.name}"
|
||||
|
||||
# The Loop body must return: (cond_out, loop_carried_deps...)
|
||||
# We use the condition output and the body outputs
|
||||
loop_body_outputs = [
|
||||
cond_node.outputs[0], # condition output (bool)
|
||||
*body_node.outputs, # updated carried inputs
|
||||
]
|
||||
|
||||
body_graph = ir.Graph(
|
||||
subgraph_inputs,
|
||||
loop_body_outputs,
|
||||
nodes=[body_node, cond_node],
|
||||
name=f"{body_func.name}_loop_body",
|
||||
)
|
||||
|
||||
# End subgraph construction
|
||||
|
||||
carried_inputs_values: list[ir.Value] = []
|
||||
for inp in carried_inputs:
|
||||
if isinstance(inp, ir.Value):
|
||||
carried_inputs_values.append(inp)
|
||||
elif isinstance(inp, int):
|
||||
const = call_op("Constant", value=ir.tensor(inp))[0]
|
||||
carried_inputs_values.append(const)
|
||||
elif isinstance(inp, float):
|
||||
const = call_op("Constant", value=ir.tensor(inp))[0]
|
||||
carried_inputs_values.append(const)
|
||||
else:
|
||||
raise NotImplementedError(
|
||||
f"Unsupported type for carried input: {type(inp)} ({inp}). "
|
||||
"Expected ir.Value, int, or float."
|
||||
)
|
||||
|
||||
# Get initial condition by calling cond_func with initial inputs
|
||||
initial_outputs = call_op(
|
||||
cond_func.name,
|
||||
*carried_inputs_values,
|
||||
*additional_inputs,
|
||||
_num_outputs=len(cond_func.outputs),
|
||||
_domain=cond_func.domain,
|
||||
)
|
||||
|
||||
if len(initial_outputs) != 1:
|
||||
raise AssertionError("Condition function must return a single boolean value.")
|
||||
|
||||
# Create the Loop operator call
|
||||
# Loop(M, cond, v_initial) where M is empty (no trip count limit)
|
||||
loop_outputs = call_op(
|
||||
"Loop",
|
||||
# M (trip count) - empty string means no limit
|
||||
None,
|
||||
# cond - initial condition
|
||||
initial_outputs[0],
|
||||
# v_initial - carried inputs (loop-carried dependencies)
|
||||
*carried_inputs_values,
|
||||
_num_outputs=len(carried_inputs_values),
|
||||
body=body_graph,
|
||||
)
|
||||
|
||||
return loop_outputs
|
||||
|
||||
|
||||
@onnx_impl(torch.ops.higher_order.invoke_subgraph, no_compile=True)
|
||||
def higher_order_invoke_subgraph(
|
||||
subgraph: ir.Function,
|
||||
identifier: str | None,
|
||||
*operands: ir.Value,
|
||||
) -> Sequence[ir.Value]:
|
||||
"""Export invoke_subgraph HOP by creating a direct function call.
|
||||
|
||||
This preserves the function as a separate entity in the ONNX graph
|
||||
instead of inlining it, which is the purpose of invoke_subgraph.
|
||||
|
||||
Note: The onnxscript optimizer should be configured to not inline functions
|
||||
created by invoke_subgraph to preserve the intended structure.
|
||||
|
||||
Args:
|
||||
subgraph: The function to invoke
|
||||
identifier: Optional identifier for the subgraph (used for caching in PyTorch,
|
||||
not needed for ONNX export as the function reference provides all necessary information)
|
||||
*operands: Input values to pass to the function
|
||||
|
||||
Returns:
|
||||
Sequence of output values from the function call
|
||||
"""
|
||||
# This key can be used by downstream to avoid inlining
|
||||
subgraph.metadata_props["pkg.torch.ops.higher_order.invoke_subgraph.identifier"] = (
|
||||
str(identifier)
|
||||
)
|
||||
|
||||
# Create the function call node
|
||||
return call_op(
|
||||
subgraph.name,
|
||||
*operands,
|
||||
_num_outputs=len(subgraph.outputs),
|
||||
_domain=subgraph.domain,
|
||||
)
|
||||
+377
@@ -0,0 +1,377 @@
|
||||
"""torch.ops.aten operators under the `core` module."""
|
||||
# mypy: disable-error-code="misc,arg-type,type-arg,valid-type,assignment,return-value,type-var,operator,no-untyped-def,index"
|
||||
# pyrefly: ignore-errors
|
||||
# ruff: noqa: TC001,TC002
|
||||
# flake8: noqa: B950
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Sequence, TYPE_CHECKING # noqa: UP035
|
||||
|
||||
from onnxscript.onnx_opset import ( # type: ignore[attr-defined]
|
||||
opset20 as op20,
|
||||
opset21 as op21,
|
||||
opset23 as op23,
|
||||
)
|
||||
|
||||
import torch
|
||||
from torch.onnx._internal._lazy_import import onnx_ir as ir
|
||||
from torch.onnx._internal.exporter._torchlib._tensor_typing import TFloat, TReal
|
||||
from torch.onnx._internal.exporter._torchlib._torchlib_registry import onnx_impl
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from onnxscript.values import Opset
|
||||
|
||||
aten = torch.ops.aten
|
||||
|
||||
|
||||
@onnx_impl(aten.gelu.default, trace_only=True, opset_introduced=20)
|
||||
def aten_gelu_opset20(
|
||||
self: TReal,
|
||||
approximate: str = "none",
|
||||
) -> TReal:
|
||||
"""gelu(Tensor self, *, str approximate="none") -> Tensor"""
|
||||
return op20.Gelu(self, approximate=approximate)
|
||||
|
||||
|
||||
@onnx_impl(aten.group_norm.default, trace_only=True, opset_introduced=21)
|
||||
def aten_group_norm(
|
||||
input: TFloat,
|
||||
num_groups: int,
|
||||
weight: TFloat | None = None,
|
||||
bias: TFloat | None = None,
|
||||
eps: float = 1e-05,
|
||||
cudnn_enabled: bool = True,
|
||||
) -> TFloat:
|
||||
"""group_norm(Tensor input, int num_groups, Tensor? weight=None, Tensor? bias=None, float eps=1e-05, bool cudnn_enabled=True) -> Tensor"""
|
||||
|
||||
c = op21.Shape(input, start=1, end=2)
|
||||
if weight is None:
|
||||
weight = op21.ConstantOfShape(c, value=ir.tensor([1.0], dtype=input.dtype))
|
||||
if bias is None:
|
||||
bias = op21.ConstantOfShape(c, value=ir.tensor([0.0], dtype=input.dtype))
|
||||
return op21.GroupNormalization(
|
||||
input, weight, bias, epsilon=eps, num_groups=num_groups
|
||||
)
|
||||
|
||||
|
||||
@onnx_impl(aten.rms_norm.default, trace_only=True, opset_introduced=23)
|
||||
def aten_rms_norm(
|
||||
input: TFloat,
|
||||
normalized_shape: Sequence[int],
|
||||
weight: TFloat | None = None,
|
||||
eps: float | None = None,
|
||||
) -> TFloat:
|
||||
"""rms_norm(Tensor input, SymInt[] normalized_shape, Tensor? weight=None, float? eps=None) -> Tensor"""
|
||||
|
||||
# Default eps value if not provided
|
||||
if eps is None:
|
||||
eps = torch.finfo(torch.float).eps # Observed from decomp
|
||||
|
||||
# Calculate axis: the first normalization dimension
|
||||
# For normalized_shape with D dimensions, normalize over last D dimensions
|
||||
# Since ONNX RMSNormalization supports negative axis values, we use -len(normalized_shape)
|
||||
# which correctly maps to the first axis of the normalized dimensions
|
||||
normalized_dims = len(normalized_shape)
|
||||
axis = -normalized_dims
|
||||
|
||||
# Create weight tensor if not provided
|
||||
if weight is None:
|
||||
weight = op23.ConstantOfShape(
|
||||
op23.Shape(input), value=ir.tensor([1], dtype=input.dtype)
|
||||
)
|
||||
|
||||
return op23.RMSNormalization(input, weight, axis=axis, epsilon=eps)
|
||||
|
||||
|
||||
@onnx_impl(
|
||||
aten.scaled_dot_product_attention.default, trace_only=True, opset_introduced=23
|
||||
)
|
||||
def aten_scaled_dot_product_attention_23(
|
||||
query: TFloat,
|
||||
key: TFloat,
|
||||
value: TFloat,
|
||||
attn_mask: TFloat | None = None,
|
||||
dropout_p: float = 0.0,
|
||||
is_causal: bool = False,
|
||||
scale: float | None = None,
|
||||
enable_gqa: bool = False,
|
||||
) -> TFloat:
|
||||
"""scaled_dot_product_attention(Tensor query, Tensor key, Tensor value, Tensor? attn_mask=None, float dropout_p=0.0, bool is_causal=False, *, float? scale=None, bool enable_gqa=False) -> Tensor
|
||||
|
||||
Reference:
|
||||
1. https://pytorch.org/docs/stable/generated/torch.nn.functional.scaled_dot_product_attention.html
|
||||
2. https://onnx.ai/onnx/operators/onnx__Attention.html
|
||||
|
||||
Attempts to convert SDPA to Attention onnx op and fallbacks to an onnx graph equivalent to the following PyTorch code::
|
||||
scale_factor = 1 / math.sqrt(Q.size(-1)) if scale is None else scale
|
||||
attn_mask = (
|
||||
torch.ones(L, S, dtype=torch.bool).tril(diagonal=0)
|
||||
if is_causal
|
||||
else attn_mask
|
||||
)
|
||||
attn_mask = (
|
||||
attn_mask.masked_fill(not attn_mask, -float("inf"))
|
||||
if attn_mask.dtype == torch.bool
|
||||
else attn_mask
|
||||
)
|
||||
attn_weight = torch.softmax(
|
||||
(Q @ K.transpose(-2, -1) * scale_factor) + attn_mask, dim=-1
|
||||
)
|
||||
attn_weight = torch.dropout(attn_weight, dropout_p)
|
||||
return attn_weight @ V
|
||||
|
||||
where Q, K, V are the query, key, and value tensors, respectively.
|
||||
L is the target sequence length, S is the source sequence length, and E is the embedding size.
|
||||
"""
|
||||
if is_causal and attn_mask is not None:
|
||||
raise AssertionError("is_causal and attn_mask cannot be set at the same time")
|
||||
if not (len(query.shape) == 4 and len(key.shape) == 4 and len(value.shape) == 4):
|
||||
raise AssertionError("only 4D query, key, and value are supported")
|
||||
|
||||
# Attention onnx op can only handle non-training scenarios where dropout is disabled.
|
||||
if dropout_p == 0:
|
||||
if enable_gqa:
|
||||
if not (
|
||||
query.shape[1] > key.shape[1] == value.shape[1]
|
||||
and query.shape[1] % key.shape[1] == 0
|
||||
):
|
||||
raise AssertionError(
|
||||
"SDPA (GQA or MQA) requires q_num_heads > kv_num_heads & "
|
||||
"q_num_heads % kv_num_heads == 0"
|
||||
)
|
||||
else:
|
||||
if not (query.shape[1] == key.shape[1] == value.shape[1]):
|
||||
raise AssertionError("SDPA (MHA) requires q_num_heads = kv_num_heads")
|
||||
|
||||
# NOTE: num_heads attributes (q_num_heads/kv_num_heads) should not be specified for 4D.
|
||||
# They are not populated with 4D inputs because this information directly comes from input shapes:
|
||||
# `q_num_heads=query.shape[1]` and `kv_num_heads=key.shape[1]`.
|
||||
# This dimension is usually static but it could not be dynamic if also given as an attribute.
|
||||
# num_heads attributes are needed for 3D attention inputs:
|
||||
# (shape: [B, S, N*H]), 4D shape is ([B, N, S, H]).
|
||||
|
||||
Y, _, _, _ = op23.Attention(
|
||||
query,
|
||||
key,
|
||||
value,
|
||||
attn_mask=attn_mask,
|
||||
scale=scale,
|
||||
is_causal=is_causal,
|
||||
)
|
||||
return Y
|
||||
|
||||
if scale is None:
|
||||
scale = _attention_scale(query, op23)
|
||||
scale = op23.CastLike(scale, query)
|
||||
|
||||
if is_causal:
|
||||
attn_mask = _causal_attention_mask(query, key, op23)
|
||||
|
||||
if enable_gqa:
|
||||
key, value = _attention_repeat_kv_for_group_query(query, key, value, op23)
|
||||
|
||||
if attn_mask is None:
|
||||
return _aten_scaled_dot_product_attention_no_mask_onnx(
|
||||
query, key, value, scale, dropout_p, op23
|
||||
)
|
||||
|
||||
return _aten_scaled_dot_product_attention_float_mask_onnx(
|
||||
query, key, value, attn_mask, scale, dropout_p, op23
|
||||
)
|
||||
|
||||
|
||||
def _attention_repeat_kv_for_group_query(
|
||||
query: TFloat, key: TFloat, value: TFloat, op: Opset
|
||||
) -> tuple[TFloat, TFloat]:
|
||||
"""Expand key and value for group query attention.
|
||||
|
||||
repeat_interleave is applied on key and value to match the number of heads in query.
|
||||
|
||||
Args:
|
||||
query: Tensor of shape [B, q_num_heads, q_S, E]
|
||||
key: Tensor of shape [B, k_num_heads, kv_S, E]
|
||||
value: Tensor of shape [B, v_num_heads, kv_S, E]
|
||||
|
||||
Returns:
|
||||
Tuple of (expanded_key, expanded_value) where:
|
||||
- expanded_key: Tensor of shape [B, q_num_heads, kv_S, E]
|
||||
- expanded_value: Tensor of shape [B, q_num_heads, kv_S, E]
|
||||
"""
|
||||
|
||||
if not (
|
||||
query.shape[1] > key.shape[1] == value.shape[1]
|
||||
and query.shape[1] % key.shape[1] == 0
|
||||
):
|
||||
raise AssertionError(
|
||||
"SDPA (GQA or MQA) requires q_num_heads > kv_num_heads & "
|
||||
"q_num_heads % kv_num_heads == 0"
|
||||
)
|
||||
|
||||
# NOTE: QKV are expected to be 4D tensors
|
||||
|
||||
batch_size = op.Shape(query, start=0, end=1) # [B]
|
||||
q_num_heads = op.Shape(query, start=1, end=2) # [Hq]
|
||||
kv_num_heads = op.Shape(key, start=1, end=2) # [Hk]
|
||||
qk_head_size = op.Shape(key, start=3, end=4) # [Dk]
|
||||
v_head_size = op.Shape(value, start=3, end=4) # [Dv]
|
||||
new_kv_seq_len = op.Shape(key, start=2, end=3) # [T]
|
||||
|
||||
interleave_dim = op.Div(q_num_heads, kv_num_heads) # Hq / Hk
|
||||
two = op.Constant(value_int=2)
|
||||
k_unsqueezed = op.Unsqueeze(key, two) # [B, Hk, 1, T, Dk]
|
||||
v_unsqueezed = op.Unsqueeze(value, two) # [B, Hv, 1, T, Dv]
|
||||
|
||||
k_expand_shape = op.Concat(
|
||||
batch_size, kv_num_heads, interleave_dim, new_kv_seq_len, qk_head_size, axis=0
|
||||
)
|
||||
k_expand = op.Expand(k_unsqueezed, k_expand_shape)
|
||||
v_expand_shape = op.Concat(
|
||||
batch_size, kv_num_heads, interleave_dim, new_kv_seq_len, v_head_size, axis=0
|
||||
)
|
||||
v_expand = op.Expand(v_unsqueezed, v_expand_shape)
|
||||
|
||||
k_attention_shape = op.Concat(
|
||||
batch_size, q_num_heads, new_kv_seq_len, qk_head_size, axis=0
|
||||
)
|
||||
v_attention_shape = op.Concat(
|
||||
batch_size, q_num_heads, new_kv_seq_len, v_head_size, axis=0
|
||||
)
|
||||
|
||||
expanded_key = op.Reshape(k_expand, k_attention_shape)
|
||||
expanded_value = op.Reshape(v_expand, v_attention_shape)
|
||||
|
||||
return expanded_key, expanded_value
|
||||
|
||||
|
||||
def _attention_scale(query: TFloat, op: Opset) -> TFloat:
|
||||
"""Calculate the scale factor for the attention result.
|
||||
|
||||
Args:
|
||||
query: Tensor of shape [..., L, E]
|
||||
|
||||
Returns:
|
||||
Scalar scale factor := 1 / math.sqrt(query.size(-1))
|
||||
"""
|
||||
q_shape = op.Shape(query)
|
||||
q_last_dim = op.Gather(q_shape, op.Constant(value_ints=[-1]))
|
||||
embedding_size = op.CastLike(q_last_dim, query)
|
||||
one = op.Constant(value_float=1.0)
|
||||
cast_one = op.CastLike(one, query)
|
||||
scale = op.Div(cast_one, op.Sqrt(embedding_size))
|
||||
return scale
|
||||
|
||||
|
||||
def _causal_attention_mask(query: TFloat, key: TFloat, op: Opset) -> TFloat:
|
||||
"""Create a causal mask for the given query and key tensors.
|
||||
|
||||
Equivalent to::
|
||||
mask = torch.ones(L, S, dtype=torch.bool).tril(diagonal=0)
|
||||
attn_mask = torch.zeros(L, S, dtype=torch.float)
|
||||
attn_mask = attn_mask.masked_fill(not mask, -float("inf"))
|
||||
|
||||
Args:
|
||||
query: Tensor of shape [..., L, E]
|
||||
key: Tensor of shape [..., S, E]
|
||||
|
||||
Returns:
|
||||
Tensor of shape [L, S]
|
||||
"""
|
||||
q_shape = op.Shape(query)
|
||||
k_shape = op.Shape(key)
|
||||
|
||||
target_length = op.Slice(
|
||||
q_shape, op.Constant(value_ints=[-2]), op.Constant(value_ints=[-1])
|
||||
)
|
||||
source_length = op.Slice(
|
||||
k_shape, op.Constant(value_ints=[-2]), op.Constant(value_ints=[-1])
|
||||
)
|
||||
# attn_mask = torch.ones(L, S) := {
|
||||
size = op.Concat(target_length, source_length, axis=0)
|
||||
attn_mask = op.Expand(op.Constant(value_float=1.0), size)
|
||||
# }
|
||||
attn_mask = op.Trilu(attn_mask, upper=0)
|
||||
# The causal mask has 0s in the lower triangle and -inf in the upper triangle.
|
||||
attn_mask = op.Where(
|
||||
op.Equal(attn_mask, op.Constant(value_float=0.0)),
|
||||
op.Constant(value_float=-float("inf")),
|
||||
op.Constant(value_float=0.0),
|
||||
)
|
||||
attn_mask = op.CastLike(attn_mask, query)
|
||||
return attn_mask
|
||||
|
||||
|
||||
def _aten_scaled_dot_product_attention_no_mask_onnx(
|
||||
query: TFloat,
|
||||
key: TFloat,
|
||||
value: TFloat,
|
||||
scale: TFloat,
|
||||
dropout_p: float,
|
||||
op: Opset,
|
||||
) -> TFloat:
|
||||
# Swap the last two axes of key
|
||||
key_last_dim = op.Shape(key, start=-1)
|
||||
key_second_last_dim = op.Shape(key, start=-2, end=-1)
|
||||
key_first_dims = op.Shape(key, end=-2)
|
||||
# Contract the dimensions that are not the last two so we can transpose
|
||||
# with a static permutation.
|
||||
key_squeezed_shape = op.Concat(
|
||||
op.Constant(value_ints=[-1]), key_second_last_dim, key_last_dim, axis=0
|
||||
)
|
||||
key_squeezed = op.Reshape(key, key_squeezed_shape)
|
||||
key_squeezed_transposed = op.Transpose(key_squeezed, perm=[0, 2, 1])
|
||||
key_transposed_shape = op.Concat(
|
||||
key_first_dims, key_last_dim, key_second_last_dim, axis=0
|
||||
)
|
||||
key_transposed = op.Reshape(key_squeezed_transposed, key_transposed_shape)
|
||||
|
||||
# https://github.com/pytorch/pytorch/blob/12da0c70378b5be9135c6fda62a9863bce4a4818/aten/src/ATen/native/transformers/attention.cpp#L653
|
||||
# Scale q, k before matmul for stability see https://tinyurl.com/sudb9s96 for math
|
||||
query_scaled = op.Mul(query, op.Sqrt(scale))
|
||||
key_transposed_scaled = op.Mul(
|
||||
key_transposed, op.CastLike(op.Sqrt(scale), key_transposed)
|
||||
)
|
||||
attn_weight = op.Softmax(
|
||||
op.MatMul(query_scaled, key_transposed_scaled),
|
||||
axis=-1,
|
||||
)
|
||||
attn_weight, _ = op.Dropout(attn_weight, dropout_p)
|
||||
return op.MatMul(attn_weight, value)
|
||||
|
||||
|
||||
def _aten_scaled_dot_product_attention_float_mask_onnx(
|
||||
query: TFloat,
|
||||
key: TFloat,
|
||||
value: TFloat,
|
||||
attn_mask: TFloat,
|
||||
scale: TFloat,
|
||||
dropout_p: float,
|
||||
op: Opset,
|
||||
) -> TFloat:
|
||||
# Swap the last two axes of key
|
||||
key_last_dim = op.Shape(key, start=-1)
|
||||
key_second_last_dim = op.Shape(key, start=-2, end=-1)
|
||||
key_first_dims = op.Shape(key, end=-2)
|
||||
# Contract the dimensions that are not the last two so we can transpose
|
||||
# with a static permutation.
|
||||
key_squeezed_shape = op.Concat(
|
||||
op.Constant(value_ints=[-1]), key_second_last_dim, key_last_dim, axis=0
|
||||
)
|
||||
key_squeezed = op.Reshape(key, key_squeezed_shape)
|
||||
key_squeezed_transposed = op.Transpose(key_squeezed, perm=[0, 2, 1])
|
||||
key_transposed_shape = op.Concat(
|
||||
key_first_dims, key_last_dim, key_second_last_dim, axis=0
|
||||
)
|
||||
key_transposed = op.Reshape(key_squeezed_transposed, key_transposed_shape)
|
||||
|
||||
# https://github.com/pytorch/pytorch/blob/12da0c70378b5be9135c6fda62a9863bce4a4818/aten/src/ATen/native/transformers/attention.cpp#L653
|
||||
# Scale q, k before matmul for stability see https://tinyurl.com/sudb9s96 for math
|
||||
query_scaled = op.Mul(query, op.Sqrt(scale))
|
||||
key_transposed_scaled = op.Mul(key_transposed, op.Sqrt(scale))
|
||||
attn_weight = op.Softmax(
|
||||
op.Add(op.MatMul(query_scaled, key_transposed_scaled), attn_mask),
|
||||
axis=-1,
|
||||
)
|
||||
attn_weight, _ = op.Dropout(attn_weight, dropout_p)
|
||||
return op.MatMul(attn_weight, value)
|
||||
+150
@@ -0,0 +1,150 @@
|
||||
"""Implementation for higher-order operators."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from onnxscript.ir import convenience as ir_convenience
|
||||
|
||||
import torch
|
||||
from torch.onnx._internal._lazy_import import onnx_ir as ir
|
||||
from torch.onnx._internal.exporter import _core
|
||||
from torch.onnx._internal.exporter._torchlib._torchlib_registry import onnx_impl
|
||||
from torch.onnx.ops import _symbolic_impl
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Sequence
|
||||
|
||||
|
||||
def _call_symbolic_op(
|
||||
op_type: str,
|
||||
domain: str,
|
||||
args: Sequence[ir.Value | None],
|
||||
kwargs: dict[str, int | float | str | bool | list[int] | list[float] | list[str]],
|
||||
dtypes: Sequence[int],
|
||||
version: int | None,
|
||||
metadata_props: dict[str, str] | None,
|
||||
) -> Sequence[ir.Value]:
|
||||
"""Call an operator with the given arguments and keyword arguments.
|
||||
|
||||
Arguments are always inputs, while keyword arguments are attributes.
|
||||
"""
|
||||
# This is a wrapper around the IR node creation that hooks into the _builder.OpRecorder
|
||||
# tracer so that all nodes created are recorded the same way as if we were to use
|
||||
# onnxscript ops directly.
|
||||
|
||||
if _core.current_tracer is None:
|
||||
raise AssertionError("current_tracer must be non-None")
|
||||
tracer = _core.current_tracer
|
||||
|
||||
inputs = list(args)
|
||||
|
||||
# If final inputs are None, strip them from the node inputs
|
||||
for input in reversed(inputs):
|
||||
if input is not None:
|
||||
break
|
||||
inputs.pop()
|
||||
|
||||
# Construct and filter out None attributes
|
||||
attributes = [
|
||||
attr
|
||||
for attr in ir_convenience.convert_attributes(kwargs) # type: ignore[arg-type]
|
||||
if attr.value is not None # type: ignore[union-attr]
|
||||
]
|
||||
tracer.nodes.append(
|
||||
node := ir.Node(
|
||||
domain,
|
||||
op_type,
|
||||
inputs=inputs,
|
||||
attributes=attributes,
|
||||
num_outputs=len(dtypes),
|
||||
version=version,
|
||||
metadata_props=metadata_props,
|
||||
)
|
||||
)
|
||||
# Set the dtypes for the outputs. We set them here because the graph builder
|
||||
# Uses PyTorch types which are sometimes inaccurate when they are ONNX only
|
||||
# types like float4e2m1.
|
||||
for value, dtype in zip(node.outputs, dtypes):
|
||||
value.dtype = ir.DataType(dtype)
|
||||
# The shape is set by the graph builder. We don't need to set it here.
|
||||
return node.outputs
|
||||
|
||||
|
||||
@onnx_impl(torch.ops.onnx_symbolic._symbolic.default, no_compile=True)
|
||||
def onnx_symbolic_symbolic(
|
||||
inputs: Sequence[ir.Value | None],
|
||||
op_type: str,
|
||||
onnx_dtype: int,
|
||||
*,
|
||||
shape: Sequence[int | ir.Value],
|
||||
attr_keys: Sequence[str],
|
||||
attr_types: Sequence[str],
|
||||
attr_pos: Sequence[tuple[int, int]],
|
||||
attr_ints: Sequence[int],
|
||||
attr_floats: Sequence[float],
|
||||
attr_strs: Sequence[str],
|
||||
metadata_props_keys: Sequence[str] = (),
|
||||
metadata_props_values: Sequence[str] = (),
|
||||
domain: str = "",
|
||||
version: int | None = None,
|
||||
) -> ir.Value:
|
||||
del shape # Unused. The shapes are set by the graph builder
|
||||
encoded = _symbolic_impl.EncodedAttrs(
|
||||
attr_keys=list(attr_keys),
|
||||
attr_types=list(attr_types),
|
||||
attr_pos=list(attr_pos),
|
||||
attr_ints=list(attr_ints),
|
||||
attr_floats=list(attr_floats),
|
||||
attr_strs=list(attr_strs),
|
||||
)
|
||||
attrs = encoded.to_dict()
|
||||
return _call_symbolic_op(
|
||||
op_type,
|
||||
domain,
|
||||
inputs,
|
||||
attrs,
|
||||
dtypes=[onnx_dtype],
|
||||
version=version,
|
||||
metadata_props=dict(zip(metadata_props_keys, metadata_props_values)),
|
||||
)[0]
|
||||
|
||||
|
||||
@onnx_impl(torch.ops.onnx_symbolic._symbolic_multi_out.default, no_compile=True)
|
||||
def onnx_symbolic_symbolic_multi_out(
|
||||
inputs: Sequence[ir.Value | None],
|
||||
op_type: str,
|
||||
onnx_dtypes: Sequence[int],
|
||||
*,
|
||||
shapes: Sequence[Sequence[int | ir.Value]],
|
||||
attr_keys: Sequence[str],
|
||||
attr_types: Sequence[str],
|
||||
attr_pos: Sequence[tuple[int, int]],
|
||||
attr_ints: Sequence[int],
|
||||
attr_floats: Sequence[float],
|
||||
attr_strs: Sequence[str],
|
||||
metadata_props_keys: Sequence[str] = (),
|
||||
metadata_props_values: Sequence[str] = (),
|
||||
domain: str = "",
|
||||
version: int | None = None,
|
||||
) -> Sequence[ir.Value]:
|
||||
del shapes # Unused. The shapes are set by the graph builder
|
||||
encoded = _symbolic_impl.EncodedAttrs(
|
||||
attr_keys=list(attr_keys),
|
||||
attr_types=list(attr_types),
|
||||
attr_pos=list(attr_pos),
|
||||
attr_ints=list(attr_ints),
|
||||
attr_floats=list(attr_floats),
|
||||
attr_strs=list(attr_strs),
|
||||
)
|
||||
attrs = encoded.to_dict()
|
||||
return _call_symbolic_op(
|
||||
op_type,
|
||||
domain,
|
||||
inputs,
|
||||
attrs,
|
||||
dtypes=onnx_dtypes,
|
||||
version=version,
|
||||
metadata_props=dict(zip(metadata_props_keys, metadata_props_values)),
|
||||
)
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
"""Implementation for torch.sym* ops."""
|
||||
|
||||
# mypy: disable-error-code="misc,arg-type,type-arg,valid-type,assignment,return-value,type-var,operator,no-untyped-def,index"
|
||||
# pyrefly: ignore-errors
|
||||
# ruff: noqa: TCH001,TCH002,TC003
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Sequence
|
||||
|
||||
from onnxscript.onnx_opset import opset18 as op
|
||||
|
||||
import torch
|
||||
from torch.onnx._internal.exporter._torchlib._tensor_typing import (
|
||||
BOOL,
|
||||
FLOAT,
|
||||
IntType,
|
||||
TensorType,
|
||||
TTensor,
|
||||
)
|
||||
from torch.onnx._internal.exporter._torchlib._torchlib_registry import onnx_impl
|
||||
|
||||
|
||||
@onnx_impl(torch.sym_float, trace_only=True)
|
||||
def sym_float(self: TensorType) -> FLOAT:
|
||||
"""sym_float(SymInt self) -> SymFloat"""
|
||||
return op.Cast(self, to=FLOAT.dtype)
|
||||
|
||||
|
||||
@onnx_impl(torch.sym_max, trace_only=True)
|
||||
def sym_max(x: IntType, y: IntType) -> IntType:
|
||||
"""sym_max(SymInt x, SymInt y) -> SymInt"""
|
||||
return op.Max(x, y)
|
||||
|
||||
|
||||
@onnx_impl(torch.sym_min, trace_only=True)
|
||||
def sym_min(x: IntType, y: IntType) -> IntType:
|
||||
"""sym_min(SymInt x, SymInt y) -> SymInt"""
|
||||
return op.Min(x, y)
|
||||
|
||||
|
||||
@onnx_impl(torch.sym_not, trace_only=True)
|
||||
def sym_not(self: BOOL) -> BOOL:
|
||||
"""sym_not(SymBool self) -> SymBool"""
|
||||
return op.Not(self)
|
||||
|
||||
|
||||
@onnx_impl(torch.sym_sum, trace_only=True)
|
||||
def sym_sum(args: Sequence[IntType]) -> IntType:
|
||||
"""sym_sum(SymInt[] args) -> SymInt"""
|
||||
if len(args) == 0:
|
||||
return op.Constant(value_int=0)
|
||||
if len(args) == 1:
|
||||
return args[0]
|
||||
result = op.Add(args[0], args[1])
|
||||
for i in range(2, len(args)):
|
||||
result = op.Add(result, args[i])
|
||||
return result
|
||||
|
||||
|
||||
@onnx_impl(torch.sym_ite, trace_only=True)
|
||||
def sym_ite(b: BOOL, t: TTensor, f: TTensor) -> TTensor:
|
||||
"""sym_ite(SymBool b, Tensor t, Tensor f) -> Tensor"""
|
||||
return op.Where(b, t, f)
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
import numpy as np
|
||||
|
||||
import torch
|
||||
|
||||
|
||||
def unpack_float4x2_as_uint8(tensor: torch.Tensor) -> np.ndarray:
|
||||
"""Convert a float4x2 tensor to unpacked uint8 np array."""
|
||||
if tensor.dtype != torch.float4_e2m1fn_x2:
|
||||
raise AssertionError(f"Expected float4_e2m1fn_x2, got {tensor.dtype}")
|
||||
data = tensor.view(torch.uint8).numpy(force=True).flatten()
|
||||
result_size = tensor.numel() * 2
|
||||
result = np.empty([result_size], dtype=np.uint8)
|
||||
array_low = data & np.uint8(0x0F)
|
||||
array_high = data & np.uint8(0xF0)
|
||||
array_high >>= np.uint8(4)
|
||||
result[0::2] = array_low
|
||||
result[1::2] = array_high
|
||||
result.resize(get_float4_shape(tensor), refcheck=False)
|
||||
return result
|
||||
|
||||
|
||||
def get_float4_shape(tensor: torch.Tensor) -> tuple[int, ...]:
|
||||
"""Get the shape of an unpacked float4 tensor.
|
||||
|
||||
The float4_e2m1fn_x2 type is a shell type described in
|
||||
https://github.com/pytorch/pytorch/issues/146414.
|
||||
|
||||
the shell dtype is takes up 1 byte per element and semantically represents
|
||||
two fp4 values packed into 1 byte. Semantically it represents (*tensor.shape[:-1], tensor.shape[-1]*2)
|
||||
fp4 elements.
|
||||
"""
|
||||
if tensor.dtype != torch.float4_e2m1fn_x2:
|
||||
raise AssertionError(f"Expected float4_e2m1fn_x2, got {tensor.dtype}")
|
||||
return (*tensor.shape[:-1], tensor.shape[-1] * 2)
|
||||
+344
@@ -0,0 +1,344 @@
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
__all__ = [
|
||||
"VerificationInfo",
|
||||
"verify_onnx_program",
|
||||
]
|
||||
|
||||
import dataclasses
|
||||
import logging
|
||||
import math
|
||||
from typing import Any, TYPE_CHECKING
|
||||
|
||||
import torch
|
||||
from torch.utils import _pytree
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from torch.onnx._internal._lazy_import import onnx_ir as ir
|
||||
from torch.onnx._internal.exporter import _onnx_program
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class VerificationInfo:
|
||||
"""Verification information for a value in the ONNX program.
|
||||
|
||||
This class contains the maximum absolute difference, maximum relative difference,
|
||||
and histograms of absolute and relative differences between the expected and actual
|
||||
values. It also includes the expected and actual data types.
|
||||
|
||||
The histograms are represented as tuples of tensors, where the first tensor is the
|
||||
histogram counts and the second tensor is the bin edges.
|
||||
|
||||
Attributes:
|
||||
name: The name of the value (output or intermediate).
|
||||
max_abs_diff: The maximum absolute difference between the expected and actual values.
|
||||
max_rel_diff: The maximum relative difference between the expected and actual values.
|
||||
abs_diff_hist: A tuple of tensors representing the histogram of absolute differences.
|
||||
The first tensor is the histogram counts and the second tensor is the bin edges.
|
||||
rel_diff_hist: A tuple of tensors representing the histogram of relative differences.
|
||||
The first tensor is the histogram counts and the second tensor is the bin edges.
|
||||
expected_dtype: The data type of the expected value.
|
||||
actual_dtype: The data type of the actual value.
|
||||
"""
|
||||
|
||||
name: str
|
||||
max_abs_diff: float
|
||||
max_rel_diff: float
|
||||
abs_diff_hist: tuple[torch.Tensor, torch.Tensor]
|
||||
rel_diff_hist: tuple[torch.Tensor, torch.Tensor]
|
||||
expected_dtype: torch.dtype
|
||||
actual_dtype: torch.dtype
|
||||
# NOTE: We don't need to include shape because the expected shape is already known
|
||||
# and checked by the runtime
|
||||
|
||||
@classmethod
|
||||
def from_tensors(
|
||||
cls,
|
||||
name: str,
|
||||
expected: torch.Tensor | float | int | bool,
|
||||
actual: torch.Tensor | float | int | bool,
|
||||
) -> VerificationInfo:
|
||||
"""Create a VerificationInfo object from two tensors.
|
||||
|
||||
Args:
|
||||
name: The name of the value.
|
||||
expected: The expected tensor.
|
||||
actual: The actual tensor.
|
||||
|
||||
Returns:
|
||||
VerificationInfo: The VerificationInfo object.
|
||||
"""
|
||||
if not isinstance(expected, torch.Tensor):
|
||||
expected = torch.tensor(expected)
|
||||
if not isinstance(actual, torch.Tensor):
|
||||
actual = torch.tensor(actual)
|
||||
|
||||
max_abs_diff, max_rel_diff, abs_diff, rel_diff = _compare_tensors(
|
||||
expected, actual
|
||||
)
|
||||
bins = torch.tensor(
|
||||
[0.0, 1e-6, 1e-5, 1e-4, 1e-3, 1e-2, 1e-1, 1.0, 10, 1000000],
|
||||
dtype=torch.float,
|
||||
)
|
||||
abs_diff_hist = torch.histogram(abs_diff.float(), bins=bins)
|
||||
rel_diff_hist = torch.histogram(rel_diff.float(), bins=bins)
|
||||
return cls(
|
||||
name=name,
|
||||
max_abs_diff=max_abs_diff,
|
||||
max_rel_diff=max_rel_diff,
|
||||
abs_diff_hist=abs_diff_hist,
|
||||
rel_diff_hist=rel_diff_hist,
|
||||
expected_dtype=expected.dtype,
|
||||
actual_dtype=actual.dtype,
|
||||
)
|
||||
|
||||
def asdict(self) -> dict[str, Any]:
|
||||
"""Convert the VerificationInfo object to a dictionary.
|
||||
|
||||
Returns:
|
||||
A dictionary representation of the VerificationInfo object.
|
||||
"""
|
||||
return {
|
||||
"name": self.name,
|
||||
"max_abs_diff": self.max_abs_diff,
|
||||
"max_rel_diff": self.max_rel_diff,
|
||||
"abs_diff_hist": [
|
||||
self.abs_diff_hist[0].tolist(),
|
||||
self.abs_diff_hist[1].tolist(),
|
||||
],
|
||||
"rel_diff_hist": [
|
||||
self.rel_diff_hist[0].tolist(),
|
||||
self.rel_diff_hist[1].tolist(),
|
||||
],
|
||||
"expected_dtype": str(self.expected_dtype),
|
||||
"actual_dtype": str(self.actual_dtype),
|
||||
}
|
||||
|
||||
|
||||
def _compare_tensors(
|
||||
expected: torch.Tensor,
|
||||
actual: torch.Tensor,
|
||||
) -> tuple[float, float, torch.Tensor, torch.Tensor]:
|
||||
# Move tensors to the same device
|
||||
expected = expected.detach().cpu()
|
||||
actual = actual.detach().cpu()
|
||||
if expected.numel() == 0 or actual.numel() == 0:
|
||||
return math.inf, math.inf, torch.tensor(math.inf), torch.tensor(math.inf)
|
||||
if expected.dtype == torch.bool:
|
||||
expected = expected.to(torch.float32)
|
||||
actual = actual.to(torch.float32)
|
||||
if torch.is_complex(expected):
|
||||
expected = torch.view_as_real(expected)
|
||||
abs_diff = torch.abs(expected - actual)
|
||||
eps = 1e-7
|
||||
normalizer = torch.abs(expected) + eps
|
||||
rel_diff = abs_diff / normalizer
|
||||
|
||||
max_absolute_difference = abs_diff.max().item()
|
||||
max_relative_difference = rel_diff.max().item()
|
||||
|
||||
return max_absolute_difference, max_relative_difference, abs_diff, rel_diff
|
||||
|
||||
|
||||
def verify_onnx_program(
|
||||
onnx_program: _onnx_program.ONNXProgram,
|
||||
args: tuple[Any, ...] | None = None,
|
||||
kwargs: dict[str, Any] | None = None,
|
||||
compare_intermediates: bool = False,
|
||||
) -> list[VerificationInfo]:
|
||||
"""Verify the ONNX model by comparing the values with the expected values from ExportedProgram.
|
||||
|
||||
Args:
|
||||
onnx_program: The ONNX program to verify.
|
||||
args: The input arguments for the model.
|
||||
kwargs: The keyword arguments for the model.
|
||||
compare_intermediates: Whether to verify intermediate values. This is going
|
||||
to take longer time, so it is disabled by default.
|
||||
|
||||
Returns:
|
||||
VerificationInfo objects containing the verification information for each value.
|
||||
"""
|
||||
exported_program = onnx_program.exported_program
|
||||
if exported_program is None:
|
||||
raise ValueError(
|
||||
"The ONNX program does not contain an exported_program. "
|
||||
"Please provide an exported_program to verify the ONNX program."
|
||||
)
|
||||
if args is None and kwargs is None:
|
||||
# User did not provide example inputs, use the default example inputs
|
||||
if exported_program.example_inputs is None:
|
||||
raise ValueError(
|
||||
"No example inputs provided and the exported_program does not contain example inputs. "
|
||||
"Please provide arguments to verify the ONNX program."
|
||||
)
|
||||
args, kwargs = exported_program.example_inputs
|
||||
if args is None:
|
||||
args = ()
|
||||
if kwargs is None:
|
||||
kwargs = {}
|
||||
|
||||
# Flatten args for ONNX program and the VerificationInterpreter
|
||||
flat_args, _ = exported_program._get_flat_args_with_check(args, kwargs)
|
||||
|
||||
if not compare_intermediates:
|
||||
# Compare the output values
|
||||
torch_outputs, _ = _pytree.tree_flatten(
|
||||
exported_program.module()(*args, **kwargs)
|
||||
)
|
||||
onnx_outputs = onnx_program(*flat_args)
|
||||
results = []
|
||||
for torch_output, onnx_output, output_val in zip(
|
||||
torch_outputs, onnx_outputs, onnx_program.model.graph.outputs
|
||||
):
|
||||
results.append(
|
||||
VerificationInfo.from_tensors(
|
||||
name=str(output_val.name),
|
||||
expected=torch_output,
|
||||
actual=onnx_output,
|
||||
)
|
||||
)
|
||||
return results
|
||||
|
||||
# Use the _VerificationInterpreter to get the intermediate values
|
||||
# By design the output values are included too
|
||||
interpreter = _VerificationInterpreter(onnx_program)
|
||||
interpreter.run(*flat_args)
|
||||
|
||||
return interpreter.verification_infos
|
||||
|
||||
|
||||
def _create_value_mapping(graph: ir.Graph) -> dict[str, ir.Value]:
|
||||
"""Return a dictionary mapping names to values in the graph.
|
||||
|
||||
The mapping does not include values from subgraphs.
|
||||
|
||||
Args:
|
||||
graph: The graph to extract the mapping from.
|
||||
|
||||
Returns:
|
||||
A dictionary mapping names to values.
|
||||
"""
|
||||
values: dict[str, ir.Value] = {}
|
||||
values.update(graph.initializers)
|
||||
# The names of the values can be None or "", which we need to exclude
|
||||
for input in graph.inputs:
|
||||
if not input.name:
|
||||
continue
|
||||
values[input.name] = input
|
||||
for node in graph:
|
||||
for value in node.outputs:
|
||||
if not value.name:
|
||||
continue
|
||||
values[value.name] = value
|
||||
return values
|
||||
|
||||
|
||||
class _VerificationInterpreter(torch.fx.Interpreter):
|
||||
"""Interpreter for verifying converted ONNX model accuracy by comparing intermediate values.
|
||||
|
||||
To compare models, first initialize the interpreter with an ONNX program.
|
||||
Then, call the :meth:`run` method with the input arguments to execute the model.
|
||||
The :meth:`run` method will execute the model and populate the
|
||||
:attr:`verification_infos` attribute with the verification information for each value.
|
||||
|
||||
::
|
||||
onnx_program = torch.onnx.export(model, args, dynamo=True)
|
||||
interpreter = _VerificationInterpreter(onnx_program)
|
||||
interpreter.run(*args)
|
||||
verification_infos = interpreter.verification_infos
|
||||
for info in verification_infos:
|
||||
print("value name:", info.name, info)
|
||||
|
||||
The verification information includes the maximum absolute difference, maximum relative
|
||||
difference, and histograms of absolute and relative differences between the expected
|
||||
and actual values. See :class:`VerificationInfo` for more details.
|
||||
|
||||
Attributes:
|
||||
verification_infos: A list of verification information for each value.
|
||||
It is populated when the `run` method is called.
|
||||
"""
|
||||
|
||||
def __init__(self, onnx_program: torch.onnx.ONNXProgram) -> None:
|
||||
"""Initialize the _VerificationInterpreter with an ONNX program.
|
||||
|
||||
Args:
|
||||
onnx_program: The ONNX program to verify.
|
||||
"""
|
||||
if onnx_program.exported_program is None:
|
||||
raise ValueError(
|
||||
"The ONNX program does not contain an exported_program. "
|
||||
"Please provide an exported_program to verify the ONNX program."
|
||||
)
|
||||
super().__init__(onnx_program.exported_program.module())
|
||||
self._onnx_program = onnx_program
|
||||
self._onnx_values = _create_value_mapping(onnx_program.model.graph)
|
||||
self._args: tuple[Any, ...] = ()
|
||||
self.verification_infos: list[VerificationInfo] = []
|
||||
|
||||
def run(
|
||||
self,
|
||||
*args: Any,
|
||||
initial_env: dict[torch.fx.Node, Any] | None = None,
|
||||
enable_io_processing: bool = True,
|
||||
) -> Any:
|
||||
"""Run the interpreter with the given input arguments.
|
||||
|
||||
This method executes the model and populates the :attr:`verification_infos` attribute
|
||||
with the verification information for each value.
|
||||
|
||||
Args:
|
||||
args: The input arguments for the model.
|
||||
initial_env: The initial environment for the interpreter.
|
||||
enable_io_processing: Whether to enable IO processing.
|
||||
|
||||
Returns:
|
||||
Any: The result of executing the model.
|
||||
"""
|
||||
self.verification_infos = []
|
||||
self._args = args
|
||||
return super().run(
|
||||
*args,
|
||||
initial_env=initial_env,
|
||||
enable_io_processing=enable_io_processing,
|
||||
)
|
||||
|
||||
def run_node(self, n: torch.fx.Node) -> Any:
|
||||
result = super().run_node(n)
|
||||
if n.op != "call_function":
|
||||
return result
|
||||
node_name = n.name
|
||||
if node_name not in self._onnx_values:
|
||||
return result
|
||||
try:
|
||||
(onnx_result,) = self._onnx_program.compute_values([node_name], self._args)
|
||||
except Exception:
|
||||
logger.warning(
|
||||
"Failed to compute value for node %s", node_name, exc_info=True
|
||||
)
|
||||
return result
|
||||
info = VerificationInfo.from_tensors(
|
||||
name=node_name,
|
||||
expected=result,
|
||||
actual=onnx_result,
|
||||
)
|
||||
self.verification_infos.append(info)
|
||||
if info.max_abs_diff > 0.01 or info.max_rel_diff > 0.1:
|
||||
logger.warning(
|
||||
"Verification info for node %s: max_abs_diff: %s, max_rel_diff: %s",
|
||||
node_name,
|
||||
info.max_abs_diff,
|
||||
info.max_rel_diff,
|
||||
)
|
||||
else:
|
||||
logger.info(
|
||||
"Verification info for node %s: max_abs_diff: %s, max_rel_diff: %s",
|
||||
node_name,
|
||||
info.max_abs_diff,
|
||||
info.max_rel_diff,
|
||||
)
|
||||
return result
|
||||
@@ -0,0 +1,237 @@
|
||||
# mypy: allow-untyped-defs
|
||||
from __future__ import annotations
|
||||
|
||||
import abc
|
||||
import contextlib
|
||||
import dataclasses
|
||||
import difflib
|
||||
import io
|
||||
import sys
|
||||
from typing import Any, TYPE_CHECKING
|
||||
|
||||
import torch
|
||||
import torch.fx
|
||||
from torch._subclasses.fake_tensor import unset_fake_temporarily
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Callable
|
||||
|
||||
from torch._subclasses import fake_tensor
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class PackageInfo:
|
||||
package_name: str
|
||||
version: str | None
|
||||
commit_hash: str | None
|
||||
|
||||
def to_onnx_domain_string(self) -> str:
|
||||
return ".".join(
|
||||
filter(None, ("pkg", self.package_name, self.version, self.commit_hash))
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_python_class(cls, python_class_name: type | str) -> PackageInfo:
|
||||
if isinstance(python_class_name, type):
|
||||
python_class_name = python_class_name.__module__
|
||||
package_name = python_class_name.split(".")[0]
|
||||
package = __import__(package_name)
|
||||
version = getattr(package, "__version__", None)
|
||||
# TODO: Figure out how to retrieve commit hash.
|
||||
commit_hash = None
|
||||
return cls(package_name, version, commit_hash)
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class GraphModuleOnnxMeta:
|
||||
package_info: PackageInfo
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def _patch_difflib_sequence_matcher_init():
|
||||
"""Context patching `difflib.SequenceMatcher` for fx readable graph.
|
||||
|
||||
Under this context, the `autojunk` argument of `difflib.SequenceMatcher` will always
|
||||
be considered as `False`. This is to prevent `difflib.SequenceMatcher` recognizing
|
||||
stacktrace messages in fx readable graph as junk, as these messages tend to be long (>200)
|
||||
and repeat multiple times, which falls under the junk filter criteria.
|
||||
|
||||
`difflib.SequenceMatcher` is used underneath by all sorts of diffing functions
|
||||
in `difflib`, including `difflib.unified_diff`, `difflib.ndiff`, `difflib.context_diff`.
|
||||
Unfortunately, there is no way to pass `autojunk` argument to these functions, and
|
||||
they all default to `True`. This context patching will affect all of them.
|
||||
|
||||
`Reference: Automatic junk heuristic <https://docs.python.org/3/library/difflib.html>`_
|
||||
"""
|
||||
original_init = difflib.SequenceMatcher.__init__
|
||||
|
||||
def patched_init(self, isjunk=None, a="", b="", autojunk=True) -> None:
|
||||
original_init(self, isjunk, a, b, autojunk=False)
|
||||
|
||||
difflib.SequenceMatcher.__init__ = patched_init # type: ignore[assignment]
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
difflib.SequenceMatcher.__init__ = original_init # type: ignore[assignment]
|
||||
|
||||
|
||||
def _unified_diff(a: str, b: str) -> str:
|
||||
"""Return a string containing the unified diff of two strings.
|
||||
|
||||
This function calls a patched version of `difflib.unified_diff` with `autojunk` set
|
||||
to `False` for `difflib.SequenceMatcher` class. More details can be found in
|
||||
`_patch_difflib_sequence_matcher_init` function.
|
||||
|
||||
Args:
|
||||
a: The first string.
|
||||
b: The second string.
|
||||
|
||||
Returns:
|
||||
The unified diff of the two strings. If there is no diff, return "<no diff>".
|
||||
|
||||
Example::
|
||||
|
||||
>>> a = '''class GraphModule(torch.nn.Module):
|
||||
... def forward(self, input_ids : torch.Tensor, attention_mask : torch.Tensor):
|
||||
... # File: /modeling.py:770, code: input_ids = input_ids.view(-1, input_shape[-1])
|
||||
... view = input_ids.view(-1, 3); input_ids = None
|
||||
... '''
|
||||
>>> b = '''class <lambda>(torch.nn.Module):
|
||||
... def forward(self, input_ids: i64[1, 3], attention_mask: i64[1, 3]):
|
||||
... # File: /modeling.py:770, code: input_ids = input_ids.view(-1, input_shape[-1])
|
||||
... view: i64[1, 3] = torch.ops.aten.view.default(input_ids, [-1, 3]); input_ids = None
|
||||
... '''
|
||||
>>> print(_unified_diff(a, b))
|
||||
---
|
||||
+++
|
||||
@@ -1,4 +1,4 @@
|
||||
-class GraphModule(torch.nn.Module):
|
||||
- def forward(self, input_ids : torch.Tensor, attention_mask : torch.Tensor):
|
||||
+class <lambda>(torch.nn.Module):
|
||||
+ def forward(self, input_ids: i64[1, 3], attention_mask: i64[1, 3]):
|
||||
# File: /modeling.py:770, code: input_ids = input_ids.view(-1, input_shape[-1])
|
||||
- view = input_ids.view(-1, 3); input_ids = None
|
||||
+ view: i64[1, 3] = torch.ops.aten.view.default(input_ids, [-1, 3]); input_ids = None
|
||||
"""
|
||||
|
||||
a_list = a.splitlines(keepends=True)
|
||||
b_list = b.splitlines(keepends=True)
|
||||
|
||||
with _patch_difflib_sequence_matcher_init():
|
||||
# Set `n` to `sys.maxsize` to show entire graph when there is a diff.
|
||||
diff = "".join(difflib.unified_diff(a_list, b_list, n=sys.maxsize))
|
||||
|
||||
if not diff:
|
||||
return "<no diff>"
|
||||
return diff
|
||||
|
||||
|
||||
def _transform_diagnose_call_message_formatter(
|
||||
run: Callable,
|
||||
self: Transform,
|
||||
*args: Any,
|
||||
**kwargs: Any,
|
||||
) -> str:
|
||||
return f"Running {self.__class__.__name__} pass. "
|
||||
|
||||
|
||||
def maybe_fx_graph_tabular(graph: torch.fx.Graph) -> str | None:
|
||||
"""Return the Graph nodes in tabular format. Equivalent to stdout of `graph.print_tabular()`.
|
||||
If `tabulate` is not installed, return `None`.
|
||||
|
||||
Args:
|
||||
graph: The Graph to print.
|
||||
|
||||
Returns:
|
||||
The Graph printed in a tabular format. None if `tabulate` is not installed.
|
||||
"""
|
||||
f = io.StringIO()
|
||||
with contextlib.redirect_stdout(f):
|
||||
try:
|
||||
graph.print_tabular()
|
||||
except ImportError:
|
||||
return None
|
||||
return f.getvalue()
|
||||
|
||||
|
||||
class Transform(abc.ABC):
|
||||
"""Base class for FX graph transformations to be used by FX-ONNX exporter.
|
||||
|
||||
Similar to `FX Interpreter <https://pytorch.org/docs/stable/fx.html#torch.fx.Interpreter>`_,
|
||||
specializations of this class execute the FX graph Node-by-Node.
|
||||
Methods in the `Transform` class can be overridden to customize the behavior of the model.
|
||||
This pattern can be useful for many things, including writing code transformations as well as analysis passes.
|
||||
|
||||
The following methods can be overridden::
|
||||
|
||||
_run()
|
||||
+-- run_node()
|
||||
+-- placeholder()
|
||||
+-- get_attr()
|
||||
+-- call_function()
|
||||
+-- call_method()
|
||||
+-- call_module()
|
||||
+-- output()
|
||||
|
||||
One important aspect to note is that if the transformation modifies the model input and/or output signature,
|
||||
(e.g. additional inputs/outputs are added to the model), :class:`InputAdaptStep` and/or :class:`OutputAdaptStep`
|
||||
are needed to reconcile :attr:`ONNXProgram.model_proto`.
|
||||
That is, the model signature and the model representation must match.
|
||||
|
||||
TODO(bowbao): Add more overridable methods in call hierarchy
|
||||
TODO(bowbao): Create an example once more overridable methods are added.
|
||||
"""
|
||||
|
||||
module: torch.fx.GraphModule
|
||||
"""The module to be transformed."""
|
||||
|
||||
fake_mode: fake_tensor.FakeTensorMode | None
|
||||
"""The existing fake mode detected from `self.module`."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
module: torch.fx.GraphModule,
|
||||
) -> None:
|
||||
"""Initialize the transform.
|
||||
|
||||
Args:
|
||||
module: The module to be transformed.
|
||||
"""
|
||||
self.module = module
|
||||
self.fake_mode = self._detect_fake_mode()
|
||||
|
||||
def _detect_fake_mode(self) -> fake_tensor.FakeTensorMode | None:
|
||||
"""Detect fake mode from the graph.
|
||||
|
||||
Scan through all nodes in graph and their meta['val'] to detect fake mode.
|
||||
"""
|
||||
fake_tensors = [node.meta.get("val") for node in self.module.graph.nodes]
|
||||
with unset_fake_temporarily():
|
||||
return torch._dynamo.utils.detect_fake_mode(fake_tensors)
|
||||
|
||||
def _maybe_fakefy_args(
|
||||
self, fake_mode: fake_tensor.FakeTensorMode | None, *args: Any
|
||||
) -> tuple[Any, ...]:
|
||||
if fake_mode is None:
|
||||
return args
|
||||
# NB: This should hit the cache if tensors were fakefied before.
|
||||
# E.g., when the fx graph is produced by Dynamo.
|
||||
return tuple(
|
||||
fake_mode.from_tensor(t) if isinstance(t, torch.Tensor) else t for t in args
|
||||
)
|
||||
|
||||
@abc.abstractmethod
|
||||
def _run(self, *args, **kwargs) -> torch.fx.GraphModule: ...
|
||||
|
||||
def run(self, *args, **kwargs) -> torch.fx.GraphModule:
|
||||
"""Run the transform on `self.module`.
|
||||
|
||||
Note that this method may or may not mutate `self.module`, and the returned
|
||||
`GraphModule` could be either `self.module` or a new `GraphModule`.
|
||||
|
||||
Args:
|
||||
*args: Positional arguments for `self.module` to run.
|
||||
**kwargs: Keyword arguments for `self.module` to run.
|
||||
"""
|
||||
return self._run(*args, **kwargs)
|
||||
@@ -0,0 +1,6 @@
|
||||
from .type_promotion import InsertTypePromotion
|
||||
|
||||
|
||||
__all__ = [
|
||||
"InsertTypePromotion",
|
||||
]
|
||||
+1682
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,38 @@
|
||||
# mypy: allow-untyped-defs
|
||||
"""Utilities for converting and operating on ONNX and torch types."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
from typing_extensions import TypeIs
|
||||
|
||||
import torch
|
||||
|
||||
|
||||
def is_torch_symbolic_type(
|
||||
value: Any,
|
||||
) -> TypeIs[torch.SymBool | torch.SymInt | torch.SymFloat]:
|
||||
return isinstance(value, (torch.SymBool, torch.SymInt, torch.SymFloat))
|
||||
|
||||
|
||||
def from_scalar_type_to_torch_dtype(scalar_type: type) -> torch.dtype | None:
|
||||
return _SCALAR_TYPE_TO_TORCH_DTYPE.get(scalar_type)
|
||||
|
||||
|
||||
_PYTHON_TYPE_TO_TORCH_DTYPE = {
|
||||
bool: torch.bool,
|
||||
int: torch.int64,
|
||||
float: torch.float32,
|
||||
complex: torch.complex64,
|
||||
}
|
||||
|
||||
_SYM_TYPE_TO_TORCH_DTYPE = {
|
||||
torch.SymInt: torch.int64,
|
||||
torch.SymFloat: torch.float32,
|
||||
torch.SymBool: torch.bool,
|
||||
}
|
||||
|
||||
_SCALAR_TYPE_TO_TORCH_DTYPE: dict[type, torch.dtype] = {
|
||||
**_PYTHON_TYPE_TO_TORCH_DTYPE,
|
||||
**_SYM_TYPE_TO_TORCH_DTYPE, # type: ignore[dict-item]
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
"""Experimental classes and functions used by ONNX export."""
|
||||
|
||||
import dataclasses
|
||||
from collections.abc import Mapping, Sequence
|
||||
|
||||
import torch
|
||||
import torch._C._onnx as _C_onnx
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class ExportOptions:
|
||||
"""Arguments used by :func:`torch.onnx.export`."""
|
||||
|
||||
# TODO(justinchuby): Deprecate and remove this class.
|
||||
|
||||
export_params: bool = True
|
||||
verbose: bool = False
|
||||
training: _C_onnx.TrainingMode = _C_onnx.TrainingMode.EVAL
|
||||
input_names: Sequence[str] | None = None
|
||||
output_names: Sequence[str] | None = None
|
||||
operator_export_type: _C_onnx.OperatorExportTypes = _C_onnx.OperatorExportTypes.ONNX
|
||||
opset_version: int | None = None
|
||||
do_constant_folding: bool = True
|
||||
dynamic_axes: Mapping[str, Mapping[int, str] | Sequence[int]] | None = None
|
||||
keep_initializers_as_inputs: bool | None = None
|
||||
custom_opsets: Mapping[str, int] | None = None
|
||||
export_modules_as_functions: bool | set[type[torch.nn.Module]] = False
|
||||
+81
@@ -0,0 +1,81 @@
|
||||
"""Globals used internally by the ONNX exporter.
|
||||
|
||||
Do not use this module outside of `torch.onnx` and its tests.
|
||||
|
||||
Be very judicious when adding any new global variables. Do not create new global
|
||||
variables unless they are absolutely necessary.
|
||||
"""
|
||||
|
||||
import torch._C._onnx as _C_onnx
|
||||
|
||||
# This module should only depend on _constants and nothing else in torch.onnx to keep
|
||||
# dependency direction clean.
|
||||
from torch.onnx import _constants
|
||||
|
||||
|
||||
class _InternalGlobals:
|
||||
"""Globals used internally by ONNX exporter.
|
||||
|
||||
NOTE: Be very judicious when adding any new variables. Do not create new
|
||||
global variables unless they are absolutely necessary.
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._export_onnx_opset_version = _constants.ONNX_DEFAULT_OPSET
|
||||
self._training_mode: _C_onnx.TrainingMode = _C_onnx.TrainingMode.EVAL
|
||||
self._in_onnx_export: bool = False
|
||||
# Whether the user's model is training during export
|
||||
self.export_training: bool = False
|
||||
self.operator_export_type: _C_onnx.OperatorExportTypes = (
|
||||
_C_onnx.OperatorExportTypes.ONNX
|
||||
)
|
||||
self.onnx_shape_inference: bool = True
|
||||
self._autograd_inlining: bool = True
|
||||
|
||||
@property
|
||||
def training_mode(self) -> _C_onnx.TrainingMode:
|
||||
"""The training mode for the exporter."""
|
||||
return self._training_mode
|
||||
|
||||
@training_mode.setter
|
||||
def training_mode(self, training_mode: _C_onnx.TrainingMode) -> None:
|
||||
if not isinstance(training_mode, _C_onnx.TrainingMode):
|
||||
raise TypeError(
|
||||
"training_mode must be of type 'torch.onnx.TrainingMode'. This is "
|
||||
"likely a bug in torch.onnx."
|
||||
)
|
||||
self._training_mode = training_mode
|
||||
|
||||
@property
|
||||
def export_onnx_opset_version(self) -> int:
|
||||
"""Opset version used during export."""
|
||||
return self._export_onnx_opset_version
|
||||
|
||||
@export_onnx_opset_version.setter
|
||||
def export_onnx_opset_version(self, value: int) -> None:
|
||||
self._export_onnx_opset_version = value
|
||||
|
||||
@property
|
||||
def in_onnx_export(self) -> bool:
|
||||
"""Whether it is in the middle of ONNX export."""
|
||||
return self._in_onnx_export
|
||||
|
||||
@in_onnx_export.setter
|
||||
def in_onnx_export(self, value: bool) -> None:
|
||||
if type(value) is not bool:
|
||||
raise TypeError("in_onnx_export must be a boolean")
|
||||
self._in_onnx_export = value
|
||||
|
||||
@property
|
||||
def autograd_inlining(self) -> bool:
|
||||
"""Whether Autograd must be inlined."""
|
||||
return self._autograd_inlining
|
||||
|
||||
@autograd_inlining.setter
|
||||
def autograd_inlining(self, value: bool) -> None:
|
||||
if type(value) is not bool:
|
||||
raise TypeError("autograd_inlining must be a boolean")
|
||||
self._autograd_inlining = value
|
||||
|
||||
|
||||
GLOBALS = _InternalGlobals()
|
||||
+393
@@ -0,0 +1,393 @@
|
||||
# mypy: allow-untyped-defs
|
||||
"""Utilities for converting and operating on ONNX, JIT and torch types."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import enum
|
||||
import typing
|
||||
from typing import Literal
|
||||
|
||||
import torch
|
||||
from torch._C import _onnx as _C_onnx
|
||||
from torch.onnx import errors
|
||||
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
# Hack to help mypy to recognize torch._C.Value
|
||||
from torch import _C # noqa: F401
|
||||
|
||||
ScalarName = Literal[
|
||||
"Byte",
|
||||
"Char",
|
||||
"Double",
|
||||
"Float",
|
||||
"Half",
|
||||
"Int",
|
||||
"Long",
|
||||
"Short",
|
||||
"Bool",
|
||||
"ComplexHalf",
|
||||
"ComplexFloat",
|
||||
"ComplexDouble",
|
||||
"QInt8",
|
||||
"QUInt8",
|
||||
"QInt32",
|
||||
"BFloat16",
|
||||
"Float8E5M2",
|
||||
"Float8E4M3FN",
|
||||
"Float8E5M2FNUZ",
|
||||
"Float8E4M3FNUZ",
|
||||
"Undefined",
|
||||
]
|
||||
|
||||
TorchName = Literal[
|
||||
"bool",
|
||||
"uint8_t",
|
||||
"int8_t",
|
||||
"double",
|
||||
"float",
|
||||
"half",
|
||||
"int",
|
||||
"int64_t",
|
||||
"int16_t",
|
||||
"complex32",
|
||||
"complex64",
|
||||
"complex128",
|
||||
"qint8",
|
||||
"quint8",
|
||||
"qint32",
|
||||
"bfloat16",
|
||||
"float8_e5m2",
|
||||
"float8_e4m3fn",
|
||||
"float8_e5m2fnuz",
|
||||
"float8_e4m3fnuz",
|
||||
]
|
||||
|
||||
|
||||
class JitScalarType(enum.IntEnum):
|
||||
"""Scalar types defined in torch.
|
||||
|
||||
Use ``JitScalarType`` to convert from torch and JIT scalar types to ONNX scalar types.
|
||||
|
||||
Examples:
|
||||
>>> # xdoctest: +REQUIRES(env:TORCH_DOCTEST_ONNX)
|
||||
>>> # xdoctest: +IGNORE_WANT("win32 has different output")
|
||||
>>> JitScalarType.from_value(torch.ones(1, 2)).onnx_type()
|
||||
TensorProtoDataType.FLOAT
|
||||
|
||||
>>> JitScalarType.from_value(torch_c_value_with_type_float).onnx_type()
|
||||
TensorProtoDataType.FLOAT
|
||||
|
||||
>>> JitScalarType.from_dtype(torch.get_default_dtype).onnx_type()
|
||||
TensorProtoDataType.FLOAT
|
||||
|
||||
"""
|
||||
|
||||
# Order defined in https://github.com/pytorch/pytorch/blob/344defc9733a45fee8d0c4d3f5530f631e823196/c10/core/ScalarType.h
|
||||
UINT8 = 0
|
||||
INT8 = enum.auto() # 1
|
||||
INT16 = enum.auto() # 2
|
||||
INT = enum.auto() # 3
|
||||
INT64 = enum.auto() # 4
|
||||
HALF = enum.auto() # 5
|
||||
FLOAT = enum.auto() # 6
|
||||
DOUBLE = enum.auto() # 7
|
||||
COMPLEX32 = enum.auto() # 8
|
||||
COMPLEX64 = enum.auto() # 9
|
||||
COMPLEX128 = enum.auto() # 10
|
||||
BOOL = enum.auto() # 11
|
||||
QINT8 = enum.auto() # 12
|
||||
QUINT8 = enum.auto() # 13
|
||||
QINT32 = enum.auto() # 14
|
||||
BFLOAT16 = enum.auto() # 15
|
||||
FLOAT8E5M2 = enum.auto() # 16
|
||||
FLOAT8E4M3FN = enum.auto() # 17
|
||||
FLOAT8E5M2FNUZ = enum.auto() # 18
|
||||
FLOAT8E4M3FNUZ = enum.auto() # 19
|
||||
UNDEFINED = enum.auto() # 20
|
||||
|
||||
@classmethod
|
||||
def _from_name(cls, name: ScalarName | TorchName | str | None) -> JitScalarType:
|
||||
"""Convert a JIT scalar type or torch type name to ScalarType.
|
||||
|
||||
Note: DO NOT USE this API when `name` comes from a `torch._C.Value.type()` calls.
|
||||
A "RuntimeError: INTERNAL ASSERT FAILED at "../aten/src/ATen/core/jit_type_base.h" can
|
||||
be raised in several scenarios where shape info is not present.
|
||||
Instead use `from_value` API which is safer.
|
||||
|
||||
Args:
|
||||
name: JIT scalar type name (Byte) or torch type name (uint8_t).
|
||||
|
||||
Returns:
|
||||
JitScalarType
|
||||
|
||||
Raises:
|
||||
OnnxExporterError: if name is not a valid scalar type name or if it is None.
|
||||
"""
|
||||
if name is None:
|
||||
raise errors.OnnxExporterError("Scalar type name cannot be None")
|
||||
if valid_scalar_name(name):
|
||||
return _SCALAR_NAME_TO_TYPE[name] # type: ignore[index]
|
||||
if valid_torch_name(name):
|
||||
return _TORCH_NAME_TO_SCALAR_TYPE[name] # type: ignore[index]
|
||||
|
||||
raise errors.OnnxExporterError(f"Unknown torch or scalar type: '{name}'")
|
||||
|
||||
@classmethod
|
||||
def from_dtype(cls, dtype: torch.dtype | None) -> JitScalarType:
|
||||
"""Convert a torch dtype to JitScalarType.
|
||||
|
||||
Note: DO NOT USE this API when `dtype` comes from a `torch._C.Value.type()` calls.
|
||||
A "RuntimeError: INTERNAL ASSERT FAILED at "../aten/src/ATen/core/jit_type_base.h" can
|
||||
be raised in several scenarios where shape info is not present.
|
||||
Instead use `from_value` API which is safer.
|
||||
|
||||
Args:
|
||||
dtype: A torch.dtype to create a JitScalarType from
|
||||
|
||||
Returns:
|
||||
JitScalarType
|
||||
|
||||
Raises:
|
||||
OnnxExporterError: if dtype is not a valid torch.dtype or if it is None.
|
||||
"""
|
||||
if dtype not in _DTYPE_TO_SCALAR_TYPE:
|
||||
raise errors.OnnxExporterError(f"Unknown dtype: {dtype}")
|
||||
# pyrefly: ignore [bad-index]
|
||||
return _DTYPE_TO_SCALAR_TYPE[dtype]
|
||||
|
||||
@classmethod
|
||||
def from_onnx_type(
|
||||
cls, onnx_type: int | _C_onnx.TensorProtoDataType | None
|
||||
) -> JitScalarType:
|
||||
"""Convert a ONNX data type to JitScalarType.
|
||||
|
||||
Args:
|
||||
onnx_type: A torch._C._onnx.TensorProtoDataType to create a JitScalarType from
|
||||
|
||||
Returns:
|
||||
JitScalarType
|
||||
|
||||
Raises:
|
||||
OnnxExporterError: if dtype is not a valid torch.dtype or if it is None.
|
||||
"""
|
||||
if onnx_type not in _ONNX_TO_SCALAR_TYPE:
|
||||
raise errors.OnnxExporterError(f"Unknown onnx_type: {onnx_type}")
|
||||
# pyrefly: ignore [redundant-cast]
|
||||
return _ONNX_TO_SCALAR_TYPE[typing.cast(_C_onnx.TensorProtoDataType, onnx_type)]
|
||||
|
||||
@classmethod
|
||||
def from_value(
|
||||
cls, value: torch._C.Value | torch.Tensor | None, default=None
|
||||
) -> JitScalarType:
|
||||
"""Create a JitScalarType from an value's scalar type.
|
||||
|
||||
Args:
|
||||
value: An object to fetch scalar type from.
|
||||
default: The JitScalarType to return if a valid scalar cannot be fetched from value
|
||||
|
||||
Returns:
|
||||
JitScalarType.
|
||||
|
||||
Raises:
|
||||
OnnxExporterError: if value does not have a valid scalar type and default is None.
|
||||
SymbolicValueError: when value.type()'s info are empty and default is None
|
||||
"""
|
||||
|
||||
if not isinstance(value, (torch._C.Value, torch.Tensor)) or (
|
||||
isinstance(value, torch._C.Value) and value.node().mustBeNone()
|
||||
):
|
||||
# default value of type JitScalarType is returned when value is not valid
|
||||
if default is None:
|
||||
raise errors.OnnxExporterError(
|
||||
"value must be either torch._C.Value or torch.Tensor objects."
|
||||
)
|
||||
elif not isinstance(default, JitScalarType):
|
||||
raise errors.OnnxExporterError(
|
||||
"default value must be a JitScalarType object."
|
||||
)
|
||||
return default
|
||||
|
||||
# Each value type has their own way of storing scalar type
|
||||
if isinstance(value, torch.Tensor):
|
||||
return cls.from_dtype(value.dtype)
|
||||
if isinstance(value.type(), torch.ListType):
|
||||
try:
|
||||
return cls.from_dtype(value.type().getElementType().dtype())
|
||||
except RuntimeError:
|
||||
return cls._from_name(str(value.type().getElementType()))
|
||||
if isinstance(value.type(), torch._C.OptionalType):
|
||||
if value.type().getElementType().dtype() is None:
|
||||
if isinstance(default, JitScalarType):
|
||||
return default
|
||||
raise errors.OnnxExporterError(
|
||||
"default value must be a JitScalarType object."
|
||||
)
|
||||
return cls.from_dtype(value.type().getElementType().dtype())
|
||||
|
||||
scalar_type = None
|
||||
if value.node().kind() != "prim::Constant" or not isinstance(
|
||||
value.type(), torch._C.NoneType
|
||||
):
|
||||
# value must be a non-list torch._C.Value scalar
|
||||
scalar_type = value.type().scalarType()
|
||||
|
||||
if scalar_type is not None:
|
||||
return cls._from_name(scalar_type)
|
||||
|
||||
# When everything fails... try to default
|
||||
if default is not None:
|
||||
return default
|
||||
raise errors.SymbolicValueError(
|
||||
f"Cannot determine scalar type for this '{type(value.type())}' instance and "
|
||||
"a default value was not provided.",
|
||||
value,
|
||||
)
|
||||
|
||||
def scalar_name(self) -> ScalarName:
|
||||
"""Convert a JitScalarType to a JIT scalar type name."""
|
||||
return _SCALAR_TYPE_TO_NAME[self]
|
||||
|
||||
def torch_name(self) -> TorchName:
|
||||
"""Convert a JitScalarType to a torch type name."""
|
||||
return _SCALAR_TYPE_TO_TORCH_NAME[self]
|
||||
|
||||
def dtype(self) -> torch.dtype:
|
||||
"""Convert a JitScalarType to a torch dtype."""
|
||||
return _SCALAR_TYPE_TO_DTYPE[self]
|
||||
|
||||
def onnx_type(self) -> _C_onnx.TensorProtoDataType:
|
||||
"""Convert a JitScalarType to an ONNX data type."""
|
||||
if self not in _SCALAR_TYPE_TO_ONNX:
|
||||
raise errors.OnnxExporterError(
|
||||
f"Scalar type {self} cannot be converted to ONNX"
|
||||
)
|
||||
return _SCALAR_TYPE_TO_ONNX[self]
|
||||
|
||||
def onnx_compatible(self) -> bool:
|
||||
"""Return whether this JitScalarType is compatible with ONNX."""
|
||||
return (
|
||||
self in _SCALAR_TYPE_TO_ONNX
|
||||
and self != JitScalarType.UNDEFINED
|
||||
and self != JitScalarType.COMPLEX32
|
||||
)
|
||||
|
||||
|
||||
def valid_scalar_name(scalar_name: ScalarName | str) -> bool:
|
||||
"""Return whether the given scalar name is a valid JIT scalar type name."""
|
||||
return scalar_name in _SCALAR_NAME_TO_TYPE
|
||||
|
||||
|
||||
def valid_torch_name(torch_name: TorchName | str) -> bool:
|
||||
"""Return whether the given torch name is a valid torch type name."""
|
||||
return torch_name in _TORCH_NAME_TO_SCALAR_TYPE
|
||||
|
||||
|
||||
# https://github.com/pytorch/pytorch/blob/344defc9733a45fee8d0c4d3f5530f631e823196/c10/core/ScalarType.h
|
||||
_SCALAR_TYPE_TO_NAME: dict[JitScalarType, ScalarName] = {
|
||||
JitScalarType.BOOL: "Bool",
|
||||
JitScalarType.UINT8: "Byte",
|
||||
JitScalarType.INT8: "Char",
|
||||
JitScalarType.INT16: "Short",
|
||||
JitScalarType.INT: "Int",
|
||||
JitScalarType.INT64: "Long",
|
||||
JitScalarType.HALF: "Half",
|
||||
JitScalarType.FLOAT: "Float",
|
||||
JitScalarType.DOUBLE: "Double",
|
||||
JitScalarType.COMPLEX32: "ComplexHalf",
|
||||
JitScalarType.COMPLEX64: "ComplexFloat",
|
||||
JitScalarType.COMPLEX128: "ComplexDouble",
|
||||
JitScalarType.QINT8: "QInt8",
|
||||
JitScalarType.QUINT8: "QUInt8",
|
||||
JitScalarType.QINT32: "QInt32",
|
||||
JitScalarType.BFLOAT16: "BFloat16",
|
||||
JitScalarType.FLOAT8E5M2: "Float8E5M2",
|
||||
JitScalarType.FLOAT8E4M3FN: "Float8E4M3FN",
|
||||
JitScalarType.FLOAT8E5M2FNUZ: "Float8E5M2FNUZ",
|
||||
JitScalarType.FLOAT8E4M3FNUZ: "Float8E4M3FNUZ",
|
||||
JitScalarType.UNDEFINED: "Undefined",
|
||||
}
|
||||
|
||||
_SCALAR_NAME_TO_TYPE: dict[ScalarName, JitScalarType] = {
|
||||
v: k for k, v in _SCALAR_TYPE_TO_NAME.items()
|
||||
}
|
||||
|
||||
_SCALAR_TYPE_TO_TORCH_NAME: dict[JitScalarType, TorchName] = {
|
||||
JitScalarType.BOOL: "bool",
|
||||
JitScalarType.UINT8: "uint8_t",
|
||||
JitScalarType.INT8: "int8_t",
|
||||
JitScalarType.INT16: "int16_t",
|
||||
JitScalarType.INT: "int",
|
||||
JitScalarType.INT64: "int64_t",
|
||||
JitScalarType.HALF: "half",
|
||||
JitScalarType.FLOAT: "float",
|
||||
JitScalarType.DOUBLE: "double",
|
||||
JitScalarType.COMPLEX32: "complex32",
|
||||
JitScalarType.COMPLEX64: "complex64",
|
||||
JitScalarType.COMPLEX128: "complex128",
|
||||
JitScalarType.QINT8: "qint8",
|
||||
JitScalarType.QUINT8: "quint8",
|
||||
JitScalarType.QINT32: "qint32",
|
||||
JitScalarType.BFLOAT16: "bfloat16",
|
||||
JitScalarType.FLOAT8E5M2: "float8_e5m2",
|
||||
JitScalarType.FLOAT8E4M3FN: "float8_e4m3fn",
|
||||
JitScalarType.FLOAT8E5M2FNUZ: "float8_e5m2fnuz",
|
||||
JitScalarType.FLOAT8E4M3FNUZ: "float8_e4m3fnuz",
|
||||
}
|
||||
|
||||
_TORCH_NAME_TO_SCALAR_TYPE: dict[TorchName, JitScalarType] = {
|
||||
v: k for k, v in _SCALAR_TYPE_TO_TORCH_NAME.items()
|
||||
}
|
||||
|
||||
_SCALAR_TYPE_TO_ONNX = {
|
||||
JitScalarType.BOOL: _C_onnx.TensorProtoDataType.BOOL,
|
||||
JitScalarType.UINT8: _C_onnx.TensorProtoDataType.UINT8,
|
||||
JitScalarType.INT8: _C_onnx.TensorProtoDataType.INT8,
|
||||
JitScalarType.INT16: _C_onnx.TensorProtoDataType.INT16,
|
||||
JitScalarType.INT: _C_onnx.TensorProtoDataType.INT32,
|
||||
JitScalarType.INT64: _C_onnx.TensorProtoDataType.INT64,
|
||||
JitScalarType.HALF: _C_onnx.TensorProtoDataType.FLOAT16,
|
||||
JitScalarType.FLOAT: _C_onnx.TensorProtoDataType.FLOAT,
|
||||
JitScalarType.DOUBLE: _C_onnx.TensorProtoDataType.DOUBLE,
|
||||
JitScalarType.COMPLEX64: _C_onnx.TensorProtoDataType.COMPLEX64,
|
||||
JitScalarType.COMPLEX128: _C_onnx.TensorProtoDataType.COMPLEX128,
|
||||
JitScalarType.BFLOAT16: _C_onnx.TensorProtoDataType.BFLOAT16,
|
||||
JitScalarType.UNDEFINED: _C_onnx.TensorProtoDataType.UNDEFINED,
|
||||
JitScalarType.COMPLEX32: _C_onnx.TensorProtoDataType.UNDEFINED,
|
||||
JitScalarType.QINT8: _C_onnx.TensorProtoDataType.INT8,
|
||||
JitScalarType.QUINT8: _C_onnx.TensorProtoDataType.UINT8,
|
||||
JitScalarType.QINT32: _C_onnx.TensorProtoDataType.INT32,
|
||||
JitScalarType.FLOAT8E5M2: _C_onnx.TensorProtoDataType.FLOAT8E5M2,
|
||||
JitScalarType.FLOAT8E4M3FN: _C_onnx.TensorProtoDataType.FLOAT8E4M3FN,
|
||||
JitScalarType.FLOAT8E5M2FNUZ: _C_onnx.TensorProtoDataType.FLOAT8E5M2FNUZ,
|
||||
JitScalarType.FLOAT8E4M3FNUZ: _C_onnx.TensorProtoDataType.FLOAT8E4M3FNUZ,
|
||||
}
|
||||
|
||||
_ONNX_TO_SCALAR_TYPE = {v: k for k, v in _SCALAR_TYPE_TO_ONNX.items()}
|
||||
|
||||
# source of truth is
|
||||
# https://github.com/pytorch/pytorch/blob/master/torch/csrc/utils/tensor_dtypes.cpp
|
||||
_SCALAR_TYPE_TO_DTYPE = {
|
||||
JitScalarType.BOOL: torch.bool,
|
||||
JitScalarType.UINT8: torch.uint8,
|
||||
JitScalarType.INT8: torch.int8,
|
||||
JitScalarType.INT16: torch.short,
|
||||
JitScalarType.INT: torch.int,
|
||||
JitScalarType.INT64: torch.int64,
|
||||
JitScalarType.HALF: torch.half,
|
||||
JitScalarType.FLOAT: torch.float,
|
||||
JitScalarType.DOUBLE: torch.double,
|
||||
JitScalarType.COMPLEX32: torch.complex32,
|
||||
JitScalarType.COMPLEX64: torch.complex64,
|
||||
JitScalarType.COMPLEX128: torch.complex128,
|
||||
JitScalarType.QINT8: torch.qint8,
|
||||
JitScalarType.QUINT8: torch.quint8,
|
||||
JitScalarType.QINT32: torch.qint32,
|
||||
JitScalarType.BFLOAT16: torch.bfloat16,
|
||||
JitScalarType.FLOAT8E5M2: torch.float8_e5m2,
|
||||
JitScalarType.FLOAT8E4M3FN: torch.float8_e4m3fn,
|
||||
JitScalarType.FLOAT8E5M2FNUZ: torch.float8_e5m2fnuz,
|
||||
JitScalarType.FLOAT8E4M3FNUZ: torch.float8_e4m3fnuz,
|
||||
}
|
||||
|
||||
_DTYPE_TO_SCALAR_TYPE = {v: k for k, v in _SCALAR_TYPE_TO_DTYPE.items()}
|
||||
+374
@@ -0,0 +1,374 @@
|
||||
# mypy: allow-untyped-defs
|
||||
"""Utilities for manipulating the torch.Graph object and the torchscript."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import dataclasses
|
||||
import re
|
||||
import typing
|
||||
from collections.abc import Iterable, Sequence
|
||||
from typing import Any
|
||||
|
||||
import torch
|
||||
from torch import _C
|
||||
from torch.onnx._internal.torchscript_exporter import registration
|
||||
from torch.onnx._internal.torchscript_exporter._globals import GLOBALS
|
||||
|
||||
|
||||
_ATTR_PATTERN = re.compile("^(.+)_(([ifstgz])|(ty))$")
|
||||
_SKIP_NODE_ATTRIBUTES = {"inplace", "aten"}
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class GraphContext:
|
||||
"""Extra context for symbolic functions with all methods from torch.Graph.
|
||||
|
||||
NOTE: This class is not meant for external consumption. Please do not depend on
|
||||
it outside of torch.onnx as the interface may evolve.
|
||||
|
||||
Attributes:
|
||||
graph: The _C.Graph being constructed.
|
||||
block: The current _C.Block being constructed.
|
||||
opset: The opset version.
|
||||
original_node: Current node that is being converted from.
|
||||
params_dict: Mapping from graph initializer name to IValue.
|
||||
env: Mapping from Torch domain graph Value to ONNX domain graph Value.
|
||||
values_in_env: Set of all values in env, for constant-time lookups.
|
||||
new_nodes: List that tracks all new nodes that are added (used to make
|
||||
sure metadata is propagated to all new nodes).
|
||||
"""
|
||||
|
||||
graph: _C.Graph
|
||||
block: _C.Block
|
||||
opset: int
|
||||
original_node: _C.Node
|
||||
params_dict: dict[str, _C.IValue]
|
||||
env: dict[_C.Value, _C.Value]
|
||||
values_in_env: set[_C.Value]
|
||||
new_nodes: list[_C.Node] = dataclasses.field(default_factory=list)
|
||||
|
||||
# Relay methods from _C.Graph for compatibility with symbolic functions that expect
|
||||
# a _C.Graph
|
||||
def __getattr__(self, name: str) -> Any:
|
||||
return getattr(self.graph, name)
|
||||
|
||||
def op(
|
||||
self,
|
||||
opname: str,
|
||||
*raw_args: torch.Tensor | _C.Value,
|
||||
outputs: int = 1,
|
||||
**kwargs,
|
||||
):
|
||||
"""Creates an ONNX operator "opname", taking "raw_args" as inputs and "kwargs" as attributes.
|
||||
|
||||
The set of operators and the inputs/attributes they take
|
||||
is documented at https://github.com/onnx/onnx/blob/master/docs/Operators.md
|
||||
|
||||
Args:
|
||||
opname: The ONNX operator name, e.g., `Abs` or `Add`, or an operator qualified
|
||||
with a namespace, e.g., `aten::add`.
|
||||
raw_args: The inputs to the operator; usually provided
|
||||
as arguments to the `symbolic` definition.
|
||||
outputs: The number of outputs this operator returns.
|
||||
By default an operator is assumed to return a single output.
|
||||
If `outputs` is greater than one, this functions returns a tuple
|
||||
of output `Value`, representing each output of the ONNX operator
|
||||
in order.
|
||||
kwargs: The attributes of the ONNX operator, whose keys are named
|
||||
according to the following convention: `alpha_f` indicates
|
||||
the `alpha` attribute with type `f`. The valid type specifiers are
|
||||
`f` (float), `i` (int), `s` (string) or `t` (Tensor). An attribute
|
||||
specified with type float accepts either a single float, or a
|
||||
list of floats (e.g., you would say `dims_i` for a `dims` attribute
|
||||
that takes a list of integers).
|
||||
|
||||
Returns:
|
||||
The value representing the single output of this operator (see the `outputs`
|
||||
keyword argument for multi-return nodes).
|
||||
"""
|
||||
return _add_op(self, opname, *raw_args, outputs=outputs, **kwargs)
|
||||
|
||||
def aten_op(self, operator: str, *args, overload_name: str = "", **kwargs):
|
||||
"""Generates an ONNX ATen op node.
|
||||
|
||||
This function is for backward compatibility with the old symbolic functions.
|
||||
"""
|
||||
return self.op(
|
||||
"aten::ATen",
|
||||
*args,
|
||||
operator_s=operator,
|
||||
overload_name_s=overload_name,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
# NOTE: For backward compatibility with the old symbolic functions.
|
||||
# We are probably going to remove this only after the fx exporter is established.
|
||||
at = aten_op
|
||||
|
||||
def onnxscript_op(
|
||||
self,
|
||||
onnx_fn,
|
||||
*raw_args: torch.Tensor | _C.Value,
|
||||
outputs: int = 1,
|
||||
**kwargs,
|
||||
):
|
||||
"""Creates an ONNX operator from onnx-script function, taking "raw_args" as inputs and "kwargs" as attributes.
|
||||
|
||||
onnx-script repository: https://github.com/microsoft/onnx-script
|
||||
|
||||
Args:
|
||||
onnx_fn: ONNXFunction from onnx-script; An example can be found at
|
||||
https://github.com/microsoft/onnx-script#example
|
||||
raw_args: The inputs to the operator; usually provided
|
||||
as arguments to the `symbolic` definition.
|
||||
outputs: The number of outputs this operator returns.
|
||||
By default an operator is assumed to return a single output.
|
||||
If `outputs` is greater than one, this functions returns a tuple
|
||||
of output `Value`, representing each output of the ONNX operator
|
||||
in order.
|
||||
kwargs: The attributes of the ONNX operator, whose keys are named
|
||||
according to the following convention: `alpha_f` indicates
|
||||
the `alpha` attribute with type `f`. The valid type specifiers are
|
||||
`f` (float), `i` (int), `s` (string) or `t` (Tensor). An attribute
|
||||
specified with type float accepts either a single float, or a
|
||||
list of floats (e.g., you would say `dims_i` for a `dims` attribute
|
||||
that takes a list of integers).
|
||||
|
||||
Returns:
|
||||
The value representing the single output of this operator (see the `outputs`
|
||||
keyword argument for multi-return nodes).
|
||||
"""
|
||||
# NOTE(titaiwang): This is using class attributes, and it needs to be updated
|
||||
# if onnx-script makes any change on these.
|
||||
symbolic_name = f"{onnx_fn.opset.domain}::{onnx_fn.name}"
|
||||
opset_version = onnx_fn.opset.version
|
||||
|
||||
registration.custom_onnx_symbolic(symbolic_name, opset_version)(onnx_fn)
|
||||
|
||||
return _add_op(self, symbolic_name, *raw_args, outputs=outputs, **kwargs)
|
||||
|
||||
|
||||
def add_op_with_blocks(
|
||||
graph_context: GraphContext,
|
||||
opname: str,
|
||||
*inputs: _C.Value,
|
||||
outputs: int = 1,
|
||||
n_blocks: int = 1,
|
||||
**attributes,
|
||||
) -> tuple[Any, tuple[GraphContext, ...], _C.Node]:
|
||||
"""Creates an ONNX operator "opname", taking inputs and attributes.
|
||||
|
||||
Args:
|
||||
graph_context: The context for the current graph.
|
||||
opname: The ONNX operator name, e.g., `Abs` or `Add`, or an operator qualified
|
||||
with a namespace, e.g., `aten::add`.
|
||||
inputs: The inputs to the operator.
|
||||
outputs: The number of outputs this operator returns.
|
||||
By default an operator is assumed to return a single output.
|
||||
If `outputs` is greater than one, this functions returns a tuple
|
||||
of output `Value`, representing each output of the ONNX operator
|
||||
in order.
|
||||
n_blocks: The number of sub-blocks to create in the node.
|
||||
attributes: The attributes of the ONNX operator.
|
||||
|
||||
Returns:
|
||||
A tuple of (output_values, new_contexts, node) where:
|
||||
output_values: One or more output value of this operator
|
||||
(see the `outputs` keyword argument for multi-return nodes).
|
||||
new_contexts: A tuple of new graph contexts for each sub-block.
|
||||
node: The node representing the operator.
|
||||
"""
|
||||
|
||||
output_values = graph_context.op(opname, *inputs, outputs=outputs, **attributes)
|
||||
if isinstance(output_values, Sequence):
|
||||
node = output_values[0].node()
|
||||
else:
|
||||
node = output_values.node()
|
||||
|
||||
new_contexts = []
|
||||
for _ in range(n_blocks):
|
||||
new_block = node.addBlock()
|
||||
# Create shallow copy of the graph context and update the block
|
||||
new_context = dataclasses.replace(graph_context, block=new_block)
|
||||
new_contexts.append(new_context)
|
||||
|
||||
return output_values, tuple(new_contexts), node
|
||||
|
||||
|
||||
def _add_op(
|
||||
graph_context: GraphContext,
|
||||
opname: str,
|
||||
*args: torch.Tensor | _C.Value,
|
||||
outputs: int = 1,
|
||||
**kwargs,
|
||||
):
|
||||
"""Creates an ONNX operator "opname", taking "args" as inputs and attributes "kwargs".
|
||||
|
||||
The set of operators and the inputs/attributes they take
|
||||
is documented at https://github.com/onnx/onnx/blob/master/docs/Operators.md
|
||||
|
||||
Args:
|
||||
graph_context: The Torch Graph or Block.
|
||||
opname: The ONNX operator name, e.g., `Abs` or `Add`, or an operator qualified
|
||||
with a namespace, e.g., `aten::add`.
|
||||
args: The inputs to the operator; usually provided
|
||||
as arguments to the `symbolic` definition.
|
||||
outputs: The number of outputs this operator returns.
|
||||
By default an operator is assumed to return a single output.
|
||||
If `outputs` is greater than one, this functions returns a tuple
|
||||
of output `Value`, representing each output of the ONNX operator
|
||||
in order.
|
||||
kwargs: The attributes of the ONNX operator, whose keys are named
|
||||
according to the following convention: `alpha_f` indicates
|
||||
the `alpha` attribute with type `f`. The valid type specifiers are
|
||||
`f` (float), `i` (int), `s` (string) or `t` (Tensor). An attribute
|
||||
specified with type float accepts either a single float, or a
|
||||
list of floats (e.g., you would say `dims_i` for a `dims` attribute
|
||||
that takes a list of integers).
|
||||
|
||||
Returns:
|
||||
(Union[_C.Value, Tuple[_C.Value, ...]])
|
||||
The value representing the single output of this operator (see the `outputs`
|
||||
keyword argument for multi-return nodes).
|
||||
"""
|
||||
inputs = [_const_if_tensor(graph_context, arg) for arg in args]
|
||||
# Filter out None attributes, this can be convenient client side because
|
||||
# now they can pass through None attributes, and have them not show up
|
||||
attributes = {k: v for k, v in kwargs.items() if v is not None}
|
||||
|
||||
if "::" not in opname:
|
||||
opname = "onnx::" + opname
|
||||
|
||||
node = _create_node(
|
||||
graph_context.block,
|
||||
opname,
|
||||
inputs,
|
||||
attributes,
|
||||
params_dict=graph_context.params_dict,
|
||||
opset_version=graph_context.opset,
|
||||
n_outputs=outputs,
|
||||
shape_inference=GLOBALS.onnx_shape_inference,
|
||||
)
|
||||
graph_context.new_nodes.append(node)
|
||||
|
||||
if outputs == 1:
|
||||
return node.output()
|
||||
return tuple(node.outputs())
|
||||
|
||||
|
||||
def _const_if_tensor(graph_context: GraphContext, arg):
|
||||
if arg is None:
|
||||
return arg
|
||||
if isinstance(arg, _C.Value):
|
||||
return arg
|
||||
|
||||
return _add_op(graph_context, "onnx::Constant", value_z=arg)
|
||||
|
||||
|
||||
def _create_node(
|
||||
graph_or_block: _C.Graph | _C.Block,
|
||||
domain_op: str,
|
||||
inputs: Sequence,
|
||||
attributes: dict,
|
||||
params_dict: dict,
|
||||
opset_version: int,
|
||||
n_outputs: int,
|
||||
shape_inference: bool = True,
|
||||
) -> _C.Node:
|
||||
"""Creates an node 'domain_op', taking inputs and attributes."""
|
||||
if isinstance(graph_or_block, _C.Graph):
|
||||
graph = graph_or_block
|
||||
node = graph.create(domain_op, inputs, n_outputs)
|
||||
node = graph.insertNode(node)
|
||||
elif isinstance(graph_or_block, _C.Block):
|
||||
block = graph_or_block
|
||||
node = block.addNode(domain_op, inputs)
|
||||
|
||||
# Block does not have create defined, so we need to add outputs manually
|
||||
if n_outputs > 1:
|
||||
for _ in range(1, n_outputs):
|
||||
node.addOutput()
|
||||
|
||||
node_outputs = tuple(node.outputs()) # type: ignore[possibly-undefined]
|
||||
if len(node_outputs) != n_outputs:
|
||||
raise AssertionError(
|
||||
f"len(node_outputs)={len(node_outputs)} != n_outputs={n_outputs}"
|
||||
)
|
||||
|
||||
aten = domain_op.startswith("aten::")
|
||||
|
||||
# Add all attributes
|
||||
for key, value in sorted(attributes.items()):
|
||||
if key in _SKIP_NODE_ATTRIBUTES:
|
||||
continue
|
||||
# pyrefly: ignore [unbound-name]
|
||||
_add_attribute(node, key, value, aten=aten)
|
||||
if shape_inference:
|
||||
# pyrefly: ignore [unbound-name]
|
||||
_C._jit_pass_onnx_node_shape_type_inference(node, params_dict, opset_version)
|
||||
# pyrefly: ignore [unbound-name]
|
||||
return node
|
||||
|
||||
|
||||
def _is_onnx_list(value):
|
||||
return isinstance(value, Iterable) and not isinstance(
|
||||
value, (str, bytes, torch.Tensor)
|
||||
)
|
||||
|
||||
|
||||
def _scalar(x: torch.Tensor):
|
||||
"""Convert a scalar tensor into a Python value."""
|
||||
if x.numel() != 1:
|
||||
raise AssertionError(f"Expected numel() == 1, got {x.numel()}")
|
||||
return x[0]
|
||||
|
||||
|
||||
def _add_attribute(node: _C.Node, key: str, value: Any, aten: bool):
|
||||
r"""Initializes the right attribute based on type of value."""
|
||||
m = _ATTR_PATTERN.match(key)
|
||||
if m is None:
|
||||
raise ValueError(
|
||||
f"Invalid attribute specifier '{key}' names "
|
||||
"must be suffixed with type, e.g. 'dim_i' or 'dims_i'"
|
||||
)
|
||||
name, kind = m.group(1), m.group(2)
|
||||
if _is_onnx_list(value):
|
||||
kind += "s"
|
||||
|
||||
return getattr(node, f"{kind}_")(name, value)
|
||||
|
||||
|
||||
def _is_tensor(x: _C.Value) -> bool:
|
||||
return x.type().isSubtypeOf(_C.TensorType.get())
|
||||
|
||||
|
||||
def get_device_from_value(value: _C.Value) -> torch.device | None:
|
||||
if not _is_tensor(value):
|
||||
return None
|
||||
tensor_type = typing.cast(_C.TensorType, value.type())
|
||||
return tensor_type.device()
|
||||
|
||||
|
||||
def parse_node_kind(kind: str) -> tuple[str, str]:
|
||||
"""Parse node kind into domain and Op name."""
|
||||
if "::" not in kind:
|
||||
raise ValueError(f"Node kind: {kind} is invalid. '::' is not in node kind.")
|
||||
domain, opname = kind.split("::", 1)
|
||||
if "::" in opname:
|
||||
raise ValueError(f"Node kind: {kind} is invalid. '::' should only appear once.")
|
||||
return domain, opname
|
||||
|
||||
|
||||
def is_aten(domain: str) -> bool:
|
||||
"""Check if the domain is official."""
|
||||
return domain == "aten"
|
||||
|
||||
|
||||
def is_prim(domain: str) -> bool:
|
||||
"""Check if the domain is official."""
|
||||
return domain == "prim"
|
||||
|
||||
|
||||
def is_onnx(domain: str) -> bool:
|
||||
"""Check if the domain is official."""
|
||||
return domain == "onnx"
|
||||
+251
@@ -0,0 +1,251 @@
|
||||
# mypy: allow-untyped-defs
|
||||
"""Utilities for manipulating the onnx and onnx-script dependencies and ONNX proto."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import glob
|
||||
import os
|
||||
import shutil
|
||||
from typing import Any, TYPE_CHECKING
|
||||
|
||||
import torch
|
||||
import torch.serialization
|
||||
from torch.onnx import errors
|
||||
from torch.onnx._internal.torchscript_exporter import jit_utils, registration
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import io
|
||||
from collections.abc import Mapping
|
||||
|
||||
|
||||
def export_as_test_case(
|
||||
model_bytes: bytes, inputs_data, outputs_data, name: str, dir: str
|
||||
) -> str:
|
||||
"""Export an ONNX model as a self contained ONNX test case.
|
||||
|
||||
The test case contains the model and the inputs/outputs data. The directory structure
|
||||
is as follows:
|
||||
|
||||
dir
|
||||
\u251c\u2500\u2500 test_<name>
|
||||
\u2502 \u251c\u2500\u2500 model.onnx
|
||||
\u2502 \u2514\u2500\u2500 test_data_set_0
|
||||
\u2502 \u251c\u2500\u2500 input_0.pb
|
||||
\u2502 \u251c\u2500\u2500 input_1.pb
|
||||
\u2502 \u251c\u2500\u2500 output_0.pb
|
||||
\u2502 \u2514\u2500\u2500 output_1.pb
|
||||
|
||||
Args:
|
||||
model_bytes: The ONNX model in bytes.
|
||||
inputs_data: The inputs data, nested data structure of numpy.ndarray.
|
||||
outputs_data: The outputs data, nested data structure of numpy.ndarray.
|
||||
|
||||
Returns:
|
||||
The path to the test case directory.
|
||||
"""
|
||||
try:
|
||||
import onnx
|
||||
except ImportError as exc:
|
||||
raise ImportError(
|
||||
"Export test case to ONNX format failed: Please install ONNX."
|
||||
) from exc
|
||||
|
||||
test_case_dir = os.path.join(dir, "test_" + name)
|
||||
os.makedirs(test_case_dir, exist_ok=True)
|
||||
_export_file(
|
||||
model_bytes,
|
||||
os.path.join(test_case_dir, "model.onnx"),
|
||||
{},
|
||||
)
|
||||
data_set_dir = os.path.join(test_case_dir, "test_data_set_0")
|
||||
if os.path.exists(data_set_dir):
|
||||
shutil.rmtree(data_set_dir)
|
||||
os.makedirs(data_set_dir)
|
||||
|
||||
proto = onnx.load_model_from_string(model_bytes) # type: ignore[attr-defined]
|
||||
|
||||
for i, (input_proto, input) in enumerate(zip(proto.graph.input, inputs_data)):
|
||||
export_data(input, input_proto, os.path.join(data_set_dir, f"input_{i}.pb"))
|
||||
for i, (output_proto, output) in enumerate(zip(proto.graph.output, outputs_data)):
|
||||
export_data(output, output_proto, os.path.join(data_set_dir, f"output_{i}.pb"))
|
||||
|
||||
return test_case_dir
|
||||
|
||||
|
||||
def load_test_case(dir: str) -> tuple[bytes, Any, Any]:
|
||||
"""Load a self contained ONNX test case from a directory.
|
||||
|
||||
The test case must contain the model and the inputs/outputs data. The directory structure
|
||||
should be as follows:
|
||||
|
||||
dir
|
||||
\u251c\u2500\u2500 test_<name>
|
||||
\u2502 \u251c\u2500\u2500 model.onnx
|
||||
\u2502 \u2514\u2500\u2500 test_data_set_0
|
||||
\u2502 \u251c\u2500\u2500 input_0.pb
|
||||
\u2502 \u251c\u2500\u2500 input_1.pb
|
||||
\u2502 \u251c\u2500\u2500 output_0.pb
|
||||
\u2502 \u2514\u2500\u2500 output_1.pb
|
||||
|
||||
Args:
|
||||
dir: The directory containing the test case.
|
||||
|
||||
Returns:
|
||||
model_bytes: The ONNX model in bytes.
|
||||
inputs: the inputs data, mapping from input name to numpy.ndarray.
|
||||
outputs: the outputs data, mapping from output name to numpy.ndarray.
|
||||
"""
|
||||
try:
|
||||
import onnx
|
||||
from onnx import numpy_helper # type: ignore[attr-defined]
|
||||
except ImportError as exc:
|
||||
raise ImportError(
|
||||
"Load test case from ONNX format failed: Please install ONNX."
|
||||
) from exc
|
||||
|
||||
with open(os.path.join(dir, "model.onnx"), "rb") as f:
|
||||
model_bytes = f.read()
|
||||
|
||||
test_data_dir = os.path.join(dir, "test_data_set_0")
|
||||
|
||||
inputs = {}
|
||||
input_files = glob.glob(os.path.join(test_data_dir, "input_*.pb"))
|
||||
for input_file in input_files:
|
||||
tensor = onnx.load_tensor(input_file) # type: ignore[attr-defined]
|
||||
inputs[tensor.name] = numpy_helper.to_array(tensor)
|
||||
outputs = {}
|
||||
output_files = glob.glob(os.path.join(test_data_dir, "output_*.pb"))
|
||||
for output_file in output_files:
|
||||
tensor = onnx.load_tensor(output_file) # type: ignore[attr-defined]
|
||||
outputs[tensor.name] = numpy_helper.to_array(tensor)
|
||||
|
||||
return model_bytes, inputs, outputs
|
||||
|
||||
|
||||
def export_data(data, value_info_proto, f: str) -> None:
|
||||
"""Export data to ONNX protobuf format.
|
||||
|
||||
Args:
|
||||
data: The data to export, nested data structure of numpy.ndarray.
|
||||
value_info_proto: The ValueInfoProto of the data. The type of the ValueInfoProto
|
||||
determines how the data is stored.
|
||||
f: The file to write the data to.
|
||||
"""
|
||||
try:
|
||||
from onnx import numpy_helper # type: ignore[attr-defined]
|
||||
except ImportError as exc:
|
||||
raise ImportError(
|
||||
"Export data to ONNX format failed: Please install ONNX."
|
||||
) from exc
|
||||
|
||||
with open(f, "wb") as opened_file:
|
||||
if value_info_proto.type.HasField("map_type"):
|
||||
opened_file.write(
|
||||
numpy_helper.from_dict(data, value_info_proto.name).SerializeToString()
|
||||
)
|
||||
elif value_info_proto.type.HasField("sequence_type"):
|
||||
opened_file.write(
|
||||
numpy_helper.from_list(data, value_info_proto.name).SerializeToString()
|
||||
)
|
||||
elif value_info_proto.type.HasField("optional_type"):
|
||||
opened_file.write(
|
||||
numpy_helper.from_optional(
|
||||
data, value_info_proto.name
|
||||
).SerializeToString()
|
||||
)
|
||||
else:
|
||||
if not value_info_proto.type.HasField("tensor_type"):
|
||||
raise AssertionError("Expected tensor_type field to be set")
|
||||
opened_file.write(
|
||||
numpy_helper.from_array(data, value_info_proto.name).SerializeToString()
|
||||
)
|
||||
|
||||
|
||||
def _export_file(
|
||||
model_bytes: bytes,
|
||||
f: io.BytesIO | str,
|
||||
export_map: Mapping[str, bytes],
|
||||
) -> None:
|
||||
"""export/write model bytes into directory/protobuf/zip"""
|
||||
if len(export_map) != 0:
|
||||
raise AssertionError(f"export_map must be empty, got {len(export_map)} items")
|
||||
with torch.serialization._open_file_like(f, "wb") as opened_file:
|
||||
opened_file.write(model_bytes)
|
||||
|
||||
|
||||
def _add_onnxscript_fn(
|
||||
model_bytes: bytes,
|
||||
custom_opsets: Mapping[str, int],
|
||||
) -> bytes:
|
||||
"""Insert model-included custom onnx-script function into ModelProto"""
|
||||
try:
|
||||
import onnx
|
||||
except ImportError as e:
|
||||
raise errors.OnnxExporterError("Module onnx is not installed!") from e
|
||||
|
||||
# For > 2GB model, onnx.load_fromstring would fail. However, because
|
||||
# in _export_onnx, the tensors should be saved separately if the proto
|
||||
# size > 2GB, and if it for some reason did not, the model would fail on
|
||||
# serialization anyway in terms of the protobuf limitation. So we don't
|
||||
# need to worry about > 2GB model getting here.
|
||||
model_proto = onnx.load_model_from_string(model_bytes) # type: ignore[attr-defined]
|
||||
|
||||
# Iterate graph nodes to insert only the included custom
|
||||
# function_proto into model_proto
|
||||
onnx_function_list = [] # type: ignore[var-annotated]
|
||||
included_node_func: set[str] = set()
|
||||
# onnx_function_list and included_node_func are expanded in-place
|
||||
_find_onnxscript_op(
|
||||
model_proto.graph, included_node_func, custom_opsets, onnx_function_list
|
||||
)
|
||||
|
||||
if onnx_function_list:
|
||||
model_proto.functions.extend(onnx_function_list)
|
||||
model_bytes = model_proto.SerializeToString()
|
||||
return model_bytes
|
||||
|
||||
|
||||
def _find_onnxscript_op(
|
||||
graph_proto,
|
||||
included_node_func: set[str],
|
||||
custom_opsets: Mapping[str, int],
|
||||
onnx_function_list: list,
|
||||
):
|
||||
"""Recursively iterate ModelProto to find ONNXFunction op as it may contain control flow Op."""
|
||||
for node in graph_proto.node:
|
||||
node_kind = node.domain + "::" + node.op_type
|
||||
# Recursive needed for control flow nodes: IF/Loop which has inner graph_proto
|
||||
for attr in node.attribute:
|
||||
if attr.g is not None:
|
||||
_find_onnxscript_op(
|
||||
attr.g, included_node_func, custom_opsets, onnx_function_list
|
||||
)
|
||||
# Only custom Op with ONNX function and aten with symbolic_fn should be found in registry
|
||||
onnx_function_group = registration.registry.get_function_group(node_kind)
|
||||
# Ruled out corner cases: onnx/prim in registry
|
||||
if (
|
||||
node.domain
|
||||
and not jit_utils.is_aten(node.domain)
|
||||
and not jit_utils.is_prim(node.domain)
|
||||
and not jit_utils.is_onnx(node.domain)
|
||||
and onnx_function_group is not None
|
||||
and node_kind not in included_node_func
|
||||
):
|
||||
specified_version = custom_opsets.get(node.domain, 1)
|
||||
onnx_fn = onnx_function_group.get(specified_version)
|
||||
if onnx_fn is not None:
|
||||
if hasattr(onnx_fn, "to_function_proto"):
|
||||
onnx_function_proto = onnx_fn.to_function_proto() # type: ignore[attr-defined]
|
||||
onnx_function_list.append(onnx_function_proto)
|
||||
included_node_func.add(node_kind)
|
||||
continue
|
||||
|
||||
raise errors.UnsupportedOperatorError(
|
||||
node_kind,
|
||||
specified_version,
|
||||
onnx_function_group.get_min_supported()
|
||||
if onnx_function_group
|
||||
else None,
|
||||
)
|
||||
return onnx_function_list, included_node_func
|
||||
+337
@@ -0,0 +1,337 @@
|
||||
# mypy: allow-untyped-defs
|
||||
"""Module for handling symbolic function registration."""
|
||||
|
||||
import warnings
|
||||
from collections.abc import Callable, Collection, Sequence
|
||||
from typing import Generic, TypeVar
|
||||
from typing_extensions import ParamSpec
|
||||
|
||||
from torch.onnx import _constants, errors
|
||||
|
||||
|
||||
OpsetVersion = int
|
||||
|
||||
|
||||
def _dispatch_opset_version(
|
||||
target: OpsetVersion, registered_opsets: Collection[OpsetVersion]
|
||||
) -> OpsetVersion | None:
|
||||
"""Finds the registered opset given a target opset version and the available opsets.
|
||||
|
||||
Args:
|
||||
target: The target opset version.
|
||||
registered_opsets: The available opsets.
|
||||
|
||||
Returns:
|
||||
The registered opset version.
|
||||
"""
|
||||
if not registered_opsets:
|
||||
return None
|
||||
|
||||
descending_registered_versions = sorted(registered_opsets, reverse=True)
|
||||
# Linear search for the opset version, which is fine since the number of opset
|
||||
# versions is small.
|
||||
|
||||
if target >= _constants.ONNX_BASE_OPSET:
|
||||
# Always look down toward opset 1 when the target is >= ONNX_BASE_OPSET (opset 9).
|
||||
# When a custom op is register at opset 1, we want to be able to discover it as a
|
||||
# fallback for all opsets >= ONNX_BASE_OPSET.
|
||||
for version in descending_registered_versions:
|
||||
if version <= target:
|
||||
return version
|
||||
return None
|
||||
|
||||
# target < opset 9. This is the legacy behavior to support opset 7 and opset 8.
|
||||
# for caffe2 support. We search up toward opset 9.
|
||||
for version in reversed(descending_registered_versions):
|
||||
# Count back up until _constants.ONNX_BASE_OPSET
|
||||
if target <= version <= _constants.ONNX_BASE_OPSET:
|
||||
return version
|
||||
|
||||
return None
|
||||
|
||||
|
||||
_K = TypeVar("_K")
|
||||
_V = TypeVar("_V")
|
||||
_R = TypeVar("_R")
|
||||
_P = ParamSpec("_P")
|
||||
|
||||
|
||||
class OverrideDict(Collection[_K], Generic[_K, _V]):
|
||||
"""A dictionary that merges built-in and custom symbolic functions.
|
||||
|
||||
It supports overriding and un-overriding built-in symbolic functions with custom
|
||||
ones.
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._base: dict[_K, _V] = {}
|
||||
self._overrides: dict[_K, _V] = {}
|
||||
self._merged: dict[_K, _V] = {}
|
||||
|
||||
def set_base(self, key: _K, value: _V) -> None:
|
||||
self._base[key] = value
|
||||
if key not in self._overrides:
|
||||
self._merged[key] = value
|
||||
|
||||
def in_base(self, key: _K) -> bool:
|
||||
"""Checks if a key is in the base dictionary."""
|
||||
return key in self._base
|
||||
|
||||
def override(self, key: _K, value: _V) -> None:
|
||||
"""Overrides a base key-value with a new pair."""
|
||||
self._overrides[key] = value
|
||||
self._merged[key] = value
|
||||
|
||||
def remove_override(self, key: _K) -> None:
|
||||
"""Un-overrides a key-value pair."""
|
||||
self._overrides.pop(key, None) # type: ignore[arg-type]
|
||||
self._merged.pop(key, None) # type: ignore[arg-type]
|
||||
if key in self._base:
|
||||
self._merged[key] = self._base[key]
|
||||
|
||||
def overridden(self, key: _K) -> bool:
|
||||
"""Checks if a key-value pair is overridden."""
|
||||
return key in self._overrides
|
||||
|
||||
def __getitem__(self, key: _K) -> _V:
|
||||
return self._merged[key]
|
||||
|
||||
def get(self, key: _K, default: _V | None = None):
|
||||
return self._merged.get(key, default)
|
||||
|
||||
def __contains__(self, key: object) -> bool:
|
||||
return key in self._merged
|
||||
|
||||
def __iter__(self):
|
||||
return iter(self._merged)
|
||||
|
||||
def __len__(self) -> int:
|
||||
return len(self._merged)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"OverrideDict(base={self._base}, overrides={self._overrides})"
|
||||
|
||||
def __bool__(self) -> bool:
|
||||
return bool(self._merged)
|
||||
|
||||
|
||||
class _SymbolicFunctionGroup:
|
||||
"""Different versions of symbolic functions registered to the same name.
|
||||
|
||||
O(number of registered versions of an op) search is performed to find the most
|
||||
recent version of the op.
|
||||
|
||||
The registration is delayed until op is used to improve startup time.
|
||||
|
||||
Function overloads with different arguments are not allowed.
|
||||
Custom op overrides are supported.
|
||||
"""
|
||||
|
||||
def __init__(self, name: str) -> None:
|
||||
self._name = name
|
||||
# A dictionary of functions, keyed by the opset version.
|
||||
self._functions: OverrideDict[OpsetVersion, Callable] = OverrideDict()
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"_SymbolicFunctionGroup({self._name}, registered={self._functions})"
|
||||
|
||||
def __getitem__(self, key: OpsetVersion) -> Callable:
|
||||
result = self.get(key)
|
||||
if result is None:
|
||||
raise KeyError(key)
|
||||
return result
|
||||
|
||||
# TODO(justinchuby): Add @functools.lru_cache(maxsize=None) if lookup time becomes
|
||||
# a problem.
|
||||
def get(self, opset: OpsetVersion) -> Callable | None:
|
||||
"""Find the most recent version of the function."""
|
||||
version = _dispatch_opset_version(opset, self._functions)
|
||||
if version is None:
|
||||
return None
|
||||
|
||||
return self._functions[version]
|
||||
|
||||
def add(self, func: Callable, opset: OpsetVersion) -> None:
|
||||
"""Adds a symbolic function.
|
||||
|
||||
Args:
|
||||
func: The function to add.
|
||||
opset: The opset version of the function to add.
|
||||
"""
|
||||
if self._functions.in_base(opset):
|
||||
warnings.warn(
|
||||
f"Symbolic function '{self._name}' already registered for opset {opset}. "
|
||||
f"Replacing the existing function with new function. This is unexpected. "
|
||||
f"Please report it on {_constants.PYTORCH_GITHUB_ISSUES_URL}.",
|
||||
errors.OnnxExporterWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
self._functions.set_base(opset, func)
|
||||
|
||||
def add_custom(self, func: Callable, opset: OpsetVersion) -> None:
|
||||
"""Adds a custom symbolic function.
|
||||
|
||||
Args:
|
||||
func: The symbolic function to register.
|
||||
opset: The corresponding opset version.
|
||||
"""
|
||||
self._functions.override(opset, func)
|
||||
|
||||
def remove_custom(self, opset: OpsetVersion) -> None:
|
||||
"""Removes a custom symbolic function.
|
||||
|
||||
Args:
|
||||
opset: The opset version of the custom function to remove.
|
||||
"""
|
||||
if not self._functions.overridden(opset):
|
||||
warnings.warn(
|
||||
f"No custom function registered for '{self._name}' opset {opset}",
|
||||
stacklevel=2,
|
||||
)
|
||||
return
|
||||
self._functions.remove_override(opset)
|
||||
|
||||
def get_min_supported(self) -> OpsetVersion:
|
||||
"""Returns the lowest built-in opset version supported by the function."""
|
||||
return min(self._functions)
|
||||
|
||||
|
||||
class SymbolicRegistry:
|
||||
"""Registry for symbolic functions.
|
||||
|
||||
The registry maintains a mapping from qualified names to symbolic functions.
|
||||
It is used to register new symbolic functions and to dispatch calls to
|
||||
the appropriate function.
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._registry: dict[str, _SymbolicFunctionGroup] = {}
|
||||
|
||||
def register(
|
||||
self, name: str, opset: OpsetVersion, func: Callable, custom: bool = False
|
||||
) -> None:
|
||||
"""Registers a symbolic function.
|
||||
|
||||
Args:
|
||||
name: The qualified name of the function to register. In the form of 'domain::op'.
|
||||
E.g. 'aten::add'.
|
||||
opset: The opset version of the function to register.
|
||||
func: The symbolic function to register.
|
||||
custom: Whether the function is a custom function that overrides existing ones.
|
||||
|
||||
Raises:
|
||||
ValueError: If the separator '::' is not in the name.
|
||||
"""
|
||||
if "::" not in name:
|
||||
raise ValueError(
|
||||
f"The name must be in the form of 'domain::op', not '{name}'"
|
||||
)
|
||||
symbolic_functions = self._registry.setdefault(
|
||||
name, _SymbolicFunctionGroup(name)
|
||||
)
|
||||
if custom:
|
||||
symbolic_functions.add_custom(func, opset)
|
||||
else:
|
||||
symbolic_functions.add(func, opset)
|
||||
|
||||
def unregister(self, name: str, opset: OpsetVersion) -> None:
|
||||
"""Unregisters a symbolic function.
|
||||
|
||||
Args:
|
||||
name: The qualified name of the function to unregister.
|
||||
opset: The opset version of the function to unregister.
|
||||
"""
|
||||
if name not in self._registry:
|
||||
return
|
||||
self._registry[name].remove_custom(opset)
|
||||
|
||||
def get_function_group(self, name: str) -> _SymbolicFunctionGroup | None:
|
||||
"""Returns the function group for the given name."""
|
||||
return self._registry.get(name)
|
||||
|
||||
def is_registered_op(self, name: str, version: int) -> bool:
|
||||
"""Returns whether the given op is registered for the given opset version."""
|
||||
functions = self.get_function_group(name)
|
||||
if functions is None:
|
||||
return False
|
||||
return functions.get(version) is not None
|
||||
|
||||
def all_functions(self) -> set[str]:
|
||||
"""Returns the set of all registered function names."""
|
||||
return set(self._registry)
|
||||
|
||||
|
||||
def onnx_symbolic(
|
||||
name: str,
|
||||
opset: OpsetVersion | Sequence[OpsetVersion],
|
||||
decorate: Sequence[Callable] | None = None,
|
||||
custom: bool = False,
|
||||
) -> Callable:
|
||||
"""Registers a symbolic function.
|
||||
|
||||
Usage::
|
||||
|
||||
```
|
||||
@onnx_symbolic(
|
||||
"aten::symbolic_b",
|
||||
opset=10,
|
||||
decorate=[quantized_aten_handler(scale=1 / 128, zero_point=0)],
|
||||
)
|
||||
@symbolic_helper.parse_args("v", "v", "b")
|
||||
def symbolic_b(g: _C.Graph, x: _C.Value, y: _C.Value, arg1: bool) -> _C.Value: ...
|
||||
```
|
||||
|
||||
Args:
|
||||
name: The qualified name of the function in the form of 'domain::op'.
|
||||
E.g. 'aten::add'.
|
||||
opset: The opset versions of the function to register at.
|
||||
decorate: A sequence of decorators to apply to the function.
|
||||
custom: Whether the function is a custom symbolic function.
|
||||
|
||||
Raises:
|
||||
ValueError: If the separator '::' is not in the name.
|
||||
"""
|
||||
|
||||
def wrapper(func: Callable[_P, _R]) -> Callable[_P, _R]:
|
||||
decorated = func
|
||||
if decorate is not None:
|
||||
for decorate_func in decorate:
|
||||
decorated = decorate_func(decorated)
|
||||
|
||||
global registry
|
||||
nonlocal opset
|
||||
if isinstance(opset, OpsetVersion):
|
||||
opset = (opset,)
|
||||
for opset_version in opset:
|
||||
registry.register(name, opset_version, decorated, custom=custom)
|
||||
|
||||
# Return the original function because the decorators in "decorate" are only
|
||||
# specific to the instance being registered.
|
||||
return func
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
def custom_onnx_symbolic(
|
||||
name: str,
|
||||
opset: OpsetVersion | Sequence[OpsetVersion],
|
||||
decorate: Sequence[Callable] | None = None,
|
||||
) -> Callable:
|
||||
"""Registers a custom symbolic function.
|
||||
|
||||
Args:
|
||||
name: the qualified name of the function.
|
||||
opset: the opset version of the function.
|
||||
decorate: a sequence of decorators to apply to the function.
|
||||
|
||||
Returns:
|
||||
The decorator.
|
||||
|
||||
Raises:
|
||||
ValueError: If the separator '::' is not in the name.
|
||||
"""
|
||||
return onnx_symbolic(name, opset, decorate, custom=True)
|
||||
|
||||
|
||||
# The registry for all symbolic functions.
|
||||
registry = SymbolicRegistry()
|
||||
+2407
File diff suppressed because it is too large
Load Diff
+1191
File diff suppressed because it is too large
Load Diff
+1476
File diff suppressed because it is too large
Load Diff
+469
@@ -0,0 +1,469 @@
|
||||
# mypy: allow-untyped-defs
|
||||
# mypy: disable-error-code=arg-type
|
||||
from __future__ import annotations
|
||||
|
||||
import functools
|
||||
import sys
|
||||
|
||||
import torch
|
||||
from torch._C import _onnx as _C_onnx
|
||||
from torch.onnx import errors
|
||||
from torch.onnx._internal.torchscript_exporter import (
|
||||
_type_utils,
|
||||
jit_utils,
|
||||
registration,
|
||||
symbolic_helper,
|
||||
symbolic_opset9 as opset9,
|
||||
utils,
|
||||
)
|
||||
|
||||
|
||||
# EDITING THIS FILE? READ THIS FIRST!
|
||||
# see Note [Edit Symbolic Files] in README.md
|
||||
|
||||
# This file exports ONNX ops for opset 12
|
||||
|
||||
__all__ = [
|
||||
"argmax",
|
||||
"argmin",
|
||||
"binary_cross_entropy_with_logits",
|
||||
"celu",
|
||||
"cross_entropy_loss",
|
||||
"dropout",
|
||||
"einsum",
|
||||
"ge",
|
||||
"le",
|
||||
"native_dropout",
|
||||
"nll_loss",
|
||||
"nll_loss2d",
|
||||
"nll_loss_nd",
|
||||
"outer",
|
||||
"pow",
|
||||
"tensordot",
|
||||
"unfold",
|
||||
]
|
||||
|
||||
_onnx_symbolic = functools.partial(registration.onnx_symbolic, opset=12)
|
||||
|
||||
|
||||
def _einsum_helper(g: jit_utils.GraphContext, equation, tensors):
|
||||
if not tensors:
|
||||
raise RuntimeError("Einsum inputs are empty.")
|
||||
# ONNX does not support bool for Einsum inputs.
|
||||
if symbolic_helper._is_bool(tensors[0]):
|
||||
tensors = [
|
||||
g.op("Cast", tensor, to_i=_C_onnx.TensorProtoDataType.INT64)
|
||||
for tensor in tensors
|
||||
]
|
||||
return g.op(
|
||||
"Cast",
|
||||
g.op("Einsum", *tensors, equation_s=equation),
|
||||
to_i=_C_onnx.TensorProtoDataType.BOOL,
|
||||
)
|
||||
else:
|
||||
return g.op("Einsum", *tensors, equation_s=equation)
|
||||
|
||||
|
||||
@_onnx_symbolic("aten::einsum")
|
||||
@symbolic_helper.parse_args("s", "v", "is")
|
||||
def einsum(g: jit_utils.GraphContext, equation, tensor_list, path=None):
|
||||
tensors = symbolic_helper._unpack_list(tensor_list)
|
||||
return _einsum_helper(g, equation, tensors)
|
||||
|
||||
|
||||
@_onnx_symbolic("aten::outer")
|
||||
@symbolic_helper.parse_args("v", "v")
|
||||
def outer(g: jit_utils.GraphContext, input, other):
|
||||
# make sure to cast other to self's type
|
||||
if _type_utils.JitScalarType.from_value(
|
||||
other, _type_utils.JitScalarType.UNDEFINED
|
||||
) != _type_utils.JitScalarType.from_value(input):
|
||||
other = g.op(
|
||||
"Cast",
|
||||
other,
|
||||
to_i=_type_utils.JitScalarType.from_value(input).onnx_type(),
|
||||
)
|
||||
return _einsum_helper(g, "i,j->ij", [input, other])
|
||||
|
||||
|
||||
def _dropout_returns_masked_input_and_mask(
|
||||
g: jit_utils.GraphContext, input: torch._C.Value, p: float, train: bool
|
||||
) -> tuple[torch._C.Value, torch._C.Value | None]:
|
||||
symbolic_helper.check_training_mode(train, "dropout")
|
||||
# In eval mode, dropout is non-op. That is, if the node's
|
||||
# train param is set to False, dropout just returns its inputs.
|
||||
if not train:
|
||||
return input, None
|
||||
p = g.op("Constant", value_t=torch.tensor(p))
|
||||
t = g.op("Constant", value_t=torch.tensor(train, dtype=torch.bool))
|
||||
r, mask = g.op("Dropout", input, p, t, outputs=2)
|
||||
return r, mask
|
||||
|
||||
|
||||
@_onnx_symbolic("aten::dropout")
|
||||
@symbolic_helper.parse_args("v", "f", "b")
|
||||
def dropout(g: jit_utils.GraphContext, input, p, train):
|
||||
masked, _ = _dropout_returns_masked_input_and_mask(g, input, p, train)
|
||||
return masked
|
||||
|
||||
|
||||
@_onnx_symbolic("aten::native_dropout")
|
||||
@symbolic_helper.parse_args("v", "f", "b")
|
||||
def native_dropout(g: jit_utils.GraphContext, input, p, train):
|
||||
return _dropout_returns_masked_input_and_mask(g, input, p, train)
|
||||
|
||||
|
||||
@_onnx_symbolic("aten::nll_loss")
|
||||
def nll_loss(g: jit_utils.GraphContext, self, target, weight, reduction, ignore_index):
|
||||
# none reduction : onnx::Constant[value={0}]
|
||||
# mean reduction : onnx::Constant[value={1}]
|
||||
# sum reduction : onnx::Constant[value={2}]
|
||||
reduction = symbolic_helper._maybe_get_const(reduction, "i")
|
||||
reduction_vals = ["none", "mean", "sum"]
|
||||
reduction = reduction_vals[reduction]
|
||||
|
||||
# in onnx NegativeLogLikelihoodLoss specification, ignore_index is optional without default value.
|
||||
# therefore we need to set ignore_index attribute even if it is not specified (e.g. ignore_index=-100).
|
||||
ignore_index = symbolic_helper._maybe_get_const(ignore_index, "i")
|
||||
if weight.node().mustBeNone():
|
||||
nllloss = g.op(
|
||||
"NegativeLogLikelihoodLoss",
|
||||
self,
|
||||
target,
|
||||
reduction_s=reduction,
|
||||
ignore_index_i=ignore_index,
|
||||
)
|
||||
else:
|
||||
nllloss = g.op(
|
||||
"NegativeLogLikelihoodLoss",
|
||||
self,
|
||||
target,
|
||||
weight,
|
||||
reduction_s=reduction,
|
||||
ignore_index_i=ignore_index,
|
||||
)
|
||||
|
||||
return nllloss
|
||||
|
||||
|
||||
@_onnx_symbolic("aten::nll_loss2d")
|
||||
def nll_loss2d(
|
||||
g: jit_utils.GraphContext, self, target, weight, reduction, ignore_index
|
||||
):
|
||||
return nll_loss(g, self, target, weight, reduction, ignore_index)
|
||||
|
||||
|
||||
@_onnx_symbolic("aten::nll_loss_nd")
|
||||
def nll_loss_nd(
|
||||
g: jit_utils.GraphContext, self, target, weight, reduction, ignore_index
|
||||
):
|
||||
return nll_loss(g, self, target, weight, reduction, ignore_index)
|
||||
|
||||
|
||||
@_onnx_symbolic("aten::cross_entropy_loss")
|
||||
def cross_entropy_loss(
|
||||
g: jit_utils.GraphContext,
|
||||
self,
|
||||
target,
|
||||
weight,
|
||||
reduction,
|
||||
ignore_index,
|
||||
label_smoothing,
|
||||
):
|
||||
# none reduction : onnx::Constant[value={0}]
|
||||
# mean reduction : onnx::Constant[value={1}]
|
||||
# sum reduction : onnx::Constant[value={2}]
|
||||
reduction = symbolic_helper._maybe_get_const(reduction, "i")
|
||||
reduction_vals = ["none", "mean", "sum"]
|
||||
reduction = reduction_vals[reduction]
|
||||
|
||||
label_smoothing = symbolic_helper._maybe_get_const(label_smoothing, "f")
|
||||
if label_smoothing is not None and label_smoothing > 0.0:
|
||||
raise errors.SymbolicValueError(
|
||||
"Unsupported: ONNX does not support label_smoothing", self
|
||||
)
|
||||
|
||||
# in onnx SoftmaxCrossEntropyLoss specification, ignore_index is optional without default value.
|
||||
# therefore we need to set ignore_index attribute even if it is not specified (e.g. ignore_index=-100).
|
||||
ignore_index = symbolic_helper._maybe_get_const(ignore_index, "i")
|
||||
if weight.node().mustBeNone():
|
||||
celoss = g.op(
|
||||
"SoftmaxCrossEntropyLoss",
|
||||
self,
|
||||
target,
|
||||
reduction_s=reduction,
|
||||
ignore_index_i=ignore_index,
|
||||
)
|
||||
else:
|
||||
celoss = g.op(
|
||||
"SoftmaxCrossEntropyLoss",
|
||||
self,
|
||||
target,
|
||||
weight,
|
||||
reduction_s=reduction,
|
||||
ignore_index_i=ignore_index,
|
||||
)
|
||||
|
||||
return celoss
|
||||
|
||||
|
||||
@_onnx_symbolic("aten::binary_cross_entropy_with_logits")
|
||||
@symbolic_helper.parse_args("v", "v", "v", "v", "i")
|
||||
def binary_cross_entropy_with_logits(
|
||||
g: jit_utils.GraphContext, input, target, weight, pos_weight, reduction
|
||||
):
|
||||
p = g.op("Constant", value_t=torch.tensor([1]))
|
||||
sig_x = opset9.sigmoid(g, input)
|
||||
log_sig_x = opset9.log(g, sig_x)
|
||||
sub_1_x = opset9.sub(g, p, sig_x)
|
||||
sub_1_y = opset9.sub(g, p, target)
|
||||
log_1_x = opset9.log(g, sub_1_x)
|
||||
if pos_weight is None or symbolic_helper._is_none(pos_weight):
|
||||
output = opset9.neg(
|
||||
g,
|
||||
opset9.add(
|
||||
g, opset9.mul(g, target, log_sig_x), opset9.mul(g, sub_1_y, log_1_x)
|
||||
),
|
||||
)
|
||||
else:
|
||||
output = opset9.neg(
|
||||
g,
|
||||
opset9.add(
|
||||
g,
|
||||
opset9.mul(g, opset9.mul(g, target, log_sig_x), pos_weight),
|
||||
opset9.mul(g, sub_1_y, log_1_x),
|
||||
),
|
||||
)
|
||||
|
||||
if weight is not None and not symbolic_helper._is_none(weight):
|
||||
output = opset9.mul(g, weight, output)
|
||||
|
||||
reduction = symbolic_helper._maybe_get_const(reduction, "i")
|
||||
if reduction == 0:
|
||||
return output
|
||||
elif reduction == 1:
|
||||
return g.op("ReduceMean", output, keepdims_i=0)
|
||||
elif reduction == 2:
|
||||
return g.op("ReduceSum", output, keepdims_i=0)
|
||||
else:
|
||||
return symbolic_helper._onnx_unsupported(
|
||||
"binary_cross_entropy_with_logits with reduction other than none, mean, or sum",
|
||||
input,
|
||||
)
|
||||
|
||||
|
||||
@_onnx_symbolic("aten::celu")
|
||||
def celu(g: jit_utils.GraphContext, self, alpha):
|
||||
alpha = symbolic_helper._maybe_get_const(alpha, "f")
|
||||
# if the input is of type double cast it to float
|
||||
if (
|
||||
_type_utils.JitScalarType.from_value(self, _type_utils.JitScalarType.UNDEFINED)
|
||||
== _type_utils.JitScalarType.DOUBLE
|
||||
):
|
||||
self = g.op("Cast", self, to_i=_C_onnx.TensorProtoDataType.FLOAT)
|
||||
out = g.op("Celu", self, alpha_f=alpha)
|
||||
return g.op("Cast", out, to_i=_C_onnx.TensorProtoDataType.DOUBLE)
|
||||
|
||||
return g.op("Celu", self, alpha_f=alpha)
|
||||
|
||||
|
||||
@_onnx_symbolic("aten::argmax")
|
||||
@symbolic_helper.parse_args("v", "v", "b")
|
||||
def argmax(
|
||||
g: jit_utils.GraphContext,
|
||||
input: torch._C.Value,
|
||||
dim: torch._C.Value,
|
||||
keepdim: bool,
|
||||
):
|
||||
return symbolic_helper._argmin_argmax_helper(g, input, dim, keepdim, "ArgMax")
|
||||
|
||||
|
||||
@_onnx_symbolic("aten::argmin")
|
||||
@symbolic_helper.parse_args("v", "v", "b")
|
||||
def argmin(
|
||||
g: jit_utils.GraphContext,
|
||||
input: torch._C.Value,
|
||||
dim: torch._C.Value,
|
||||
keepdim: bool,
|
||||
):
|
||||
return symbolic_helper._argmin_argmax_helper(g, input, dim, keepdim, "ArgMin")
|
||||
|
||||
|
||||
@_onnx_symbolic("aten::pow")
|
||||
def pow(g: jit_utils.GraphContext, self, exponent):
|
||||
return g.op("Pow", self, exponent)
|
||||
|
||||
|
||||
@_onnx_symbolic("aten::ge")
|
||||
def ge(g: jit_utils.GraphContext, input, other):
|
||||
return g.op("GreaterOrEqual", input, other)
|
||||
|
||||
|
||||
@_onnx_symbolic("aten::le")
|
||||
def le(g: jit_utils.GraphContext, input, other):
|
||||
return g.op("LessOrEqual", input, other)
|
||||
|
||||
|
||||
@_onnx_symbolic("aten::unfold")
|
||||
@symbolic_helper.parse_args("v", "i", "v", "v")
|
||||
def unfold(g: jit_utils.GraphContext, input, dimension, size, step):
|
||||
const_size = symbolic_helper._maybe_get_const(size, "i")
|
||||
const_step = symbolic_helper._maybe_get_const(step, "i")
|
||||
if not symbolic_helper._is_value(const_size) and not symbolic_helper._is_value(
|
||||
const_step
|
||||
):
|
||||
return opset9.unfold(g, input, dimension, const_size, const_step)
|
||||
|
||||
sizedim = symbolic_helper._get_tensor_dim_size(input, dimension)
|
||||
if sizedim is not None:
|
||||
low_start = g.op("Constant", value_t=torch.tensor(0))
|
||||
low_end = g.op("Constant", value_t=torch.tensor(sizedim))
|
||||
hi_end = g.op("Constant", value_t=torch.tensor(sizedim + 1))
|
||||
low_indices = g.op("Range", low_start, low_end, step)
|
||||
hi_indices = g.op("Range", size, hi_end, step)
|
||||
|
||||
low_size = symbolic_helper._size_helper(
|
||||
g, low_indices, g.op("Constant", value_t=torch.tensor(0))
|
||||
)
|
||||
hi_size = symbolic_helper._size_helper(
|
||||
g, hi_indices, g.op("Constant", value_t=torch.tensor(0))
|
||||
)
|
||||
|
||||
ndim = symbolic_helper._get_tensor_rank(input)
|
||||
if ndim is None:
|
||||
raise AssertionError("ndim must be non-None")
|
||||
perm = list(range(ndim))
|
||||
perm.append(perm.pop(dimension))
|
||||
|
||||
unsqueeze_list = []
|
||||
loop_condition = g.op("Constant", value_t=torch.tensor(1))
|
||||
loop_condition = g.op(
|
||||
"Cast", loop_condition, to_i=_C_onnx.TensorProtoDataType.BOOL
|
||||
)
|
||||
loop_len = g.op("Min", low_size, hi_size)
|
||||
|
||||
loop, (loop_context,), _ = jit_utils.add_op_with_blocks(
|
||||
g, "Loop", loop_len, loop_condition, n_blocks=1
|
||||
)
|
||||
|
||||
loop_block = loop_context.block
|
||||
block_input_iter = utils._add_input_to_block(loop_block)
|
||||
cond = utils._add_input_to_block(loop_block) # noqa: F841
|
||||
|
||||
starts = loop_context.op("Gather", low_indices, block_input_iter)
|
||||
ends = loop_context.op("Gather", hi_indices, block_input_iter)
|
||||
axes = loop_context.op("Constant", value_t=torch.tensor([2]))
|
||||
starts = symbolic_helper._unsqueeze_helper(loop_context, starts, [0])
|
||||
ends = symbolic_helper._unsqueeze_helper(loop_context, ends, [0])
|
||||
stack = loop_context.op("Slice", input, starts, ends, axes)
|
||||
|
||||
unsqueeze = symbolic_helper._unsqueeze_helper(
|
||||
loop_context, loop_context.op("Transpose", stack, perm_i=perm), [dimension]
|
||||
)
|
||||
unsqueeze_list.append(unsqueeze)
|
||||
concat = loop_context.op("Concat", *unsqueeze_list, axis_i=0)
|
||||
|
||||
cond_out = loop_context.op(
|
||||
"Cast",
|
||||
loop_condition,
|
||||
# pyrefly: ignore [bad-argument-type]
|
||||
_C_onnx.TensorProtoDataType.BOOL,
|
||||
)
|
||||
utils._add_output_to_block(loop_block, cond_out)
|
||||
utils._add_output_to_block(loop_block, concat)
|
||||
|
||||
loop_output = loop.node().output()
|
||||
perm = [0, 1, 2, 3, 4]
|
||||
perm[0], perm[dimension + 1] = perm[dimension + 1], perm[0]
|
||||
transpose = g.op("Transpose", loop_output, perm_i=perm)
|
||||
squeeze = symbolic_helper._squeeze_helper(g, transpose, [0])
|
||||
|
||||
return squeeze
|
||||
|
||||
return symbolic_helper._unimplemented("Unfold", "input size not accessible")
|
||||
|
||||
|
||||
@_onnx_symbolic("aten::tensordot")
|
||||
@symbolic_helper.parse_args("v", "v", "is", "is", "v")
|
||||
def tensordot(g: jit_utils.GraphContext, input_a, input_b, dims_a, dims_b, out=None):
|
||||
if out is not None:
|
||||
symbolic_helper._unimplemented(
|
||||
"Tensordot", "Out parameter is not supported for tensordot."
|
||||
)
|
||||
|
||||
dim_count_a = symbolic_helper._get_tensor_rank(input_a)
|
||||
if dim_count_a is None:
|
||||
raise errors.SymbolicValueError(
|
||||
"Unsupported: ONNX export of tensordot for tensor(input_a) of unknown rank.",
|
||||
input_a,
|
||||
)
|
||||
|
||||
dim_count_b = symbolic_helper._get_tensor_rank(input_b)
|
||||
if dim_count_b is None:
|
||||
raise errors.SymbolicValueError(
|
||||
"Unsupported: ONNX export of tensordot for tensor(input_b) of unknown rank.",
|
||||
input_b,
|
||||
)
|
||||
|
||||
dims_a = [
|
||||
(dims_a[i] + dim_count_a) if (dims_a[i] < 0) else dims_a[i]
|
||||
for i in range(len(dims_a))
|
||||
]
|
||||
dims_b = [
|
||||
(dims_b[i] + dim_count_b) if (dims_b[i] < 0) else dims_b[i]
|
||||
for i in range(len(dims_b))
|
||||
]
|
||||
|
||||
left_dims_a = [i for i in range(dim_count_a) if (i not in dims_a)]
|
||||
left_dims_b = [i for i in range(dim_count_b) if (i not in dims_b)]
|
||||
|
||||
new_input_a = opset9.permute(g, input_a, left_dims_a + dims_a)
|
||||
new_input_b = opset9.permute(g, input_b, dims_b + left_dims_b)
|
||||
|
||||
input_shape = g.op("Shape", new_input_a)
|
||||
left_sizes_a = symbolic_helper._slice_helper(
|
||||
g, input_shape, axes=[0], starts=[0], ends=[len(left_dims_a)]
|
||||
)
|
||||
shape_sizes = [
|
||||
left_sizes_a,
|
||||
g.op("Constant", value_t=torch.tensor([-1], dtype=torch.long)),
|
||||
]
|
||||
output_a = opset9._reshape_from_tensor(g, new_input_a, shape_sizes)
|
||||
|
||||
input_shape = g.op("Shape", output_a)
|
||||
slices = symbolic_helper._slice_helper(
|
||||
g, input_shape, axes=[0], starts=[-1], ends=[sys.maxsize]
|
||||
)
|
||||
shape_sizes = [
|
||||
g.op("Constant", value_t=torch.tensor([-1], dtype=torch.long)),
|
||||
slices,
|
||||
]
|
||||
output_a = opset9._reshape_from_tensor(g, new_input_a, shape_sizes)
|
||||
|
||||
input_shape = g.op("Shape", new_input_b)
|
||||
left_sizes_b = symbolic_helper._slice_helper(
|
||||
g, input_shape, axes=[0], starts=[len(dims_b)], ends=[sys.maxsize]
|
||||
)
|
||||
slices = symbolic_helper._slice_helper(
|
||||
g, input_shape, axes=[0], starts=[0], ends=[len(dims_b)]
|
||||
)
|
||||
shape_sizes = [
|
||||
slices,
|
||||
g.op("Constant", value_t=torch.tensor([-1], dtype=torch.long)),
|
||||
]
|
||||
output_b = opset9._reshape_from_tensor(g, new_input_b, shape_sizes)
|
||||
|
||||
input_shape = g.op("Shape", output_b)
|
||||
slices = symbolic_helper._slice_helper(
|
||||
g, input_shape, axes=[0], starts=[-1], ends=[sys.maxsize]
|
||||
)
|
||||
shape_sizes = [
|
||||
g.op("Constant", value_t=torch.tensor([-1], dtype=torch.long)),
|
||||
slices,
|
||||
]
|
||||
output_b = opset9._reshape_from_tensor(g, new_input_b, shape_sizes)
|
||||
|
||||
output = einsum(g, "ij,jk->ik", g.op("prim::ListConstruct", *[output_a, output_b]))
|
||||
|
||||
shape_sizes = [left_sizes_a, left_sizes_b]
|
||||
return opset9._reshape_from_tensor(g, output, shape_sizes)
|
||||
+1118
File diff suppressed because it is too large
Load Diff
+301
@@ -0,0 +1,301 @@
|
||||
# mypy: allow-untyped-defs
|
||||
# mypy: disable-error-code=arg-type
|
||||
"""This file exports ONNX ops for opset 14.
|
||||
|
||||
Note [ONNX operators that are added/updated in opset 14]
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
New operators:
|
||||
HardSwish, Trilu
|
||||
|
||||
Updated operators:
|
||||
Reshape
|
||||
Add, Sub, Mul, Div
|
||||
GRU, LSTM, RNN
|
||||
BatchNorm, Cumsum, Relu
|
||||
"""
|
||||
|
||||
# EDITING THIS FILE? READ THIS FIRST!
|
||||
# see Note [Edit Symbolic Files] in README.md
|
||||
from __future__ import annotations
|
||||
|
||||
import functools
|
||||
|
||||
import torch
|
||||
from torch.onnx import _constants
|
||||
from torch.onnx._internal.torchscript_exporter import (
|
||||
_type_utils,
|
||||
jit_utils,
|
||||
registration,
|
||||
symbolic_helper,
|
||||
)
|
||||
from torch.onnx._internal.torchscript_exporter._globals import GLOBALS
|
||||
|
||||
|
||||
__all__ = [
|
||||
"hardswish",
|
||||
"tril",
|
||||
"triu",
|
||||
"reshape",
|
||||
"batch_norm",
|
||||
"quantized_hardswish",
|
||||
"scaled_dot_product_attention",
|
||||
]
|
||||
|
||||
_onnx_symbolic = functools.partial(registration.onnx_symbolic, opset=14)
|
||||
|
||||
|
||||
@_onnx_symbolic("aten::hardswish")
|
||||
@symbolic_helper.parse_args("v")
|
||||
def hardswish(g: jit_utils.GraphContext, self):
|
||||
return g.op("HardSwish", self)
|
||||
|
||||
|
||||
@_onnx_symbolic("aten::tril")
|
||||
def tril(g: jit_utils.GraphContext, self, diagonal, out=None):
|
||||
return g.op("Trilu", self, diagonal, upper_i=0)
|
||||
|
||||
|
||||
@_onnx_symbolic("aten::triu")
|
||||
def triu(g: jit_utils.GraphContext, self, diagonal, out=None):
|
||||
return g.op("Trilu", self, diagonal, upper_i=1)
|
||||
|
||||
|
||||
@_onnx_symbolic("aten::reshape")
|
||||
@symbolic_helper.quantized_args(True)
|
||||
@symbolic_helper.parse_args("v", "v")
|
||||
def reshape(g: jit_utils.GraphContext, self, shape):
|
||||
# NOTE: Due to bug in ORT https://github.com/microsoft/onnxruntime/issues/10664
|
||||
# Reshape export cannot utilize the new allowzero attribute introduced in opset 14.
|
||||
return symbolic_helper._reshape_helper(g, self, shape, allowzero=0)
|
||||
|
||||
|
||||
@_onnx_symbolic("aten::batch_norm")
|
||||
@symbolic_helper.parse_args("v", "v", "v", "v", "v", "i", "f", "f", "i")
|
||||
def batch_norm(
|
||||
g: jit_utils.GraphContext,
|
||||
input,
|
||||
weight,
|
||||
bias,
|
||||
running_mean,
|
||||
running_var,
|
||||
training,
|
||||
momentum,
|
||||
eps,
|
||||
cudnn_enabled,
|
||||
):
|
||||
if (
|
||||
torch.is_autocast_enabled()
|
||||
and not symbolic_helper.args_have_same_dtype(
|
||||
[input, weight, bias, running_mean, running_var]
|
||||
)
|
||||
and GLOBALS.export_onnx_opset_version < 15
|
||||
):
|
||||
return symbolic_helper._onnx_opset_unsupported_detailed(
|
||||
"BatchNormalization",
|
||||
14,
|
||||
15,
|
||||
"All input tensors must have the same `dtype`."
|
||||
" Turn off Autocast or export using opset version 15.",
|
||||
input,
|
||||
)
|
||||
|
||||
symbolic_helper.check_training_mode(training, "batch_norm")
|
||||
weight, bias, running_mean, running_var = symbolic_helper._batchnorm_helper(
|
||||
g, input, weight, bias, running_mean, running_var
|
||||
)
|
||||
out = g.op(
|
||||
"BatchNormalization",
|
||||
input,
|
||||
weight,
|
||||
bias,
|
||||
running_mean,
|
||||
running_var,
|
||||
epsilon_f=eps,
|
||||
momentum_f=1 - momentum,
|
||||
training_mode_i=0 if not training else 1,
|
||||
outputs=1 if not training else 3,
|
||||
)
|
||||
if not training:
|
||||
return out
|
||||
else:
|
||||
res, new_running_mean, new_running_var = out
|
||||
new_running_mean.setType(running_mean.type())
|
||||
new_running_var.setType(running_var.type())
|
||||
return res
|
||||
|
||||
|
||||
@_onnx_symbolic("quantized::hardswish")
|
||||
def quantized_hardswish(g: jit_utils.GraphContext, x, op_scale, op_zero_point):
|
||||
x, _, _, _ = symbolic_helper.dequantize_helper(g, x)
|
||||
|
||||
output = hardswish(g, x)
|
||||
|
||||
return symbolic_helper.quantize_helper(g, output, op_scale, op_zero_point)
|
||||
|
||||
|
||||
# Ported from
|
||||
# https://github.com/microsoft/onnxscript/blob/6b1b81700b4523f31d8c6d3321e5d8ef5d42b764/onnxscript/function_libs/torch_aten/ops/nn.py#L1504
|
||||
# aten_scaled_dot_product_attention
|
||||
# NOTE: Need op.Trilu
|
||||
@_onnx_symbolic("aten::scaled_dot_product_attention")
|
||||
@symbolic_helper.parse_args("v", "v", "v", "v", "f", "b", "v", "b")
|
||||
def scaled_dot_product_attention(
|
||||
g: jit_utils.GraphContext,
|
||||
query: torch._C.Value,
|
||||
key: torch._C.Value,
|
||||
value: torch._C.Value,
|
||||
attn_mask: torch._C.Value | None = None,
|
||||
dropout_p: float = 0.0,
|
||||
is_causal: bool = False,
|
||||
scale: torch._C.Value | None = None,
|
||||
enable_gqa: bool = False,
|
||||
):
|
||||
if is_causal and not symbolic_helper._is_none(attn_mask):
|
||||
raise AssertionError("is_causal and attn_mask cannot be set at the same time")
|
||||
if enable_gqa:
|
||||
raise AssertionError(
|
||||
"conversion of scaled_dot_product_attention not implemented if enable_gqa is True"
|
||||
)
|
||||
|
||||
if symbolic_helper._is_none(scale):
|
||||
scale = _attention_scale(g, query)
|
||||
|
||||
if is_causal:
|
||||
attn_mask = _causal_attention_mask(g, query, key)
|
||||
|
||||
# Swap the last two axes of key
|
||||
# NOTE: onnx-script has different logic here, because the attribute perms in
|
||||
# transpose needs list of ints
|
||||
key_shape_builtin = symbolic_helper._get_tensor_rank(key)
|
||||
# pyrefly: ignore [bad-argument-type, no-matching-overload]
|
||||
key_transposed_axes = list(range(key_shape_builtin))
|
||||
key_transposed_axes[-1], key_transposed_axes[-2] = (
|
||||
key_transposed_axes[-2],
|
||||
key_transposed_axes[-1],
|
||||
)
|
||||
key_transposed = g.op("Transpose", key, perm_i=key_transposed_axes)
|
||||
|
||||
# https://github.com/pytorch/pytorch/blob/12da0c70378b5be9135c6fda62a9863bce4a4818/aten/src/ATen/native/transformers/attention.cpp#L653
|
||||
# Scale q, k before matmul for stability see https://tinyurl.com/sudb9s96 for math
|
||||
# pyrefly: ignore [bad-argument-type]
|
||||
query_scaled = g.op("Mul", query, g.op("Sqrt", scale))
|
||||
# pyrefly: ignore [bad-argument-type]
|
||||
key_transposed_scaled = g.op("Mul", key_transposed, g.op("Sqrt", scale))
|
||||
mul_qk = g.op("MatMul", query_scaled, key_transposed_scaled)
|
||||
|
||||
if symbolic_helper._is_none(attn_mask):
|
||||
mul_qk_add = mul_qk
|
||||
attn_weight = g.op("Softmax", mul_qk_add, axis_i=-1)
|
||||
elif (
|
||||
_type_utils.JitScalarType.from_value(attn_mask)
|
||||
== _type_utils.JitScalarType.BOOL
|
||||
):
|
||||
# Turn the Boolean mask to float: attn_mask.masked_fill(not attn_mask, -float('inf'))
|
||||
const_zero = g.op("Constant", value_t=torch.tensor([0.0]))
|
||||
const_neg_inf = g.op("Constant", value_t=torch.tensor([-float("inf")]))
|
||||
# pyrefly: ignore [bad-argument-type]
|
||||
attn_mask = g.op("Where", attn_mask, const_zero, const_neg_inf)
|
||||
mul_qk_add = g.op("Add", mul_qk, attn_mask)
|
||||
attn_weight = g.op("Softmax", mul_qk_add, axis_i=-1)
|
||||
# When using scaled dot product attention with a boolean mask, the softmax operation might return NaN values
|
||||
# due to the presence of -inf in an entire row (padding tokens), resulting in 0/0 (NaN) in the softmax output.
|
||||
# This is because there's no safe softmax imp in ONNX, so we need to handle NaN values explicitly to match
|
||||
# the behavior of PyTorch with boolean masks.
|
||||
attn_weight = g.op("Where", g.op("IsNaN", attn_weight), const_zero, attn_weight)
|
||||
elif _type_utils.JitScalarType.from_value(attn_mask) in (
|
||||
_type_utils.JitScalarType.FLOAT,
|
||||
_type_utils.JitScalarType.HALF,
|
||||
_type_utils.JitScalarType.BFLOAT16,
|
||||
):
|
||||
# pyrefly: ignore [bad-argument-type]
|
||||
mul_qk_add = g.op("Add", mul_qk, attn_mask)
|
||||
attn_weight = g.op("Softmax", mul_qk_add, axis_i=-1)
|
||||
else:
|
||||
raise ValueError(
|
||||
f"Unsupported type for attn_mask: {_type_utils.JitScalarType.from_value(attn_mask)}"
|
||||
)
|
||||
|
||||
if dropout_p != 0:
|
||||
attn_weight = g.op(
|
||||
"Dropout",
|
||||
attn_weight,
|
||||
g.op("Constant", value_t=torch.tensor(dropout_p, dtype=torch.float)),
|
||||
)
|
||||
|
||||
return g.op("MatMul", attn_weight, value)
|
||||
|
||||
|
||||
def _attention_scale(
|
||||
g: jit_utils.GraphContext, query: torch._C.Value
|
||||
) -> torch._C.Value:
|
||||
"""Calculate the scale factor for the attention result.
|
||||
|
||||
Args:
|
||||
query: Tensor of shape [..., L, E]
|
||||
|
||||
Returns:
|
||||
Scalar scale factor := 1 / math.sqrt(query.size(-1))
|
||||
"""
|
||||
query_shape = g.op("Shape", query)
|
||||
query_shape_last = g.op(
|
||||
"Slice",
|
||||
query_shape,
|
||||
g.op("Constant", value_t=torch.tensor([-1], dtype=torch.int64)),
|
||||
g.op(
|
||||
"Constant", value_t=torch.tensor([_constants.INT64_MAX], dtype=torch.int64)
|
||||
),
|
||||
)
|
||||
embedding_size = g.op(
|
||||
"Cast",
|
||||
query_shape_last,
|
||||
to_i=_type_utils.JitScalarType.from_value(query).onnx_type(),
|
||||
)
|
||||
const_one = g.op("Constant", value_t=torch.tensor([1.0], dtype=torch.float))
|
||||
scale = g.op("Div", const_one, g.op("Sqrt", embedding_size))
|
||||
# Add a Cast to convert the scale back to original type
|
||||
scale = g.op(
|
||||
"Cast",
|
||||
scale,
|
||||
to_i=_type_utils.JitScalarType.from_value(query).onnx_type(),
|
||||
)
|
||||
return scale
|
||||
|
||||
|
||||
def _causal_attention_mask(
|
||||
g: jit_utils.GraphContext, query: torch._C.Value, key: torch._C.Value
|
||||
) -> torch._C.Value:
|
||||
"""Create a causal mask for the given query and key tensors.
|
||||
|
||||
Equivalent to::
|
||||
mask = torch.ones(L, S, dtype=torch.bool).tril(diagonal=0)
|
||||
attn_mask = torch.zeros(L, S, dtype=torch.float)
|
||||
attn_mask = attn_mask.masked_fill(not mask, -float("inf"))
|
||||
|
||||
Args:
|
||||
query: Tensor of shape [..., L, E]
|
||||
key: Tensor of shape [..., S, E]
|
||||
|
||||
Returns:
|
||||
Tensor of shape [L, S]
|
||||
"""
|
||||
|
||||
query_shape = g.op("Shape", query)
|
||||
key_shape = g.op("Shape", key)
|
||||
|
||||
last_idx = g.op("Constant", value_t=torch.tensor([-1], dtype=torch.int64))
|
||||
second_last_idx = g.op("Constant", value_t=torch.tensor([-2], dtype=torch.int64))
|
||||
target_length = g.op("Slice", query_shape, second_last_idx, last_idx)
|
||||
source_length = g.op("Slice", key_shape, second_last_idx, last_idx)
|
||||
# attn_mask = torch.ones(L, S) := {
|
||||
size = g.op("Concat", target_length, source_length, axis_i=0)
|
||||
const_one = g.op("Constant", value_t=torch.tensor([1.0]))
|
||||
attn_mask = g.op("Expand", const_one, size)
|
||||
# }
|
||||
attn_mask = g.op("Trilu", attn_mask, upper_i=0)
|
||||
# The causal mask has 0s in the lower triangle and -inf in the upper triangle.
|
||||
const_zero = g.op("Constant", value_t=torch.tensor([0.0]))
|
||||
const_neg_inf = g.op("Constant", value_t=torch.tensor([-float("inf")]))
|
||||
attn_mask = g.op(
|
||||
"Where", g.op("Equal", attn_mask, const_zero), const_neg_inf, const_zero
|
||||
)
|
||||
return attn_mask
|
||||
+84
@@ -0,0 +1,84 @@
|
||||
# mypy: allow-untyped-defs
|
||||
"""This file exports ONNX ops for opset 15.
|
||||
|
||||
Note [ONNX operators that are added/updated in opset 15]
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
https://github.com/onnx/onnx/blob/master/docs/Changelog.md#version-15-of-the-default-onnx-operator-set
|
||||
New operators:
|
||||
Bernoulli
|
||||
CastLike
|
||||
Optional
|
||||
OptionalGetElement
|
||||
OptionalHasElement
|
||||
|
||||
Updated operators:
|
||||
BatchNormalization https://github.com/onnx/onnx/pull/3545
|
||||
Backwards compatible
|
||||
TODO: test coverage for mixed types inputs.
|
||||
Pow https://github.com/onnx/onnx/pull/3412
|
||||
Backwards compatible
|
||||
TODO: bfloat16 support.
|
||||
Shape https://github.com/onnx/onnx/pull/3580
|
||||
Backwards compatible
|
||||
TODO: optional start/end attribute.
|
||||
"""
|
||||
|
||||
# EDITING THIS FILE? READ THIS FIRST!
|
||||
# see Note [Edit Symbolic Files] in README.md
|
||||
|
||||
import functools
|
||||
|
||||
import torch
|
||||
from torch import _C
|
||||
from torch.onnx._internal.torchscript_exporter import (
|
||||
jit_utils,
|
||||
registration,
|
||||
symbolic_helper,
|
||||
symbolic_opset9 as opset9,
|
||||
)
|
||||
|
||||
|
||||
_onnx_symbolic = functools.partial(registration.onnx_symbolic, opset=15)
|
||||
|
||||
|
||||
@_onnx_symbolic("aten::__is_")
|
||||
def aten__is_(g: jit_utils.GraphContext, self, other):
|
||||
if symbolic_helper._is_none(other):
|
||||
if isinstance(self.type(), _C.OptionalType):
|
||||
none = g.op("OptionalHasElement", self)
|
||||
return g.op("Not", none)
|
||||
else:
|
||||
return g.op("Constant", value_t=torch.BoolTensor([0]))
|
||||
return opset9.eq(g, self, other)
|
||||
|
||||
|
||||
@_onnx_symbolic("aten::__isnot_")
|
||||
@opset9.wrap_logical_op_with_negation # type: ignore[has-type]
|
||||
def aten__isnot_(g: jit_utils.GraphContext, self, other):
|
||||
return aten__is_(g, self, other)
|
||||
|
||||
|
||||
@_onnx_symbolic("aten::bernoulli")
|
||||
def bernoulli(g: jit_utils.GraphContext, input, p=None, generator=None, out=None):
|
||||
if out is not None and not symbolic_helper._is_none(out):
|
||||
symbolic_helper._unimplemented(
|
||||
"Bernoulli", "out parameter is not supported for bernoulli", input
|
||||
)
|
||||
if generator is not None and not symbolic_helper._is_none(generator):
|
||||
symbolic_helper._unimplemented(
|
||||
"Bernoulli", "generator is not supported for bernoulli", input
|
||||
)
|
||||
if p is None or symbolic_helper._is_none(p):
|
||||
return g.op("Bernoulli", input)
|
||||
return opset9.bernoulli(g, input, p, generator, out)
|
||||
|
||||
|
||||
@_onnx_symbolic("prim::unchecked_cast")
|
||||
def prim_unchecked_cast(g: jit_utils.GraphContext, self):
|
||||
# exists to refine the type of the Value
|
||||
# if x is Optional[Tensor], unchecked_cast will cast
|
||||
# x to Tensor, so the rest of the graph knows that x is a Tensor.
|
||||
if isinstance(self.type(), _C.OptionalType):
|
||||
return g.op("OptionalGetElement", self)
|
||||
|
||||
return self
|
||||
+191
@@ -0,0 +1,191 @@
|
||||
# mypy: allow-untyped-defs
|
||||
"""This file exports ONNX ops for opset 16.
|
||||
|
||||
Note [ONNX Operators that are added/updated in opset 16]
|
||||
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
https://github.com/onnx/onnx/blob/main/docs/Changelog.md#version-16-of-the-default-onnx-operator-set
|
||||
New operators:
|
||||
GridSample https://github.com/onnx/onnx/pull/3557
|
||||
|
||||
Updated operators:
|
||||
Identity
|
||||
If
|
||||
LeakyRelu
|
||||
Loop
|
||||
PRelu
|
||||
RoiAlign
|
||||
Scan
|
||||
ScatterElements
|
||||
ScatterND
|
||||
Where
|
||||
GreaterOrEqual
|
||||
LessOrEqual
|
||||
"""
|
||||
|
||||
# EDITING THIS FILE? READ THIS FIRST!
|
||||
# see Note [Edit Symbolic Files] in README.md
|
||||
|
||||
import functools
|
||||
|
||||
import torch
|
||||
from torch.nn.functional import (
|
||||
GRID_SAMPLE_INTERPOLATION_MODES,
|
||||
GRID_SAMPLE_PADDING_MODES,
|
||||
)
|
||||
from torch.onnx import errors
|
||||
from torch.onnx._internal.torchscript_exporter import (
|
||||
_type_utils,
|
||||
jit_utils,
|
||||
registration,
|
||||
symbolic_helper,
|
||||
utils,
|
||||
)
|
||||
|
||||
|
||||
_onnx_symbolic = functools.partial(registration.onnx_symbolic, opset=16)
|
||||
|
||||
|
||||
# note (mkozuki): Why `grid_sampler` instead of `grid_sample`?
|
||||
# Because `torch.nn.functional.grid_sample` calls `torch.grid_sampler`.
|
||||
@_onnx_symbolic("aten::grid_sampler")
|
||||
@symbolic_helper.parse_args("v", "v", "i", "i", "b")
|
||||
def grid_sampler(
|
||||
g: jit_utils.GraphContext,
|
||||
input,
|
||||
grid,
|
||||
mode_enum,
|
||||
padding_mode_enum,
|
||||
align_corners,
|
||||
):
|
||||
# Check the input and grid tensor rank beforehand.
|
||||
if symbolic_helper._get_tensor_rank(input) == 5:
|
||||
return symbolic_helper._onnx_unsupported("GridSample with 5D volumetric input")
|
||||
mode_s = {v: k for k, v in GRID_SAMPLE_INTERPOLATION_MODES.items()}[mode_enum] # type: ignore[call-arg]
|
||||
padding_mode_s = {v: k for k, v in GRID_SAMPLE_PADDING_MODES.items()}[ # type: ignore[call-arg]
|
||||
padding_mode_enum
|
||||
]
|
||||
return g.op(
|
||||
"GridSample",
|
||||
input,
|
||||
grid,
|
||||
align_corners_i=int(align_corners),
|
||||
mode_s=mode_s,
|
||||
padding_mode_s=padding_mode_s,
|
||||
)
|
||||
|
||||
|
||||
@_onnx_symbolic("aten::scatter_add")
|
||||
@symbolic_helper.parse_args("v", "i", "v", "v")
|
||||
def scatter_add(g: jit_utils.GraphContext, self, dim, index, src):
|
||||
src_type = _type_utils.JitScalarType.from_value(
|
||||
src, _type_utils.JitScalarType.UNDEFINED
|
||||
)
|
||||
src_sizes = symbolic_helper._get_tensor_sizes(src)
|
||||
index_sizes = symbolic_helper._get_tensor_sizes(index)
|
||||
|
||||
if len(src_sizes) != len(index_sizes):
|
||||
return symbolic_helper._unimplemented(
|
||||
"scatter_add",
|
||||
f"`index` ({index_sizes}) should have the same dimensionality as `src` ({src_sizes})",
|
||||
)
|
||||
|
||||
# PyTorch only allows index shape <= src shape, so we can only consider
|
||||
# taking index as subset size to src, like PyTorch does. When sizes for src
|
||||
# and index are not matched or there are dynamic axes, we take index shape to
|
||||
# slice src to accommodate.
|
||||
if src_sizes != index_sizes or None in index_sizes:
|
||||
adjusted_shape = g.op("Shape", index)
|
||||
starts = g.op("Constant", value_t=torch.tensor([0] * len(index_sizes)))
|
||||
src = g.op("Slice", src, starts, adjusted_shape)
|
||||
|
||||
src = symbolic_helper._maybe_get_scalar(src)
|
||||
if symbolic_helper._is_value(src):
|
||||
return g.op("ScatterElements", self, index, src, axis_i=dim, reduction_s="add")
|
||||
else:
|
||||
# Check if scalar "src" has same type as self (PyTorch allows different
|
||||
# type for scalar src (but not when src is tensor)). If not, insert Cast node.
|
||||
if _type_utils.JitScalarType.from_value(self) != src_type:
|
||||
src = g.op(
|
||||
"Cast",
|
||||
src,
|
||||
to_i=_type_utils.JitScalarType.from_value(self).onnx_type(),
|
||||
)
|
||||
|
||||
return g.op(
|
||||
"ScatterElements",
|
||||
self,
|
||||
index,
|
||||
src,
|
||||
axis_i=dim,
|
||||
reduction_s="add",
|
||||
)
|
||||
|
||||
|
||||
@_onnx_symbolic("aten::scatter_reduce")
|
||||
@symbolic_helper.parse_args("v", "i", "v", "v", "s", "b")
|
||||
def scatter_reduce(
|
||||
g: jit_utils.GraphContext,
|
||||
self: torch._C.Value,
|
||||
dim: int,
|
||||
index: torch._C.Value,
|
||||
src: torch._C.Value,
|
||||
reduce: str,
|
||||
include_self: bool,
|
||||
):
|
||||
if reduce == "mean":
|
||||
raise errors.OnnxExporterError(
|
||||
"ONNX does not support mean reduction for scatter_reduce"
|
||||
)
|
||||
if not include_self:
|
||||
raise errors.OnnxExporterError(
|
||||
"ONNX does not support include_self=False for scatter_reduce"
|
||||
)
|
||||
|
||||
reduce_mode = { # convert torch string name to onnx string name
|
||||
"mean": "none", # 'mean' doesn't support in ONNX 1.14 definition
|
||||
"sum": "add",
|
||||
"prod": "mul",
|
||||
"amin": "min",
|
||||
"amax": "max",
|
||||
}
|
||||
onnx_reduce = reduce_mode[reduce]
|
||||
|
||||
self_rank = g.op("Size", g.op("Shape", self))
|
||||
|
||||
# if self_rank == 0: # assert (index_rank == 0 and rank_src == 0)
|
||||
self_rank_is_zero = g.op(
|
||||
"Equal", self_rank, g.op("Constant", value_t=torch.tensor(0, dtype=torch.int64))
|
||||
)
|
||||
if_op, (if_context, else_context), _ = jit_utils.add_op_with_blocks(
|
||||
g, "If", self_rank_is_zero, n_blocks=2, outputs=3
|
||||
)
|
||||
neg_1 = if_context.op("Constant", value_t=torch.tensor([-1], dtype=torch.int64))
|
||||
|
||||
self_reshape = if_context.op("Reshape", self, neg_1)
|
||||
utils._add_output_to_block(if_context.block, self_reshape)
|
||||
index_reshape = if_context.op("Reshape", index, neg_1)
|
||||
utils._add_output_to_block(if_context.block, index_reshape)
|
||||
src_reshape = if_context.op("Reshape", src, neg_1)
|
||||
utils._add_output_to_block(if_context.block, src_reshape)
|
||||
|
||||
self_identity = else_context.op("Identity", self)
|
||||
utils._add_output_to_block(else_context.block, self_identity)
|
||||
index_identitye = else_context.op("Identity", index)
|
||||
utils._add_output_to_block(else_context.block, index_identitye)
|
||||
src_identity = else_context.op("Identity", src)
|
||||
utils._add_output_to_block(else_context.block, src_identity)
|
||||
|
||||
result = g.op("ScatterElements", *if_op, axis_i=dim, reduction_s=onnx_reduce)
|
||||
|
||||
# if self_rank == 0:
|
||||
if_op, (if_context, else_context), _ = jit_utils.add_op_with_blocks(
|
||||
g, "If", self_rank_is_zero, n_blocks=2, outputs=1
|
||||
)
|
||||
result_squeezed = if_context.op("Squeeze", result)
|
||||
utils._add_output_to_block(if_context.block, result_squeezed)
|
||||
result_identity = else_context.op("Identity", result)
|
||||
utils._add_output_to_block(else_context.block, result_identity)
|
||||
result_final = if_op.node().output()
|
||||
|
||||
return result_final
|
||||
+252
@@ -0,0 +1,252 @@
|
||||
# mypy: allow-untyped-defs
|
||||
# mypy: disable-error-code=arg-type
|
||||
"""This file exports ONNX ops for opset 17.
|
||||
|
||||
Note [ONNX Operators that are added/updated in opset 17]
|
||||
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
https://github.com/onnx/onnx/blob/main/docs/Changelog.md#version-17-of-the-default-onnx-operator-set
|
||||
New operators:
|
||||
BlackmanWindow
|
||||
DFT
|
||||
HammingWindow
|
||||
HannWindow
|
||||
LayerNormalization
|
||||
MelWeightMatrix
|
||||
STFT
|
||||
SequenceMap
|
||||
"""
|
||||
|
||||
import functools
|
||||
from collections.abc import Sequence
|
||||
|
||||
import torch
|
||||
from torch import _C
|
||||
from torch.onnx import errors
|
||||
from torch.onnx._internal.torchscript_exporter import (
|
||||
_type_utils,
|
||||
jit_utils,
|
||||
registration,
|
||||
symbolic_helper,
|
||||
)
|
||||
|
||||
|
||||
# EDITING THIS FILE? READ THIS FIRST!
|
||||
# see Note [Edit Symbolic Files] in README.md
|
||||
|
||||
__all__ = ["layer_norm", "stft", "quantized_layer_norm"]
|
||||
|
||||
_onnx_symbolic = functools.partial(registration.onnx_symbolic, opset=17)
|
||||
|
||||
|
||||
@_onnx_symbolic("aten::layer_norm")
|
||||
@symbolic_helper.parse_args("v", "is", "v", "v", "f", "none")
|
||||
def layer_norm(
|
||||
g: jit_utils.GraphContext,
|
||||
input: _C.Value,
|
||||
normalized_shape: Sequence[int],
|
||||
weight: _C.Value,
|
||||
bias: _C.Value,
|
||||
eps: float,
|
||||
cudnn_enable: bool,
|
||||
):
|
||||
# normalized_shape: input shape from an expected input of size
|
||||
# axis: The first normalization dimension.
|
||||
# layer_norm normalizes on the last D dimensions,
|
||||
# where D is the size of normalized_shape
|
||||
axis = -len(normalized_shape)
|
||||
scalar_type = _type_utils.JitScalarType.from_value(
|
||||
input, _type_utils.JitScalarType.FLOAT
|
||||
)
|
||||
dtype = scalar_type.dtype()
|
||||
if symbolic_helper._is_none(weight):
|
||||
weight_value = torch.ones(normalized_shape, dtype=dtype)
|
||||
weight = g.op("Constant", value_t=weight_value)
|
||||
if symbolic_helper._is_none(bias):
|
||||
bias_value = torch.zeros(normalized_shape, dtype=dtype)
|
||||
bias = g.op("Constant", value_t=bias_value)
|
||||
return g.op(
|
||||
"LayerNormalization",
|
||||
input,
|
||||
weight,
|
||||
bias,
|
||||
epsilon_f=eps,
|
||||
axis_i=axis,
|
||||
)
|
||||
|
||||
|
||||
@_onnx_symbolic("quantized::layer_norm")
|
||||
def quantized_layer_norm(
|
||||
g: jit_utils.GraphContext,
|
||||
x,
|
||||
normalized_shape,
|
||||
weight,
|
||||
bias,
|
||||
eps,
|
||||
op_scale,
|
||||
op_zero_point,
|
||||
):
|
||||
x, _, _, _ = symbolic_helper.dequantize_helper(g, x)
|
||||
|
||||
output = layer_norm(g, x, normalized_shape, weight, bias, eps, False)
|
||||
|
||||
return symbolic_helper.quantize_helper(g, output, op_scale, op_zero_point)
|
||||
|
||||
|
||||
def _compute_edge_sizes(n_fft, window_size):
|
||||
"""Helper function to compute the sizes of the edges (left and right)
|
||||
of a given window centered within an FFT size."""
|
||||
left = (n_fft - window_size) // 2
|
||||
right = n_fft - left - window_size
|
||||
return left, right
|
||||
|
||||
|
||||
@_onnx_symbolic("aten::stft")
|
||||
@symbolic_helper.parse_args("v", "i", "i", "i", "v", "b", "b", "b", "b")
|
||||
def stft(
|
||||
g: jit_utils.GraphContext,
|
||||
input: _C.Value,
|
||||
n_fft: int,
|
||||
hop_length: int | None = None,
|
||||
win_length: int | None = None,
|
||||
window: _C.Value | None = None,
|
||||
normalized: bool = False,
|
||||
onesided: bool | None = True,
|
||||
return_complex: bool | None = False,
|
||||
align_to_window: bool | None = None,
|
||||
) -> _C.Value:
|
||||
"""Associates `torch.stft` with the `STFT` ONNX operator.
|
||||
Note that torch.stft calls _VF.stft, without centering or padding options.
|
||||
Hence, this function does not contain these two arguments.
|
||||
See torch.stft source code for more info.
|
||||
|
||||
Args:
|
||||
g: Graph to write the ONNX representation into
|
||||
input: Input tensor for the transformation
|
||||
n_fft: FFT size
|
||||
hop_length: Size of the hop. Defaults to `floot(n_fft // 4)`
|
||||
win_length: Size of the analysis window. Defaults to `n_fft`
|
||||
window: Analysis window. Defaults to a window of all ones
|
||||
normalized: Whether to return a normalized STFT
|
||||
onesided: Whether to return only half (+1) of the results, given the
|
||||
symmetry of the STFT
|
||||
return_complex: Whether to return the complex value (Note: Must be
|
||||
`False` or `None`)
|
||||
|
||||
Returns:
|
||||
op: Operator for torch.stft associated with STFT (ONNX)
|
||||
"""
|
||||
# Checks
|
||||
if return_complex:
|
||||
raise errors.SymbolicValueError(
|
||||
msg="STFT does not currently support complex types", value=input
|
||||
)
|
||||
|
||||
if align_to_window is not None:
|
||||
raise errors.SymbolicValueError(
|
||||
msg="STFT does not currently support the align_to_window option",
|
||||
value=input,
|
||||
) # TODO(#145944): add compatibility with align_to_window option.
|
||||
|
||||
# Get STFT sizes
|
||||
frame_step_value = hop_length if hop_length is not None else n_fft // 4
|
||||
frame_step_const = g.op(
|
||||
"Constant", value_t=torch.tensor(frame_step_value, dtype=torch.int64)
|
||||
)
|
||||
frame_length_const = g.op(
|
||||
"Constant", value_t=torch.tensor(n_fft, dtype=torch.int64)
|
||||
)
|
||||
|
||||
# Pre-process input if needed
|
||||
signal = input
|
||||
signal_rank = symbolic_helper._get_tensor_rank(signal)
|
||||
if signal_rank == 1:
|
||||
# Add batch dimension
|
||||
signal = g.op(
|
||||
"Unsqueeze",
|
||||
signal,
|
||||
g.op("Constant", value_t=torch.tensor([0], dtype=torch.int64)),
|
||||
)
|
||||
elif signal_rank is None or signal_rank > 2:
|
||||
raise errors.SymbolicValueError(
|
||||
msg="STFT can only take inputs of 1 [signal] or 2 [batch, signal] dimensions. "
|
||||
f"Current rank of signal is {signal_rank}, please reduce it.",
|
||||
value=input,
|
||||
)
|
||||
|
||||
# Get window and make sure it's the same size as `win_length` or `n_fft`
|
||||
# pyrefly: ignore [bad-argument-type]
|
||||
n_win = symbolic_helper._get_tensor_dim_size(window, dim=0)
|
||||
if n_win is not None:
|
||||
win_length_default = win_length if win_length else n_fft
|
||||
if n_win != win_length_default:
|
||||
raise AssertionError(
|
||||
"Analysis window size must equal `win_length` or `n_fft`. "
|
||||
f"Please, set `win_length` or `n_fft` to match `window` size ({n_win})"
|
||||
)
|
||||
|
||||
# Center window around zeros if needed (required by ONNX's STFT)
|
||||
if n_win < n_fft:
|
||||
left, right = _compute_edge_sizes(n_fft, n_win)
|
||||
left_win = g.op("Constant", value_t=torch.zeros(left))
|
||||
right_win = g.op("Constant", value_t=torch.zeros(right))
|
||||
# pyrefly: ignore [bad-argument-type]
|
||||
window = g.op("Concat", left_win, window, right_win, axis_i=0)
|
||||
|
||||
# Create window, if needed
|
||||
if symbolic_helper._is_none(window):
|
||||
if win_length:
|
||||
if win_length > n_fft:
|
||||
raise errors.SymbolicValueError(
|
||||
msg="The analysis window can't be longer than the size of the FFT. "
|
||||
f"Please set `win_length` ({win_length}) to `n_fft` ({n_fft}) or less.",
|
||||
value=input,
|
||||
)
|
||||
|
||||
# Center window, if needed
|
||||
left, right = _compute_edge_sizes(n_fft, win_length)
|
||||
torch_window = torch.hstack(
|
||||
(torch.zeros(left), torch.ones(win_length), torch.zeros(right))
|
||||
)
|
||||
else:
|
||||
# Rectangle window
|
||||
torch_window = torch.ones(n_fft)
|
||||
if torch_window.shape[0] != n_fft:
|
||||
raise AssertionError(
|
||||
f"torch_window.shape[0]={torch_window.shape[0]} != n_fft={n_fft}"
|
||||
)
|
||||
window = g.op("Constant", value_t=torch_window)
|
||||
window = g.op(
|
||||
"Cast",
|
||||
# pyrefly: ignore [bad-argument-type]
|
||||
window,
|
||||
to_i=_type_utils.JitScalarType.from_value(signal).onnx_type(),
|
||||
)
|
||||
|
||||
# Run STFT
|
||||
result = g.op(
|
||||
"STFT",
|
||||
signal,
|
||||
frame_step_const,
|
||||
window,
|
||||
frame_length_const,
|
||||
onesided_i=1 if onesided is None or onesided else 0,
|
||||
)
|
||||
|
||||
# Transpose to mimic torch.stft's behavior
|
||||
result = g.op("Transpose", result, perm_i=[0, 2, 1, 3])
|
||||
|
||||
# Remove batch dimension, if needed
|
||||
if signal_rank == 1:
|
||||
result = g.op(
|
||||
"Squeeze",
|
||||
result,
|
||||
g.op("Constant", value_t=torch.tensor([0], dtype=torch.int64)),
|
||||
)
|
||||
|
||||
# Normalize, if needed
|
||||
if normalized:
|
||||
sqrt_nfft = torch.sqrt(torch.tensor(n_fft, dtype=signal.type().dtype()))
|
||||
result = g.op("Div", result, g.op("Constant", value_t=sqrt_nfft))
|
||||
|
||||
return result
|
||||
+272
@@ -0,0 +1,272 @@
|
||||
# mypy: allow-untyped-defs
|
||||
"""This file exports ONNX ops for opset 18.
|
||||
|
||||
Note [ONNX Operators that are added/updated in opset 18]
|
||||
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
https://github.com/onnx/onnx/blob/main/docs/Changelog.md#version-18-of-the-default-onnx-operator-set
|
||||
New operators:
|
||||
BitwiseAnd
|
||||
CenterCropPad
|
||||
Col2Im
|
||||
Mish
|
||||
OptionalGetElement
|
||||
OptionalHasElement
|
||||
Pad
|
||||
Resize
|
||||
ScatterElements
|
||||
ScatterND
|
||||
Split
|
||||
"""
|
||||
|
||||
import functools
|
||||
from collections.abc import Sequence
|
||||
|
||||
import torch
|
||||
from torch import _C
|
||||
from torch.onnx._internal.torchscript_exporter import (
|
||||
_type_utils,
|
||||
jit_utils,
|
||||
registration,
|
||||
symbolic_helper,
|
||||
symbolic_opset9 as opset9,
|
||||
)
|
||||
|
||||
|
||||
# EDITING THIS FILE? READ THIS FIRST!
|
||||
# see Note [Edit Symbolic Files] in symbolic_helper.py
|
||||
|
||||
__all__ = [
|
||||
"col2im",
|
||||
]
|
||||
|
||||
_onnx_symbolic = functools.partial(registration.onnx_symbolic, opset=18)
|
||||
|
||||
|
||||
@_onnx_symbolic("aten::__and_")
|
||||
@_onnx_symbolic("aten::bitwise_and")
|
||||
def __and_(g: jit_utils.GraphContext, self, other):
|
||||
# do type promotion (scalars don't seem to apply)
|
||||
args = [self, other]
|
||||
# type promotion doesn't happen with torch.bitwise_and(tensor, scalar)
|
||||
prom_args = [arg for arg in args if symbolic_helper._get_tensor_rank(arg)]
|
||||
if len(prom_args) == 0:
|
||||
prom_args = args
|
||||
promotion_jit_type = symbolic_helper._type_promote_from_values(*prom_args)
|
||||
self = symbolic_helper._maybe_cast_to_type(g, self, promotion_jit_type)
|
||||
other = symbolic_helper._maybe_cast_to_type(g, other, promotion_jit_type)
|
||||
if promotion_jit_type == _type_utils.JitScalarType.BOOL:
|
||||
return g.op("And", self, other)
|
||||
return g.op("BitwiseAnd", self, other)
|
||||
|
||||
|
||||
@_onnx_symbolic("aten::col2im")
|
||||
@symbolic_helper.parse_args("v", "v", "v", "is", "is", "is")
|
||||
def col2im(
|
||||
g,
|
||||
input: _C.Value,
|
||||
output_size: _C.Value,
|
||||
kernel_size: _C.Value,
|
||||
dilation: Sequence[int],
|
||||
padding: Sequence[int],
|
||||
stride: Sequence[int],
|
||||
):
|
||||
# convert [i0, i1, ..., in] into [i0, i0, i1, i1, ..., in, in]
|
||||
adjusted_padding: list[int] = []
|
||||
for pad in padding:
|
||||
adjusted_padding.extend(pad for _ in range(2))
|
||||
|
||||
num_dimensional_axis = symbolic_helper._get_tensor_sizes(output_size)[0]
|
||||
if not adjusted_padding:
|
||||
adjusted_padding = [0, 0] * num_dimensional_axis
|
||||
|
||||
if not dilation:
|
||||
dilation = [1] * num_dimensional_axis
|
||||
|
||||
if not stride:
|
||||
stride = [1] * num_dimensional_axis
|
||||
|
||||
return g.op(
|
||||
"Col2Im",
|
||||
input,
|
||||
output_size,
|
||||
kernel_size,
|
||||
dilations_i=dilation,
|
||||
pads_i=adjusted_padding,
|
||||
strides_i=stride,
|
||||
)
|
||||
|
||||
|
||||
@_onnx_symbolic(
|
||||
"aten::mean", decorate=[symbolic_helper._apply_params("ReduceMean", "mean")]
|
||||
)
|
||||
@_onnx_symbolic(
|
||||
"aten::prod",
|
||||
decorate=[
|
||||
symbolic_helper._apply_params(
|
||||
"ReduceProd", "prod", allow_multi_dim_support=False
|
||||
)
|
||||
],
|
||||
)
|
||||
def _reduce_with_dtype(onnx_op: str, name: str, allow_multi_dim_support: bool = True):
|
||||
return symbolic_helper._reduce_with_dtype_helper(
|
||||
onnx_op, name, allow_multi_dim_support
|
||||
)
|
||||
|
||||
|
||||
@_onnx_symbolic("aten::native_layer_norm")
|
||||
@symbolic_helper.quantized_args(True, False, False, False)
|
||||
@symbolic_helper.parse_args("v", "is", "v", "v", "f")
|
||||
def _native_layer_norm(
|
||||
g: jit_utils.GraphContext,
|
||||
input: _C.Value,
|
||||
normalized_shape: Sequence[int],
|
||||
weight: _C.Value,
|
||||
bias: _C.Value,
|
||||
eps: float,
|
||||
) -> tuple[_C.Value, _C.Value, _C.Value]:
|
||||
return opset9.native_layer_norm(g, input, normalized_shape, weight, bias, eps)
|
||||
|
||||
|
||||
@_onnx_symbolic("aten::glu")
|
||||
@symbolic_helper.parse_args("v", "i")
|
||||
def _glu(g: jit_utils.GraphContext, input, dim):
|
||||
dim_size = symbolic_helper._get_tensor_dim_size(input, dim)
|
||||
if dim_size is not None:
|
||||
if dim_size % 2 != 0:
|
||||
raise AssertionError(f"dim_size must be even, got {dim_size}")
|
||||
|
||||
first, second = g.op("Split", input, axis_i=dim, num_outputs_i=2, outputs=2)
|
||||
return g.op("Mul", first, g.op("Sigmoid", second))
|
||||
|
||||
|
||||
@_onnx_symbolic("aten::max")
|
||||
# torch.max (same for torch.min) actually has two interfaces smashed together:
|
||||
# torch.max(x, dim, keepdim) and torch.max(x, y)
|
||||
# TODO(justinchuby): Support multiple quantized args in output
|
||||
def max(g: jit_utils.GraphContext, self, dim_or_y=None, keepdim=None):
|
||||
return symbolic_helper._max_helper(g, self, dim_or_y, keepdim)
|
||||
|
||||
|
||||
@_onnx_symbolic("aten::maximum")
|
||||
@symbolic_helper.quantized_args(True, True)
|
||||
def maximum(g: jit_utils.GraphContext, input, other):
|
||||
# pyrefly: ignore [no-matching-overload]
|
||||
return max(g, input, dim_or_y=other)
|
||||
|
||||
|
||||
@_onnx_symbolic("aten::min")
|
||||
# TODO(justinchuby): Support multiple quantized args in output
|
||||
def min(g: jit_utils.GraphContext, self, dim_or_y=None, keepdim=None):
|
||||
return symbolic_helper._min_helper(g, self, dim_or_y, keepdim)
|
||||
|
||||
|
||||
@_onnx_symbolic("aten::minimum")
|
||||
@symbolic_helper.quantized_args(True, True)
|
||||
def minimum(g: jit_utils.GraphContext, input, other):
|
||||
# pyrefly: ignore [no-matching-overload]
|
||||
return min(g, input, dim_or_y=other)
|
||||
|
||||
|
||||
@_onnx_symbolic("aten::amax")
|
||||
@symbolic_helper.quantized_args(True)
|
||||
@symbolic_helper.parse_args("v", "is", "i")
|
||||
def amax(g: jit_utils.GraphContext, self, dim, keepdim):
|
||||
axes = g.op("Constant", value_t=torch.tensor(dim, dtype=torch.long))
|
||||
return g.op("ReduceMax", self, axes, keepdims_i=keepdim)
|
||||
|
||||
|
||||
@_onnx_symbolic("aten::amin")
|
||||
@symbolic_helper.quantized_args(True)
|
||||
@symbolic_helper.parse_args("v", "is", "i")
|
||||
def amin(g: jit_utils.GraphContext, self, dim, keepdim):
|
||||
axes = g.op("Constant", value_t=torch.tensor(dim, dtype=torch.long))
|
||||
return g.op("ReduceMin", self, axes, keepdims_i=keepdim)
|
||||
|
||||
|
||||
@_onnx_symbolic("aten::aminmax")
|
||||
@symbolic_helper.quantized_args(True)
|
||||
@symbolic_helper.parse_args("v", "v", "i")
|
||||
def aminmax(g: jit_utils.GraphContext, self, dim, keepdim):
|
||||
if not symbolic_helper._is_none(dim):
|
||||
dim = symbolic_helper._get_const(dim, "i", "dim")
|
||||
axes = g.op("Constant", value_t=torch.tensor([dim], dtype=torch.long))
|
||||
return g.op("ReduceMin", self, axes, keepdims_i=keepdim), g.op(
|
||||
"ReduceMax", self, axes, keepdims_i=keepdim
|
||||
)
|
||||
else:
|
||||
return g.op("ReduceMin", self, keepdims_i=keepdim), g.op(
|
||||
"ReduceMax", self, keepdims_i=keepdim
|
||||
)
|
||||
|
||||
|
||||
@_onnx_symbolic("aten::var_mean")
|
||||
def _var_mean(g: jit_utils.GraphContext, input, *args):
|
||||
if len(args) == 1:
|
||||
return symbolic_helper._var_mean_helper(g, input, None, args[0], None)
|
||||
else:
|
||||
return symbolic_helper._var_mean_helper(g, input, *args)
|
||||
|
||||
|
||||
@_onnx_symbolic("aten::logsumexp")
|
||||
@symbolic_helper.parse_args("v", "is", "i")
|
||||
def _logsumexp(g: jit_utils.GraphContext, input, dim, keepdim):
|
||||
if dim is None:
|
||||
return g.op("ReduceLogSumExp", input, keepdims_i=0)
|
||||
else:
|
||||
axes = g.op("Constant", value_t=torch.tensor(dim, dtype=torch.long))
|
||||
return g.op("ReduceLogSumExp", input, axes, keepdims_i=keepdim)
|
||||
|
||||
|
||||
@_onnx_symbolic("aten::linalg_matrix_norm")
|
||||
@symbolic_helper.parse_args("v", "v", "is", "b", "v")
|
||||
def _linalg_matrix_norm(
|
||||
g: jit_utils.GraphContext,
|
||||
self: torch._C.Value,
|
||||
ord: torch._C.Value,
|
||||
dim: list[int],
|
||||
keepdim: bool,
|
||||
dtype: torch._C.Value,
|
||||
):
|
||||
return opset9.linalg_matrix_norm(g, self, ord, dim, keepdim, dtype)
|
||||
|
||||
|
||||
@_onnx_symbolic("aten::embedding_bag")
|
||||
@symbolic_helper.parse_args("v", "v", "v", "i", "i", "i", "v", "i", "i")
|
||||
def embedding_bag(
|
||||
g: jit_utils.GraphContext,
|
||||
embedding_matrix,
|
||||
indices,
|
||||
offsets,
|
||||
scale_grad_by_freq,
|
||||
mode,
|
||||
sparse,
|
||||
per_sample_weights,
|
||||
include_last_offset,
|
||||
padding_idx,
|
||||
):
|
||||
return symbolic_helper._embedding_bag_helper(
|
||||
g,
|
||||
embedding_matrix,
|
||||
indices,
|
||||
offsets,
|
||||
scale_grad_by_freq,
|
||||
mode,
|
||||
sparse,
|
||||
per_sample_weights,
|
||||
include_last_offset,
|
||||
padding_idx,
|
||||
)
|
||||
|
||||
|
||||
@_onnx_symbolic("aten::linalg_vector_norm")
|
||||
@symbolic_helper.parse_args("v", "f", "is", "b", "v")
|
||||
def linalg_vector_norm(
|
||||
g: jit_utils.GraphContext,
|
||||
self: torch._C.Value,
|
||||
ord: float,
|
||||
dim: Sequence[int] | None,
|
||||
keepdim: bool,
|
||||
dtype: torch._C.Value,
|
||||
):
|
||||
return symbolic_helper._linalg_vector_norm_helper(g, self, ord, dim, keepdim, dtype)
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
"""This file exports ONNX ops for opset 19.
|
||||
|
||||
Note [ONNX Operators that are added/updated in opset 19]
|
||||
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
https://github.com/onnx/onnx/blob/main/docs/Changelog.md#version-19-of-the-default-onnx-operator-set
|
||||
New operators:
|
||||
AveragePool
|
||||
Cast
|
||||
CastLike
|
||||
Constant
|
||||
DeformConv
|
||||
DequantizeLinear
|
||||
Equal
|
||||
Identity
|
||||
If
|
||||
Loop
|
||||
Pad
|
||||
QuantizeLinear
|
||||
Reshape
|
||||
Resize
|
||||
Scan
|
||||
Shape
|
||||
Size
|
||||
"""
|
||||
|
||||
|
||||
# EDITING THIS FILE? READ THIS FIRST!
|
||||
# see Note [Edit Symbolic Files] in symbolic_helper.py
|
||||
|
||||
__all__: list[str] = []
|
||||
+95
@@ -0,0 +1,95 @@
|
||||
# mypy: allow-untyped-defs
|
||||
"""This file exports ONNX ops for opset 20.
|
||||
|
||||
Note [ONNX Operators that are added/updated in opset 20]
|
||||
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
https://github.com/onnx/onnx/blob/main/docs/Changelog.md#version-20-of-the-default-onnx-operator-set
|
||||
New operators:
|
||||
AffineGrid
|
||||
ConstantOfShape
|
||||
DFT
|
||||
Gelu
|
||||
GridSample
|
||||
ImageDecoder
|
||||
IsInf
|
||||
IsNaN
|
||||
ReduceMax
|
||||
ReduceMin
|
||||
RegexFullMatch
|
||||
StringConcat
|
||||
StringSplit
|
||||
"""
|
||||
|
||||
import functools
|
||||
|
||||
import torch.nn.functional as F
|
||||
from torch import _C
|
||||
from torch.onnx._internal.torchscript_exporter import (
|
||||
jit_utils,
|
||||
registration,
|
||||
symbolic_helper,
|
||||
)
|
||||
|
||||
|
||||
# EDITING THIS FILE? READ THIS FIRST!
|
||||
# see Note [Edit Symbolic Files] in symbolic_helper.py
|
||||
|
||||
__all__ = ["_grid_sampler", "_affine_grid_generator", "gelu"]
|
||||
|
||||
|
||||
def convert_grid_sample_mode(mode_s):
|
||||
return (
|
||||
"linear" if mode_s == "bilinear" else "cubic" if mode_s == "bicubic" else mode_s
|
||||
)
|
||||
|
||||
|
||||
_onnx_symbolic = functools.partial(registration.onnx_symbolic, opset=20)
|
||||
|
||||
|
||||
@_onnx_symbolic("aten::grid_sampler")
|
||||
@symbolic_helper.parse_args("v", "v", "i", "i", "b")
|
||||
def _grid_sampler(
|
||||
g: jit_utils.GraphContext,
|
||||
input: _C.Value,
|
||||
grid: _C.Value,
|
||||
mode_enum: int,
|
||||
padding_mode_enum: int,
|
||||
align_corners: bool,
|
||||
):
|
||||
mode_s = {v: k for k, v in F.GRID_SAMPLE_INTERPOLATION_MODES.items()}[mode_enum] # type: ignore[call-arg, index]
|
||||
# mode string changes at https://onnx.ai/onnx/operators/text_diff_GridSample_16_20.html
|
||||
mode_s = convert_grid_sample_mode(mode_s)
|
||||
padding_mode_s = {v: k for k, v in F.GRID_SAMPLE_PADDING_MODES.items()}[ # type: ignore[call-arg, index]
|
||||
padding_mode_enum # type: ignore[index]
|
||||
]
|
||||
return g.op(
|
||||
"GridSample",
|
||||
input,
|
||||
grid,
|
||||
align_corners_i=int(align_corners),
|
||||
mode_s=mode_s,
|
||||
padding_mode_s=padding_mode_s,
|
||||
)
|
||||
|
||||
|
||||
@_onnx_symbolic("aten::affine_grid_generator")
|
||||
@symbolic_helper.parse_args("v", "v", "b")
|
||||
def _affine_grid_generator(
|
||||
g: jit_utils.GraphContext,
|
||||
theta: _C.Value,
|
||||
size: _C.Value,
|
||||
align_corners: bool,
|
||||
):
|
||||
return g.op(
|
||||
"AffineGrid",
|
||||
theta,
|
||||
size,
|
||||
align_corners_i=int(align_corners),
|
||||
)
|
||||
|
||||
|
||||
@_onnx_symbolic("aten::gelu")
|
||||
@symbolic_helper.parse_args("v", "s")
|
||||
def gelu(g: jit_utils.GraphContext, self: _C.Value, approximate: str = "none"):
|
||||
return g.op("Gelu", self, approximate_s=approximate)
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
# mypy: allow-untyped-defs
|
||||
"""
|
||||
Note [ONNX operators that are added/updated from opset 7 to opset 8]
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
New operators:
|
||||
Expand
|
||||
|
||||
Updated operators:
|
||||
Min, Max, Sum, Mean: supports multidirectional broadcasting.
|
||||
MaxPool: added optional indices output.
|
||||
Scan
|
||||
"""
|
||||
|
||||
import functools
|
||||
import warnings
|
||||
|
||||
from torch.onnx._internal.torchscript_exporter import (
|
||||
jit_utils,
|
||||
registration,
|
||||
symbolic_helper,
|
||||
symbolic_opset9 as opset9,
|
||||
)
|
||||
|
||||
|
||||
_onnx_symbolic = functools.partial(registration.onnx_symbolic, opset=7)
|
||||
|
||||
block_listed_operators = (
|
||||
"scan",
|
||||
"expand",
|
||||
"expand_as",
|
||||
"meshgrid",
|
||||
"adaptive_max_pool1d",
|
||||
"adaptive_max_pool2d",
|
||||
"adaptive_max_pool3d",
|
||||
"max_pool1d_with_indices",
|
||||
"max_pool2d_with_indices",
|
||||
"max_pool3d_with_indices",
|
||||
)
|
||||
|
||||
|
||||
# NOTE: max, min, sum, mean: broadcasting is not supported in opset 7.
|
||||
# torch.max (same for torch.min) actually has two interfaces smashed together:
|
||||
# torch.max(x, dim, keepdim) and torch.max(x, y)
|
||||
@_onnx_symbolic("aten::max")
|
||||
def max(g: jit_utils.GraphContext, self, dim_or_y=None, keepdim=None):
|
||||
# torch.max(input, other)
|
||||
if keepdim is None and dim_or_y is not None:
|
||||
warnings.warn(
|
||||
"Multidirectional broadcasting is not supported in opset 7. "
|
||||
"This might cause the onnx model to be incorrect, if inputs to max operators "
|
||||
"have different shapes",
|
||||
stacklevel=2,
|
||||
)
|
||||
return opset9.max(g, self, dim_or_y, keepdim)
|
||||
|
||||
|
||||
@_onnx_symbolic("aten::min")
|
||||
def min(g: jit_utils.GraphContext, self, dim_or_y=None, keepdim=None):
|
||||
# torch.min(input, other)
|
||||
if keepdim is None and dim_or_y is not None:
|
||||
warnings.warn(
|
||||
"Multidirectional broadcasting is not supported in opset 7. "
|
||||
"This might cause the onnx model to be incorrect, if inputs to min operators "
|
||||
"have different shapes",
|
||||
stacklevel=2,
|
||||
)
|
||||
return opset9.min(g, self, dim_or_y, keepdim)
|
||||
|
||||
|
||||
for block_listed_op in block_listed_operators:
|
||||
_onnx_symbolic(f"aten::{block_listed_op}")(
|
||||
symbolic_helper._block_list_in_opset(block_listed_op)
|
||||
)
|
||||
+470
@@ -0,0 +1,470 @@
|
||||
# mypy: allow-untyped-defs
|
||||
"""
|
||||
Note [ONNX operators that are added/updated from opset 8 to opset 9]
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
New operators:
|
||||
Compress
|
||||
ConstantOfShape
|
||||
EyeLike
|
||||
MaxUnpool
|
||||
OneHot
|
||||
Sinh
|
||||
Cosh
|
||||
Asinh
|
||||
Acosh
|
||||
Atanh
|
||||
Shrink
|
||||
IsNaN
|
||||
Sign
|
||||
Erf
|
||||
Scatter
|
||||
Where
|
||||
NonZero
|
||||
TfIdfVectorizer
|
||||
MeanVarianceNormalization
|
||||
|
||||
Updated operators:
|
||||
BatchNormalization: removed spatial attribute.
|
||||
Greater, Less, Constant, MatMul, PRelu, Gemm, Flatten: more data types{integers} supported.
|
||||
Cast: more data types{string} supported.
|
||||
Upsample: moved scales from attribute to input.
|
||||
Scan
|
||||
"""
|
||||
|
||||
import functools
|
||||
import warnings
|
||||
|
||||
import torch
|
||||
from torch._C import _onnx as _C_onnx
|
||||
from torch.onnx import errors
|
||||
from torch.onnx._internal.torchscript_exporter import (
|
||||
_type_utils,
|
||||
jit_utils,
|
||||
registration,
|
||||
symbolic_helper,
|
||||
symbolic_opset9 as opset9,
|
||||
)
|
||||
|
||||
|
||||
_onnx_symbolic = functools.partial(registration.onnx_symbolic, opset=8)
|
||||
|
||||
block_listed_operators = (
|
||||
"nonzero",
|
||||
"where",
|
||||
"scatter",
|
||||
"scatter_add",
|
||||
"erf",
|
||||
"sign",
|
||||
"isnan",
|
||||
"gather",
|
||||
"arange",
|
||||
"masked_fill",
|
||||
"index_fill",
|
||||
"index_copy",
|
||||
"repeat_interleave",
|
||||
"any",
|
||||
"all",
|
||||
)
|
||||
|
||||
for block_listed_op in block_listed_operators:
|
||||
_onnx_symbolic(f"aten::{block_listed_op}")(
|
||||
symbolic_helper._block_list_in_opset(block_listed_op)
|
||||
)
|
||||
|
||||
|
||||
@_onnx_symbolic(
|
||||
"aten::upsample_nearest1d",
|
||||
decorate=[symbolic_helper._apply_params("upsample_nearest1d", 3, "nearest")],
|
||||
)
|
||||
@_onnx_symbolic(
|
||||
"aten::upsample_nearest2d",
|
||||
decorate=[symbolic_helper._apply_params("upsample_nearest2d", 4, "nearest")],
|
||||
)
|
||||
@_onnx_symbolic(
|
||||
"aten::upsample_nearest3d",
|
||||
decorate=[symbolic_helper._apply_params("upsample_nearest3d", 5, "nearest")],
|
||||
)
|
||||
@_onnx_symbolic(
|
||||
"aten::upsample_linear1d",
|
||||
decorate=[symbolic_helper._apply_params("upsample_linear1d", 3, "linear")],
|
||||
)
|
||||
@_onnx_symbolic(
|
||||
"aten::upsample_bilinear2d",
|
||||
decorate=[symbolic_helper._apply_params("upsample_bilinear2d", 4, "linear")],
|
||||
)
|
||||
@_onnx_symbolic(
|
||||
"aten::upsample_trilinear3d",
|
||||
decorate=[symbolic_helper._apply_params("upsample_trilinear3d", 5, "linear")],
|
||||
)
|
||||
def _interpolate(name, dim, interpolate_mode):
|
||||
def symbolic_fn(g, input, output_size, *args):
|
||||
scales, align_corners = symbolic_helper._get_interpolate_attributes(
|
||||
g, interpolate_mode, args
|
||||
)
|
||||
symbolic_helper._interpolate_warning(interpolate_mode)
|
||||
align_corners = symbolic_helper._maybe_get_scalar(align_corners)
|
||||
if align_corners:
|
||||
return symbolic_helper._unimplemented(name, "align_corners == True", input)
|
||||
output_size = symbolic_helper._maybe_get_const(output_size, "is")
|
||||
if symbolic_helper._is_value(output_size):
|
||||
return symbolic_helper._unimplemented(
|
||||
name, "torch._C.Value (output_size) indexing"
|
||||
)
|
||||
if scales is None:
|
||||
scales = [
|
||||
1.0
|
||||
if i < 2
|
||||
else float(output_size[-(dim - i)])
|
||||
/ float(input.type().sizes()[-(dim - i)])
|
||||
for i in range(dim)
|
||||
]
|
||||
return g.op("Upsample", input, mode_s=interpolate_mode, scales_f=scales)
|
||||
|
||||
return symbolic_fn
|
||||
|
||||
|
||||
@_onnx_symbolic("aten::__interpolate")
|
||||
def __interpolate(
|
||||
g: jit_utils.GraphContext,
|
||||
input,
|
||||
size,
|
||||
scale_factor,
|
||||
mode,
|
||||
align_corners,
|
||||
recompute_scale_factor,
|
||||
antialias,
|
||||
):
|
||||
align_corners = symbolic_helper._maybe_get_const(align_corners, "b")
|
||||
if not symbolic_helper._is_none(align_corners) and align_corners:
|
||||
return symbolic_helper._unimplemented("interpolate", "align_corners == True")
|
||||
|
||||
if not symbolic_helper._is_none(scale_factor) and symbolic_helper._is_value(
|
||||
scale_factor
|
||||
):
|
||||
return symbolic_helper._unimplemented(
|
||||
"interpolate", "dynamic scales in opset 8"
|
||||
)
|
||||
|
||||
if not symbolic_helper._is_none(size) and symbolic_helper._is_value(size):
|
||||
return symbolic_helper._unimplemented("interpolate", "dynamic size in opset 8")
|
||||
|
||||
scales, mode = symbolic_helper._interpolate_get_scales_and_mode(
|
||||
g, input, size, scale_factor, mode, align_corners
|
||||
)
|
||||
return g.op("Upsample", input, mode_s=mode, scales_f=scales)
|
||||
|
||||
|
||||
# NOTE: We should create a wrapper for this kind of operation, after resolving the shape/type propagation
|
||||
# issue for "cast" operators. Some symbolic functions depend on shape information of input tensor, which
|
||||
# is lost after casting.
|
||||
def _try_cast_integer_to_float(g: jit_utils.GraphContext, *args):
|
||||
floating_scalar_types = {
|
||||
_type_utils.JitScalarType.HALF,
|
||||
_type_utils.JitScalarType.FLOAT,
|
||||
_type_utils.JitScalarType.DOUBLE,
|
||||
}
|
||||
old_type = None
|
||||
# Cast the input tensor to Float if its scalarType is known and is not floating number.
|
||||
# If casting is performed, return the old scalarType, otherwise return None.
|
||||
arg0_type = _type_utils.JitScalarType.from_value(
|
||||
args[0], _type_utils.JitScalarType.UNDEFINED
|
||||
)
|
||||
if arg0_type != _type_utils.JitScalarType.UNDEFINED:
|
||||
old_type = arg0_type
|
||||
if old_type not in floating_scalar_types:
|
||||
old_type = old_type.scalar_name() # type: ignore[assignment]
|
||||
args = tuple(
|
||||
g.op("Cast", arg, to_i=_C_onnx.TensorProtoDataType.FLOAT)
|
||||
for arg in args
|
||||
)
|
||||
else:
|
||||
return (None,) + args
|
||||
else:
|
||||
warnings.warn(
|
||||
"Only floating datatype is supported for these operators: "
|
||||
"{Greater, Less, MatMul, PRelu, Gemm, Flatten}. This might cause "
|
||||
"the onnx model to be incorrect, if inputs have integer datatypes.",
|
||||
stacklevel=2,
|
||||
)
|
||||
return (old_type,) + args
|
||||
|
||||
|
||||
def _cast_to_type(g: jit_utils.GraphContext, input, to_type):
|
||||
if to_type is None:
|
||||
return input
|
||||
return g.op("Cast", input, to_i=symbolic_helper.cast_pytorch_to_onnx[to_type])
|
||||
|
||||
|
||||
def _comparison_operator(g: jit_utils.GraphContext, input, other, op_name):
|
||||
other = symbolic_helper._maybe_get_scalar(other)
|
||||
other = symbolic_helper._if_scalar_type_as(other, input)
|
||||
_, input, other = _try_cast_integer_to_float(g, input, other)
|
||||
return g.op(op_name, input, other)
|
||||
|
||||
|
||||
# NOTE: For symbolics {gt, lt, bmm, matmul, prelu, mm, addmm, view, flatten},
|
||||
# integer input type not supported in opset8. Cast to float if possible.
|
||||
@_onnx_symbolic("aten::gt")
|
||||
def gt(g: jit_utils.GraphContext, input, other):
|
||||
return _comparison_operator(g, input, other, "Greater")
|
||||
|
||||
|
||||
@_onnx_symbolic("aten::lt")
|
||||
def lt(g: jit_utils.GraphContext, input, other):
|
||||
return _comparison_operator(g, input, other, "Less")
|
||||
|
||||
|
||||
@_onnx_symbolic("aten::bmm")
|
||||
def bmm(g: jit_utils.GraphContext, self, other):
|
||||
if symbolic_helper._try_get_scalar_type(self):
|
||||
old_type, self, other = _try_cast_integer_to_float(g, self, other)
|
||||
return _cast_to_type(g, g.op("MatMul", self, other), old_type)
|
||||
else:
|
||||
return g.op("MatMul", self, other)
|
||||
|
||||
|
||||
@_onnx_symbolic("aten::matmul")
|
||||
def matmul(g: jit_utils.GraphContext, self, other):
|
||||
return bmm(g, self, other)
|
||||
|
||||
|
||||
@_onnx_symbolic("aten::prelu")
|
||||
def prelu(g: jit_utils.GraphContext, self, weight):
|
||||
self_rank = symbolic_helper._get_tensor_rank(self)
|
||||
weight_sizes = symbolic_helper._get_tensor_sizes(weight)
|
||||
if self_rank is not None and self_rank > 2:
|
||||
weight = g.op("Unsqueeze", weight, axes_i=list(range(1, self_rank - 1)))
|
||||
elif self_rank == 0 and weight_sizes == [1]:
|
||||
# self and weight are both scalar but weight has rank == 1, squeeze weight.
|
||||
weight = symbolic_helper._squeeze_helper(g, weight, [0])
|
||||
if symbolic_helper._try_get_scalar_type(self):
|
||||
old_type, self, weight = _try_cast_integer_to_float(g, self, weight)
|
||||
return _cast_to_type(g, g.op("PRelu", self, weight), old_type)
|
||||
else:
|
||||
return g.op("PRelu", self, weight)
|
||||
|
||||
|
||||
@_onnx_symbolic("aten::mm")
|
||||
def mm(g: jit_utils.GraphContext, self, other):
|
||||
# Create a dummy C tensor. Only needed for API purposes, the value is
|
||||
# since beta = 0
|
||||
scalar_type = symbolic_helper._try_get_scalar_type(self, other)
|
||||
if scalar_type is None:
|
||||
raise errors.SymbolicValueError(
|
||||
"mm can only operate on tensors with known types", self
|
||||
)
|
||||
zero_constant = g.op(
|
||||
"Constant",
|
||||
value_t=torch.tensor([0], dtype=scalar_type.dtype()),
|
||||
)
|
||||
|
||||
if symbolic_helper._try_get_scalar_type(self):
|
||||
old_type, self, other, zero_constant = _try_cast_integer_to_float(
|
||||
g, self, other, zero_constant
|
||||
)
|
||||
return _cast_to_type(
|
||||
g,
|
||||
g.op("Gemm", self, other, zero_constant, beta_f=0.0, alpha_f=1.0),
|
||||
old_type,
|
||||
)
|
||||
return g.op("Gemm", self, other, zero_constant, beta_f=0.0, alpha_f=1.0)
|
||||
|
||||
|
||||
@_onnx_symbolic("aten::addmm")
|
||||
@symbolic_helper.parse_args("v", "v", "v", "t", "t")
|
||||
def addmm(g: jit_utils.GraphContext, self, mat1, mat2, beta, alpha):
|
||||
if symbolic_helper._try_get_scalar_type(self):
|
||||
old_type, self, mat1, mat2 = _try_cast_integer_to_float(g, self, mat1, mat2)
|
||||
return _cast_to_type(
|
||||
g,
|
||||
g.op(
|
||||
"Gemm",
|
||||
mat1,
|
||||
mat2,
|
||||
self,
|
||||
beta_f=symbolic_helper._scalar(beta),
|
||||
alpha_f=symbolic_helper._scalar(alpha),
|
||||
),
|
||||
old_type,
|
||||
)
|
||||
else:
|
||||
return g.op(
|
||||
"Gemm",
|
||||
mat1,
|
||||
mat2,
|
||||
self,
|
||||
beta_f=symbolic_helper._scalar(beta),
|
||||
alpha_f=symbolic_helper._scalar(alpha),
|
||||
)
|
||||
|
||||
|
||||
@_onnx_symbolic("aten::flatten")
|
||||
def flatten(g: jit_utils.GraphContext, input, start_dim, end_dim):
|
||||
start_dim_i = symbolic_helper._get_const(start_dim, "i", "start_dim")
|
||||
end_dim_i = symbolic_helper._get_const(end_dim, "i", "end_dim")
|
||||
|
||||
dim = input.type().dim()
|
||||
if end_dim_i < 0:
|
||||
end_dim_i = dim + end_dim_i
|
||||
# use ONNX's Flatten operator for cases where the output shape is 2D
|
||||
if start_dim_i == 1 and end_dim_i == dim - 1:
|
||||
if symbolic_helper._try_get_scalar_type(input):
|
||||
old_type, input = _try_cast_integer_to_float(g, input)
|
||||
return _cast_to_type(
|
||||
g, g.op("Flatten", input, axis_i=start_dim_i), old_type
|
||||
)
|
||||
else:
|
||||
return g.op("Flatten", input, axis_i=start_dim_i)
|
||||
if start_dim_i == 0 and end_dim_i == dim - 2:
|
||||
if symbolic_helper._try_get_scalar_type(input):
|
||||
old_type, input = _try_cast_integer_to_float(g, input)
|
||||
return _cast_to_type(
|
||||
g, g.op("Flatten", input, axis_i=end_dim_i + 1), old_type
|
||||
)
|
||||
else:
|
||||
return g.op("Flatten", input, axis_i=end_dim_i + 1)
|
||||
|
||||
return opset9.flatten(g, input, start_dim, end_dim)
|
||||
|
||||
|
||||
def _constant_fill(g: jit_utils.GraphContext, sizes, dtype: int, const_value):
|
||||
if dtype is None:
|
||||
scalar_type = _type_utils.JitScalarType.FLOAT
|
||||
else:
|
||||
scalar_type = _type_utils.JitScalarType(dtype)
|
||||
if not scalar_type.dtype().is_floating_point:
|
||||
result = g.op(
|
||||
"ConstantFill",
|
||||
sizes,
|
||||
dtype_i=_type_utils.JitScalarType.FLOAT.onnx_type(),
|
||||
input_as_shape_i=1,
|
||||
value_f=const_value,
|
||||
)
|
||||
return g.op("Cast", result, to_i=scalar_type.onnx_type())
|
||||
else:
|
||||
return g.op(
|
||||
"ConstantFill",
|
||||
sizes,
|
||||
dtype_i=scalar_type.onnx_type(),
|
||||
input_as_shape_i=1,
|
||||
value_f=const_value,
|
||||
)
|
||||
|
||||
|
||||
@_onnx_symbolic("aten::empty")
|
||||
@symbolic_helper.parse_args("v", "i", "v", "v", "v", "v")
|
||||
def empty(
|
||||
g: jit_utils.GraphContext,
|
||||
sizes,
|
||||
dtype,
|
||||
layout,
|
||||
device,
|
||||
pin_memory=False,
|
||||
memory_format=None,
|
||||
):
|
||||
return zeros(g, sizes, dtype, layout, device, pin_memory)
|
||||
|
||||
|
||||
@_onnx_symbolic("aten::empty_like")
|
||||
@symbolic_helper.parse_args("v", "i", "v", "v", "v", "v")
|
||||
def empty_like(
|
||||
g: jit_utils.GraphContext,
|
||||
input,
|
||||
dtype,
|
||||
layout,
|
||||
device,
|
||||
pin_memory=False,
|
||||
memory_format=None,
|
||||
):
|
||||
return zeros_like(g, input, dtype, layout, device, pin_memory)
|
||||
|
||||
|
||||
@_onnx_symbolic("aten::zeros")
|
||||
@symbolic_helper.parse_args("v", "i", "v", "v", "v")
|
||||
def zeros(g: jit_utils.GraphContext, sizes, dtype, layout, device, pin_memory=False):
|
||||
# NOTE: no way to set device and layout in ONNX, so we ignore it
|
||||
return _constant_fill(g, sizes, dtype, 0)
|
||||
|
||||
|
||||
@_onnx_symbolic("aten::zeros_like")
|
||||
@symbolic_helper.parse_args("v", "i", "v", "v", "v", "v")
|
||||
def zeros_like(
|
||||
g: jit_utils.GraphContext,
|
||||
input,
|
||||
dtype,
|
||||
layout,
|
||||
device,
|
||||
pin_memory=False,
|
||||
memory_format=None,
|
||||
):
|
||||
shape = g.op("Shape", input)
|
||||
return _constant_fill(g, shape, dtype, 0)
|
||||
|
||||
|
||||
@_onnx_symbolic("aten::ones")
|
||||
@symbolic_helper.parse_args("v", "i", "v", "v", "v")
|
||||
def ones(g: jit_utils.GraphContext, sizes, dtype, layout, device, pin_memory=False):
|
||||
return _constant_fill(g, sizes, dtype, 1)
|
||||
|
||||
|
||||
@_onnx_symbolic("aten::ones_like")
|
||||
@symbolic_helper.parse_args("v", "i", "v", "v", "v", "v")
|
||||
def ones_like(
|
||||
g: jit_utils.GraphContext,
|
||||
input,
|
||||
dtype,
|
||||
layout,
|
||||
device,
|
||||
pin_memory=False,
|
||||
memory_format=None,
|
||||
):
|
||||
shape = g.op("Shape", input)
|
||||
return _constant_fill(g, shape, dtype, 1)
|
||||
|
||||
|
||||
@_onnx_symbolic("aten::full")
|
||||
def full(
|
||||
g: jit_utils.GraphContext, sizes, value, dtype, layout, device, pin_memory=False
|
||||
):
|
||||
const_value = symbolic_helper._maybe_get_const(value, "t")
|
||||
if symbolic_helper._is_value(const_value):
|
||||
tmp = zeros(g, sizes, dtype, layout, device)
|
||||
return opset9.add(g, tmp, value, g.op("Constant", value_t=torch.tensor(1)))
|
||||
else:
|
||||
dtype = symbolic_helper._get_const(dtype, "i", "dtype")
|
||||
return _constant_fill(g, sizes, dtype, const_value)
|
||||
|
||||
|
||||
@_onnx_symbolic("aten::full_like")
|
||||
@symbolic_helper.parse_args("v", "f", "i", "v", "v", "v", "v")
|
||||
def full_like(
|
||||
g: jit_utils.GraphContext,
|
||||
input,
|
||||
fill_value,
|
||||
dtype,
|
||||
layout,
|
||||
device,
|
||||
pin_memory=False,
|
||||
memory_format=None,
|
||||
):
|
||||
shape = g.op("Shape", input)
|
||||
return _constant_fill(g, shape, dtype, fill_value)
|
||||
|
||||
|
||||
@_onnx_symbolic("aten::repeat")
|
||||
def repeat(g: jit_utils.GraphContext, self, repeats):
|
||||
if not symbolic_helper._is_value(repeats):
|
||||
repeats = g.op("Constant", value_t=torch.LongTensor(repeats))
|
||||
if symbolic_helper._is_packed_list(repeats):
|
||||
repeat_size_len = len(symbolic_helper._unpack_list(repeats))
|
||||
else:
|
||||
const_repeats = symbolic_helper._maybe_get_const(repeats, "is")
|
||||
repeat_size_len = len(const_repeats)
|
||||
if self.isCompleteTensor():
|
||||
sizes = self.type().sizes()
|
||||
diff_dims = repeat_size_len - len(sizes)
|
||||
if diff_dims > 0:
|
||||
self = opset9.view(
|
||||
g, self, g.op("Constant", value_t=torch.tensor([1] * diff_dims + sizes))
|
||||
)
|
||||
return g.op("Tile", self, repeats)
|
||||
+6668
File diff suppressed because it is too large
Load Diff
+1942
File diff suppressed because it is too large
Load Diff
+516
@@ -0,0 +1,516 @@
|
||||
# mypy: allow-untyped-defs
|
||||
"""The ONNX verification module provides a set of tools to verify the correctness of ONNX models."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
__all__ = [
|
||||
"OnnxBackend",
|
||||
"VerificationOptions",
|
||||
"verify",
|
||||
]
|
||||
|
||||
import contextlib
|
||||
import copy
|
||||
import dataclasses
|
||||
import enum
|
||||
import io
|
||||
import os
|
||||
import tempfile
|
||||
import warnings
|
||||
from collections.abc import Mapping, Sequence
|
||||
from typing import Any
|
||||
|
||||
import numpy as np
|
||||
import numpy.typing as npt
|
||||
|
||||
import torch
|
||||
import torch._C._onnx as _C_onnx
|
||||
from torch.onnx._internal.torchscript_exporter import utils
|
||||
from torch.types import Number
|
||||
|
||||
|
||||
# Everything below are deprecated ##############################################
|
||||
|
||||
_ORT_PROVIDERS = ("CPUExecutionProvider",)
|
||||
|
||||
_NumericType = Number | torch.Tensor | np.ndarray
|
||||
_ModelType = torch.nn.Module | torch.jit.ScriptModule
|
||||
_InputArgsType = torch.Tensor | tuple[Any, ...]
|
||||
_InputKwargsType = Mapping[str, Any]
|
||||
_OutputsType = Sequence[_NumericType] | Sequence
|
||||
|
||||
|
||||
class OnnxBackend(enum.Enum):
|
||||
"""Enum class for ONNX backend used for export verification.
|
||||
|
||||
.. deprecated:: 2.7
|
||||
Consider using ``torch.onnx.export(..., dynamo=True)`` and use the returned
|
||||
``ONNXProgram`` to test the ONNX model.
|
||||
"""
|
||||
|
||||
REFERENCE = "ONNXReferenceEvaluator"
|
||||
ONNX_RUNTIME_CPU = "CPUExecutionProvider"
|
||||
ONNX_RUNTIME_CUDA = "CUDAExecutionProvider"
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class VerificationOptions:
|
||||
"""Options for ONNX export verification.
|
||||
|
||||
.. deprecated:: 2.7
|
||||
Consider using ``torch.onnx.export(..., dynamo=True)`` and use the returned
|
||||
``ONNXProgram`` to test the ONNX model.
|
||||
|
||||
Attributes:
|
||||
flatten: If True, unpack nested list/tuple/dict inputs into a flattened list of
|
||||
Tensors for ONNX. Set this to False if nested structures are to be preserved
|
||||
for ONNX, which is usually the case with exporting ScriptModules. Default True.
|
||||
ignore_none: Whether to ignore None type in torch output, which is usually the
|
||||
case with tracing. Set this to False, if torch output should keep None type,
|
||||
which is usually the case with exporting ScriptModules. Default to True.
|
||||
check_shape: Whether to check the shapes between PyTorch and ONNX Runtime outputs
|
||||
are exactly the same. Set this to False to allow output shape broadcasting.
|
||||
Default to True.
|
||||
check_dtype: Whether to check the dtypes between PyTorch and ONNX Runtime outputs
|
||||
are consistent. Default to True.
|
||||
backend: ONNX backend for verification. Default to OnnxBackend.ONNX_RUNTIME_CPU.
|
||||
rtol: relative tolerance in comparison between ONNX and PyTorch outputs.
|
||||
atol: absolute tolerance in comparison between ONNX and PyTorch outputs.
|
||||
remained_onnx_input_idx: If provided, only the specified inputs will be passed
|
||||
to the ONNX model. Supply a list when there are unused inputs in the model.
|
||||
Since unused inputs will be removed in the exported ONNX model, supplying
|
||||
all inputs will cause an error on unexpected inputs. This parameter tells
|
||||
the verifier which inputs to pass into the ONNX model.
|
||||
acceptable_error_percentage: acceptable percentage of element mismatches in comparison.
|
||||
It should be a float of value between 0.0 and 1.0.
|
||||
"""
|
||||
|
||||
flatten: bool = True
|
||||
ignore_none: bool = True
|
||||
check_shape: bool = True
|
||||
check_dtype: bool = True
|
||||
backend: OnnxBackend = OnnxBackend.ONNX_RUNTIME_CPU
|
||||
rtol: float = 1e-3
|
||||
atol: float = 1e-7
|
||||
remained_onnx_input_idx: Sequence[int] | None = None
|
||||
acceptable_error_percentage: float | None = None
|
||||
|
||||
|
||||
def _flatten_tuples(elem):
|
||||
flattened = []
|
||||
for t in elem:
|
||||
if isinstance(t, tuple):
|
||||
flattened.extend(_flatten_tuples(t))
|
||||
else:
|
||||
flattened.append(t)
|
||||
return flattened
|
||||
|
||||
|
||||
def _to_numpy(elem) -> list | npt.NDArray:
|
||||
if isinstance(elem, torch.Tensor):
|
||||
if elem.requires_grad:
|
||||
return elem.detach().cpu().numpy()
|
||||
else:
|
||||
return elem.cpu().numpy()
|
||||
elif isinstance(elem, (list, tuple)):
|
||||
return [_to_numpy(inp) for inp in elem]
|
||||
elif isinstance(elem, (bool, int, float)):
|
||||
return np.array(elem)
|
||||
elif isinstance(elem, dict):
|
||||
flattened = []
|
||||
for k in elem:
|
||||
flattened.extend([_to_numpy(k), _to_numpy(elem[k])])
|
||||
return flattened
|
||||
return elem
|
||||
|
||||
|
||||
def _inline_flatten_list(inputs, res_list) -> list:
|
||||
for i in inputs:
|
||||
res_list.append(i) if not isinstance(
|
||||
i, (list, tuple)
|
||||
) else _inline_flatten_list(i, res_list)
|
||||
return res_list
|
||||
|
||||
|
||||
def _unpack_to_numpy(values, cast_onnx_accepted=True) -> list:
|
||||
value_unpacked = []
|
||||
for value in values:
|
||||
value_unpacked.extend(
|
||||
utils.unpack_quantized_tensor(value, cast_onnx_accepted=cast_onnx_accepted)
|
||||
)
|
||||
return [_to_numpy(v) for v in value_unpacked]
|
||||
|
||||
|
||||
def _run_onnx(onnx_session, inputs) -> _OutputsType:
|
||||
kw_inputs = {}
|
||||
if inputs and isinstance(inputs[-1], dict):
|
||||
kw_inputs = inputs[-1]
|
||||
inputs = inputs[:-1]
|
||||
inputs = _unpack_to_numpy(_flatten_tuples(inputs))
|
||||
ort_inputs = {}
|
||||
for input_name, input in kw_inputs.items():
|
||||
ort_inputs[input_name] = _to_numpy(input)
|
||||
inputs = _to_numpy(inputs)
|
||||
if hasattr(onnx_session, "get_inputs"):
|
||||
# onnxruntime.InferenceSession
|
||||
input_names = [i.name for i in onnx_session.get_inputs()]
|
||||
elif hasattr(onnx_session, "input_names"):
|
||||
# onnx.reference.ReferenceEvaluator
|
||||
input_names = onnx_session.input_names
|
||||
else:
|
||||
raise ValueError(f"Unknown ONNX backend type: {type(onnx_session)}.")
|
||||
|
||||
for i, input in enumerate(inputs):
|
||||
if i == len(input_names) or input_names[i] in ort_inputs:
|
||||
raise ValueError(
|
||||
f"got too many positional inputs. inputs: {inputs}. kw_inputs: {kw_inputs}. "
|
||||
f"input names: {input_names}."
|
||||
)
|
||||
ort_inputs[input_names[i]] = input
|
||||
onnx_outs = onnx_session.run(None, ort_inputs)
|
||||
return onnx_outs
|
||||
|
||||
|
||||
def _ort_session(
|
||||
model: str | io.BytesIO, ort_providers: Sequence[str] = _ORT_PROVIDERS
|
||||
):
|
||||
try:
|
||||
import onnxruntime # type: ignore[import]
|
||||
except ImportError as e:
|
||||
raise ImportError("onnxruntime is required for export verification.") from e
|
||||
|
||||
if ort_providers is None:
|
||||
ort_providers = _ORT_PROVIDERS
|
||||
|
||||
session_options = onnxruntime.SessionOptions()
|
||||
# suppress ort warnings.
|
||||
# 0:Verbose, 1:Info, 2:Warning. 3:Error, 4:Fatal. Default is 2.
|
||||
session_options.log_severity_level = 3
|
||||
ort_session = onnxruntime.InferenceSession(
|
||||
model if isinstance(model, str) else model.getvalue(),
|
||||
session_options,
|
||||
providers=ort_providers,
|
||||
)
|
||||
return ort_session
|
||||
|
||||
|
||||
def _onnx_backend_session(model: str | io.BytesIO, backend: OnnxBackend):
|
||||
if backend == OnnxBackend.REFERENCE:
|
||||
raise NotImplementedError
|
||||
elif backend in {OnnxBackend.ONNX_RUNTIME_CPU, OnnxBackend.ONNX_RUNTIME_CUDA}:
|
||||
onnx_session = _ort_session(model, (backend.value,))
|
||||
else:
|
||||
raise ValueError(f"Unsupported backend: {backend}")
|
||||
return onnx_session
|
||||
|
||||
|
||||
def _compare_onnx_pytorch_outputs_in_np(
|
||||
onnx_outs: _OutputsType,
|
||||
pt_outs: _OutputsType,
|
||||
options: VerificationOptions,
|
||||
) -> None:
|
||||
if len(onnx_outs) != len(pt_outs):
|
||||
raise AssertionError(
|
||||
f"Number of outputs differ ONNX runtime: ({len(onnx_outs)}) PyTorch: ({len(pt_outs)})"
|
||||
)
|
||||
acceptable_error_percentage = options.acceptable_error_percentage
|
||||
if acceptable_error_percentage and (
|
||||
acceptable_error_percentage > 1.0 or acceptable_error_percentage < 0.0
|
||||
):
|
||||
raise ValueError(
|
||||
"If set, acceptable_error_percentage should be between 0.0 and 1.0"
|
||||
)
|
||||
|
||||
for ort_out, pt_out in zip(onnx_outs, pt_outs):
|
||||
try:
|
||||
# TODO: Remove `check_shape` option once every shape inconsistent issue is addressed.
|
||||
if not options.check_shape:
|
||||
# Allow different but broadcastable output shapes.
|
||||
ort_out, pt_out = np.broadcast_arrays(ort_out, pt_out)
|
||||
torch.testing.assert_close(
|
||||
ort_out,
|
||||
pt_out,
|
||||
rtol=options.rtol,
|
||||
atol=options.atol,
|
||||
check_dtype=options.check_dtype,
|
||||
equal_nan=True,
|
||||
)
|
||||
except AssertionError as e:
|
||||
if acceptable_error_percentage:
|
||||
error_percentage = 1 - np.sum(
|
||||
np.isclose(ort_out, pt_out, rtol=options.rtol, atol=options.atol)
|
||||
) / np.prod(ort_out.shape) # pyrefly: ignore [missing-attribute]
|
||||
if error_percentage <= acceptable_error_percentage:
|
||||
warnings.warn(
|
||||
f"Suppressed AssertionError:\n{e}.\n"
|
||||
f"Error percentage {error_percentage} "
|
||||
f"within acceptable range {acceptable_error_percentage}.",
|
||||
stacklevel=2,
|
||||
)
|
||||
continue
|
||||
# pyrefly: ignore [missing-attribute]
|
||||
if ort_out.dtype == np.uint8 or ort_out.dtype == np.int8:
|
||||
warnings.warn("ONNX output is quantized", stacklevel=2)
|
||||
# pyrefly: ignore [missing-attribute]
|
||||
if pt_out.dtype == np.uint8 or pt_out.dtype == np.int8:
|
||||
warnings.warn("PyTorch output is quantized", stacklevel=2)
|
||||
raise
|
||||
|
||||
|
||||
def _compare_onnx_pytorch_outputs(
|
||||
onnx_outs: _OutputsType,
|
||||
pt_outs: Any,
|
||||
options: VerificationOptions,
|
||||
) -> None:
|
||||
"""
|
||||
Compare ONNX and PyTorch outputs.
|
||||
|
||||
Args:
|
||||
onnx_outs: outputs from ONNX backend.
|
||||
pt_outs: outputs from PyTorch.
|
||||
options: options for verification.
|
||||
|
||||
Raises:
|
||||
AssertionError: if outputs from ONNX model and PyTorch model are not
|
||||
equal up to specified precision.
|
||||
ValueError: if arguments provided are invalid.
|
||||
"""
|
||||
if options.ignore_none:
|
||||
# torch.jit._flatten filters None type
|
||||
pt_outs, _ = torch.jit._flatten(pt_outs)
|
||||
else:
|
||||
pt_outs = _inline_flatten_list([pt_outs], [])
|
||||
pt_outs_np = _unpack_to_numpy(pt_outs, cast_onnx_accepted=False)
|
||||
onnx_outs = _inline_flatten_list(onnx_outs, [])
|
||||
_compare_onnx_pytorch_outputs_in_np(onnx_outs, pt_outs_np, options)
|
||||
|
||||
|
||||
def _prepare_input_for_pytorch(args, kwargs):
|
||||
"""Prepare input for PyTorch model execution.
|
||||
|
||||
Any future changes/formatting to the input before dispatching to the PyTorch
|
||||
model should be made in this function.
|
||||
|
||||
Args:
|
||||
args: positional arguments for PyTorch model forward method.
|
||||
kwargs: keyword arguments for PyTorch model forward method.
|
||||
|
||||
Returns:
|
||||
args: positional arguments for PyTorch model forward method.
|
||||
kwargs: keyword arguments for PyTorch model forward method.
|
||||
"""
|
||||
if isinstance(args, (torch.Tensor, dict)):
|
||||
args = (args,)
|
||||
# In-place operators will update input tensor data as well.
|
||||
# Thus inputs are replicated before every forward call.
|
||||
args = copy.deepcopy(args)
|
||||
if kwargs:
|
||||
kwargs = copy.deepcopy(kwargs)
|
||||
else:
|
||||
kwargs = {}
|
||||
return args, kwargs
|
||||
|
||||
|
||||
def _prepare_input_for_export(args, kwargs):
|
||||
"""Prepare input for ONNX model export.
|
||||
|
||||
Any future changes/formatting to the input before dispatching to the
|
||||
:func:`torch.onnx.export` api should be made in this function.
|
||||
|
||||
Args:
|
||||
args: positional arguments for PyTorch model forward method.
|
||||
kwargs: keyword arguments for PyTorch model forward method.
|
||||
|
||||
Returns:
|
||||
onnx_inputs: positional arguments for ONNX model export, as `args` in
|
||||
:func:`torch.onnx.export`.
|
||||
"""
|
||||
args, kwargs = _prepare_input_for_pytorch(args, kwargs)
|
||||
if not kwargs and len(args) > 0 and isinstance(args[-1], dict):
|
||||
onnx_inputs = args + ({},)
|
||||
elif kwargs:
|
||||
onnx_inputs = args + (kwargs,)
|
||||
else:
|
||||
onnx_inputs = args
|
||||
return onnx_inputs
|
||||
|
||||
|
||||
def _prepare_input_for_onnx(
|
||||
args, kwargs, remained_onnx_input_idx: Sequence[int] | None, flatten: bool
|
||||
):
|
||||
"""Prepare input for ONNX model execution in ONNX backend.
|
||||
|
||||
Any future changes/formatting to the input before dispatching to the ONNX backend
|
||||
run should be made in this function.
|
||||
|
||||
Args:
|
||||
args: positional arguments for PyTorch model forward method.
|
||||
kwargs: keyword arguments for PyTorch model forward method.
|
||||
remained_onnx_input_idx: indices of inputs to be used for ONNX model execution.
|
||||
flatten: whether to flatten the input before dispatching to the ONNX model execution.
|
||||
|
||||
Returns:
|
||||
onnx_inputs: positional arguments for ONNX model execution in ONNX backend.
|
||||
"""
|
||||
onnx_inputs = _prepare_input_for_export(args, kwargs)
|
||||
if flatten:
|
||||
onnx_inputs, _ = torch.jit._flatten(onnx_inputs)
|
||||
elif onnx_inputs and onnx_inputs[-1] == {}:
|
||||
# Handle empty kwargs (normally removed by flatten).
|
||||
onnx_inputs = onnx_inputs[:-1]
|
||||
if remained_onnx_input_idx is not None:
|
||||
return [onnx_inputs[i] for i in remained_onnx_input_idx]
|
||||
else:
|
||||
return onnx_inputs
|
||||
|
||||
|
||||
def _try_clone_model(model):
|
||||
"""Used for preserving original model in case forward mutates model states."""
|
||||
try:
|
||||
return copy.deepcopy(model)
|
||||
except Exception:
|
||||
warnings.warn(
|
||||
"Failed to clone model. Model state might be mutated during verification.",
|
||||
stacklevel=2,
|
||||
)
|
||||
return model
|
||||
|
||||
|
||||
def _compare_onnx_pytorch_model(
|
||||
pt_model: _ModelType,
|
||||
onnx_model_f: str | io.BytesIO,
|
||||
input_args: _InputArgsType,
|
||||
input_kwargs: _InputKwargsType | None,
|
||||
additional_test_inputs: Sequence[_InputArgsType] | None,
|
||||
options: VerificationOptions,
|
||||
) -> None:
|
||||
"""Compare outputs from ONNX model runs with outputs from PyTorch model runs.
|
||||
|
||||
Args:
|
||||
pt_model: PyTorch model.
|
||||
onnx_model_f: ONNX model file path or file-like object.
|
||||
input_args: positional arguments for PyTorch model forward method.
|
||||
input_kwargs: keyword arguments for PyTorch model forward method.
|
||||
additional_test_inputs: additional positional arguments for PyTorch model
|
||||
forward method.
|
||||
options: options for verification.
|
||||
|
||||
Raises:
|
||||
AssertionError: if outputs from ONNX model and PyTorch model are not
|
||||
equal up to specified precision.
|
||||
"""
|
||||
onnx_session = _onnx_backend_session(onnx_model_f, options.backend)
|
||||
|
||||
def compare_onnx_pytorch_model_with_input(input_args, input_kwargs) -> None:
|
||||
pt_args, pt_kwargs = _prepare_input_for_pytorch(input_args, input_kwargs)
|
||||
# TODO: remove this and treat mutating model separately. See #77679
|
||||
pt_model_copy = _try_clone_model(pt_model)
|
||||
pt_outs = pt_model_copy(*pt_args, **pt_kwargs)
|
||||
|
||||
onnx_inputs = _prepare_input_for_onnx(
|
||||
input_args, input_kwargs, options.remained_onnx_input_idx, options.flatten
|
||||
)
|
||||
|
||||
onnx_outs = _run_onnx(onnx_session, onnx_inputs)
|
||||
|
||||
_compare_onnx_pytorch_outputs(
|
||||
onnx_outs=onnx_outs,
|
||||
pt_outs=pt_outs,
|
||||
options=options,
|
||||
)
|
||||
|
||||
compare_onnx_pytorch_model_with_input(input_args, input_kwargs)
|
||||
|
||||
if additional_test_inputs:
|
||||
for test_input_args in additional_test_inputs:
|
||||
compare_onnx_pytorch_model_with_input(test_input_args, {})
|
||||
|
||||
|
||||
def verify(
|
||||
model: _ModelType,
|
||||
input_args: _InputArgsType,
|
||||
input_kwargs: _InputKwargsType | None = None,
|
||||
do_constant_folding: bool = True,
|
||||
dynamic_axes: Mapping[str, Mapping[int, str] | Mapping[str, Sequence[int]]]
|
||||
| None = None,
|
||||
input_names: Sequence[str] | None = None,
|
||||
output_names: Sequence[str] | None = None,
|
||||
training: _C_onnx.TrainingMode = _C_onnx.TrainingMode.EVAL,
|
||||
opset_version: int | None = None,
|
||||
keep_initializers_as_inputs: bool = True,
|
||||
verbose: bool = False,
|
||||
fixed_batch_size: bool = False,
|
||||
use_external_data: bool = False,
|
||||
additional_test_inputs: Sequence[_InputArgsType] | None = None,
|
||||
options: VerificationOptions | None = None,
|
||||
) -> None:
|
||||
"""Verify model export to ONNX against original PyTorch model.
|
||||
|
||||
.. deprecated:: 2.7
|
||||
Consider using ``torch.onnx.export(..., dynamo=True)`` and use the returned
|
||||
``ONNXProgram`` to test the ONNX model.
|
||||
|
||||
Args:
|
||||
model: See :func:`torch.onnx.export`.
|
||||
input_args: See :func:`torch.onnx.export`.
|
||||
input_kwargs: See :func:`torch.onnx.export`.
|
||||
do_constant_folding: See :func:`torch.onnx.export`.
|
||||
dynamic_axes: See :func:`torch.onnx.export`.
|
||||
input_names: See :func:`torch.onnx.export`.
|
||||
output_names: See :func:`torch.onnx.export`.
|
||||
training: See :func:`torch.onnx.export`.
|
||||
opset_version: See :func:`torch.onnx.export`.
|
||||
keep_initializers_as_inputs: See :func:`torch.onnx.export`.
|
||||
verbose: See :func:`torch.onnx.export`.
|
||||
fixed_batch_size: Legacy argument, used only by rnn test cases.
|
||||
use_external_data: Explicitly specify whether to export the model with external data.
|
||||
additional_test_inputs: List of tuples. Each tuple is a group of
|
||||
input arguments to test. Currently only ``*args`` are supported.
|
||||
options: A VerificationOptions object that controls the verification behavior.
|
||||
|
||||
Raises:
|
||||
AssertionError: if outputs from ONNX model and PyTorch model are not
|
||||
equal up to specified precision.
|
||||
ValueError: if arguments provided are invalid.
|
||||
"""
|
||||
if options is None:
|
||||
options = VerificationOptions()
|
||||
|
||||
if training == torch.onnx.TrainingMode.TRAINING:
|
||||
model.train()
|
||||
elif training == torch.onnx.TrainingMode.EVAL:
|
||||
model.eval()
|
||||
with torch.no_grad(), contextlib.ExitStack() as stack:
|
||||
model_f: str | io.BytesIO = io.BytesIO()
|
||||
if use_external_data:
|
||||
tmpdir_path = stack.enter_context(tempfile.TemporaryDirectory())
|
||||
model_f = os.path.join(tmpdir_path, "model.onnx")
|
||||
|
||||
inputs_for_export = _prepare_input_for_export(input_args, input_kwargs)
|
||||
|
||||
# TODO(#77679): remove this and treat mutating model separately.
|
||||
model_copy = _try_clone_model(model)
|
||||
utils._export(
|
||||
model,
|
||||
inputs_for_export,
|
||||
model_f,
|
||||
opset_version=opset_version,
|
||||
do_constant_folding=do_constant_folding,
|
||||
keep_initializers_as_inputs=keep_initializers_as_inputs,
|
||||
dynamic_axes=dynamic_axes,
|
||||
input_names=input_names,
|
||||
output_names=output_names,
|
||||
fixed_batch_size=fixed_batch_size,
|
||||
training=training,
|
||||
verbose=verbose,
|
||||
)
|
||||
|
||||
_compare_onnx_pytorch_model(
|
||||
pt_model=model_copy,
|
||||
onnx_model_f=model_f,
|
||||
input_args=input_args,
|
||||
input_kwargs=input_kwargs,
|
||||
additional_test_inputs=additional_test_inputs,
|
||||
options=options,
|
||||
)
|
||||
Reference in New Issue
Block a user