onnx-ir 0.0.1__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.
Potentially problematic release.
This version of onnx-ir might be problematic. Click here for more details.
- onnx_ir/__init__.py +154 -0
- onnx_ir/_convenience.py +439 -0
- onnx_ir/_core.py +2875 -0
- onnx_ir/_display.py +49 -0
- onnx_ir/_enums.py +154 -0
- onnx_ir/_external_data.py +323 -0
- onnx_ir/_graph_comparison.py +23 -0
- onnx_ir/_internal/version_utils.py +118 -0
- onnx_ir/_io.py +50 -0
- onnx_ir/_linked_list.py +276 -0
- onnx_ir/_metadata.py +44 -0
- onnx_ir/_name_authority.py +72 -0
- onnx_ir/_protocols.py +598 -0
- onnx_ir/_tape.py +104 -0
- onnx_ir/_thirdparty/asciichartpy.py +313 -0
- onnx_ir/_type_casting.py +91 -0
- onnx_ir/convenience.py +32 -0
- onnx_ir/passes/__init__.py +33 -0
- onnx_ir/passes/_pass_infra.py +172 -0
- onnx_ir/serde.py +1551 -0
- onnx_ir/traversal.py +82 -0
- onnx_ir-0.0.1.dist-info/LICENSE +22 -0
- onnx_ir-0.0.1.dist-info/METADATA +73 -0
- onnx_ir-0.0.1.dist-info/RECORD +26 -0
- onnx_ir-0.0.1.dist-info/WHEEL +5 -0
- onnx_ir-0.0.1.dist-info/top_level.txt +1 -0
onnx_ir/_io.py
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# Copyright (c) Microsoft Corporation.
|
|
2
|
+
# Licensed under the MIT License.
|
|
3
|
+
"""Load and save ONNX models."""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
__all__ = ["load", "save"]
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
|
|
11
|
+
import onnx
|
|
12
|
+
|
|
13
|
+
from onnx_ir import _core, _external_data, serde
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def load(path: str | os.PathLike, format: str | None = None) -> _core.Model:
|
|
17
|
+
"""Load an ONNX model from a file.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
path: The path to the ONNX file.
|
|
21
|
+
format: The format of the file (e.g. protobuf, textproto, json, etc.).
|
|
22
|
+
If None, the format is inferred from the file extension.
|
|
23
|
+
|
|
24
|
+
Returns:
|
|
25
|
+
The loaded model.
|
|
26
|
+
"""
|
|
27
|
+
# Do not use ONNX to load external data because the IR handles external data
|
|
28
|
+
# by doing memory mapping directly.
|
|
29
|
+
proto = onnx.load(path, format=format, load_external_data=False)
|
|
30
|
+
model = serde.deserialize_model(proto)
|
|
31
|
+
base_dir = os.path.dirname(path)
|
|
32
|
+
# Set the base directory for external data to the directory of the ONNX file
|
|
33
|
+
# so that relative paths are resolved correctly.
|
|
34
|
+
_external_data.set_base_dir(model.graph, base_dir)
|
|
35
|
+
return model
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def save(model: _core.Model, path: str | os.PathLike, format: str | None = None) -> None:
|
|
39
|
+
"""Save an ONNX model to a file.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
model: The model to save.
|
|
43
|
+
path: The path to save the model to.
|
|
44
|
+
format: The format of the file (e.g. protobuf, textproto, json, etc.).
|
|
45
|
+
If None, the format is inferred from the file extension.
|
|
46
|
+
"""
|
|
47
|
+
proto = serde.serialize_model(model)
|
|
48
|
+
onnx.save(proto, path, format=format)
|
|
49
|
+
# TODO(justinchuby): Handle external data when the relative path has changed
|
|
50
|
+
# TODO(justinchuby): Handle off loading external data to disk when saving
|
onnx_ir/_linked_list.py
ADDED
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
# Copyright (c) Microsoft Corporation.
|
|
2
|
+
# Licensed under the MIT License.
|
|
3
|
+
"""Mutable list for nodes in a graph with safe mutation properties."""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from typing import Generic, Iterable, Iterator, Sequence, TypeVar
|
|
8
|
+
|
|
9
|
+
T = TypeVar("T")
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class _LinkBox(Generic[T]):
|
|
13
|
+
"""A link in a doubly linked list that has a reference to the actual object in the link.
|
|
14
|
+
|
|
15
|
+
The :class:`_LinkBox` is a container for the actual object in the list. It is used to
|
|
16
|
+
maintain the links between the elements in the linked list. The actual object is stored in the
|
|
17
|
+
:attr:`value` attribute.
|
|
18
|
+
|
|
19
|
+
By using a separate container for the actual object, we can safely remove the object from the
|
|
20
|
+
list without losing the links. This allows us to remove the object from the list during
|
|
21
|
+
iteration and place the object into a different list without breaking any chains.
|
|
22
|
+
|
|
23
|
+
This is an internal class and should only be initialized by the :class:`DoublyLinkedSet`.
|
|
24
|
+
|
|
25
|
+
Attributes:
|
|
26
|
+
prev: The previous box in the list.
|
|
27
|
+
next: The next box in the list.
|
|
28
|
+
erased: A flag to indicate if the box has been removed from the list.
|
|
29
|
+
owning_list: The :class:`DoublyLinkedSet` to which the box belongs.
|
|
30
|
+
value: The actual object in the list.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
__slots__ = ("next", "owning_list", "prev", "value")
|
|
34
|
+
|
|
35
|
+
def __init__(self, owner: DoublyLinkedSet[T], value: T | None) -> None:
|
|
36
|
+
"""Create a new link box.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
owner: The linked list to which this box belongs.
|
|
40
|
+
value: The value to be stored in the link box. When the value is None,
|
|
41
|
+
the link box is considered erased (default). The root box of the list
|
|
42
|
+
should be created with a None value.
|
|
43
|
+
"""
|
|
44
|
+
self.prev: _LinkBox[T] = self
|
|
45
|
+
self.next: _LinkBox[T] = self
|
|
46
|
+
self.value: T | None = value
|
|
47
|
+
self.owning_list: DoublyLinkedSet[T] = owner
|
|
48
|
+
|
|
49
|
+
@property
|
|
50
|
+
def erased(self) -> bool:
|
|
51
|
+
return self.value is None
|
|
52
|
+
|
|
53
|
+
def erase(self) -> None:
|
|
54
|
+
"""Remove the link from the list and detach the value from the box."""
|
|
55
|
+
if self.value is None:
|
|
56
|
+
raise ValueError("_LinkBox is already erased")
|
|
57
|
+
# Update the links
|
|
58
|
+
prev, next_ = self.prev, self.next
|
|
59
|
+
prev.next, next_.prev = next_, prev
|
|
60
|
+
# Detach the value
|
|
61
|
+
self.value = None
|
|
62
|
+
|
|
63
|
+
def __repr__(self) -> str:
|
|
64
|
+
return f"_LinkBox({self.value!r}, erased={self.erased}, prev={self.prev.value!r}, next={self.next.value!r})"
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class DoublyLinkedSet(Sequence[T], Generic[T]):
|
|
68
|
+
"""A doubly linked ordered set of nodes.
|
|
69
|
+
|
|
70
|
+
The container can be viewed as a set as it does not allow duplicate values. The order of the
|
|
71
|
+
elements is maintained. One can typically treat it as a doubly linked list with list-like
|
|
72
|
+
methods implemented.
|
|
73
|
+
|
|
74
|
+
Adding and removing elements from the set during iteration is safe. Moving elements
|
|
75
|
+
from one set to another is also safe.
|
|
76
|
+
|
|
77
|
+
During the iteration:
|
|
78
|
+
- If new elements are inserted after the current node, the iterator will
|
|
79
|
+
iterate over them as well.
|
|
80
|
+
- If new elements are inserted before the current node, they will
|
|
81
|
+
not be iterated over in this iteration.
|
|
82
|
+
- If the current node is lifted and inserted in a different location,
|
|
83
|
+
iteration will start from the "next" node at the _original_ location.
|
|
84
|
+
|
|
85
|
+
Time complexity:
|
|
86
|
+
Inserting and removing nodes from the set is O(1). Accessing nodes by index is O(n),
|
|
87
|
+
although accessing nodes at either end of the set is O(1). I.e.
|
|
88
|
+
``linked_set[0]`` and ``linked_set[-1]`` are O(1).
|
|
89
|
+
|
|
90
|
+
Values need to be hashable. ``None`` is not a valid value in the set.
|
|
91
|
+
"""
|
|
92
|
+
|
|
93
|
+
__slots__ = ("_length", "_root", "_value_ids_to_boxes")
|
|
94
|
+
|
|
95
|
+
def __init__(self, values: Iterable[T] | None = None) -> None:
|
|
96
|
+
# Using the root node simplifies the mutation implementation a lot
|
|
97
|
+
# The list is circular. The root node is the only node that is not a part of the list values
|
|
98
|
+
root_ = _LinkBox(self, None)
|
|
99
|
+
self._root: _LinkBox = root_
|
|
100
|
+
self._length = 0
|
|
101
|
+
self._value_ids_to_boxes: dict[int, _LinkBox] = {}
|
|
102
|
+
if values is not None:
|
|
103
|
+
self.extend(values)
|
|
104
|
+
|
|
105
|
+
def __iter__(self) -> Iterator[T]:
|
|
106
|
+
"""Iterate over the elements in the list.
|
|
107
|
+
|
|
108
|
+
- If new elements are inserted after the current node, the iterator will
|
|
109
|
+
iterate over them as well.
|
|
110
|
+
- If new elements are inserted before the current node, they will
|
|
111
|
+
not be iterated over in this iteration.
|
|
112
|
+
- If the current node is lifted and inserted in a different location,
|
|
113
|
+
iteration will start from the "next" node at the _original_ location.
|
|
114
|
+
"""
|
|
115
|
+
box = self._root.next
|
|
116
|
+
while box is not self._root:
|
|
117
|
+
if box.owning_list is not self:
|
|
118
|
+
raise RuntimeError(f"Element {box!r} is not in the list")
|
|
119
|
+
if not box.erased:
|
|
120
|
+
assert box.value is not None
|
|
121
|
+
yield box.value
|
|
122
|
+
box = box.next
|
|
123
|
+
|
|
124
|
+
def __reversed__(self) -> Iterator[T]:
|
|
125
|
+
"""Iterate over the elements in the list in reverse order."""
|
|
126
|
+
box = self._root.prev
|
|
127
|
+
while box is not self._root:
|
|
128
|
+
if not box.erased:
|
|
129
|
+
assert box.value is not None
|
|
130
|
+
yield box.value
|
|
131
|
+
box = box.prev
|
|
132
|
+
|
|
133
|
+
def __len__(self) -> int:
|
|
134
|
+
assert self._length == len(
|
|
135
|
+
self._value_ids_to_boxes
|
|
136
|
+
), "Bug in the implementation: length mismatch"
|
|
137
|
+
return self._length
|
|
138
|
+
|
|
139
|
+
def __getitem__(self, index: int) -> T:
|
|
140
|
+
"""Get the node at the given index.
|
|
141
|
+
|
|
142
|
+
Complexity is O(n).
|
|
143
|
+
"""
|
|
144
|
+
if index >= self._length or index < -self._length:
|
|
145
|
+
raise IndexError(
|
|
146
|
+
f"Index out of range: {index} not in range [-{self._length}, {self._length})"
|
|
147
|
+
)
|
|
148
|
+
if index < 0:
|
|
149
|
+
# Look up from the end of the list
|
|
150
|
+
iterator = reversed(self)
|
|
151
|
+
item = next(iterator)
|
|
152
|
+
for _ in range(-index - 1):
|
|
153
|
+
item = next(iterator)
|
|
154
|
+
else:
|
|
155
|
+
iterator = iter(self) # type: ignore[assignment]
|
|
156
|
+
item = next(iterator)
|
|
157
|
+
for _ in range(index):
|
|
158
|
+
item = next(iterator)
|
|
159
|
+
return item
|
|
160
|
+
|
|
161
|
+
def _insert_one_after(
|
|
162
|
+
self,
|
|
163
|
+
box: _LinkBox[T],
|
|
164
|
+
new_value: T,
|
|
165
|
+
) -> _LinkBox[T]:
|
|
166
|
+
"""Insert a new value after the given box.
|
|
167
|
+
|
|
168
|
+
All insertion methods should call this method to ensure that the list is updated correctly.
|
|
169
|
+
|
|
170
|
+
Example::
|
|
171
|
+
Before: A <-> B <-> C
|
|
172
|
+
^v0 ^v1 ^v2
|
|
173
|
+
Call: _insert_one_after(B, v3)
|
|
174
|
+
After: A <-> B <-> new_box <-> C
|
|
175
|
+
^v0 ^v1 ^v3 ^v2
|
|
176
|
+
|
|
177
|
+
Args:
|
|
178
|
+
box: The box which the new value is to be inserted.
|
|
179
|
+
new_value: The new value to be inserted.
|
|
180
|
+
"""
|
|
181
|
+
if new_value is None:
|
|
182
|
+
raise TypeError(f"{self.__class__.__name__} does not support None values")
|
|
183
|
+
if box.value is new_value:
|
|
184
|
+
# Do nothing if the new value is the same as the old value
|
|
185
|
+
return box
|
|
186
|
+
if box.owning_list is not self:
|
|
187
|
+
raise ValueError(f"Value {box.value!r} is not in the list")
|
|
188
|
+
|
|
189
|
+
if (new_value_id := id(new_value)) in self._value_ids_to_boxes:
|
|
190
|
+
# If the value is already in the list, remove it first
|
|
191
|
+
self.remove(new_value)
|
|
192
|
+
|
|
193
|
+
# Create a new _LinkBox for the new value
|
|
194
|
+
new_box = _LinkBox(self, new_value)
|
|
195
|
+
# original_box <=> original_next
|
|
196
|
+
# becomes
|
|
197
|
+
# original_box <=> new_box <=> original_next
|
|
198
|
+
original_next = box.next
|
|
199
|
+
box.next = new_box
|
|
200
|
+
new_box.prev = box
|
|
201
|
+
new_box.next = original_next
|
|
202
|
+
original_next.prev = new_box
|
|
203
|
+
|
|
204
|
+
# Be sure to update the length and mapping
|
|
205
|
+
self._length += 1
|
|
206
|
+
self._value_ids_to_boxes[new_value_id] = new_box
|
|
207
|
+
|
|
208
|
+
return new_box
|
|
209
|
+
|
|
210
|
+
def _insert_many_after(
|
|
211
|
+
self,
|
|
212
|
+
box: _LinkBox[T],
|
|
213
|
+
new_values: Iterable[T],
|
|
214
|
+
):
|
|
215
|
+
"""Insert multiple new values after the given box."""
|
|
216
|
+
insertion_point = box
|
|
217
|
+
for new_value in new_values:
|
|
218
|
+
insertion_point = self._insert_one_after(insertion_point, new_value)
|
|
219
|
+
|
|
220
|
+
def remove(self, value: T) -> None:
|
|
221
|
+
"""Remove a node from the list."""
|
|
222
|
+
if (value_id := id(value)) not in self._value_ids_to_boxes:
|
|
223
|
+
raise ValueError(f"Value {value!r} is not in the list")
|
|
224
|
+
box = self._value_ids_to_boxes[value_id]
|
|
225
|
+
# Remove the link box and detach the value from the box
|
|
226
|
+
box.erase()
|
|
227
|
+
|
|
228
|
+
# Be sure to update the length and mapping
|
|
229
|
+
self._length -= 1
|
|
230
|
+
del self._value_ids_to_boxes[value_id]
|
|
231
|
+
|
|
232
|
+
def append(self, value: T) -> None:
|
|
233
|
+
"""Append a node to the list."""
|
|
234
|
+
_ = self._insert_one_after(self._root.prev, value)
|
|
235
|
+
|
|
236
|
+
def extend(
|
|
237
|
+
self,
|
|
238
|
+
values: Iterable[T],
|
|
239
|
+
) -> None:
|
|
240
|
+
for value in values:
|
|
241
|
+
self.append(value)
|
|
242
|
+
|
|
243
|
+
def insert_after(
|
|
244
|
+
self,
|
|
245
|
+
value: T,
|
|
246
|
+
new_values: Iterable[T],
|
|
247
|
+
) -> None:
|
|
248
|
+
"""Insert new nodes after the given node.
|
|
249
|
+
|
|
250
|
+
Args:
|
|
251
|
+
value: The value after which the new values are to be inserted.
|
|
252
|
+
new_values: The new values to be inserted.
|
|
253
|
+
"""
|
|
254
|
+
if (value_id := id(value)) not in self._value_ids_to_boxes:
|
|
255
|
+
raise ValueError(f"Value {value!r} is not in the list")
|
|
256
|
+
insertion_point = self._value_ids_to_boxes[value_id]
|
|
257
|
+
return self._insert_many_after(insertion_point, new_values)
|
|
258
|
+
|
|
259
|
+
def insert_before(
|
|
260
|
+
self,
|
|
261
|
+
value: T,
|
|
262
|
+
new_values: Iterable[T],
|
|
263
|
+
) -> None:
|
|
264
|
+
"""Insert new nodes before the given node.
|
|
265
|
+
|
|
266
|
+
Args:
|
|
267
|
+
value: The value before which the new values are to be inserted.
|
|
268
|
+
new_values: The new values to be inserted.
|
|
269
|
+
"""
|
|
270
|
+
if (value_id := id(value)) not in self._value_ids_to_boxes:
|
|
271
|
+
raise ValueError(f"Value {value!r} is not in the list")
|
|
272
|
+
insertion_point = self._value_ids_to_boxes[value_id].prev
|
|
273
|
+
return self._insert_many_after(insertion_point, new_values)
|
|
274
|
+
|
|
275
|
+
def __repr__(self) -> str:
|
|
276
|
+
return f"DoublyLinkedSet({list(self)})"
|
onnx_ir/_metadata.py
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# Copyright (c) Microsoft Corporation.
|
|
2
|
+
# Licensed under the MIT License.
|
|
3
|
+
"""Class for storing metadata about the IR objects."""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import collections
|
|
8
|
+
from typing import Any, Mapping
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class MetadataStore(collections.UserDict):
|
|
12
|
+
"""Class for storing metadata about the IR objects.
|
|
13
|
+
|
|
14
|
+
Metadata is stored as key-value pairs. The keys are strings and the values
|
|
15
|
+
can be any Python object.
|
|
16
|
+
|
|
17
|
+
The metadata store also supports marking keys as invalid. This is useful
|
|
18
|
+
when a pass wants to mark a key that needs to be recomputed.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def __init__(self, data: Mapping[str, Any] | None = None, /) -> None:
|
|
22
|
+
super().__init__(data)
|
|
23
|
+
self._invalid_keys: set[str] = set()
|
|
24
|
+
|
|
25
|
+
def __setitem__(self, key: str, item: Any) -> None:
|
|
26
|
+
self.data[key] = item
|
|
27
|
+
self._invalid_keys.discard(key)
|
|
28
|
+
|
|
29
|
+
def invalidate(self, key: str) -> None:
|
|
30
|
+
self._invalid_keys.add(key)
|
|
31
|
+
|
|
32
|
+
def is_valid(self, key: str) -> bool:
|
|
33
|
+
"""Returns whether the value is valid.
|
|
34
|
+
|
|
35
|
+
Note that default values (None) are not necessarily invalid. For example,
|
|
36
|
+
a shape that is unknown (None) may be still valid if shape inference has
|
|
37
|
+
determined that the shape is unknown.
|
|
38
|
+
|
|
39
|
+
Whether a value is valid is solely determined by the user that sets the value.
|
|
40
|
+
"""
|
|
41
|
+
return key not in self._invalid_keys
|
|
42
|
+
|
|
43
|
+
def __repr__(self) -> str:
|
|
44
|
+
return f"{self.__class__.__name__}({self.data!r}, invalid_keys={self._invalid_keys!r})"
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# Copyright (c) Microsoft Corporation.
|
|
2
|
+
# Licensed under the MIT License.
|
|
3
|
+
"""Auxiliary class for managing names in the IR."""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from onnx_ir import _core
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class NameAuthority:
|
|
11
|
+
"""Class for giving names to values and nodes in the IR.
|
|
12
|
+
|
|
13
|
+
The names are generated in the format ``val_{value_counter}`` for values and
|
|
14
|
+
``node_{op_type}_{node_counter}`` for nodes. The counter is incremented each time
|
|
15
|
+
a new value or node is named.
|
|
16
|
+
|
|
17
|
+
This class keeps tracks of the names it has generated and existing names
|
|
18
|
+
in the graph to prevent producing duplicated names.
|
|
19
|
+
|
|
20
|
+
.. note::
|
|
21
|
+
Once a name is tracked, it will not be made available even if the node/value
|
|
22
|
+
is removed from the graph. It is possible to improve this behavior by keeping
|
|
23
|
+
track of the names that are no longer used, but it is not implemented yet.
|
|
24
|
+
|
|
25
|
+
However, if a value/node is already named when added to the graph,
|
|
26
|
+
the name authority will not change its name.
|
|
27
|
+
It is the responsibility of the user to ensure that the names are unique
|
|
28
|
+
(typically by running a name-fixing pass on the graph).
|
|
29
|
+
|
|
30
|
+
TODO(justichuby): Describe the pass when we have a reference implementation.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
def __init__(self):
|
|
34
|
+
self._value_counter = 0
|
|
35
|
+
self._node_counter = 0
|
|
36
|
+
self._value_names: set[str] = set()
|
|
37
|
+
self._node_names: set[str] = set()
|
|
38
|
+
|
|
39
|
+
def _unique_value_name(self) -> str:
|
|
40
|
+
"""Generate a unique name for a value."""
|
|
41
|
+
while True:
|
|
42
|
+
name = f"val_{self._value_counter}"
|
|
43
|
+
self._value_counter += 1
|
|
44
|
+
if name not in self._value_names:
|
|
45
|
+
return name
|
|
46
|
+
|
|
47
|
+
def _unique_node_name(self, op_type: str) -> str:
|
|
48
|
+
"""Generate a unique name for a node."""
|
|
49
|
+
while True:
|
|
50
|
+
name = f"node_{op_type}_{self._node_counter}"
|
|
51
|
+
self._node_counter += 1
|
|
52
|
+
if name not in self._node_names:
|
|
53
|
+
return name
|
|
54
|
+
|
|
55
|
+
def register_or_name_value(self, value: _core.Value) -> None:
|
|
56
|
+
# TODO(justinchuby): Record names of the initializers and graph inputs
|
|
57
|
+
if value.name is None:
|
|
58
|
+
value.name = self._unique_value_name()
|
|
59
|
+
# If the name is already specified, we do not change it because keeping
|
|
60
|
+
# track of the used names can be costly when nodes can be removed from the graph:
|
|
61
|
+
# How do we know if a name is no longer used? We cannot reserve unused names
|
|
62
|
+
# because users may want to use them.
|
|
63
|
+
self._value_names.add(value.name)
|
|
64
|
+
|
|
65
|
+
def register_or_name_node(self, node: _core.Node) -> None:
|
|
66
|
+
if node.name is None:
|
|
67
|
+
node.name = self._unique_node_name(node.op_type)
|
|
68
|
+
# If the name is already specified, we do not change it because keeping
|
|
69
|
+
# track of the used names can be costly when nodes can be removed from the graph:
|
|
70
|
+
# How do we know if a name is no longer used? We cannot reserve unused names
|
|
71
|
+
# because users may want to use them.
|
|
72
|
+
self._node_names.add(node.name)
|