From de461944f8f3d35440357360e4f6dbfa1bb2abdd Mon Sep 17 00:00:00 2001 From: Andrey Churkin Date: Fri, 8 Mar 2024 06:38:39 +0000 Subject: [PATCH] Introduce an ignored subgraph (#2548) ### Changes Introduce an ability to define subgraph in ignored scope ### Reason for changes The models get more complicated in terms of structure. Noticed many times, that it's impossible to exclude some parts of the model from quantization using the existing ignore scope functionality. ### Related tickets Ref: 100999 ### Tests tests/common/test_ignored_scope.py --- nncf/__init__.py | 1 + nncf/scopes.py | 53 +++++++++++++++++++++++++++++- tests/common/test_ignored_scope.py | 7 ++++ 3 files changed, 60 insertions(+), 1 deletion(-) diff --git a/nncf/__init__.py b/nncf/__init__.py index d14ff86960e..eaaa755a49e 100644 --- a/nncf/__init__.py +++ b/nncf/__init__.py @@ -47,6 +47,7 @@ ) from nncf.quantization.advanced_parameters import AdvancedQuantizationParameters as AdvancedQuantizationParameters from nncf.scopes import IgnoredScope as IgnoredScope +from nncf.scopes import Subgraph as Subgraph from nncf.version import __version__ as __version__ _SUPPORTED_FRAMEWORKS = ["torch", "tensorflow", "onnx", "openvino"] diff --git a/nncf/scopes.py b/nncf/scopes.py index c052680f343..c7cf631ad57 100644 --- a/nncf/scopes.py +++ b/nncf/scopes.py @@ -20,6 +20,42 @@ from nncf.common.utils.api_marker import api +@api(canonical_alias="nncf.Subgraph") +@dataclass +class Subgraph: + """ + Defines the ignored subgraph as follows: A subgraph comprises all nodes along + all simple paths in the graph from input to output nodes. + + :param inputs: Input node names. + :type inputs: List[str] + :param outputs: Output node names. + :type outputs: List[str] + """ + + inputs: List[str] = field(default_factory=list) + outputs: List[str] = field(default_factory=list) + + +def get_ignored_node_names_from_subgraph(graph: NNCFGraph, subgraph: Subgraph) -> List[str]: + """ + Returns all names that should be ignored according to given subgraph. + + :param graph: Given NNCFGraph. + :param subgraph: Given subgraph instance. + :return: All names that should be ignored according to given subgraph. + """ + ignored_names = set() + for start_node_name in subgraph.inputs: + for end_node_name in subgraph.outputs: + for path in graph.get_all_simple_paths(start_node_name, end_node_name): + for node_key in path: + node = graph.get_node_by_key(node_key) + ignored_names.add(node.node_name) + + return list(sorted(ignored_names)) + + @api(canonical_alias="nncf.IgnoredScope") @dataclass class IgnoredScope: @@ -61,6 +97,8 @@ class IgnoredScope: :type patterns: List[str] :param types: List of ignored operation types. :type types: List[str] + :param subgraphs: List of ignored subgraphs. + :type subgraphs: List[Subgraph] :param validate: If set to True, then a RuntimeError will be raised if any ignored scope does not match in the model graph. :type types: bool @@ -69,6 +107,7 @@ class IgnoredScope: names: List[str] = field(default_factory=list) patterns: List[str] = field(default_factory=list) types: List[str] = field(default_factory=list) + subgraphs: List[Subgraph] = field(default_factory=list) validate: bool = True @@ -153,4 +192,16 @@ def get_ignored_node_names_from_ignored_scope( ) nncf_logger.info(f"{len(matched_by_types)} ignored nodes were found by types in the NNCFGraph") - return set(matched_by_names + matched_by_types + matched_by_patterns) + matched_by_subgraphs = [] + if ignored_scope.subgraphs: + for subgraph in ignored_scope.subgraphs: + names_from_subgraph = get_ignored_node_names_from_subgraph(nncf_graph, subgraph) + if strict and not names_from_subgraph: + raise nncf.ValidationError( + f"Ignored subgraph with input names {subgraph.inputs} and output names {subgraph.outputs} " + "was not found in the NNCFGraph. " + error_msg + ) + + matched_by_subgraphs.extend(names_from_subgraph) + + return set(matched_by_names + matched_by_types + matched_by_patterns + matched_by_subgraphs) diff --git a/tests/common/test_ignored_scope.py b/tests/common/test_ignored_scope.py index 1bd35b47584..2a4c3a9364f 100644 --- a/tests/common/test_ignored_scope.py +++ b/tests/common/test_ignored_scope.py @@ -15,6 +15,7 @@ from nncf.common.graph.operator_metatypes import InputNoopMetatype from nncf.common.graph.operator_metatypes import OutputNoopMetatype from nncf.scopes import IgnoredScope +from nncf.scopes import Subgraph from nncf.scopes import get_ignored_node_names_from_ignored_scope from tests.common.quantization.metatypes import Conv2dTestMetatype from tests.common.quantization.metatypes import LinearTestMetatype @@ -59,6 +60,10 @@ def __init__(self, conv_metatype, linear_metatype): IgnoredScope(["/Conv_1_0", "/Linear_1_0"], [".*Marked.*"], [LINEAR_TYPE]), ["/Conv_1_0", "/Linear_1_0", "/Linear_2_0", "/Linear_3_0", "/Marked_Conv_3_0"], ), + ( + IgnoredScope(subgraphs=[Subgraph(inputs=["/Linear_1_0"], outputs=["/Linear_3_0"])]), + ["/Conv_2_0", "/Linear_1_0", "/Linear_2_0", "/Linear_3_0", "/Marked_Conv_3_0"], + ), ] @@ -73,6 +78,8 @@ def test_ignored_scopes(ignored_scope, ref_ignored_names): IgnoredScope(["/Conv_0_0", "/Conv_1_0", "/Linear_1_0"]), IgnoredScope(patterns=[".*Maarked.*"]), IgnoredScope(types=["wrong_type"]), + IgnoredScope(subgraphs=[Subgraph(inputs=["/Linear_1_0"], outputs=["/Linear_1_0"])]), + IgnoredScope(subgraphs=[Subgraph(inputs=["/Linear_3_0"], outputs=["/Linear_1_0"])]), ]