Improve Transformer Benchmark for FP16 (#3970)

Disable ORT in offline optimization script (ORT could generate some fused ops (like FusedGemm) which cannot be converted to fp16).
Remove some models from benchmark until we have optimizations for them.
This commit is contained in:
Tianlei Wu 2020-05-17 21:50:45 -07:00 committed by GitHub
parent 0d11649bb3
commit 523d70f667
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 81 additions and 60 deletions

View file

@ -16,6 +16,15 @@ This tool can be installed using pip as follows:
pip install onnxruntime-tools
```
## Export a transformer model to ONNX
PyTorch could export model to ONNX. The tf2onnx and keras2onnx tools can be used to convert model that trained by Tensorflow.
Huggingface transformers has a [notebook](https://github.com/huggingface/transformers/blob/master/notebooks/04-onnx-export.ipynb) shows an example of exporting a pretrained model to ONNX.
For Keras2onnx, please refer to its [example script](https://github.com/onnx/keras-onnx/blob/master/applications/nightly_build/test_transformers.py).
For tf2onnx, please refer to its [BERT tutorial](https://github.com/onnx/tensorflow-onnx/blob/master/tutorials/BertTutorial.ipynb).
## Model Optimizer
In your python code, you can use it like the following:
```python
@ -25,29 +34,16 @@ optimized_model.convert_model_float32_to_float16()
optimized_model.save_model_to_file("gpt2_fp16.onnx")
```
You can also use command like the following to optimize model:
```console
python -m onnxruntime_tools.optimizer_cli --input gpt2.onnx --output gpt2_opt.onnx --model_type gpt2
```
If you want to use the latest script, you can get script files from [here](https://github.com/microsoft/onnxruntime/tree/master/onnxruntime/python/tools/transformers/). Then run it like the following:
```console
python optimizer.py --input gpt2.onnx --output gpt2_opt.onnx --model_type gpt2
```
## Export a transformer model to ONNX
PyTorch could export model to ONNX. The tf2onnx and keras2onnx tools can be used to convert model that trained by Tensorflow.
Huggingface transformers has a [notebook](https://github.com/huggingface/transformers/blob/master/notebooks/04-onnx-export.ipynb) shows an example of exporting a pretrained model to ONNX.
For Keras2onnx, please refere to its [example script](https://github.com/onnx/keras-onnx/blob/master/applications/nightly_build/test_transformers.py).
For tf2onnx, please refer to [its BERT tutorial](https://github.com/onnx/tensorflow-onnx/blob/master/tutorials/BertTutorial.ipynb).
## Model Optimizer
Example of using the script optimizer.py to optimize a BERT-large model to run in V100 GPU:
You can also use command line. Example of optimizing a BERT-large model to use mixed precision (float16):
```console
python -m onnxruntime_tools.optimizer_cli --input bert_large.onnx --output bert_large_fp16.onnx --num_heads 16 --hidden_size 1024 --float16
```
You can also download the latest script files from [here](https://github.com/microsoft/onnxruntime/tree/master/onnxruntime/python/tools/transformers/). Then run it like the following:
```console
python optimizer.py --input gpt2.onnx --output gpt2_opt.onnx --model_type gpt2
```
### Options
See below for description of some options of optimizer.py:
@ -64,6 +60,10 @@ See below for description of some options of optimizer.py:
Exported model ususally uses int64 tensor as input. If this flag is specified, int32 tensors will be used as input, and it could avoid un-necessary Cast nodes and get better performance.
- **float16**: (*optional*)
By default, model uses float32 in computation. If this flag is specified, half-precision float will be used. This option is recommended for NVidia GPU with Tensor Core like V100 and T4. For older GPUs, float32 is likely faster.
**use_gpu**: (*optional*)
When opt_level > 1, please set this flag for GPU inference.
- **opt_level**: (*optional*)
Set a proper graph optimization level of OnnxRuntime: 0 - disable all (default), 1 - basic, 2 - extended, 99 - all. If the value is positive, OnnxRuntime will be used to optimize graph first.
- **verbose**: (*optional*)
Print verbose information when this flag is specified.
@ -86,9 +86,11 @@ For GPT2 models, current optimization does not support past state (both inputs a
## Benchmark
There is a benchmark script that measure inference performance of OnnxRuntime, PyTorch or PyTorch+TorchScript on pretrained models of Huggingface Transformers.
The benchmark script requires PyTorch be installed.
You can run benchmark script to see the inference speed of OnnxRuntime. Here is an example to run benchmark on pretrained model bert-base-cased on GPU.
Here is an example to run benchmark on pretrained model bert-base-cased on GPU.
```console
python -m onnxruntime_tools.transformers.benchmark -g -m bert-base-cased -o -v -b 0
@ -96,17 +98,17 @@ python -m onnxruntime_tools.transformers.benchmark -g -m bert-base-cased -o
python -m onnxruntime_tools.transformers.benchmark -g -m bert-base-cased -e torch
python -m onnxruntime_tools.transformers.benchmark -g -m bert-base-cased -e torchscript
```
The first command will generate ONNX models (both before and after optimizations), but not run performance tests since we set batch size to 0. The other three commands will run performance test on three engines: OnnxRuntime, PyTorch and PyTorch+TorchScript.
The first command will generate ONNX models (both before and after optimizations), but not run performance tests since batch size is 0. The other three commands will run performance test on each of three engines: OnnxRuntime, PyTorch and PyTorch+TorchScript.
If you remove -o parameter, optimizer is not used in benchmark.
If you remove -o parameter, optimizer script is not used in benchmark.
If your GPU (like V100 or T4) has TensorCore, you can append --fp16 to the above commands to enable mixed precision using float16.
If you want to benchmark on CPU, you can remove -g option in the commands.
Note that our current benchmark on GPT2 model has disabled past state from inputs and outputs.
Note that our current benchmark on GPT2 and DistilGPT2 models has disabled past state from inputs and outputs.
By default, ONNX model has only one input (input_ids). You can use -i parameter to test models with more inputs. For example, we can add "-i 3" to command line to test a bert model with 3 inputs (input_ids, token_type_ids and attention_mask). The performance result might be different. This option only supports OnnxRuntime right now.
By default, ONNX model has only one input (input_ids). You can use -i parameter to test models with multiple inputs. For example, we can add "-i 3" to command line to test a bert model with 3 inputs (input_ids, token_type_ids and attention_mask). This option only supports OnnxRuntime right now.
## Model Verification

View file

@ -58,15 +58,16 @@ MODELS = {
# No past state inputs for GPT models.
"gpt2": (["input_ids"], 11, False, "gpt2"), # no past state inputs & outputs
"distilgpt2": (["input_ids"], 11, False, "gpt2"), # no past state inputs & outputs
"openai-gpt": (["input_ids"], 11, False, "gpt2"), # no past state inputs
#"openai-gpt": (["input_ids"], 11, False, "gpt2"), # no past state inputs
# Models uses Einsum, which need opset version 12 and PyTorch 1.5.0 or above.
# Currently OnnxRuntime lacks cuda op for Einsum. GPU inference will be very slow.
"albert-base-v2": (["input_ids"], 12, False, "bert"),
"xlnet-base-cased": (["input_ids"], 12, False, "bert"),
#"albert-base-v2": (["input_ids"], 12, False, "bert"),
#"xlnet-base-cased": (["input_ids"], 12, False, "bert"),
# This model is very large. Need use_external_data_format=True to export it.
"xlm-mlm-en-2048": (["input_ids"], 11, True, "bert"),
#"xlm-mlm-en-2048": (["input_ids"], 11, True, "bert"),
}
cpu_count = psutil.cpu_count(logical=True)
@ -93,9 +94,12 @@ def load_pretrained_model(model_name, config, cache_dir):
return AutoModel.from_pretrained(model_name, config=config, cache_dir=cache_dir)
def create_onnxruntime_session(onnx_model_path, use_gpu):
def create_onnxruntime_session(onnx_model_path, use_gpu, enable_all_optimization):
import onnxruntime
sess_options = onnxruntime.SessionOptions()
sess_options.graph_optimization_level = onnxruntime.GraphOptimizationLevel.ORT_ENABLE_ALL
if not enable_all_optimization:
sess_options.graph_optimization_level = onnxruntime.GraphOptimizationLevel.ORT_ENABLE_BASIC
if (not use_gpu) and (version.parse(onnxruntime.__version__) < version.parse('1.3.0')):
# Set intra_op_num_threads = 1 to enable OpenMP for onnxruntime 1.2.0 (cpu)
@ -160,9 +164,8 @@ def build_dynamic_axes(example_inputs, outputs_flatten):
return dynamic_axes, output_names
def validate_onnx_model(onnx_model_filename, example_inputs, example_outputs_flatten):
use_gpu = "_fp16" in onnx_model_filename
test_session = create_onnxruntime_session(onnx_model_filename, use_gpu)
def validate_onnx_model(onnx_model_filename, example_inputs, example_outputs_flatten, use_gpu):
test_session = create_onnxruntime_session(onnx_model_filename, use_gpu, enable_all_optimization=False)
if test_session is None:
logger.error(f"{onnx_model_filename} is an invalid ONNX model")
return False
@ -179,18 +182,20 @@ def validate_onnx_model(onnx_model_filename, example_inputs, example_outputs_fla
for i in range(len(example_outputs_flatten)):
if not numpy.allclose(example_ort_outputs[i], example_outputs_flatten[i].cpu(), rtol=1e-03, atol=1e-03):
logger.error(f"Value of output tensor {i} is not close to expected result")
abs_diff = numpy.amax(numpy.abs(example_ort_outputs[i] - example_outputs_flatten[i].cpu()))
logger.error(f"Output tensor {i} is not close to expected result. Max diff={abs_diff}")
return False
logger.info(f"inference result of onnxruntime is validated on {onnx_model_filename}")
return True
optimize_model_statistics = {}
model_fusion_statistics = {}
def optimize_onnx_model(onnx_model_filename, model_type, num_attention_heads, hidden_size, fp16, overwrite):
optimized_model_filename = onnx_model_filename.replace(".onnx", "_fp16.onnx" if fp16 else "_fp32.onnx")
def optimize_onnx_model(onnx_model_filename, model_type, num_attention_heads, hidden_size, use_gpu, fp16, overwrite):
suffix = "_fp{}_{}.onnx".format(16 if fp16 else 32, "gpu" if use_gpu else "cpu")
optimized_model_filename = onnx_model_filename.replace(".onnx", suffix)
if overwrite or not os.path.exists(optimized_model_filename):
from optimizer import optimize_model
from BertOnnxModel import BertOptimizationOptions
@ -205,16 +210,22 @@ def optimize_onnx_model(onnx_model_filename, model_type, num_attention_heads, hi
hidden_size=hidden_size,
opt_level=99,
optimization_options=optimization_options,
use_gpu=use_gpu,
only_onnxruntime=True)
optimize_model_statistics[onnx_model_filename] = opt_model.get_fused_operator_statistics()
model_fusion_statistics[onnx_model_filename] = opt_model.get_fused_operator_statistics()
# Use script to optimize model.
# Use opt_level <= 1 for models to be converted to fp16, because some fused op (like FusedGemm) has only fp32 and no fp16.
# It is better to be conservative so we use opt_level=0 here, in case MemcpyFromHost is added to the graph by OnnxRuntime.
opt_model = optimize_model(onnx_model_filename,
model_type,
num_heads=num_attention_heads,
hidden_size=hidden_size,
opt_level=99)
optimize_model_statistics[optimized_model_filename] = opt_model.get_fused_operator_statistics()
opt_level=0,
optimization_options=optimization_options,
use_gpu=use_gpu,
only_onnxruntime=False)
model_fusion_statistics[optimized_model_filename] = opt_model.get_fused_operator_statistics()
if fp16:
opt_model.convert_model_float32_to_float16()
@ -224,7 +235,7 @@ def optimize_onnx_model(onnx_model_filename, model_type, num_attention_heads, hi
return optimized_model_filename
def export_onnx_model(model_name, cache_dir, input_names, fp16, optimize_onnx, validate_onnx, overwrite):
def export_onnx_model(model_name, cache_dir, input_names, use_gpu, fp16, optimize_onnx, validate_onnx, overwrite):
config = AutoConfig.from_pretrained(model_name, cache_dir=cache_dir)
model = load_pretrained_model(model_name, config=config, cache_dir=cache_dir)
model.cpu()
@ -262,15 +273,16 @@ def export_onnx_model(model_name, cache_dir, input_names, fp16, optimize_onnx, v
is_valid_onnx_model = True
if validate_onnx:
is_valid_onnx_model = validate_onnx_model(onnx_model_filename, example_inputs, example_outputs_flatten)
is_valid_onnx_model = validate_onnx_model(onnx_model_filename, example_inputs, example_outputs_flatten, use_gpu)
if optimize_onnx or fp16:
model_type = MODELS[model_name][3]
onnx_model_filename = optimize_onnx_model(onnx_model_filename, model_type, config.num_attention_heads,
config.hidden_size, fp16, overwrite)
config.hidden_size, use_gpu, fp16, overwrite)
if validate_onnx:
is_valid_onnx_model = validate_onnx_model(onnx_model_filename, example_inputs, example_outputs_flatten)
is_valid_onnx_model = validate_onnx_model(onnx_model_filename, example_inputs, example_outputs_flatten,
use_gpu)
return onnx_model_filename, is_valid_onnx_model, config.vocab_size, tokenizer.max_model_input_sizes[model_name]
@ -315,11 +327,11 @@ def run_onnxruntime(use_gpu, model_names, fp16, batch_sizes, sequence_lengths, r
with torch.no_grad():
onnx_model_file, is_valid_onnx_model, vocab_size, max_sequence_length = export_onnx_model(
model_name, cache_dir, input_names, fp16, optimize_onnx, validate_onnx, overwrite)
model_name, cache_dir, input_names, use_gpu, fp16, optimize_onnx, validate_onnx, overwrite)
if not is_valid_onnx_model:
continue
ort_session = create_onnxruntime_session(onnx_model_file, use_gpu)
ort_session = create_onnxruntime_session(onnx_model_file, use_gpu, enable_all_optimization=True)
if ort_session is None:
continue
@ -468,18 +480,18 @@ def output_summary(results, csv_filename, args):
logger.info(f"Summary results are saved to csv file: {csv_filename}")
def output_fusion_statistics(optimize_model_statistics, csv_filename):
def output_fusion_statistics(model_fusion_statistics, csv_filename):
from transformers import __version__ as transformers_version
with open(csv_filename, mode="a", newline='') as csv_file:
column_names = ["model_filename", "transformers", "torch"] + list(
next(iter(optimize_model_statistics.values())).keys())
next(iter(model_fusion_statistics.values())).keys())
csv_writer = csv.DictWriter(csv_file, fieldnames=column_names)
csv_writer.writeheader()
for key in optimize_model_statistics.keys():
optimize_model_statistics[key]["transformers"] = transformers_version
optimize_model_statistics[key]["torch"] = torch.__version__
optimize_model_statistics[key]["model_filename"] = key
csv_writer.writerow(optimize_model_statistics[key])
for key in model_fusion_statistics.keys():
model_fusion_statistics[key]["transformers"] = transformers_version
model_fusion_statistics[key]["torch"] = torch.__version__
model_fusion_statistics[key]["model_filename"] = key
csv_writer.writerow(model_fusion_statistics[key])
logger.info(f"Fusion statistics is saved to csv file: {csv_filename}")
@ -612,9 +624,9 @@ def main():
logger.error(f"Exception", exc_info=True)
time_stamp = datetime.now().strftime("%Y%m%d-%H%M%S")
if optimize_model_statistics:
if model_fusion_statistics:
csv_filename = args.fusion_csv or f"benchmark_fusion_{time_stamp}.csv"
output_fusion_statistics(optimize_model_statistics, csv_filename)
output_fusion_statistics(model_fusion_statistics, csv_filename)
if len(results) == 0:
logger.warning("No any result avaiable.")

View file

@ -76,7 +76,7 @@ def optimize_by_onnxruntime(onnx_model_path, use_gpu=False, optimized_model_path
if optimized_model_path is None:
path_prefix = onnx_model_path[:-5] #remove .onnx suffix
optimized_model_path = "{}_ort_{}.onnx".format(path_prefix, "gpu" if use_gpu else "cpu")
optimized_model_path = "{}_o{}_{}.onnx".format(path_prefix, opt_level, "gpu" if use_gpu else "cpu")
sess_options.optimized_model_filepath = optimized_model_path
@ -174,11 +174,17 @@ def parse_arguments():
parser.add_argument('--verbose', required=False, action='store_true')
parser.set_defaults(verbose=False)
parser.add_argument('--use_gpu', required=False, action='store_true', help="use GPU inference")
parser.set_defaults(use_gpu=False)
parser.add_argument('--only_onnxruntime', required=False, action='store_true', help="optimized by onnxruntime only")
parser.set_defaults(only_onnxruntime=False)
parser.add_argument('--opt_level',
required=False,
type=int,
choices=[0, 1, 2, 99],
default=99,
default=0,
help="onnxruntime optimization level. 0 will disable onnxruntime.")
args = parser.parse_args()
@ -211,14 +217,15 @@ def optimize_model(input,
model_type,
num_heads,
hidden_size,
opt_level=99,
opt_level=0,
optimization_options=None,
use_gpu=False,
only_onnxruntime=False):
(optimizer_class, producer, run_onnxruntime) = MODEL_CLASSES[model_type]
input_model_path = input
if run_onnxruntime and opt_level > 0:
input_model_path = optimize_by_onnxruntime(input_model_path, use_gpu=False, opt_level=opt_level)
input_model_path = optimize_by_onnxruntime(input_model_path, use_gpu=use_gpu, opt_level=opt_level)
model = ModelProto()
with open(input_model_path, "rb") as f:
@ -254,8 +261,8 @@ def main():
optimization_options = get_optimization_options(args)
bert_model = optimize_model(args.input, args.model_type, args.num_heads, args.hidden_size, args.opt_level,
optimization_options)
bert_model = optimize_model(args.input, args.model_type, args.num_heads, args.hidden_size, opt_level=args.opt_level,
optimization_options=optimization_options, use_gpu=args.use_gpu, only_onnxruntime=args.only_onnxruntime)
if args.float16:
bert_model.convert_model_float32_to_float16()