195 lines
7.7 KiB
Python
195 lines
7.7 KiB
Python
# Copyright (C) 2018-2021 Intel Corporation
|
|
# SPDX-License-Identifier: Apache-2.0
|
|
|
|
import logging as log
|
|
from typing import Dict
|
|
|
|
import numpy as np
|
|
|
|
from extensions.middle.BinarizeWeightsM1P1 import BinarizeWeightsM1P1
|
|
from extensions.middle.MulFakeQuantizeFuse import resolve_shared_inputs
|
|
from mo.graph.graph import Graph, Node
|
|
from mo.middle.replacement import MiddleReplacementPattern
|
|
|
|
|
|
class ReluFakeQuantizeMark(MiddleReplacementPattern):
|
|
"""
|
|
This pass marks Relu operations that can be fused to FakeQuantize op with `removable_before_quantize` flag.
|
|
|
|
1. We count the number of Relu outputs that are Quantize and can absorb Relu (`quantized_to_fuse_count` attribute).
|
|
2. Relu is fusible if all its outputs can absorb it.
|
|
|
|
"""
|
|
enabled = True
|
|
graph_condition = [lambda graph: not graph.graph['cmd_params'].disable_fusing]
|
|
|
|
def run_after(self):
|
|
return [BinarizeWeightsM1P1]
|
|
|
|
def run_before(self):
|
|
from extensions.middle.SharedWeightsDuplication import SharedWeightsDuplication
|
|
return [SharedWeightsDuplication]
|
|
|
|
def pattern(self):
|
|
return dict(
|
|
nodes=[
|
|
('relu', dict(op='ReLU')),
|
|
('relu_d', dict()),
|
|
('quantize', dict(op='FakeQuantize')),
|
|
],
|
|
edges=[
|
|
('relu', 'relu_d'),
|
|
('relu_d', 'quantize', {'in': 0}),
|
|
]
|
|
)
|
|
|
|
def replace_pattern(self, graph: Graph, match: Dict[str, Node]):
|
|
relu = match['relu']
|
|
quantize = match['quantize']
|
|
|
|
if not relu.has_valid('quantized_to_fuse_count'):
|
|
relu['quantized_to_fuse_count'] = 0
|
|
|
|
if quantize.in_node(1).id == quantize.in_node(2).id:
|
|
# Provisional limitation that related to binary quantization
|
|
assert quantize.has_valid('levels') and quantize.levels == 2
|
|
|
|
threshold = quantize.in_port(1).data.get_value()
|
|
if threshold is None:
|
|
log.debug('ReluQuantizeFuse: cannot fuse because FakeQuantize op has dynamic input on the 1st port. '
|
|
'levels=`{}`'.format(quantize.levels))
|
|
return
|
|
|
|
relu['quantized_to_fuse_count'] += 1
|
|
|
|
else:
|
|
assert quantize.has_valid('levels') and quantize.levels != 2
|
|
min_value = quantize.in_port(1).data.get_value()
|
|
if min_value is None:
|
|
log.debug('ReluQuantizeFuse: cannot fuse because FakeQuantize op has dynamic input on the 1st port, '
|
|
'levels=`{}`'.format(quantize.levels))
|
|
return
|
|
if np.all(min_value >= 0):
|
|
relu['quantized_to_fuse_count'] += 1
|
|
|
|
relu['removable_before_quantize'] = relu['quantized_to_fuse_count'] == len(relu.out_port(0).get_destinations())
|
|
|
|
|
|
class ClampQuantizeMark(MiddleReplacementPattern):
|
|
"""
|
|
This pass marks Clamp operations that can be fused to FakeQuantize op with `removable_before_quantize` flag.
|
|
|
|
1. We count the number of Clamp outputs that are FakeQuantize and can absorb Clamp (`quantized_to_fuse_count` attribute)
|
|
2. Clamp is fusible if all its outputs can absorb it.
|
|
|
|
"""
|
|
enabled = True
|
|
|
|
def run_after(self):
|
|
return [BinarizeWeightsM1P1]
|
|
|
|
def run_before(self):
|
|
from extensions.middle.SharedWeightsDuplication import SharedWeightsDuplication
|
|
return [SharedWeightsDuplication]
|
|
|
|
def pattern(self):
|
|
return dict(
|
|
nodes=[
|
|
('clamp', dict(op='Clamp')),
|
|
('clamp_d', dict()),
|
|
('quantize', dict(op='FakeQuantize')),
|
|
],
|
|
edges=[
|
|
('clamp', 'clamp_d'),
|
|
('clamp_d', 'quantize', {'in': 0}),
|
|
]
|
|
)
|
|
|
|
def replace_pattern(self, graph: Graph, match: Dict[str, Node]):
|
|
clamp = match['clamp']
|
|
quantize = match['quantize']
|
|
clamp_min = clamp.in_port(1).data.get_value()
|
|
clamp_max = clamp.in_port(2).data.get_value()
|
|
if clamp_min is None or clamp_max is None:
|
|
log.debug('ReluQuantizeFuse: cannot fuse because Clamp op has dynamic input on the 1st or 2nd port')
|
|
return
|
|
|
|
if not clamp.has_valid('quantized_to_fuse_count'):
|
|
clamp['quantized_to_fuse_count'] = 0
|
|
|
|
if quantize.in_node(1).id == quantize.in_node(2).id:
|
|
# Binary case is not tested so we won't fuse Clamp
|
|
assert quantize.has_valid('levels') and quantize.levels == 2
|
|
clamp['removable_before_quantize'] = False
|
|
else:
|
|
assert quantize.has_valid('levels') and quantize.levels != 2
|
|
min_value = quantize.in_port(1).data.get_value()
|
|
if min_value is None:
|
|
log.debug('ReluQuantizeFuse: cannot fuse because FakeQuantize op has dynamic input on the 1st port, '
|
|
'levels=`{}`'.format(quantize.levels))
|
|
return
|
|
max_value = quantize.in_port(2).data.get_value()
|
|
if max_value is None:
|
|
log.debug('ReluQuantizeFuse: cannot fuse because FakeQuantize op has dynamic input on the 2nd port, '
|
|
'levels=`{}`'.format(quantize.levels))
|
|
return
|
|
if np.all(min_value >= clamp_min) and np.all(max_value <= clamp_max):
|
|
clamp['quantized_to_fuse_count'] += 1
|
|
|
|
clamp['removable_before_quantize'] = clamp['quantized_to_fuse_count'] == len(clamp.out_port(0).get_destinations())
|
|
|
|
|
|
class ReluQuantizeFuse(MiddleReplacementPattern):
|
|
""" Fuses ReLU --> FakeQuantize sequence if possible
|
|
|
|
Relu --> FakeQuantize fusion is possible if:
|
|
1. Relu is consumed to 0-th port of FakeQuantize
|
|
2. FakeQuantize ports 1 and 2 defines such input range that 0 is not included
|
|
"""
|
|
enabled = True
|
|
|
|
def run_after(self):
|
|
return [ReluFakeQuantizeMark]
|
|
|
|
def run_before(self):
|
|
from extensions.middle.SharedWeightsDuplication import SharedWeightsDuplication
|
|
return [SharedWeightsDuplication]
|
|
|
|
def pattern(self):
|
|
return dict(
|
|
nodes=[
|
|
('relu', dict(removable_before_quantize=True)),
|
|
('relu_d', dict()),
|
|
('quantize', dict(op='FakeQuantize')),
|
|
],
|
|
edges=[
|
|
('relu', 'relu_d'),
|
|
('relu_d', 'quantize', {'in': 0}),
|
|
]
|
|
)
|
|
|
|
def replace_pattern(self, graph: Graph, match: dict):
|
|
quantize = match['quantize']
|
|
|
|
if quantize.levels == 2:
|
|
# extra logic due to special 1 & 2 port input meaning in binary case - it is threshold separating two quants
|
|
threshold = quantize.in_port(1).data.get_value()
|
|
|
|
# Direct modifications to quantize 1-st port input are performed.
|
|
# So the data node at this input shouldn't have more than 1 consumer maximum 2 consumers to the same
|
|
# quantize op (consumed by 1st and 2nd ports). So we duplicate FakeQuantize in_port 1 data if needed
|
|
resolve_shared_inputs(node=quantize, port_ids_to_duplicate=[1])
|
|
|
|
# As we restricted to binarization case only, so we need to detect from
|
|
# which side of 0 FakeQuantize threshold resides:
|
|
# if the threshold > 0, it remains the same;
|
|
# if the threshold == 0, it also remains the same;
|
|
# if the threshold < 0, it should be modified to -infinity that means that all inputs map to output_high
|
|
modification_mask = threshold < 0
|
|
threshold[modification_mask] = float('-inf')
|
|
|
|
# Reconnect ReLU as it no longer needed for current FakeQuantize
|
|
in_relu_connection = quantize.in_port(0).get_source().node.in_port(0).get_connection()
|
|
quantize.in_port(0).disconnect()
|
|
in_relu_connection.add_destination(quantize.in_port(0))
|