345 lines
12 KiB
Python
345 lines
12 KiB
Python
# copyright (c) 2019 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 argparse
|
|
import logging
|
|
import os
|
|
import struct
|
|
import sys
|
|
import time
|
|
import unittest
|
|
|
|
import numpy as np
|
|
|
|
import paddle
|
|
from paddle.base.framework import IrGraph
|
|
from paddle.framework import core
|
|
from paddle.static.quantization import QuantInt8OnednnPass
|
|
|
|
paddle.enable_static()
|
|
|
|
logging.basicConfig(format='%(asctime)s-%(levelname)s: %(message)s')
|
|
_logger = logging.getLogger(__name__)
|
|
_logger.setLevel(logging.INFO)
|
|
|
|
|
|
def parse_args():
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument('--batch_size', type=int, default=1, help='Batch size.')
|
|
parser.add_argument(
|
|
'--skip_batch_num',
|
|
type=int,
|
|
default=0,
|
|
help='Number of the first minibatches to skip in performance statistics.',
|
|
)
|
|
parser.add_argument(
|
|
'--debug',
|
|
action='store_true',
|
|
help='If used, the graph of Quant model is drawn.',
|
|
)
|
|
parser.add_argument(
|
|
'--quant_model', type=str, default='', help='A path to a Quant model.'
|
|
)
|
|
parser.add_argument('--infer_data', type=str, default='', help='Data file.')
|
|
parser.add_argument(
|
|
'--batch_num',
|
|
type=int,
|
|
default=0,
|
|
help='Number of batches to process. 0 or less means whole dataset. Default: 0.',
|
|
)
|
|
parser.add_argument(
|
|
'--acc_diff_threshold',
|
|
type=float,
|
|
default=0.01,
|
|
help='Accepted accuracy difference threshold.',
|
|
)
|
|
|
|
test_args, args = parser.parse_known_args(namespace=unittest)
|
|
return test_args, sys.argv[:1] + args
|
|
|
|
|
|
class QuantInt8ImageClassificationComparisonTest(unittest.TestCase):
|
|
"""
|
|
Test for accuracy comparison of Quant FP32 and INT8 Image Classification inference.
|
|
"""
|
|
|
|
def _reader_creator(self, data_file='data.bin'):
|
|
def reader():
|
|
with open(data_file, 'rb') as fp:
|
|
num = fp.read(8)
|
|
num = struct.unpack('q', num)[0]
|
|
imgs_offset = 8
|
|
img_ch = 3
|
|
img_w = 224
|
|
img_h = 224
|
|
img_pixel_size = 4
|
|
img_size = img_ch * img_h * img_w * img_pixel_size
|
|
label_size = 8
|
|
labels_offset = imgs_offset + num * img_size
|
|
|
|
step = 0
|
|
while step < num:
|
|
fp.seek(imgs_offset + img_size * step)
|
|
img = fp.read(img_size)
|
|
img = struct.unpack_from(f'{img_ch * img_w * img_h}f', img)
|
|
img = np.array(img)
|
|
img.shape = (img_ch, img_w, img_h)
|
|
fp.seek(labels_offset + label_size * step)
|
|
label = fp.read(label_size)
|
|
label = struct.unpack('q', label)[0]
|
|
yield img, int(label)
|
|
step += 1
|
|
|
|
return reader
|
|
|
|
def _get_batch_accuracy(self, batch_output=None, labels=None):
|
|
total = 0
|
|
correct = 0
|
|
correct_5 = 0
|
|
for n, result in enumerate(batch_output):
|
|
index = result.argsort()
|
|
top_1_index = index[-1]
|
|
top_5_index = index[-5:]
|
|
total += 1
|
|
if top_1_index == labels[n]:
|
|
correct += 1
|
|
if labels[n] in top_5_index:
|
|
correct_5 += 1
|
|
acc1 = float(correct) / float(total)
|
|
acc5 = float(correct_5) / float(total)
|
|
return acc1, acc5
|
|
|
|
def _prepare_for_fp32_onednn(self, graph):
|
|
ops = graph.all_op_nodes()
|
|
for op_node in ops:
|
|
name = op_node.name()
|
|
if name in ['depthwise_conv2d']:
|
|
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, op_node.input("Filter")[0]
|
|
)
|
|
output_var_node = graph._find_node_by_name(
|
|
graph.all_var_nodes(), op_node.output("Output")[0]
|
|
)
|
|
attrs = {
|
|
name: op_node.op().attr(name)
|
|
for name in op_node.op().attr_names()
|
|
}
|
|
|
|
conv_op_node = graph.create_op_node(
|
|
op_type='conv2d',
|
|
attrs=attrs,
|
|
inputs={'Input': input_var_node, 'Filter': weight_var_node},
|
|
outputs={'Output': output_var_node},
|
|
)
|
|
|
|
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)
|
|
|
|
return graph
|
|
|
|
def _predict(
|
|
self,
|
|
test_reader=None,
|
|
model_path=None,
|
|
batch_size=1,
|
|
batch_num=1,
|
|
skip_batch_num=0,
|
|
transform_to_int8=False,
|
|
):
|
|
place = paddle.CPUPlace()
|
|
exe = paddle.static.Executor(place)
|
|
inference_scope = paddle.static.global_scope()
|
|
with paddle.static.scope_guard(inference_scope):
|
|
if os.path.exists(os.path.join(model_path, '__model__')):
|
|
[
|
|
inference_program,
|
|
feed_target_names,
|
|
fetch_targets,
|
|
] = paddle.static.io.load_inference_model(
|
|
model_path, exe, model_filename=None, params_filename=None
|
|
)
|
|
else:
|
|
[
|
|
inference_program,
|
|
feed_target_names,
|
|
fetch_targets,
|
|
] = paddle.static.load_inference_model(
|
|
model_path,
|
|
exe,
|
|
model_filename='model',
|
|
params_filename='params',
|
|
)
|
|
|
|
graph = IrGraph(core.Graph(inference_program.desc), for_test=True)
|
|
if self._debug:
|
|
graph.draw('.', 'quant_orig', graph.all_op_nodes())
|
|
if transform_to_int8:
|
|
onednn_int8_pass = QuantInt8OnednnPass(
|
|
_scope=inference_scope, _place=place
|
|
)
|
|
graph = onednn_int8_pass.apply(graph)
|
|
else:
|
|
graph = self._prepare_for_fp32_onednn(graph)
|
|
|
|
inference_program = graph.to_program()
|
|
|
|
dshape = [3, 224, 224]
|
|
outputs = []
|
|
infer_accs1 = []
|
|
infer_accs5 = []
|
|
fpses = []
|
|
batch_times = []
|
|
total_samples = 0
|
|
iters = 0
|
|
infer_start_time = time.time()
|
|
for data in test_reader():
|
|
if batch_num > 0 and iters >= batch_num:
|
|
break
|
|
if iters == skip_batch_num:
|
|
total_samples = 0
|
|
infer_start_time = time.time()
|
|
images = [x[0].reshape(dshape) for x in data]
|
|
images = np.array(images).astype('float32')
|
|
labels = np.array([x[1] for x in data]).astype('int64')
|
|
|
|
start = time.time()
|
|
out = exe.run(
|
|
inference_program,
|
|
feed={feed_target_names[0]: images},
|
|
fetch_list=fetch_targets,
|
|
)
|
|
batch_time = (time.time() - start) * 1000 # in milliseconds
|
|
outputs.append(out[0])
|
|
batch_acc1, batch_acc5 = self._get_batch_accuracy(
|
|
out[0], labels
|
|
)
|
|
infer_accs1.append(batch_acc1)
|
|
infer_accs5.append(batch_acc5)
|
|
samples = len(data)
|
|
total_samples += samples
|
|
batch_times.append(batch_time)
|
|
fps = samples / batch_time * 1000
|
|
fpses.append(fps)
|
|
iters += 1
|
|
appx = ' (warm-up)' if iters <= skip_batch_num else ''
|
|
_logger.info(
|
|
f'batch {iters}{appx}, acc1: {batch_acc1:.4f}, acc5: {batch_acc5:.4f}, '
|
|
f'latency: {batch_time / batch_size:.4f} ms, fps: {fps:.2f}'
|
|
)
|
|
|
|
# Postprocess benchmark data
|
|
batch_latencies = batch_times[skip_batch_num:]
|
|
batch_latency_avg = np.average(batch_latencies)
|
|
latency_avg = batch_latency_avg / batch_size
|
|
fpses = fpses[skip_batch_num:]
|
|
fps_avg = np.average(fpses)
|
|
infer_total_time = time.time() - infer_start_time
|
|
acc1_avg = np.mean(infer_accs1)
|
|
acc5_avg = np.mean(infer_accs5)
|
|
_logger.info(f'Total inference run time: {infer_total_time:.2f} s')
|
|
|
|
return outputs, acc1_avg, acc5_avg, fps_avg, latency_avg
|
|
|
|
def _summarize_performance(self, fp32_fps, fp32_lat, int8_fps, int8_lat):
|
|
_logger.info('--- Performance summary ---')
|
|
_logger.info(
|
|
f'FP32: avg fps: {fp32_fps:.2f}, avg latency: {fp32_lat:.4f} ms'
|
|
)
|
|
_logger.info(
|
|
f'INT8: avg fps: {int8_fps:.2f}, avg latency: {int8_lat:.4f} ms'
|
|
)
|
|
|
|
def _compare_accuracy(
|
|
self, fp32_acc1, fp32_acc5, int8_acc1, int8_acc5, threshold
|
|
):
|
|
_logger.info('--- Accuracy summary ---')
|
|
_logger.info(
|
|
f'Accepted top1 accuracy drop threshold: {threshold}. (condition: (FP32_top1_acc - IN8_top1_acc) <= threshold)'
|
|
)
|
|
_logger.info(
|
|
f'FP32: avg top1 accuracy: {fp32_acc1:.4f}, avg top5 accuracy: {fp32_acc5:.4f}'
|
|
)
|
|
_logger.info(
|
|
f'INT8: avg top1 accuracy: {int8_acc1:.4f}, avg top5 accuracy: {int8_acc5:.4f}'
|
|
)
|
|
assert fp32_acc1 > 0.0
|
|
assert int8_acc1 > 0.0
|
|
assert fp32_acc1 - int8_acc1 <= threshold
|
|
|
|
def test_graph_transformation(self):
|
|
if not core.is_compiled_with_onednn():
|
|
return
|
|
|
|
quant_model_path = test_case_args.quant_model
|
|
assert quant_model_path, (
|
|
'The Quant model path cannot be empty. Please, use the --quant_model option.'
|
|
)
|
|
data_path = test_case_args.infer_data
|
|
assert data_path, (
|
|
'The dataset path cannot be empty. Please, use the --infer_data option.'
|
|
)
|
|
batch_size = test_case_args.batch_size
|
|
batch_num = test_case_args.batch_num
|
|
skip_batch_num = test_case_args.skip_batch_num
|
|
acc_diff_threshold = test_case_args.acc_diff_threshold
|
|
self._debug = test_case_args.debug
|
|
|
|
_logger.info('Quant FP32 & INT8 prediction run.')
|
|
_logger.info(f'Quant model: {quant_model_path}')
|
|
_logger.info(f'Dataset: {data_path}')
|
|
_logger.info(f'Batch size: {batch_size}')
|
|
_logger.info(f'Batch number: {batch_num}')
|
|
_logger.info(f'Accuracy drop threshold: {acc_diff_threshold}.')
|
|
|
|
_logger.info('--- Quant FP32 prediction start ---')
|
|
val_reader = paddle.batch(
|
|
self._reader_creator(data_path), batch_size=batch_size
|
|
)
|
|
fp32_output, fp32_acc1, fp32_acc5, fp32_fps, fp32_lat = self._predict(
|
|
val_reader,
|
|
quant_model_path,
|
|
batch_size,
|
|
batch_num,
|
|
skip_batch_num,
|
|
transform_to_int8=False,
|
|
)
|
|
_logger.info('--- Quant INT8 prediction start ---')
|
|
val_reader = paddle.batch(
|
|
self._reader_creator(data_path), batch_size=batch_size
|
|
)
|
|
int8_output, int8_acc1, int8_acc5, int8_fps, int8_lat = self._predict(
|
|
val_reader,
|
|
quant_model_path,
|
|
batch_size,
|
|
batch_num,
|
|
skip_batch_num,
|
|
transform_to_int8=True,
|
|
)
|
|
|
|
self._summarize_performance(fp32_fps, fp32_lat, int8_fps, int8_lat)
|
|
self._compare_accuracy(
|
|
fp32_acc1, fp32_acc5, int8_acc1, int8_acc5, acc_diff_threshold
|
|
)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
global test_case_args
|
|
test_case_args, remaining_args = parse_args()
|
|
unittest.main(argv=remaining_args)
|