# # SPDX-FileCopyrightText: Copyright (c) 1993-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # 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. # """ Build and test TensorRT engines generated from the DeBERTa model. Different precisions are supported. Usage: Build and test a model: - build: python deberta_tensorrt_inference.py --onnx=xx.onnx --build fp16 # build TRT engines - test: python deberta_tensorrt_inference.py --onnx=xx.onnx --test fp16 # test will measure the inference time - build and test: python deberta_tensorrt_inference.py --onnx=xx.onnx --build fp16 --test fp16 Correctness check is done by comparing engines generated from the original model and the plugin model: - [1] export ONNX model with extra output nodes: python deberta_onnx_modify.py xx.onnx --correctness-check - [2] build original model: python deberta_tensorrt_inference.py --onnx=xx_correctness_check_original.onnx --build fp16 - [3] build plugin model: python deberta_tensorrt_inference.py --onnx=xx_correctness_check_plugin.onnx --build fp16 - [4] correctness check: python deberta_tensorrt_inference.py --onnx=deberta --correctness_check fp16 Notes: - supported precisions are fp32/tf32/fp16. For both --build and --test, you can specify more than one precisions, and TensorRT engines of each precision will be built sequentially. - engine files are saved as `**/[Model name]_[GPU name]_[Precision].engine`. Note that TensorRT engines are specific to both GPU architecture and TensorRT version, and therefore are not compatible cross-version nor cross-device. - in --correctness-check mode, the argument for --onnx is the `root` name for the models [root]_correctness_check_original/plugin.onnx """ import torch import tensorrt as trt import os, sys, argparse import numpy as np from time import time from cuda.bindings import driver as cuda, runtime as cudart from cuda_utils import ( cuda_call, CudaStreamContext, memcpy_host_to_device_async, memcpy_device_to_host_async, memcpy_host_to_device, memcpy_device_to_device_async, memcpy_device_to_device, ) TRT_VERSION = int(trt.__version__[:3].replace('.','')) # e.g., version 8.4.1.5 becomes 84 def GPU_ABBREV(name): ''' Map GPU device query name to abbreviation. ::param str name Device name from torch.cuda.get_device_name(). ::return str GPU abbreviation. ''' GPU_LIST = [ 'V100', 'TITAN', 'T4', 'A100', 'A10G', 'A10' ] # Partial list, can be extended. The order of A100, A10G, A10 matters. They're put in a way to not detect substring A10 as A100 for i in GPU_LIST: if i in name: return i return 'GPU' # for names not in the partial list, use 'GPU' as default gpu_name = GPU_ABBREV(torch.cuda.get_device_name()) VALID_PRECISION = [ 'fp32', 'tf32', 'fp16' ] parser = argparse.ArgumentParser(description="Build and test TensorRT engine.") parser.add_argument('--onnx', required=True, help='ONNX model path (or filename stem if in correctness check mode).') parser.add_argument('--build', nargs='+', help='Build TRT engine in precision fp32/tf32/fp16. You can list multiple precisions to build all of them.') parser.add_argument('--test', nargs='+', help='Test TRT engine in precision fp32/tf32/fp16. You can list multiple precisions to test all of them.') parser.add_argument('--correctness-check', nargs='+', help='Correctness check for original & plugin TRT engines in precision fp32/tf32/fp16. You can list multiple precisions to check all of them.') args = parser.parse_args() ONNX_MODEL = args.onnx MODEL_NAME = os.path.splitext(args.onnx)[0] BUILD = args.build TEST = args.test CORRECTNESS = args.correctness_check if not (args.build or args.test or args.correctness_check): parser.error('Please specify --build and/or --test and/or --correctness-check' ) if BUILD: for i in BUILD: if i not in VALID_PRECISION: parser.error(f'Unsupported precision {i}') if TEST: for i in TEST: if i not in VALID_PRECISION: parser.error(f'Unsupported precision {i}') if CORRECTNESS: for i in CORRECTNESS: if i not in VALID_PRECISION: parser.error(f'Unsupported precision {i}') class TRTModel: ''' Generic class to run a TRT engine by specifying engine path and giving input data. ''' class HostDeviceMem(object): ''' Helper class to record host-device memory pointer pairs ''' def __init__(self, host_mem, device_mem): self.host = host_mem self.device = device_mem def __str__(self): return "Host:\n" + str(self.host) + "\nDevice:\n" + str(self.device) def __repr__(self): return self.__str__() def __init__(self, engine_path): self.engine_path = engine_path self.logger = trt.Logger(trt.Logger.WARNING) self.runtime = trt.Runtime(self.logger) # load and deserialize TRT engine self.engine = self.load_engine() # allocate input/output memory buffers self.inputs, self.outputs, self.bindings, self.stream = self.allocate_buffers(self.engine) # create context self.context = self.engine.create_execution_context() # Dict of NumPy dtype -> torch dtype (when the correspondence exists). From: https://github.com/pytorch/pytorch/blob/e180ca652f8a38c479a3eff1080efe69cbc11621/torch/testing/_internal/common_utils.py#L349 self.numpy_to_torch_dtype_dict = { bool : torch.bool, np.uint8 : torch.uint8, np.int8 : torch.int8, np.int16 : torch.int16, np.int32 : torch.int32, np.int64 : torch.int64, np.float16 : torch.float16, np.float32 : torch.float32, np.float64 : torch.float64, np.complex64 : torch.complex64, np.complex128 : torch.complex128 } def load_engine(self): with open(self.engine_path, 'rb') as f: engine = self.runtime.deserialize_cuda_engine(f.read()) return engine def allocate_buffers(self, engine): ''' Allocates all buffers required for an engine, i.e. host/device inputs/outputs. ''' inputs = [] outputs = [] bindings = [] stream = CudaStreamContext() for i in range(engine.num_io_tensors): tensor_name = engine.get_tensor_name(i) size = trt.volume(engine.get_tensor_shape(tensor_name)) dtype = trt.nptype(engine.get_tensor_dtype(tensor_name)) # Allocate host and device buffers host_mem = np.empty(size, dtype) device_mem = cuda_call(cudart.cudaMalloc(host_mem.nbytes)) # Append the device buffer address to device bindings. When cast to int, it's a linear index into the context's memory (like memory address). bindings.append(int(device_mem)) # Append to the appropriate input/output list. if engine.get_tensor_mode(tensor_name) == trt.TensorIOMode.INPUT: inputs.append(self.HostDeviceMem(host_mem, device_mem)) else: outputs.append(self.HostDeviceMem(host_mem, device_mem)) return inputs, outputs, bindings, stream def __call__(self, model_inputs: list, timing=False): ''' Inference step (like forward() in PyTorch). model_inputs: list of numpy array or list of torch.Tensor (on GPU) ''' NUMPY = False TORCH = False if isinstance(model_inputs[0], np.ndarray): NUMPY = True elif torch.is_tensor(model_inputs[0]): TORCH = True else: assert False, 'Unsupported input data format!' # batch size consistency check if NUMPY: batch_size = np.unique(np.array([i.shape[0] for i in model_inputs])) elif TORCH: batch_size = np.unique(np.array([i.size(dim=0) for i in model_inputs])) assert len(batch_size) == 1, 'Input batch sizes are not consistent!' batch_size = batch_size[0] for i, model_input in enumerate(model_inputs): binding_name = self.engine.get_tensor_name(i) # i-th input/output name binding_dtype = trt.nptype(self.engine.get_tensor_dtype(binding_name)) # trt can only tell to numpy dtype # input type cast if NUMPY: model_input = model_input.astype(binding_dtype) elif TORCH: model_input = model_input.to(self.numpy_to_torch_dtype_dict[binding_dtype]) if NUMPY: # fill host memory with flattened input data np.copyto(self.inputs[i].host, model_input.ravel()) elif TORCH: nbytes = model_input.element_size() * model_input.nelement() if timing: memcpy_device_to_device(self.inputs[i].device, model_input.data_ptr(), nbytes) else: # for Torch GPU tensor it's easier, can just do Device to Device copy memcpy_device_to_device_async(self.inputs[i].device, model_input.data_ptr(), nbytes, self.stream.stream) if NUMPY: if timing: [memcpy_host_to_device(inp.device, inp.host) for inp in self.inputs] else: # input, Host to Device [memcpy_host_to_device_async(inp.device, inp.host, self.stream.stream) for inp in self.inputs] for i in range(self.engine.num_io_tensors): self.context.set_tensor_address(self.engine.get_tensor_name(i), self.bindings[i]) duration = 0 if timing: start_time = time() self.context.execute_v2(bindings=self.bindings) end_time = time() duration = end_time - start_time else: # run inference self.context.execute_async_v3(stream_handle=self.stream.stream) if timing: [cuda_call(cudart.cudaMemcpy(out.host.ctypes.data, out.device, out.host.nbytes, cudart.cudaMemcpyKind.cudaMemcpyDeviceToHost)) for out in self.outputs] else: # output, Device to Host [memcpy_device_to_host_async(out.host, out.device, self.stream.stream) for out in self.outputs] if not timing: # synchronize to ensure completion of async calls self.stream.synchronize() if NUMPY: return [out.host.reshape(batch_size,-1) for out in self.outputs], duration elif TORCH: return [torch.from_numpy(out.host.reshape(batch_size,-1)) for out in self.outputs], duration def build_engine(): TRT_LOGGER = trt.Logger(trt.Logger.INFO) TRT_BUILDER = trt.Builder(TRT_LOGGER) for precision in BUILD: engine_filename = '_'.join([MODEL_NAME, gpu_name, precision]) + '.engine' if os.path.exists(engine_filename): print(f'Engine file {engine_filename} exists. Skip building...') continue print(f'Building {precision} engine of {MODEL_NAME} model on {gpu_name} GPU...') ## parse ONNX model network_creation_flag = 0 if "EXPLICIT_BATCH" in trt.NetworkDefinitionCreationFlag.__members__.keys(): network_creation_flag = 1 << int(trt.NetworkDefinitionCreationFlag.EXPLICIT_BATCH) network = TRT_BUILDER.create_network(network_creation_flag) onnx_parser = trt.OnnxParser(network, TRT_LOGGER) parse_success = onnx_parser.parse_from_file(ONNX_MODEL) for idx in range(onnx_parser.num_errors): print(onnx_parser.get_error(idx)) if not parse_success: sys.exit('ONNX model parsing failed') ## build TRT engine (configuration options at: https://docs.nvidia.com/deeplearning/tensorrt/api/python_api/infer/Core/BuilderConfig.html#ibuilderconfig) config = TRT_BUILDER.create_builder_config() seq_len = network.get_input(0).shape[1] # handle dynamic shape (min/opt/max): https://docs.nvidia.com/deeplearning/tensorrt/developer-guide/index.html#work_dynamic_shapes # by default batch dim set as 1 for all min/opt/max. If there are batch need, change the value for opt and max accordingly profile = TRT_BUILDER.create_optimization_profile() profile.set_shape("input_ids", (1,seq_len), (1,seq_len), (1,seq_len)) profile.set_shape("attention_mask", (1,seq_len), (1,seq_len), (1,seq_len)) config.add_optimization_profile(profile) config.set_memory_pool_limit(trt.MemoryPoolType.WORKSPACE, 4096 * (1 << 20)) # 4096 MiB # precision if precision == 'fp32': config.clear_flag(trt.BuilderFlag.TF32) # TF32 enabled by default, need to clear flag elif precision == 'tf32': pass elif precision == 'fp16': config.set_flag(trt.BuilderFlag.FP16) # build serialized_engine = TRT_BUILDER.build_serialized_network(network, config) ## save TRT engine with open(engine_filename, 'wb') as f: f.write(serialized_engine) print(f'Engine is saved to {engine_filename}') def test_engine(): for precision in TEST: ## load and deserialize TRT engine engine_filename = '_'.join([MODEL_NAME, gpu_name, precision]) + '.engine' print(f'Running inference on engine {engine_filename}') model = TRTModel(engine_filename) ## psuedo-random input test batch_size = 1 seq_len = model.engine.get_tensor_shape(model.engine.get_tensor_name(0))[1] vocab = 128203 gpu = torch.device('cuda') torch.manual_seed(0) # make sure in each test the seed are the same input_ids = torch.randint(0, vocab, (batch_size, seq_len), dtype=torch.long, device=gpu) attention_mask = torch.randint(0, 2, (batch_size, seq_len), dtype=torch.long, device=gpu) inputs = [input_ids, attention_mask] outputs, duration = model(inputs, timing=True) nreps = 100 duration_total = 0 for _ in range(nreps): outputs, duration = model(inputs, timing=True) duration_total += duration print(f'Average Inference time (ms) of {nreps} runs: {duration_total/nreps*1000:.3f}') def correctness_check_engines(): for precision in CORRECTNESS: ## load and deserialize TRT engine engine_filename1 = '_'.join([ONNX_MODEL, 'correctness_check_original', gpu_name, precision]) + '.engine' engine_filename2 = '_'.join([ONNX_MODEL, 'correctness_check_plugin', gpu_name, precision]) + '.engine' assert os.path.exists(engine_filename1), f'Engine file {engine_filename1} does not exist. Please build the engine first by --build' assert os.path.exists(engine_filename2), f'Engine file {engine_filename2} does not exist. Please build the engine first by --build' print(f'Running inference on original engine {engine_filename1} and plugin engine {engine_filename2}') model1 = TRTModel(engine_filename1) model2 = TRTModel(engine_filename2) ## psuedo-random input test batch_size = 1 seq_len = model1.engine.get_tensor_shape(model1.engine.get_tensor_name(0))[1] vocab = 128203 gpu = torch.device('cuda') # torch.manual_seed(0) # make sure in each test the seed are the same input_ids = torch.randint(0, vocab, (batch_size, seq_len), dtype=torch.long, device=gpu) attention_mask = torch.randint(0, 2, (batch_size, seq_len), dtype=torch.long, device=gpu) inputs = [input_ids, attention_mask] outputs1, _ = model1(inputs) outputs2, _ = model2(inputs) # element-wise and layer-wise output comparison for i in range(len(outputs1)): avg_abs_error = torch.sum(torch.abs(torch.sub(outputs1[i], outputs2[i]))) / torch.numel(outputs1[i]) max_abs_error = torch.max(torch.abs(torch.sub(outputs1[i], outputs2[i]))) print(f"[Layer {i} Element-wise Check] Avgerage absolute error: {avg_abs_error.item():e}, Maximum absolute error: {max_abs_error.item():e}. 1e-2~1e-3 expected for FP16 (10 significance bits) and 1e-6~1e-7 expected for FP32 (23 significance bits)" ) # machine epsilon for different precisions: https://en.wikipedia.org/wiki/Machine_epsilon if BUILD: build_engine() if TEST: test_engine() if CORRECTNESS: correctness_check_engines()