mirror of
https://github.com/saymrwulf/onnxruntime.git
synced 2026-07-30 20:18:08 +00:00
Update to mobile model usability checker (#19843)
### Description
<!-- Describe your changes. -->
- Add check for CoreML MLProgram supported ops
- Only check usability with ORT Mobile package if requested
- this package will be deprecated so info is a) of minimal value and b)
can be confusing.
- Output more things at INFO level
- a lot of meaningful info was only output at DEBUG level. The default
INFO level is more useful
- dump full partition info at DEBUG level
- Check subgraphs fully
- CoreML can handle a subgraph
- TBD if we want to add support for adding a subgraph to the parent
graph for Loop and If nodes
- most likely will be required for simple If nodes to be performant
- Check 5D CoreML limitation
### Motivation and Context
<!-- - Why is this change required? What problem does it solve?
- If it fixes an open issue, please link to the issue here. -->
Improve helper tools
---------
Co-authored-by: Edward Chen <18449977+edgchen1@users.noreply.github.com>
This commit is contained in:
parent
7b3fff650a
commit
159fe9d4f3
8 changed files with 368 additions and 275 deletions
|
|
@ -514,7 +514,8 @@ file(GLOB onnxruntime_mobile_helpers_srcs CONFIGURE_DEPENDS
|
|||
${REPO_ROOT}/tools/python/util/mobile_helpers/*.py
|
||||
${REPO_ROOT}/tools/ci_build/github/android/mobile_package.required_operators.config
|
||||
${REPO_ROOT}/tools/ci_build/github/android/nnapi_supported_ops.md
|
||||
${REPO_ROOT}/tools/ci_build/github/apple/coreml_supported_ops.md
|
||||
${REPO_ROOT}/tools/ci_build/github/apple/coreml_supported_mlprogram_ops.md
|
||||
${REPO_ROOT}/tools/ci_build/github/apple/coreml_supported_neuralnetwork_ops.md
|
||||
)
|
||||
file(GLOB onnxruntime_qdq_helper_srcs CONFIGURE_DEPENDS
|
||||
${REPO_ROOT}/tools/python/util/qdq_helpers/*.py
|
||||
|
|
|
|||
|
|
@ -0,0 +1,20 @@
|
|||
<!--
|
||||
Keep in sync with doco generated from /docs/execution-providers/CoreML-ExecutionProvider.md on the gh_pages branch
|
||||
-->
|
||||
|Operator|Note|
|
||||
|--------|------|
|
||||
|ai.onnx:Add||
|
||||
|ai.onnx:AveragePool|Only 2D Pool is supported currently. 3D and 5D support can be added if needed.|
|
||||
|ai.onnx:Clip||
|
||||
|ai.onnx:Conv|Only 1D/2D Conv is supported.<br/>Bias if provided must be constant.|
|
||||
|ai.onnx:Div||
|
||||
|ai.onnx:Gemm|Input B must be constant.|
|
||||
|ai.onnx:GlobalAveragePool|Only 2D Pool is supported currently. 3D and 5D support can be added if needed.|
|
||||
|ai.onnx:GlobalMaxPool|Only 2D Pool is supported currently. 3D and 5D support can be added if needed.|
|
||||
|ai.onnx:MatMul|Only support for transA == 0, alpha == 1.0 and beta == 1.0 is currently implemented.|
|
||||
|ai.onnx:MaxPool|Only 2D Pool is supported currently. 3D and 5D support can be added if needed.|
|
||||
|ai.onnx:Mul||
|
||||
|ai.onnx:Pow|Only supports cases when both inputs are fp32.|
|
||||
|ai.onnx:Relu||
|
||||
|ai.onnx:Reshape||
|
||||
|ai.onnx:Sub||
|
||||
|
|
@ -30,7 +30,7 @@ Keep in sync with doco generated from /docs/execution-providers/CoreML-Execution
|
|||
|ai.onnx.ReduceSum||
|
||||
|ai.onnx:Relu||
|
||||
|ai.onnx:Reshape||
|
||||
|ai.onnx:Resize||
|
||||
|ai.onnx:Resize|4D input.<br/>`coordinate_transformation_mode` == `asymmetric`.<br/>`mode` == `linear` or `nearest`.<br/>`nearest_mode` == `floor`.<br/>`exclude_outside` == false<br/>`scales` or `sizes` must be constant.|
|
||||
|ai.onnx:Shape|Attribute `start` with non-default value is not supported.<br/>Attribute `end` is not supported.|
|
||||
|ai.onnx:Sigmoid||
|
||||
|ai.onnx:Slice|Inputs `starts`, `ends`, `axes`, and `steps` should be constant. Empty slice is not supported.|
|
||||
|
|
@ -8,26 +8,15 @@ import pathlib
|
|||
# need this before the mobile helper imports for some reason
|
||||
logging.basicConfig(format="%(levelname)s: %(message)s")
|
||||
|
||||
from .mobile_helpers import check_model_can_use_ort_mobile_pkg, usability_checker # noqa: E402
|
||||
from .mobile_helpers import usability_checker # noqa: E402
|
||||
|
||||
|
||||
def check_usability():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="""Analyze an ONNX model to determine how well it will work in mobile scenarios, and whether
|
||||
it is likely to be able to use the pre-built ONNX Runtime Mobile Android or iOS package.""",
|
||||
description="""Analyze an ONNX model to determine how well it will work in mobile scenarios.""",
|
||||
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--config_path",
|
||||
help="Path to required operators and types configuration used to build the pre-built ORT mobile package.",
|
||||
required=False,
|
||||
type=pathlib.Path,
|
||||
default=check_model_can_use_ort_mobile_pkg.get_default_config_path(),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--log_level", choices=["debug", "info", "warning", "error"], default="info", help="Logging level"
|
||||
)
|
||||
parser.add_argument("--log_level", choices=["debug", "info"], default="info", help="Logging level")
|
||||
parser.add_argument("model_path", help="Path to ONNX model to check", type=pathlib.Path)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
|
@ -43,24 +32,15 @@ def check_usability():
|
|||
logger.setLevel(logging.ERROR)
|
||||
|
||||
try_eps = usability_checker.analyze_model(args.model_path, skip_optimize=False, logger=logger)
|
||||
check_model_can_use_ort_mobile_pkg.run_check(args.model_path, args.config_path, logger)
|
||||
|
||||
logger.info(
|
||||
"Run `python -m onnxruntime.tools.convert_onnx_models_to_ort ...` to convert the ONNX model to ORT "
|
||||
"format. "
|
||||
"By default, the conversion tool will create an ORT format model with saved optimizations which can "
|
||||
"potentially be applied at runtime (with a .with_runtime_opt.ort file extension) for use with NNAPI "
|
||||
"or CoreML, and a fully optimized ORT format model (with a .ort file extension) for use with the CPU "
|
||||
"EP."
|
||||
)
|
||||
if try_eps:
|
||||
logger.info(
|
||||
"As NNAPI or CoreML may provide benefits with this model it is recommended to compare the "
|
||||
"performance of the <model>.with_runtime_opt.ort model using the NNAPI EP on Android, and the "
|
||||
"CoreML EP on iOS, against the performance of the <model>.ort model using the CPU EP."
|
||||
"performance of the model using the NNAPI EP on Android, and the CoreML EP on iOS, "
|
||||
"against the performance using the CPU EP."
|
||||
)
|
||||
else:
|
||||
logger.info("For optimal performance the <model>.ort model should be used with the CPU EP. ")
|
||||
logger.info("For optimal performance the model should be used with the CPU EP. ")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
|
|
|||
|
|
@ -239,7 +239,8 @@ def run_check_with_model(
|
|||
"on what is supported in the pre-built package."
|
||||
)
|
||||
logger.info(
|
||||
"A custom build of ONNX Runtime will be required to run the model. Please see "
|
||||
"The 'full' ORT package for Android (onnxruntime-android) or iOS (onnxruntime-{objc|c}) could be used, "
|
||||
"or a custom build of ONNX Runtime will be required if binary size is critical. Please see "
|
||||
"https://onnxruntime.ai/docs/build/custom.html for details on performing that."
|
||||
)
|
||||
else:
|
||||
|
|
|
|||
|
|
@ -1,81 +0,0 @@
|
|||
# Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
# Licensed under the MIT License.
|
||||
|
||||
import logging
|
||||
import pathlib
|
||||
import unittest
|
||||
|
||||
import onnx
|
||||
from testfixtures import LogCapture
|
||||
|
||||
from ..check_model_can_use_ort_mobile_pkg import run_check, run_check_with_model
|
||||
|
||||
# example usage from <ort root>/tools/python
|
||||
# python -m unittest util/mobile_helpers/test/test_check_model_can_use_ort_mobile_pkg.py
|
||||
# NOTE: at least on Windows you must use that as the working directory for all the imports to be happy
|
||||
|
||||
script_dir = pathlib.Path(__file__).parent
|
||||
ort_root = script_dir.parents[4]
|
||||
|
||||
ort_package_build_config_filename = (
|
||||
ort_root / "tools" / "ci_build" / "github" / "android" / "mobile_package.required_operators.config"
|
||||
)
|
||||
|
||||
|
||||
def _create_logger():
|
||||
logger = logging.getLogger("default")
|
||||
logger.setLevel(logging.DEBUG)
|
||||
return logger
|
||||
|
||||
|
||||
class TestMobilePackageModelChecker(unittest.TestCase):
|
||||
def test_supported_model(self):
|
||||
with LogCapture() as log_capture:
|
||||
logger = _create_logger()
|
||||
model_path = ort_root / "onnxruntime" / "test" / "testdata" / "ort_github_issue_4031.onnx"
|
||||
supported = run_check(model_path, ort_package_build_config_filename, logger)
|
||||
self.assertTrue(supported)
|
||||
|
||||
# print(log_capture)
|
||||
log_capture.check_present(
|
||||
("default", "INFO", "Model should work with the pre-built package."),
|
||||
)
|
||||
|
||||
def test_model_invalid_opset(self):
|
||||
with LogCapture() as log_capture:
|
||||
logger = _create_logger()
|
||||
model_path = ort_root / "onnxruntime" / "test" / "testdata" / "mnist.onnx"
|
||||
supported = run_check(model_path, ort_package_build_config_filename, logger)
|
||||
self.assertFalse(supported)
|
||||
|
||||
# print(log_capture)
|
||||
log_capture.check_present(
|
||||
("default", "INFO", "Model uses ONNX opset 8."),
|
||||
("default", "INFO", "The pre-built package only supports ONNX opsets [12, 13, 14, 15]."),
|
||||
)
|
||||
|
||||
def test_model_unsupported_op_and_types(self):
|
||||
with LogCapture() as log_capture:
|
||||
logger = _create_logger()
|
||||
model_path = ort_root / "onnxruntime" / "test" / "testdata" / "sequence_insert.onnx"
|
||||
|
||||
# Model uses opset 11 which is not supported in the mobile package. Update to supported opset first
|
||||
# Note: Ideally this would use update_onnx_opset however the ONNX opset update tools isn't working with
|
||||
# that at the moment (fix on ONNX side is pending).
|
||||
# For the sake of this test do a manual update. As the spec hasn't changed for the operators in the model
|
||||
# this is safe.
|
||||
# from ...onnx_model_utils import update_onnx_opset
|
||||
# model = update_onnx_opset(model_path, 13, logger=logger)
|
||||
# model = shape_inference.infer_shapes(model, strict_mode=True)
|
||||
model = onnx.load(str(model_path))
|
||||
model.opset_import[0].version = 13
|
||||
model = onnx.shape_inference.infer_shapes(model)
|
||||
supported = run_check_with_model(model, ort_package_build_config_filename, logger)
|
||||
self.assertFalse(supported)
|
||||
|
||||
# print(log_capture)
|
||||
log_capture.check_present(
|
||||
("default", "DEBUG", "Data type sequence_type of graph input input_seq is not supported."),
|
||||
("default", "INFO", "Unsupported operators:"),
|
||||
("default", "INFO", " ai.onnx:13:SequenceInsert"),
|
||||
)
|
||||
|
|
@ -40,14 +40,23 @@ class TestAnalyzer(unittest.TestCase):
|
|||
log_capture.check_present(
|
||||
("default", "INFO", "1 partitions with a total of 8/8 nodes can be handled by the NNAPI EP."),
|
||||
("default", "INFO", "Model should perform well with NNAPI as is: YES"),
|
||||
("default", "INFO", "1 partitions with a total of 8/8 nodes can be handled by the CoreML EP."),
|
||||
("default", "INFO", "Model should perform well with CoreML as is: YES"),
|
||||
(
|
||||
"default",
|
||||
"INFO",
|
||||
"1 partitions with a total of 8/8 nodes can be handled by the CoreML NeuralNetwork EP.",
|
||||
),
|
||||
("default", "INFO", "Model should perform well with CoreML NeuralNetwork as is: YES"),
|
||||
(
|
||||
"default",
|
||||
"INFO",
|
||||
"1 partitions with a total of 8/8 nodes can be handled by the CoreML MLProgram EP.",
|
||||
),
|
||||
("default", "INFO", "Model should perform well with CoreML MLProgram as is: YES"),
|
||||
)
|
||||
|
||||
def test_scan_model(self):
|
||||
"""
|
||||
Test a Speech model where all the top level nodes are Scan. All the real operators are in subgraphs, so we
|
||||
don't use NNAPI/CoreML currently. We want to make sure nodes in subgraphs are counted.
|
||||
Test a Speech model where all the top level nodes are Scan. We want to make sure nodes in subgraphs are counted.
|
||||
"""
|
||||
with LogCapture() as log_capture:
|
||||
logger = _create_logger()
|
||||
|
|
@ -57,12 +66,21 @@ class TestAnalyzer(unittest.TestCase):
|
|||
|
||||
# print(log_capture)
|
||||
log_capture.check_present(
|
||||
("default", "INFO", "0 partitions with a total of 0/76 nodes can be handled by the NNAPI EP."),
|
||||
("default", "INFO", "72 nodes are in subgraphs, which are currently not handled."),
|
||||
("default", "INFO", "Unsupported ops: ai.onnx:Scan"),
|
||||
("default", "INFO", "4 partitions with a total of 72/76 nodes can be handled by the NNAPI EP."),
|
||||
("default", "INFO", "72 nodes are in 4 subgraphs. Check EP as to whether subgraphs are supported."),
|
||||
("default", "INFO", "Model should perform well with NNAPI as is: NO"),
|
||||
("default", "INFO", "0 partitions with a total of 0/76 nodes can be handled by the CoreML EP."),
|
||||
("default", "INFO", "Model should perform well with CoreML as is: NO"),
|
||||
(
|
||||
"default",
|
||||
"INFO",
|
||||
"4 partitions with a total of 60/76 nodes can be handled by the CoreML NeuralNetwork EP.",
|
||||
),
|
||||
("default", "INFO", "Model should perform well with CoreML NeuralNetwork as is: NO"),
|
||||
(
|
||||
"default",
|
||||
"INFO",
|
||||
"12 partitions with a total of 24/76 nodes can be handled by the CoreML MLProgram EP.",
|
||||
),
|
||||
("default", "INFO", "Model should perform well with CoreML MLProgram as is: NO"),
|
||||
)
|
||||
|
||||
def test_dynamic_shape(self):
|
||||
|
|
@ -80,10 +98,18 @@ class TestAnalyzer(unittest.TestCase):
|
|||
("default", "INFO", "0 partitions with a total of 0/1 nodes can be handled by the NNAPI EP."),
|
||||
("default", "INFO", "Model should perform well with NNAPI as is: NO"),
|
||||
("default", "INFO", "Model should perform well with NNAPI if modified to have fixed input shapes: YES"),
|
||||
("default", "INFO", "0 partitions with a total of 0/1 nodes can be handled by the CoreML EP."),
|
||||
("default", "INFO", "CoreML cannot run any nodes in this model."),
|
||||
("default", "INFO", "Model should perform well with CoreML as is: NO"),
|
||||
("default", "INFO", "Model should perform well with CoreML if modified to have fixed input shapes: NO"),
|
||||
(
|
||||
"default",
|
||||
"INFO",
|
||||
"0 partitions with a total of 0/1 nodes can be handled by the CoreML MLProgram EP.",
|
||||
),
|
||||
("default", "INFO", "CoreML MLProgram cannot run any nodes in this model."),
|
||||
("default", "INFO", "Model should perform well with CoreML MLProgram as is: NO"),
|
||||
(
|
||||
"default",
|
||||
"INFO",
|
||||
"Model should perform well with CoreML MLProgram if modified to have fixed input shapes: NO",
|
||||
),
|
||||
)
|
||||
|
||||
def test_multi_partitions(self):
|
||||
|
|
@ -97,26 +123,24 @@ class TestAnalyzer(unittest.TestCase):
|
|||
|
||||
# print(log_capture)
|
||||
log_capture.check_present(
|
||||
("default", "INFO", "3 partitions with a total of 17/46 nodes can be handled by the NNAPI EP."),
|
||||
("default", "INFO", "Partition sizes: [5, 4, 8]"),
|
||||
("default", "INFO", "3 partitions with a total of 22/50 nodes can be handled by the NNAPI EP."),
|
||||
("default", "INFO", "\tPartition sizes: [13, 2, 7]"),
|
||||
(
|
||||
"default",
|
||||
"INFO",
|
||||
"Unsupported ops: ai.onnx:Gather,ai.onnx:ReduceProd,ai.onnx:ReduceSum,"
|
||||
"ai.onnx:Shape,ai.onnx:Unsqueeze",
|
||||
"\tUnsupported ops: ai.onnx:ReduceProd,ai.onnx:ReduceSum,ai.onnx:Shape",
|
||||
),
|
||||
(
|
||||
"default",
|
||||
"INFO",
|
||||
"NNAPI is not recommended with this model as there are 3 partitions "
|
||||
"covering 37.0% of the nodes in the model. "
|
||||
"covering 44.0% of the nodes in the model. "
|
||||
"This will most likely result in worse performance than just using the CPU EP.",
|
||||
),
|
||||
("default", "INFO", "Model should perform well with NNAPI as is: NO"),
|
||||
("default", "INFO", "Partition information if the model was updated to make the shapes fixed:"),
|
||||
("default", "INFO", "3 partitions with a total of 23/46 nodes can be handled by the NNAPI EP."),
|
||||
("default", "INFO", "Partition sizes: [3, 12, 8]"),
|
||||
("default", "INFO", "3 partitions with a total of 15/46 nodes can be handled by the CoreML EP."),
|
||||
("default", "INFO", "Partition sizes: [4, 4, 7]"),
|
||||
("default", "INFO", "Model should perform well with CoreML as is: NO"),
|
||||
(
|
||||
"default",
|
||||
"INFO",
|
||||
"4 partitions with a total of 20/50 nodes can be handled by the CoreML NeuralNetwork EP.",
|
||||
),
|
||||
("default", "INFO", "\tPartition sizes: [11, 3, 5, 1]"),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
# Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
# Licensed under the MIT License.
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import logging
|
||||
|
|
@ -8,25 +9,18 @@ import pathlib
|
|||
import tempfile
|
||||
from collections import deque
|
||||
from enum import IntEnum
|
||||
from typing import Optional
|
||||
|
||||
import onnx
|
||||
|
||||
from ..onnx_model_utils import (
|
||||
ModelProtoWithShapeInfo,
|
||||
get_producer_consumer_maps,
|
||||
is_fixed_size_tensor,
|
||||
iterate_graph_per_graph_func,
|
||||
iterate_graph_per_node_func,
|
||||
optimize_model,
|
||||
)
|
||||
from ..onnx_model_utils import ModelProtoWithShapeInfo, get_producer_consumer_maps, is_fixed_size_tensor, optimize_model
|
||||
|
||||
|
||||
class _SupportedOpsChecker:
|
||||
"""
|
||||
Class to process the md file with list of supported ops and caveats for an execution provider.
|
||||
e.g. /tools/ci_build/github/android/nnapi_supported_ops.md
|
||||
/tools/ci_build/github/apple/coreml_supported_ops.md
|
||||
/tools/ci_build/github/apple/coreml_supported_mlprogram_ops.md
|
||||
/tools/ci_build/github/apple/coreml_supported_neuralnetwork_ops.md
|
||||
"""
|
||||
|
||||
def __init__(self, filename):
|
||||
|
|
@ -75,28 +69,58 @@ class PartitioningInfo:
|
|||
MAYBE = (1,)
|
||||
YES = 2
|
||||
|
||||
def __init__(self):
|
||||
self.num_nodes = -1 # main graph only
|
||||
self.num_supported_nodes = -1
|
||||
self.num_partitions = -1
|
||||
self.num_nodes_in_subgraphs = -1 # nodes not covered as we don't currently handle subgraphs in nnapi/coreml
|
||||
self.supported_ops_checker = None
|
||||
self.supported_groups = []
|
||||
self.unsupported_ops = set()
|
||||
self.nodes_unsupported_due_to_op = -1
|
||||
self.nodes_unsupported_due_to_dynamic_input = -1
|
||||
def __init__(
|
||||
self,
|
||||
num_nodes: int,
|
||||
num_supported_nodes: int,
|
||||
num_partitions: int,
|
||||
supported_ops_checker: _SupportedOpsChecker,
|
||||
supported_groups: list[onnx.NodeProto],
|
||||
unsupported_ops: set[str],
|
||||
nodes_unsupported_due_to_op: int,
|
||||
nodes_unsupported_due_to_dynamic_input: int,
|
||||
num_unsupported_nodes_due_to_rank: int,
|
||||
ops_with_unsupported_rank: set[str],
|
||||
):
|
||||
self.num_nodes = num_nodes
|
||||
self.num_supported_nodes = num_supported_nodes
|
||||
self.num_partitions = num_partitions
|
||||
self.supported_ops_checker = supported_ops_checker
|
||||
self.supported_groups = supported_groups
|
||||
self.unsupported_ops = unsupported_ops
|
||||
self.nodes_unsupported_due_to_op = nodes_unsupported_due_to_op
|
||||
self.nodes_unsupported_due_to_dynamic_input = nodes_unsupported_due_to_dynamic_input
|
||||
self.num_unsupported_nodes_due_to_rank = num_unsupported_nodes_due_to_rank
|
||||
self.ops_with_unsupported_rank = ops_with_unsupported_rank
|
||||
|
||||
self.num_subgraphs = 0
|
||||
self.num_nodes_in_subgraphs = 0
|
||||
|
||||
def merge(self, other: PartitioningInfo):
|
||||
"""
|
||||
Merge the information from another PartitioningInfo instance into this one.
|
||||
"""
|
||||
self.num_nodes += other.num_nodes
|
||||
self.num_supported_nodes += other.num_supported_nodes
|
||||
self.num_partitions += other.num_partitions
|
||||
self.supported_groups.extend(other.supported_groups)
|
||||
self.unsupported_ops.update(other.unsupported_ops)
|
||||
self.nodes_unsupported_due_to_op += other.nodes_unsupported_due_to_op
|
||||
self.nodes_unsupported_due_to_dynamic_input += other.nodes_unsupported_due_to_dynamic_input
|
||||
self.num_unsupported_nodes_due_to_rank += other.num_unsupported_nodes_due_to_rank
|
||||
self.ops_with_unsupported_rank.update(other.ops_with_unsupported_rank)
|
||||
|
||||
# hard assumption that we merge into the main graph partitioning info
|
||||
self.num_subgraphs += 1
|
||||
self.num_nodes_in_subgraphs += other.num_nodes
|
||||
|
||||
def suitability(self):
|
||||
# for now add up all the nodes. if there are subgraphs, the percentage of covered nodes will be reduced by all
|
||||
# nodes in the subgraphs.
|
||||
num_nodes = self.num_nodes + self.num_nodes_in_subgraphs
|
||||
|
||||
# semi-arbitrary choices that err on the side of MAYBE.
|
||||
# having 1 partition is always preferred, but if that is small it may not be useful.
|
||||
# having 2 partitions may be okay if they cover most nodes
|
||||
# more than 2 partitions and the device copy cost is almost guaranteed to outweight the benefit of using the NPU
|
||||
# more than 2 partitions and the device copy cost is almost guaranteed to outweigh the benefit of using the NPU
|
||||
# NOTE: This assumes the EP is not CPU based and there is device copy overhead to consider
|
||||
pct_supported = self.num_supported_nodes / num_nodes * 100
|
||||
pct_supported = self.num_supported_nodes / self.num_nodes * 100
|
||||
if self.num_partitions == 1:
|
||||
if pct_supported > 75:
|
||||
return PartitioningInfo.TryWithEP.YES
|
||||
|
|
@ -113,46 +137,66 @@ class PartitioningInfo:
|
|||
|
||||
return PartitioningInfo.TryWithEP.NO
|
||||
|
||||
def dump_analysis(self, logger: logging.Logger, ep_name: str):
|
||||
def print_analysis(self, logger: logging.Logger, ep_name: str):
|
||||
"""
|
||||
Analyze the partitioning information and log the analysis
|
||||
:param logger: Logger to use
|
||||
:param ep_name: Execution provider name to use in the log messages
|
||||
"""
|
||||
|
||||
num_nodes = self.num_nodes + self.num_nodes_in_subgraphs
|
||||
logger.info(
|
||||
f"{self.num_partitions} partitions with a total of {self.num_supported_nodes}/{num_nodes} "
|
||||
f"{self.num_partitions} partitions with a total of {self.num_supported_nodes}/{self.num_nodes} "
|
||||
f"nodes can be handled by the {ep_name} EP."
|
||||
)
|
||||
if self.num_nodes_in_subgraphs:
|
||||
logger.info(f"{self.num_nodes_in_subgraphs} nodes are in subgraphs, which are currently not handled.")
|
||||
|
||||
if self.supported_groups:
|
||||
logger.info(f'Partition sizes: [{", ".join([str(len(partition)) for partition in self.supported_groups])}]')
|
||||
logger.info(f"Unsupported nodes due to operator={self.nodes_unsupported_due_to_op}")
|
||||
if self.nodes_unsupported_due_to_dynamic_input:
|
||||
logger.info(
|
||||
"Unsupported nodes due to input having a dynamic shape=%d",
|
||||
self.nodes_unsupported_due_to_dynamic_input,
|
||||
)
|
||||
logger.info(
|
||||
f'\tPartition sizes: [{", ".join([str(len(partition)) for partition in self.supported_groups])}]'
|
||||
)
|
||||
|
||||
if logger.getEffectiveLevel() <= logging.DEBUG:
|
||||
# Enable this manually if you need to look at specific partitions.
|
||||
# for group in supported_groups:
|
||||
# logger.debug(f'Nodes in group: {",".join([f"{node.name}:{node.op_type}" for node in group])}')
|
||||
if self.unsupported_ops:
|
||||
logger.info(f'Unsupported ops: {",".join(sorted(self.unsupported_ops))}')
|
||||
# dump full groups if debug output is enabled
|
||||
for group in self.supported_groups:
|
||||
logger.debug(f'Nodes in group: {",".join([f"{node.op_type}:{node.name}" for node in group])}')
|
||||
|
||||
caveats = self.supported_ops_checker.get_caveats()
|
||||
if caveats:
|
||||
indent = " " * 5
|
||||
logger.debug(
|
||||
"Caveats that have not been checked and may result in a node not being supported: "
|
||||
f'{"".join([os.linesep + indent + caveat for caveat in caveats])}'
|
||||
)
|
||||
logger.info(f"Unsupported nodes due to operator={self.nodes_unsupported_due_to_op}")
|
||||
if self.unsupported_ops:
|
||||
logger.info(f'\tUnsupported ops: {",".join(sorted(self.unsupported_ops))}')
|
||||
|
||||
pct_nodes_using_ep = self.num_supported_nodes / num_nodes * 100
|
||||
caveats = self.supported_ops_checker.get_caveats()
|
||||
if caveats:
|
||||
indent = " " * 5
|
||||
logger.info(
|
||||
"\tCaveats that have not been checked and may result in a node not actually being supported: "
|
||||
f'{"".join([os.linesep + indent + caveat for caveat in caveats])}'
|
||||
)
|
||||
|
||||
if self.nodes_unsupported_due_to_dynamic_input:
|
||||
logger.info(
|
||||
"Unsupported nodes due to input having a dynamic shape=%d",
|
||||
self.nodes_unsupported_due_to_dynamic_input,
|
||||
)
|
||||
|
||||
if self.num_unsupported_nodes_due_to_rank:
|
||||
logger.info(f"Unsupported nodes due to rank of input data={self.num_unsupported_nodes_due_to_rank}")
|
||||
logger.info(f"\tOps with unsupported rank: {','.join(sorted(self.ops_with_unsupported_rank))}")
|
||||
|
||||
if self.num_subgraphs > 0:
|
||||
# TODO: CoreML has a flag. NNAPI doesn't. Either should be able to support a subgraph when treated as a
|
||||
# separate graph (only extra detail would be making sure implicit inputs are handled).
|
||||
# Merging the subgraph into the parent graph would be more complex.
|
||||
# e.g. for CoreML we could potentially convert Loop to while_loop and If to cond if the subgraphs in the
|
||||
# control flow node are fully supported.
|
||||
# NNAPI also has While and If.
|
||||
|
||||
# It most likely will be necessary to support merging in If nodes with fully supported subgraphs,
|
||||
# as the subgraphs in those are often very simple, so the performance cost of going to the CPU EP and back
|
||||
# is high.
|
||||
logger.info(
|
||||
f"{self.num_nodes_in_subgraphs} nodes are in {self.num_subgraphs} subgraphs. "
|
||||
"Check EP as to whether subgraphs are supported."
|
||||
)
|
||||
|
||||
pct_nodes_using_ep = self.num_supported_nodes / self.num_nodes * 100
|
||||
if self.num_partitions == 0:
|
||||
logger.info(f"{ep_name} cannot run any nodes in this model.")
|
||||
elif self.num_partitions == 1:
|
||||
|
|
@ -186,43 +230,24 @@ class PartitioningInfo:
|
|||
)
|
||||
|
||||
|
||||
def check_partitioning(
|
||||
def _check_partitioning_for_graph(
|
||||
graph: onnx.GraphProto,
|
||||
node_to_producers: dict[onnx.NodeProto, set[onnx.NodeProto]],
|
||||
node_to_consumers: dict[onnx.NodeProto, set[onnx.NodeProto]],
|
||||
supported_ops_checker: _SupportedOpsChecker,
|
||||
require_fixed_input_sizes: bool = False,
|
||||
value_info: Optional[dict] = None,
|
||||
outer_scope_initializers: set[str],
|
||||
require_fixed_input_sizes: bool,
|
||||
value_info: dict[str, onnx.ValueInfoProto],
|
||||
max_rank: int = 999, # max rank if EP has a limitation
|
||||
):
|
||||
"""
|
||||
Estimate the partitions the graph will be split into for nodes that is_node_supported_fn returns true for.
|
||||
|
||||
The check on whether a node is supported is purely based on the operator type. Additional limitations
|
||||
(e.g. NNAPI EP only supports 2D Conv) are not checked, so partitions may not be 100% accurate. The limitations
|
||||
for operators in the partitions are printed so the user can manually check.
|
||||
:param graph: Graph to process
|
||||
:param supported_ops_checker: Checker with info on supported ops.
|
||||
:param require_fixed_input_sizes: If True, require that the inputs to a potentially supported node are
|
||||
fixed size tensors for it to be considered as supported.
|
||||
If True, onnx.shape_inference.infer_shapes should have been run on the model
|
||||
to populate the shape information.
|
||||
:param value_info: Map of value name to ValueInfoProto. Required if require_fixed_input_sizes is True to lookup
|
||||
the shape of a value.
|
||||
:return PartitioningInfo instance with details
|
||||
"""
|
||||
|
||||
if require_fixed_input_sizes and not value_info:
|
||||
raise ValueError("value_info must be provided if require_fixed_input_sizes is True.")
|
||||
|
||||
node_to_producers, node_to_consumers = get_producer_consumer_maps(graph)
|
||||
|
||||
# initializers have fixed sizes.
|
||||
# TODO: when adding subgraph support we also need to match against initializers in ancestor graphs as they are
|
||||
# be accessible from the outer scope (unless shadowed locally)
|
||||
initializers = [i.name for i in graph.initializer]
|
||||
|
||||
def _is_fixed_shape_value(value):
|
||||
if value in value_info:
|
||||
return is_fixed_size_tensor(value_info[value])
|
||||
if value in initializers:
|
||||
|
||||
if value in initializers or value in outer_scope_initializers:
|
||||
return True
|
||||
|
||||
# if something has an unknown shape (e.g. something downstream of a Reshape with dynamic input for the shape)
|
||||
|
|
@ -236,6 +261,10 @@ def check_partitioning(
|
|||
# We keep the structure and variable names as close as possible to the C++ implementation to simplify keeping them
|
||||
# in sync if future updates are needed.
|
||||
#
|
||||
# NOTE: CreateSupportedPartitionNodeGroups was recently updated to be QDQ aware so that partitions did not split
|
||||
# QDQ node groups. This code does not need to be QDQ aware as splitting a QDQ node group does not affect the total
|
||||
# number of partitions or supported nodes.
|
||||
#
|
||||
|
||||
# we don't currently support a callback for additional group closure checks in the python implementation
|
||||
on_group_closed_fn = None
|
||||
|
|
@ -256,23 +285,15 @@ def check_partitioning(
|
|||
# node is only dependent on graph input or initializers
|
||||
nodes_to_process.append(node)
|
||||
|
||||
# currently we don't support checking subgraphs in the partitioning as they're not handled by NNAPI/CoreML.
|
||||
# check how many nodes are in that blind spot so we can adjust the recommendation accordingly.
|
||||
# note: need to pass count in an array so that it's by reference
|
||||
def _count_subgraph_nodes(cur_graph: onnx.GraphProto, original_graph: onnx.GraphProto, count: [int]):
|
||||
if cur_graph != original_graph:
|
||||
count[0] += len(cur_graph.node)
|
||||
|
||||
nodes_in_subgraphs = [0] # array with single value
|
||||
iterate_graph_per_graph_func(graph, _count_subgraph_nodes, original_graph=graph, count=nodes_in_subgraphs)
|
||||
|
||||
supported_group = []
|
||||
# the partition node group's border is the aggregate of its nodes' output nodes
|
||||
supported_group_border = set()
|
||||
num_supported_nodes = 0
|
||||
num_unsupported_nodes_due_to_op = 0
|
||||
num_unsupported_nodes_due_to_dynamic_input = 0
|
||||
num_unsupported_nodes_due_to_rank = 0
|
||||
unsupported_ops = set()
|
||||
ops_with_unsupported_rank = set()
|
||||
|
||||
def close_group():
|
||||
if supported_group:
|
||||
|
|
@ -295,7 +316,22 @@ def check_partitioning(
|
|||
|
||||
is_op_supported = supported_ops_checker.is_op_supported(node)
|
||||
is_input_shape_supported = not require_fixed_input_sizes or all(_is_fixed_shape_value(i) for i in node.input)
|
||||
is_node_supported = is_op_supported and is_input_shape_supported
|
||||
|
||||
is_rank_supported = True
|
||||
if value_info:
|
||||
for node_input in node.input:
|
||||
if node_input and node_input in value_info and value_info[node_input].type.HasField("tensor_type"):
|
||||
input_rank = len(value_info[node_input].type.tensor_type.shape.dim)
|
||||
if input_rank > max_rank:
|
||||
is_rank_supported = False
|
||||
break
|
||||
|
||||
# special-case if we can infer the rank from the length of the 'perms' Transpose attribute
|
||||
# e.g. this works with SegmentAnything where dynamic Reshape operators result in no shape info.
|
||||
if node.op_type == "Transpose" and len(node.attribute[0].ints) > max_rank:
|
||||
is_rank_supported = False
|
||||
|
||||
is_node_supported = is_op_supported and is_input_shape_supported and is_rank_supported
|
||||
|
||||
if not is_node_supported:
|
||||
if node in supported_group_border:
|
||||
|
|
@ -307,9 +343,14 @@ def check_partitioning(
|
|||
if not is_op_supported:
|
||||
unsupported_ops.add(f'{node.domain if node.domain else "ai.onnx"}:{node.op_type}')
|
||||
num_unsupported_nodes_due_to_op += 1
|
||||
else:
|
||||
|
||||
if not is_input_shape_supported:
|
||||
num_unsupported_nodes_due_to_dynamic_input += 1
|
||||
|
||||
if not is_rank_supported:
|
||||
num_unsupported_nodes_due_to_rank += 1
|
||||
ops_with_unsupported_rank.add(f'{node.domain if node.domain else "ai.onnx"}:{node.op_type}')
|
||||
|
||||
if is_node_supported:
|
||||
num_supported_nodes += 1
|
||||
|
||||
|
|
@ -337,34 +378,128 @@ def check_partitioning(
|
|||
|
||||
close_group()
|
||||
|
||||
# find any subgraphs and check supported for nodes in the subgraphs. this won't change the partitioning as we skip
|
||||
# Scan/Loop/If nodes, but will provide additional info on operators that are not supported if we changed that.
|
||||
iterate_graph_per_node_func(graph, supported_ops_checker.is_op_supported)
|
||||
|
||||
num_nodes = len(graph.node)
|
||||
num_partitions = len(supported_groups)
|
||||
|
||||
info = PartitioningInfo()
|
||||
info.num_nodes = num_nodes
|
||||
info.num_supported_nodes = num_supported_nodes
|
||||
info.num_partitions = num_partitions
|
||||
info.num_nodes_in_subgraphs = nodes_in_subgraphs[0]
|
||||
info.supported_ops_checker = supported_ops_checker
|
||||
info.supported_groups = supported_groups
|
||||
info.unsupported_ops = unsupported_ops
|
||||
info.nodes_unsupported_due_to_op = num_unsupported_nodes_due_to_op
|
||||
info.nodes_unsupported_due_to_dynamic_input = num_unsupported_nodes_due_to_dynamic_input
|
||||
info = PartitioningInfo(
|
||||
num_nodes,
|
||||
num_supported_nodes,
|
||||
num_partitions,
|
||||
supported_ops_checker,
|
||||
supported_groups,
|
||||
unsupported_ops,
|
||||
num_unsupported_nodes_due_to_op,
|
||||
num_unsupported_nodes_due_to_dynamic_input,
|
||||
num_unsupported_nodes_due_to_rank,
|
||||
ops_with_unsupported_rank,
|
||||
)
|
||||
|
||||
return info
|
||||
|
||||
|
||||
def _check_ep_partitioning(model, supported_ops_config, value_info: Optional[dict] = None):
|
||||
def check_partitioning(
|
||||
main_graph: onnx.GraphProto,
|
||||
supported_ops_checker: _SupportedOpsChecker,
|
||||
require_fixed_input_sizes: bool,
|
||||
max_rank: int = 999,
|
||||
) -> PartitioningInfo:
|
||||
"""
|
||||
Estimate the partitions the graph will be split into for nodes that is_node_supported_fn returns true for.
|
||||
|
||||
The check on whether a node is supported is purely based on the operator type. Additional limitations
|
||||
(e.g. NNAPI EP only supports 2D Conv) are not checked, so partitions may not be 100% accurate. The limitations
|
||||
for operators in the partitions are printed so the user can manually check.
|
||||
:param main_graph: Graph to process
|
||||
:param supported_ops_checker: Checker with info on supported ops.
|
||||
:param require_fixed_input_sizes: If True, require that the inputs to a potentially supported node are fixed size
|
||||
tensors for it to be considered as supported. This requires
|
||||
onnx.shape_inference.infer_shapes to have been run on the model to populate the
|
||||
shape information.
|
||||
If False, shapes are ignored during the check.
|
||||
:param max_rank: Set if EP has a limitation on the rank of tensors it supports.
|
||||
:return PartitioningInfo instance with details
|
||||
"""
|
||||
|
||||
if require_fixed_input_sizes and len(main_graph.value_info) == 0 and len(main_graph.node) > 1:
|
||||
raise ValueError("Run onnx.shape_inference.infer_shapes on the model to populate the shape information.")
|
||||
|
||||
# create lookup map from ValueInfo for efficiency
|
||||
def _update_value_info(graph: onnx.GraphProto, value_to_shape: dict[str, onnx.ValueInfoProto]):
|
||||
for v in graph.input:
|
||||
value_to_shape[v.name] = v
|
||||
for v in graph.output:
|
||||
value_to_shape[v.name] = v
|
||||
for v in graph.value_info:
|
||||
value_to_shape[v.name] = v
|
||||
|
||||
# the producer/consumer maps are for the entire model
|
||||
node_to_producers, node_to_consumers = get_producer_consumer_maps(main_graph)
|
||||
|
||||
def _check_graph(
|
||||
graph: onnx.GraphProto,
|
||||
outer_scope_value_info: dict[str, onnx.ValueInfoProto] | None,
|
||||
outer_scope_initializers: set[str] | None = None,
|
||||
partitioning_info: PartitioningInfo | None = None,
|
||||
) -> PartitioningInfo:
|
||||
if outer_scope_value_info is not None:
|
||||
# extend value info if we're using it. we replace any value shadowed with a local one
|
||||
value_info = outer_scope_value_info.copy()
|
||||
_update_value_info(graph, value_info)
|
||||
else:
|
||||
value_info = {}
|
||||
|
||||
if outer_scope_initializers is None:
|
||||
outer_scope_initializers = set()
|
||||
|
||||
info = _check_partitioning_for_graph(
|
||||
graph,
|
||||
node_to_producers,
|
||||
node_to_consumers,
|
||||
supported_ops_checker,
|
||||
outer_scope_initializers,
|
||||
require_fixed_input_sizes,
|
||||
value_info,
|
||||
max_rank,
|
||||
)
|
||||
|
||||
if partitioning_info:
|
||||
# merge in subgraph info
|
||||
partitioning_info.merge(info)
|
||||
else:
|
||||
# main graph info
|
||||
partitioning_info = info
|
||||
|
||||
# setup outer scope initializers. we copy the input set as a model may have multiple subgraphs
|
||||
# on multiple levels, so we need to keep the set for each descent separate
|
||||
subgraph_outer_scope_initializers = set(outer_scope_initializers)
|
||||
for initializer in graph.initializer:
|
||||
subgraph_outer_scope_initializers.add(initializer.name)
|
||||
|
||||
for node in graph.node:
|
||||
# recurse into nodes with subgraphs
|
||||
for attr in node.attribute:
|
||||
if attr.HasField("g"):
|
||||
subgraph = attr.g
|
||||
partitioning_info = _check_graph(
|
||||
subgraph, value_info, subgraph_outer_scope_initializers, partitioning_info
|
||||
)
|
||||
|
||||
return partitioning_info
|
||||
|
||||
aggregated_partitioning_info = _check_graph(main_graph, {} if require_fixed_input_sizes else None)
|
||||
|
||||
return aggregated_partitioning_info
|
||||
|
||||
|
||||
def _check_ep_partitioning(
|
||||
model: onnx.ModelProto, supported_ops_config: pathlib.Path, require_fixed_input_sizes: bool, max_rank: int = 999
|
||||
):
|
||||
supported_ops = _SupportedOpsChecker(supported_ops_config)
|
||||
partition_info = check_partitioning(model.graph, supported_ops, value_info is not None, value_info)
|
||||
partition_info = check_partitioning(model.graph, supported_ops, require_fixed_input_sizes, max_rank)
|
||||
return partition_info
|
||||
|
||||
|
||||
def check_nnapi_partitions(model, value_info: Optional[dict] = None):
|
||||
def check_nnapi_partitions(model, require_fixed_input_sizes: bool):
|
||||
# if we're running in the ORT python package the file should be local. otherwise assume we're running from the
|
||||
# ORT repo
|
||||
script_dir = pathlib.Path(__file__).parent
|
||||
|
|
@ -375,10 +510,10 @@ def check_nnapi_partitions(model, value_info: Optional[dict] = None):
|
|||
ort_root = script_dir.parents[3]
|
||||
config_path = ort_root / "tools" / "ci_build" / "github" / "android" / "nnapi_supported_ops.md"
|
||||
|
||||
return _check_ep_partitioning(model, config_path, value_info)
|
||||
return _check_ep_partitioning(model, config_path, require_fixed_input_sizes)
|
||||
|
||||
|
||||
def check_coreml_partitions(model, value_info: Optional[dict] = None):
|
||||
def check_coreml_partitions(model: onnx.ModelProto, require_fixed_input_sizes: bool, config_filename):
|
||||
# if we're running in the ORT python package the file should be local. otherwise assume we're running from the
|
||||
# ORT repo
|
||||
script_dir = pathlib.Path(__file__).parent
|
||||
|
|
@ -387,15 +522,16 @@ def check_coreml_partitions(model, value_info: Optional[dict] = None):
|
|||
config_path = local_config
|
||||
else:
|
||||
ort_root = script_dir.parents[3]
|
||||
config_path = ort_root / "tools" / "ci_build" / "github" / "apple" / "coreml_supported_ops.md"
|
||||
config_path = ort_root / "tools" / "ci_build" / "github" / "apple" / config_filename
|
||||
|
||||
return _check_ep_partitioning(model, config_path, value_info)
|
||||
max_rank = 5
|
||||
return _check_ep_partitioning(model, config_path, require_fixed_input_sizes, max_rank)
|
||||
|
||||
|
||||
def check_shapes(graph: onnx.GraphProto, logger: Optional[logging.Logger] = None):
|
||||
def check_shapes(graph: onnx.GraphProto, logger: logging.Logger | None = None):
|
||||
"""
|
||||
Check the shapes of graph inputs, values and graph outputs to determine if they have static or dynamic sizes.
|
||||
NNAPI and CoreML do not support dynamically sized values.
|
||||
NNAPI does not support dynamically sized values. CoreML does, but it will most likely cost performance.
|
||||
:param graph: Graph to check. If shape inferencing has been run the checks on values will be meaningful.
|
||||
:param logger: Optional logger for diagnostic information.
|
||||
:return: Tuple of List of inputs with dynamic shapes, Number of dynamic values found
|
||||
|
|
@ -469,62 +605,76 @@ def checker(model_path: pathlib.Path, logger: logging.Logger):
|
|||
model_with_shape_info_wrapper = ModelProtoWithShapeInfo(model_path)
|
||||
model_with_shape_info = model_with_shape_info_wrapper.model_with_shape_info
|
||||
|
||||
# create lookup map for efficiency
|
||||
value_to_shape = {}
|
||||
for v in model_with_shape_info.graph.input:
|
||||
value_to_shape[v.name] = v
|
||||
for v in model_with_shape_info.graph.output:
|
||||
value_to_shape[v.name] = v
|
||||
for v in model_with_shape_info.graph.value_info:
|
||||
value_to_shape[v.name] = v
|
||||
|
||||
dynamic_inputs, num_dynamic_values = check_shapes(model_with_shape_info.graph)
|
||||
|
||||
def check_ep(ep_name, checker_func):
|
||||
logger.info(f"Checking {ep_name}")
|
||||
|
||||
# check with shape info first so supported nodes takes into account values with dynamic shapes
|
||||
partition_info = checker_func(model_with_shape_info, value_to_shape)
|
||||
if logger.getEffectiveLevel() <= logging.DEBUG:
|
||||
partition_info.dump_analysis(logger, ep_name)
|
||||
require_fixed_input_sizes = True
|
||||
partition_info = checker_func(model_with_shape_info, require_fixed_input_sizes)
|
||||
if logger.getEffectiveLevel() <= logging.INFO:
|
||||
partition_info.print_analysis(logger, ep_name)
|
||||
|
||||
suitability = partition_info.suitability()
|
||||
logger.info(f"Model should perform well with {ep_name} as is: {suitability.name}")
|
||||
|
||||
if suitability != PartitioningInfo.TryWithEP.YES and dynamic_inputs:
|
||||
logger.info("--------")
|
||||
logger.info("Checking if model will perform better if the dynamic shapes are fixed...")
|
||||
partition_info_with_fixed_shapes = checker_func(model_with_shape_info)
|
||||
if logger.getEffectiveLevel() <= logging.DEBUG:
|
||||
require_fixed_input_sizes = False
|
||||
partition_info_with_fixed_shapes = checker_func(model_with_shape_info, require_fixed_input_sizes)
|
||||
|
||||
if logger.getEffectiveLevel() <= logging.INFO:
|
||||
# analyze and log detailed info
|
||||
logger.info("Partition information if the model was updated to make the shapes fixed:")
|
||||
partition_info_with_fixed_shapes.dump_analysis(logger, ep_name)
|
||||
partition_info_with_fixed_shapes.print_analysis(logger, ep_name)
|
||||
|
||||
fixed_shape_suitability = partition_info_with_fixed_shapes.suitability()
|
||||
logger.info(
|
||||
f"Model should perform well with {ep_name} if modified to have fixed input shapes: "
|
||||
f"{fixed_shape_suitability.name}"
|
||||
)
|
||||
|
||||
if fixed_shape_suitability != PartitioningInfo.TryWithEP.NO:
|
||||
logger.info("Shapes can be altered using python -m onnxruntime.tools.make_dynamic_shape_fixed")
|
||||
|
||||
if fixed_shape_suitability.value > suitability.value:
|
||||
suitability = fixed_shape_suitability
|
||||
|
||||
logger.info("================")
|
||||
logger.info("")
|
||||
|
||||
return suitability
|
||||
|
||||
nnapi_suitability = check_ep("NNAPI", check_nnapi_partitions)
|
||||
coreml_suitability = check_ep("CoreML", check_coreml_partitions)
|
||||
|
||||
# Check for NeuralNetwork CoreML model
|
||||
def check_nn_coreml(model: onnx.ModelProto, require_fixed_input_sizes):
|
||||
return check_coreml_partitions(model, require_fixed_input_sizes, "coreml_supported_neuralnetwork_ops.md")
|
||||
|
||||
# Check for MLProgram CoreML model
|
||||
def check_mlprogram_coreml(model: onnx.ModelProto, require_fixed_input_sizes):
|
||||
return check_coreml_partitions(model, require_fixed_input_sizes, "coreml_supported_mlprogram_ops.md")
|
||||
|
||||
coreml_nn_suitability = check_ep("CoreML NeuralNetwork", check_nn_coreml)
|
||||
coreml_mlprogram_suitability = check_ep("CoreML MLProgram", check_mlprogram_coreml)
|
||||
|
||||
if (
|
||||
nnapi_suitability != PartitioningInfo.TryWithEP.YES or coreml_suitability != PartitioningInfo.TryWithEP.YES
|
||||
) and logger.getEffectiveLevel() > logging.DEBUG:
|
||||
logger.info("Re-run with log level of DEBUG for more details on the NNAPI/CoreML issues.")
|
||||
nnapi_suitability != PartitioningInfo.TryWithEP.YES
|
||||
or coreml_nn_suitability != PartitioningInfo.TryWithEP.YES
|
||||
or coreml_mlprogram_suitability != PartitioningInfo.TryWithEP.YES
|
||||
) and logger.getEffectiveLevel() > logging.INFO:
|
||||
logger.info("Re-run with log level of INFO for more details on the NNAPI/CoreML issues.")
|
||||
|
||||
logger.info("---------------")
|
||||
return nnapi_suitability != PartitioningInfo.TryWithEP.NO or coreml_suitability != PartitioningInfo.TryWithEP.NO
|
||||
return (
|
||||
nnapi_suitability != PartitioningInfo.TryWithEP.NO
|
||||
or coreml_nn_suitability != PartitioningInfo.TryWithEP.NO
|
||||
or coreml_mlprogram_suitability != PartitioningInfo.TryWithEP.NO
|
||||
)
|
||||
|
||||
|
||||
def analyze_model(model_path: pathlib.Path, skip_optimize: bool = False, logger: Optional[logging.Logger] = None):
|
||||
def analyze_model(model_path: pathlib.Path, skip_optimize: bool = False, logger: logging.Logger | None = None):
|
||||
"""
|
||||
Analyze the provided model to determine if it's likely to work well with the NNAPI or CoreML Execution Providers
|
||||
:param model_path: Model to analyze.
|
||||
|
|
@ -555,9 +705,7 @@ def parse_args():
|
|||
os.path.basename(__file__), description="""Analyze an ONNX model for usage with the ORT mobile"""
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--log_level", choices=["debug", "info", "warning", "error"], default="info", help="Logging level"
|
||||
)
|
||||
parser.add_argument("--log_level", choices=["debug", "info"], default="info", help="Logging level")
|
||||
parser.add_argument(
|
||||
"--skip_optimize",
|
||||
action="store_true",
|
||||
|
|
|
|||
Loading…
Reference in a new issue