Skip to content

Commit 35697d2

Browse files
authored
Fix check in model collector for quantized FLN node (#1462)
To decide whether to collect statistics for a node inside FLN, it is not enough to check its quantization mode, but also needs to verify if it is supposed to be quantized based on the noe config in the fusing information.
1 parent ab5da23 commit 35697d2

8 files changed

Lines changed: 46 additions & 26 deletions

File tree

model_compression_toolkit/core/common/fusion/fusing_info.py

Lines changed: 29 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -39,13 +39,13 @@ class FusingInfo:
3939
- 'fusing_patterns': The patterns to generate the fused operators from.
4040
- 'manual_fused_ops': List of sequence of node names to handle as fused ops (even if they are not part of the fusing patterns).
4141
- `fusing_data`: A dictionary mapping fused operation IDs to lists of nodes that belong to that operation.
42-
- `node_to_fused_node_map`: A dictionary mapping each node name to the ID of the fused operation it belongs to.
42+
- `node_name_to_fused_op_id`: A dictionary mapping each node name to the ID of the fused operation it belongs to.
4343
4444
"""
4545
fusing_patterns: List[list[any]] = None
4646
manual_fused_ops: List[List[str]] = None
4747
fusing_data: Dict[str, Tuple['BaseNode']] = field(default_factory=dict)
48-
node_to_fused_node_map: Dict[str, str] = field(init=False, default_factory=dict)
48+
node_name_to_fused_op_id: Dict[str, str] = field(init=False, default_factory=dict)
4949
fused_op_id_to_quant_config: Dict[str, OpQuantizationConfig] = field(default_factory=dict)
5050

5151
def __post_init__(self):
@@ -64,10 +64,10 @@ def _init_node_mapping(self) -> None:
6464
"""
6565
Init the node-to-fused-node mapping based on the initial fusing data.
6666
"""
67-
self.node_to_fused_node_map.clear()
67+
self.node_name_to_fused_op_id.clear()
6868
for op_id, nodes in self.fusing_data.items():
6969
for node in nodes:
70-
self.node_to_fused_node_map[node.name] = op_id
70+
self.node_name_to_fused_op_id[node.name] = op_id
7171

7272
def get_manual_nodes_to_fuse(self) -> List[List[str]]:
7373
"""
@@ -115,7 +115,7 @@ def add_fused_operation(self, op_id: str, nodes: Tuple['BaseNode']) -> None:
115115
self.fusing_data[op_id] = nodes
116116
# Update the mapping for these nodes
117117
for node in nodes:
118-
self.node_to_fused_node_map[node.name] = op_id
118+
self.node_name_to_fused_op_id[node.name] = op_id
119119

120120
# Update the quantization config mapping for this operation
121121
if self.fusing_patterns is not None:
@@ -152,7 +152,7 @@ def remove_fused_operation(self, op_id: str) -> None:
152152
self._manual_fused_ops.remove(node_names)
153153

154154
for node in nodes:
155-
self.node_to_fused_node_map.pop(node.name, None)
155+
self.node_name_to_fused_op_id.pop(node.name, None)
156156
del self.fusing_data[op_id]
157157
self.fused_op_id_to_quant_config.pop(op_id, None)
158158

@@ -166,7 +166,7 @@ def get_fused_op_id_for_node(self, node_name: str) -> Optional[str]:
166166
Returns:
167167
The name of the fused node containing this node, or None if not fused.
168168
"""
169-
return self.node_to_fused_node_map.get(node_name)
169+
return self.node_name_to_fused_op_id.get(node_name)
170170

171171
def get_node_to_fused_node_map(self) -> Dict[str, str]:
172172
"""
@@ -175,7 +175,7 @@ def get_node_to_fused_node_map(self) -> Dict[str, str]:
175175
Returns:
176176
A dictionary mapping each original node name to its fused node name.
177177
"""
178-
return self.node_to_fused_node_map.copy()
178+
return self.node_name_to_fused_op_id.copy()
179179

180180
def get_fusing_quantization_config_map(self) -> Dict[str, OpQuantizationConfig]:
181181
"""
@@ -198,10 +198,12 @@ def get_fused_nodes(self, op_id: str) -> Optional[List['BaseNode']]:
198198
"""
199199
return self.fusing_data.get(op_id)
200200

201-
def get_nodes_to_disable_activation_quantization(self) -> List['BaseNode']:
201+
def get_inner_fln_nodes(self) -> List['BaseNode']:
202202
"""
203-
Returns a list of the nodes that their activation quantization is disabled due to fusing.
203+
Returns a list of the nodes that are part but not the last node of an FLN.
204204
"""
205+
# TODO: the order of the nodes is not gurenteed when returned as dict from get_all_fused_operations -
206+
# then, removing the last one can cause issues
205207
return [node for nodes in self.get_all_fused_operations().values() for node in nodes[:-1]]
206208

207209
def get_fused_op_quantization_config(self, op_id: str) -> OpQuantizationConfig:
@@ -228,6 +230,22 @@ def is_node_in_fused_op(self, node: 'BaseNode') -> bool:
228230
"""
229231
return any(node in nodes for nodes in self.fusing_data.values())
230232

233+
def is_quantized_node_in_fln(self, node: 'BaseNode') -> bool:
234+
"""
235+
Check whether a node inside an FLN and should be quantized.
236+
237+
Args:
238+
node (BaseNode): The node to check.
239+
240+
Returns:
241+
bool: True if the node is in any fused operation and should be quantized.
242+
"""
243+
if self.is_node_in_fused_op(node):
244+
node_q_cfg = self.fused_op_id_to_quant_config[self.node_name_to_fused_op_id[node.name]]
245+
return node_q_cfg is not None and node_q_cfg.enable_activation_quantization
246+
247+
return False
248+
231249
def get_all_fused_operations(self) -> Dict[str, Tuple['BaseNode']]:
232250
"""
233251
Retrieve fused information.
@@ -340,7 +358,7 @@ def __repr__(self) -> str:
340358
for op_id, nodes in self.fusing_data.items()
341359
)
342360
mapping_repr = ", ".join(
343-
f"{node} -> {op_id}" for node, op_id in self.node_to_fused_node_map.items()
361+
f"{node} -> {op_id}" for node, op_id in self.node_name_to_fused_op_id.items()
344362
)
345363
return (
346364
f"FusingInfo(\n"

model_compression_toolkit/core/common/graph/base_graph.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -908,7 +908,7 @@ def disable_fused_nodes_activation_quantization(self):
908908
Disable activation quantization for all nodes in fused operations,
909909
except for the last node in each fused group.
910910
"""
911-
nodes_to_disable = self.fusing_info.get_nodes_to_disable_activation_quantization()
911+
nodes_to_disable = self.fusing_info.get_inner_fln_nodes()
912912
for node in nodes_to_disable:
913913
for qc in node.candidates_quantization_cfg:
914914
qc.activation_quantization_cfg.quant_mode = ActivationQuantizationMode.FLN_QUANT

model_compression_toolkit/core/common/mixed_precision/resource_utilization_tools/resource_utilization_calculator.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -677,7 +677,7 @@ def _get_target_activation_nodes(self,
677677
elif target_criterion == TargetInclusionCriterion.AnyQuantizedNonFused:
678678
nodes = [n for n in nodes if n.is_activation_quantization_enabled() or n.is_quantization_preserving()]
679679
# remove fused nodes (due to SNC, where the non-linear is quantized, even though it should not be quantized)
680-
nodes = [n for n in nodes if n not in self.graph.fusing_info.get_nodes_to_disable_activation_quantization()]
680+
nodes = [n for n in nodes if n not in self.graph.fusing_info.get_inner_fln_nodes()]
681681
elif target_criterion == TargetInclusionCriterion.QNonConfigurable:
682682
nodes = [n for n in nodes if n.is_activation_quantization_enabled() and not n.has_configurable_activation()]
683683
elif target_criterion != TargetInclusionCriterion.Any: # pragma: no cover

model_compression_toolkit/core/common/model_collector.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,8 @@
3030

3131

3232
def create_stats_collector_for_node(node: common.BaseNode,
33-
fw_info: FrameworkInfo) -> BaseStatsCollector:
33+
fw_info: FrameworkInfo,
34+
quant_node_in_fln: bool) -> BaseStatsCollector:
3435
"""
3536
Gets a node and a groups list and create and return a statistics collector for a node
3637
according to whether its statistics should be collected and the prior information we
@@ -44,7 +45,7 @@ def create_stats_collector_for_node(node: common.BaseNode,
4445
Statistics collector for statistics collection for the node.
4546
"""
4647

47-
if node.is_activation_quantization_enabled() or node.is_fln_quantization():
48+
if node.is_activation_quantization_enabled() or quant_node_in_fln:
4849
min_output = getattr(node.prior_info, 'min_output', None)
4950
max_output = getattr(node.prior_info, 'max_output', None)
5051
stats_collector = common.StatsCollector(out_channel_axis=fw_info.out_channel_axis_mapping.get(node.type),
@@ -160,7 +161,8 @@ def __init__(self, graph: Graph,
160161

161162
# Assign statistics collectors to nodes
162163
for n in graph.get_topo_sorted_nodes():
163-
sc = create_stats_collector_for_node(n, fw_info=fw_info) # Get static collector for the node
164+
quant_node_in_fln = n.is_fln_quantization() and graph.fusing_info.is_quantized_node_in_fln(n)
165+
sc = create_stats_collector_for_node(n, fw_info=fw_info, quant_node_in_fln=quant_node_in_fln) # Get static collector for the node
164166
# If we use bias correction, and the node has kernel weights to quantize, we need to make sure
165167
# its previous nodes' tensors are consistent with this node.
166168
kernel_attr = fw_info.get_kernel_op_attributes(n.type)[0]

tests_pytest/_fw_tests_common_base/base_ru_integration_test.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313
# limitations under the License.
1414
# ==============================================================================
1515
import abc
16-
import copy
1716
from typing import Generator
1817
from unittest.mock import Mock
1918

@@ -245,7 +244,7 @@ def test_snc_fusing(self):
245244
f"{layer_names['act2']}": f"FusedNode_{layer_names['conv2']}_{layer_names['act2']}",
246245
f"{layer_names['conv3']}": f"FusedNode_{layer_names['conv3']}_{layer_names['act3']}",
247246
f"{layer_names['act3']}": f"FusedNode_{layer_names['conv3']}_{layer_names['act3']}"}
248-
assert graph.fusing_info.node_to_fused_node_map == expected_node_fuse_op
247+
assert graph.fusing_info.node_name_to_fused_op_id == expected_node_fuse_op
249248

250249
ru_calculator = ResourceUtilizationCalculator(graph, self.fw_impl, self.fw_info)
251250
ru_before_snc, detailed_ru_before_snc = ru_calculator.compute_resource_utilization(
@@ -277,7 +276,7 @@ def test_snc_fusing(self):
277276
f"{layer_names['conv3']}_pre_pad": f"FusedNode_{layer_names['conv3']}_pre_pad_{layer_names['conv3']}_{layer_names['act3']}",
278277
f"{layer_names['conv3']}": f"FusedNode_{layer_names['conv3']}_pre_pad_{layer_names['conv3']}_{layer_names['act3']}",
279278
f"{layer_names['act3']}": f"FusedNode_{layer_names['conv3']}_pre_pad_{layer_names['conv3']}_{layer_names['act3']}"}
280-
assert graph.fusing_info.node_to_fused_node_map == expected_node_fuse_op
279+
assert graph.fusing_info.node_name_to_fused_op_id == expected_node_fuse_op
281280

282281
linear_w_min_nbit, linear_a_min_nbit, default_w_nbits, default_a_nbit, binary_out_a_bit = nbits
283282

tests_pytest/_fw_tests_common_base/fusing/base_fusing_info_generator_test.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ def fused_graph(self, graph_with_fusion_metadata):
118118

119119
def test_expected_fusing_info(self, graph_with_fusion_metadata):
120120
actual_fi = graph_with_fusion_metadata.fusing_info
121-
assert self.expected_fi.node_to_fused_node_map == actual_fi.node_to_fused_node_map
121+
assert self.expected_fi.node_name_to_fused_op_id == actual_fi.node_name_to_fused_op_id
122122

123123
def test_expected_fused_graph(self, fused_graph):
124124
expected_fused_nodes = self.expected_fi.fusing_data

tests_pytest/_fw_tests_common_base/fusing/base_graph_with_fusing_metadata_test.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ def test_expected_fusing_info(self, graph_with_fusion_metadata):
106106
actual_fi = graph_with_fusion_metadata.fusing_info
107107
assert len(actual_fi.get_all_fused_operations()) == 2
108108
assert sorted(actual_fi.get_all_fused_operations().keys()) == ['FusedNode_conv_relu', 'FusedNode_linear_softmax']
109-
assert actual_fi.node_to_fused_node_map == {'conv': 'FusedNode_conv_relu',
109+
assert actual_fi.node_name_to_fused_op_id == {'conv': 'FusedNode_conv_relu',
110110
'relu': 'FusedNode_conv_relu',
111111
'linear': 'FusedNode_linear_softmax',
112112
'softmax': 'FusedNode_linear_softmax'}

tests_pytest/common_tests/unit_tests/test_model_collector.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ def test_create_stats_collector_for_node_activation_enabled(self, fw_info_mock):
5959
node.type = DummyLayer
6060
node.prior_info = Mock(min_output=-1, max_output=9)
6161

62-
collector = create_stats_collector_for_node(node, fw_info_mock)
62+
collector = create_stats_collector_for_node(node, fw_info_mock, False)
6363
assert isinstance(collector, StatsCollector)
6464

6565
def test_create_stats_collector_for_node_activation_disabled(self, fw_info_mock):
@@ -74,7 +74,7 @@ def test_create_stats_collector_for_node_activation_disabled(self, fw_info_mock)
7474
# Even if prior_info exists, it should not be used.
7575
node.prior_info = Mock(min_output=None, max_output=None)
7676

77-
collector = create_stats_collector_for_node(node, fw_info_mock)
77+
collector = create_stats_collector_for_node(node, fw_info_mock, False)
7878
assert isinstance(collector, NoStatsCollector)
7979

8080
def test_create_stats_collector_for_node_fln_activation_enabled(self, fw_info_mock):
@@ -91,7 +91,7 @@ def test_create_stats_collector_for_node_fln_activation_enabled(self, fw_info_mo
9191
node.type = DummyLayer
9292
node.prior_info = Mock(min_output=-8, max_output=9)
9393

94-
collector = create_stats_collector_for_node(node, fw_info_mock)
94+
collector = create_stats_collector_for_node(node, fw_info_mock, True)
9595
assert isinstance(collector, StatsCollector)
9696
assert collector.mpcc.init_min_value == -8
9797
assert collector.mpcc.init_max_value == 9
@@ -108,7 +108,7 @@ def test_create_stats_collector_for_node_fln_activation_enabled_no_prior_info(se
108108
node.type = DummyLayer
109109
node.prior_info = None
110110

111-
collector = create_stats_collector_for_node(node, fw_info_mock)
111+
collector = create_stats_collector_for_node(node, fw_info_mock, True)
112112
assert isinstance(collector, StatsCollector)
113113
assert collector.mpcc.init_min_value is None
114114
assert collector.mpcc.init_max_value is None
@@ -125,7 +125,7 @@ def test_create_stats_collector_for_node_fln_activation_enabled_no_prior_info_mo
125125
node.type = DummyLayer
126126
node.prior_info = Mock(min_output=None, max_output=None)
127127

128-
collector = create_stats_collector_for_node(node, fw_info_mock)
128+
collector = create_stats_collector_for_node(node, fw_info_mock, True)
129129
assert isinstance(collector, StatsCollector)
130130
assert collector.mpcc.init_min_value is None
131131
assert collector.mpcc.init_max_value is None
@@ -264,6 +264,7 @@ def test_assigns_stats_collectors_to_fln_quantization_nodes(self, fw_impl_mock,
264264
output_nodes=[OutTensor(node3, 0)],
265265
edge_list=[Edge(node1, node2, 0, 0), Edge(node2, node3, 0, 0)])
266266
graph.set_out_stats_collector_to_node = Mock(wraps=graph.set_out_stats_collector_to_node)
267+
graph.fusing_info.is_quantized_node_in_fln = lambda n: n.is_fln_quantization
267268

268269
# Simulate kernel attribute retrieval.
269270
fw_info_mock.get_kernel_op_attributes.return_value = [None]

0 commit comments

Comments
 (0)