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 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")