Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/onnx/utils.py: 16%
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1# Copyright (c) ONNX Project Contributors
2#
3# SPDX-License-Identifier: Apache-2.0
4from __future__ import annotations
6import os
7import tarfile
8from collections import deque
9from typing import TYPE_CHECKING
11import onnx.checker
12import onnx.helper
13import onnx.shape_inference
15if TYPE_CHECKING:
16 from onnx.onnx_pb import (
17 FunctionProto,
18 ModelProto,
19 NodeProto,
20 TensorProto,
21 ValueInfoProto,
22 )
25class Extractor:
26 def __init__(self, model: ModelProto) -> None:
27 self.model = model
28 self.graph = self.model.graph
29 self.initializers: dict[str, TensorProto] = self._build_name2obj_dict(
30 self.graph.initializer
31 )
32 self.value_infos: dict[str, ValueInfoProto] = self._build_name2obj_dict(
33 self.graph.value_info
34 )
35 # Add input and output values (not included in the value_info for intermediate values)
36 self.value_infos.update(self._build_name2obj_dict(self.graph.input))
37 self.value_infos.update(self._build_name2obj_dict(self.graph.output))
38 self.outmap: dict[str, int] = self._build_output_dict(self.graph)
40 @staticmethod
41 def _build_name2obj_dict(objs) -> dict:
42 return {obj.name: obj for obj in objs}
44 @staticmethod
45 def _build_output_dict(graph) -> dict[str, int]:
46 output_to_index: dict[str, int] = {}
47 for index, node in enumerate(graph.node):
48 for output_name in node.output:
49 if output_name == "":
50 continue
51 assert output_name not in output_to_index # output_name is unique
52 output_to_index[output_name] = index
53 return output_to_index
55 def _collect_new_io(self, io_names_to_extract: list[str]) -> list[ValueInfoProto]:
56 # Validate that all names exist in self.value_infos
57 missing_names = [
58 name for name in io_names_to_extract if name not in self.value_infos
59 ]
60 if missing_names:
61 raise ValueError(
62 f"The following names were not found in value_infos: {', '.join(missing_names)}"
63 )
64 return [self.value_infos[name] for name in io_names_to_extract]
66 def _dfs_search_reachable_nodes(
67 self,
68 node_output_name: str,
69 graph_input_names: set[str],
70 reachable: set[int],
71 ) -> None:
72 """Helper function to find nodes which are connected to an output
74 Arguments:
75 node_output_name (str): The name of the output
76 graph_input_names (set of string): The names of all inputs of the graph
77 reachable (set of int): The set of indexes to reachable nodes in `nodes`
78 """
79 stack = [node_output_name]
80 while stack:
81 current_output_name = stack.pop()
82 # finish search at inputs
83 if current_output_name in graph_input_names:
84 continue
85 # find nodes connected to this output
86 if current_output_name in self.outmap:
87 index = self.outmap[current_output_name]
88 if index not in reachable:
89 # add nodes connected to this output to sets
90 reachable.add(index)
91 stack += [
92 input_name
93 for input_name in self.graph.node[index].input
94 if input_name != ""
95 ]
97 def _collect_reachable_nodes(
98 self,
99 input_names: list[str],
100 output_names: list[str],
101 ) -> list[NodeProto]:
102 _input_names = set(input_names)
103 reachable: set[int] = set()
104 for name in output_names:
105 self._dfs_search_reachable_nodes(name, _input_names, reachable)
106 # needs to be topologically sorted
107 return [self.graph.node[index] for index in sorted(reachable)]
109 def _collect_referred_local_functions(
110 self,
111 nodes: list[NodeProto],
112 ) -> list[FunctionProto]:
113 # a node in a model graph may refer a function.
114 # a function contains nodes, some of which may in turn refer a function.
115 # we need to find functions referred by graph nodes and
116 # by nodes used to define functions, including their subgraphs.
117 function_map: dict[tuple[str, str], FunctionProto] = {}
118 for function in self.model.functions:
119 function_map[(function.name, function.domain)] = function
120 referred_local_functions: list[FunctionProto] = []
121 queue = deque(nodes)
122 while queue and function_map:
123 node = queue.popleft()
124 # check if the node is a function op
125 if (node.op_type, node.domain) in function_map:
126 function = function_map.pop((node.op_type, node.domain))
127 referred_local_functions.append(function)
128 queue.extend(function.node)
129 for attribute in node.attribute:
130 if attribute.HasField("g"):
131 queue.extend(attribute.g.node)
132 for graph in attribute.graphs:
133 queue.extend(graph.node)
134 # needs to be topologically sorted
135 return referred_local_functions
137 def _collect_reachable_tensors(
138 self,
139 nodes: list[NodeProto],
140 ) -> tuple[list[TensorProto], list[ValueInfoProto]]:
141 all_tensors_names: set[str] = set()
142 for node in nodes:
143 all_tensors_names.update(node.input)
144 all_tensors_names.update(node.output)
145 initializer = [
146 self.initializers[t] for t in self.initializers if t in all_tensors_names
147 ]
148 value_info = [
149 self.value_infos[t] for t in self.value_infos if t in all_tensors_names
150 ]
151 len_sparse_initializer = len(self.graph.sparse_initializer)
152 if len_sparse_initializer != 0:
153 raise ValueError(
154 f"len_sparse_initializer is {len_sparse_initializer}, it must be 0."
155 )
156 len_quantization_annotation = len(self.graph.quantization_annotation)
157 if len_quantization_annotation != 0:
158 raise ValueError(
159 f"len_quantization_annotation is {len_quantization_annotation}, it must be 0."
160 )
161 return initializer, value_info
163 def _make_model(
164 self,
165 nodes: list[NodeProto],
166 inputs: list[ValueInfoProto],
167 outputs: list[ValueInfoProto],
168 initializer: list[TensorProto],
169 value_info: list[ValueInfoProto],
170 local_functions: list[FunctionProto],
171 ) -> ModelProto:
172 name = "Extracted from {" + self.graph.name + "}"
173 graph = onnx.helper.make_graph(
174 nodes, name, inputs, outputs, initializer=initializer, value_info=value_info
175 )
176 meta = {
177 "ir_version": self.model.ir_version,
178 "opset_imports": self.model.opset_import,
179 "producer_name": "onnx.utils.extract_model",
180 "functions": local_functions,
181 }
182 return onnx.helper.make_model(graph, **meta)
184 def extract_model(
185 self,
186 input_names: list[str],
187 output_names: list[str],
188 ) -> ModelProto:
189 inputs = self._collect_new_io(input_names)
190 outputs = self._collect_new_io(output_names)
191 nodes = self._collect_reachable_nodes(input_names, output_names)
192 initializer, value_info = self._collect_reachable_tensors(nodes)
193 local_functions = self._collect_referred_local_functions(nodes)
194 return self._make_model(
195 nodes, inputs, outputs, initializer, value_info, local_functions
196 )
199def extract_model(
200 input_path: str | os.PathLike,
201 output_path: str | os.PathLike,
202 input_names: list[str],
203 output_names: list[str],
204 check_model: bool = True,
205 infer_shapes: bool = True,
206) -> None:
207 """Extracts sub-model from an ONNX model.
209 The sub-model is defined by the names of the input and output tensors *exactly*.
211 Note: For control-flow operators, e.g. If and Loop, the *boundary of sub-model*,
212 which is defined by the input and output tensors, should not *cut through* the
213 subgraph that is connected to the *main graph* as attributes of these operators.
215 Note: When the extracted model size is larger than 2GB, the extra data will be saved in "output_path.data".
217 Arguments:
218 input_path (str | os.PathLike): The path to original ONNX model.
219 output_path (str | os.PathLike): The path to save the extracted ONNX model.
220 input_names (list of string): The names of the input tensors that to be extracted.
221 output_names (list of string): The names of the output tensors that to be extracted.
222 check_model (bool): Whether to run model checker on the original model and the extracted model.
223 infer_shapes (bool): Whether to infer the shapes of the original model.
224 """
225 if not os.path.exists(input_path):
226 raise ValueError(f"Invalid input model path: {input_path}")
227 if not output_path:
228 raise ValueError("Output model path shall not be empty!")
229 if not input_names:
230 raise ValueError("Input tensor names shall not be empty!")
231 if not output_names:
232 raise ValueError("Output tensor names shall not be empty!")
234 if len(input_names) != len(set(input_names)):
235 raise ValueError("Duplicate names found in the input tensor names.")
236 if len(output_names) != len(set(output_names)):
237 raise ValueError("Duplicate names found in the output tensor names.")
239 if check_model:
240 onnx.checker.check_model(input_path)
242 if infer_shapes and os.path.getsize(input_path) > onnx.checker.MAXIMUM_PROTOBUF:
243 onnx.shape_inference.infer_shapes_path(input_path, output_path)
244 model = onnx.load(output_path)
245 elif infer_shapes:
246 model = onnx.load(input_path, load_external_data=False)
247 model = onnx.shape_inference.infer_shapes(model)
248 base_dir = os.path.dirname(input_path)
249 onnx.load_external_data_for_model(model, base_dir)
250 else:
251 model = onnx.load(input_path)
253 e = Extractor(model)
254 extracted = e.extract_model(input_names, output_names)
256 if extracted.ByteSize() > onnx.checker.MAXIMUM_PROTOBUF:
257 location = os.path.basename(output_path) + ".data"
258 onnx.save(extracted, output_path, save_as_external_data=True, location=location)
259 else:
260 onnx.save(extracted, output_path)
262 if check_model:
263 onnx.checker.check_model(output_path)
266def _tar_members_filter(
267 tar: tarfile.TarFile, base: str | os.PathLike
268) -> list[tarfile.TarInfo]:
269 """Check that the content of ``tar`` will be extracted safely
271 Args:
272 tar: The tarball file
273 base: The directory where the tarball will be extracted
275 Returns:
276 list of tarball members
277 """
278 result = []
279 abs_base = os.path.abspath(base)
280 for member in tar:
281 member_path = os.path.join(base, member.name)
282 abs_member = os.path.abspath(member_path)
283 try:
284 is_within_base = os.path.commonpath([abs_base, abs_member]) == abs_base
285 except ValueError:
286 is_within_base = False
287 if not is_within_base:
288 raise RuntimeError(
289 f"The tarball member {member_path} in downloading model contains "
290 f"directory traversal sequence which may contain harmful payload."
291 )
292 if member.issym() or member.islnk():
293 raise RuntimeError(
294 f"The tarball member {member_path} in downloading model contains "
295 f"symbolic links which may contain harmful payload."
296 )
297 result.append(member)
298 return result
301def _extract_model_safe(
302 model_tar_path: str | os.PathLike, local_model_with_data_dir_path: str | os.PathLike
303) -> None:
304 """Safely extracts a tar file to a specified directory.
306 This function ensures that the extraction process mitigates against
307 directory traversal vulnerabilities by validating or sanitizing paths
308 within the tar file. It also provides compatibility for different versions
309 of the tarfile module by checking for the availability of certain attributes
310 or methods before invoking them.
312 Args:
313 model_tar_path: The path to the tar file to be extracted.
314 local_model_with_data_dir_path: The directory path where the tar file
315 contents will be extracted to.
316 """
317 with tarfile.open(model_tar_path) as model_with_data_zipped:
318 # Mitigate tarball directory traversal risks
319 if hasattr(tarfile, "data_filter"):
320 model_with_data_zipped.extractall(
321 path=local_model_with_data_dir_path, filter="data"
322 )
323 else:
324 model_with_data_zipped.extractall( # noqa: S202
325 path=local_model_with_data_dir_path,
326 members=_tar_members_filter(
327 model_with_data_zipped, local_model_with_data_dir_path
328 ),
329 )