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.
@@ -0,0 +1,134 @@
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 functools import update_wrapper
19
+ from typing import Dict, List
20
+
21
+ import torch
22
+ from torch import Tensor
23
+ from torch.nn import Module
24
+ from torch.utils.hooks import RemovableHandle
25
+
26
+ from ..typing import AnyTensor, ExportStatus, ModuleSpec
27
+ from .utils import plain_tensor_container
28
+
29
+
30
+ def make_duck_forward(module_spec: ModuleSpec):
31
+ """Make a duck forward function to replace this module with a duck type."""
32
+
33
+ class DuckForward(torch.autograd.Function): # pylint: disable=abstract-method
34
+ """A duck forward function to export a custom op in onnx."""
35
+
36
+ @staticmethod
37
+ def forward(*args, **kwargs):
38
+ loop = module_spec["loops"]
39
+ if loop == 0:
40
+ output = module_spec.get("output")
41
+ else:
42
+ output = module_spec.get("loop_outputs")[loop - 1]
43
+ if module_spec.get("loop_outputs"):
44
+ module_spec["loops"] += 1
45
+ if isinstance(output, torch.Tensor) or output is None:
46
+ return output
47
+ plained_outputs = plain_tensor_container(output)
48
+ return tuple(i for i in plained_outputs if i is not None)
49
+
50
+ @staticmethod
51
+ def backward(ctx, *grad_outputs):
52
+ raise RuntimeError("A duck forward function can't be backwarded.")
53
+
54
+ @staticmethod
55
+ def symbolic(g, *args):
56
+ """Make onnx symbolic.
57
+
58
+ TODO:
59
+ This method is high likely to be changed in the future.
60
+ Refer to this link to use dynamo to export custom op:
61
+
62
+ """
63
+ output = module_spec.get("output")
64
+ type_name = module_spec["type_name"]
65
+ assert output is not None
66
+ if isinstance(output, Tensor):
67
+ outputs = 1
68
+ else:
69
+ outputs = len(plain_tensor_container(output))
70
+ # TODO: g.op doesn't accept None as one of inputs
71
+ # but we don't know the side effect of this WA.
72
+ vargs = filter(lambda x: x is not None, args)
73
+ op = g.op(type_name, *vargs, outputs=outputs)
74
+ return op
75
+
76
+ @torch.inference_mode()
77
+ def _forward(*args, **kwargs):
78
+ new_args = list(plain_tensor_container(args))
79
+ if kwargs:
80
+ # ISSUE (#158): caller kwargs order may not follow the signature order,
81
+ # to replace forward function with a duck type, we need to reverse the
82
+ # caller order to match the signature order.
83
+ sig = module_spec["signature"]
84
+ ordered_kwargs = {k: kwargs[k] for k in sig.parameters if k in kwargs}
85
+ new_args.extend(plain_tensor_container(ordered_kwargs))
86
+ # NOTE: apply doesn't take **kwargs, so we need to plain input container
87
+ # and convert objects to Tensor
88
+ return DuckForward.apply(*new_args)
89
+
90
+ return _forward
91
+
92
+
93
+ @contextmanager
94
+ def replace_duck_forward(model: Module, module_spec: Dict[Module, ModuleSpec]):
95
+ """Replace the forward function of modules in `module_spec` with a duck type.
96
+
97
+ It's used to laterly replace the duck type with the embedded onnx functions.
98
+
99
+ Args:
100
+ model (Module): The torch module which is the top level of the model.
101
+ module_spec (Dict[Module, ModuleSpec]): The dictionary to store the spec of
102
+ modules. See :func:`make_hierarchical_hook` for more details.
103
+ """
104
+
105
+ def _hook_output_restore(
106
+ module: Module,
107
+ args: tuple, # pylint: disable=unused-argument
108
+ result: AnyTensor, # pylint: disable=unused-argument
109
+ ):
110
+ spec = module_spec[module]
111
+ output = spec.get("output")
112
+ return output
113
+
114
+ handles: List[RemovableHandle] = []
115
+ try:
116
+ for child in filter(lambda c: c in module_spec, model.modules()):
117
+ if module_spec[child]["status"] == ExportStatus.EXPORTED:
118
+ setattr(child, "__ori_forward", child.forward)
119
+ if module_spec[child]["output_need_to_restore"]:
120
+ handle = child.register_forward_hook(_hook_output_restore)
121
+ handles.append(handle)
122
+ # makes child.forward keep the same signature as original forward
123
+ child.forward = update_wrapper(
124
+ make_duck_forward(module_spec[child]), child.forward
125
+ )
126
+ module_spec[child]["loops"] = 0
127
+ yield
128
+ finally:
129
+ for child in model.modules():
130
+ if getattr(child, "__ori_forward", None):
131
+ child.forward = getattr(child, "__ori_forward")
132
+ delattr(child, "__ori_forward")
133
+ for handle in handles:
134
+ handle.remove()
@@ -0,0 +1,143 @@
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 itertools import chain
18
+ from typing import Sequence, Tuple
19
+
20
+ import torch
21
+ from onnxifier.logger import warning
22
+ from torch import Tensor
23
+
24
+ from ..typing import AnyTensor, ModuleSpec
25
+
26
+
27
+ def plain_tensor_container(obj: AnyTensor) -> Tuple[Tensor, ...]:
28
+ """Iteratively flatten an arbitrary output of Tensors into a tuple of Tensors.
29
+
30
+ Args:
31
+ obj (AnyTensor): Acceptable object could be a tuple, list, dict, or a nested
32
+ tuple, list, or dict of Tensors.
33
+
34
+ Examples::
35
+
36
+ obj = dict(a=[torch.rand(1), torch.rand(2)], b=dict(x=torch.ones(1)))
37
+ y = plain_tensor_container(obj)
38
+ # y => (torch.rand(1), torch.rand(2), torch.ones(1))
39
+
40
+ Raises:
41
+ ValueError: If any object can't be recursively flattened.
42
+
43
+ Returns:
44
+ Tuple[Tensor, ...]: a tuple of tensors.
45
+ """
46
+ if isinstance(obj, dict):
47
+ return plain_tensor_container(tuple(obj.values()))
48
+ elif isinstance(obj, Sequence):
49
+ if isinstance(obj, str):
50
+ # WA: for string type, return directly
51
+ return (obj,) # type: ignore
52
+ obj_seq = tuple(chain(*[plain_tensor_container(v) for v in obj]))
53
+ cls = type(obj)
54
+ try:
55
+ # restore tuple or list
56
+ return cls(obj_seq) # type: ignore
57
+ except Exception: # pylint: disable=broad-except
58
+ warning(
59
+ "During the flattening of module output, "
60
+ f"Can't construct {cls.__name__} from a tuple, we turn this output "
61
+ "into a pure tuple. However, this may lead to unexpected behavior in "
62
+ "forward function."
63
+ )
64
+ return obj_seq
65
+ elif isinstance(obj, Tensor):
66
+ return (obj,)
67
+ else: # may be constant POD
68
+ try:
69
+ return (torch.as_tensor(obj),)
70
+ except (RuntimeError, ValueError):
71
+ # I.e. None, Cache
72
+ return (obj,)
73
+ except Exception as ex:
74
+ raise ValueError(f"Unsupported output type: {type(obj)}") from ex
75
+
76
+
77
+ def detach_module_outputs(output: AnyTensor, spec: ModuleSpec) -> AnyTensor:
78
+ """Detach tensors in module outputs and record structural information.
79
+
80
+ This function recursively processes module outputs to detach tensors and
81
+ record the structural information needed for reconstruction. It handles
82
+ various output types including single tensors, dictionaries, sequences,
83
+ and None values.
84
+
85
+ Note:
86
+ If one of the output is None, it will be replaced with a tensor of False
87
+ to avoid breaking the torchscript forward function.
88
+
89
+ Args:
90
+ output (AnyTensor): The module output to process. Can be a single tensor,
91
+ a dictionary of tensors, a sequence of tensors, or None.
92
+ spec (ModuleSpec): A specification dictionary to record structural
93
+ information. Will be modified in-place to store metadata about
94
+ the output structure.
95
+
96
+ Returns:
97
+ AnyTensor: The processed output with tensors detached. The structure
98
+ is preserved when possible, but may be converted to simpler types
99
+ (like tuple) if reconstruction fails.
100
+
101
+ Raises:
102
+ TypeError: If the output type is not supported (not Tensor, dict,
103
+ Sequence, or None).
104
+ """
105
+
106
+ if isinstance(output, Tensor):
107
+ return output.detach()
108
+ elif isinstance(output, dict):
109
+ spec["output_need_to_restore"] = True
110
+ dict_value = {k: detach_module_outputs(v, spec) for k, v in output.items()}
111
+ try:
112
+ cls = type(output)
113
+ return cls(**dict_value)
114
+ except Exception: # pylint: disable=broad-except
115
+ warning(
116
+ f"The original output type was {type(output).__name__}, "
117
+ "but failed to construct it from dict, so return as a dict directly."
118
+ )
119
+ return dict_value
120
+ elif isinstance(output, Sequence):
121
+ spec["output_need_to_restore"] = True
122
+ out_seq = tuple(detach_module_outputs(v, spec) for v in output)
123
+ cls = type(output)
124
+ try:
125
+ # restore tuple or list
126
+ return cls(out_seq) # type: ignore
127
+ except Exception: # pylint: disable=broad-except
128
+ warning(
129
+ "During the analysis of module output, "
130
+ f"Can't construct {cls.__name__} from a tuple, we turn this output "
131
+ "into a pure tuple. However, this may lead to unexpected behavior in "
132
+ "forward function."
133
+ )
134
+ return out_seq
135
+ elif output is None:
136
+ # return None in duck module is a bad case, because it will be treated as
137
+ # an output of the module, while None is not a valid torch IR, leads to
138
+ # an export failure in torch.
139
+ # However, this WA has a risk that codes like `if tensor is None: ...` will
140
+ # fail.
141
+ return torch.tensor(False)
142
+ else:
143
+ raise TypeError(f"Unsupported output type: {type(output)}")