# copyright (c) 2020 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 sys import time import unittest import numpy as np import paddle from paddle import base from paddle.inference import Config, create_predictor 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( '--quant_model', type=str, default='', help='A path to a Quant model.' ) parser.add_argument( '--fp32_model', type=str, default='', help='A path to an FP32 model. If empty, the Quant model will be used for FP32 inference.', ) parser.add_argument('--infer_data', type=str, default='', help='Data file.') parser.add_argument( '--labels', type=str, default='', help='File with labels.' ) 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.', ) parser.add_argument( '--ops_to_quantize', type=str, default='', help='A comma separated list of operators to quantize. Only quantizable operators are taken into account. If the option is not used, an attempt to quantize all quantizable operators will be made.', ) parser.add_argument( '--op_ids_to_skip', type=str, default='', help='A comma separated list of operator ids to skip in quantization.', ) parser.add_argument( '--targets', type=str, default='quant,int8,fp32', help='A comma separated list of inference types to run ("int8", "fp32", "quant"). Default: "quant,int8,fp32"', ) parser.add_argument( '--debug', action='store_true', help='If used, the graph of Quant model is drawn.', ) test_args, args = parser.parse_known_args(namespace=unittest) return test_args, sys.argv[:1] + args class QuantInt8NLPComparisonTest(unittest.TestCase): """ Test for accuracy comparison of Quant FP32 and INT8 NLP inference. """ def _reader_creator(self, data_file=None, labels_file=None): assert data_file, "The dataset file is missing." assert labels_file, "The labels file is missing." def reader(): with ( open(data_file, 'r') as df, open(labels_file, 'r') as lf, ): data_lines = df.readlines() labels_lines = lf.readlines() assert len(data_lines) == len(labels_lines), ( "The number of labels does not match the length of the dataset." ) for i in range(len(data_lines)): data_fields = data_lines[i].split(';') assert len(data_fields) >= 2, ( "The number of data fields in the dataset is less than 2" ) buffers = [] shape = [] for j in range(2): data = data_fields[j].split(':') assert len(data) >= 2, ( "Size of data in the dataset is less than 2" ) # Shape is stored under index 0, while data under 1 shape = data[0].split() shape.pop(0) shape_np = np.array(shape).astype("int64") buffer_i = data[1].split() buffer_np = np.array(buffer_i).astype("int64") buffer_np.shape = tuple(shape_np) buffers.append(buffer_np) yield buffers[0], buffers[1], int(labels_lines[i]) return reader def _get_batch_correct(self, batch_output=None, labels=None): total = len(batch_output) assert total > 0, "The batch output is empty." correct = 0 for n, output in enumerate(batch_output): max_idx = np.where(output == output.max()) if max_idx[0] == labels[n]: correct += 1 return correct def set_config( self, model_path, target='quant', ): config = Config(model_path) config.disable_gpu() config.switch_specify_input_names(True) config.switch_ir_optim(True) config.switch_use_feed_fetch_ops(True) config.enable_onednn() if target == 'int8': config.enable_onednn_int8(self._quantized_ops) config.delete_pass( "constant_folding_pass" ) # same reason as in analyzer_ernie_int8_tester.cc return config def _predict( self, test_reader=None, model_path=None, batch_size=1, batch_num=1, skip_batch_num=0, target='fp32', ): assert target in ['quant', 'int8', 'fp32'] print(f"target: {target}, model path: {model_path}") config = self.set_config( model_path, target, ) predictor = create_predictor(config) input_names = predictor.get_input_names() output_names = predictor.get_output_names() total_correct = 0 total_samples = 0 batch_times = [] ppses = [] # predictions per second 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() # check data inputs = [] inputs.append(np.array([x[0] for x in data])) inputs.append(np.array([x[1] for x in data])) labels = np.array([x[2] for x in data]) for i, name in enumerate(input_names): input_tensor = predictor.get_input_handle(name) input_tensor.reshape(inputs[i].shape) input_tensor.copy_from_cpu(inputs[i].copy()) start = time.time() predictor.run() batch_time = (time.time() - start) * 1000 # in milliseconds out = [] out = predictor.get_output_handle(output_names[0]).copy_to_cpu() batch_times.append(batch_time) batch_correct = self._get_batch_correct(out, labels) batch_len = len(labels) total_samples += batch_len total_correct += batch_correct batch_acc = float(batch_correct) / float(batch_len) pps = batch_len / batch_time * 1000 ppses.append(pps) latency = batch_time / batch_len iters += 1 appx = ' (warm-up)' if iters <= skip_batch_num else '' _logger.info( f'batch {iters}{appx}, acc: {batch_acc:.4f}, latency: {latency:.4f} ms, predictions per sec: {pps:.2f}' ) # Postprocess benchmark data infer_total_time = time.time() - infer_start_time batch_latencies = batch_times[skip_batch_num:] batch_latency_avg = np.average(batch_latencies) latency_avg = batch_latency_avg / batch_size ppses = ppses[skip_batch_num:] pps_avg = np.average(ppses) acc_avg = float(np.sum(total_correct)) / float(total_samples) _logger.info(f'Total inference run time: {infer_total_time:.2f} s') return acc_avg, pps_avg, latency_avg def _print_performance(self, title, pps, lat): _logger.info( f'{title}: avg predictions per sec: {pps:.2f}, avg latency: {lat:.4f} ms' ) def _print_accuracy(self, title, acc): _logger.info(f'{title}: avg accuracy: {acc:.6f}') def _summarize_performance( self, quant_pps, quant_lat, int8_pps, int8_lat, fp32_pps, fp32_lat ): _logger.info('--- Performance summary ---') self._print_performance('QUANT', quant_pps, quant_lat) self._print_performance('INT8', int8_pps, int8_lat) if fp32_lat >= 0: self._print_performance('FP32', fp32_pps, fp32_lat) def _summarize_accuracy(self, quant_acc, int8_acc, fp32_acc): _logger.info('--- Accuracy summary ---') self._print_accuracy('Quant', quant_acc) self._print_accuracy('INT8', int8_acc) if fp32_acc >= 0: self._print_accuracy('FP32', fp32_acc) def _compare_accuracy(self, threshold, quant_acc, int8_acc): _logger.info( f'Accepted accuracy drop threshold: {threshold}. (condition: (Quant_acc - INT8_acc) <= threshold)' ) # Random outputs give accuracy about 0.33, we assume valid accuracy to be at least 0.5 assert quant_acc > 0.5 assert int8_acc > 0.5 assert quant_acc - int8_acc <= threshold def _strings_from_csv(self, string): return {s.strip() for s in string.split(',')} def _ints_from_csv(self, string): return set(map(int, string.split(','))) def test_graph_transformation(self): if not base.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.' ) fp32_model_path = test_case_args.fp32_model labels_path = test_case_args.labels 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 self._quantized_ops = set() if test_case_args.ops_to_quantize: self._quantized_ops = self._strings_from_csv( test_case_args.ops_to_quantize ) self._op_ids_to_skip = {-1} if test_case_args.op_ids_to_skip: self._op_ids_to_skip = self._ints_from_csv( test_case_args.op_ids_to_skip ) self._targets = self._strings_from_csv(test_case_args.targets) assert self._targets.intersection({'quant', 'int8', 'fp32'}), ( 'The --targets option, if used, must contain at least one of the targets: "quant", "int8", "fp32".' ) _logger.info('Quant & INT8 prediction run.') _logger.info(f'Quant model: {quant_model_path}') if fp32_model_path: _logger.info(f'FP32 model: {fp32_model_path}') _logger.info(f'Dataset: {data_path}') _logger.info(f'Labels: {labels_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( 'Quantized ops: {}.'.format( ','.join(self._quantized_ops) if self._quantized_ops else 'all quantizable' ) ) _logger.info( 'Op ids to skip quantization: {}.'.format( ','.join(map(str, self._op_ids_to_skip)) if test_case_args.op_ids_to_skip else 'none' ) ) _logger.info('Targets: {}.'.format(','.join(self._targets))) if 'quant' in self._targets: _logger.info('--- Quant prediction start ---') val_reader = paddle.batch( self._reader_creator(data_path, labels_path), batch_size=batch_size, ) quant_acc, quant_pps, quant_lat = self._predict( val_reader, quant_model_path, batch_size, batch_num, skip_batch_num, target='quant', ) self._print_performance('Quant', quant_pps, quant_lat) self._print_accuracy('Quant', quant_acc) if 'int8' in self._targets: _logger.info('--- INT8 prediction start ---') val_reader = paddle.batch( self._reader_creator(data_path, labels_path), batch_size=batch_size, ) int8_acc, int8_pps, int8_lat = self._predict( val_reader, quant_model_path, batch_size, batch_num, skip_batch_num, target='int8', ) self._print_performance('INT8', int8_pps, int8_lat) self._print_accuracy('INT8', int8_acc) fp32_acc = fp32_pps = fp32_lat = -1 if 'fp32' in self._targets and fp32_model_path: _logger.info('--- FP32 prediction start ---') val_reader = paddle.batch( self._reader_creator(data_path, labels_path), batch_size=batch_size, ) fp32_acc, fp32_pps, fp32_lat = self._predict( val_reader, fp32_model_path, batch_size, batch_num, skip_batch_num, target='fp32', ) self._print_performance('FP32', fp32_pps, fp32_lat) self._print_accuracy('FP32', fp32_acc) if {'int8', 'quant', 'fp32'}.issubset(self._targets): self._summarize_performance( quant_pps, quant_lat, int8_pps, int8_lat, fp32_pps, fp32_lat ) if {'int8', 'quant'}.issubset(self._targets): self._summarize_accuracy(quant_acc, int8_acc, fp32_acc) self._compare_accuracy(acc_diff_threshold, quant_acc, int8_acc) if __name__ == '__main__': global test_case_args test_case_args, remaining_args = parse_args() unittest.main(argv=remaining_args)