hyperonnx 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- hyperonnx/__init__.py +22 -0
- hyperonnx/auto.py +207 -0
- hyperonnx/exporter/__init__.py +51 -0
- hyperonnx/exporter/dynamo.py +286 -0
- hyperonnx/exporter/torchscript.py +134 -0
- hyperonnx/exporter/utils.py +143 -0
- hyperonnx/function_rewriter.py +544 -0
- hyperonnx/hyper_export.py +474 -0
- hyperonnx/patch.py +66 -0
- hyperonnx/torch_export.py +155 -0
- hyperonnx/typing.py +84 -0
- hyperonnx/utils.py +27 -0
- hyperonnx-1.0.0.dist-info/METADATA +154 -0
- hyperonnx-1.0.0.dist-info/RECORD +16 -0
- hyperonnx-1.0.0.dist-info/WHEEL +4 -0
- hyperonnx-1.0.0.dist-info/licenses/LICENSE +176 -0
hyperonnx/__init__.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Copyright (C) 2025 The HYPERONNX Authors.
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from .auto import auto_trace_method
|
|
18
|
+
from .hyper_export import export_hyper_onnx
|
|
19
|
+
from .utils import HYPER_DOMAIN
|
|
20
|
+
|
|
21
|
+
__all__ = ["auto_trace_method", "export_hyper_onnx", "HYPER_DOMAIN"]
|
|
22
|
+
__version__ = "1.0.0"
|
hyperonnx/auto.py
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Copyright (C) 2025 The HYPERONNX Authors.
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
import inspect
|
|
18
|
+
from contextlib import contextmanager
|
|
19
|
+
from functools import wraps
|
|
20
|
+
from io import BytesIO
|
|
21
|
+
from os import PathLike
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
from types import MethodType
|
|
24
|
+
from typing import Collection, Dict, List, Optional, Tuple
|
|
25
|
+
|
|
26
|
+
from torch import inference_mode
|
|
27
|
+
from torch.nn import Module
|
|
28
|
+
|
|
29
|
+
from .hyper_export import export_hyper_onnx
|
|
30
|
+
from .typing import AnyTensor, ModuleSpec
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class AutoTraceMethod(Module):
|
|
34
|
+
"""Automatically trace export all functionalities of a class method.
|
|
35
|
+
|
|
36
|
+
Example::
|
|
37
|
+
|
|
38
|
+
class MyClass:
|
|
39
|
+
|
|
40
|
+
def _some_real_work(self, *args):
|
|
41
|
+
...
|
|
42
|
+
|
|
43
|
+
def sample(self, input_tokens):
|
|
44
|
+
# some code here
|
|
45
|
+
self._some_real_work(features)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
my_class = MyClass()
|
|
49
|
+
with AutoTraceMethod(my_class._some_real_work) as tracer:
|
|
50
|
+
# normal inference
|
|
51
|
+
my_class.sample(input_tokens=tokens)
|
|
52
|
+
tracer.export("some_real_work.onnx", hiera=[MySubClass])
|
|
53
|
+
|
|
54
|
+
Note:
|
|
55
|
+
|
|
56
|
+
* The method should be a method of any object, can't be a pure function.
|
|
57
|
+
* The method should be "tensor-to-tensor", any non-tensor and non-POD data
|
|
58
|
+
or cached members can't be traced.
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
def __init__(self, clsmethod: MethodType, expected_stages: int = 1):
|
|
62
|
+
super().__init__()
|
|
63
|
+
self.pos_args: List[Tuple[AnyTensor, ...]] = []
|
|
64
|
+
self.kwargs: List[Dict[str, AnyTensor]] = []
|
|
65
|
+
model = clsmethod.__self__
|
|
66
|
+
if not isinstance(model, Module):
|
|
67
|
+
for k, v in model.__dict__.items():
|
|
68
|
+
setattr(self, k, v)
|
|
69
|
+
self._forward = None
|
|
70
|
+
else:
|
|
71
|
+
self._forward = model.forward
|
|
72
|
+
self._method = clsmethod
|
|
73
|
+
self._cnt = 0
|
|
74
|
+
self._expected_stages = expected_stages
|
|
75
|
+
|
|
76
|
+
def forward(self, *args, **kwargs):
|
|
77
|
+
"""Redirect to the original method"""
|
|
78
|
+
if self._cnt >= self._expected_stages:
|
|
79
|
+
raise StopIteration(
|
|
80
|
+
f"Method call exceeds the expected stages {self._expected_stages}."
|
|
81
|
+
)
|
|
82
|
+
self.pos_args.append(args)
|
|
83
|
+
self.kwargs.append(kwargs)
|
|
84
|
+
self._cnt += 1
|
|
85
|
+
return self._method(*args, **kwargs)
|
|
86
|
+
|
|
87
|
+
def export(
|
|
88
|
+
self,
|
|
89
|
+
f: str | PathLike | BytesIO,
|
|
90
|
+
*,
|
|
91
|
+
input_names: Optional[List[str]] = None,
|
|
92
|
+
output_names: Optional[List[str]] = None,
|
|
93
|
+
opset_version: int = 19,
|
|
94
|
+
dynamo: bool = False,
|
|
95
|
+
external_data: bool = False,
|
|
96
|
+
hiera: Optional[Collection[type[Module]]] = None,
|
|
97
|
+
module_spec: Optional[Dict[Module, ModuleSpec]] = None,
|
|
98
|
+
do_optimization: bool = True,
|
|
99
|
+
external_directory: Optional[str | PathLike] = None,
|
|
100
|
+
):
|
|
101
|
+
"""Export onnx model according to the traced data."""
|
|
102
|
+
if not self.pos_args and not self.kwargs:
|
|
103
|
+
raise RuntimeError(
|
|
104
|
+
"No input data traced, did you call the method under context "
|
|
105
|
+
"HYPERONNX.export.auto_trace_method?"
|
|
106
|
+
)
|
|
107
|
+
for i in range(self._expected_stages):
|
|
108
|
+
args = self.pos_args[i]
|
|
109
|
+
kwargs = self.kwargs[i]
|
|
110
|
+
if isinstance(self._method.__self__, Module):
|
|
111
|
+
model: Module = self._method.__self__
|
|
112
|
+
model.forward = self._method
|
|
113
|
+
else:
|
|
114
|
+
model = self
|
|
115
|
+
args = tuple(args) + tuple(kwargs.values())
|
|
116
|
+
kwargs = {}
|
|
117
|
+
try:
|
|
118
|
+
if isinstance(f, BytesIO) and self._expected_stages > 1:
|
|
119
|
+
raise RuntimeError(
|
|
120
|
+
"Can't export multiple stages into a single BytesIO."
|
|
121
|
+
)
|
|
122
|
+
elif isinstance(f, (str, PathLike)) and self._expected_stages > 1:
|
|
123
|
+
f = Path(f)
|
|
124
|
+
f = f.with_suffix(f".{i}{f.suffix}")
|
|
125
|
+
if external_directory and self._expected_stages > 1:
|
|
126
|
+
external_directory = Path(external_directory)
|
|
127
|
+
external_directory = external_directory / f"{i}"
|
|
128
|
+
external_directory.mkdir(parents=True, exist_ok=True)
|
|
129
|
+
export_hyper_onnx(
|
|
130
|
+
model,
|
|
131
|
+
args,
|
|
132
|
+
f,
|
|
133
|
+
kwargs=kwargs,
|
|
134
|
+
input_names=input_names,
|
|
135
|
+
output_names=output_names,
|
|
136
|
+
opset_version=opset_version,
|
|
137
|
+
dynamo=dynamo,
|
|
138
|
+
external_data=external_data,
|
|
139
|
+
hiera=hiera,
|
|
140
|
+
module_spec=module_spec,
|
|
141
|
+
do_optimization=do_optimization,
|
|
142
|
+
external_directory=external_directory,
|
|
143
|
+
)
|
|
144
|
+
finally:
|
|
145
|
+
if (
|
|
146
|
+
isinstance(self._method.__self__, Module)
|
|
147
|
+
and self._forward is not None
|
|
148
|
+
):
|
|
149
|
+
self._method.__self__.forward = self._forward
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
@contextmanager
|
|
153
|
+
def auto_trace_method(clsmethod: MethodType, expected_stages: int = 1):
|
|
154
|
+
"""Enter a context to trace the given method, and try to export this method
|
|
155
|
+
directly by wrapping it with AutoTraceMethod.
|
|
156
|
+
|
|
157
|
+
Args:
|
|
158
|
+
clsmethod (MethodType): a class method to export
|
|
159
|
+
expected_stages (int): number of expected `clsmethod` calls, will raise
|
|
160
|
+
a StopIteration exception if call of `clsmethod` exceeds this number.
|
|
161
|
+
Defaults to 1.
|
|
162
|
+
|
|
163
|
+
Yields:
|
|
164
|
+
AutoTraceMethod: a tracer handle to help export with traced data.
|
|
165
|
+
|
|
166
|
+
Raises:
|
|
167
|
+
StopIteration: if the call of `clsmethod` exceeds `expected_stages`.
|
|
168
|
+
|
|
169
|
+
Note:
|
|
170
|
+
The method be traced must be a tensor-to-tensor method. The array of
|
|
171
|
+
numpy is not supported.
|
|
172
|
+
|
|
173
|
+
Warning:
|
|
174
|
+
This function is still in experimental stage, and may change in the future.
|
|
175
|
+
|
|
176
|
+
Example::
|
|
177
|
+
|
|
178
|
+
with auto_trace_method(my_model.function1) as tracer:
|
|
179
|
+
# inference normally
|
|
180
|
+
my_model(input_data)
|
|
181
|
+
# call tracer.export within the context
|
|
182
|
+
tracer.export("function1.onnx")
|
|
183
|
+
"""
|
|
184
|
+
|
|
185
|
+
if not inspect.ismethod(clsmethod):
|
|
186
|
+
raise ValueError(
|
|
187
|
+
"clsmethod should be a method of a nn.Module object, "
|
|
188
|
+
"call auto_trace_method(Foo().bar) rather than auto_trace_method(Foo.bar)."
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
self = clsmethod.__self__
|
|
192
|
+
method_ori = clsmethod
|
|
193
|
+
context = inference_mode()
|
|
194
|
+
try:
|
|
195
|
+
# pylint: disable=unnecessary-dunder-call
|
|
196
|
+
context.__enter__()
|
|
197
|
+
tracer = AutoTraceMethod(clsmethod, expected_stages)
|
|
198
|
+
|
|
199
|
+
@wraps(clsmethod)
|
|
200
|
+
def _func(*args, **kwargs):
|
|
201
|
+
return tracer(*args, **kwargs)
|
|
202
|
+
|
|
203
|
+
setattr(self, clsmethod.__name__, _func)
|
|
204
|
+
yield tracer
|
|
205
|
+
finally:
|
|
206
|
+
setattr(self, clsmethod.__name__, method_ori)
|
|
207
|
+
context.__exit__(None, None, None)
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Copyright (C) 2025 The HYPERONNX Authors.
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from contextlib import contextmanager
|
|
18
|
+
from typing import Dict
|
|
19
|
+
|
|
20
|
+
from torch.nn import Module
|
|
21
|
+
|
|
22
|
+
from ..typing import ModuleSpec
|
|
23
|
+
from .dynamo import replace_with_custom_op
|
|
24
|
+
from .torchscript import replace_duck_forward
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@contextmanager
|
|
28
|
+
def replace_with_duck_module(
|
|
29
|
+
model: Module, dynamo: bool, module_spec: Dict[Module, ModuleSpec]
|
|
30
|
+
):
|
|
31
|
+
"""Replace the forward function of modules in `module_spec` with a duck type.
|
|
32
|
+
|
|
33
|
+
It's used to laterly replace the duck type with the embedded onnx functions.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
model (Module): The torch module which is the top level of the model.
|
|
37
|
+
dynamo (bool): Whether to export the model with ``torch.export`` ExportedProgram
|
|
38
|
+
instead of TorchScript.
|
|
39
|
+
module_spec (Dict[Module, ModuleSpec]): The dictionary to store the spec of
|
|
40
|
+
modules. See :func:`make_hierarchical_hook` for more details.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
try:
|
|
44
|
+
if dynamo:
|
|
45
|
+
with replace_with_custom_op(model, module_spec) as tb:
|
|
46
|
+
yield tb
|
|
47
|
+
else:
|
|
48
|
+
with replace_duck_forward(model, module_spec) as _:
|
|
49
|
+
yield _
|
|
50
|
+
finally:
|
|
51
|
+
pass
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Copyright (C) 2026 The HYPERONNX Authors.
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
import inspect
|
|
18
|
+
from ast import AST
|
|
19
|
+
from collections import OrderedDict
|
|
20
|
+
from contextlib import contextmanager
|
|
21
|
+
from typing import Callable, Dict
|
|
22
|
+
|
|
23
|
+
import onnxscript
|
|
24
|
+
import onnxscript.ir._schemas as schemas
|
|
25
|
+
import onnxscript.irbuilder as irbuilder
|
|
26
|
+
import onnxscript.sourceinfo
|
|
27
|
+
import torch
|
|
28
|
+
from onnxifier.logger import warning
|
|
29
|
+
from torch.library import custom_op
|
|
30
|
+
from torch.nn import Module
|
|
31
|
+
|
|
32
|
+
from ..typing import AnyTensor, ExportStatus, ModuleSpec
|
|
33
|
+
from .utils import plain_tensor_container
|
|
34
|
+
|
|
35
|
+
NAMESPACE = "hyper"
|
|
36
|
+
DOMAIN = onnxscript.values.Opset(domain=NAMESPACE, version=1)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _tensor_dtype_to_onnx_dtype(dtype: torch.dtype):
|
|
40
|
+
if dtype == torch.float32:
|
|
41
|
+
return onnxscript.onnx_types.FLOAT
|
|
42
|
+
elif dtype == torch.float16:
|
|
43
|
+
return onnxscript.onnx_types.FLOAT16
|
|
44
|
+
elif dtype == torch.bfloat16:
|
|
45
|
+
return onnxscript.onnx_types.BFLOAT16
|
|
46
|
+
elif dtype == torch.float64:
|
|
47
|
+
return onnxscript.onnx_types.DOUBLE
|
|
48
|
+
elif dtype == torch.float8_e5m2:
|
|
49
|
+
return onnxscript.onnx_types.FLOAT8E5M2
|
|
50
|
+
elif dtype == torch.float8_e5m2fnuz:
|
|
51
|
+
return onnxscript.onnx_types.FLOAT8E5M2FNUZ
|
|
52
|
+
elif dtype == torch.float8_e4m3fn:
|
|
53
|
+
return onnxscript.onnx_types.FLOAT8E4M3FN
|
|
54
|
+
elif dtype == torch.float8_e4m3fnuz:
|
|
55
|
+
return onnxscript.onnx_types.FLOAT8E4M3FNUZ
|
|
56
|
+
elif dtype == torch.int8:
|
|
57
|
+
return onnxscript.onnx_types.INT8
|
|
58
|
+
elif dtype == torch.int16:
|
|
59
|
+
return onnxscript.onnx_types.INT16
|
|
60
|
+
elif dtype == torch.int32:
|
|
61
|
+
return onnxscript.onnx_types.INT32
|
|
62
|
+
elif dtype == torch.int64:
|
|
63
|
+
return onnxscript.onnx_types.INT64
|
|
64
|
+
elif dtype == torch.uint8:
|
|
65
|
+
return onnxscript.onnx_types.UINT8
|
|
66
|
+
elif dtype == torch.uint16:
|
|
67
|
+
return onnxscript.onnx_types.UINT16
|
|
68
|
+
elif dtype == torch.uint32:
|
|
69
|
+
return onnxscript.onnx_types.UINT32
|
|
70
|
+
elif dtype == torch.uint64:
|
|
71
|
+
return onnxscript.onnx_types.UINT64
|
|
72
|
+
elif dtype == torch.bool:
|
|
73
|
+
return onnxscript.onnx_types.BOOL
|
|
74
|
+
raise ValueError(f"Unsupported dtype: {dtype}")
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def build_onnxscript(spec: ModuleSpec) -> onnxscript.OnnxFunction:
|
|
78
|
+
"""Dynamically build an onnx script for custom translation table."""
|
|
79
|
+
|
|
80
|
+
func_name = spec["name"] + "_func"
|
|
81
|
+
result = irbuilder.IRFunction(func_name, NAMESPACE)
|
|
82
|
+
# Note: op_name must not be the same as function name, or it would cause
|
|
83
|
+
# onnx infinite recursion (function referencing itself).
|
|
84
|
+
op_name = spec["type_name"]
|
|
85
|
+
stmt = irbuilder.IRStmt([], onnxscript.values.Op(DOMAIN, op_name), [], [])
|
|
86
|
+
annotations: dict[str, type] = OrderedDict()
|
|
87
|
+
sig_parameters: list[inspect.Parameter] = []
|
|
88
|
+
return_types = []
|
|
89
|
+
for args, name in zip(spec["args"], spec["signature"].parameters):
|
|
90
|
+
for i, arg in enumerate(plain_tensor_container(args)):
|
|
91
|
+
if arg is None:
|
|
92
|
+
continue
|
|
93
|
+
elif isinstance(arg, str):
|
|
94
|
+
irtype = onnxscript.onnx_types.STRING
|
|
95
|
+
else:
|
|
96
|
+
irtype = _tensor_dtype_to_onnx_dtype(arg.dtype)
|
|
97
|
+
sourceinfo = onnxscript.sourceinfo.SourceInfo(AST())
|
|
98
|
+
result.append_input(irbuilder.IRVar(f"{name}:{i}", irtype, sourceinfo))
|
|
99
|
+
annotations[f"{name}_{i}"] = irtype
|
|
100
|
+
sig_parameters.append(
|
|
101
|
+
inspect.Parameter(
|
|
102
|
+
name=f"{name}_{i}",
|
|
103
|
+
kind=inspect.Parameter.POSITIONAL_OR_KEYWORD,
|
|
104
|
+
)
|
|
105
|
+
)
|
|
106
|
+
if kwargs := spec.get("kwargs"):
|
|
107
|
+
sig = spec["signature"]
|
|
108
|
+
ordered_kwargs = {k: kwargs[k] for k in sig.parameters if k in kwargs}
|
|
109
|
+
for name, args in ordered_kwargs.items():
|
|
110
|
+
for i, arg in enumerate(plain_tensor_container(args)):
|
|
111
|
+
if not isinstance(arg, torch.Tensor):
|
|
112
|
+
continue
|
|
113
|
+
irtype = _tensor_dtype_to_onnx_dtype(arg.dtype)
|
|
114
|
+
sourceinfo = onnxscript.sourceinfo.SourceInfo(AST())
|
|
115
|
+
result.append_input(irbuilder.IRVar(f"{name}:{i}", irtype, sourceinfo))
|
|
116
|
+
annotations[f"{name}_{i}"] = irtype
|
|
117
|
+
sig_parameters.append(
|
|
118
|
+
inspect.Parameter(
|
|
119
|
+
name=f"{name}_{i}",
|
|
120
|
+
kind=inspect.Parameter.POSITIONAL_OR_KEYWORD,
|
|
121
|
+
)
|
|
122
|
+
)
|
|
123
|
+
if "output" in spec:
|
|
124
|
+
for i, output in enumerate(plain_tensor_container(spec["output"])):
|
|
125
|
+
irtype = _tensor_dtype_to_onnx_dtype(output.dtype)
|
|
126
|
+
sourceinfo = onnxscript.sourceinfo.SourceInfo(AST())
|
|
127
|
+
result.append_output(irbuilder.IRVar(f"output:{i}", irtype, sourceinfo))
|
|
128
|
+
return_types.append(_tensor_dtype_to_onnx_dtype(output.dtype))
|
|
129
|
+
stmt.args = [i.name for i in result.inputs]
|
|
130
|
+
stmt.result = [i.name for i in result.outputs]
|
|
131
|
+
result.append_stmt(stmt)
|
|
132
|
+
|
|
133
|
+
def _f(*args, **kwargs): # this function does nothing
|
|
134
|
+
return getattr(DOMAIN, op_name)(*args, **kwargs)
|
|
135
|
+
|
|
136
|
+
onnx_fn = onnxscript.OnnxFunction(DOMAIN, _f, result, "", {})
|
|
137
|
+
if onnx_fn.op_schema is not None:
|
|
138
|
+
# FIXME: this hack will cause infinite loop during translate the graph in ONNX
|
|
139
|
+
op_signature = schemas.OpSignature.from_op_schema(onnx_fn.op_schema)
|
|
140
|
+
onnx_fn.op_signature = op_signature
|
|
141
|
+
if len(return_types) == 1:
|
|
142
|
+
annotations["return"] = return_types[0]
|
|
143
|
+
else:
|
|
144
|
+
annotations["return"] = tuple[*return_types] # type: ignore
|
|
145
|
+
setattr(
|
|
146
|
+
onnx_fn,
|
|
147
|
+
"__signature__",
|
|
148
|
+
inspect.Signature(sig_parameters, return_annotation=annotations["return"]),
|
|
149
|
+
)
|
|
150
|
+
setattr(onnx_fn, "__annotations__", annotations)
|
|
151
|
+
|
|
152
|
+
return onnx_fn
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _assign_plain_tensors(container: dict, name: str, value: AnyTensor):
|
|
156
|
+
plain_values = plain_tensor_container(value)
|
|
157
|
+
for i, arg in enumerate(plain_values):
|
|
158
|
+
if len(plain_values) == 1:
|
|
159
|
+
container[name] = arg
|
|
160
|
+
else:
|
|
161
|
+
container[f"{name}_{i}"] = arg
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _plain_args_and_kwargs(args: tuple, kwargs: dict, signature: inspect.Signature):
|
|
165
|
+
new_args: dict[str, AnyTensor] = OrderedDict()
|
|
166
|
+
params = signature.parameters
|
|
167
|
+
for args, name in zip(args, params):
|
|
168
|
+
_assign_plain_tensors(new_args, name, args)
|
|
169
|
+
signature_has_var_kw = None
|
|
170
|
+
for k in params:
|
|
171
|
+
v = kwargs.pop(k, None)
|
|
172
|
+
if v is not None:
|
|
173
|
+
_assign_plain_tensors(new_args, k, v)
|
|
174
|
+
elif params[k].kind == inspect.Parameter.VAR_KEYWORD:
|
|
175
|
+
# forward like forward(self, x, y, **kwargs) and invoked with
|
|
176
|
+
# forward(x, y, a=1, b=2)
|
|
177
|
+
signature_has_var_kw = True
|
|
178
|
+
if kwargs and signature_has_var_kw:
|
|
179
|
+
# extend the signature from **kwargs to specific names
|
|
180
|
+
for k, v in kwargs.items():
|
|
181
|
+
_assign_plain_tensors(new_args, k, v)
|
|
182
|
+
return new_args
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def make_custom_op(module: Module, spec: ModuleSpec):
|
|
186
|
+
"""Create a custom op and registered into `torch.library`.
|
|
187
|
+
To replace the module with created custom op during dynamo export.
|
|
188
|
+
"""
|
|
189
|
+
spec_name = spec["name"] or "main" # can't be empty
|
|
190
|
+
spec_name = spec_name.replace(".", "_") # no dot allowed in op name
|
|
191
|
+
name = f"{NAMESPACE}::{spec_name}"
|
|
192
|
+
new_args = _plain_args_and_kwargs(
|
|
193
|
+
spec["args"], spec.get("kwargs", {}).copy(), spec["signature"]
|
|
194
|
+
)
|
|
195
|
+
# simple schema inference, refer to torch._library.infer_schema.infer_schema
|
|
196
|
+
# for complete logic.
|
|
197
|
+
schemas_str = []
|
|
198
|
+
for k, v in new_args.items():
|
|
199
|
+
if isinstance(v, str):
|
|
200
|
+
# WA: in onnx_ir._convenience._constructors, there is a bug for
|
|
201
|
+
# str encoding. So we filter out str arguments.
|
|
202
|
+
pass
|
|
203
|
+
# elif v is None:
|
|
204
|
+
# # Treat None as boolean
|
|
205
|
+
# schemas_str.append(f"bool {k}")
|
|
206
|
+
else:
|
|
207
|
+
schemas_str.append(f"{type(v).__name__} {k}")
|
|
208
|
+
schema = f"({','.join(schemas_str)})"
|
|
209
|
+
if "output" in spec:
|
|
210
|
+
outputs = plain_tensor_container(spec["output"])
|
|
211
|
+
if len(outputs) == 1:
|
|
212
|
+
schema += f" -> {type(outputs[0]).__name__}"
|
|
213
|
+
elif len(outputs) > 1:
|
|
214
|
+
return_vals = [type(o).__name__ for o in outputs if o is not None]
|
|
215
|
+
schema += f" -> ({','.join(return_vals)})"
|
|
216
|
+
|
|
217
|
+
def _duck_forward(*args, **kwargs):
|
|
218
|
+
output = spec.get("output", None)
|
|
219
|
+
if fw := getattr(module, "__ori_forward", None):
|
|
220
|
+
if output is None:
|
|
221
|
+
output = fw(*args, **kwargs)
|
|
222
|
+
if isinstance(output, dict):
|
|
223
|
+
warning(
|
|
224
|
+
"dynamo custom op doesn't support dict output, "
|
|
225
|
+
f"while {type(module).__qualname__} returns a dict."
|
|
226
|
+
)
|
|
227
|
+
output = plain_tensor_container(output)
|
|
228
|
+
assert output is not None
|
|
229
|
+
return output
|
|
230
|
+
|
|
231
|
+
custom_fn = custom_op(
|
|
232
|
+
name,
|
|
233
|
+
_duck_forward,
|
|
234
|
+
mutates_args=(),
|
|
235
|
+
schema=schema,
|
|
236
|
+
)
|
|
237
|
+
custom_fn.register_fake(_duck_forward)
|
|
238
|
+
onnx_fn = build_onnxscript(spec)
|
|
239
|
+
|
|
240
|
+
class _CustomWrapper(torch.nn.Module):
|
|
241
|
+
def __init__(self, fn: Callable, signature: inspect.Signature):
|
|
242
|
+
super().__init__()
|
|
243
|
+
self._fn = fn
|
|
244
|
+
self._sig = signature
|
|
245
|
+
|
|
246
|
+
def forward(self, *args, **kwargs):
|
|
247
|
+
new_args = _plain_args_and_kwargs(args, kwargs.copy(), self._sig)
|
|
248
|
+
# WA: in onnx_ir._convenience._constructors, there is a bug for
|
|
249
|
+
# str encoding. So we filter out str arguments here. Note this
|
|
250
|
+
# requires that the string argument must have a default value.
|
|
251
|
+
for k in list(new_args.keys()):
|
|
252
|
+
if isinstance(new_args[k], str):
|
|
253
|
+
new_args.pop(k)
|
|
254
|
+
return self._fn(**new_args)
|
|
255
|
+
|
|
256
|
+
return _CustomWrapper(custom_fn, spec["signature"]), {name: onnx_fn}
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
@contextmanager
|
|
260
|
+
def replace_with_custom_op(model: Module, module_spec: Dict[Module, ModuleSpec]):
|
|
261
|
+
"""Replace the forward function of modules in `module_spec` with a duck type.
|
|
262
|
+
|
|
263
|
+
It's used to laterly replace the duck type with the embedded onnx functions.
|
|
264
|
+
|
|
265
|
+
Args:
|
|
266
|
+
model (Module): The torch module which is the top level of the model.
|
|
267
|
+
module_spec (Dict[Module, ModuleSpec]): The dictionary to store the spec of
|
|
268
|
+
modules. See :func:`make_hierarchical_hook` for more details.
|
|
269
|
+
"""
|
|
270
|
+
|
|
271
|
+
try:
|
|
272
|
+
custom_translation_table = {}
|
|
273
|
+
for child in filter(lambda c: c in module_spec, model.modules()):
|
|
274
|
+
spec = module_spec[child]
|
|
275
|
+
setattr(child, "__ori_forward", child.forward)
|
|
276
|
+
if spec["status"] == ExportStatus.EXPORTED:
|
|
277
|
+
setattr(child, "__ori_forward", child.forward)
|
|
278
|
+
custom_mod, translation_table = make_custom_op(child, spec)
|
|
279
|
+
custom_translation_table.update(translation_table)
|
|
280
|
+
child.forward = custom_mod.forward
|
|
281
|
+
yield custom_translation_table
|
|
282
|
+
finally:
|
|
283
|
+
for child in model.modules():
|
|
284
|
+
if getattr(child, "__ori_forward", None):
|
|
285
|
+
child.forward = getattr(child, "__ori_forward")
|
|
286
|
+
delattr(child, "__ori_forward")
|