chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 12:40:42 +08:00
commit e25996e7db
15472 changed files with 3536181 additions and 0 deletions
@@ -0,0 +1,45 @@
# Copyright (c) 2022 PaddlePaddle Authors. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from .post_training_quantization import ( # noqa: F401
PostTrainingQuantization,
PostTrainingQuantizationProgram,
WeightQuantization,
)
from .quant2_int8_onednn_pass import ( # noqa: F401
Quant2Int8MkldnnPass,
Quant2Int8OnednnPass,
)
from .quant_int8_onednn_pass import ( # noqa: F401
QuantInt8MkldnnPass,
QuantInt8OnednnPass,
)
from .quanter import ( # noqa: F401
convert,
quant_aware,
)
from .quantization_pass import ( # noqa: F401
AddQuantDequantForInferencePass,
AddQuantDequantPass,
AddQuantDequantPassV2,
ConvertToInt8Pass,
OutScaleForInferencePass,
OutScaleForTrainingPass,
QuantizationFreezePass,
QuantizationTransformPass,
QuantizationTransformPassV2,
QuantWeightPass,
ReplaceFakeQuantDequantPass,
TransformForMobilePass,
)
@@ -0,0 +1,377 @@
# Copyright (c) 2022 PaddlePaddle Authors. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import logging
import sys
import time
import numpy as np
import paddle
from paddle import static
from ..log_helper import get_logger
from .utils import (
_channelwise_quant_axis1_ops,
bias_correction_w,
calculate_quant_cos_error,
dequant_tensor,
load_variable_data,
quant_tensor,
set_variable_data,
stable_sigmoid,
)
_logger = get_logger(
__name__, logging.INFO, fmt='%(asctime)s-%(levelname)s: %(message)s'
)
GAMMA = -0.1
ZETA = 1.1
def compute_soft_rounding(alpha_v):
return paddle.clip(
paddle.nn.functional.sigmoid(alpha_v) * (ZETA - GAMMA) + GAMMA,
min=0,
max=1,
)
def compute_soft_rounding_np(alpha_v):
return np.clip(
stable_sigmoid(alpha_v) * (ZETA - GAMMA) + GAMMA, a_min=0, a_max=1
)
class AdaRoundLoss:
def __init__(self, reg_param=0.01, default_beta_range=(20, 2)):
self.default_reg_param = reg_param
self.default_beta_range = default_beta_range
def compute_recon_loss(self, ada_quantized_output, orig_output):
square_cost = paddle.nn.functional.square_error_cost(
ada_quantized_output, orig_output
)
recon_loss = paddle.mean(paddle.sum(square_cost, axis=-1))
return recon_loss
def compute_round_loss(self, alpha_v, warm_start, beta):
def round_loss_fn():
# compute rectified sigmoid of parameter 'alpha' which maps it between zero and one
h_v = compute_soft_rounding(alpha_v)
# calculate regularization term - which ensures parameter to converge to exactly zeros and ones
# at the end of optimization
reg_term = paddle.sum(
-paddle.pow(paddle.abs(2 * h_v - 1), beta) + 1
)
# calculate the rounding loss
round_loss = self.default_reg_param * reg_term
return round_loss
round_loss = static.nn.cond(
warm_start,
lambda: paddle.full(shape=[1], dtype='float32', fill_value=0.0),
round_loss_fn,
)
return round_loss
def compute_beta(self, max_iter, cur_iter, warm_start):
# Start and stop beta for annealing of rounding loss (start_beta, end_beta)
start_beta, end_beta = self.default_beta_range
# iteration at end of warm start period, which is 20% of max iterations
warm_start_end_iter = warm_start * max_iter
# compute relative iteration of current iteration
rel_iter = (cur_iter - warm_start_end_iter) / (
max_iter - warm_start_end_iter
)
beta = end_beta + 0.5 * (start_beta - end_beta) * (
1 + np.cos(rel_iter * np.pi)
)
return beta
class AdaRound:
def __init__(
self,
scale,
weight_tensor,
scope=None,
weight_var_name=None,
weight_op_type=None,
is_train=True,
num_iterations=1000,
):
self.is_train = is_train
self.num_iterations = num_iterations
self.warm_start = 0.1
self.weight_bits = 8
self.offset = 0.0 # zero-point offset
self.adaround_loss = AdaRoundLoss()
self.ori_weight_tensor = weight_tensor
self.scale = scale
self.scope = scope
self.quant_axis = 0
if weight_op_type in _channelwise_quant_axis1_ops:
self.quant_axis = 1
self.weight_var_name = weight_var_name
self.alpha_name = weight_var_name + ".alpha"
self.initialize_alpha(weight_tensor.copy(), scale, weight_var_name)
def initialize_alpha(self, tensor, scale, var_name):
"""
Initializes alpha parameter, same shape as the weight tensor
"""
tensor_scale = quant_tensor(tensor, scale, quant_axis=self.quant_axis)
tensor_floor = np.floor(tensor_scale)
tensor = tensor_scale - tensor_floor
alpha = -np.log((ZETA - GAMMA) / (tensor - GAMMA) - 1)
self.alpha_v = paddle.create_parameter(
shape=alpha.shape,
dtype="float32",
name=var_name + ".alpha",
default_initializer=paddle.nn.initializer.Assign(alpha),
)
def _calculate_output_with_adarounded_weights(
self, program, place, exe, data, fp32_fetch_list, weight_tensor_dequant
):
set_variable_data(
self.scope, place, self.weight_var_name, weight_tensor_dequant
)
adaround_out_tensor = exe.run(
program=program,
feed=data,
fetch_list=[fp32_fetch_list],
return_numpy=True,
scope=self.scope,
)
return adaround_out_tensor
def _calculate_quant_weight(self):
np_alpha = load_variable_data(self.scope, self.alpha_name)
h_alpha = compute_soft_rounding_np(np_alpha)
# Scale the tensor
tensor_scale = quant_tensor(
self.ori_weight_tensor.copy(),
self.scale,
quant_axis=self.quant_axis,
)
weight_tensor = np.floor(tensor_scale)
# Adaround the tensor
weight_tensor_quant = np.add(weight_tensor, h_alpha)
return weight_tensor_quant
def _calculate_adarounded_weights(self):
weight_tensor_quant = self._calculate_quant_weight()
# Dequantize the tensor
weight_tensor_dequant = dequant_tensor(
weight_tensor_quant + self.offset,
self.scale,
quant_axis=self.quant_axis,
)
return weight_tensor_dequant
def update_final_weights(self):
weight_tensor_quant = self._calculate_quant_weight()
return weight_tensor_quant
def get_loss(self, beta, warm_start, adaround_out_tensor, orig_out_tensor):
round_loss = self.adaround_loss.compute_round_loss(
self.alpha_v, warm_start, beta
)
recon_loss = self.adaround_loss.compute_recon_loss(
adaround_out_tensor, orig_out_tensor
)
loss = round_loss + recon_loss
losses = {
'loss': loss,
'round_loss': round_loss,
'recon_loss': recon_loss,
}
return losses
def update_beta_warm(self, cur_iteration):
warm_start = cur_iteration < self.num_iterations * self.warm_start
beta = self.adaround_loss.compute_beta(
self.num_iterations, cur_iteration, self.warm_start
)
return beta, warm_start
def run_adaround(
data_loader,
fp32_program,
fetch_list,
exe,
scope,
place,
quantized_op_pairs,
weight_op_pairs,
scale_dict,
num_iterations=1000,
lr=0.001,
bias_correction=False,
fast_mode=True,
):
fetch_op_name = fetch_list[0].name
final_weight_tensor_quant_dict = {}
for weight_var_name, quant_op_out_name in quantized_op_pairs.items():
_logger.info(f'Start adaround op: {weight_var_name}')
weight_op_type = weight_op_pairs[weight_var_name]
# get scale and weight tensor
weight_var_tensor = load_variable_data(scope, weight_var_name)
scale = scale_dict[weight_var_name]
fp32_fetch_list = None
for _op in fp32_program.global_block().ops:
if _op.type == "fetch":
_op._rename_input(fetch_op_name, quant_op_out_name)
fp32_fetch_list = fp32_program.global_block().var(
quant_op_out_name
)
fetch_op_name = quant_op_out_name
# build adaround program
startup_program = static.Program()
train_program = static.Program()
with (
static.program_guard(train_program, startup_program),
paddle.utils.unique_name.guard(),
):
# initialize adaround
adaround = AdaRound(
scale,
weight_var_tensor,
scope=scope,
weight_var_name=weight_var_name,
weight_op_type=weight_op_type,
num_iterations=num_iterations,
)
orig_out_tensor = static.data(
name='orig_out_tensor',
shape=(-1, *fp32_fetch_list.shape),
dtype='float32',
)
adaround_out_tensor = static.data(
name='adaround_out_tensor',
shape=(-1, *fp32_fetch_list.shape),
dtype='float32',
)
beta_tensor = static.data(
name='beta', shape=[-1, 1], dtype='float32'
)
warm_start_tensor = static.data(
name='warm_start', shape=[-1, 1], dtype='bool'
)
train_fetches_loss = adaround.get_loss(
beta_tensor,
warm_start_tensor,
adaround_out_tensor,
orig_out_tensor,
)
optimizer = paddle.optimizer.Adam(learning_rate=lr)
loss = train_fetches_loss['loss']
optimizer.minimize(loss)
exe.run(startup_program)
start_time = time.time()
prev_start_time = start_time
for i, data in enumerate(data_loader()):
prev_start_time = start_time
start_time = time.time()
# run fp32 model
np_orig_out_tensor = exe.run(
program=fp32_program,
feed=data,
fetch_list=[fp32_fetch_list],
return_numpy=True,
scope=scope,
)
adaround_weight_tensor_dequant = (
adaround._calculate_adarounded_weights()
)
np_adaround_out_tensor = (
adaround._calculate_output_with_adarounded_weights(
fp32_program,
place,
exe,
data,
fp32_fetch_list,
adaround_weight_tensor_dequant,
)
)
# If the cosine distance of the two tensor is small, skip training
cos_error = calculate_quant_cos_error(
np_orig_out_tensor[0], np_adaround_out_tensor[0]
)
if fast_mode and cos_error > 0.99:
_logger.info("The cosine error is small, skip training.")
break
beta, warm_start = adaround.update_beta_warm(i)
feed_dict = {
'orig_out_tensor': np_orig_out_tensor[0],
'adaround_out_tensor': np_adaround_out_tensor[0],
'beta': beta,
'warm_start': warm_start,
}
out = exe.run(
train_program,
feed=feed_dict,
fetch_list=[v.name for v in train_fetches_loss.values()],
return_numpy=True,
)
_logger.info(
f"Iter {i:d}, lr {lr:.5f}, loss {np.mean(out[0]):.5f}, loss_round {np.mean(out[1]):.5f}, loss_recon {np.mean(out[2]):.5f}, time {start_time - prev_start_time:.5f}s"
)
sys.stdout.flush()
if i == num_iterations:
break
final_weight_tensor_quant_dict[weight_var_name] = (
adaround.update_final_weights()
)
if bias_correction:
final_weight_tensor_quant_dict[weight_var_name] = bias_correction_w(
weight_var_tensor,
final_weight_tensor_quant_dict[weight_var_name],
scale,
adaround.quant_axis,
weight_bits=adaround.weight_bits,
)
del adaround
# update adarounded calibrated weights
for weight_var_name in quantized_op_pairs.keys():
set_variable_data(
scope,
place,
weight_var_name,
final_weight_tensor_quant_dict[weight_var_name],
)
@@ -0,0 +1,144 @@
# Copyright (c) 2022 PaddlePaddle Authors. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import logging
import math
import numpy as np
from ..log_helper import get_logger
_logger = get_logger(
__name__, logging.INFO, fmt='%(asctime)s-%(levelname)s: %(message)s'
)
def expand_quantized_bins(quantized_bins, reference_bins):
'''
Expand hist bins.
'''
expanded_quantized_bins = [0] * len(reference_bins)
num_merged_bins = int(len(reference_bins) / len(quantized_bins))
j_start = 0
j_end = num_merged_bins
for idx in range(len(quantized_bins)):
zero_count = reference_bins[j_start:j_end].count(0)
num_merged_bins = j_end - j_start
if zero_count == num_merged_bins:
avg_bin_ele = 0
else:
avg_bin_ele = quantized_bins[idx] / (
num_merged_bins - zero_count + 0.0
)
for idx1 in range(j_start, j_end):
expanded_quantized_bins[idx1] = (
0 if reference_bins[idx1] == 0 else avg_bin_ele
)
j_start += num_merged_bins
j_end += num_merged_bins
if (idx + 1) == len(quantized_bins) - 1:
j_end = len(reference_bins)
return expanded_quantized_bins
def safe_entropy(reference_distr_P, P_sum, candidate_distr_Q, Q_sum):
'''
Calculate the entropy.
'''
assert len(reference_distr_P) == len(candidate_distr_Q)
tmp_sum1 = 0
tmp_sum2 = 0
for idx in range(len(reference_distr_P)):
p_idx = reference_distr_P[idx]
q_idx = candidate_distr_Q[idx]
if p_idx == 0:
tmp_sum1 += 0
tmp_sum2 += 0
else:
if q_idx == 0:
_logger.error(
"Fatal error!, idx = "
+ str(idx)
+ " qindex = 0! p_idx = "
+ str(p_idx)
)
tmp_sum1 += p_idx * (math.log(Q_sum * p_idx))
tmp_sum2 += p_idx * (math.log(P_sum * q_idx))
return (tmp_sum1 - tmp_sum2) / P_sum
def cal_kl_threshold(hist, bin_width, bits):
'''
Using the KL-divergence method to get the more precise threshold.
Args:
hist(List): The hist of the tensor.
bin_width(float): The bin width for the hist.
bits(int): The quantization bits.
'''
assert hist.ndim == 1
hist_bins = hist.shape[0]
starting_iter = int((hist_bins - 1) * 0.5)
quant_range = 2 ** (bits - 1) - 1
P_sum = np.sum(np.array(hist).ravel())
min_kl_divergence = 0
min_kl_index = 0
kl_inited = False
for i in range(starting_iter, hist_bins):
reference_distr_P = hist[0:i].tolist()
outliers_count = sum(hist[i:])
if reference_distr_P[i - 1] == 0:
continue
reference_distr_P[i - 1] += outliers_count
reference_distr_bins = reference_distr_P[:]
candidate_distr_Q = hist[0:i].tolist()
num_merged_bins = int(i / quant_range)
candidate_distr_Q_quantized = [0] * quant_range
j_start = 0
j_end = num_merged_bins
for idx in range(quant_range):
candidate_distr_Q_quantized[idx] = sum(
candidate_distr_Q[j_start:j_end]
)
j_start += num_merged_bins
j_end += num_merged_bins
if (idx + 1) == quant_range - 1:
j_end = i
candidate_distr_Q = expand_quantized_bins(
candidate_distr_Q_quantized, reference_distr_bins
)
Q_sum = sum(candidate_distr_Q)
kl_divergence = safe_entropy(
reference_distr_P, P_sum, candidate_distr_Q, Q_sum
)
if not kl_inited:
min_kl_divergence = kl_divergence
min_kl_index = i
kl_inited = True
elif kl_divergence < min_kl_divergence:
min_kl_divergence = kl_divergence
min_kl_index = i
else:
pass
if min_kl_index == 0:
while starting_iter > 0:
if hist[starting_iter] == 0:
starting_iter -= 1
continue
else:
break
min_kl_index = starting_iter
return (min_kl_index + 0.5) * bin_width
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,736 @@
# Copyright (c) 2022 PaddlePaddle Authors. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import numpy as np
from paddle.utils import deprecated
from ...base.framework import IrGraph
from ...framework import _get_paddle_place, core
OpRole = core.op_proto_and_checker_maker.OpRole
class Quant2Int8OnednnPass:
"""
Transform a quant model IrGraph into MKL-DNN supported INT8 IrGraph.
The pass consists of the following transformations:
1. gather scale values from fake quantize/dequantize operators,
2. extract FP32 inference model graph from the quant graph, i.e.
a. remove fake quantize/dequantize operators,
b. dequantize conv2d and mul's weights,
3. optimize the FP32 graph using standard FP32 optimization fuses
(e.g. `conv2d`+`bn` -> `conv2d`),
4. quantize the optimized FP32 graph using standard INT8v2 quantization
passes (`cpu_quantize_pass`, `cpu_quantize_squash_pass`).
"""
def __init__(
self,
_ops_to_quantize,
_op_ids_to_skip=None,
_scope=None,
_place=None,
_core=None,
_debug=False,
):
self._scope = _scope
self._place = _get_paddle_place(_place)
self._core = _core
self._debug = _debug
self._fake_quantize_types = [
'fake_quantize_moving_average_abs_max',
'fake_quantize_range_abs_max',
]
self._fake_dequantize_types = [
'fake_dequantize_max_abs',
'fake_channel_wise_dequantize_max_abs',
]
self._fake_quantize_dequantize_types = [
'fake_quantize_dequantize_abs_max',
'fake_quantize_dequantize_moving_average_abs_max',
'fake_channel_wise_quantize_dequantize_abs_max',
]
self._ops_to_quantize = _ops_to_quantize
self._op_ids_to_skip = (
_op_ids_to_skip if _op_ids_to_skip is not None else {-1}
)
self._scale_immutable_ops = [
'transpose2',
'reshape2',
'pool2d',
'slice',
'shape',
'nearest_interp',
'nearest_interp_v2',
'split',
]
self._scale_ops = ['scale']
self._conv_ops = ['conv2d', 'depthwise_conv2d']
self._pool_ops = ['pool2d']
self._mul_ops = ['mul']
self._fc_ops = ['fc']
self._relu_ops = ['relu', 'relu6']
self._matmul_ops = ['matmul', 'matmul_v2']
self._gru_ops = ['fusion_gru', 'multi_gru']
self._lstm_ops = ['fusion_lstm']
self._weight_thresholds = {}
# Collect the Input and Output scales from Fake quant models
self._var_quant_scales = {}
self._max_range = {}
self._s8_max = 127
self._pass_idx = 0
self._pass_group = 'int8'
def apply(self, graph):
assert isinstance(graph, IrGraph), (
'graph must be the instance of IrGraph.'
)
self._reset_pass_idx_and_group('int8')
graph = self._label_skip_quantized_op(graph)
graph = self._gather_weight_thresholds_from_fake(graph)
graph = self._gather_input_scales_from_fake(graph)
graph = self._gather_output_scales_from_attr(graph)
graph = self._remove_fake_ops(graph)
graph = self._dequantize_weights(graph)
graph = self._optimize_fp32_graph(graph)
graph = self._compute_weight_scales(graph)
# This function causes nondeterministic quantization behavior
# graph = self._update_relu_output_scales(graph)
graph = self._propagate_scales(graph)
graph = self._quantize_fp32_graph(graph)
graph = self._cleanup(graph)
return graph
def prepare_and_optimize_fp32(self, graph):
assert isinstance(graph, IrGraph), (
'graph must be the instance of IrGraph.'
)
self._reset_pass_idx_and_group('fp32')
graph = self._optimize_fp32_graph(graph)
graph = self._cleanup(graph)
return graph
def _reset_pass_idx_and_group(self, group):
self._pass_idx = 0
self._pass_group = group
def _convert_scale2tensor(self, scale):
tensor = core.DenseTensor()
tensor.set(scale, core.CPUPlace())
return tensor
def _is_quantizing_all_ops(self):
return len(self._ops_to_quantize) == 0
def _is_any_of_op_types_in_graph(self, op_types, graph):
return any(op.name() in op_types for op in graph.all_op_nodes())
def _is_any_of_op_types_quantized(self, op_types, graph):
return self._is_any_of_op_types_in_graph(op_types, graph) and (
self._is_quantizing_all_ops()
or any(op_type in self._ops_to_quantize for op_type in op_types)
)
def _is_conv_quantized(self, graph):
return self._is_any_of_op_types_quantized(self._conv_ops, graph)
def _is_fc_quantized(self, graph):
return self._is_any_of_op_types_quantized(self._fc_ops, graph)
def _label_skip_quantized_op(self, graph):
"""
For some ops(conv2d, depthwise_conv2d, mul, matmul), find and label
the skip quantized ops. cpu_quantize_placement_pass will use the
label to identify it.
For static models, the skip quantized ops have `skip_quant` attr.
Therefore, it only needs to find and label the skip quantized ops for
dygraph models, in which the quantized ops don't have `quantization_type`
attr.
"""
target_ops = self._conv_ops + self._mul_ops + self._matmul_ops
for op_node in graph.all_op_nodes():
if op_node.name() in target_ops and not op_node.op().has_attr(
"quantization_type"
):
is_quantized_op = True
for var_node in op_node.inputs:
for front_op_node in var_node.inputs:
if "quantize" not in front_op_node.name():
is_quantized_op = False
if not is_quantized_op:
op_node.op()._set_attr("skip_quant", True)
return graph
def _add_scale_for_vars(self, var_names, use_unsigned_int, lod_tensor):
"""
Save quantization scales for variables. Do not overwrite.
"""
scales = self._var_quant_scales
for var_name in var_names:
if var_name not in scales:
scales[var_name] = (use_unsigned_int, lod_tensor)
def _gather_input_scales_from_fake(self, graph):
# fake_quantize_dequantize_abs_max doesn't have scale value
fake_ops = ['fake_quantize_dequantize_moving_average_abs_max']
fake_ops.extend(self._fake_quantize_types)
for op in graph.all_op_nodes():
if op.name() in fake_ops:
bit_length = op.op().attr("bit_length")
assert bit_length == 8, (
f'Unsupported number quantization bits ({bit_length}). Only 8 is supported now.'
)
input_name = op.input("X")[0]
scale_name = op.input("InScale")[0]
output_name = op.output("Out")[0]
# Gather new weight scales after folding batchnorm in convolution
scale = np.array(
1.0 / self._load_param(self._scope, scale_name)[0]
).astype(np.float64)
scale[scale == np.inf] = 0.0
lod_tensor = self._convert_scale2tensor(scale)
use_unsigned_int = False
self._add_scale_for_vars(
[input_name, output_name], use_unsigned_int, lod_tensor
)
return graph
def _gather_weight_thresholds_from_fake(self, graph):
for op in graph.all_op_nodes():
if op.name() in self._fake_dequantize_types:
input_name = op.input("X")[0]
if op.op().has_attr("max_range"):
_max_range = np.array(op.op().attr("max_range")).astype(
np.float64
)
self._weight_thresholds[input_name] = np.array(
self._s8_max * self._s8_max / _max_range
).astype(np.float64)
else:
scale_name = op.input("Scales")[0]
self._weight_thresholds[input_name] = np.array(
self._load_param(self._scope, scale_name)
).astype(np.float64)
return graph
def _gather_output_scales_from_attr(self, graph):
for op in graph.all_op_nodes():
if op.op().has_attr("out_threshold"):
attr_scale = op.op().attr("out_threshold")
if attr_scale == 0.0:
continue
scale = np.array(1.0 / attr_scale).astype(np.float64)
scale[scale == np.inf] = 0.0
scale_lod_tensor = self._convert_scale2tensor(scale)
use_unsigned_int = False
for output_name in op.op().outputs():
for out_var_name in op.op().output(output_name):
self._add_scale_for_vars(
[out_var_name], use_unsigned_int, scale_lod_tensor
)
return graph
def _propagate_scales(self, graph):
def _update_scale_op_in_scale(op, input, output):
unsigned, tensor = self._var_quant_scales[output]
scale = np.array(tensor) * op.op().attr("scale")
new_tensor = self._convert_scale2tensor(scale.astype(np.float64))
self._var_quant_scales[input] = (unsigned, new_tensor)
def _update_scales(graph):
waiting_for_scale = set()
for op in graph.all_op_nodes():
if op.name() in self._scale_immutable_ops:
if op.name() == 'slice' or op.name() == 'shape':
input_name = op.input("Input")[0]
else:
input_name = op.input("X")[0]
output_name = op.output("Out")[0]
tensor_names = [input_name, output_name]
if all(
name not in self._var_quant_scales
for name in tensor_names
):
waiting_for_scale.update(tensor_names)
continue
elif input_name in self._var_quant_scales:
self._var_quant_scales[output_name] = (
self._var_quant_scales[input_name]
)
elif output_name in self._var_quant_scales:
self._var_quant_scales[input_name] = (
self._var_quant_scales[output_name]
)
elif op.name() == 'concat':
output_name = op.output("Out")[0]
if output_name in self._var_quant_scales:
input_names = op.input("X")
for input_name in input_names:
self._var_quant_scales[input_name] = (
self._var_quant_scales[output_name]
)
elif op.name() in self._scale_ops:
input_name = op.input("X")[0]
output_name = op.output("Out")[0]
if output_name in self._var_quant_scales:
_update_scale_op_in_scale(op, input_name, output_name)
return waiting_for_scale
waiting_for_scale = _update_scales(graph)
waiting_for_scale_prev = set()
while (
len(waiting_for_scale) != 0
and waiting_for_scale != waiting_for_scale_prev
):
waiting_for_scale_prev = waiting_for_scale
waiting_for_scale = _update_scales(graph)
return graph
def _load_param(self, scope, param_name):
return np.array(scope.find_var(param_name).get_tensor())
def _remove_fake_ops(self, graph):
for op in graph.all_op_nodes():
if op.name() in self._fake_quantize_types:
self._remove_fake_quantize(graph, op)
elif op.name() in self._fake_dequantize_types:
self._remove_fake_dequantize(graph, op)
elif op.name() in self._fake_quantize_dequantize_types:
self._remove_fake_dequantize(graph, op)
return graph
def _remove_fake_quantize(self, graph, op):
fake_quant_in = graph._find_node_by_name(op.inputs, op.input("X")[0])
fake_quant_in_scale = graph._find_node_by_name(
op.inputs, op.input("InScale")[0]
)
fake_quant_out = graph._find_node_by_name(
op.outputs, op.output("Out")[0]
)
fake_quant_out_scale = graph._find_node_by_name(
op.outputs, op.output("OutScale")[0]
)
next_ops = fake_quant_out.outputs
for next_op in next_ops:
self._swap_inputs(next_op, fake_quant_out, fake_quant_in)
graph.link_to(fake_quant_in, next_op)
graph.safe_remove_nodes(
{op, fake_quant_in_scale, fake_quant_out, fake_quant_out_scale}
)
return graph
def _remove_fake_dequantize(self, graph, op):
fake_dequant_in = graph._find_node_by_name(op.inputs, op.input("X")[0])
fake_dequant_out = graph._find_node_by_name(
op.outputs, op.output("Out")[0]
)
next_ops = fake_dequant_out.outputs
for next_op in next_ops:
self._swap_inputs(next_op, fake_dequant_out, fake_dequant_in)
graph.link_to(fake_dequant_in, next_op)
graph.safe_remove_nodes({op, fake_dequant_out})
return graph
def _swap_inputs(self, op, old_input, new_input):
for input_name in op.op().input_names():
if old_input.name() in op.input(input_name):
op.op().set_input(
input_name,
[
new_input.name() if x == old_input.name() else x
for x in op.input(input_name)
],
)
def _dequantize_weights(self, graph):
def _is_int8_weights(op_node, weight_name):
weight_var_name = op_node.input(weight_name)[0]
if self._scope.find_var(weight_var_name) is None:
return False
weight = self._load_param(self._scope, weight_var_name)
return np.all(np.mod(weight, 1) == 0)
mul_and_matmul_ops = self._mul_ops + self._matmul_ops
for op in graph.all_op_nodes():
if op.name() in self._conv_ops and _is_int8_weights(op, "Filter"):
self._dequantize_op_weights(graph, op, "Filter", "Output")
elif op.name() in mul_and_matmul_ops and _is_int8_weights(op, "Y"):
self._dequantize_op_weights(graph, op, "Y", "Out")
return graph
def _dequantize_op_weights(self, graph, op_node, weight_name, output_name):
weight_var_name = op_node.input(weight_name)[0]
output_var_name = op_node.output(output_name)[0]
# Convert int8 range weights to fp32 range weights
scales = self._weight_thresholds[output_var_name]
weight = self._load_param(self._scope, weight_var_name)
if scales.size == 1 or scales.size == weight.shape[0]:
w_fp32 = np.multiply(np.divide(weight, self._s8_max).T, scales.T).T
elif len(weight.shape) > 1 and scales.size == weight.shape[1]:
w_fp32 = np.multiply(np.divide(weight, self._s8_max), scales)
else:
raise ValueError(
f"The size of weight scales vector ({scales.size}) does not match the dimensions ({weight.shape}) of the weights tensor {weight_var_name}."
)
w_fp32 = w_fp32.reshape(weight.shape).astype(np.float32)
self._restore_var(weight_var_name, w_fp32)
def _restore_var(self, name, array):
tensor = self._scope.find_var(name).get_tensor()
tensor.set(array, self._place)
def _update_activations(self, graph):
for op in graph.all_op_nodes():
if op.name() in self._conv_ops and not op.op().has_attr(
"fuse_activation"
):
activation = ""
if op.op().has_attr("fuse_relu") and op.op().attr("fuse_relu"):
activation = "relu"
op.set_attr("fuse_activation", activation)
return graph
def _remove_ctrl_vars(self, graph):
remove_ctr_vars = set()
for node in graph.all_var_nodes():
if node.is_ctrl_var():
remove_ctr_vars.add(node)
graph.safe_remove_nodes(remove_ctr_vars)
return graph
def _optimize_fp32_graph(self, graph):
graph = self._update_activations(graph)
graph = self._remove_ctrl_vars(graph)
graph = self._apply_pass(
graph, 'onednn_placement_pass', ['onednn_enabled_op_types'], [set()]
)
# remove dropout ops
graph = self._apply_pass(graph, 'simplify_with_basic_ops_pass')
graph = self._apply_pass(graph, 'layer_norm_fuse_pass')
graph = self._apply_pass(graph, 'attention_lstm_fuse_pass')
graph = self._apply_pass(graph, 'seqconv_eltadd_relu_fuse_pass')
graph = self._apply_pass(graph, 'fc_lstm_fuse_pass')
graph = self._apply_pass(graph, 'mul_lstm_fuse_pass')
graph = self._apply_pass(graph, 'fc_gru_fuse_pass')
graph = self._apply_pass(graph, 'mul_gru_fuse_pass')
graph = self._apply_pass(graph, 'multi_gru_fuse_pass')
graph = self._apply_pass(graph, 'multi_gru_seq_fuse_pass')
graph = self._apply_pass(graph, 'seq_concat_fc_fuse_pass')
graph = self._apply_pass(graph, 'gpu_cpu_squeeze2_matmul_fuse_pass')
graph = self._apply_pass(graph, 'gpu_cpu_reshape2_matmul_fuse_pass')
graph = self._apply_pass(graph, 'gpu_cpu_flatten2_matmul_fuse_pass')
graph = self._apply_pass(graph, 'matmul_v2_scale_fuse_pass')
graph = self._apply_pass(graph, 'squared_mat_sub_fuse_pass')
graph = self._apply_pass(graph, 'is_test_pass')
graph = self._apply_pass(graph, 'gpu_cpu_map_matmul_v2_to_mul_pass')
graph = self._apply_pass(graph, 'gpu_cpu_map_matmul_v2_to_matmul_pass')
graph = self._apply_pass(graph, 'matmul_scale_fuse_pass')
graph = self._apply_pass(graph, 'gpu_cpu_map_matmul_to_mul_pass')
graph = self._apply_pass(graph, 'repeated_fc_relu_fuse_pass')
graph = self._apply_pass(graph, 'depthwise_conv_onednn_pass')
graph = self._apply_pass(graph, 'conv_bn_fuse_pass')
graph = self._apply_pass(graph, 'conv_eltwiseadd_bn_fuse_pass')
graph = self._apply_pass(graph, 'conv_affine_channel_onednn_fuse_pass')
graph = self._apply_pass(graph, 'conv_transpose_bn_fuse_pass')
graph = self._apply_pass(
graph, 'conv_transpose_eltwiseadd_bn_fuse_pass'
)
graph = self._apply_pass(graph, 'conv_bias_onednn_fuse_pass')
graph = self._apply_pass(graph, 'conv_transpose_bias_onednn_fuse_pass')
graph = self._apply_pass(graph, 'conv_elementwise_add_onednn_fuse_pass')
graph = self._apply_pass(graph, 'conv_activation_onednn_fuse_pass')
graph = self._apply_pass(
graph, 'fc_fuse_pass', ['use_gpu', 'use_fc_padding'], [False, False]
)
graph = self._apply_pass(graph, 'repeated_fc_relu_fuse_pass')
if self._is_fc_quantized(graph):
# Disabled due to topology-dependent speed-up
graph = self._apply_pass(graph, 'fc_onednn_pass')
graph = self._apply_pass(graph, 'fc_act_onednn_fuse_pass')
graph = self._apply_pass(
graph, 'matmul_transpose_reshape_onednn_fuse_pass'
)
graph = self._apply_pass(
graph, 'matmul_elementwise_add_onednn_fuse_pass'
)
graph = self._apply_pass(graph, 'matmul_activation_onednn_fuse_pass')
graph = self._apply_pass(graph, 'batch_norm_act_fuse_pass')
graph = self._apply_pass(graph, 'softplus_activation_onednn_fuse_pass')
graph = self._apply_pass(graph, 'scale_matmul_fuse_pass')
graph = self._apply_pass(
graph, 'reshape_transpose_matmul_onednn_fuse_pass'
)
# the following pass should be the last one since it will work on all fused ops.
graph = self._apply_pass(graph, 'runtime_context_cache_pass')
return graph
def _apply_pass(self, graph, pass_name, attrs=None, attr_values=None):
ir_pass = core.get_pass(pass_name)
cpp_graph = graph.graph
if not cpp_graph.has('__param_scope__'):
cpp_graph.set_not_owned('__param_scope__', self._scope)
if attrs:
assert attr_values and len(attrs) == len(attr_values), (
"Different number of pass attributes and their values."
)
for attr, value in zip(attrs, attr_values):
ir_pass.set(attr, value)
ir_pass.apply(cpp_graph)
if self._debug:
graph.draw(
'.',
f'{self._pass_group}_{self._pass_idx}_{pass_name}',
graph.all_op_nodes(),
)
self._remove_unused_var_nodes(graph)
self._pass_idx += 1
return graph
def _cleanup(self, graph):
graph = self._remove_unused_var_nodes(graph)
graph = self._set_op_role_forward(graph)
return graph
def _remove_unused_var_nodes(self, graph):
all_used_vars = set()
ops = graph.all_op_nodes()
for op_node in ops:
for input_node in op_node.inputs:
all_used_vars.add(input_node)
for output_node in op_node.outputs:
all_used_vars.add(output_node)
all_used_vars = {n.node for n in all_used_vars}
all_unused_vars = set(
filter(
lambda node: node.node not in all_used_vars,
graph.all_var_nodes(),
)
)
graph.safe_remove_nodes(all_unused_vars)
return graph
def _set_op_role_forward(self, graph):
ops = graph.all_op_nodes()
for op in ops:
op.set_attr("op_role", OpRole.Forward)
return graph
def _compute_weight_scales(self, graph):
def _compute_var_scales(ops, w_name, axis):
for op in graph.all_op_nodes():
if op.op().type() in ops:
weight_var_name = op.input(w_name)[0]
weights = np.array(
self._load_param(self._scope, weight_var_name)
)
scales = 1.0 / np.amax(
np.abs(weights.reshape(weights.shape[0], -1)).astype(
np.float64
),
axis=axis,
)
scales[scales == np.inf] = 0.0
lod_tensor = self._convert_scale2tensor(scales)
use_unsigned_int = False
self._var_quant_scales[weight_var_name] = (
use_unsigned_int,
lod_tensor,
)
def _compute_single_gru_weight_scales(wx_var_name, wh_var_name):
wx = np.array(self._load_param(self._scope, wx_var_name))
wh = np.array(self._load_param(self._scope, wh_var_name))
OC = wh.shape[0]
scale_ur = 1.0 / np.max(
np.abs(
np.concatenate(
[
wx[:, : 2 * OC],
wh.flatten()[: 2 * OC * OC].reshape(OC, 2 * OC),
],
axis=0,
)
),
axis=0,
)
scale_o = 1.0 / np.max(
np.abs(
np.concatenate(
[
wx[:, 2 * OC :],
wh.flatten()[2 * OC * OC :].reshape(OC, OC),
],
axis=0,
)
),
axis=0,
)
gru_weights_scale = np.concatenate([scale_ur, scale_o]).astype(
'float'
)
return self._convert_scale2tensor(gru_weights_scale)
def _compute_gru_weight_scales(wx_name, wh_name):
for op in graph.all_op_nodes():
if op.op().type() in self._gru_ops:
assert len(op.input(wx_name)) == len(op.input(wh_name)), (
f'Mismatch in number of weights inputs ({len(op.input(wx_name))} for WeightX vs. {len(op.input(wh_name))} for WeightH).'
)
for i, wx_var_name in enumerate(op.input(wx_name)):
wh_var_name = op.input(wh_name)[i]
use_unsigned_int = False
lod_tensor = _compute_single_gru_weight_scales(
wx_var_name, wh_var_name
)
self._var_quant_scales[wx_var_name] = (
use_unsigned_int,
lod_tensor,
)
def _compute_single_lstm_weight_scales(wx_var_name, wh_var_name):
wx = np.array(self._load_param(self._scope, wx_var_name))
wh = np.array(self._load_param(self._scope, wh_var_name))
lstm_weights_scale = 1.0 / np.max(
np.abs(np.concatenate([wx[:, :], wh[:, :]], axis=0)), axis=0
)
lstm_weights_scale = lstm_weights_scale.astype('float')
return self._convert_scale2tensor(lstm_weights_scale)
def _compute_lstm_weight_scales(wx_name, wh_name):
for op in graph.all_op_nodes():
if op.op().type() in self._lstm_ops:
assert len(op.input(wx_name)) == len(op.input(wh_name)), (
f'Mismatch in number of weights inputs ({len(op.input(wx_name))} for WeightX vs. {len(op.input(wh_name))} for WeightH).'
)
for i, wx_var_name in enumerate(op.input(wx_name)):
wh_var_name = op.input(wh_name)[i]
use_unsigned_int = False
lod_tensor = _compute_single_lstm_weight_scales(
wx_var_name, wh_var_name
)
self._var_quant_scales[wx_var_name] = (
use_unsigned_int,
lod_tensor,
)
_compute_var_scales(self._conv_ops, "Filter", axis=1)
_compute_var_scales(self._fc_ops, "W", axis=0)
_compute_var_scales(self._gru_ops, "WeightH", axis=0)
_compute_var_scales(self._lstm_ops, "WeightH", axis=0)
_compute_gru_weight_scales("WeightX", "WeightH")
_compute_lstm_weight_scales("WeightX", "WeightH")
return graph
def _update_relu_output_scales(self, graph):
def _set_unsigned_scale(graph, ops, op_out_name, predicate):
'''
Sets the type of an output scale of a passed op type(s) to 'unsigned int8' if the
predicate applied on op passes. Typically, the predicate checks if op's
activation is set to relu.
'''
for op in graph.all_op_nodes():
if op.name() in ops:
out_name = op.output(op_out_name)[0]
if out_name in self._var_quant_scales and predicate(
op.op()
):
is_unsigned, tensor = self._var_quant_scales[out_name]
if is_unsigned is False:
# If the variable is signed, it means that the scales for this var
# were computed for signed data, so the scale must be multiplied by 2
# to fill the entire range of uint8
scale = np.array(tensor) * 2
tensor = self._convert_scale2tensor(
scale.astype(np.float64)
)
self._var_quant_scales[out_name] = (True, tensor)
return graph
def conv_predicate(op):
return op.attr("fuse_activation") in self._relu_ops
graph = _set_unsigned_scale(
graph, self._conv_ops, "Output", conv_predicate
)
def fc_predicate(op):
return op.attr("activation_type") in self._relu_ops
graph = _set_unsigned_scale(graph, self._fc_ops, "Out", fc_predicate)
graph = _set_unsigned_scale(
graph, self._relu_ops, 'Out', lambda op: True
)
return graph
def _get_data_layout(self, graph):
return 'NHWC' if self._is_conv_quantized(graph) else 'NCHW'
def _quantize_fp32_graph(self, graph):
graph = self._apply_pass(graph, 'scale_matmul_fuse_pass')
graph = self._apply_pass(
graph, 'reshape_transpose_matmul_onednn_fuse_pass'
)
graph = self._apply_pass(
graph,
'cpu_quantize_placement_pass',
['quantize_enabled_op_types'],
[self._ops_to_quantize],
)
graph = self._apply_pass(
graph,
'cpu_quantize_pass',
['quant_var_scales', 'data_layout'],
[self._var_quant_scales, self._get_data_layout(graph)],
)
graph = self._apply_pass(graph, 'cpu_quantize_squash_pass')
graph = self._apply_pass(graph, 'int8_scale_calculation_onednn_pass')
graph = self._apply_pass(graph, 'params_quantization_onednn_pass')
return graph
class Quant2Int8MkldnnPass(Quant2Int8OnednnPass):
@deprecated(
since="3.1.0",
update_to="paddle.static.quantization.Quant2Int8OnednnPass",
level=1,
reason="Quant2Int8MkldnnPass will be removed in future",
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
@@ -0,0 +1,287 @@
# Copyright (c) 2022 PaddlePaddle Authors. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# A dict of operators that contain weights and support quantization,
# including operator names, actual input and output names.
SUPPORT_WEIGHT_QUANTIZATION_OP_DICT = {
"conv2d": [["Input", "Filter"], ["Output"]],
"depthwise_conv2d": [["Input", "Filter"], ["Output"]],
"conv2d_transpose": [["Input", "Filter"], ["Output"]],
"mul": [["X", "Y"], ["Out"]],
"matmul": [["X", "Y"], ["Out"]],
"matmul_v2": [["X", "Y"], ["Out"]],
}
# A dict of operators that supports quantization and has only activation inputs,
# including operator names, actual input and output names.
SUPPORT_ACT_QUANTIZATION_OP_DICT = {
"mul": [["X", "Y"], ["Out"]],
"matmul": [["X", "Y"], ["Out"]],
"matmul_v2": [["X", "Y"], ["Out"]],
"pool2d": [["X"], ["Out"]],
"elementwise_add": [["X", "Y"], ["Out"]],
"concat": [["X"], ["Out"]],
"softmax": [["X"], ["Out"]],
"argmax": [["X"], ["Out"]],
"transpose": [["X"], ["Out"]],
"equal": [["X", "Y"], ["Out"]],
"gather": [["X"], ["Out"]],
"greater_equal": [["X", "Y"], ["Out"]],
"greater_than": [["X", "Y"], ["Out"]],
"less_equal": [["X", "Y"], ["Out"]],
"less_than": [["X", "Y"], ["Out"]],
"mean": [["X"], ["Out"]],
"not_equal": [["X", "Y"], ["Out"]],
"reshape": [["X"], ["Out"]],
"reshape2": [["X"], ["Out"]],
"transpose2": [["X"], ["Out"]],
"nearest_interp": [["X"], ["Out"]],
"trilinear_interp": [["X"], ["Out"]],
"slice": [["Input"], ["Out"]],
"squeeze": [["X"], ["Out"]],
"elementwise_sub": [["X", "Y"], ["Out"]],
"relu": [["X"], ["Out"]],
"relu6": [["X"], ["Out"]],
"leaky_relu": [["X"], ["Out"]],
"prelu": [["X", "Alpha"], ["Out"]],
"tanh": [["X"], ["Out"]],
"swish": [["X"], ["Out"]],
"dropout": [["X"], ["Out"]],
"batch_norm": [["X"], ["Y"]],
"layer_norm": [["X"], ["Y"]],
"sigmoid": [["X"], ["Out"]],
"elementwise_mul": [["X", "Y"], ["Out"]],
"elementwise_pow": [["X", "Y"], ["Out"]],
"hard_swish": [["X"], ["Out"]],
"hard_sigmoid": [["X"], ["Out"]],
"gru": [["Input", "Weight"], ["Hidden"]],
"lstm": [["Input", "Weight"], ["Hidden"]],
"pad2d": [["X"], ["Out"]],
"pad3d": [["X"], ["Out"]],
"flatten": [["X"], ["Out"]],
"flatten2": [["X"], ["Out"]],
"unsqueeze2": [["X"], ["Out"]],
"flatten_contiguous_range": [["X"], ["Out"]],
"split": [["X"], ["Out"]],
"squeeze2": [["X"], ["Out"]],
"nearest_interp_v2": [["X"], ["Out"]],
"bilinear_interp": [["X"], ["Out"]],
"bilinear_interp_v2": [["X"], ["Out"]],
"fill_constant_batch_size_like": [["Input"], ["Out"]],
"arg_max": [["X"], ["Out"]],
"abs": [["X"], ["Out"]],
"assign": [["X"], ["Out"]],
"cast": [["X"], ["Out"]],
"clip": [["X"], ["Out"]],
"box_coder": [["PriorBox"], ["OutputBox"]],
"crop": [["X"], ["Out"]],
"cumsum": [["X"], ["Out"]],
"expand_v2": [["X"], ["Out"]],
"fill_any_like": [["X"], ["Out"]],
"fill_constant": [[], ["Out"]],
"gelu": [["X"], ["Out"]],
"instance_norm": [["X"], ["Y"]],
"lookup_table": [["W", "Ids"], ["Out"]],
"lookup_table_v2": [["W", "Ids"], ["Out"]],
"norm": [["X"], ["Norm"]],
"p_norm": [["X"], ["Out"]],
"pow": [["X"], ["Out"]],
"reduce_mean": [["X"], ["Out"]],
"stack": [["X"], ["Y"]],
"top_k_v2": [["X"], ["Out", "Indices"]],
"logical_and": [["X", "Y"], ["Out"]],
"logical_not": [["X"], ["Out"]],
"meshgrid": [["X"], ["Out"]],
"roi_align": [["X", "ROIs"], ["Out"]],
"strided_slice": [["Input"], ["Out"]],
"where": [["Condition", "X", "Y"], ["Out"]],
"grid_sampler": [["X", "Grid"], ["Output"]],
"tile": [["X"], ["Out"]],
"group_norm": [["X"], ["Y", "Mean", "Variance"]],
"reduce_sum": [["X"], ["Out"]],
"square": [["X"], ["Out"]],
"softplus": [["X"], ["Out"]],
"shuffle_channel": [["X"], ["Out"]],
"reduce_max": [["X"], ["Out"]],
"scale": [["X"], ["Out"]],
}
# A full dict of operators that supports quantization,
# including operator names, actual input and output names.
SUPPORT_QUANTIZATION_OP_DICT = SUPPORT_WEIGHT_QUANTIZATION_OP_DICT.copy()
SUPPORT_QUANTIZATION_OP_DICT.update(SUPPORT_ACT_QUANTIZATION_OP_DICT)
class BaseQuantizer:
"""
Basic quantization configuration class, which configures some hyperparameters
required for quantization, including the list of op types to be quantized,
quantization bit number for weight and activation and the range of quantization values.
Args:
quantizable_op_type(list[str], optional): List the type of ops
that will be quantized. Default is []. If quantizable_op_type is [],
it will use the default quantization op type of the qunat config in
the current Quantizer.
quant_bits(int, optional): Quantization bit number for weight and activation.
Default is 8.
"""
def __init__(
self,
quantizable_op_type=[],
quant_bits=8,
):
self._quantizable_op_type = quantizable_op_type
self._quant_bits = quant_bits
self._quant_min = -128
self._quant_max = 127
@property
def weight_quant_operation_types(self):
"""
Operation type list which should support weight quantization.
And before these ops, quant dequant nodes will be inserted.
"""
base_weight_op_type_list = list(
SUPPORT_WEIGHT_QUANTIZATION_OP_DICT.keys()
)
if self._quantizable_op_type:
weight_list = []
for _op_type in self._quantizable_op_type:
if _op_type in base_weight_op_type_list:
weight_list.append(_op_type)
return weight_list
else:
return base_weight_op_type_list
@property
def activation_quant_operation_types(self):
"""
Operation type list which should support activation quantization.
And before these ops, quant dequant nodes will be inserted.
"""
base_act_op_type_list = list(SUPPORT_ACT_QUANTIZATION_OP_DICT.keys())
act_quant_op_list = []
if self._quantizable_op_type:
for _op_type in self._quantizable_op_type:
if _op_type in base_act_op_type_list:
act_quant_op_list.append(_op_type)
else:
act_quant_op_list = [
'mul',
'matmul',
'matmul_v2',
]
return act_quant_op_list
@property
def observer_operation_types(self):
"""
Operation type list for observer in quantization. These nodes only count the
calibration boundary scale and do not participate in the fake quantization.
In order to facilitate the deployment of the prediction engine, quant
and dequant nodes will be inserted after these ops when exporting the model.
"""
return list(SUPPORT_ACT_QUANTIZATION_OP_DICT.keys())
class TensorRTQuantizer(BaseQuantizer):
"""
TensorRT quantization configuration class.
Args:
quantizable_op_type(list[str], optional): List the type of ops
that will be quantized. Default is []. If quantizable_op_type is [],
it will use the default quantization op type of the qunat config in
the current Quantizer.
quant_bits(int, optional): Quantization bit number for weight and activation.
Default is 8.
"""
def __init__(
self,
quantizable_op_type=[],
quant_bits=8,
):
super().__init__()
self._quantizable_op_type = quantizable_op_type
self._quant_bits = quant_bits
self._quant_min = -128
self._quant_max = 127
@property
def activation_quant_operation_types(self):
"""
Operation type list which should support activation quantization.
And before these ops, quant dequant nodes will be inserted.
"""
return [
"pool2d",
"elementwise_add",
"elementwise_sub",
"elementwise_mul",
"elementwise_pow",
"concat",
"softmax",
"argmax",
"mean",
"relu",
"relu6",
"leaky_relu",
"tanh",
"swish",
"softplus",
"gelu",
"hard_sigmoid",
"hard_swish",
"sigmoid",
"layer_norm",
"matmul_v2",
"split",
"bilinear_interp",
"nearest_interp",
"trilinear_interp",
"nearest_interp_v2",
"bilinear_interp",
"bilinear_interp_v2",
"clip",
"pow",
"reduce_mean",
"reduce_sum",
"reduce_max",
]
class ARMCPUQuantizer(BaseQuantizer):
"""
ARM CPU with Paddle Lite quantization configuration class.
Args:
quantizable_op_type(list[str], optional): List the type of ops
that will be quantized. Default is []. If quantizable_op_type is [],
it will use the default quantization op type of the qunat config in
the current Quantizer.
quant_bits(int, optional): Quantization bit number for weight and activation.
Default is 8.
"""
def __init__(
self,
quantizable_op_type=[],
quant_bits=8,
):
super().__init__()
self._quantizable_op_type = quantizable_op_type
self._quant_bits = quant_bits
self._quant_min = -127
self._quant_max = 127
@@ -0,0 +1,302 @@
# Copyright (c) 2022 PaddlePaddle Authors. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import numpy as np
from paddle.utils import deprecated
from ...base.framework import IrGraph
from ...framework import _get_paddle_place
class QuantInt8OnednnPass:
"""
Convert QuantizationFreezePass generated IrGraph to MKL-DNN supported INT8
IrGraph. Following transformations did in this pass:
1. Convert int8 range weights with float32 data type, which are generated by
the QuantizationFreezePass, to float32 range weights with float32 data type
by using the corresponding scales. This conversion is because MKL-DNN INT8
conv2d kernel and mul kernel now only support float32 weights input, hence
weights quantization will happen inside the conv2d and mul INT8 kernel.
2. Create the new conv2d or mul op with the converted weights and link its output
to fake_dequantize_abs_max op's output and set conv2d's attribute "force_fp32
_output" as true
3. Transform fake_quantize_xx op to quantize op
4. Remove fake_dequantize_abs_max op
"""
def __init__(self, _scope=None, _place=None):
r"""
Args:
scope(static.Scope): scope is used to initialize the new parameters.
place(static.CPUPlace|str): place is used to initialize the new parameters.
When it is string, it can be only 'cpu'.
Examples:
.. code-block:: pycon
>>> # The original graph will be rewrite.
>>> import paddle
>>> from paddle import static
>>> from paddle.static.quantization import QuantInt8OnednnPass
>>> from paddle.framework import IrGraph
>>> from paddle.framework import core
>>> graph = IrGraph(core.Graph(static.Program().desc), for_test=False)
>>> place = paddle.CPUPlace()
>>> onednn_pass = QuantInt8OnednnPass(static.global_scope(), place)
>>> onednn_pass.apply(graph)
"""
self._scope = _scope
self._place = _get_paddle_place(_place)
self._quantize_type = [
'fake_quantize_moving_average_abs_max',
'fake_quantize_range_abs_max',
]
self._dequantize_type = ['fake_dequantize_max_abs']
self._quantize_dequantize_type = [
'fake_quantize_dequantize_moving_average_abs_max'
]
self._quantizable_ops = ['conv2d', 'depthwise_conv2d', 'mul']
self._conv_ops = ['conv2d', 'depthwise_conv2d']
self._pool_ops = ['pool2d']
self._in_scale = {}
self._max_range = {}
self._new_output = {}
self._s8_max = 127
def apply(self, graph):
"""
Quantize the graph for running MKL-DNN INT8 inference. According
to activation quantization type, the graph will transform fake
quantize ops to quantize ops and remove the fake dequantize ops.
Args:
graph(IrGraph): the applied graph.
"""
assert isinstance(graph, IrGraph), (
'graph must be the instance of IrGraph.'
)
ops = graph.all_op_nodes()
persistable_vars = [p.name() for p in graph.all_persistable_nodes()]
# Collect the _in_scales and _max_range to calculate the new scales for MKL-DNN
# INT8 conv2d and mul
for op_node in ops:
if op_node.name() in self._dequantize_type:
input_name = op_node.input("X")[0]
scale_name = op_node.input("Scale")[0]
self._in_scale[input_name] = self._load_param(
self._scope, scale_name
)[0]
self._max_range[input_name] = op_node.op().attr("max_range")
self._new_output[input_name] = op_node.output("Out")[0]
if op_node.name() in self._quantize_dequantize_type:
inputs = op_node.op().input_names()
attrs = op_node.op().attr_names()
input_name = op_node.input("X")[0]
scale_name = op_node.input("InScale")[0]
self._in_scale[input_name] = self._load_param(
self._scope, scale_name
)[0]
# self._max_range[input_name] = op_node.op().attr("max_range")
self._new_output[input_name] = op_node.output("Out")[0]
for op_node in ops:
if op_node.name() in self._quantizable_ops:
if op_node.name() in self._conv_ops:
self._transform_to_conv_onednn(graph, op_node)
elif op_node.name() in self._pool_ops:
self._transform_to_pool_onednn(graph, op_node)
else:
self._transform_to_mul_onednn(graph, op_node)
elif op_node.name() in self._quantize_type:
self._transform_to_quantize_onednn(graph, op_node)
elif op_node.name() in self._dequantize_type:
self._remove_fake_dequantize_op(graph, op_node)
self._remove_unused_var_nodes(graph)
return graph
def _transform_to_pool_onednn(self, graph, op):
output_name = op.output("Out")[0]
input_name = op.input("X")[0]
def _transform_to_conv_onednn(self, graph, op_node):
weight_name = op_node.input("Filter")[0]
output_name = op_node.output("Output")[0]
# Convert int8 range weights to fp32 range weights
weight = self._load_param(self._scope, weight_name)
w_fp32 = np.divide(
np.multiply(weight, self._s8_max), self._max_range[output_name]
)
w_fp32 = w_fp32.reshape(weight.shape)
self._restore_var(weight_name, w_fp32)
input_var_node = graph._find_node_by_name(
op_node.inputs, op_node.input("Input")[0]
)
weight_var_node = graph._find_node_by_name(op_node.inputs, weight_name)
# Set fake_dequantize_abs_max's output as new output of conv2d
output_var_node = graph._find_node_by_name(
graph.all_var_nodes(), self._new_output[output_name]
)
attrs = {
name: op_node.op().attr(name) for name in op_node.op().attr_names()
}
conv_op_node = graph.create_op_node(
op_type='fused_conv2d',
attrs=attrs,
inputs={'Input': input_var_node, 'Filter': weight_var_node},
outputs={'Output': output_var_node},
)
# Based on the Quant's scales to calculate the scales of MKL-DNN INT8 conv2d
scale_in = self._s8_max / self._in_scale[output_name]
scale_w = []
scale_w = [self._max_range[output_name] / self._s8_max]
conv_op_node.set_attr("Scale_weights", scale_w)
conv_op_node.set_attr("Scale_in", scale_in)
conv_op_node.set_attr("Scale_out", 1.0)
conv_op_node.set_attr("use_onednn", 1)
conv_op_node.set_attr("force_fp32_output", 1)
graph.link_to(input_var_node, conv_op_node)
graph.link_to(weight_var_node, conv_op_node)
graph.link_to(conv_op_node, output_var_node)
graph.safe_remove_nodes(op_node)
def _transform_to_mul_onednn(self, graph, op_node):
# For MKL-DNN INT8 mul, input Y should be the weights
weight_name = op_node.input("Y")[0]
output_name = op_node.output("Out")[0]
# Convert int8 range weights to fp32 range weights
weight = self._load_param(self._scope, weight_name)
w_fp32 = np.divide(
np.multiply(weight, self._s8_max), self._max_range[output_name]
)
w_fp32 = w_fp32.reshape(weight.shape)
self._restore_var(weight_name, w_fp32)
input_var_node = graph._find_node_by_name(
op_node.inputs, op_node.input("X")[0]
)
weight_var_node = graph._find_node_by_name(op_node.inputs, weight_name)
# Set fake_dequantize_abs_max's output as new output of mul
output_var_node = graph._find_node_by_name(
graph.all_var_nodes(), self._new_output[output_name]
)
attrs = {
name: op_node.op().attr(name) for name in op_node.op().attr_names()
}
mul_op_node = graph.create_op_node(
op_type='mul',
attrs=attrs,
inputs={'X': input_var_node, 'Y': weight_var_node},
outputs={'Out': output_var_node},
)
# Based on the Quant's scales to calculate MKL-DNN INT8 mul's scales
scale_in = self._s8_max / self._in_scale[output_name]
scale_w = []
scale_w = [self._max_range[output_name] / self._s8_max]
mul_op_node.set_attr("scale_y", scale_w)
mul_op_node.set_attr("scale_x", scale_in)
mul_op_node.set_attr("scale_out", 1.0)
mul_op_node.set_attr("use_onednn", 1)
mul_op_node.set_attr("force_fp32_output", 1)
graph.link_to(input_var_node, mul_op_node)
graph.link_to(weight_var_node, mul_op_node)
graph.link_to(mul_op_node, output_var_node)
graph.safe_remove_nodes(op_node)
def _transform_to_quantize_onednn(self, graph, op_node):
"""
Transform fake_quantize_xx op to quantize onednn op in the graph.
"""
input_var_node = graph._find_node_by_name(
op_node.inputs, op_node.input("X")[0]
)
output_var_node = graph._find_node_by_name(
op_node.outputs, op_node.output("Out")[0]
)
scale_in = (
self._s8_max
/ self._load_param(self._scope, op_node.input("InScale")[0])[0]
)
quant_op_node = graph.create_op_node(
op_type='quantize',
attrs={
'data_format': 'ONEDNNLAYOUT',
'use_onednn': 1,
'Scale': scale_in,
'is_negative_input': 1,
},
inputs={'Input': input_var_node},
outputs={'Output': output_var_node},
)
graph.link_to(input_var_node, quant_op_node)
graph.link_to(quant_op_node, output_var_node)
graph.safe_remove_nodes(op_node)
def _remove_fake_dequantize_op(self, graph, op_node):
input_var_node = graph._find_node_by_name(
op_node.inputs, op_node.input("X")[0]
)
graph.safe_remove_nodes(op_node)
def _load_param(self, scope, param_name):
return np.array(scope.find_var(param_name).get_tensor())
def _restore_var(self, name, array):
tensor = self._scope.find_var(name).get_tensor()
tensor.set(array, self._place)
def _remove_unused_var_nodes(self, graph):
all_used_vars = set()
ops = graph.all_op_nodes()
for op_node in ops:
for input_node in op_node.inputs:
all_used_vars.add(input_node)
for output_node in op_node.outputs:
all_used_vars.add(output_node)
all_used_vars = {n.node for n in all_used_vars}
all_unused_vars = set(
filter(
lambda node: node.node not in all_used_vars,
graph.all_var_nodes(),
)
)
graph.safe_remove_nodes(all_unused_vars)
class QuantInt8MkldnnPass(QuantInt8OnednnPass):
@deprecated(
since="3.1.0",
update_to="paddle.static.quantization.QuantInt8OnednnPass",
level=1,
reason="QuantInt8MkldnnPass will be removed in future",
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
@@ -0,0 +1,534 @@
# Copyright (c) 2023 PaddlePaddle Authors. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import copy
import json
import logging
import os
import paddle
from ...base.framework import IrGraph, core
from ..log_helper import get_logger
from .quantization_pass import (
AddQuantDequantForResidual,
AddQuantDequantPass,
ConvertToInt8Pass,
OutScaleForInferencePass,
OutScaleForTrainingPass,
QuantizationFreezePass,
QuantizationTransformPass,
)
_logger = get_logger(__name__, level=logging.INFO)
from . import quant_config
from .post_training_quantization import PostTrainingQuantizationProgram
from .quantization_pass import (
AddQuantDequantForInferencePass,
AddQuantDequantPassV2,
QuantizationTransformPassV2,
QuantWeightPass,
)
WEIGHT_QUANTIZATION_TYPES = [
'abs_max',
'channel_wise_abs_max',
'range_abs_max',
'moving_average_abs_max',
]
WEIGHT_QUANTIZATION_TYPES_TENSORRT = ['channel_wise_abs_max']
ACTIVATION_QUANTIZATION_TYPES = [
'abs_max',
'range_abs_max',
'moving_average_abs_max',
]
ACTIVATION_QUANTIZATION_TYPES_TENSORRT = [
'range_abs_max',
'moving_average_abs_max',
]
VALID_DTYPES = ['int8']
TRANSFORM_PASS_OP_TYPES = list(
quant_config.SUPPORT_WEIGHT_QUANTIZATION_OP_DICT.keys()
)
QUANT_DEQUANT_PASS_OP_TYPES = list(
quant_config.SUPPORT_ACT_QUANTIZATION_OP_DICT.keys()
)
TENSORRT_OP_TYPES = [
'mul',
'conv2d',
'pool2d',
'depthwise_conv2d',
'elementwise_add',
'leaky_relu',
]
VARS_MAPPING_TABLE = './mapping_table_for_saving_inference_model'
_quant_config_default = {
# weight quantize type, default is 'channel_wise_abs_max'
'weight_quantize_type': 'channel_wise_abs_max',
# activation quantize type, default is 'moving_average_abs_max'
'activation_quantize_type': 'moving_average_abs_max',
# weight quantize bit num, default is 8
'weight_bits': 8,
# activation quantize bit num, default is 8
'activation_bits': 8,
# ops of name_scope in not_quant_pattern list, will not be quantized
'not_quant_pattern': ['skip_quant'],
# ops of type in quantize_op_types, will be quantized
'quantize_op_types': ['conv2d', 'depthwise_conv2d', 'mul'],
# data type after quantization, such as 'uint8', 'int8', etc. default is 'int8'
'dtype': 'int8',
# window size for 'range_abs_max' quantization. default is 10000
'window_size': 10000,
# The decay coefficient of moving average, default is 0.9
'moving_rate': 0.9,
# if True, 'quantize_op_types' will be TENSORRT_OP_TYPES
'for_tensorrt': False,
# if True, 'quantize_op_types' will be TRANSFORM_PASS_OP_TYPES + QUANT_DEQUANT_PASS_OP_TYPES
'is_full_quantize': False,
# if True, use onnx format to quant.
'onnx_format': True,
# quant post to get initial scale for quant_aware
'quant_post_first': False,
# whether scale can be train
'scale_trainable': True,
}
def load_dict():
with open(VARS_MAPPING_TABLE, 'r') as file:
data = file.read()
data = json.loads(data)
return data
def save_dict(table):
with open(VARS_MAPPING_TABLE, 'w') as file:
file.write(json.dumps(table))
def _parse_configs(user_config):
"""
check if user's configs are valid.
Args:
user_config(dict): user's config.
Return:
configs(dict): final configs will be used.
"""
configs = copy.deepcopy(_quant_config_default)
configs.update(user_config)
assert isinstance(configs['for_tensorrt'], bool) and isinstance(
configs['is_full_quantize'], bool
), "'for_tensorrt' and 'is_full_quantize' must both be bool'"
# check if configs is valid
if configs['for_tensorrt']:
weight_types = WEIGHT_QUANTIZATION_TYPES_TENSORRT
activation_types = ACTIVATION_QUANTIZATION_TYPES_TENSORRT
platform = 'TensorRT'
else:
weight_types = WEIGHT_QUANTIZATION_TYPES
activation_types = WEIGHT_QUANTIZATION_TYPES
platform = 'PaddleLite'
assert configs['weight_quantize_type'] in weight_types, (
"Unknown weight_quantize_type: {}. {} only supports {} ".format(
configs['weight_quantize_type'], platform, weight_types
)
)
assert configs['activation_quantize_type'] in activation_types, (
"Unknown activation_quantize_type: {}. {} only supports {}".format(
configs['activation_quantize_type'], platform, activation_types
)
)
assert isinstance(configs['weight_bits'], int), (
"weight_bits must be int value."
)
assert configs['weight_bits'] >= 1 and configs['weight_bits'] <= 16, (
"weight_bits should be between 1 and 16."
)
assert isinstance(configs['activation_bits'], int), (
"activation_bits must be int value."
)
assert (
configs['activation_bits'] >= 1 and configs['activation_bits'] <= 16
), "activation_bits should be between 1 and 16."
assert isinstance(configs['not_quant_pattern'], (list, str)), (
"not_quant_pattern must be list or str"
)
assert isinstance(configs['quantize_op_types'], list), (
"quantize_op_types must be a list"
)
if configs['for_tensorrt']:
configs['quantize_op_types'] = TENSORRT_OP_TYPES
elif configs['is_full_quantize']:
configs['quantize_op_types'] = (
TRANSFORM_PASS_OP_TYPES + QUANT_DEQUANT_PASS_OP_TYPES
)
else:
for op_type in configs['quantize_op_types']:
assert (op_type in QUANT_DEQUANT_PASS_OP_TYPES) or (
op_type in TRANSFORM_PASS_OP_TYPES
), (
f"{op_type} is not support, \
now support op types are {TRANSFORM_PASS_OP_TYPES + QUANT_DEQUANT_PASS_OP_TYPES}"
)
assert isinstance(configs['dtype'], str), "dtype must be a str."
assert configs['dtype'] in VALID_DTYPES, "dtype can only be " + " ".join(
VALID_DTYPES
)
assert isinstance(configs['window_size'], int), (
"window_size must be int value, window size for 'range_abs_max' quantization, default is 10000."
)
assert isinstance(configs['moving_rate'], float), (
"moving_rate must be float value, The decay coefficient of moving average, default is 0.9."
)
return configs
def quant_aware(
program,
place,
config=None,
scope=None,
for_test=False,
weight_quantize_func=None,
act_quantize_func=None,
weight_preprocess_func=None,
act_preprocess_func=None,
optimizer_func=None,
executor=None,
return_program=False,
calib_config={},
draw_graph=False,
return_scale_dict=False,
scale_dict=None,
model_type=None,
pattern_ops=None,
):
"""Add quantization and dequantization operators to "program"
for quantization training or testing.
Args:
program(paddle.static.Program): training or testing ``program``.
place(paddle.CPUPlace or paddle.CUDAPlace): This parameter represents
the executor run on which device.
config(dict, optional): configs for quantization. if None, will use default config.
Default: None.
scope(paddle.static.Scope): Scope records the mapping between variable names and variables,
similar to brackets in programming languages. Usually users can use
`paddle.static.global_scope <https://www.paddlepaddle.org.cn/documentation/docs/zh/develop/api_cn/executor_cn/global_scope_cn.html>`_.
When ``None`` will use `paddle.static.global_scope() <https://www.paddlepaddle.org.cn/documentation/docs/zh/develop/api_cn/executor_cn/global_scope_cn.html>`_ .
Default: ``None``.
for_test(bool): If the 'program' parameter is a test program, this parameter should be set to ``True``.
Otherwise, set to ``False``.Default: False
weight_quantize_func(function): Function that defines how to quantize weight. Using this
can quickly test if user's quantization method works or not. In this function, user should
both define quantization function and dequantization function, that is, the function's input
is non-quantized weight and function returns dequantized weight. If None, will use
quantization op defined by 'weight_quantize_type'.
Default is None.
act_quantize_func(function): Function that defines how to quantize activation. Using this
can quickly test if user's quantization method works or not. In this function, user should
both define quantization and dequantization process, that is, the function's input
is non-quantized activation and function returns dequantized activation. If None, will use
quantization op defined by 'activation_quantize_type'.
Default is None.
weight_preprocess_func(function): Function that defines how to preprocess weight before quantization. Using this
can quickly test if user's preprocess method works or not. The function's input
is non-quantized weight and function returns processed weight to be quantized. If None, the weight will
be quantized directly.
Default is None.
act_preprocess_func(function): Function that defines how to preprocess activation before quantization. Using this
can quickly test if user's preprocess method works or not. The function's input
is non-quantized activation and function returns processed activation to be quantized. If None, the activation will
be quantized directly.
Default is None.
optimizer_func(function): Function return a optimizer. When 'is_test' is False and user want to use self-defined
quantization function and preprocess function, this function must be set. Default is None.
exe(paddle.static.Executor): If user want to use self-defined quantization function and preprocess function, exe must be set for
initialization. Default is None.
return_program(bool): If user want return value is a Program rather than Compiled Program, This argument should be set True.
Default is False.
draw_graph(bool): whether to draw graph when quantization is initialized. In order to prevent cycle,
the ERNIE model needs to be set to True. Default is False.
return_scale_dict(bool): If user want to return scale dict, model_type and pattern_ops, this argument should be set True.
Default is False.
scale_dict(dict): Use scale dict to initialize scales in program. Default is None.
model_type(str): Model type can be 'transformer' or 'non-transformer'. If model type is transformer, patterns will be analyzed.
Default is None.
pattern_ops(dict): Pattern_ops contain pattern name and corresponding ops. Default is None.
Returns:
paddle.static.CompiledProgram | paddle.static.Program: Program with quantization and dequantization ``operators``
"""
scope = paddle.static.global_scope() if not scope else scope
if config is None:
config = _quant_config_default
else:
assert isinstance(config, dict), "config must be dict"
config = _parse_configs(config)
_logger.info(f"quant_aware config {config}")
skip_tensor_list = []
same_scale_tensor_list = []
is_test = True if for_test else not config['scale_trainable']
if config['quant_post_first'] and for_test:
if 'quantizable_op_type' not in calib_config:
calib_config['quantizable_op_type'] = config['quantize_op_types']
exe = paddle.static.Executor() if executor is None else executor
post_training_quantization = PostTrainingQuantizationProgram(
exe,
program,
freeze_model=False,
skip_tensor_list=skip_tensor_list,
same_scale_tensor_list=same_scale_tensor_list,
batch_nums=10,
scale_dict=scale_dict,
return_graph=True,
**calib_config,
)
main_graph = post_training_quantization.quantize()
scale_dict = post_training_quantization._scale_dict
sub_graphs = list(main_graph.all_sub_graphs())
else:
main_graph = IrGraph(core.Graph(program.desc), for_test=for_test)
sub_graphs = list(main_graph.all_sub_graphs())
transform_pass_ops = []
quant_dequant_ops = []
if config.get('quant_config'):
transform_pass_ops = config[
'quant_config'
].weight_quant_operation_types
quant_dequant_ops = config[
'quant_config'
].activation_quant_operation_types
else:
for op_type in config['quantize_op_types']:
if op_type in TRANSFORM_PASS_OP_TYPES:
transform_pass_ops.append(op_type)
elif op_type in QUANT_DEQUANT_PASS_OP_TYPES:
quant_dequant_ops.append(op_type)
if len(transform_pass_ops) > 0:
transform_func = (
QuantizationTransformPassV2
if config['onnx_format']
else QuantizationTransformPass
)
transform_pass = transform_func(
scope=scope,
place=place,
weight_bits=config['weight_bits'],
activation_bits=config['activation_bits'],
activation_quantize_type=config['activation_quantize_type'],
weight_quantize_type=config['weight_quantize_type'],
window_size=config['window_size'],
moving_rate=config['moving_rate'],
quantizable_op_type=transform_pass_ops,
skip_pattern=config['not_quant_pattern'],
weight_quantize_func=weight_quantize_func,
act_quantize_func=act_quantize_func,
weight_preprocess_func=weight_preprocess_func,
act_preprocess_func=act_preprocess_func,
optimizer_func=optimizer_func,
executor=executor,
is_test=is_test,
)
for sub_graph in sub_graphs:
transform_pass.apply(sub_graph)
residual_pass = AddQuantDequantForResidual(
scope=scope,
place=place,
quant_bits=config['activation_bits'],
is_test=is_test,
)
for subgraph in sub_graphs:
residual_pass.apply(sub_graph)
if len(quant_dequant_ops) > 0:
qdq_func = (
AddQuantDequantPassV2
if config['onnx_format']
else AddQuantDequantPass
)
quant_dequant_pass = qdq_func(
scope=scope,
place=place,
moving_rate=config['moving_rate'],
quant_bits=config['activation_bits'],
skip_pattern=config['not_quant_pattern'],
quantizable_op_type=quant_dequant_ops,
is_test=is_test,
)
for sub_graph in sub_graphs:
quant_dequant_pass.apply(sub_graph)
out_scale_training_pass = OutScaleForTrainingPass(
scope=scope,
place=place,
moving_rate=config['moving_rate'],
is_test=is_test,
scale_dict=scale_dict,
)
for sub_graph in sub_graphs:
out_scale_training_pass.apply(sub_graph)
if (
(weight_preprocess_func is not None or act_preprocess_func is not None)
and not for_test
and not config['onnx_format']
):
_logger.info(
"When a preprocess_func is used in quant_aware, Need to save a mapping table to match variable names in the convert phase."
)
_logger.info(f"The mapping table is saved as '{VARS_MAPPING_TABLE}'.")
for sub_graph in sub_graphs:
save_dict(sub_graph.out_node_mapping_table)
# TODO: remove it.
if draw_graph:
main_graph.draw('./', 'graph.pdf')
if for_test or return_program:
quant_program = main_graph.to_program()
else:
quant_program = paddle.static.CompiledProgram(main_graph.graph)
if return_scale_dict:
return quant_program, scale_dict, model_type, pattern_ops
else:
return quant_program
def convert(program, place, config=None, scope=None, save_int8=False):
"""
convert quantized and well-trained ``program`` to final quantized
``program``that can be used to save ``inference model``.
Args:
program(paddle.static.Program): quantized and well-trained ``test program``.
place(paddle.CPUPlace or paddle.CUDAPlace): This parameter represents
the executor run on which device.
config(dict, optional): configs for convert. if set None, will use
default config. It must be same with config that used in
'quant_aware'. Default is None.
scope(paddle.static.Scope, optional): Scope records the mapping between
variable names and variables, similar to brackets in
programming languages. Usually users can use
`paddle.static.global_scope <https://www.paddlepaddle.org.cn/documentation/docs/zh/develop/api_cn/executor_cn/global_scope_cn.html>`_.
When ``None`` will use
`paddle.static.global_scope() <https://www.paddlepaddle.org.cn/documentation/docs/zh/develop/api_cn/executor_cn/global_scope_cn.html>`_
. Default: ``None``.
save_int8: Whether to return ``program`` which model parameters'
dtype is ``int8``. This parameter can only be used to
get model size. Default: ``False``.
Returns:
Tuple : freezed program which can be used for inference.
when ``save_int8`` is False, return ``freezed_program(paddle.static.Program)``.
when ``save_int8`` is True, return ``freezed_program(paddle.static.Program)``
and ``freezed_program_int8(paddle.static.Program)``
"""
scope = paddle.static.global_scope() if not scope else scope
if config is None:
config = _quant_config_default
else:
assert isinstance(config, dict), "config must be dict"
config = _parse_configs(config)
_logger.info(f"convert config {config}")
test_graph = IrGraph(core.Graph(program.desc), for_test=True)
if config['onnx_format']:
quant_weight_pass = QuantWeightPass(scope, place)
for sub_graph in test_graph.all_sub_graphs():
quant_weight_pass.apply(sub_graph)
out_scale_infer_pass = AddQuantDequantForInferencePass(
scope=scope, place=place, quant_bits=config['activation_bits']
)
for sub_graph in test_graph.all_sub_graphs():
out_scale_infer_pass.apply(sub_graph)
else:
out_scale_infer_pass = OutScaleForInferencePass(scope=scope)
for sub_graph in test_graph.all_sub_graphs():
out_scale_infer_pass.apply(sub_graph)
# Freeze the graph after training by adjusting the quantize
# operators' order for the inference.
freeze_pass = QuantizationFreezePass(
scope=scope,
place=place,
weight_bits=config['weight_bits'],
activation_bits=config['activation_bits'],
weight_quantize_type=config['weight_quantize_type'],
)
if os.path.exists(VARS_MAPPING_TABLE):
test_graph.out_node_mapping_table = load_dict()
for sub_graph in test_graph.all_sub_graphs():
freeze_pass.apply(sub_graph)
freezed_program = test_graph.to_program()
# Move sub blocks persistable var to global block
global_block = freezed_program.global_block()
for _op in global_block.ops:
if _op.type == "while":
_block_id = _op.attr("sub_block").id
_block = freezed_program.block(_block_id)
persistables = []
for _name, _var in _block.vars.items():
if _var.persistable:
global_block._clone_variable(_var)
persistables.append(_name)
for _name in persistables:
_block._remove_var(_name)
persistables.extend(_op.input('X'))
_op.desc.set_input("X", persistables)
assert not (save_int8 and config['onnx_format']), (
"When onnx_format=True, already saved int8 weight,so you can't set save_int8=True."
)
if save_int8:
convert_int8_pass = ConvertToInt8Pass(scope=scope, place=place)
for sub_graph in test_graph.all_sub_graphs():
convert_int8_pass.apply(sub_graph)
freezed_program_int8 = test_graph.to_program()
return freezed_program, freezed_program_int8
else:
return freezed_program
File diff suppressed because it is too large Load Diff
+293
View File
@@ -0,0 +1,293 @@
# Copyright (c) 2022 PaddlePaddle Authors. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import sys
import numpy as np
from ...base.framework import IrNode, Operator
from .quant_config import SUPPORT_QUANTIZATION_OP_DICT
_channelwise_quant_axis1_ops = [
'conv2d_transpose',
'mul',
'matmul',
'matmul_v2',
]
def _get_op_input_var_names(op):
"""
Get the input var names of the op.
Args:
op(IrNode, Operator): the input op.
Returns:
input_var_names or None.
"""
assert isinstance(op, (IrNode, Operator)), (
"The input op should be IrNode or Operator."
)
var_names = []
op_name = op.name() if isinstance(op, IrNode) else op.type
if op_name not in SUPPORT_QUANTIZATION_OP_DICT:
return []
name_list = SUPPORT_QUANTIZATION_OP_DICT[op_name][0]
for name in name_list:
var_name = op.input(name)
if isinstance(var_name, list):
var_names.extend(var_name)
else:
var_names.append(var_name)
return var_names
def _get_op_output_var_names(op):
""" """
assert isinstance(op, (IrNode, Operator)), (
"The input op should be IrNode or Operator."
)
var_names = []
op_name = op.name() if isinstance(op, IrNode) else op.type
if op_name not in SUPPORT_QUANTIZATION_OP_DICT:
return []
name_list = SUPPORT_QUANTIZATION_OP_DICT[op_name][1]
for name in name_list:
var_name = op.output(name)
if isinstance(var_name, list):
var_names.extend(var_name)
else:
var_names.append(var_name)
return var_names
def _get_input_name_index(op, input_var_name):
"""Get the input name and index of the var_name in the op"""
assert isinstance(op, (IrNode, Operator)), (
"The input op should be IrNode or Operator."
)
op_name = op.name() if isinstance(op, IrNode) else op.type
if op_name not in SUPPORT_QUANTIZATION_OP_DICT:
return None
res = None
for argname in SUPPORT_QUANTIZATION_OP_DICT[op_name][0]:
var_names = op.input(argname)
for index, name in enumerate(var_names):
if name == input_var_name:
res = (argname, index)
return res
def _get_output_name_index(op, output_var_name):
"""Get the output name and index of the var_name in the op"""
assert isinstance(op, (IrNode, Operator)), (
"The input op should be IrNode or Operator."
)
op_name = op.name() if isinstance(op, IrNode) else op.type
if op_name not in SUPPORT_QUANTIZATION_OP_DICT:
return None
name_list = SUPPORT_QUANTIZATION_OP_DICT[op_name][1]
res = None
for name in name_list:
var_name = op.output(name)
for index, val in enumerate(var_name):
if val == output_var_name:
res = (name, index)
return res
def load_variable_data(scope, var_name):
'''
Load variable value from scope
'''
var_node = scope.find_var(var_name)
assert var_node is not None, "Cannot find " + var_name + " in scope."
tensor = np.array(var_node.get_tensor())
if tensor.shape == ():
return tensor.reshape(1)
else:
return tensor
def set_variable_data(scope, place, var_name, np_value):
'''
Set the value of var node by name, if the node exits,
'''
assert isinstance(np_value, np.ndarray), (
'The type of value should be numpy array.'
)
var_node = scope.find_var(var_name)
if var_node is not None:
tensor = var_node.get_tensor()
tensor.set(np_value, place)
def quant_tensor(x, scale, quant_axis=0, weight_bits=8, onnx_format=False):
# symmetry quant
def _clip(x, scale):
x[x > scale] = scale
x[x < -scale] = -scale
return x
bnt = (1 << (weight_bits - 1)) - 1
if isinstance(scale, list) and len(scale) == 1:
scale = scale[0]
if isinstance(scale, list):
assert quant_axis in [-1, 0, 1], 'quant_axis should be 0 or 1 for now.'
for i, s in enumerate(scale):
if s == 0.0:
s = 1e-8
if quant_axis == 0:
if onnx_format:
x[i] = np.round(x[i] / s * bnt)
x[i] = np.clip(x[i], -bnt - 1, bnt)
else:
x[i] = _clip(x[i], s)
x[i] = x[i] / s * bnt
else:
if onnx_format:
x[:, i] = np.round(x[:, i] / s * bnt)
x[:, i] = np.clip(x[:, i], -bnt - 1, bnt)
else:
x[:, i] = _clip(x[:, i], s)
x[:, i] = x[:, i] / s * bnt
else:
scale = 1e-8 if scale == 0.0 else scale
if onnx_format:
x = np.round(x / scale * bnt)
x = np.clip(x, -bnt - 1, bnt)
else:
x = _clip(x, scale)
x = x / scale * bnt
return x
def dequant_tensor(x, scale, quant_axis=0, weight_bits=8):
assert quant_axis in [0, 1], 'quant_axis should be 0 or 1 for now.'
bnt = (1 << (weight_bits - 1)) - 1
if isinstance(scale, list):
for i, s in enumerate(scale):
if s == 0.0:
s = 1e-8
if quant_axis == 0:
x[i] = x[i] * s / bnt
else:
x[:, i] = x[:, i] * s / bnt
else:
scale = 1e-8 if scale == 0.0 else scale
x = x * scale / bnt
return x
def bias_correction_w(x, x_quant, scale_v, quant_axis, weight_bits=8):
'''
Bias correction for weight
'''
eps = 1e-8
bnt = (1 << (weight_bits - 1)) - 1
x_dequant = x_quant.copy()
if isinstance(scale_v, list):
if quant_axis == 0:
for i, s in enumerate(scale_v):
x_dequant[i] = x_dequant[i] * s / bnt
quant_bias = x - x_dequant
mean_bias = quant_bias.reshape(quant_bias.shape[0], -1).mean(-1)
std_orig = x.reshape(x.shape[0], -1).std(-1)
std_quant = x_dequant.reshape(x_dequant.shape[0], -1).std(-1)
std_bias = std_orig / (std_quant + eps)
else:
for i, s in enumerate(scale_v):
x_dequant[:, i] = x_quant[:, i] * s / bnt
quant_bias = x - x_dequant
mean_bias = np.array(
[quant_bias[:, i].mean() for i in range(quant_bias.shape[1])]
)
std_orig = np.array([x[:, i].std() for i in range(x.shape[1])])
std_quant = np.array(
[x_dequant[:, i].std() for i in range(x_dequant.shape[1])]
)
std_bias = std_orig / (std_quant + eps)
else:
x_dequant = x_quant * scale_v / bnt
mean_bias = (x - x_dequant).mean()
std_bias = x.std() / (x_dequant.std() + eps)
if mean_bias.ndim == 1:
std_bias = np.resize(std_bias, x.shape)
mean_bias = np.resize(mean_bias, x.shape)
x_dequant = (mean_bias + x_dequant) * std_bias
quantized_param_v = quant_tensor(
x_dequant, scale_v, quant_axis, weight_bits
)
return quantized_param_v
def stable_sigmoid(x):
sig = np.where(x < 0, np.exp(x) / (1 + np.exp(x)), 1 / (1 + np.exp(-x)))
return sig
def calculate_quant_cos_error(orig_tensor, qdq_tensor):
cos_sim = np.inner(orig_tensor.flatten(), qdq_tensor.flatten()) / (
np.linalg.norm(orig_tensor.flatten())
* np.linalg.norm(qdq_tensor.flatten())
)
return cos_sim
def move_persistable_var_to_global_block(program):
# Move sub blocks persistable var to global block
global_block = program.global_block()
for _op in global_block.ops:
if _op.type == "while":
_block_id = _op.attr("sub_block").id
_block = program.block(_block_id)
persistables = []
for _name, _var in _block.vars.items():
if _var.persistable:
global_block._clone_variable(_var)
persistables.append(_name)
for _name in persistables:
_block._remove_var(_name)
persistables.extend(_op.input('X'))
_op.desc.set_input("X", persistables)
def l2_loss(gt, pred):
return ((gt - pred) ** 2).mean()
class tqdm:
def __init__(self, total, bar_format='Loading|{bar}', ncols=80):
self.total = total
self.bar_format = bar_format
self.ncols = ncols
self.n = 0
def update(self, n=1):
self.n += n
a = "=" * round((self.n / self.total) * self.ncols)
b = " " * (self.ncols - len(a))
prefix = self.bar_format.split('|')[0]
sys.stderr.write(f"\r{prefix}|{a}=>{b}| {self.n}/{self.total}")
sys.stderr.flush()
def __enter__(self):
return self
def __exit__(self, exc_type, exc_val, exc_tb):
sys.stderr.write('\n')