onnx-ir 0.1.15__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.
- onnx_ir/__init__.py +176 -0
- onnx_ir/_cloner.py +229 -0
- onnx_ir/_convenience/__init__.py +558 -0
- onnx_ir/_convenience/_constructors.py +291 -0
- onnx_ir/_convenience/_extractor.py +191 -0
- onnx_ir/_core.py +4435 -0
- onnx_ir/_display.py +54 -0
- onnx_ir/_enums.py +474 -0
- onnx_ir/_graph_comparison.py +23 -0
- onnx_ir/_graph_containers.py +373 -0
- onnx_ir/_io.py +133 -0
- onnx_ir/_linked_list.py +284 -0
- onnx_ir/_metadata.py +45 -0
- onnx_ir/_name_authority.py +72 -0
- onnx_ir/_polyfill.py +26 -0
- onnx_ir/_protocols.py +627 -0
- onnx_ir/_safetensors/__init__.py +510 -0
- onnx_ir/_tape.py +242 -0
- onnx_ir/_thirdparty/asciichartpy.py +310 -0
- onnx_ir/_type_casting.py +89 -0
- onnx_ir/_version_utils.py +48 -0
- onnx_ir/analysis/__init__.py +21 -0
- onnx_ir/analysis/_implicit_usage.py +74 -0
- onnx_ir/convenience.py +38 -0
- onnx_ir/external_data.py +459 -0
- onnx_ir/passes/__init__.py +41 -0
- onnx_ir/passes/_pass_infra.py +351 -0
- onnx_ir/passes/common/__init__.py +54 -0
- onnx_ir/passes/common/_c_api_utils.py +76 -0
- onnx_ir/passes/common/clear_metadata_and_docstring.py +60 -0
- onnx_ir/passes/common/common_subexpression_elimination.py +207 -0
- onnx_ir/passes/common/constant_manipulation.py +230 -0
- onnx_ir/passes/common/default_attributes.py +99 -0
- onnx_ir/passes/common/identity_elimination.py +120 -0
- onnx_ir/passes/common/initializer_deduplication.py +179 -0
- onnx_ir/passes/common/inliner.py +223 -0
- onnx_ir/passes/common/naming.py +280 -0
- onnx_ir/passes/common/onnx_checker.py +57 -0
- onnx_ir/passes/common/output_fix.py +141 -0
- onnx_ir/passes/common/shape_inference.py +112 -0
- onnx_ir/passes/common/topological_sort.py +37 -0
- onnx_ir/passes/common/unused_removal.py +215 -0
- onnx_ir/py.typed +1 -0
- onnx_ir/serde.py +2043 -0
- onnx_ir/tape.py +15 -0
- onnx_ir/tensor_adapters.py +210 -0
- onnx_ir/testing.py +197 -0
- onnx_ir/traversal.py +118 -0
- onnx_ir-0.1.15.dist-info/METADATA +68 -0
- onnx_ir-0.1.15.dist-info/RECORD +53 -0
- onnx_ir-0.1.15.dist-info/WHEEL +5 -0
- onnx_ir-0.1.15.dist-info/licenses/LICENSE +202 -0
- onnx_ir-0.1.15.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Copyright (c) ONNX Project Contributors
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
"""Analysis utilities for ONNX IR graphs."""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"analyze_implicit_usage",
|
|
9
|
+
]
|
|
10
|
+
|
|
11
|
+
from onnx_ir.analysis._implicit_usage import analyze_implicit_usage
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def __set_module() -> None:
|
|
15
|
+
"""Set the module of all functions in this module to this public module."""
|
|
16
|
+
global_dict = globals()
|
|
17
|
+
for name in __all__:
|
|
18
|
+
global_dict[name].__module__ = __name__
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
__set_module()
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# Copyright (c) ONNX Project Contributors
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
"""Identify implicit uses of values in ONNX sub-graphs."""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"analyze_implicit_usage",
|
|
9
|
+
]
|
|
10
|
+
|
|
11
|
+
import onnx_ir as ir
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def analyze_implicit_usage(graph: ir.Graph) -> dict[ir.Graph, set[ir.Value]]:
|
|
15
|
+
"""Analyze implicit usage of values in sub-graphs.
|
|
16
|
+
|
|
17
|
+
This function returns a mapping from each sub-graph to a set of
|
|
18
|
+
:class:`~onnx_ir.Value`s that are captured from outer scopes (i.e., not defined
|
|
19
|
+
within the sub-graph itself).
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
graph: The graph to analyze.
|
|
23
|
+
|
|
24
|
+
Returns:
|
|
25
|
+
A dictionary mapping sub-graphs to sets of implicitly used values.
|
|
26
|
+
"""
|
|
27
|
+
graph_stack: list[ir.Graph] = [graph]
|
|
28
|
+
implicit_usages: dict[ir.Graph, set[ir.Value]] = {}
|
|
29
|
+
for node in graph:
|
|
30
|
+
_process_node(node, implicit_usages, graph_stack)
|
|
31
|
+
return implicit_usages
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _collect_implicit_usages(
|
|
35
|
+
node: ir.Node,
|
|
36
|
+
subgraph: ir.Graph,
|
|
37
|
+
graph_stack: list[ir.Graph],
|
|
38
|
+
implicit_usages: dict[ir.Graph, set[ir.Value]],
|
|
39
|
+
) -> None:
|
|
40
|
+
for inp in node.inputs:
|
|
41
|
+
if inp is None or inp.graph is subgraph:
|
|
42
|
+
continue
|
|
43
|
+
# This is a closed variable, add to implicit usages of all graphs that enclose it
|
|
44
|
+
for g in reversed(graph_stack):
|
|
45
|
+
if g is inp.graph:
|
|
46
|
+
break
|
|
47
|
+
implicit_usages[g].add(inp)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _process_node(
|
|
51
|
+
node: ir.Node,
|
|
52
|
+
implicit_usages: dict[ir.Graph, set[ir.Value]],
|
|
53
|
+
graph_stack: list[ir.Graph],
|
|
54
|
+
) -> None:
|
|
55
|
+
"""Perform a DFS to find all implicit usages in subgraphs."""
|
|
56
|
+
for attr in node.attributes.values():
|
|
57
|
+
if attr.type == ir.AttributeType.GRAPH:
|
|
58
|
+
subgraph = attr.as_graph()
|
|
59
|
+
graph_stack.append(subgraph)
|
|
60
|
+
if subgraph not in implicit_usages:
|
|
61
|
+
implicit_usages[subgraph] = set()
|
|
62
|
+
for node in subgraph:
|
|
63
|
+
_collect_implicit_usages(node, subgraph, graph_stack, implicit_usages)
|
|
64
|
+
_process_node(node, implicit_usages, graph_stack)
|
|
65
|
+
graph_stack.pop()
|
|
66
|
+
elif attr.type == ir.AttributeType.GRAPHS:
|
|
67
|
+
for subgraph in attr.as_graphs():
|
|
68
|
+
graph_stack.append(subgraph)
|
|
69
|
+
if subgraph not in implicit_usages:
|
|
70
|
+
implicit_usages[subgraph] = set()
|
|
71
|
+
for node in subgraph:
|
|
72
|
+
_collect_implicit_usages(node, subgraph, graph_stack, implicit_usages)
|
|
73
|
+
_process_node(node, implicit_usages, graph_stack)
|
|
74
|
+
graph_stack.pop()
|
onnx_ir/convenience.py
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# Copyright (c) ONNX Project Contributors
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
"""Convenience methods for constructing and manipulating the IR."""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"convert_attribute",
|
|
9
|
+
"convert_attributes",
|
|
10
|
+
"create_value_mapping",
|
|
11
|
+
"extract",
|
|
12
|
+
"get_const_tensor",
|
|
13
|
+
"replace_all_uses_with",
|
|
14
|
+
"replace_nodes_and_values",
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
from onnx_ir._convenience import (
|
|
18
|
+
convert_attribute,
|
|
19
|
+
convert_attributes,
|
|
20
|
+
create_value_mapping,
|
|
21
|
+
get_const_tensor,
|
|
22
|
+
replace_all_uses_with,
|
|
23
|
+
replace_nodes_and_values,
|
|
24
|
+
)
|
|
25
|
+
from onnx_ir._convenience._extractor import extract
|
|
26
|
+
|
|
27
|
+
# NOTE: Do not implement any other functions in this module.
|
|
28
|
+
# implement them in the _convenience module and import them here instead.
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def __set_module() -> None:
|
|
32
|
+
"""Set the module of all functions in this module to this public module."""
|
|
33
|
+
global_dict = globals()
|
|
34
|
+
for name in __all__:
|
|
35
|
+
global_dict[name].__module__ = __name__
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
__set_module()
|
onnx_ir/external_data.py
ADDED
|
@@ -0,0 +1,459 @@
|
|
|
1
|
+
# Copyright (c) ONNX Project Contributors
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
"""External data related utilities."""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from typing import Callable
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
"set_base_dir",
|
|
11
|
+
"unload_from_model",
|
|
12
|
+
"load_to_model",
|
|
13
|
+
"convert_tensors_to_external",
|
|
14
|
+
"convert_tensors_from_external",
|
|
15
|
+
"CallbackInfo",
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
import dataclasses
|
|
19
|
+
import logging
|
|
20
|
+
import os
|
|
21
|
+
from collections.abc import Iterator, Sequence
|
|
22
|
+
|
|
23
|
+
from onnx_ir import _core, _enums, _protocols
|
|
24
|
+
from onnx_ir import traversal as _traversal
|
|
25
|
+
from onnx_ir._polyfill import zip
|
|
26
|
+
|
|
27
|
+
# Note: If needed in future, add these as parameters to the function calls
|
|
28
|
+
# align_offset: Offset will always be page aligned and alloction granularity aligned for mmap support. This is done by padding previous tensor data with zeros keeping same length. Tensor data will be aligned if > align_threshold
|
|
29
|
+
_ALIGN_OFFSET = True
|
|
30
|
+
# align_threshold: Alignment threshold for size of data. Having a low threshold will waste file space for small initializers. Only when tensor's data is > the page_align_threshold it will be force aligned.
|
|
31
|
+
_ALIGN_THRESHOLD = 1048576 # 1MB
|
|
32
|
+
# allocation_granularity: The allocation Granularity for mmap() support. Typically 64KB for Windows & 4KB for other OSes.
|
|
33
|
+
_ALLOCATION_GRANULARITY = 65536 # 64KB
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
logger = logging.getLogger(__name__)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclasses.dataclass
|
|
40
|
+
class _ExternalDataInfo:
|
|
41
|
+
"""A class that stores information about a tensor that is to be stored as external data.
|
|
42
|
+
|
|
43
|
+
Attributes:
|
|
44
|
+
name: The name of the tensor that is to be stored as external data.
|
|
45
|
+
offset: The offset is used to determine where exactly in the file the external data is written to.
|
|
46
|
+
length: Stores the size of the tensor.
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
name: str | None
|
|
50
|
+
offset: int
|
|
51
|
+
length: int
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@dataclasses.dataclass
|
|
55
|
+
class CallbackInfo:
|
|
56
|
+
"""A class that shares information about a tensor that is to be saved as external data for callback functions.
|
|
57
|
+
|
|
58
|
+
Attributes:
|
|
59
|
+
total: The total number of tensors to save.
|
|
60
|
+
index: The index of the tensor being saved.
|
|
61
|
+
offset: The offset of the tensor in the external data file.
|
|
62
|
+
filename: The filename of the external data file.
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
total: int
|
|
66
|
+
index: int
|
|
67
|
+
offset: int
|
|
68
|
+
filename: str
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _all_tensors(
|
|
72
|
+
graph: _core.Graph, include_attributes: bool = False
|
|
73
|
+
) -> Iterator[_protocols.TensorProtocol]:
|
|
74
|
+
"""Iterate over all tensors in the graph.
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
graph: The graph to traverse tensors on.
|
|
78
|
+
include_attributes: Whether to include tensors in attributes.
|
|
79
|
+
|
|
80
|
+
Yields:
|
|
81
|
+
Tensors in the graph.
|
|
82
|
+
"""
|
|
83
|
+
# Yield all tensors in initializers
|
|
84
|
+
for value in graph.initializers.values():
|
|
85
|
+
if (tensor := value.const_value) is not None:
|
|
86
|
+
yield tensor
|
|
87
|
+
if not include_attributes:
|
|
88
|
+
return
|
|
89
|
+
# Look at constant attributes in nodes
|
|
90
|
+
for node in _traversal.RecursiveGraphIterator(graph):
|
|
91
|
+
for attr in node.attributes.values():
|
|
92
|
+
if attr.is_ref():
|
|
93
|
+
continue
|
|
94
|
+
if attr.type == _enums.AttributeType.TENSOR and attr.value is not None:
|
|
95
|
+
yield attr.value
|
|
96
|
+
elif attr.type == _enums.AttributeType.TENSORS and attr.value is not None:
|
|
97
|
+
yield from attr.value
|
|
98
|
+
elif attr.type == _enums.AttributeType.GRAPH and attr.value is not None:
|
|
99
|
+
for value in attr.value.initializers.values():
|
|
100
|
+
if (tensor := value.const_value) is not None:
|
|
101
|
+
yield tensor
|
|
102
|
+
elif attr.type == _enums.AttributeType.GRAPHS and attr.value is not None:
|
|
103
|
+
for g in attr.value:
|
|
104
|
+
for value in g.initializers.values():
|
|
105
|
+
if (tensor := value.const_value) is not None:
|
|
106
|
+
yield tensor
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def set_base_dir(graph: _core.Graph, base_dir: str | os.PathLike) -> None:
|
|
110
|
+
"""Set the base directory for external data in a graph (including all of its subgraphs).
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
graph: The graph to traverse tensors on.
|
|
114
|
+
base_dir: The base directory. This is the directory where the ONNX file is.
|
|
115
|
+
"""
|
|
116
|
+
for tensor in _all_tensors(graph, include_attributes=True):
|
|
117
|
+
if isinstance(tensor, _core.ExternalTensor):
|
|
118
|
+
tensor.base_dir = base_dir
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _external_tensor_to_memory_tensor(
|
|
122
|
+
tensor: _protocols.TensorProtocol,
|
|
123
|
+
) -> _protocols.TensorProtocol:
|
|
124
|
+
"""Convert an external tensor to an in memory tensor.
|
|
125
|
+
|
|
126
|
+
Args:
|
|
127
|
+
tensor: An external tensor to load.
|
|
128
|
+
base_dir: Path of base directory.
|
|
129
|
+
relative_path: Path to which external data is to be stored, relative to the ONNX file.
|
|
130
|
+
|
|
131
|
+
Returns:
|
|
132
|
+
An ir.Tensor object with the data loaded into memory.
|
|
133
|
+
"""
|
|
134
|
+
if not isinstance(tensor, _core.ExternalTensor):
|
|
135
|
+
raise TypeError(f"Expected ExternalTensor, got {type(tensor)}")
|
|
136
|
+
# Copy the data as the .numpy() call references data from a file whose data is eventually modified
|
|
137
|
+
tensor_data = tensor.numpy().copy()
|
|
138
|
+
tensor.release()
|
|
139
|
+
return _core.Tensor(tensor_data, name=tensor.name, dtype=tensor.dtype)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _compute_new_offset(
|
|
143
|
+
current_offset: int,
|
|
144
|
+
tensor_size: int,
|
|
145
|
+
align_offset: bool = _ALIGN_OFFSET,
|
|
146
|
+
align_threshold: int = _ALIGN_THRESHOLD,
|
|
147
|
+
allocation_granularity: int = _ALLOCATION_GRANULARITY,
|
|
148
|
+
) -> int:
|
|
149
|
+
"""Compute the offset to align the tensor data based on the current offset.
|
|
150
|
+
|
|
151
|
+
Args:
|
|
152
|
+
current_offset: Current location in the file at which tensor data will be written to.
|
|
153
|
+
tensor_size: Size of the tensor data to be written to file.
|
|
154
|
+
align_offset: Offset will always be page aligned and alloction granularity aligned for mmap support. This is done by padding previous tensor data with zeros keeping same length. Tensor data will be aligned if > align_threshold
|
|
155
|
+
align_threshold: Alignment threshold for size of data. Having a low threshold will waste file space for small initializers. Only when tensor's data is > the page_align_threshold it will be force aligned.
|
|
156
|
+
allocation_granularity: The allocation Granularity for mmap() support. Typically 64KB for Windows & 4KB for other OSes.
|
|
157
|
+
|
|
158
|
+
Returns:
|
|
159
|
+
The updated offset value.
|
|
160
|
+
"""
|
|
161
|
+
if align_offset and tensor_size > align_threshold:
|
|
162
|
+
alignment_factor = max(4096, allocation_granularity)
|
|
163
|
+
# Align to the next page or alloc granularity
|
|
164
|
+
return (current_offset + alignment_factor - 1) // alignment_factor * alignment_factor
|
|
165
|
+
return current_offset
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _compute_external_data_info(
|
|
169
|
+
tensor: _protocols.TensorProtocol,
|
|
170
|
+
current_offset: int,
|
|
171
|
+
) -> _ExternalDataInfo:
|
|
172
|
+
"""Capture information about a tensor that is to be stored as external data."""
|
|
173
|
+
tensor_size = tensor.nbytes
|
|
174
|
+
# Calculate updated offset and align tensors
|
|
175
|
+
current_offset = _compute_new_offset(current_offset, tensor_size)
|
|
176
|
+
# Store offset and tensor size as ExternalDataInfo
|
|
177
|
+
external_data_info = _ExternalDataInfo(
|
|
178
|
+
tensor.name,
|
|
179
|
+
current_offset,
|
|
180
|
+
tensor_size,
|
|
181
|
+
)
|
|
182
|
+
return external_data_info
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def _write_external_data(
|
|
186
|
+
tensors: Sequence[_protocols.TensorProtocol],
|
|
187
|
+
external_data_infos: Sequence[_ExternalDataInfo],
|
|
188
|
+
file_path: str | os.PathLike,
|
|
189
|
+
callback: Callable[[_protocols.TensorProtocol, CallbackInfo], None] | None = None,
|
|
190
|
+
) -> None:
|
|
191
|
+
"""Write tensor data to an external file according to information stored in ExternalDataInfo objects.
|
|
192
|
+
|
|
193
|
+
Args:
|
|
194
|
+
tensors: Tensors to be written as external data.
|
|
195
|
+
external_data_infos: External data information stored for each tensor to be written as external data.
|
|
196
|
+
file_path: Location to which external data is to be stored.
|
|
197
|
+
callback: A callback function that is called for each tensor that is saved to external data
|
|
198
|
+
for debugging or logging purposes.
|
|
199
|
+
"""
|
|
200
|
+
tensors_count = len(tensors)
|
|
201
|
+
assert tensors_count == len(external_data_infos), (
|
|
202
|
+
"Number of tensors and external data infos should match"
|
|
203
|
+
)
|
|
204
|
+
with open(file_path, "wb") as data_file:
|
|
205
|
+
for i, (tensor, tensor_info) in enumerate(
|
|
206
|
+
zip(tensors, external_data_infos, strict=True)
|
|
207
|
+
):
|
|
208
|
+
if callback is not None:
|
|
209
|
+
callback(
|
|
210
|
+
tensor,
|
|
211
|
+
CallbackInfo(
|
|
212
|
+
total=tensors_count,
|
|
213
|
+
index=i,
|
|
214
|
+
offset=tensor_info.offset,
|
|
215
|
+
filename=os.path.basename(file_path),
|
|
216
|
+
),
|
|
217
|
+
)
|
|
218
|
+
current_offset = tensor_info.offset
|
|
219
|
+
assert tensor is not None
|
|
220
|
+
# Pad file to required offset if needed
|
|
221
|
+
file_size = data_file.tell()
|
|
222
|
+
if current_offset > file_size:
|
|
223
|
+
data_file.write(b"\0" * (current_offset - file_size))
|
|
224
|
+
|
|
225
|
+
if hasattr(tensor, "tofile"):
|
|
226
|
+
# Some existing implementation of TensorProtocol
|
|
227
|
+
# may not have tofile() as it was introduced in v0.1.11
|
|
228
|
+
tensor.tofile(data_file)
|
|
229
|
+
else:
|
|
230
|
+
raw_data = tensor.tobytes()
|
|
231
|
+
if isinstance(tensor, _core.ExternalTensor):
|
|
232
|
+
tensor.release()
|
|
233
|
+
data_file.write(raw_data)
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def _create_external_tensor(
|
|
237
|
+
tensor: _protocols.TensorProtocol,
|
|
238
|
+
external_data_info: _ExternalDataInfo,
|
|
239
|
+
base_dir: str | os.PathLike,
|
|
240
|
+
relative_path: str | os.PathLike,
|
|
241
|
+
) -> _core.ExternalTensor:
|
|
242
|
+
"""Create external tensors from external data information.
|
|
243
|
+
|
|
244
|
+
Args:
|
|
245
|
+
tensor: Tensor to be converted to external tensor.
|
|
246
|
+
external_data_info: External data information stored for the tensor to be written as external data.
|
|
247
|
+
base_dir: Path of base directory.
|
|
248
|
+
relative_path: Path to which external data is to be stored, relative to the ONNX file.
|
|
249
|
+
|
|
250
|
+
Returns:
|
|
251
|
+
External tensor created from the information.
|
|
252
|
+
"""
|
|
253
|
+
return _core.ExternalTensor(
|
|
254
|
+
os.path.normpath(relative_path),
|
|
255
|
+
external_data_info.offset,
|
|
256
|
+
external_data_info.length,
|
|
257
|
+
tensor.dtype, # type: ignore[arg-type]
|
|
258
|
+
shape=tensor.shape, # type: ignore[arg-type]
|
|
259
|
+
name=tensor.name, # type: ignore[arg-type]
|
|
260
|
+
base_dir=os.path.normpath(base_dir),
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def convert_tensors_from_external(
|
|
265
|
+
tensors: Sequence[_protocols.TensorProtocol],
|
|
266
|
+
) -> list[_protocols.TensorProtocol]:
|
|
267
|
+
"""Convert a sequence of external tensors to in-memory tensors.
|
|
268
|
+
|
|
269
|
+
Args:
|
|
270
|
+
tensors: External tensors to be converted to in-memory tensors.
|
|
271
|
+
|
|
272
|
+
Returns:
|
|
273
|
+
A list of in-memory tensors derived from a list of external tensors.
|
|
274
|
+
"""
|
|
275
|
+
return [_external_tensor_to_memory_tensor(tensor) for tensor in tensors]
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def convert_tensors_to_external(
|
|
279
|
+
tensors: Sequence[_protocols.TensorProtocol],
|
|
280
|
+
base_dir: str | os.PathLike,
|
|
281
|
+
relative_path: str | os.PathLike,
|
|
282
|
+
callback: Callable[[_protocols.TensorProtocol, CallbackInfo], None] | None = None,
|
|
283
|
+
) -> list[_core.ExternalTensor]:
|
|
284
|
+
"""Convert a sequence of any TensorProtocol tensors to external tensors.
|
|
285
|
+
|
|
286
|
+
Existing external tensors are loaded to memory if they are referring to the
|
|
287
|
+
same file path as the destination path.
|
|
288
|
+
|
|
289
|
+
Args:
|
|
290
|
+
tensors: Tensors to be converted to external tensors. They can be external tensors themselves.
|
|
291
|
+
base_dir: Path of base directory.
|
|
292
|
+
relative_path: Path to which external data is to be stored, relative to the ONNX file.
|
|
293
|
+
callback: A callback function that is called for each tensor that is saved to external data
|
|
294
|
+
for debugging or logging purposes.
|
|
295
|
+
|
|
296
|
+
Returns:
|
|
297
|
+
A list of external tensors derived from a list of input tensors. The order
|
|
298
|
+
should match the input tensor order.
|
|
299
|
+
"""
|
|
300
|
+
path = os.path.join(base_dir, relative_path)
|
|
301
|
+
|
|
302
|
+
# Check if output path exists. Load pre-existing external data if it does.
|
|
303
|
+
if os.path.exists(path):
|
|
304
|
+
# Check if any tensor provided is using the destination file
|
|
305
|
+
new_tensors = []
|
|
306
|
+
for tensor in tensors:
|
|
307
|
+
if (
|
|
308
|
+
isinstance(tensor, _core.ExternalTensor)
|
|
309
|
+
and os.path.exists(tensor.path)
|
|
310
|
+
and os.path.samefile(path, tensor.path)
|
|
311
|
+
):
|
|
312
|
+
# FIXME(shubhambhokare1): If there is a non-initializer tensor that
|
|
313
|
+
# is referring to this file, that tensor is now invalid.
|
|
314
|
+
# This is a special case we are ok not handling right now.
|
|
315
|
+
new_tensors.append(_external_tensor_to_memory_tensor(tensor))
|
|
316
|
+
# Mark the original external tensor as invalid because it is now pointing
|
|
317
|
+
# to a file that is going to be overwritten.
|
|
318
|
+
tensor.invalidate()
|
|
319
|
+
logger.warning(
|
|
320
|
+
"External tensor %s is referring to the same file as the destination path. "
|
|
321
|
+
"It has been invalidated because the data file is changed. To avoid this, "
|
|
322
|
+
"save the external data to a different path or load the newly saved model back "
|
|
323
|
+
"with ir.load().",
|
|
324
|
+
tensor,
|
|
325
|
+
)
|
|
326
|
+
else:
|
|
327
|
+
new_tensors.append(tensor)
|
|
328
|
+
tensors = new_tensors
|
|
329
|
+
|
|
330
|
+
external_data_infos: list[_ExternalDataInfo] = []
|
|
331
|
+
# Sort all tensors based on tensor sizes, in order to avoid unnecessary alignment.
|
|
332
|
+
# All the smaller tensors are written earlier and alignment is performed for the larger tensors.
|
|
333
|
+
sorted_indices = sorted(range(len(tensors)), key=lambda i: tensors[i].nbytes)
|
|
334
|
+
sorted_tensors = [tensors[i] for i in sorted_indices]
|
|
335
|
+
|
|
336
|
+
# Compute external data information for each tensor and write to disk
|
|
337
|
+
current_offset = 0
|
|
338
|
+
for tensor in sorted_tensors:
|
|
339
|
+
external_info = _compute_external_data_info(tensor, current_offset)
|
|
340
|
+
external_data_infos.append(external_info)
|
|
341
|
+
current_offset = external_info.offset + external_info.length
|
|
342
|
+
_write_external_data(sorted_tensors, external_data_infos, path, callback=callback)
|
|
343
|
+
|
|
344
|
+
# Create external tensor objects
|
|
345
|
+
external_tensors: list[_core.ExternalTensor] = [
|
|
346
|
+
_create_external_tensor(tensor, external_info, base_dir, relative_path)
|
|
347
|
+
for tensor, external_info in zip(sorted_tensors, external_data_infos, strict=True)
|
|
348
|
+
]
|
|
349
|
+
|
|
350
|
+
# Sort external_tensors based on original key order. So that it can match the input tensor order
|
|
351
|
+
external_tensors = [
|
|
352
|
+
external_tensors[i]
|
|
353
|
+
for i in sorted(range(len(external_tensors)), key=lambda i: sorted_indices[i])
|
|
354
|
+
]
|
|
355
|
+
|
|
356
|
+
return external_tensors
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
def load_to_model(model: _core.Model) -> _core.Model:
|
|
360
|
+
"""Convert all external model initializers to memory tensors in-place.
|
|
361
|
+
|
|
362
|
+
All initializers in the main graph and subgraphs are handled.
|
|
363
|
+
|
|
364
|
+
Args:
|
|
365
|
+
model: Model to process.
|
|
366
|
+
"""
|
|
367
|
+
# TODO(justinchuby): Load tensor attributes in subgraphs
|
|
368
|
+
values_to_convert = []
|
|
369
|
+
for graph in model.graphs():
|
|
370
|
+
for value in graph.initializers.values():
|
|
371
|
+
if value.const_value is None:
|
|
372
|
+
# Filter out the uninitialized initializer values
|
|
373
|
+
continue
|
|
374
|
+
if isinstance(value.const_value, _core.ExternalTensor):
|
|
375
|
+
values_to_convert.append(value)
|
|
376
|
+
loaded_tensors = convert_tensors_from_external(
|
|
377
|
+
[v.const_value for v in values_to_convert] # type: ignore[misc]
|
|
378
|
+
)
|
|
379
|
+
for value, tensor in zip(values_to_convert, loaded_tensors, strict=True):
|
|
380
|
+
value.const_value = tensor
|
|
381
|
+
|
|
382
|
+
# Return the model because we may change the implementation to an out of place one
|
|
383
|
+
# to keep the input unchanged
|
|
384
|
+
return model
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
def unload_from_model(
|
|
388
|
+
model: _core.Model,
|
|
389
|
+
base_dir: str | os.PathLike,
|
|
390
|
+
relative_path: str | os.PathLike,
|
|
391
|
+
*,
|
|
392
|
+
size_threshold_bytes: int = 0,
|
|
393
|
+
callback: Callable[[_protocols.TensorProtocol, CallbackInfo], None] | None = None,
|
|
394
|
+
) -> _core.Model:
|
|
395
|
+
"""Convert all initializers equal or above size_threshold_bytes to external tensors in-place and save data to a single data file.
|
|
396
|
+
|
|
397
|
+
It should only replace the initializers in the model with external tensors
|
|
398
|
+
and not make any other modifications to the model.
|
|
399
|
+
|
|
400
|
+
If any existing external tensor
|
|
401
|
+
references the provided ``external_data`` path, it will be invalidated
|
|
402
|
+
after the external data is overwritten. To obtain a valid model, use :func:`load`
|
|
403
|
+
to load the newly saved model, or provide a different external data path that
|
|
404
|
+
is not currently referenced by any tensors in the model.
|
|
405
|
+
|
|
406
|
+
All initializers in the main graph and subgraphs are handled.
|
|
407
|
+
|
|
408
|
+
Args:
|
|
409
|
+
model: Model to process.
|
|
410
|
+
base_dir: Path the directory where the ONNX model file is.
|
|
411
|
+
relative_path: Path to which external data is to be stored, relative to the ONNX file.
|
|
412
|
+
E.g. "model.data"
|
|
413
|
+
size_threshold_bytes: Save to external data if the tensor size in bytes is larger than this threshold.
|
|
414
|
+
callback: A callback function that is called for each tensor that is saved to external data
|
|
415
|
+
for debugging or logging purposes.
|
|
416
|
+
|
|
417
|
+
Returns:
|
|
418
|
+
An ir.Model with all initializer data equal or above ``size_threshold_bytes``
|
|
419
|
+
converted to external tensors.
|
|
420
|
+
"""
|
|
421
|
+
# In-memory or external tensors, if equal to or above the threshold, should be converted to or re-saved as external tensors
|
|
422
|
+
initializers_to_become_external = []
|
|
423
|
+
# Existing external tensors, if below the threshold, should be loaded to memory
|
|
424
|
+
initializers_to_load_to_memory = []
|
|
425
|
+
for graph in model.graphs():
|
|
426
|
+
for value in graph.initializers.values():
|
|
427
|
+
if value.const_value is None:
|
|
428
|
+
# Filter out the uninitialized initializer values
|
|
429
|
+
continue
|
|
430
|
+
if value.const_value.nbytes > size_threshold_bytes:
|
|
431
|
+
initializers_to_become_external.append(value)
|
|
432
|
+
elif isinstance(value.const_value, _core.ExternalTensor):
|
|
433
|
+
initializers_to_load_to_memory.append(value)
|
|
434
|
+
|
|
435
|
+
# Load to memory first, then convert to external tensors, because
|
|
436
|
+
# the existing external tensors may be overwritten by the new external data
|
|
437
|
+
memory_tensors = convert_tensors_from_external(
|
|
438
|
+
[v.const_value for v in initializers_to_load_to_memory] # type: ignore[misc]
|
|
439
|
+
)
|
|
440
|
+
external_tensors = convert_tensors_to_external(
|
|
441
|
+
[v.const_value for v in initializers_to_become_external], # type: ignore[misc]
|
|
442
|
+
base_dir=base_dir,
|
|
443
|
+
relative_path=relative_path,
|
|
444
|
+
callback=callback,
|
|
445
|
+
)
|
|
446
|
+
|
|
447
|
+
# Replace the initializer values with external tensors and save the model
|
|
448
|
+
for value, external_tensor in zip(
|
|
449
|
+
initializers_to_become_external, external_tensors, strict=True
|
|
450
|
+
):
|
|
451
|
+
value.const_value = external_tensor
|
|
452
|
+
for value, memory_tensor in zip(
|
|
453
|
+
initializers_to_load_to_memory, memory_tensors, strict=True
|
|
454
|
+
):
|
|
455
|
+
value.const_value = memory_tensor
|
|
456
|
+
|
|
457
|
+
# Return the model because we may change the implementation to an out of place one
|
|
458
|
+
# to keep the input unchanged
|
|
459
|
+
return model
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# Copyright (c) ONNX Project Contributors
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
__all__ = [
|
|
5
|
+
"PassBase",
|
|
6
|
+
"PassResult",
|
|
7
|
+
"PassManager",
|
|
8
|
+
"Sequential",
|
|
9
|
+
"InPlacePass",
|
|
10
|
+
"FunctionalPass",
|
|
11
|
+
"functionalize",
|
|
12
|
+
# Errors
|
|
13
|
+
"InvariantError",
|
|
14
|
+
"PreconditionError",
|
|
15
|
+
"PostconditionError",
|
|
16
|
+
"PassError",
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
from onnx_ir.passes._pass_infra import (
|
|
20
|
+
FunctionalPass,
|
|
21
|
+
InPlacePass,
|
|
22
|
+
InvariantError,
|
|
23
|
+
PassBase,
|
|
24
|
+
PassError,
|
|
25
|
+
PassManager,
|
|
26
|
+
PassResult,
|
|
27
|
+
PostconditionError,
|
|
28
|
+
PreconditionError,
|
|
29
|
+
Sequential,
|
|
30
|
+
functionalize,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def __set_module() -> None:
|
|
35
|
+
"""Set the module of all functions in this module to this public module."""
|
|
36
|
+
global_dict = globals()
|
|
37
|
+
for name in __all__:
|
|
38
|
+
global_dict[name].__module__ = __name__
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
__set_module()
|