From c468d67760e066f4d85c3e7f2fa89aa221fac83f Mon Sep 17 00:00:00 2001 From: Kevin Chen <45886021+kevinch-nv@users.noreply.github.com> Date: Tue, 5 Nov 2024 13:52:52 -0800 Subject: [PATCH] TensorRT 10.6-GA OSS Release (#4238) Signed-off-by: Kevin Chen --- .gitmodules | 2 +- CHANGELOG.md | 32 + README.md | 18 +- VERSION | 2 +- demo/BERT/README.md | 29 +- demo/BERT/builder.py | 310 ++++-- demo/BERT/builder_utils.py | 81 ++ demo/BERT/builder_varseqlen.py | 257 ++++- demo/Diffusion/README.md | 37 +- demo/Diffusion/calibration-images/rocket.png | Bin 0 -> 493509 bytes demo/Diffusion/demo_img2vid.py | 17 +- demo/Diffusion/demo_txt2img_flux.py | 16 +- demo/Diffusion/diffusion_pipeline.py | 143 ++- demo/Diffusion/flux_pipeline.py | 104 +- demo/Diffusion/models.py | 143 +-- demo/Diffusion/requirements.txt | 7 +- demo/Diffusion/stable_diffusion_pipeline.py | 44 +- .../stable_video_diffusion_pipeline.py | 98 +- demo/Diffusion/utilities.py | 108 +- demo/Diffusion/utils_modelopt.py | 40 +- docker/rockylinux8.Dockerfile | 18 +- docker/rockylinux9.Dockerfile | 18 +- docker/ubuntu-20.04.Dockerfile | 18 +- docker/ubuntu-22.04-aarch64.Dockerfile | 2 +- docker/ubuntu-22.04.Dockerfile | 18 +- docker/ubuntu-cross-aarch64.Dockerfile | 2 +- include/NvInfer.h | 47 +- include/NvInferImpl.h | 13 +- include/NvInferLegacyDims.h | 4 +- include/NvInferPluginBase.h | 372 +++++++ include/NvInferRuntime.h | 685 ++++++++++--- include/NvInferRuntimeBase.h | 574 +---------- include/NvInferRuntimeCommon.h | 6 +- include/NvInferRuntimePlugin.h | 143 +-- include/NvInferVersion.h | 4 +- parsers/onnx | 2 +- plugin/README.md | 4 +- plugin/api/inferPlugin.cpp | 14 +- ...mbLayerNormPluginDynamic_PluginConfig.yaml | 4 +- plugin/embLayerNormPlugin/README.md | 29 +- .../embLayerNormPlugin/embLayerNormPlugin.cpp | 566 +++++----- .../embLayerNormPlugin/embLayerNormPlugin.h | 98 +- .../embLayerNormPluginLegacy.cpp | 669 ++++++++++++ .../embLayerNormPluginLegacy.h | 151 +++ plugin/fcPlugin/README.md | 6 +- python/docstrings/infer/pyCoreDoc.h | 24 +- python/docstrings/infer/pyGraphDoc.h | 15 +- python/include/impl/plugin.h | 290 ++++++ python/packaging/bindings_wheel/setup.cfg | 25 +- python/packaging/bindings_wheel/setup.py | 8 +- .../tensorrt/plugin/__init__.py | 46 + .../tensorrt/plugin/_autotune.py | 270 +++++ .../bindings_wheel/tensorrt/plugin/_export.py | 36 + .../bindings_wheel/tensorrt/plugin/_lib.py | 523 ++++++++++ .../tensorrt/plugin/_plugin_class.py | 322 ++++++ .../bindings_wheel/tensorrt/plugin/_tensor.py | 808 +++++++++++++++ .../tensorrt/plugin/_top_level.py | 132 +++ .../bindings_wheel/tensorrt/plugin/_utils.py | 77 ++ .../tensorrt/plugin/_validate.py | 357 +++++++ python/packaging/frontend_sdist/setup.cfg | 25 +- python/packaging/frontend_sdist/setup.py | 8 +- python/packaging/libs_wheel/setup.cfg | 25 +- python/packaging/libs_wheel/setup.py | 4 +- python/packaging/metapackage/setup.py | 4 +- python/src/infer/pyCore.cpp | 12 +- python/src/infer/pyGraph.cpp | 14 + python/src/infer/pyPlugin.cpp | 966 +++++++++++++++++- samples/common/sampleDevice.cpp | 4 +- samples/common/sampleDevice.h | 3 + samples/common/sampleEngines.cpp | 10 +- samples/common/sampleEngines.h | 1 + samples/common/sampleInference.cpp | 26 +- samples/common/sampleOptions.cpp | 3 + samples/common/sampleOptions.h | 1 + samples/common/sampleUtils.h | 2 + samples/common/streamReader.h | 3 +- samples/python/detectron2/requirements.txt | 2 +- samples/python/downloader.py | 13 +- samples/python/efficientdet/requirements.txt | 2 +- samples/python/efficientnet/requirements.txt | 2 +- .../data_processing.py | 4 +- .../engine_refit_onnx_bidaf/requirements.txt | 4 +- .../requirements.txt | 2 +- .../network_api_pytorch_mnist/README.md | 4 +- .../requirements.txt | 10 +- .../python/non_zero_plugin/requirements.txt | 2 +- .../onnx_custom_plugin/requirements.txt | 4 +- samples/python/onnx_packnet/requirements.txt | 8 +- samples/python/python_plugin/CMakeLists.txt | 86 ++ samples/python/python_plugin/requirements.txt | 2 +- .../quickly_deployable_plugins/README.md | 221 ++++ .../oait_kernels.py | 74 ++ .../quickly_deployable_plugins/qdp_defs.py | 248 +++++ .../quickly_deployable_plugins/qdp_runner.py | 359 +++++++ .../requirements.txt | 13 + .../requirements.yml | 33 + .../sample_weight_stripping/requirements.txt | 2 +- .../simple_progress_monitor/requirements.txt | 2 +- .../requirements.txt | 2 +- samples/python/yolov3_onnx/requirements.txt | 2 +- samples/trtexec/README.md | 33 +- samples/trtexec/trtexec.cpp | 7 +- tools/Polygraphy/CHANGELOG.md | 5 + tools/Polygraphy/polygraphy/__init__.py | 2 +- .../polygraphy/datatype/datatype.py | 1 + .../polygraphy/datatype/tensorrt.py | 1 + .../Polygraphy/tests/common/test_datatype.py | 5 + tools/onnx-graphsurgeon/CHANGELOG.md | 2 +- .../onnx_graphsurgeon/__init__.py | 2 +- .../onnx-graphsurgeon/tests/test_examples.py | 6 +- 110 files changed, 8376 insertions(+), 1788 deletions(-) create mode 100644 demo/Diffusion/calibration-images/rocket.png create mode 100644 include/NvInferPluginBase.h create mode 100644 plugin/embLayerNormPlugin/embLayerNormPluginLegacy.cpp create mode 100644 plugin/embLayerNormPlugin/embLayerNormPluginLegacy.h create mode 100644 python/include/impl/plugin.h create mode 100644 python/packaging/bindings_wheel/tensorrt/plugin/__init__.py create mode 100644 python/packaging/bindings_wheel/tensorrt/plugin/_autotune.py create mode 100644 python/packaging/bindings_wheel/tensorrt/plugin/_export.py create mode 100644 python/packaging/bindings_wheel/tensorrt/plugin/_lib.py create mode 100644 python/packaging/bindings_wheel/tensorrt/plugin/_plugin_class.py create mode 100644 python/packaging/bindings_wheel/tensorrt/plugin/_tensor.py create mode 100644 python/packaging/bindings_wheel/tensorrt/plugin/_top_level.py create mode 100644 python/packaging/bindings_wheel/tensorrt/plugin/_utils.py create mode 100644 python/packaging/bindings_wheel/tensorrt/plugin/_validate.py create mode 100644 samples/python/quickly_deployable_plugins/README.md create mode 100644 samples/python/quickly_deployable_plugins/oait_kernels.py create mode 100644 samples/python/quickly_deployable_plugins/qdp_defs.py create mode 100644 samples/python/quickly_deployable_plugins/qdp_runner.py create mode 100644 samples/python/quickly_deployable_plugins/requirements.txt create mode 100644 samples/python/quickly_deployable_plugins/requirements.yml diff --git a/.gitmodules b/.gitmodules index 0ebafd5b..87cf0015 100644 --- a/.gitmodules +++ b/.gitmodules @@ -9,4 +9,4 @@ [submodule "parsers/onnx"] path = parsers/onnx url = https://github.com/onnx/onnx-tensorrt.git - branch = main + branch = release/10.6-GA diff --git a/CHANGELOG.md b/CHANGELOG.md index f1d4f86a..3bce1c8d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,37 @@ # TensorRT OSS Release Changelog +## 10.6.0 GA - 2024-11-05 +Key Feature and Updates: +- Demo Changes + - demoBERT: The use of `fcPlugin` in demoBERT has been removed. + - demoBERT: All TensorRT plugins now used in demoBERT (`CustomEmbLayerNormDynamic`, `CustomSkipLayerNormDynamic`, and `CustomQKVToContextDynamic`) now have versions that inherit from IPluginV3 interface classes. The user can opt-in to use these V3 plugins by specifying `--use-v3-plugins` to the builder scripts. + - Opting-in to use V3 plugins does not affect performance, I/O, or plugin attributes. + - There is a known issue in the V3 (version 4) of `CustomQKVToContextDynamic` plugin from TensorRT 10.6.0, causing an internal assertion error if either the batch or sequence dimensions differ at runtime from the ones used to serialize the engine. See the “known issues” section of the [TensorRT-10.6.0 release notes](https://docs.nvidia.com/deeplearning/tensorrt/release-notes/index.html#rel-10-6-0). + - For smoother migration, the default behavior is still using the deprecated `IPluginV2DynamicExt`-derived plugins, when the flag: `--use-v3-plugins` isn't specified in the builder scripts. The flag `--use-deprecated-plugins` was added as an explicit way to enforce the default behavior, and is mutually exclusive with `--use-v3-plugins`. + - demoDiffusion + - Introduced BF16 and FP8 support for the [Flux.1-dev](demo/Diffusion#generate-an-image-guided-by-a-text-prompt-using-flux) pipeline. + - Expanded FP8 support on Ada platforms. + - Enabled LoRA adapter compatibility for SDv1.5, SDv2.1, and SDXL pipelines using Diffusers version 0.30.3. + +- Sample Changes + - Added the Python sample [quickly_deployable_plugins](samples/python/quickly_deployable_plugins), which demonstrates quickly deployable Python-based plugin definitions (QDPs) in TensorRT. QDPs are a simple and intuitive decorator-based approach to defining TensorRT plugins, requiring drastically less code. + +- Plugin Changes + - The `fcPlugin` has been deprecated. Its functionality has been superseded by the [IMatrixMultiplyLayer](https://docs.nvidia.com/deeplearning/tensorrt/api/c_api/classnvinfer1_1_1_i_matrix_multiply_layer.html) that is natively provided by TensorRT. + - Migrated `IPluginV2`-descendent version 1 of `CustomEmbLayerNormDynamic`, to version 6, which implements `IPluginV3`. + - The newer versions preserve the attributes and I/O of the corresponding older plugin version. + - The older plugin versions are deprecated and will be removed in a future release. + +- Parser Changes + - Updated ONNX submodule version to 1.17.0. + - Fixed issue where conditional layers were incorrectly being added. + - Updated local function metadata to contain more information. + - Added support for parsing nodes with Quickly Deployable Plugins. + - Fixed handling of optional outputs. + +- Tool Updates + - ONNX-Graphsurgeon updated to version 0.5.3 + - Polygraphy updated to 0.49.14. ## 10.5.0 GA - 2024-09-30 Key Features and Updates: diff --git a/README.md b/README.md index bb2f2e41..247f86e2 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ You can skip the **Build** section to enjoy TensorRT with Python. To build the TensorRT-OSS components, you will first need the following software packages. **TensorRT GA build** -* TensorRT v10.5.0.18 +* TensorRT v10.6.0.26 * Available from direct download links listed below **System Packages** @@ -73,25 +73,25 @@ To build the TensorRT-OSS components, you will first need the following software If using the TensorRT OSS build container, TensorRT libraries are preinstalled under `/usr/lib/x86_64-linux-gnu` and you may skip this step. Else download and extract the TensorRT GA build from [NVIDIA Developer Zone](https://developer.nvidia.com) with the direct links below: - - [TensorRT 10.5.0.18 for CUDA 11.8, Linux x86_64](https://developer.nvidia.com/downloads/compute/machine-learning/tensorrt/10.5.0/tars/TensorRT-10.5.0.18.Linux.x86_64-gnu.cuda-11.8.tar.gz) - - [TensorRT 10.5.0.18 for CUDA 12.6, Linux x86_64](https://developer.nvidia.com/downloads/compute/machine-learning/tensorrt/10.5.0/tars/TensorRT-10.5.0.18.Linux.x86_64-gnu.cuda-12.6.tar.gz) - - [TensorRT 10.5.0.18 for CUDA 11.8, Windows x86_64](https://developer.nvidia.com/downloads/compute/machine-learning/tensorrt/10.5.0/zip/TensorRT-10.5.0.18.Windows.win10.cuda-11.8.zip) - - [TensorRT 10.5.0.18 for CUDA 12.6, Windows x86_64](https://developer.nvidia.com/downloads/compute/machine-learning/tensorrt/10.5.0/zip/TensorRT-10.5.0.18.Windows.win10.cuda-12.6.zip) + - [TensorRT 10.6.0.26 for CUDA 11.8, Linux x86_64](https://developer.nvidia.com/downloads/compute/machine-learning/tensorrt/10.6.0/tars/TensorRT-10.6.0.26.Linux.x86_64-gnu.cuda-11.8.tar.gz) + - [TensorRT 10.6.0.26 for CUDA 12.6, Linux x86_64](https://developer.nvidia.com/downloads/compute/machine-learning/tensorrt/10.6.0/tars/TensorRT-10.6.0.26.Linux.x86_64-gnu.cuda-12.6.tar.gz) + - [TensorRT 10.6.0.26 for CUDA 11.8, Windows x86_64](https://developer.nvidia.com/downloads/compute/machine-learning/tensorrt/10.6.0/zip/TensorRT-10.6.0.26.Windows.win10.cuda-11.8.zip) + - [TensorRT 10.6.0.26 for CUDA 12.6, Windows x86_64](https://developer.nvidia.com/downloads/compute/machine-learning/tensorrt/10.6.0/zip/TensorRT-10.6.0.26.Windows.win10.cuda-12.6.zip) **Example: Ubuntu 20.04 on x86-64 with cuda-12.6** ```bash cd ~/Downloads - tar -xvzf TensorRT-10.5.0.18.Linux.x86_64-gnu.cuda-12.6.tar.gz - export TRT_LIBPATH=`pwd`/TensorRT-10.5.0.18 + tar -xvzf TensorRT-10.6.0.26.Linux.x86_64-gnu.cuda-12.6.tar.gz + export TRT_LIBPATH=`pwd`/TensorRT-10.6.0.26 ``` **Example: Windows on x86-64 with cuda-12.6** ```powershell - Expand-Archive -Path TensorRT-10.5.0.18.Windows.win10.cuda-12.6.zip - $env:TRT_LIBPATH="$pwd\TensorRT-10.5.0.18\lib" + Expand-Archive -Path TensorRT-10.6.0.26.Windows.win10.cuda-12.6.zip + $env:TRT_LIBPATH="$pwd\TensorRT-10.6.0.26\lib" ``` ## Setting Up The Build Environment diff --git a/VERSION b/VERSION index 5099d096..eafccb08 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -10.5.0.18 +10.6.0.26 diff --git a/demo/BERT/README.md b/demo/BERT/README.md index 074848c3..5abc76fa 100755 --- a/demo/BERT/README.md +++ b/demo/BERT/README.md @@ -75,7 +75,7 @@ The following software version configuration has been tested: |Software|Version| |--------|-------| |Python|>=3.8| -|TensorRT|10.5.0.18| +|TensorRT|10.6.0.26| |CUDA|12.6| ## Setup @@ -122,7 +122,7 @@ This demo BERT application can be run within the TensorRT OSS build container. I bash scripts/download_model.sh ``` -**Note:** Since the datasets and checkpoints are stored in the directory mounted from the host, they do *not* need to be downloaded each time the container is launched. +**Note:** Since the datasets and checkpoints are stored in the directory mounted from the host, they do *not* need to be downloaded each time the container is launched. **Warning:** In the event of encountering an error message stating, "Missing API key and missing Email Authentication. This command requires an API key or authentication via browser login", the recommended steps for resolution are as follows: * Generate an API key by logging in https://ngc.nvidia.com/setup/api-key and copy the generated API key. @@ -153,11 +153,11 @@ Completing these steps should resolve the error you encountered and allow the co jupyter notebook --ip 0.0.0.0 inference.ipynb ``` Then, use your browser to open the link displayed. The link should look similar to: `http://127.0.0.1:8888/?token=` - + 6. Run inference with CUDA Graph support. A separate python `inference_c.py` script is provided to run inference with CUDA Graph support. This is necessary since CUDA Graph is only supported through CUDA C/C++ APIs, not pyCUDA. The `inference_c.py` script uses pybind11 to interface with C/C++ for CUDA graph capturing and launching. The cmdline interface is the same as `inference.py` except for an extra `--enable-graph` option. - + ```bash mkdir -p build; pushd build cmake .. -DPYTHON_EXECUTABLE=$(which python) @@ -167,11 +167,11 @@ Completing these steps should resolve the error you encountered and allow the co ``` A separate C/C++ inference benchmark executable `perf` (compiled from `perf.cpp`) is provided to run inference benchmarks with CUDA Graph. The cmdline interface is the same as `perf.py` except for an extra `--enable_graph` option. - + ```bash build/perf -e engines/bert_large_128.engine -b 1 -s 128 -w 100 -i 1000 --enable_graph ``` - + ### (Optional) Trying a different configuration @@ -220,6 +220,9 @@ The `infer_c/` folder contains all the necessary C/C++ files required for CUDA G To view the available parameters for each script, you can use the help flag (`-h`). +**Note:** In the builder scripts (`builder.py` and `builder_varseqlen.py`), the options `--use-deprecated-plugins` and `--use-v3-plugins` toggle the underlying implementation of the plugins used in demoBERT. They are mutually exclusive, and enabling either should not affect functionality, or performance. The `--use-deprecated-plugins` uses plugin versions that inherit from `IPluginV2DynamicExt`, while `--use-v3-plugins` uses plugin versions that inherit from `IPluginV3` classes. +If unspecified, `--use-deprecated-plugins` is used by default. + ### TensorRT inference process As mentioned in the [Quick Start Guide](#quick-start-guide), two options are provided for running inference: @@ -245,7 +248,7 @@ As mentioned in the [Quick Start Guide](#quick-start-guide), two options are pro **Xavier GPU** ```bash # Only supports SkipLayerNormPlugin running with INT8 I/O. Use -iln builder flag to enable. - mkdir -p engines && python3 builder.py -m models/fine-tuned/bert_tf_ckpt_large_qa_squad2_amp_384_v19.03.1/model.ckpt -o engines/bert_large_384_int8mix.engine -b 1 -s 384 --int8 --fp16 --strict -c models/fine-tuned/bert_tf_ckpt_large_qa_squad2_amp_384_v19.03.1 --squad-json ./squad/train-v1.1.json -v models/fine-tuned/bert_tf_ckpt_large_qa_squad2_amp_384_v19.03.1/vocab.txt --calib-num 100 -iln + mkdir -p engines && python3 builder.py -m models/fine-tuned/bert_tf_ckpt_large_qa_squad2_amp_384_v19.03.1/model.ckpt -o engines/bert_large_384_int8mix.engine -b 1 -s 384 --int8 --fp16 --strict -c models/fine-tuned/bert_tf_ckpt_large_qa_squad2_amp_384_v19.03.1 --squad-json ./squad/train-v1.1.json -v models/fine-tuned/bert_tf_ckpt_large_qa_squad2_amp_384_v19.03.1/vocab.txt --calib-num 100 -iln ``` **Volta GPU** @@ -278,13 +281,13 @@ As mentioned in the [Quick Start Guide](#quick-start-guide), two options are pro **Xavier GPU** ```bash # Only supports SkipLayerNormPlugin running with INT8 I/O. Use -iln builder flag to enable. - mkdir -p engines && python3 builder.py -o engines/bert_large_384_int8mix.engine -b 1 -s 384 --int8 --fp16 --strict -c models/fine-tuned/bert_tf_ckpt_large_qa_squad2_amp_384_v19.03.1 -v models/fine-tuned/bert_tf_ckpt_large_qa_squad2_amp_384_v19.03.1/vocab.txt -x models/fine-tuned/bert_pyt_onnx_large_qa_squad11_amp_fake_quant_v1/bert_large_v1_1_fake_quant.onnx -iln + mkdir -p engines && python3 builder.py -o engines/bert_large_384_int8mix.engine -b 1 -s 384 --int8 --fp16 --strict -c models/fine-tuned/bert_tf_ckpt_large_qa_squad2_amp_384_v19.03.1 -v models/fine-tuned/bert_tf_ckpt_large_qa_squad2_amp_384_v19.03.1/vocab.txt -x models/fine-tuned/bert_pyt_onnx_large_qa_squad11_amp_fake_quant_v1/bert_large_v1_1_fake_quant.onnx -iln ``` **Volta GPU** ```bash # No support for QKVToContextPlugin or SkipLayerNormPlugin running with INT8 I/O. Don't specify -imh or -iln in builder flags. - mkdir -p engines && python3 builder.py -o engines/bert_large_384_int8mix.engine -b 1 -s 384 --int8 --fp16 --strict -c models/fine-tuned/bert_tf_ckpt_large_qa_squad2_amp_384_v19.03.1 -v models/fine-tuned/bert_tf_ckpt_large_qa_squad2_amp_384_v19.03.1/vocab.txt -x models/fine-tuned/bert_pyt_onnx_large_qa_squad11_amp_fake_quant_v1/bert_large_v1_1_fake_quant.onnx + mkdir -p engines && python3 builder.py -o engines/bert_large_384_int8mix.engine -b 1 -s 384 --int8 --fp16 --strict -c models/fine-tuned/bert_tf_ckpt_large_qa_squad2_amp_384_v19.03.1 -v models/fine-tuned/bert_tf_ckpt_large_qa_squad2_amp_384_v19.03.1/vocab.txt -x models/fine-tuned/bert_pyt_onnx_large_qa_squad11_amp_fake_quant_v1/bert_large_v1_1_fake_quant.onnx ``` This will build and engine with a maximum batch size of 1 (`-b 1`) and sequence length of 384 (`-s 384`) using INT8 mixed precision computation where possible (`--int8 --fp16 --strict`). @@ -324,10 +327,10 @@ Note this is an experimental feature because we only support Xavier+ GPUs, also This will build and engine with a maximum batch size of 1 (`-b 1`) and sequence length of 256 (`-s 256`) using INT8 precision computation where possible (`--int8`). -3. Run inference +3. Run inference Evaluate the F1 score and exact match score using the squad dataset: - + ```bash python3 inference_varseqlen.py -e engines/bert_varseq_int8.engine -s 256 -sq ./squad/dev-v1.1.json -v models/fine-tuned/bert_tf_ckpt_large_qa_squad2_amp_384_v19.03.1/vocab.txt -o ./predictions.json python3 squad/evaluate-v1.1.py squad/dev-v1.1.json ./predictions.json 90 @@ -345,11 +348,11 @@ Note this is an experimental feature because we only support Xavier+ GPUs, also python3 perf_varseqlen.py -e engines/bert_varseq_int8.engine -b 1 -s 256 ``` - This will collect performance data run use batch size 1 (`-b 1`) and sequence length of 256 (`-s 256`). + This will collect performance data run use batch size 1 (`-b 1`) and sequence length of 256 (`-s 256`). 5. Collect performance data with CUDA graph enabled - We can use the same `inference_c.py` and `build/perf` to collect performance data with cuda graph enabled. The command line is the same as run without variable sequence length. + We can use the same `inference_c.py` and `build/perf` to collect performance data with cuda graph enabled. The command line is the same as run without variable sequence length. ### Sparsity with Quantization Aware Training diff --git a/demo/BERT/builder.py b/demo/BERT/builder.py index 90060ed2..c2d2c082 100755 --- a/demo/BERT/builder.py +++ b/demo/BERT/builder.py @@ -35,6 +35,10 @@ from builder_utils import WQKV, BQKV # Attention Keys from builder_utils import W_AOUT, B_AOUT, W_MID, B_MID, W_LOUT, B_LOUT # Transformer Keys from builder_utils import SQD_W, SQD_B # SQuAD Output Keys +from builder_utils import ( + create_plugin, + add_plugin_to_network, +) # Plugin Helper functions """ TensorRT Initialization @@ -51,13 +55,23 @@ trt.init_libnvinfer_plugins(TRT_LOGGER, "") plg_registry = trt.get_plugin_registry() -emln_plg_creator = plg_registry.get_plugin_creator("CustomEmbLayerNormPluginDynamic", "1", "") -qkv2_plg_creator = plg_registry.get_plugin_creator("CustomQKVToContextPluginDynamic", "1", "") -skln_plg_creator = plg_registry.get_plugin_creator("CustomSkipLayerNormPluginDynamic", "1", "") -fc_plg_creator = plg_registry.get_plugin_creator("CustomFCPluginDynamic", "1", "") + class BertConfig: - def __init__(self, bert_config_path, use_fp16, use_int8, use_strict, use_fc2_gemm, use_int8_skipln, use_int8_multihead, use_qat, use_sparsity, timing_cache): + def __init__( + self, + bert_config_path, + use_fp16, + use_int8, + use_strict, + use_fc2_gemm, + use_int8_skipln, + use_int8_multihead, + use_qat, + use_sparsity, + timing_cache, + use_deprecated_plugins=False, + ): with open(bert_config_path, "r") as f: data = json.load(f) self.num_attention_heads = data["num_attention_heads"] @@ -75,6 +89,8 @@ def __init__(self, bert_config_path, use_fp16, use_int8, use_strict, use_fc2_gem self.use_qat = use_qat self.use_sparsity = use_sparsity self.timing_cache = timing_cache + self.use_deprecated_plugins = use_deprecated_plugins + def set_tensor_name(tensor, prefix, name): tensor.name = prefix + name @@ -131,19 +147,26 @@ def attention_layer_opt(prefix, config, init_dict, network, input_tensor, imask) pf_dq_probs = trt.PluginField("dq_probs", np.array([dq_probs], np.float32), trt.PluginFieldType.FLOAT32) pfc = trt.PluginFieldCollection([pf_hidden_size, pf_num_heads, pf_has_mask, pf_type, pf_dq_probs]) else: - pfc = trt.PluginFieldCollection([pf_hidden_size, pf_num_heads, pf_has_mask, pf_type]) - qkv2ctx_plug = qkv2_plg_creator.create_plugin("qkv2ctx", pfc) + pfc = trt.PluginFieldCollection( + [pf_hidden_size, pf_num_heads, pf_has_mask, pf_type] + ) + qkv2ctx_plugin = create_plugin( + "qkv_to_context", plg_registry, pfc, use_deprecated_plugins=config.use_deprecated_plugins + ) qkv_in = [mult_all.get_output(0)] if has_mask: qkv_in.append(imask) - qkv2ctx = network.add_plugin_v2(qkv_in, qkv2ctx_plug) + + qkv2ctx_layer = add_plugin_to_network( + network, qkv2ctx_plugin, qkv_in, use_deprecated_plugins=config.use_deprecated_plugins + ) if config.use_qat: dr_ctx = init_dict[prefix + 'output_dense_input_amax'] - set_output_range(qkv2ctx, dr_ctx) - set_output_name(qkv2ctx, prefix, "context_layer") - return qkv2ctx + set_output_range(qkv2ctx_layer, dr_ctx) + set_output_name(qkv2ctx_layer, prefix, "context_layer") + return qkv2ctx_layer def skipln(prefix, config, init_dict, network, input_tensor, skip, bias=None): """ @@ -174,26 +197,15 @@ def skipln(prefix, config, init_dict, network, input_tensor, skip, bias=None): fields.append(pf_bias) pfc = trt.PluginFieldCollection(fields) - skipln_plug = skln_plg_creator.create_plugin("skipln", pfc) + skipln_plugin = create_plugin( + "skip_layer_norm", plg_registry, pfc, use_deprecated_plugins=config.use_deprecated_plugins + ) skipln_inputs = [input_tensor, skip] - layer = network.add_plugin_v2(skipln_inputs, skipln_plug) - return layer - -# Custom FC plugin is faster than native FC only on older architectures. -def use_custom_fc(): - cc = pycuda.autoinit.device.compute_capability() - return cc[0] * 10 + cc[1] <= 70 - -def custom_fc(config, network, input_tensor, out_dims, W): - pf_out_dims = trt.PluginField("out_dims", np.array([out_dims], dtype=np.int32), trt.PluginFieldType.INT32) - pf_W = trt.PluginField("W", W.numpy(), trt.PluginFieldType.FLOAT32) - pf_type = trt.PluginField("type_id", np.array([1 if config.use_fp16 else 0], np.int32), trt.PluginFieldType.INT32) - pfc = trt.PluginFieldCollection([pf_out_dims, pf_W, pf_type]) - fc_plugin = fc_plg_creator.create_plugin("fcplugin", pfc) - plug_inputs = [input_tensor] - out_dense = network.add_plugin_v2(plug_inputs, fc_plugin) - return out_dense + skipln_layer = add_plugin_to_network( + network, skipln_plugin, skipln_inputs, use_deprecated_plugins=config.use_deprecated_plugins + ) + return skipln_layer def transformer_layer_opt(prefix, config, init_dict, network, input_tensor, imask): """ @@ -214,22 +226,17 @@ def transformer_layer_opt(prefix, config, init_dict, network, input_tensor, imas # FC0 B_aout = init_dict[prefix + B_AOUT] - if not config.use_int8 and use_custom_fc(): - W_aoutT = init_dict[prefix + W_AOUT + "_notrans"] - attention_out_fc = custom_fc(config, network, attention_heads, hidden_size, W_aoutT) - else: - W_aout = init_dict[prefix + W_AOUT] - attention_out_fc = network.add_convolution_nd(attention_heads, hidden_size, (1, 1), W_aout, B_aout) - B_aout = None + W_aout = init_dict[prefix + W_AOUT] + attention_out_fc = network.add_convolution_nd(attention_heads, hidden_size, (1, 1), W_aout, B_aout) - if config.use_int8 and not config.use_int8_skipln: - attention_out_fc.set_output_type(0, trt.DataType.HALF if config.use_fp16 else trt.DataType.FLOAT) + if config.use_int8 and not config.use_int8_skipln: + attention_out_fc.set_output_type(0, trt.DataType.HALF if config.use_fp16 else trt.DataType.FLOAT) - if config.use_int8 and config.use_qat: - dr_fc_aout = init_dict[prefix + 'attention_output_add_local_input_quantizer_amax'] - set_output_range(attention_out_fc, dr_fc_aout) + if config.use_int8 and config.use_qat: + dr_fc_aout = init_dict[prefix + 'attention_output_add_local_input_quantizer_amax'] + set_output_range(attention_out_fc, dr_fc_aout) - skiplayer = skipln(prefix + "attention_output_layernorm_",config, init_dict, network, attention_out_fc.get_output(0), input_tensor, B_aout) + skiplayer = skipln(prefix + "attention_output_layernorm_",config, init_dict, network, attention_out_fc.get_output(0), input_tensor, bias=None) attention_ln = skiplayer.get_output(0) if config.use_qat: dr_skln1 = init_dict[prefix + 'intermediate_dense_input_amax'] @@ -271,24 +278,18 @@ def transformer_layer_opt(prefix, config, init_dict, network, input_tensor, imas # FC2 # Dense to hidden size B_lout = init_dict[prefix + B_LOUT] - prefer_conv = config.use_int8 and not config.use_fc2_gemm - if not prefer_conv and use_custom_fc(): - W_loutT = init_dict[prefix + W_LOUT + "_notrans"] - out_dense = custom_fc(config, network, intermediate_act, hidden_size, W_loutT) - else: - W_lout = init_dict[prefix + W_LOUT] - out_dense = network.add_convolution_nd(intermediate_act, hidden_size, (1, 1), W_lout, B_lout) - B_lout = None + W_lout = init_dict[prefix + W_LOUT] + out_dense = network.add_convolution_nd(intermediate_act, hidden_size, (1, 1), W_lout, B_lout) - if config.use_int8 and not config.use_int8_skipln: - out_dense.set_output_type(0, trt.DataType.HALF if config.use_fp16 else trt.DataType.FLOAT) + if config.use_int8 and not config.use_int8_skipln: + out_dense.set_output_type(0, trt.DataType.HALF if config.use_fp16 else trt.DataType.FLOAT) if config.use_qat: dr_fc_out = init_dict[prefix + 'output_add_local_input_quantizer_amax'] set_output_range(out_dense, dr_fc_out) set_output_name(out_dense, prefix + "output_", "dense") - out_layer = skipln(prefix + "output_layernorm_", config, init_dict, network, out_dense.get_output(0), attention_ln, B_lout) + out_layer = skipln(prefix + "output_layernorm_", config, init_dict, network, out_dense.get_output(0), attention_ln, bias=None) set_output_name(out_layer, prefix + "output_", "reshape") return out_layer @@ -366,8 +367,12 @@ def emb_layernorm(builder, network, config, weights_dict, builder_config, sequen output_fp16 = trt.PluginField("output_fp16", np.array([1 if config.use_fp16 else 0]).astype(np.int32), trt.PluginFieldType.INT32) mha_type = trt.PluginField("mha_type_id", np.array([get_mha_dtype(config)], np.int32), trt.PluginFieldType.INT32) - pfc = trt.PluginFieldCollection([wbeta, wgamma, wwordemb, wtokemb, wposemb, output_fp16, mha_type]) - fn = emln_plg_creator.create_plugin("embeddings", pfc) + pfc = trt.PluginFieldCollection( + [wbeta, wgamma, wwordemb, wtokemb, wposemb, output_fp16, mha_type] + ) + emln_plugin = create_plugin( + "emb_layer_norm", plg_registry, pfc, use_deprecated_plugins=config.use_deprecated_plugins + ) input_ids = network.add_shuffle(input_ids) input_ids.second_transpose = (1, 0) @@ -375,10 +380,14 @@ def emb_layernorm(builder, network, config, weights_dict, builder_config, sequen segment_ids.second_transpose = (1, 0) input_mask = network.add_shuffle(input_mask) input_mask.second_transpose = (1, 0) - inputs = [input_ids.get_output(0), - segment_ids.get_output(0), - input_mask.get_output(0)] - emb_layer = network.add_plugin_v2(inputs, fn) + inputs = [ + input_ids.get_output(0), + segment_ids.get_output(0), + input_mask.get_output(0), + ] + emb_layer = add_plugin_to_network( + network, emln_plugin, inputs, use_deprecated_plugins=config.use_deprecated_plugins + ) if config.use_qat: set_output_range(emb_layer, 1, 1) @@ -490,30 +499,151 @@ def generate_calibration_cache(sequence_lengths, workspace_size, config, weights config.is_calib_mode = False def main(): - parser = argparse.ArgumentParser(description="TensorRT BERT Sample", formatter_class=argparse.ArgumentDefaultsHelpFormatter) - parser.add_argument("-m", "--ckpt", required=False, - help="The checkpoint file basename, e.g.: basename(model.ckpt-766908.data-00000-of-00001) is model.ckpt-766908") - parser.add_argument("-x", "--onnx", required=False, help="The ONNX model file path.") - parser.add_argument("-pt", "--pytorch", required=False, help="The PyTorch checkpoint file path.") - parser.add_argument("-o", "--output", required=True, default="bert_base_384.engine", help="The bert engine file, ex bert.engine") - parser.add_argument("-b", "--batch-size", default=[], action="append", help="Batch size(s) to optimize for. The engine will be usable with any batch size below this, but may not be optimal for smaller sizes. Can be specified multiple times to optimize for more than one batch size.", type=int) - parser.add_argument("-s", "--sequence-length", default=[], action="append", help="Sequence length of the BERT model", type=int) - parser.add_argument("-c", "--config-dir", required=True, - help="The folder containing the bert_config.json, which can be downloaded e.g. from https://github.com/google-research/bert#pre-trained-models or by running download_models.py in dle/TensorFlow/LanguageModeling/BERT/data/pretrained_models_google") - parser.add_argument("-f", "--fp16", action="store_true", help="Indicates that inference should be run in FP16 precision", required=False) - parser.add_argument("-i", "--int8", action="store_true", help="Indicates that inference should be run in INT8 precision", required=False) - parser.add_argument("-t", "--strict", action="store_true", help="Indicates that inference should be run in strict precision mode", required=False) - parser.add_argument("-w", "--workspace-size", default=2500, help="Workspace size in MiB for building the BERT engine", type=int) - parser.add_argument("-j", "--squad-json", default="squad/dev-v1.1.json", help="squad json dataset used for int8 calibration", required=False) - parser.add_argument("-v", "--vocab-file", default="./pre-trained_model/uncased_L-24_H-1024_A-16/vocab.txt", help="Path to file containing entire understandable vocab", required=False) - parser.add_argument("-n", "--calib-num", default=100, help="calibration batch numbers", type=int) - parser.add_argument("-p", "--calib-path", help="calibration cache path", required=False) - parser.add_argument("-g", "--force-fc2-gemm", action="store_true", help="Force use gemm to implement FC2 layer", required=False) - parser.add_argument("-iln", "--force-int8-skipln", action="store_true", help="Run skip layernorm with INT8 (FP32 or FP16 by default) inputs and output", required=False) - parser.add_argument("-imh", "--force-int8-multihead", action="store_true", help="Run multi-head attention with INT8 (FP32 or FP16 by default) input and output", required=False) - parser.add_argument("-sp", "--sparse", action="store_true", help="Indicates that model is sparse", required=False) - parser.add_argument("-tcf", "--timing-cache-file", help="Path to tensorrt build timeing cache file, only available for tensorrt 8.0 and later", required=False) - parser.add_argument("--verbose", action="store_true", help="Turn on verbose logger and set profiling verbosity to DETAILED", required=False) + parser = argparse.ArgumentParser( + description="TensorRT BERT Sample", + formatter_class=argparse.RawTextHelpFormatter, + ) + parser.add_argument( + "-m", + "--ckpt", + required=False, + help="The checkpoint file basename, e.g.: basename(model.ckpt-766908.data-00000-of-00001) is model.ckpt-766908 (default: None)", + ) + parser.add_argument( + "-x", "--onnx", required=False, help="The ONNX model file path. (default: None)" + ) + parser.add_argument( + "-pt", "--pytorch", required=False, help="The PyTorch checkpoint file path. (default: None)" + ) + parser.add_argument( + "-o", + "--output", + required=True, + default="bert_base_384.engine", + help="The bert engine file, ex bert.engine (default: bert_base_384.engine)", + ) + parser.add_argument( + "-b", + "--batch-size", + default=[], + action="append", + help="Batch size(s) to optimize for. The engine will be usable with any batch size below this, but may not be optimal for smaller sizes. Can be specified multiple times to optimize for more than one batch size. (default: [1])", + type=int, + ) + parser.add_argument( + "-s", + "--sequence-length", + default=[], + action="append", + help="Sequence length of the BERT model (default: [128])", + type=int, + ) + parser.add_argument( + "-c", + "--config-dir", + required=True, + help="The folder containing the bert_config.json, which can be downloaded e.g. from https://github.com/google-research/bert#pre-trained-models or by running download_models.py in dle/TensorFlow/LanguageModeling/BERT/data/pretrained_models_google", + ) + parser.add_argument( + "-f", + "--fp16", + action="store_true", + help="Indicates that inference should be run in FP16 precision (default: false)", + required=False, + ) + parser.add_argument( + "-i", + "--int8", + action="store_true", + help="Indicates that inference should be run in INT8 precision (default: false)", + required=False, + ) + parser.add_argument( + "-t", + "--strict", + action="store_true", + help="Indicates that inference should be run in strict precision mode (default: false)", + required=False, + ) + parser.add_argument( + "-w", + "--workspace-size", + default=2500, + help="Workspace size in MiB for building the BERT engine (default: 2500)", + type=int, + ) + parser.add_argument( + "-j", + "--squad-json", + default="squad/dev-v1.1.json", + help="squad json dataset used for int8 calibration (default: squad/dev-v1.1.json)", + required=False, + ) + parser.add_argument( + "-v", + "--vocab-file", + default="./pre-trained_model/uncased_L-24_H-1024_A-16/vocab.txt", + help="Path to file containing entire understandable vocab (default: ./pre-trained_model/uncased_L-24_H-1024_A-16/vocab.txt)", + required=False, + ) + parser.add_argument( + "-n", "--calib-num", default=100, help="calibration batch numbers (default: 100)", type=int + ) + parser.add_argument( + "-p", "--calib-path", help="calibration cache path (default: None)", required=False + ) + parser.add_argument( + "-g", + "--force-fc2-gemm", + action="store_true", + help="Force use gemm to implement FC2 layer (default: false)", + required=False, + ) + parser.add_argument( + "-iln", + "--force-int8-skipln", + action="store_true", + help="Run skip layernorm with INT8 (FP32 or FP16 by default) inputs and output (default: false)", + required=False, + ) + parser.add_argument( + "-imh", + "--force-int8-multihead", + action="store_true", + help="Run multi-head attention with INT8 (FP32 or FP16 by default) input and output (default: false)", + required=False, + ) + parser.add_argument( + "-sp", + "--sparse", + action="store_true", + help="Indicates that model is sparse (default: false)", + required=False, + ) + parser.add_argument( + "-tcf", + "--timing-cache-file", + help="Path to tensorrt build timeing cache file, only available for tensorrt 8.0 and later (default: None)", + required=False, + ) + parser.add_argument( + "--verbose", + action="store_true", + help="Turn on verbose logger and set profiling verbosity to DETAILED (default: false)", + required=False, + ) + + plugin_group = parser.add_mutually_exclusive_group(required=False) + plugin_group.add_argument('--use-v3-plugins', + dest='use_deprecated_plugins', + action='store_false', + help="Use plugins implementing the IPluginV3 interface wherever TensorRT plugins are used. Cannot be used with --use-deprecated-plugins. Enabling this option should not affect functionality or performance. (default: false)") + plugin_group.add_argument('--use-deprecated-plugins', + dest='use_deprecated_plugins', + action='store_true', + help="Use deprecated plugins implementing the IPluginV2 interface wherever TensorRT plugins are used (instead of updated plugins implementing the IPluginV3 interface). Cannot be used with --use-v3-plugins. Disabling this option should not affect functionality or performance. (default: true)") + + parser.set_defaults(use_deprecated_plugins=True) args, _ = parser.parse_known_args() args.batch_size = args.batch_size or [1] @@ -531,7 +661,19 @@ def main(): bert_config_path = os.path.join(args.config_dir, "bert_config.json") TRT_LOGGER.log(TRT_LOGGER.INFO, "Using configuration file: {:}".format(bert_config_path)) - config = BertConfig(bert_config_path, args.fp16, args.int8, args.strict, args.force_fc2_gemm, args.force_int8_skipln, args.force_int8_multihead, args.int8 and args.onnx != None, args.sparse, args.timing_cache_file) + config = BertConfig( + bert_config_path, + args.fp16, + args.int8, + args.strict, + args.force_fc2_gemm, + args.force_int8_skipln, + args.force_int8_multihead, + args.int8 and args.onnx != None, + args.sparse, + args.timing_cache_file, + args.use_deprecated_plugins, + ) if args.calib_path != None: calib_cache = args.calib_path diff --git a/demo/BERT/builder_utils.py b/demo/BERT/builder_utils.py index abf0f514..ed7f7448 100644 --- a/demo/BERT/builder_utils.py +++ b/demo/BERT/builder_utils.py @@ -313,3 +313,84 @@ def load_megatron_pickle_weights(path, config): TRT_LOGGER.log(TRT_LOGGER.INFO, "Found {:} entries in weight map".format(len(weight_dict))) return weight_dict + + +""" +Common Plugin Helper/Wrapper Functions +""" +BERT_PLUGINS_INFO_MAP = { + # MHA variants + "qkv_to_context": { + "IPluginV2_version": "1", + "IPluginV3_version": "4", + "trt_plugin_name": "CustomQKVToContextPluginDynamic", + }, + "qkv_to_context_varseqlen": { + "IPluginV2_version": "2", + "IPluginV3_version": "5", + "trt_plugin_name": "CustomQKVToContextPluginDynamic", + }, + "qkv_to_context_interleaved": { + "IPluginV2_version": "3", + "IPluginV3_version": "6", + "trt_plugin_name": "CustomQKVToContextPluginDynamic", + }, + # skipLayernorm variants + "skip_layer_norm": { + "IPluginV2_version": "1", + "IPluginV3_version": "5", + "trt_plugin_name": "CustomSkipLayerNormPluginDynamic", + }, + "skip_layer_norm_varseqlen": { + "IPluginV2_version": "2", + "IPluginV3_version": "6", + "trt_plugin_name": "CustomSkipLayerNormPluginDynamic", + }, + "skip_layer_norm_huggingface": { + "IPluginV2_version": "3", + "IPluginV3_version": "7", + "trt_plugin_name": "CustomSkipLayerNormPluginDynamic", + }, + "skip_layer_norm_megatron": { + "IPluginV2_version": "4", + "IPluginV3_version": "8", + "trt_plugin_name": "CustomSkipLayerNormPluginDynamic", + }, + # embLayernorm variants + "emb_layer_norm": { + "IPluginV2_version": "1", + "IPluginV3_version": "6", + "trt_plugin_name": "CustomEmbLayerNormPluginDynamic", + }, + "emb_layer_norm_huggingface": { + "IPluginV2_version": "2", + "IPluginV3_version": "4", + "trt_plugin_name": "CustomEmbLayerNormPluginDynamic", + }, + "emb_layer_norm_megatron": { + "IPluginV2_version": "3", + "IPluginV3_version": "5", + "trt_plugin_name": "CustomEmbLayerNormPluginDynamic", + }, +} + + +def create_plugin(layer_name, plg_registry, pfc, use_deprecated_plugins=False): + plg_trt_name = BERT_PLUGINS_INFO_MAP[layer_name]["trt_plugin_name"] + plg_version = BERT_PLUGINS_INFO_MAP[layer_name][ + ("IPluginV2_version" if use_deprecated_plugins else "IPluginV3_version") + ] + plg_namespace = "" + + creator = plg_registry.get_creator(plg_trt_name, plg_version, plg_namespace) + if use_deprecated_plugins: + return creator.create_plugin(layer_name, pfc) + else: + return creator.create_plugin(layer_name, pfc, trt.TensorRTPhase.BUILD) + + +def add_plugin_to_network(network, plugin, inputs, use_deprecated_plugins=False): + if use_deprecated_plugins: + return network.add_plugin_v2(inputs, plugin) + else: + return network.add_plugin_v3(inputs, [], plugin) diff --git a/demo/BERT/builder_varseqlen.py b/demo/BERT/builder_varseqlen.py index 33dd5060..b7328cd3 100755 --- a/demo/BERT/builder_varseqlen.py +++ b/demo/BERT/builder_varseqlen.py @@ -34,6 +34,11 @@ from builder_utils import WQKV, BQKV # Attention Keys from builder_utils import W_AOUT, B_AOUT, W_MID, B_MID, W_LOUT, B_LOUT # Transformer Keys from builder_utils import SQD_W, SQD_B # SQuAD Output Keys +from builder_utils import ( + create_plugin, + add_plugin_to_network, +) # Plugin Helper functions + """ TensorRT Initialization @@ -50,19 +55,21 @@ trt.init_libnvinfer_plugins(TRT_LOGGER, "") plg_registry = trt.get_plugin_registry() -emln_plg_creator2 = plg_registry.get_plugin_creator("CustomEmbLayerNormPluginDynamic", "2", "") -mha_plg_creator2 = plg_registry.get_plugin_creator("CustomQKVToContextPluginDynamic", "2", "") -skln_plg_creator2 = plg_registry.get_plugin_creator("CustomSkipLayerNormPluginDynamic", "2", "") - -mha_plg_creator3 = plg_registry.get_plugin_creator("CustomQKVToContextPluginDynamic", "3", "") -skln_plg_creator3 = plg_registry.get_plugin_creator("CustomSkipLayerNormPluginDynamic", "3", "") -# Megatron Plugins -emln_plg_creator3 = plg_registry.get_plugin_creator("CustomEmbLayerNormPluginDynamic", "3", "") -skln_plg_creator4 = plg_registry.get_plugin_creator("CustomSkipLayerNormPluginDynamic", "4", "") class BertConfig: - def __init__(self, bert_config_path, use_fp16, use_int8, use_qat, interleaved, timing_cache, use_sparsity, use_megatron): + def __init__( + self, + bert_config_path, + use_fp16, + use_int8, + use_qat, + interleaved, + timing_cache, + use_sparsity, + use_megatron, + use_deprecated_plugins=False, + ): with open(bert_config_path, "r") as f: data = json.load(f) self.num_attention_heads = data["num_attention_heads"] @@ -77,6 +84,7 @@ def __init__(self, bert_config_path, use_fp16, use_int8, use_qat, interleaved, t self.timing_cache = timing_cache self.use_sparsity = use_sparsity self.use_megatron = use_megatron + self.use_deprecated_plugins = use_deprecated_plugins def get_trt_dtype(self): dtype = trt.float32 @@ -137,17 +145,30 @@ def attention_layer_opt(prefix, config, init_dict, network, input_tensor, mask_i if config.use_int8 and config.interleaved: pfc = trt.PluginFieldCollection(fields) - qkv2ctx_plug = mha_plg_creator3.create_plugin("qkv2ctx", pfc) + qkv2ctx_plug = create_plugin( + "qkv_to_context_interleaved", + plg_registry, + pfc, + use_deprecated_plugins=config.use_deprecated_plugins, + ) qkv_in = [mult_all.get_output(0), cu_seqlens, max_seqlen] else: fields.append(pf_has_mask) fields.append(pf_type) fields.append(pf_var_seqlen) pfc = trt.PluginFieldCollection(fields) - qkv2ctx_plug = mha_plg_creator2.create_plugin("qkv2ctx", pfc) + qkv2ctx_plug = create_plugin( + "qkv_to_context_varseqlen", + plg_registry, + pfc, + use_deprecated_plugins=config.use_deprecated_plugins, + ) qkv_in = [mult_all.get_output(0), mask_idx, cu_seqlens, max_seqlen] - qkv2ctx = network.add_plugin_v2(qkv_in, qkv2ctx_plug) - qkv2ctx.name = prefix + 'qkv_to_ctx' + qkv2ctx = add_plugin_to_network( + network, qkv2ctx_plug, qkv_in, use_deprecated_plugins=config.use_deprecated_plugins + ) + + qkv2ctx.name = prefix + "qkv_to_ctx" if config.use_qat: dr_ctx = init_dict[prefix + 'output_dense_input_amax'] @@ -171,14 +192,27 @@ def skipln(prefix, config, init_dict, network, input_tensor, skip, is_last_skipl if config.use_int8 and config.interleaved: pfc = trt.PluginFieldCollection([pf_beta, pf_gamma]) - creator = skln_plg_creator3 if not config.use_megatron or is_last_skipln else skln_plg_creator4 - skipln_plug = creator.create_plugin("skipln", pfc) + variant_name = ( + "skip_layer_norm_huggingface" + if not config.use_megatron or is_last_skipln + else "skip_layer_norm_megatron" + ) + skipln_plug = create_plugin( + variant_name, plg_registry, pfc, use_deprecated_plugins=config.use_deprecated_plugins + ) else: pfc = trt.PluginFieldCollection([pf_ld, pf_beta, pf_gamma, pf_type]) - skipln_plug = skln_plg_creator2.create_plugin("skipln", pfc) + skipln_plug = create_plugin( + "skip_layer_norm_varseqlen", + plg_registry, + pfc, + use_deprecated_plugins=config.use_deprecated_plugins, + ) skipln_inputs = [input_tensor, skip] - layer = network.add_plugin_v2(skipln_inputs, skipln_plug) + layer = add_plugin_to_network( + network, skipln_plug, skipln_inputs, use_deprecated_plugins=config.use_deprecated_plugins + ) return layer def transformer_layer_opt(prefix, config, init_dict, network, input_tensor, residual, mask_idx, cu_seqlens, max_seqlen): @@ -361,11 +395,23 @@ def emb_layernorm(builder, network, config, weights_dict, builder_config, max_se wposemb = trt.PluginField("bert_embeddings_position_embeddings", weights_dict["bert_embeddings_position_embeddings"].numpy(), trt.PluginFieldType.FLOAT32) output_fp16 = trt.PluginField("output_fp16", np.array([1 if config.use_fp16 or config.use_int8 else 0]).astype(np.int32), trt.PluginFieldType.INT32) - pfc = trt.PluginFieldCollection([wbeta, wgamma, wwordemb, wtokemb, wposemb, output_fp16]) - fn = (emln_plg_creator3 if config.use_megatron else emln_plg_creator2).create_plugin("embeddings", pfc) + pfc = trt.PluginFieldCollection( + [wbeta, wgamma, wwordemb, wtokemb, wposemb, output_fp16] + ) + variant_name = ( + "emb_layer_norm_megatron" + if config.use_megatron + else "emb_layer_norm_huggingface" + ) + fn = create_plugin( + variant_name, plg_registry, pfc, use_deprecated_plugins=config.use_deprecated_plugins + ) inputs = [input_ids, segment_ids, cu_seqlens, max_seqlen] - emb_layer = network.add_plugin_v2(inputs, fn) + + emb_layer = add_plugin_to_network( + network, fn, inputs, use_deprecated_plugins=config.use_deprecated_plugins + ) if config.use_int8 and config.use_qat: dr_input = weights_dict['l0_attention_self_query_input_amax'] @@ -458,29 +504,140 @@ def build_engine(batch_sizes, workspace_size, sequence_length, config, weights_d return serialized_engine def main(): - parser = argparse.ArgumentParser(description="TensorRT BERT Sample", formatter_class=argparse.ArgumentDefaultsHelpFormatter) - parser.add_argument("-m", "--ckpt", required=False, - help="The checkpoint file basename, e.g.: basename(model.ckpt-766908.data-00000-of-00001) is model.ckpt-766908") - parser.add_argument("-x", "--onnx", required=False, help="The ONNX model file path.") - parser.add_argument("-pt", "--pytorch", required=False, help="The PyTorch checkpoint file path.") - parser.add_argument("-pkl", "--pickle", required=False, help="The Pickle weights dictionary file path for the Megatron variant of BERT.") - parser.add_argument("-o", "--output", required=True, default="bert_base_384.engine", help="The bert engine file, ex bert.engine") - parser.add_argument("-b", "--max-batch-size", default=[], action="append", help="Max batch size. The engine will be usable with any input with (batch-size * sequence-length) below (max-batch-size * max-sequence-length). Can be specified multiple times to build optimization profiles for more than one batch size.", type=int) - parser.add_argument("-s", "--max-sequence-length", default=128, help="Max sequence length of the BERT model. The engine will be usable with any input with (batch-size * sequence-length) below (max-batch-size * max-sequence-length).", type=int) - parser.add_argument("-c", "--config-dir", required=True, - help="The folder containing the bert_config.json, which can be downloaded e.g. from https://github.com/google-research/bert#pre-trained-models or by running download_models.py in dle/TensorFlow/LanguageModeling/BERT/data/pretrained_models_google") - parser.add_argument("-f", "--fp16", action="store_true", help="Indicates that inference should be run in FP16 precision", required=False) - parser.add_argument("-i", "--int8", action="store_true", help="Indicates that inference should be run in INT8 precision", required=False) - parser.add_argument("-w", "--workspace-size", default=2500, help="Workspace size in MiB for building the BERT engine", type=int) - parser.add_argument("-j", "--squad-json", default="squad/dev-v1.1.json", help="squad json dataset used for int8 calibration", required=False) - parser.add_argument("-v", "--vocab-file", default="./pre-trained_model/uncased_L-24_H-1024_A-16/vocab.txt", help="Path to file containing entire understandable vocab", required=False) - parser.add_argument("-n", "--calib-num", default=100, help="calibration batch numbers", type=int) - parser.add_argument("-p", "--calib-path", help="calibration cache path", required=False) - parser.add_argument("-il", "--interleaved", action="store_true", help="use interleaved format, only valid in INT8 precision", required=False) - parser.add_argument("-tcf", "--timing-cache-file", help="Path to tensorrt build timeing cache file, only available for tensorrt 8.0 and later", required=False) - parser.add_argument("-sp", "--sparse", action="store_true", help="Indicates that model is sparse", required=False) - parser.add_argument("--megatron", action="store_true", help="Indicates that model is the Megatron-style architecture", required=False) - parser.add_argument("--verbose", action="store_true", help="Turn on verbose logger and set profiling verbosity to verbose", required=False) + parser = argparse.ArgumentParser( + description="TensorRT BERT Sample", + formatter_class=argparse.RawTextHelpFormatter, + ) + parser.add_argument( + "-m", + "--ckpt", + required=False, + help="The checkpoint file basename, e.g.: basename(model.ckpt-766908.data-00000-of-00001) is model.ckpt-766908 (default: None)", + ) + parser.add_argument( + "-x", "--onnx", required=False, help="The ONNX model file path. (default: None)" + ) + parser.add_argument( + "-pt", "--pytorch", required=False, help="The PyTorch checkpoint file path. (default: None)" + ) + parser.add_argument( + "-pkl", + "--pickle", + required=False, + help="The Pickle weights dictionary file path for the Megatron variant of BERT. (default: None)", + ) + parser.add_argument( + "-o", + "--output", + required=True, + default="bert_base_384.engine", + help="The bert engine file, ex bert.engine (default: bert_base_384.engine)", + ) + parser.add_argument( + "-b", + "--max-batch-size", + default=[], + action="append", + help="Max batch size. The engine will be usable with any input with (batch-size * sequence-length) below (max-batch-size * max-sequence-length). Can be specified multiple times to build optimization profiles for more than one batch size. (default: [1])", + type=int, + ) + parser.add_argument( + "-s", + "--max-sequence-length", + default=128, + help="Max sequence length of the BERT model. The engine will be usable with any input with (batch-size * sequence-length) below (max-batch-size * max-sequence-length). (default: 128)", + type=int, + ) + parser.add_argument( + "-c", + "--config-dir", + required=True, + help="The folder containing the bert_config.json, which can be downloaded e.g. from https://github.com/google-research/bert#pre-trained-models or by running download_models.py in dle/TensorFlow/LanguageModeling/BERT/data/pretrained_models_google", + ) + parser.add_argument( + "-f", + "--fp16", + action="store_true", + help="Indicates that inference should be run in FP16 precision (default: false)", + required=False, + ) + parser.add_argument( + "-i", + "--int8", + action="store_true", + help="Indicates that inference should be run in INT8 precision (default: false)", + required=False, + ) + parser.add_argument( + "-w", + "--workspace-size", + default=2500, + help="Workspace size in MiB for building the BERT engine (default: 2500)", + type=int, + ) + parser.add_argument( + "-j", + "--squad-json", + default="squad/dev-v1.1.json", + help="squad json dataset used for int8 calibration (default: squad/dev-v1.1.json)", + required=False, + ) + parser.add_argument( + "-v", + "--vocab-file", + default="./pre-trained_model/uncased_L-24_H-1024_A-16/vocab.txt", + help="Path to file containing entire understandable vocab (default: ./pre-trained_model/uncased_L-24_H-1024_A-16/vocab.txt)", + required=False, + ) + parser.add_argument( + "-n", "--calib-num", default=100, help="calibration batch numbers (default: 100)", type=int + ) + parser.add_argument( + "-p", "--calib-path", help="calibration cache path (default: None)", required=False + ) + parser.add_argument( + "-il", + "--interleaved", + action="store_true", + help="use interleaved format, only valid in INT8 precision (default: false)", + required=False, + ) + parser.add_argument( + "-tcf", + "--timing-cache-file", + help="Path to tensorrt build timeing cache file, only available for tensorrt 8.0 and later (default: None)", + required=False, + ) + parser.add_argument( + "-sp", + "--sparse", + action="store_true", + help="Indicates that model is sparse (default: false)", + required=False, + ) + parser.add_argument( + "--megatron", + action="store_true", + help="Indicates that model is the Megatron-style architecture (default: false)", + required=False, + ) + parser.add_argument( + "--verbose", + action="store_true", + help="Turn on verbose logger and set profiling verbosity to verbose (default: false)", + required=False, + ) + + plugin_group = parser.add_mutually_exclusive_group(required=False) + plugin_group.add_argument('--use-v3-plugins', + dest='use_deprecated_plugins', + action='store_false', + help="Use plugins implementing the IPluginV3 interface wherever TensorRT plugins are used. Cannot be used with --use-deprecated-plugins. Enabling this option should not affect functionality or performance. (default: false)") + plugin_group.add_argument('--use-deprecated-plugins', + dest='use_deprecated_plugins', + action='store_true', + help="Use deprecated plugins implementing the IPluginV2 interface wherever TensorRT plugins are used (instead of updated plugins implementing the IPluginV3 interface). Cannot be used with --use-v3-plugins. Disabling this option should not affect functionality or performance. (default: true)") + parser.set_defaults(use_deprecated_plugins=True) args, _ = parser.parse_known_args() args.max_batch_size = args.max_batch_size or [1] @@ -501,7 +658,17 @@ def main(): bert_config_path = os.path.join(args.config_dir, "bert_config.json") TRT_LOGGER.log(TRT_LOGGER.INFO, "Using configuration file: {:}".format(bert_config_path)) - config = BertConfig(bert_config_path, args.fp16, args.int8, args.int8 and (args.onnx or args.pytorch or args.pickle), args.interleaved, args.timing_cache_file, args.sparse, args.megatron) + config = BertConfig( + bert_config_path, + args.fp16, + args.int8, + args.int8 and (args.onnx or args.pytorch or args.pickle), + args.interleaved, + args.timing_cache_file, + args.sparse, + args.megatron, + args.use_deprecated_plugins, + ) if args.calib_path != None: calib_cache = args.calib_path diff --git a/demo/Diffusion/README.md b/demo/Diffusion/README.md index 778767a5..974bad5b 100755 --- a/demo/Diffusion/README.md +++ b/demo/Diffusion/README.md @@ -48,7 +48,7 @@ onnx 1.15.0 onnx-graphsurgeon 0.5.2 onnxruntime 1.16.3 polygraphy 0.49.9 -tensorrt 10.5.0.18 +tensorrt 10.6.0.26 tokenizers 0.13.3 torch 2.2.0 transformers 4.42.2 @@ -148,7 +148,7 @@ python3 demo_txt2img_xl.py "a photo of an astronaut riding a horse on mars" --hf ### Generate an image guided by a text prompt, and using specified LoRA model weight updates ```bash -python3 demo_txt2img_xl.py "Picture of a rustic Italian village with Olive trees and mountains" --version=xl-1.0 --lora-path "ostris/crayon_style_lora_sdxl" "ostris/watercolor_style_lora_sdxl" --lora-scale 0.3 0.7 --onnx-dir onnx-sdxl-lora --engine-dir engine-sdxl-lora --build-enable-refit +python3 demo_txt2img_xl.py "Picture of a rustic Italian village with Olive trees and mountains" --version=xl-1.0 --lora-path "ostris/crayon_style_lora_sdxl" "ostris/watercolor_style_lora_sdxl" --lora-weight 0.3 0.7 --onnx-dir onnx-sdxl-lora --engine-dir engine-sdxl-lora --build-enable-refit ``` ### Faster Text-to-image using SDXL INT8 & FP8 quantization using ModelOpt @@ -174,7 +174,7 @@ For step-by-step tutorials to run INT8 & FP8 inference on stable diffusion model [LCM-LoRA](https://arxiv.org/abs/2311.05556) produces good quality images in 4 to 8 denoising steps instead of 30+ needed base model. Note that we use LCM scheduler and disable classifier-free-guidance by setting `--guidance-scale` to 0. LoRA weights are fused into the ONNX and finalized TensorRT plan files in this example. ```bash -python3 demo_txt2img_xl.py "Einstein" --version xl-1.0 --lora-path "latent-consistency/lcm-lora-sdxl" --lora-scale 1.0 --onnx-dir onnx-sdxl-lcm-nocfg --engine-dir engine-sdxl-lcm-nocfg --denoising-steps 4 --scheduler LCM --guidance-scale 0.0 +python3 demo_txt2img_xl.py "Einstein" --version xl-1.0 --lora-path "latent-consistency/lcm-lora-sdxl" --lora-weight 1.0 --onnx-dir onnx-sdxl-lcm-nocfg --engine-dir engine-sdxl-lcm-nocfg --denoising-steps 4 --scheduler LCM --guidance-scale 0.0 ``` ### Faster Text-to-Image using SDXL Turbo Even faster image generation than LCM, producing coherent images in just 1 step. Note: SDXL Turbo works best for 512x512 resolution, EulerA scheduler and classifier-free-guidance disabled. @@ -215,6 +215,21 @@ SVD-XT-1.1 (25 frames at resolution 576x1024) python3 demo_img2vid.py --version svd-xt-1.1 --onnx-dir onnx-svd-xt-1-1 --engine-dir engine-svd-xt-1-1 --hf-token=$HF_TOKEN ``` + +Run the command below to generate a video in FP8. + +```bash +python3 demo_img2vid.py --version svd-xt-1.1 --onnx-dir onnx-svd-xt-1-1 --engine-dir engine-svd-xt-1-1 --hf-token=$HF_TOKEN --fp8 +``` + +> NOTE: There is a bug in HuggingFace, you can workaround with following this [PR](https://github.com/huggingface/diffusers/pull/6562/files) + +``` +if torch.is_tensor(num_frames): + num_frames = num_frames.item() +emb = emb.repeat_interleave(num_frames, dim=0) +``` + You may also specify a custom conditioning image using `--input-image`: ```bash python3 demo_img2vid.py --version svd-xt-1.1 --onnx-dir onnx-svd-xt-1-1 --engine-dir engine-svd-xt-1-1 --input-image https://www.hdcarwallpapers.com/walls/2018_chevrolet_camaro_zl1_nascar_race_car_2-HD.jpg --hf-token=$HF_TOKEN @@ -246,6 +261,18 @@ python3 demo_stable_cascade.py --onnx-opset=16 "Anthropomorphic cat dressed as a python3 demo_txt2img_flux.py "a beautiful photograph of Mt. Fuji during cherry blossom" --hf-token=$HF_TOKEN ``` +Run the below command to generate an image with FLUX in BF16. + +```bash +python3 demo_txt2img_flux.py "a beautiful photograph of Mt. Fuji during cherry blossom" --hf-token=$HF_TOKEN --bf16 +``` + +Run the below command to generate an image with FLUX in FP8. (FP8 is only supppoted on Hopper.) + +```bash +python3 demo_txt2img_flux.py "a beautiful photograph of Mt. Fuji during cherry blossom" --hf-token=$HF_TOKEN --fp8 +``` + NOTE: Running the Flux pipeline requires 80GB of GPU memory or higher ## Configuration options @@ -254,8 +281,4 @@ NOTE: Running the Flux pipeline requires 80GB of GPU memory or higher - Specify new directories for storing onnx and engine files when switching between versions, LoRAs, ControlNets, etc. This can be done using `--onnx-dir ` and `--engine-dir `. - Inference performance can be improved by enabling [CUDA graphs](https://docs.nvidia.com/cuda/cuda-c-programming-guide/index.html#cuda-graphs) using `--use-cuda-graph`. Enabling CUDA graphs requires fixed input shapes, so this flag must be combined with `--build-static-batch` and cannot be combined with `--build-dynamic-shape`. -## Known Issues -- LoRA adapter functionality is compatible with diffusers version 0.26.3. To run the LoRA pipeline, we recommend installing this specific version. However, the Stable Cascade pipeline requires diffusers version 0.29.2 or higher and will not be compatible if diffusers is downgraded. - - diff --git a/demo/Diffusion/calibration-images/rocket.png b/demo/Diffusion/calibration-images/rocket.png new file mode 100644 index 0000000000000000000000000000000000000000..3f6fef6fafe7a32cef32bcf01f9493bb9441c379 GIT binary patch literal 493509 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⪙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 z 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

V5%LJ#L^W!{`| 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

JC8-px`(EiX?Kce#6*Io8Kx>ZJRQi)(L(^* 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

M6|(5J5_CUXZ1CP|pN&!I$!o-#A2 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 literal 0 HcmV?d00001 diff --git a/demo/Diffusion/demo_img2vid.py b/demo/Diffusion/demo_img2vid.py index 7d7b2b04..197d3905 100644 --- a/demo/Diffusion/demo_img2vid.py +++ b/demo/Diffusion/demo_img2vid.py @@ -63,6 +63,19 @@ def process_pipeline_args(args): if not args.build_static_batch or args.build_dynamic_shape: raise ValueError(f"Dynamic shapes not supported. Do not specify `--build-dynamic-shape`") + if args.fp8: + import torch + device_info = torch.cuda.get_device_properties(0) + version = device_info.major * 10 + device_info.minor + if version < 90: # FP8 is only supppoted on Hopper. + raise ValueError(f"Cannot apply FP8 quantization for GPU with compute capability {version / 10.0}. FP8 is only supppoted on Hopper.") + args.optimization_level = 4 + print(f"[I] The default optimization level has been set to {args.optimization_level} for FP8.") + + if args.quantization_level == 0.0 and args.fp8: + args.quantization_level = 3.0 + print("[I] The default quantization level has been set to 3.0 for FP8.") + kwargs_init_pipeline = { 'version': args.version, 'max_batch_size': max_batch_size, @@ -89,6 +102,9 @@ def process_pipeline_args(args): 'enable_all_tactics': args.build_all_tactics, 'enable_refit': args.build_enable_refit, 'timing_cache': args.timing_cache, + 'fp8': args.fp8, + 'quantization_level': args.quantization_level, + } args_run_demo = (input_image, args.height, args.width, args.batch_size, args.batch_count, args.num_warmup_runs, args.use_cuda_graph) @@ -99,7 +115,6 @@ def process_pipeline_args(args): print("[I] Initializing StableDiffusion img2vid demo using TensorRT") args = parseArgs() kwargs_init_pipeline, kwargs_load_engine, args_run_demo = process_pipeline_args(args) - # Initialize demo demo = StableVideoDiffusionPipeline( pipeline_type=PIPELINE_TYPE.IMG2VID, diff --git a/demo/Diffusion/demo_txt2img_flux.py b/demo/Diffusion/demo_txt2img_flux.py index 067c2c8f..501a6a05 100644 --- a/demo/Diffusion/demo_txt2img_flux.py +++ b/demo/Diffusion/demo_txt2img_flux.py @@ -68,6 +68,17 @@ def parse_args(): default=512, help="Maximum sequence length to use with the prompt", ) + parser.add_argument( + "--bf16", + action='store_true', + help="Run pipeline in BFloat16 precision" + ) + parser.add_argument( + "--low-vram", + action='store_true', + help="Optimize for low VRAM usage, possibly at the expense of inference performance. Disabled by default." + ) + return parser.parse_args() @@ -118,8 +129,9 @@ def process_demo_args(args): demo = FluxPipeline( pipeline_type=PIPELINE_TYPE.TXT2IMG, max_sequence_length=args.max_sequence_length, - **kwargs_init_pipeline, - ) + bf16=args.bf16, + low_vram=args.low_vram, + **kwargs_init_pipeline) # Load TensorRT engines and pytorch modules demo.load_engines( diff --git a/demo/Diffusion/diffusion_pipeline.py b/demo/Diffusion/diffusion_pipeline.py index 02c7a631..37332375 100644 --- a/demo/Diffusion/diffusion_pipeline.py +++ b/demo/Diffusion/diffusion_pipeline.py @@ -51,14 +51,18 @@ from typing import Optional, List from utils_modelopt import ( filter_func, + filter_func_no_proj_out, quantize_lvl, get_int8_config, check_lora, set_fmha, + set_quant_precision, generate_fp8_scales, + SD_FP8_BF16_DEFAULT_CONFIG, SD_FP8_FP16_DEFAULT_CONFIG, SD_FP8_FP32_DEFAULT_CONFIG, ) +import gc class DiffusionPipeline(ABC): """ @@ -101,7 +105,8 @@ def __init__( max_batch_size=16, denoising_steps=30, scheduler=None, - lora_scale: Optional[List[int]] = None, + lora_scale: float = 1.0, + lora_weight: Optional[List[float]] = None, lora_path: Optional[List[str]] = None, device='cuda', output_dir='.', @@ -129,7 +134,9 @@ def __init__( scheduler (str): The scheduler to guide the denoising process. Must be one of the values listed in DiffusionPipeline.SCHEDULER_DEFAULTS.values(). lora_scale (float): - Scale of LoRA weights, default 1 (must between 0 and 1). + Controls how much to influence the outputs with the LoRA parameters. (must between 0 and 1). + lora_weight (float): + The LoRA adapter(s) weights to use with the UNet. (must between 0 and 1). lora_path (str): Path to LoRA adaptor. Ex: 'latent-consistency/lcm-lora-sdv1-5'. device (str): @@ -204,16 +211,17 @@ def __init__( self.models = {} self.torch_models = {} self.engine = {} + self.shape_dicts = {} self.shared_device_memory = None # initialize lora loader and scales self.lora_loader = None - self.lora_scales = dict() + self.lora_weights = dict() if lora_path: - self.lora_loader = LoraLoader(lora_path) - assert len(lora_path) == len(lora_scale) + self.lora_loader = LoraLoader(lora_path, lora_weight, lora_scale) + assert len(lora_path) == len(lora_weight) for i, path in enumerate(lora_path): - self.lora_scales[path] = lora_scale[i] + self.lora_weights[path] = lora_weight[i] # initialized in load_resources() self.events = {} @@ -254,7 +262,9 @@ def load_resources(self, image_height, image_width, batch_size, seed): for model_name, obj in self.models.items(): if self.torch_fallback[model_name]: continue - self.engine[model_name].allocate_buffers(shape_dict=obj.get_shape_dict(batch_size, image_height, image_width), device=self.device) + self.shape_dicts[model_name] = obj.get_shape_dict(batch_size, image_height, image_width) + if not self.low_vram: + self.engine[model_name].allocate_buffers(shape_dict=obj.get_shape_dict(batch_size, image_height, image_width), device=self.device) def _create_directories(self, engine_dir, onnx_dir): # Create directories if missing @@ -297,7 +307,7 @@ def _initialize_models(self): def _get_lora_suffix(self): if self.lora_loader: - return '-' + '-'.join([str(md5(path.encode('utf-8')).hexdigest()) + '-' + ('%.2f' % self.lora_scales[path]) for path in sorted(self.lora_loader.paths)]) + return '-' + '-'.join([str(md5(path.encode('utf-8')).hexdigest()) + '-' + ('%.2f' % self.lora_weights[path]) + '-' + ('%.2f' % self.lora_loader.scale) for path in sorted(self.lora_loader.paths)]) return '' def _prepare_model_configs(self, onnx_dir, engine_dir, enable_refit, int8, fp8, quantization_level, quantization_percentile, quantization_alpha, calibration_size): @@ -317,12 +327,15 @@ def _prepare_model_configs(self, onnx_dir, engine_dir, enable_refit, int8, fp8, if int8: assert self.pipeline_type.is_sd_xl_base() or self.version in ["1.5", "2.1", "2.1-base"], "int8 quantization only supported for SDXL, SD1.5 and SD2.1 pipeline" - if model_name == ('unetxl' if self.pipeline_type.is_sd_xl() else 'unet'): + if (self.pipeline_type.is_sd_xl() and model_name == 'unetxl') or \ + (model_name == 'unet'): config['use_int8'] = True config['model_suffix'] += f"-int8.l{quantization_level}.bs2.s{self.denoising_steps}.c{calibration_size}.p{quantization_percentile}.a{quantization_alpha}" elif fp8: - assert self.pipeline_type.is_sd_xl() or self.version in ["1.5", "2.1", "2.1-base"], "fp8 quantization only supported for SDXL, SD1.5 and SD2.1 pipeline" - if model_name == ('unetxl' if self.pipeline_type.is_sd_xl() else 'unet'): + assert self.pipeline_type.is_sd_xl() or self.version in ["1.5", "2.1", "2.1-base", "flux.1-dev"], "fp8 quantization only supported for SDXL, SD1.5, SD2.1 and FLUX pipeline" + if (self.pipeline_type.is_sd_xl() and model_name == 'unetxl') or \ + (self.version == "flux.1-dev" and model_name == 'transformer') or \ + (model_name == 'unet'): config['use_fp8'] = True config['model_suffix'] += f"-fp8.l{quantization_level}.bs2.s{self.denoising_steps}.c{calibration_size}.p{quantization_percentile}.a{quantization_alpha}" @@ -337,7 +350,7 @@ def _prepare_model_configs(self, onnx_dir, engine_dir, enable_refit, int8, fp8, return configs - def _calibrate_and_save_model(self, pipeline, model, model_config, quantization_level, quantization_percentile, quantization_alpha, calibration_size, calib_batch_size): + def _calibrate_and_save_model(self, pipeline, model, model_config, quantization_level, quantization_percentile, quantization_alpha, calibration_size, calib_batch_size, **kwargs): print(f"[I] Calibrated weights not found, generating {model_config['state_dict_path']}") calibration_file = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'calibration-prompts.txt') calibration_prompts = load_calib_prompts(calib_batch_size, calibration_file) @@ -347,22 +360,42 @@ def do_calibrate(pipeline, calibration_prompts, **kwargs): for i_th, prompts in enumerate(calibration_prompts): if i_th >= kwargs["calib_size"]: return - pipeline( - prompt=prompts, - num_inference_steps=kwargs["n_steps"], - negative_prompt=[ - "normal quality, low quality, worst quality, low res, blurry, nsfw, nude" - ] - * len(prompts), - ).images + if kwargs["model_id"] == "flux.1-dev": + + height = kwargs.get("height", 1024) + width = kwargs.get("width", 1024) + pipeline( + prompt=prompts, + prompt_2=prompts, + num_inference_steps=kwargs["n_steps"], + height=height, + width=width, + guidance_scale=3.5, + max_sequence_length=512 + ).images + else: + pipeline( + prompt=prompts, + num_inference_steps=kwargs["n_steps"], + negative_prompt=[ + "normal quality, low quality, worst quality, low res, blurry, nsfw, nude" + ] + * len(prompts), + ).images def forward_loop(model): - pipeline.unet = model + if self.version not in ["sd3", "flux.1-dev"]: + pipeline.unet = model + else: + pipeline.transformer = model + do_calibrate( pipeline=pipeline, calibration_prompts=calibration_prompts, calib_size=calibration_size // calib_batch_size, n_steps=self.denoising_steps, + model_id=self.version, + **kwargs ) print(f"[I] Performing calibration for {calibration_size} steps.") @@ -375,25 +408,36 @@ def forward_loop(model): self.denoising_steps ) elif model_config['use_fp8']: - quant_config = SD_FP8_FP32_DEFAULT_CONFIG if self.version == "2.1" else SD_FP8_FP16_DEFAULT_CONFIG + if self.version == "flux.1-dev": + quant_config = SD_FP8_BF16_DEFAULT_CONFIG + elif self.version == "2.1": + quant_config = SD_FP8_FP32_DEFAULT_CONFIG + else: + quant_config = SD_FP8_FP16_DEFAULT_CONFIG + check_lora(model) + if self.version == "flux.1-dev": + set_quant_precision(quant_config, "BFloat16") mtq.quantize(model, quant_config, forward_loop) mto.save(model, model_config['state_dict_path']) - def _get_quantized_model(self, obj, model_config, quantization_level, quantization_percentile, quantization_alpha, calibration_size, calib_batch_size): + def _get_quantized_model(self, obj, model_config, quantization_level, quantization_percentile, quantization_alpha, calibration_size, calib_batch_size, **kwargs): pipeline = obj.get_pipeline() - model = pipeline.unet + model = pipeline.unet if self.version not in ["sd3", "flux.1-dev"] else pipeline.transformer if model_config['use_fp8'] and quantization_level == 4.0: set_fmha(model) if not os.path.exists(model_config['state_dict_path']): - self._calibrate_and_save_model(pipeline, model, model_config, quantization_level, quantization_percentile, quantization_alpha, calibration_size, calib_batch_size) + self._calibrate_and_save_model(pipeline, model, model_config, quantization_level, quantization_percentile, quantization_alpha, calibration_size, calib_batch_size, **kwargs) else: mto.restore(model, model_config['state_dict_path']) if not os.path.exists(model_config['onnx_path']): quantize_lvl(model, quantization_level) - mtq.disable_quantizer(model, filter_func) + if self.version in ["flux.1-dev"]: + mtq.disable_quantizer(model, filter_func_no_proj_out) + else: + mtq.disable_quantizer(model, filter_func) if model_config['use_fp8']: generate_fp8_scales(model) else: @@ -407,10 +451,10 @@ def _export_onnx(self, obj, model_config, opt_image_height, opt_image_width, sta if do_export_onnx or do_export_weights_map: if not model_config['use_int8'] and not model_config['use_fp8']: - obj.export_onnx(model_config['onnx_path'], model_config['onnx_opt_path'], onnx_opset, opt_image_height, opt_image_width, enable_lora_merge=model_config['do_lora_merge'], static_shape=static_shape) + obj.export_onnx(model_config['onnx_path'], model_config['onnx_opt_path'], onnx_opset, opt_image_height, opt_image_width, enable_lora_merge=model_config['do_lora_merge'], static_shape=static_shape, lora_loader=self.lora_loader) else: print(f"[I] Generating quantized ONNX model: {model_config['onnx_path']}") - quantized_model = self._get_quantized_model(obj, model_config, quantization_level, quantization_percentile, quantization_alpha, calibration_size, calib_batch_size) + quantized_model = self._get_quantized_model(obj, model_config, quantization_level, quantization_percentile, quantization_alpha, calibration_size, calib_batch_size, height=opt_image_width, width=opt_image_width) obj.export_onnx(model_config['onnx_path'], model_config['onnx_opt_path'], onnx_opset, opt_image_height, opt_image_width, custom_model=quantized_model, static_shape=static_shape) # FIXME do_export_weights_map needs ONNX graph @@ -453,14 +497,14 @@ def _refit_engine(self, obj, model_name, model_config): if not os.path.exists(model_config['refit_weights_path']): print(f"[I] Saving refit weights: {model_config['refit_weights_path']}") - model = merge_loras(obj.get_model(), obj.lora_dict, obj.lora_alphas, obj.lora_scales) - refit_weights = get_refit_weights(model.state_dict(), model_config['onnx_opt_path'], weights_name_mapping, weights_shape_mapping) - torch.save(refit_weights, model_config['refit_weights_path']) + model = merge_loras(obj.get_model(), self.lora_loader) + refit_weights, updated_weight_names = get_refit_weights(model.state_dict(), model_config['onnx_opt_path'], weights_name_mapping, weights_shape_mapping) + torch.save((refit_weights, updated_weight_names), model_config['refit_weights_path']) unload_model(model) else: print(f"[I] Loading refit weights: {model_config['refit_weights_path']}") - refit_weights = torch.load(model_config['refit_weights_path']) - self.engine[model_name].refit(refit_weights, obj.fp16) + refit_weights, updated_weight_names = torch.load(model_config['refit_weights_path']) + self.engine[model_name].refit(refit_weights, updated_weight_names) def _load_torch_models(self): # Load torch models @@ -551,6 +595,10 @@ def load_engines( continue self._export_onnx(obj, model_configs[model_name], opt_image_height, opt_image_width, static_shape, onnx_opset, quantization_level, quantization_percentile, quantization_alpha, calibration_size, calib_batch_size) + # Release temp GPU memory during onnx export to avoid OOM. + gc.collect() + torch.cuda.empty_cache() + # Build TensorRT engines for model_name, obj in self.models.items(): if self.torch_fallback[model_name]: @@ -566,11 +614,21 @@ def load_engines( for model_name, obj in self.models.items(): if self.torch_fallback[model_name]: continue - self.engine[model_name].load() - model_config = model_configs[model_name] - if model_config['do_engine_refit'] and obj.lora_dict: + + # For non low_vram case, the engines will remain in GPU memory from now on. + assert self.engine[model_name].engine is None + if not self.low_vram: + self.engine[model_name].load() + + if model_config['do_engine_refit'] and self.lora_loader: + # For low_vram, using on-demand load and unload for refit. + if self.low_vram: + assert self.engine[model_name].engine is None + self.engine[model_name].load() self._refit_engine(obj, model_name, model_config) + if self.low_vram: + self.engine[model_name].unload() # Load PyTorch models if torch-inference mode is enabled self._load_torch_models() @@ -581,7 +639,11 @@ def load_engines( def calculate_max_device_memory(self): max_device_memory = 0 for model_name, engine in self.engine.items(): + if self.low_vram: + engine.load() max_device_memory = max(max_device_memory, engine.engine.device_memory_size) + if self.low_vram: + engine.unload() return max_device_memory def activate_engines(self, shared_device_memory=None): @@ -590,11 +652,15 @@ def activate_engines(self, shared_device_memory=None): _, shared_device_memory = cudart.cudaMalloc(max_device_memory) self.shared_device_memory = shared_device_memory # Load and activate TensorRT engines - for engine in self.engine.values(): - engine.activate(device_memory=self.shared_device_memory) + if not self.low_vram: + for engine in self.engine.values(): + engine.activate(device_memory=self.shared_device_memory) def run_engine(self, model_name, feed_dict): engine = self.engine[model_name] + # CUDA graphs should be disabled when low_vram is enabled. + if self.low_vram: + assert self.use_cuda_graph == False return engine.infer(feed_dict, self.stream, use_cuda_graph=self.use_cuda_graph) def teardown(self): @@ -645,4 +711,3 @@ def infer(self): def run(self): """Run the pipeline.""" raise NotImplementedError("Please Implement the run method") - diff --git a/demo/Diffusion/flux_pipeline.py b/demo/Diffusion/flux_pipeline.py index 4de8e0f7..fb764d68 100644 --- a/demo/Diffusion/flux_pipeline.py +++ b/demo/Diffusion/flux_pipeline.py @@ -61,7 +61,9 @@ def __init__( pipeline_type=PIPELINE_TYPE.TXT2IMG, guidance_scale=3.5, max_sequence_length=512, - **kwargs, + bf16=False, + low_vram=False, + **kwargs ): """ Initializes the Flux pipeline. @@ -72,10 +74,14 @@ def __init__( Higher guidance scale encourages to generate images that are closely linked to the text prompt, usually at the expense of lower image quality. max_sequence_length (`int`, defaults to 512): Maximum sequence length to use with the `prompt`. + bf16 (`bool`, defaults to False): + Whether to run the pipeline in BFloat16 precision. """ super().__init__(version=version, pipeline_type=pipeline_type, **kwargs) self.guidance_scale = guidance_scale self.max_sequence_length = max_sequence_length + self.bf16=bf16 + self.low_vram = low_vram # Pipeline type self.stages = ["clip", "t5", "transformer", "vae"] @@ -105,13 +111,15 @@ def _initialize_models(self, framework_model_dir, int8, fp8): "max_batch_size": self.max_batch_size, } - self.fp16 = True + self.bf16 = True if int8 or fp8 else self.bf16 + self.fp16 = True if not self.bf16 else False self.tf32 = True if "clip" in self.stages: self.models["clip"] = CLIPModel( **models_args, fp16=self.fp16, tf32=self.tf32, + bf16=self.bf16, embedding_dim=get_clip_embedding_dim(self.version, self.pipeline_type), keep_pooled_output=True, subfolder="text_encoder", @@ -123,6 +131,7 @@ def _initialize_models(self, framework_model_dir, int8, fp8): **models_args, fp16=False, tf32=self.tf32, + bf16=self.bf16, subfolder="text_encoder_2", text_maxlen=self.max_sequence_length, ) @@ -130,15 +139,18 @@ def _initialize_models(self, framework_model_dir, int8, fp8): if "transformer" in self.stages: self.models["transformer"] = FluxTransformerModel( **models_args, - fp16=self.fp16, + bf16=True if int8 or fp8 else self.bf16, + fp16=False if int8 or fp8 else self.fp16, + int8=int8, + fp8=fp8, tf32=self.tf32, text_maxlen=self.max_sequence_length, - build_strongly_typed=False, + build_strongly_typed=True, ) if "vae" in self.stages: # Accuracy issues with FP16 - self.models["vae"] = VAEModel(**models_args, fp16=False, tf32=self.tf32) + self.models["vae"] = VAEModel(**models_args, fp16=False, tf32=self.tf32, bf16=self.bf16) self.vae_scale_factor = ( 2 ** (len(self.models["vae"].config["block_out_channels"])) @@ -297,9 +309,7 @@ def tokenize(prompt, max_sequence_length): text_encoder_output = tokenize(prompt, max_sequence_length) self.profile_stop(encoder) - return ( - text_encoder_output.to(torch.float16) if self.fp16 else text_encoder_output - ) + return text_encoder_output.to(torch.float16) if self.fp16 else text_encoder_output.to(torch.bfloat16) if self.bf16 else text_encoder_output def denoise_latent( self, @@ -347,7 +357,7 @@ def denoise_latent( )[0] self.profile_stop(denoiser) - return latents.to(dtype=torch.float32) + return latents.to(dtype=torch.bfloat16) if self.bf16 else latents.to(dtype=torch.float32) def decode_latent(self, latents, decoder="vae"): self.profile_start(decoder, color="red") @@ -445,17 +455,40 @@ def infer( // 4, latent_height=latent_height, latent_width=latent_width, - latents_dtype=torch.float16 if self.fp16 else torch.float32, - ) + latents_dtype=torch.float16 if self.fp16 else torch.bfloat16 if self.bf16 else torch.float32) + + class LoadModelContext: + def __init__(ctx, model_names, low_vram=False): + ctx.model_names = model_names + ctx.low_vram = low_vram + def __enter__(ctx): + if not ctx.low_vram: + return + for model_name in ctx.model_names: + # creating engine object (load from plan file) + self.engine[model_name].load() + # creating context + self.engine[model_name].activate(device_memory=self.shared_device_memory) + # creating input and output buffer + self.engine[model_name].allocate_buffers(shape_dict=self.shape_dicts[model_name], device=self.device) + def __exit__(ctx, exc_type, exc_val, exc_tb): + if not ctx.low_vram: + return + for model_name in ctx.model_names: + self.engine[model_name].deallocate_buffers() + self.engine[model_name].deactivate() + self.engine[model_name].unload() # CLIP and T5 text encoder(s) - pooled_embeddings = self.encode_prompt(prompt, pooled_output=True) - text_embeddings = self.encode_prompt( - prompt2, encoder="t5", max_sequence_length=self.max_sequence_length - ) - text_ids = torch.zeros(text_embeddings.shape[1], 3).to( - device=self.device, dtype=text_embeddings.dtype - ) + + with LoadModelContext(["clip","t5"], low_vram=self.low_vram): + pooled_embeddings = self.encode_prompt(prompt, pooled_output=True) + text_embeddings = self.encode_prompt( + prompt2, encoder="t5", max_sequence_length=self.max_sequence_length + ) + text_ids = torch.zeros(text_embeddings.shape[1], 3).to( + device=self.device, dtype=text_embeddings.dtype + ) # Prepare timesteps sigmas = np.linspace(1.0, 1 / num_inference_steps, num_inference_steps) @@ -486,23 +519,25 @@ def infer( num_inference_steps = len(timesteps) # DiT denoiser - latents = self.denoise_latent( - latents, - timesteps, - text_embeddings, - pooled_embeddings, - text_ids, - latent_image_ids, - ) + with LoadModelContext(["transformer"], low_vram=self.low_vram): + latents = self.denoise_latent( + latents, + timesteps, + text_embeddings, + pooled_embeddings, + text_ids, + latent_image_ids, + ) # VAE decode latent - latents = self._unpack_latents( - latents, image_height, image_width, self.vae_scale_factor - ) - latents = ( - latents / self.models["vae"].config["scaling_factor"] - ) + self.models["vae"].config["shift_factor"] - images = self.decode_latent(latents) + with LoadModelContext(["vae"], low_vram=self.low_vram): + latents = self._unpack_latents( + latents, image_height, image_width, self.vae_scale_factor + ) + latents = ( + latents / self.models["vae"].config["scaling_factor"] + ) + self.models["vae"].config["shift_factor"] + images = self.decode_latent(latents) torch.cuda.synchronize() e2e_toc = time.perf_counter() @@ -539,6 +574,9 @@ def run( use_cuda_graph, **kwargs, ): + if self.low_vram and self.use_cuda_graph: + print("[W] Using low_vram, use_cuda_graph will be disabled") + self.use_cuda_graph = False num_warmup_runs = max(1, num_warmup_runs) if use_cuda_graph else num_warmup_runs if num_warmup_runs > 0: print("[I] Warming up ..") diff --git a/demo/Diffusion/models.py b/demo/Diffusion/models.py index 241ca821..1f9a71ed 100755 --- a/demo/Diffusion/models.py +++ b/demo/Diffusion/models.py @@ -16,7 +16,7 @@ # from diffusers import DiffusionPipeline -from diffusers.loaders import LoraLoaderMixin +from diffusers.loaders import StableDiffusionLoraLoaderMixin from diffusers.pipelines.wuerstchen import PaellaVQModel import json import numpy as np @@ -186,7 +186,7 @@ def fuse_mha_qkv_int8_sq(self): print(f"Removed {removed} QDQ nodes") return removed # expected 72 for L2.5 - def modify_fp8_graph(self): + def modify_fp8_graph(self, is_fp16_io=True): onnx_graph = gs.export_onnx(self.graph) # Convert INT8 Zero to FP8. onnx_graph = convert_zp_fp8(onnx_graph) @@ -196,7 +196,8 @@ def modify_fp8_graph(self): # Add cast nodes to Resize I/O. cast_resize_io(self.graph) # Convert model inputs and outputs to fp16 I/O. - convert_fp16_io(self.graph) + if is_fp16_io: + convert_fp16_io(self.graph) # Add cast nodes to MHA's BMM1 and BMM2's I/O. cast_fp8_mha_io(self.graph) @@ -289,52 +290,15 @@ def optimize_checkpoint(model, torch_inference): assert torch_inference in torch_inference_modes return torch.compile(model, mode=torch_inference, dynamic=False, fullgraph=False) -class LoraLoader(LoraLoaderMixin): +class LoraLoader(StableDiffusionLoraLoaderMixin): def __init__(self, paths, + weights, + scale ): self.paths = paths - self.state_dict = dict() - self.network_alphas = dict() - - for path in paths: - state_dict, network_alphas = self.lora_state_dict(path) - is_correct_format = all("lora" in key for key in state_dict.keys()) - if not is_correct_format: - raise ValueError("Invalid LoRA checkpoint.") - - self.state_dict[path] = state_dict - self.network_alphas[path] = network_alphas - - def get_dicts(self, - prefix='unet', - convert_to_diffusers=False, - ): - state_dict = dict() - network_alphas = dict() - - for path in self.paths: - keys = list(self.state_dict[path].keys()) - if all(key.startswith(('unet', 'text_encoder')) for key in keys): - keys = [k for k in keys if k.startswith(prefix)] - if keys: - print(f"Processing {prefix} LoRA: {path}") - state_dict[path] = {k.replace(f"{prefix}.", ""): v for k, v in self.state_dict[path].items() if k in keys} - - network_alphas[path] = None - if path in self.network_alphas and self.network_alphas[path] is not None: - alpha_keys = [k for k in self.network_alphas[path].keys() if k.startswith(prefix)] - network_alphas[path] = { - k.replace(f"{prefix}.", ""): v for k, v in self.network_alphas[path].items() if k in alpha_keys - } - - else: - # Otherwise, we're dealing with the old format. - warn_message = "You have saved the LoRA weights using the old format. To convert LoRA weights to the new format, first load them in a dictionary and then create a new dictionary as follows: `new_state_dict = {f'unet.{module_name}': params for module_name, params in old_state_dict.items()}`." - print(warn_message) - - return state_dict, network_alphas - + self.weights = weights + self.scale = scale class BaseModel(): def __init__(self, @@ -383,12 +347,11 @@ def __init__(self, self.embedding_dim = embedding_dim self.extra_output_names = [] - self.lora_dict = None self.do_constant_folding = True def get_pipeline(self): model_opts = {'variant': 'fp16', 'torch_dtype': torch.float16} if self.fp16 else {} - model_opts = {'variant': 'bf16', 'torch_dtype': torch.bfloat16} if self.bf16 else model_opts + model_opts = {'torch_dtype': torch.bfloat16} if self.bf16 else model_opts return DiffusionPipeline.from_pretrained( self.path, use_safetensors=self.hf_safetensor, @@ -434,6 +397,7 @@ def export_onnx( custom_model=None, enable_lora_merge=False, static_shape=False, + lora_loader=None ): onnx_opt_graph = None # Export optimized ONNX model (if missing) @@ -442,7 +406,8 @@ def export_onnx( print(f"[I] Exporting ONNX model: {onnx_path}") def export_onnx(model): if enable_lora_merge: - model = merge_loras(model, self.lora_dict, self.lora_alphas, self.lora_scales) + assert lora_loader is not None + model = merge_loras(model, lora_loader) inputs = self.get_sample_input(1, opt_image_height, opt_image_width, static_shape) torch.onnx.export(model, inputs, @@ -530,16 +495,16 @@ def optimize(self, onnx_graph, return_onnx=True, **kwargs): opt.cleanup() opt.info(self.name + ': cleanup') if kwargs.get('modify_fp8_graph', False): - opt.modify_fp8_graph() + is_fp16_io = kwargs.get('is_fp16_io', True) + opt.modify_fp8_graph(is_fp16_io=is_fp16_io) opt.info(self.name + ': modify fp8 graph') - else: - opt.fold_constants() - opt.info(self.name + ': fold constants') - opt.infer_shapes() - opt.info(self.name + ': shape inference') - if kwargs.get('fuse_mha_qkv_int8', False): - opt.fuse_mha_qkv_int8_sq() - opt.info(self.name + ': fuse QKV nodes') + opt.fold_constants() + opt.info(self.name + ': fold constants') + opt.infer_shapes() + opt.info(self.name + ': shape inference') + if kwargs.get('fuse_mha_qkv_int8', False): + opt.fuse_mha_qkv_int8_sq() + opt.info(self.name + ': fuse QKV nodes') onnx_opt_graph = opt.cleanup(return_onnx=return_onnx) opt.info(self.name + ': finished') return onnx_opt_graph @@ -584,8 +549,6 @@ def __init__(self, output_hidden_states=False, keep_pooled_output=False, subfolder="text_encoder", - lora_dict=None, - lora_alphas=None, ): super(CLIPModel, self).__init__(version, pipeline, device=device, hf_token=hf_token, verbose=verbose, framework_model_dir=framework_model_dir, fp16=fp16, tf32=tf32, bf16=bf16, max_batch_size=max_batch_size, embedding_dim=embedding_dim) self.subfolder = subfolder @@ -597,7 +560,7 @@ def __init__(self, self.extra_output_names = ['hidden_states'] def get_model(self, torch_inference=''): - model_opts = {'torch_dtype': torch.float16} if self.fp16 else {} + model_opts = {'torch_dtype': torch.float16} if self.fp16 else {'torch_dtype': torch.bfloat16} if self.bf16 else {} clip_model_dir = get_checkpoint_dir(self.framework_model_dir, self.version, self.pipeline, self.subfolder) if not os.path.exists(clip_model_dir): model = CLIPTextModel.from_pretrained(self.path, @@ -686,8 +649,6 @@ def __init__(self, max_batch_size=16, output_hidden_states=False, subfolder="text_encoder_2", - lora_dict=None, - lora_alphas=None, ): super(CLIPWithProjModel, self).__init__(version, pipeline, device=device, hf_token=hf_token, verbose=verbose, framework_model_dir=framework_model_dir, fp16=fp16, bf16=bf16, max_batch_size=max_batch_size, embedding_dim=get_clipwithproj_embedding_dim(version, pipeline), output_hidden_states=output_hidden_states) @@ -769,7 +730,7 @@ def __init__(self, self.config = AutoConfig.from_pretrained(self.path, subfolder=self.subfolder, token=self.hf_token) def get_model(self, torch_inference=''): - model_opts = {'torch_dtype': torch.float16} if self.fp16 else {} + model_opts = {'torch_dtype': torch.float16} if self.fp16 else {'torch_dtype': torch.bfloat16} if self.bf16 else {} t5_model_dir = get_checkpoint_dir(self.framework_model_dir, self.version, self.pipeline, self.subfolder) if not os.path.exists(t5_model_dir): model = T5EncoderModel.from_pretrained(self.path, @@ -1083,9 +1044,6 @@ def __init__(self, max_batch_size = 16, text_maxlen = 77, controlnets = None, - lora_scales = None, - lora_dict = None, - lora_alphas = None, do_classifier_free_guidance = False, ): @@ -1093,9 +1051,6 @@ def __init__(self, self.subfolder = 'unet' self.controlnets = get_path(version, pipeline, controlnets) if controlnets else None self.unet_dim = (9 if pipeline.is_inpaint() else 4) - self.lora_scales = lora_scales - self.lora_dict = lora_dict - self.lora_alphas = lora_alphas self.xB = 2 if do_classifier_free_guidance else 1 # batch multiplier def get_model(self, torch_inference=''): @@ -1241,18 +1196,12 @@ def __init__(self, fp8 = False, max_batch_size = 16, text_maxlen = 77, - lora_scales = None, - lora_dict = None, - lora_alphas = None, do_classifier_free_guidance = False, ): super(UNetXLModel, self).__init__(version, pipeline, device=device, hf_token=hf_token, verbose=verbose, framework_model_dir=framework_model_dir, fp16=fp16, int8=int8, fp8=fp8, max_batch_size=max_batch_size, text_maxlen=text_maxlen, embedding_dim=get_unet_embedding_dim(version, pipeline)) self.subfolder = 'unet' self.unet_dim = (9 if pipeline.is_inpaint() else 4) self.time_dim = (5 if pipeline.is_sd_xl_refiner() else 6) - self.lora_scales = lora_scales - self.lora_dict = lora_dict - self.lora_alphas = lora_alphas self.xB = 2 if do_classifier_free_guidance else 1 # batch multiplier def get_model(self, torch_inference=''): @@ -1498,7 +1447,7 @@ def get_shape_dict(self, batch_size, image_height, image_width): 'added_time_ids': (self.xB*batch_size, 3), } - def get_sample_input(self, batch_size, image_height, image_width): + def get_sample_input(self, batch_size, image_height, image_width, static_shape): # TODO chunk_size if forward_chunking is used latent_height, latent_width = self.check_dims(batch_size, image_height, image_width) @@ -1668,18 +1617,18 @@ def __init__(self, tf32=False, int8 = False, fp8 = False, + bf16 = False, max_batch_size = 16, text_maxlen = 77, build_strongly_typed=False ): - - super(FluxTransformerModel, self).__init__(version, pipeline, device=device, hf_token=hf_token, verbose=verbose, framework_model_dir=framework_model_dir, fp16=fp16, tf32=tf32, int8=int8, fp8=fp8, max_batch_size=max_batch_size, text_maxlen=text_maxlen) + super(FluxTransformerModel, self).__init__(version, pipeline, device=device, hf_token=hf_token, verbose=verbose, framework_model_dir=framework_model_dir, fp16=fp16, tf32=tf32, int8=int8, fp8=fp8, bf16=bf16, max_batch_size=max_batch_size, text_maxlen=text_maxlen) self.subfolder = 'transformer' self.config = FluxTransformer2DModel.load_config(self.path, subfolder=self.subfolder, token=self.hf_token) self.build_strongly_typed = build_strongly_typed def get_model(self, torch_inference=''): - model_opts = {'torch_dtype': torch.float16} if self.fp16 else {} + model_opts = {'torch_dtype': torch.float16} if self.fp16 else {'torch_dtype': torch.bfloat16} if self.bf16 else {} transformer_model_dir = get_checkpoint_dir(self.framework_model_dir, self.version, self.pipeline, self.subfolder) transformer_path = self.get_model_path(transformer_model_dir, model_opts) if not os.path.exists(transformer_path): @@ -1698,7 +1647,7 @@ def get_model(self, torch_inference=''): return model def get_input_names(self): - return ['hidden_states', 'encoder_hidden_states', 'pooled_projections', 'timestep', 'img_ids', 'txt_ids', 'guidance'] + return ['hidden_states', 'encoder_hidden_states', 'pooled_projections', 'timestep', 'img_ids', 'txt_ids', 'guidance'] def get_output_names(self): return ['latent'] @@ -1743,18 +1692,28 @@ def get_shape_dict(self, batch_size, image_height, image_width): def get_sample_input(self, batch_size, image_height, image_width, static_shape): latent_height, latent_width = self.check_dims(batch_size, image_height, image_width) - dtype = torch.float16 if self.fp16 else torch.float32 + dtype = torch.float32 + assert not (self.fp16 and self.bf16), "fp16 and bf16 cannot be enabled simultaneously" + tensor_dtype = torch.bfloat16 if self.bf16 else (torch.float16 if self.fp16 else torch.float32) + return ( - torch.randn(batch_size, (latent_height // 2) * (latent_width // 2), self.config['in_channels'], dtype=dtype, device=self.device), - torch.randn(batch_size, self.text_maxlen, self.config['joint_attention_dim'], dtype=dtype, device=self.device), - torch.randn(batch_size, self.config['pooled_projection_dim'], dtype=dtype, device=self.device), - torch.tensor([1.]*batch_size, dtype=dtype, device=self.device), + torch.randn(batch_size, (latent_height // 2) * (latent_width // 2), self.config['in_channels'], dtype=tensor_dtype, device=self.device), + torch.randn(batch_size, self.text_maxlen, self.config['joint_attention_dim'], dtype=tensor_dtype, device=self.device), + torch.randn(batch_size, self.config['pooled_projection_dim'], dtype=tensor_dtype, device=self.device), + torch.tensor([1.]*batch_size, dtype=tensor_dtype, device=self.device), torch.randn((latent_height // 2) * (latent_width // 2), 3, dtype=dtype, device=self.device), torch.randn(self.text_maxlen, 3, dtype=dtype, device=self.device), { 'guidance': torch.tensor([1.]*batch_size, dtype=dtype, device=self.device), } ) + def optimize(self, onnx_graph): + if self.fp8: + return super().optimize(onnx_graph, modify_fp8_graph=True, is_fp16_io=False) + if self.int8: + return super().optimize(onnx_graph, fuse_mha_qkv_int8=True) + return super().optimize(onnx_graph) + class VAEModel(BaseModel): def __init__(self, @@ -1766,23 +1725,26 @@ def __init__(self, framework_model_dir, fp16=False, tf32=False, + bf16=False, max_batch_size=16, ): - super(VAEModel, self).__init__(version, pipeline, device=device, hf_token=hf_token, verbose=verbose, framework_model_dir=framework_model_dir, fp16=fp16, tf32=tf32, max_batch_size=max_batch_size) + super(VAEModel, self).__init__(version, pipeline, device=device, hf_token=hf_token, verbose=verbose, framework_model_dir=framework_model_dir, fp16=fp16, tf32=tf32, bf16=bf16, max_batch_size=max_batch_size) self.subfolder = 'vae' self.config = AutoencoderKL.load_config(self.path, subfolder=self.subfolder, token=self.hf_token) def get_model(self, torch_inference=''): + model_opts = {'torch_dtype': torch.float16} if self.fp16 else {'torch_dtype': torch.bfloat16} if self.bf16 else {} vae_decoder_model_path = get_checkpoint_dir(self.framework_model_dir, self.version, self.pipeline, self.subfolder) if not os.path.exists(vae_decoder_model_path): model = AutoencoderKL.from_pretrained(self.path, subfolder=self.subfolder, use_safetensors=self.hf_safetensor, - token=self.hf_token).to(self.device) - model.save_pretrained(vae_decoder_model_path) + token=self.hf_token, + **model_opts).to(self.device) + model.save_pretrained(vae_decoder_model_path, **model_opts) else: print(f"[I] Load AutoencoderKL (decoder) model from: {vae_decoder_model_path}") - model = AutoencoderKL.from_pretrained(vae_decoder_model_path).to(self.device) + model = AutoencoderKL.from_pretrained(vae_decoder_model_path, **model_opts).to(self.device) model.forward = model.decode model = optimize_checkpoint(model, torch_inference) return model @@ -1819,7 +1781,8 @@ def get_shape_dict(self, batch_size, image_height, image_width): def get_sample_input(self, batch_size, image_height, image_width, static_shape): latent_height, latent_width = self.check_dims(batch_size, image_height, image_width) - return torch.randn(batch_size, self.config['latent_channels'], latent_height, latent_width, dtype=torch.float32, device=self.device) + dtype = torch.float16 if self.fp16 else torch.bfloat16 if self.bf16 else torch.float32 + return torch.randn(batch_size, self.config['latent_channels'], latent_height, latent_width, dtype=dtype, device=self.device) class SD3_VAEDecoderModel(BaseModel): def __init__(self, diff --git a/demo/Diffusion/requirements.txt b/demo/Diffusion/requirements.txt index 4e494185..2316b878 100755 --- a/demo/Diffusion/requirements.txt +++ b/demo/Diffusion/requirements.txt @@ -7,13 +7,14 @@ git+https://github.com/huggingface/diffusers.git # Install from source for the l ftfy matplotlib nvtx -onnx==1.15.0 -onnxruntime==1.17.3 +onnx==1.17.0 +onnxruntime==1.19.2 opencv-python==4.8.0.74 scipy transformers==4.42.2 --extra-index-url https://pypi.nvidia.com -nvidia-modelopt[torch,onnx]==0.15.1 +nvidia-modelopt[torch,onnx]==0.19.0 onnx-graphsurgeon +peft==0.13.0 polygraphy==0.49.9 sentencepiece diff --git a/demo/Diffusion/stable_diffusion_pipeline.py b/demo/Diffusion/stable_diffusion_pipeline.py index 332b5572..cc64e70e 100644 --- a/demo/Diffusion/stable_diffusion_pipeline.py +++ b/demo/Diffusion/stable_diffusion_pipeline.py @@ -32,7 +32,6 @@ import inspect from models import ( get_clip_embedding_dim, - get_path, LoraLoader, make_tokenizer, CLIPModel, @@ -108,7 +107,8 @@ def __init__( vae_scaling_factor=0.18215, framework_model_dir='pytorch_model', controlnets=None, - lora_scale: Optional[List[int]] = None, + lora_scale: float = 1.0, + lora_weight: Optional[List[float]] = None, lora_path: Optional[List[str]] = None, return_latents=False, torch_inference='', @@ -239,12 +239,12 @@ def __init__( # initialize lora loader and scales self.lora_loader = None - self.lora_scales = dict() + self.lora_weights = dict() if lora_path: - self.lora_loader = LoraLoader(lora_path) - assert len(lora_path) == len(lora_scale) + self.lora_loader = LoraLoader(lora_path, lora_weight, lora_scale) + assert len(lora_path) == len(lora_weight) for i, path in enumerate(lora_path): - self.lora_scales[path] = lora_scale[i] + self.lora_weights[path] = lora_weight[i] # initialized in loadResources() self.events = {} @@ -335,20 +335,11 @@ def initializeModels(self, framework_model_dir, int8, fp8): subfolder = 'text_encoder_2' self.models['clip2'] = CLIPWithProjModel(**models_args, fp16=True, output_hidden_states=self.config.get('clip_hidden_states', False), subfolder=subfolder) - lora_dict, lora_alphas = (None, None) if 'unet' in self.stages: - if self.lora_loader: - lora_dict, lora_alphas = self.lora_loader.get_dicts('unet') - assert len(lora_dict) == len(self.lora_scales) - self.models['unet'] = UNetModel(**models_args, fp16=True, int8=int8, fp8=fp8, controlnets=self.controlnets, - lora_scales=self.lora_scales, lora_dict=lora_dict, lora_alphas=lora_alphas, do_classifier_free_guidance=self.do_classifier_free_guidance) + self.models['unet'] = UNetModel(**models_args, fp16=True, int8=int8, fp8=fp8, controlnets=self.controlnets, do_classifier_free_guidance=self.do_classifier_free_guidance) if 'unetxl' in self.stages: - if not self.pipeline_type.is_sd_xl_refiner() and self.lora_loader: - lora_dict, lora_alphas = self.lora_loader.get_dicts('unet') - assert len(lora_dict) == len(self.lora_scales) - self.models['unetxl'] = UNetXLModel(**models_args, fp16=True, int8=int8, fp8=fp8, - lora_scales=self.lora_scales, lora_dict=lora_dict, lora_alphas=lora_alphas, do_classifier_free_guidance=self.do_classifier_free_guidance) + self.models['unetxl'] = UNetXLModel(**models_args, fp16=True, int8=int8, fp8=fp8, do_classifier_free_guidance=self.do_classifier_free_guidance) vae_fp16 = not self.pipeline_type.is_sd_xl() @@ -441,7 +432,7 @@ def loadEngines( # Configure pipeline models to load model_names = self.models.keys() - lora_suffix = '-'+'-'.join([str(md5(path.encode('utf-8')).hexdigest())+'-'+('%.2f' % self.lora_scales[path]) for path in sorted(self.lora_loader.paths)]) if self.lora_loader else '' + lora_suffix = '-'+'-'.join([str(md5(path.encode('utf-8')).hexdigest())+'-'+('%.2f' % self.lora_weights[path])+'-'+('%.2f' % self.lora_loader.scale) for path in sorted(self.lora_loader.paths)]) if self.lora_loader else '' # Enable refit and LoRA merging only for UNet & UNetXL for now do_engine_refit = dict(zip(model_names, [not self.pipeline_type.is_sd_xl_refiner() and enable_refit and model_name.startswith('unet') for model_name in model_names])) do_lora_merge = dict(zip(model_names, [not enable_refit and self.lora_loader and model_name.startswith('unet') for model_name in model_names])) @@ -474,7 +465,7 @@ def loadEngines( if do_export_onnx or do_export_weights_map: # Non-quantized ONNX export if not use_int8[model_name] and not use_fp8[model_name]: - obj.export_onnx(onnx_path[model_name], onnx_opt_path[model_name], onnx_opset, opt_image_height, opt_image_width, enable_lora_merge=do_lora_merge[model_name], static_shape=static_shape) + obj.export_onnx(onnx_path[model_name], onnx_opt_path[model_name], onnx_opset, opt_image_height, opt_image_width, enable_lora_merge=do_lora_merge[model_name], static_shape=static_shape, lora_loader=self.lora_loader) else: pipeline = obj.get_pipeline() model = pipeline.unet @@ -576,7 +567,7 @@ def forward_loop(model): if torch_fallback[model_name]: continue self.engine[model_name].load() - if do_engine_refit[model_name] and obj.lora_dict: + if do_engine_refit[model_name] and self.lora_loader: assert weights_map_path[model_name] with open(weights_map_path[model_name], 'r') as fp_wts: print(f"[I] Loading weights map: {weights_map_path[model_name]} ") @@ -584,14 +575,14 @@ def forward_loop(model): refit_weights_path = self.getRefitNodesPath(model_name, engine_dir, suffix=lora_suffix) if not os.path.exists(refit_weights_path): print(f"[I] Saving refit weights: {refit_weights_path}") - model = merge_loras(obj.get_model(), obj.lora_dict, obj.lora_alphas, obj.lora_scales) - refit_weights = get_refit_weights(model.state_dict(), onnx_opt_path[model_name], weights_name_mapping, weights_shape_mapping) - torch.save(refit_weights, refit_weights_path) + model = merge_loras(obj.get_model(), self.lora_loader) + refit_weights, updated_weight_names = get_refit_weights(model.state_dict(), onnx_opt_path[model_name], weights_name_mapping, weights_shape_mapping) + torch.save((refit_weights, updated_weight_names), refit_weights_path) unload_model(model) else: print(f"[I] Loading refit weights: {refit_weights_path}") - refit_weights = torch.load(refit_weights_path) - self.engine[model_name].refit(refit_weights, obj.fp16) + refit_weights, updated_weight_names = torch.load(refit_weights_path) + self.engine[model_name].refit(refit_weights, updated_weight_names) # Load torch models for model_name, obj in self.models.items(): @@ -837,6 +828,9 @@ def encode_image(self, input_image): def decode_latent(self, latents): self.profile_start('vae', color='red') + cast_to = torch.float16 if self.models['vae'].fp16 else torch.bfloat16 if self.models['vae'].bf16 else torch.float32 + latents = latents.to(dtype=cast_to) + if self.torch_inference: images = self.torch_models['vae'](latents, return_dict=False)[0] else: diff --git a/demo/Diffusion/stable_video_diffusion_pipeline.py b/demo/Diffusion/stable_video_diffusion_pipeline.py index e2370539..c9af7247 100644 --- a/demo/Diffusion/stable_video_diffusion_pipeline.py +++ b/demo/Diffusion/stable_video_diffusion_pipeline.py @@ -41,7 +41,19 @@ _append_dims, _resize_with_antialiasing, tensor2vid, + load_calibration_images, ) +import modelopt.torch.opt as mto +import modelopt.torch.quantization as mtq +from utils_modelopt import ( + filter_func, + quantize_lvl, + check_lora, + set_fmha, + generate_fp8_scales, + SD_FP8_FP16_DEFAULT_CONFIG, +) + from stable_diffusion_pipeline import StableDiffusionPipeline class StableVideoDiffusionPipeline(StableDiffusionPipeline): @@ -151,6 +163,10 @@ def loadEngines( enable_refit=False, enable_all_tactics=False, timing_cache=None, + fp8=False, + quantization_level=0.0, + calibration_size=32, + calib_batch_size=2 ): """ Build and load engines for TensorRT accelerated inference. @@ -181,6 +197,14 @@ def loadEngines( Enable all tactic sources during TensorRT engine builds. timing_cache (str): Path to the timing cache to speed up TensorRT build. + fp8 (bool): + Whether to quantize to fp8 format or not. + quantization_level (float): + Controls which layers to quantize. + calibration_size (int): + The number of steps to use for calibrating the model for quantization. + calib_batch_size (int): + The batch size to use for calibration. Defaults to 2. """ # Create directories if missing for directory in [engine_dir, onnx_dir]: @@ -210,13 +234,83 @@ def loadEngines( engine_path = dict(zip(model_names, [self.getEnginePath(model_name, engine_dir) for model_name in model_names])) do_engine_refit = dict(zip(model_names, [enable_refit and model_name.startswith('unet') for model_name in model_names])) + # Quantization. + model_suffix = dict(zip(model_names, ['' for model_name in model_names])) + use_fp8 = dict.fromkeys(model_names, False) + if fp8: + model_name = "unet-temp" + use_fp8[model_name] = True + model_suffix[model_name] += f"-fp8.l{quantization_level}.bs2.s{self.denoising_steps}.c{calibration_size}" + onnx_path = { model_name : self.getOnnxPath(model_name, onnx_dir, opt=False, suffix=model_suffix[model_name]) for model_name in model_names } + onnx_opt_path = { model_name : self.getOnnxPath(model_name, onnx_dir, suffix=model_suffix[model_name]) for model_name in model_names } + engine_path = { model_name : self.getEnginePath(model_name, engine_dir, do_engine_refit[model_name], suffix=model_suffix[model_name]) for model_name in model_names } + weights_map_path = { model_name : (self.getWeightsMapPath(model_name, onnx_dir) if do_engine_refit[model_name] else None) for model_name in model_names } + + # Export models to ONNX for model_name, obj in self.models.items(): if self.torch_fallback[model_name]: continue do_export_onnx = not os.path.exists(engine_path[model_name]) and not os.path.exists(onnx_opt_path[model_name]) - if do_export_onnx: - obj.export_onnx(onnx_path[model_name], onnx_opt_path[model_name], onnx_opset, opt_image_height, opt_image_width) + do_export_weights_map = weights_map_path[model_name] and not os.path.exists(weights_map_path[model_name]) + if do_export_onnx or do_export_weights_map: + if use_fp8[model_name]: + pipeline = obj.get_pipeline() + model = pipeline.unet + + state_dict_path = self.getStateDictPath(model_name, onnx_dir, suffix=model_suffix[model_name]) + if not os.path.exists(state_dict_path): + # Load calibration images + print(f"[I] Calibrated weights not found, generating {state_dict_path}") + calibration_image_folder = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'calibration-images') + calibration_image_list = load_calibration_images(calibration_image_folder) + print("Number of images loaded:", len(calibration_image_list)) + + # TODO check size > calibration_size + def do_calibrate(pipeline, calibration_images, **kwargs): + for i_th, image in enumerate(calibration_images): + if i_th >= kwargs["calib_size"]: + return + pipeline( + image=image, + num_inference_steps=kwargs["n_steps"], + ).frames[0] + + def forward_loop(model): + pipeline.unet = model + do_calibrate( + pipeline=pipeline, + calibration_images=calibration_image_list, + calib_size=calibration_size // calib_batch_size, + n_steps=self.denoising_steps, + ) + + print(f"[I] Performing calibration for {calibration_size} steps.") + if use_fp8[model_name]: + quant_config = SD_FP8_FP16_DEFAULT_CONFIG + check_lora(model) + mtq.quantize(model, quant_config, forward_loop) + mto.save(model, state_dict_path) + else: + mto.restore(model, state_dict_path) + + print(f"[I] Generating quantized ONNX model: {onnx_opt_path[model_name]}") + if not os.path.exists(onnx_path[model_name]): + """ + Error: Torch bug, ONNX export failed due to unknown kernel shape in QuantConv3d. + TRT_FP8QuantizeLinear and TRT_FP8DequantizeLinear operations in UNetSpatioTemporalConditionModel for svd + cause issues. Inputs on different devices (CUDA vs CPU) may contribute to the problem. + """ + quantize_lvl(model, quantization_level, enable_conv_3d=False) + mtq.disable_quantizer(model, filter_func) + if use_fp8[model_name]: + generate_fp8_scales(model) + else: + model = None + + obj.export_onnx(onnx_path[model_name], onnx_opt_path[model_name], onnx_opset, opt_image_height, opt_image_width, custom_model=model, static_shape=static_shape) + else: + obj.export_onnx(onnx_path[model_name], onnx_opt_path[model_name], onnx_opset, opt_image_height, opt_image_width) # Build TensorRT engines for model_name, obj in self.models.items(): diff --git a/demo/Diffusion/utilities.py b/demo/Diffusion/utilities.py index 92c9c089..93277153 100755 --- a/demo/Diffusion/utilities.py +++ b/demo/Diffusion/utilities.py @@ -21,6 +21,7 @@ from collections import OrderedDict from cuda import cudart from diffusers.models.lora import LoRACompatibleConv, LoRACompatibleLinear +from diffusers.utils import load_image from enum import Enum, auto import gc from io import BytesIO @@ -45,6 +46,7 @@ import tensorrt as trt import torch import types +import gc TRT_LOGGER = trt.Logger(trt.Logger.ERROR) @@ -141,12 +143,18 @@ def lora_forward(self, x, scale=None): new_linear._torch_forward = new_linear.forward new_linear.forward = types.MethodType(lora_forward, new_linear) -def merge_loras(model, lora_dict, lora_alphas, lora_scales): - assert len(lora_scales) == len(lora_dict) - for path, lora in lora_dict.items(): - print(f"[I] Fusing LoRA: {path}, scale {lora_scales[path]}") - model.load_attn_procs(lora, network_alphas=lora_alphas[path]) - model.fuse_lora(lora_scale=lora_scales[path]) +def merge_loras(model, lora_loader): + paths, weights, scale = lora_loader.paths, lora_loader.weights, lora_loader.scale + for i, path in enumerate(paths): + print(f"[I] Loading LoRA: {path}, weight {weights[i]}") + state_dict, network_alphas = lora_loader.lora_state_dict(path, unet_config=model.config) + lora_loader.load_lora_into_unet(state_dict, network_alphas=network_alphas, + unet=model, adapter_name=path) + + model.set_adapters(paths, weights=weights) + # NOTE: fuse_lora an experimental API in Diffusers + model.fuse_lora(adapter_names=paths, lora_scale=scale) + model.unload_lora() return model def CUASSERT(cuda_ret): @@ -219,21 +227,15 @@ def __del__(self): del self.buffers del self.tensors - def refit(self, refit_weights, is_fp16): + def refit(self, refit_weights, updated_weight_names): # Initialize refitter refitter = trt.Refitter(self.engine, TRT_LOGGER) - refitted_weights = set() - # iterate through all tensorrt refittable weights - for trt_weight_name in refitter.get_all_weights(): - if trt_weight_name not in refit_weights: - continue + def refit_single_weight(trt_weight_name): # get weight from state dict - trt_datatype = trt.DataType.FLOAT - if is_fp16: - refit_weights[trt_weight_name] = refit_weights[trt_weight_name].half() - trt_datatype = trt.DataType.HALF + trt_datatype = refitter.get_weights_prototype(trt_weight_name).dtype + refit_weights[trt_weight_name] = refit_weights[trt_weight_name].to(trt_to_torch_dtype_dict[trt_datatype]) # trt.Weight and trt.TensorLocation trt_wt_tensor = trt.Weights(trt_datatype, refit_weights[trt_weight_name].data_ptr(), torch.numel(refit_weights[trt_weight_name])) @@ -243,7 +245,17 @@ def refit(self, refit_weights, is_fp16): refitter.set_named_weights(trt_weight_name, trt_wt_tensor, trt_wt_location) refitted_weights.add(trt_weight_name) - assert set(refitted_weights) == set(refit_weights.keys()) + # iterate through all tensorrt refittable weights + for trt_weight_name in refitter.get_all_weights(): + if trt_weight_name not in updated_weight_names: + continue + + refit_single_weight(trt_weight_name) + + # iterate through missing weights required by tensorrt - addresses the case where lora_scale=0 + for trt_weight_name in refitter.get_missing_weights(): + refit_single_weight(trt_weight_name) + if not refitter.refit_cuda_engine(): print("Error: failed to refit new weights.") exit(0) @@ -306,8 +318,24 @@ def build(self, save_engine(engine, path=self.engine_path) def load(self): - print(f"Loading TensorRT engine: {self.engine_path}") - self.engine = engine_from_bytes(bytes_from_path(self.engine_path)) + if self.engine is not None: + print(f"[W]: Engine {self.engine_path} already loaded, skip reloading") + return + if not hasattr(self,'engine_bytes_cpu') or self.engine_bytes_cpu is None: + # keep a cpu copy of the engine to reduce reloading time. + print(f"Loading TensorRT engine to cpu bytes: {self.engine_path}") + self.engine_bytes_cpu = bytes_from_path(self.engine_path) + print(f"Loading TensorRT engine from bytes: {self.engine_path}") + self.engine = engine_from_bytes(self.engine_bytes_cpu) + + def unload(self): + if self.engine is not None: + print(f"Unloading TensorRT engine: {self.engine_path}") + del self.engine + self.engine = None + gc.collect() + else: + print(f"[W]: Unload an unloaded engine {self.engine_path}, skip unloading") def activate(self, device_memory=None): if device_memory: @@ -559,6 +587,7 @@ def get_refit_weights(state_dict, onnx_opt_path, weight_name_mapping, weight_sha initializer_hash_mapping[initializer.name] = initializer_hash refit_weights = OrderedDict() + updated_weight_names = set() # save names of updated weights to refit only the required weights for wt_name, wt in state_dict.items(): # query initializer to compare initializer_name = weight_name_mapping[wt_name] @@ -574,14 +603,28 @@ def get_refit_weights(state_dict, onnx_opt_path, weight_name_mapping, weight_sha # include weight if hashes differ wt_hash = hash(wt.cpu().detach().numpy().astype(np.float16).data.tobytes()) if initializer_hash != wt_hash: - refit_weights[initializer_name] = wt.contiguous() - return refit_weights + updated_weight_names.add(initializer_name) + # Store all weights as the refitter may require unchanged weights too + # docs: https://docs.nvidia.com/deeplearning/tensorrt/developer-guide/index.html#refitting-engine-c + refit_weights[initializer_name] = wt.contiguous() + return refit_weights, updated_weight_names def load_calib_prompts(batch_size, calib_data_path): with open(calib_data_path, "r") as file: lst = [line.rstrip("\n") for line in file] return [lst[i : i + batch_size] for i in range(0, len(lst), batch_size)] +def load_calibration_images(folder_path): + images = [] + for filename in os.listdir(folder_path): + img_path = os.path.join(folder_path, filename) + if os.path.isfile(img_path): + image = load_image(img_path) + if image is not None: + images.append(image) + return images + + class PercentileAmaxes: def __init__(self, total_step, percentile) -> None: self.data = {} @@ -609,7 +652,8 @@ def add_arguments(parser): parser.add_argument('--denoising-steps', type=int, default=30, help="Number of denoising steps") parser.add_argument('--scheduler', type=str, default=None, choices=("DDIM", "DDPM", "EulerA", "Euler", "LCM", "LMSD", "PNDM", "UniPC", "DDPMWuerstchen", "FlowMatchEuler"), help="Scheduler for diffusion process") parser.add_argument('--guidance-scale', type=float, default=7.5, help="Value of classifier-free guidance scale (must be greater than 1)") - parser.add_argument('--lora-scale', type=float, nargs='+', default=None, help="Scale of LoRA weights, default 1 (must between 0 and 1)") + parser.add_argument('--lora-scale', type=float, default=1.0, help="Controls how much to influence the outputs with the LoRA parameters. (must between 0 and 1)") + parser.add_argument('--lora-weight', type=float, nargs='+', default=None, help="The LoRA adapter(s) weights to use with the UNet. (must between 0 and 1)") parser.add_argument('--lora-path', type=str, nargs='+', default=None, help="Path to LoRA adaptor. Ex: 'latent-consistency/lcm-lora-sdv1-5'") # ONNX export @@ -666,8 +710,8 @@ def process_pipeline_args(args): if args.int8 and not any(args.version.startswith(prefix) for prefix in ['xl', '1.4', '1.5', '2.1']): raise ValueError(f"int8 quantization is only supported for SDXL, SD1.4, SD1.5 and SD2.1 pipelines.") - if args.fp8 and not any(args.version.startswith(prefix) for prefix in ['xl', '1.4', '1.5', '2.1']): - raise ValueError(f"fp8 quantization is only supported for SDXL, SD1.4, SD1.5 and SD2.1 pipelines.") + if args.fp8 and not any(args.version.startswith(prefix) for prefix in ['xl', '1.4', '1.5', '2.1', 'flux.1-dev']): + raise ValueError(f"fp8 quantization is only supported for SDXL, SD1.4, SD1.5, SD2.1 and FLUX pipelines.") if args.fp8 and args.int8: raise ValueError(f"Cannot apply both int8 and fp8 quantization, please choose only one.") @@ -675,8 +719,8 @@ def process_pipeline_args(args): if args.fp8: device_info = torch.cuda.get_device_properties(0) version = device_info.major * 10 + device_info.minor - if version < 90: # if Ada or older - raise ValueError(f"Cannot apply FP8 quantization for GPU with compute capability {version / 10.0}. Only Hopper is supported.") + if version < 89: + raise ValueError(f"Cannot apply FP8 quantization for GPU with compute capability {version / 10.0}. Only Ada and Hopper are supported.") if args.quantization_level == 0.0: def override_quant_level(level : float, dtype_str : str): @@ -684,16 +728,19 @@ def override_quant_level(level : float, dtype_str : str): print(f"The default quantization level has been set to {level} for {dtype_str}.") if args.fp8: - override_quant_level(3.0 if args.version in ("1.4", "1.5") else 4.0, "FP8") + override_quant_level(3.0 if args.version in ("1.4", "1.5", "flux.1-dev") else 4.0, "FP8") elif args.int8: override_quant_level(3.0, "INT8") if args.lora_path and not any(args.version.startswith(prefix) for prefix in ('1.5', '2.1', 'xl')): raise ValueError(f"LoRA adapter support is only supported for SD1.5, SD2.1 and SDXL pipelines") - if args.lora_scale: - for lora_scale in (lora_scale for lora_scale in args.lora_scale if not 0 <= lora_scale <= 1): - raise ValueError(f"Scale of LoRA weights must be between 0 and 1, provided {lora_scale}") + if args.lora_weight: + for weight in (weight for weight in args.lora_weight if not 0 <= weight <= 1): + raise ValueError(f"LoRA adapter weights must be between 0 and 1, provided {weight}") + + if not 0 <= args.lora_scale <= 1: + raise ValueError(f"LoRA scale value must be between 0 and 1, provided {args.lora_scale}") kwargs_init_pipeline = { 'version': args.version, @@ -707,6 +754,7 @@ def override_quant_level(level : float, dtype_str : str): 'nvtx_profile': args.nvtx_profile, 'use_cuda_graph': args.use_cuda_graph, 'lora_scale': args.lora_scale, + 'lora_weight': args.lora_weight, 'lora_path': args.lora_path, 'framework_model_dir': args.framework_model_dir, 'torch_inference': args.torch_inference, diff --git a/demo/Diffusion/utils_modelopt.py b/demo/Diffusion/utils_modelopt.py index 9e8755c5..e8b9d789 100644 --- a/demo/Diffusion/utils_modelopt.py +++ b/demo/Diffusion/utils_modelopt.py @@ -107,7 +107,13 @@ def filter_func(name): ) return pattern.match(name) is not None -def quantize_lvl(unet, quant_level=2.5, linear_only=False): +def filter_func_no_proj_out(name): + pattern = re.compile( + r".*(time_emb_proj|time_embedding|conv_in|conv_out|conv_shortcut|add_embedding|pos_embed|time_text_embed|context_embedder|norm_out).*" + ) + return pattern.match(name) is not None + +def quantize_lvl(unet, quant_level=2.5, linear_only=False, enable_conv_3d=True): """ We should disable the unwanted quantizer when exporting the onnx Because in the current modelopt setting, it will load the quantizer amax for all the layers even @@ -132,6 +138,14 @@ def quantize_lvl(unet, quant_level=2.5, linear_only=False): else: module.input_quantizer.disable() module.weight_quantizer.disable() + elif isinstance(module, torch.nn.Conv3d) and not enable_conv_3d: + """ + Error: Torch bug, ONNX export failed due to unknown kernel shape in QuantConv3d. + TRT_FP8QuantizeLinear and TRT_FP8DequantizeLinear operations in UNetSpatioTemporalConditionModel for svd + cause issues. Inputs on different devices (CUDA vs CPU) may contribute to the problem. + """ + module.input_quantizer.disable() + module.weight_quantizer.disable() elif isinstance(module, Attention): # TRT only supports FP8 MHA with head_size % 16 == 0. head_size = int(module.inner_dim / module.heads) @@ -215,6 +229,25 @@ def get_int8_config( "algorithm": "max", } +SD_FP8_BF16_DEFAULT_CONFIG = { + "quant_cfg": { + "*weight_quantizer": {"num_bits": (4, 3), "axis": None, "trt_high_precision_dtype": "BFloat16"}, + "*input_quantizer": {"num_bits": (4, 3), "axis": None, "trt_high_precision_dtype": "BFloat16"}, + "*output_quantizer": {"enable": False}, + "*q_bmm_quantizer": {"num_bits": (4, 3), "axis": None, "trt_high_precision_dtype": "BFloat16"}, + "*k_bmm_quantizer": {"num_bits": (4, 3), "axis": None, "trt_high_precision_dtype": "BFloat16"}, + "*v_bmm_quantizer": {"num_bits": (4, 3), "axis": None, "trt_high_precision_dtype": "BFloat16"}, + "*softmax_quantizer": { + "num_bits": (4, 3), + "axis": None, + "trt_high_precision_dtype": "BFloat16", + }, + "default": {"enable": False}, + }, + "algorithm": "max", +} + + SD_FP8_FP32_DEFAULT_CONFIG = { "quant_cfg": { "*weight_quantizer": {"num_bits": (4, 3), "axis": None, "trt_high_precision_dtype": "Float"}, @@ -465,6 +498,11 @@ def cast_fp8_mha_io(graph): insert_cast(graph, input_tensor=bmm2_node.inputs[1], attrs={"to": np.float32}) insert_cast(graph, input_tensor=bmm2_node.outputs[0], attrs={"to": np.float16}) +def set_quant_precision(quant_config, precision: str = "Half"): + for key in quant_config["quant_cfg"]: + if "trt_high_precision_dtype" in quant_config["quant_cfg"][key]: + quant_config["quant_cfg"][key]["trt_high_precision_dtype"] = precision + def convert_fp16_io(graph): """ Convert graph I/O to FP16. diff --git a/docker/rockylinux8.Dockerfile b/docker/rockylinux8.Dockerfile index ffe26034..70c5a0a6 100644 --- a/docker/rockylinux8.Dockerfile +++ b/docker/rockylinux8.Dockerfile @@ -25,7 +25,7 @@ ENV NV_CUDNN_VERSION 8.9.6.50-1 ENV NV_CUDNN_PACKAGE libcudnn8-${NV_CUDNN_VERSION}.cuda12.2 ENV NV_CUDNN_PACKAGE_DEV libcudnn8-devel-${NV_CUDNN_VERSION}.cuda12.2 -ENV TRT_VERSION 10.5.0.18 +ENV TRT_VERSION 10.6.0.26 SHELL ["/bin/bash", "-c"] RUN dnf install -y \ @@ -62,15 +62,15 @@ RUN dnf install -y python38 python38-devel &&\ # Install TensorRT RUN if [ "${CUDA_VERSION:0:2}" = "11" ]; then \ - wget https://developer.nvidia.com/downloads/compute/machine-learning/tensorrt/10.5.0/tars/TensorRT-10.5.0.18.Linux.x86_64-gnu.cuda-11.8.tar.gz \ - && tar -xf TensorRT-10.5.0.18.Linux.x86_64-gnu.cuda-11.8.tar.gz \ - && cp -a TensorRT-10.5.0.18/lib/*.so* /usr/lib64 \ - && pip install TensorRT-10.5.0.18/python/tensorrt-10.5.0-cp38-none-linux_x86_64.whl ;\ + wget https://developer.nvidia.com/downloads/compute/machine-learning/tensorrt/10.6.0/tars/TensorRT-10.6.0.26.Linux.x86_64-gnu.cuda-11.8.tar.gz \ + && tar -xf TensorRT-10.6.0.26.Linux.x86_64-gnu.cuda-11.8.tar.gz \ + && cp -a TensorRT-10.6.0.26/lib/*.so* /usr/lib64 \ + && pip install TensorRT-10.6.0.26/python/tensorrt-10.6.0-cp38-none-linux_x86_64.whl ;\ elif [ "${CUDA_VERSION:0:2}" = "12" ]; then \ - wget https://developer.nvidia.com/downloads/compute/machine-learning/tensorrt/10.5.0/tars/TensorRT-10.5.0.18.Linux.x86_64-gnu.cuda-12.6.tar.gz \ - && tar -xf TensorRT-10.5.0.18.Linux.x86_64-gnu.cuda-12.6.tar.gz \ - && cp -a TensorRT-10.5.0.18/lib/*.so* /usr/lib64 \ - && pip install TensorRT-10.5.0.18/python/tensorrt-10.5.0-cp38-none-linux_x86_64.whl ;\ + wget https://developer.nvidia.com/downloads/compute/machine-learning/tensorrt/10.6.0/tars/TensorRT-10.6.0.26.Linux.x86_64-gnu.cuda-12.6.tar.gz \ + && tar -xf TensorRT-10.6.0.26.Linux.x86_64-gnu.cuda-12.6.tar.gz \ + && cp -a TensorRT-10.6.0.26/lib/*.so* /usr/lib64 \ + && pip install TensorRT-10.6.0.26/python/tensorrt-10.6.0-cp38-none-linux_x86_64.whl ;\ else \ echo "Invalid CUDA_VERSION"; \ exit 1; \ diff --git a/docker/rockylinux9.Dockerfile b/docker/rockylinux9.Dockerfile index bed779a7..70994b92 100644 --- a/docker/rockylinux9.Dockerfile +++ b/docker/rockylinux9.Dockerfile @@ -25,7 +25,7 @@ ENV NV_CUDNN_VERSION 8.9.6.50-1 ENV NV_CUDNN_PACKAGE libcudnn8-${NV_CUDNN_VERSION}.cuda12.2 ENV NV_CUDNN_PACKAGE_DEV libcudnn8-devel-${NV_CUDNN_VERSION}.cuda12.2 -ENV TRT_VERSION 10.5.0.18 +ENV TRT_VERSION 10.6.0.26 SHELL ["/bin/bash", "-c"] RUN dnf install -y \ @@ -67,15 +67,15 @@ RUN dnf -y install \ # Install TensorRT RUN if [ "${CUDA_VERSION:0:2}" = "11" ]; then \ - wget https://developer.nvidia.com/downloads/compute/machine-learning/tensorrt/10.5.0/tars/TensorRT-10.5.0.18.Linux.x86_64-gnu.cuda-11.8.tar.gz \ - && tar -xf TensorRT-10.5.0.18.Linux.x86_64-gnu.cuda-11.8.tar.gz \ - && cp -a TensorRT-10.5.0.18/lib/*.so* /usr/lib64 \ - && pip install TensorRT-10.5.0.18/python/tensorrt-10.5.0-cp39-none-linux_x86_64.whl ;\ + wget https://developer.nvidia.com/downloads/compute/machine-learning/tensorrt/10.6.0/tars/TensorRT-10.6.0.26.Linux.x86_64-gnu.cuda-11.8.tar.gz \ + && tar -xf TensorRT-10.6.0.26.Linux.x86_64-gnu.cuda-11.8.tar.gz \ + && cp -a TensorRT-10.6.0.26/lib/*.so* /usr/lib64 \ + && pip install TensorRT-10.6.0.26/python/tensorrt-10.6.0-cp39-none-linux_x86_64.whl ;\ elif [ "${CUDA_VERSION:0:2}" = "12" ]; then \ - wget https://developer.nvidia.com/downloads/compute/machine-learning/tensorrt/10.5.0/tars/TensorRT-10.5.0.18.Linux.x86_64-gnu.cuda-12.6.tar.gz \ - && tar -xf TensorRT-10.5.0.18.Linux.x86_64-gnu.cuda-12.6.tar.gz \ - && cp -a TensorRT-10.5.0.18/lib/*.so* /usr/lib64 \ - && pip install TensorRT-10.5.0.18/python/tensorrt-10.5.0-cp39-none-linux_x86_64.whl ;\ + wget https://developer.nvidia.com/downloads/compute/machine-learning/tensorrt/10.6.0/tars/TensorRT-10.6.0.26.Linux.x86_64-gnu.cuda-12.6.tar.gz \ + && tar -xf TensorRT-10.6.0.26.Linux.x86_64-gnu.cuda-12.6.tar.gz \ + && cp -a TensorRT-10.6.0.26/lib/*.so* /usr/lib64 \ + && pip install TensorRT-10.6.0.26/python/tensorrt-10.6.0-cp39-none-linux_x86_64.whl ;\ else \ echo "Invalid CUDA_VERSION"; \ exit 1; \ diff --git a/docker/ubuntu-20.04.Dockerfile b/docker/ubuntu-20.04.Dockerfile index a2ebb605..939eb89d 100644 --- a/docker/ubuntu-20.04.Dockerfile +++ b/docker/ubuntu-20.04.Dockerfile @@ -28,7 +28,7 @@ ENV CUDA_VERSION_MAJOR_MINOR=12.2 ENV NV_CUDNN_PACKAGE "libcudnn8=$NV_CUDNN_VERSION-1+cuda${CUDA_VERSION_MAJOR_MINOR}" ENV NV_CUDNN_PACKAGE_DEV "libcudnn8-dev=$NV_CUDNN_VERSION-1+cuda${CUDA_VERSION_MAJOR_MINOR}" -ENV TRT_VERSION 10.5.0.18 +ENV TRT_VERSION 10.6.0.26 SHELL ["/bin/bash", "-c"] RUN apt-get update && apt-get install -y --no-install-recommends \ @@ -84,15 +84,15 @@ RUN apt-get install -y --no-install-recommends \ # Install TensorRT RUN if [ "${CUDA_VERSION:0:2}" = "11" ]; then \ - wget https://developer.nvidia.com/downloads/compute/machine-learning/tensorrt/10.5.0/tars/TensorRT-10.5.0.18.Linux.x86_64-gnu.cuda-11.8.tar.gz \ - && tar -xf TensorRT-10.5.0.18.Linux.x86_64-gnu.cuda-11.8.tar.gz \ - && cp -a TensorRT-10.5.0.18/lib/*.so* /usr/lib/x86_64-linux-gnu \ - && pip install TensorRT-10.5.0.18/python/tensorrt-10.5.0-cp38-none-linux_x86_64.whl ;\ + wget https://developer.nvidia.com/downloads/compute/machine-learning/tensorrt/10.6.0/tars/TensorRT-10.6.0.26.Linux.x86_64-gnu.cuda-11.8.tar.gz \ + && tar -xf TensorRT-10.6.0.26.Linux.x86_64-gnu.cuda-11.8.tar.gz \ + && cp -a TensorRT-10.6.0.26/lib/*.so* /usr/lib/x86_64-linux-gnu \ + && pip install TensorRT-10.6.0.26/python/tensorrt-10.6.0-cp38-none-linux_x86_64.whl ;\ elif [ "${CUDA_VERSION:0:2}" = "12" ]; then \ - wget https://developer.nvidia.com/downloads/compute/machine-learning/tensorrt/10.5.0/tars/TensorRT-10.5.0.18.Linux.x86_64-gnu.cuda-12.6.tar.gz \ - && tar -xf TensorRT-10.5.0.18.Linux.x86_64-gnu.cuda-12.6.tar.gz \ - && cp -a TensorRT-10.5.0.18/lib/*.so* /usr/lib/x86_64-linux-gnu \ - && pip install TensorRT-10.5.0.18/python/tensorrt-10.5.0-cp38-none-linux_x86_64.whl ;\ + wget https://developer.nvidia.com/downloads/compute/machine-learning/tensorrt/10.6.0/tars/TensorRT-10.6.0.26.Linux.x86_64-gnu.cuda-12.6.tar.gz \ + && tar -xf TensorRT-10.6.0.26.Linux.x86_64-gnu.cuda-12.6.tar.gz \ + && cp -a TensorRT-10.6.0.26/lib/*.so* /usr/lib/x86_64-linux-gnu \ + && pip install TensorRT-10.6.0.26/python/tensorrt-10.6.0-cp38-none-linux_x86_64.whl ;\ else \ echo "Invalid CUDA_VERSION"; \ exit 1; \ diff --git a/docker/ubuntu-22.04-aarch64.Dockerfile b/docker/ubuntu-22.04-aarch64.Dockerfile index e28f058c..bd28c2bf 100644 --- a/docker/ubuntu-22.04-aarch64.Dockerfile +++ b/docker/ubuntu-22.04-aarch64.Dockerfile @@ -20,7 +20,7 @@ ARG CUDA_VERSION=12.6.0 # Multi-arch container support available in non-cudnn containers. FROM nvidia/cuda:${CUDA_VERSION}-devel-ubuntu22.04 -ENV TRT_VERSION 10.5.0.18 +ENV TRT_VERSION 10.6.0.26 SHELL ["/bin/bash", "-c"] # Setup user account diff --git a/docker/ubuntu-22.04.Dockerfile b/docker/ubuntu-22.04.Dockerfile index 0bb09b2d..e72671ba 100644 --- a/docker/ubuntu-22.04.Dockerfile +++ b/docker/ubuntu-22.04.Dockerfile @@ -28,7 +28,7 @@ ENV CUDA_VERSION_MAJOR_MINOR=12.2 ENV NV_CUDNN_PACKAGE "libcudnn8=$NV_CUDNN_VERSION-1+cuda${CUDA_VERSION_MAJOR_MINOR}" ENV NV_CUDNN_PACKAGE_DEV "libcudnn8-dev=$NV_CUDNN_VERSION-1+cuda${CUDA_VERSION_MAJOR_MINOR}" -ENV TRT_VERSION 10.5.0.18 +ENV TRT_VERSION 10.6.0.26 SHELL ["/bin/bash", "-c"] RUN apt-get update && apt-get install -y --no-install-recommends \ @@ -84,15 +84,15 @@ RUN apt-get install -y --no-install-recommends \ # Install TensorRT RUN if [ "${CUDA_VERSION:0:2}" = "11" ]; then \ - wget https://developer.nvidia.com/downloads/compute/machine-learning/tensorrt/10.5.0/tars/TensorRT-10.5.0.18.Linux.x86_64-gnu.cuda-11.8.tar.gz \ - && tar -xf TensorRT-10.5.0.18.Linux.x86_64-gnu.cuda-11.8.tar.gz \ - && cp -a TensorRT-10.5.0.18/lib/*.so* /usr/lib/x86_64-linux-gnu \ - && pip install TensorRT-10.5.0.18/python/tensorrt-10.5.0-cp310-none-linux_x86_64.whl ;\ + wget https://developer.nvidia.com/downloads/compute/machine-learning/tensorrt/10.6.0/tars/TensorRT-10.6.0.26.Linux.x86_64-gnu.cuda-11.8.tar.gz \ + && tar -xf TensorRT-10.6.0.26.Linux.x86_64-gnu.cuda-11.8.tar.gz \ + && cp -a TensorRT-10.6.0.26/lib/*.so* /usr/lib/x86_64-linux-gnu \ + && pip install TensorRT-10.6.0.26/python/tensorrt-10.6.0-cp310-none-linux_x86_64.whl ;\ elif [ "${CUDA_VERSION:0:2}" = "12" ]; then \ - wget https://developer.nvidia.com/downloads/compute/machine-learning/tensorrt/10.5.0/tars/TensorRT-10.5.0.18.Linux.x86_64-gnu.cuda-12.6.tar.gz \ - && tar -xf TensorRT-10.5.0.18.Linux.x86_64-gnu.cuda-12.6.tar.gz \ - && cp -a TensorRT-10.5.0.18/lib/*.so* /usr/lib/x86_64-linux-gnu \ - && pip install TensorRT-10.5.0.18/python/tensorrt-10.5.0-cp310-none-linux_x86_64.whl ;\ + wget https://developer.nvidia.com/downloads/compute/machine-learning/tensorrt/10.6.0/tars/TensorRT-10.6.0.26.Linux.x86_64-gnu.cuda-12.6.tar.gz \ + && tar -xf TensorRT-10.6.0.26.Linux.x86_64-gnu.cuda-12.6.tar.gz \ + && cp -a TensorRT-10.6.0.26/lib/*.so* /usr/lib/x86_64-linux-gnu \ + && pip install TensorRT-10.6.0.26/python/tensorrt-10.6.0-cp310-none-linux_x86_64.whl ;\ else \ echo "Invalid CUDA_VERSION"; \ exit 1; \ diff --git a/docker/ubuntu-cross-aarch64.Dockerfile b/docker/ubuntu-cross-aarch64.Dockerfile index 3243b4a7..8e3c3845 100644 --- a/docker/ubuntu-cross-aarch64.Dockerfile +++ b/docker/ubuntu-cross-aarch64.Dockerfile @@ -21,7 +21,7 @@ ARG OS_VERSION=22.04 FROM nvidia/cuda:${CUDA_VERSION}-devel-ubuntu${OS_VERSION} LABEL maintainer="NVIDIA CORPORATION" -ENV TRT_VERSION 10.5.0.18 +ENV TRT_VERSION 10.6.0.26 ENV DEBIAN_FRONTEND=noninteractive ARG uid=1000 diff --git a/include/NvInfer.h b/include/NvInfer.h index e0231d4d..61d71ecc 100644 --- a/include/NvInfer.h +++ b/include/NvInfer.h @@ -8434,6 +8434,19 @@ enum class BuilderFlag : int32_t //! enabled. This flag cannot be used together with kREFIT or kREFIT_IDENTICAL. kREFIT_INDIVIDUAL = 23, + //! Disable floating-point optimizations: 0*x => 0, x-x => 0, or x/x => 1. These identities are + //! not true when x is a NaN or Inf, and thus might hide propagation or generation of NaNs. This flag is typically + //! used in combination with kSPARSE_WEIGHTS. + //! There are three valid sparsity configurations. + //! 1. Disable all sparsity. Both kSPARSE_WEIGHTS and kSTRICT_NANS are unset + //! 2. Enable sparsity only where it does not affect propagation/generation of NaNs. Both kSPARSE_WEIGHTS and + //! kSTRICT_NANS are set + //! 3. Enable all sparsity. kSPARSE_WEIGHTS is set and kSTRICT_NANS is unset + kSTRICT_NANS = 24, + + //! Enable memory monitor during build time. + kMONITOR_MEMORY = 25, + }; //! @@ -8444,7 +8457,7 @@ enum class BuilderFlag : int32_t template <> constexpr inline int32_t EnumMax() noexcept { - return 24; + return 26; } //! @@ -9024,9 +9037,9 @@ class IBuilderConfig : public INoCopy } //! - //! \brief Set the cuda stream that is used to profile this network. + //! \brief Set the CUDA stream that is used to profile this network. //! - //! \param stream The cuda stream used for profiling by the builder. + //! \param stream The CUDA stream used for profiling by the builder. //! //! \see getProfileStream() //! @@ -9036,9 +9049,9 @@ class IBuilderConfig : public INoCopy } //! - //! \brief Get the cuda stream that is used to profile this network. + //! \brief Get the CUDA stream that is used to profile this network. //! - //! \return The cuda stream set by setProfileStream, nullptr if setProfileStream has not been called. + //! \return The CUDA stream set by setProfileStream, nullptr if setProfileStream has not been called. //! //! \see setProfileStream() //! @@ -9838,7 +9851,7 @@ class IBuilder : public INoCopy //! //! \return A pointer to a IHostMemory object that contains a serialized network. //! - //! \note This function will synchronize the cuda stream returned by \p config.getProfileStream() before returning. + //! \note This function will synchronize the CUDA stream returned by \p config.getProfileStream() before returning. //! //! \see INetworkDefinition, IBuilderConfig, IHostMemory //! @@ -9847,6 +9860,26 @@ class IBuilder : public INoCopy return mImpl->buildSerializedNetwork(network, config); } + //! + //! \brief Builds a network for the given INetworkDefinition and IBuilderConfig. + //! + //! \param network Network definition. + //! \param config Builder configuration. + //! + //! \return A pointer to a ICudaEngine object that contains an engine. + //! + //! \note This function will synchronize the CUDA stream returned by \p config.getProfileStream() before returning. + //! + //! \note This function does not support \p BuilderFlag::kVERSION_COMPATIBLE. + //! Please use \p buildSerializedNetwork to get a version compatible engine. + //! + //! \see INetworkDefinition, IBuilderConfig, ICudaEngine + //! + nvinfer1::ICudaEngine* buildEngineWithConfig(INetworkDefinition& network, IBuilderConfig& config) noexcept + { + return mImpl->buildEngineWithConfig(network, config); + } + //! //! \brief Checks that a network is within the scope of the IBuilderConfig settings. //! @@ -9862,7 +9895,7 @@ class IBuilder : public INoCopy //! \return True if network is within the scope of the restrictions specified by the builder config, //! false otherwise. //! - //! \note This function will synchronize the cuda stream returned by \p config.getProfileStream() before returning. + //! \note This function will synchronize the CUDA stream returned by \p config.getProfileStream() before returning. //! bool isNetworkSupported(INetworkDefinition const& network, IBuilderConfig const& config) const noexcept { diff --git a/include/NvInferImpl.h b/include/NvInferImpl.h index 2c7df74a..3bb39fa4 100644 --- a/include/NvInferImpl.h +++ b/include/NvInferImpl.h @@ -26,6 +26,8 @@ namespace nvinfer1 { +class ILogger; + namespace v_1_0 { class IProgressMonitor; @@ -113,6 +115,12 @@ class IPluginV3; } // namespace v_1_0 using IPluginV3 = v_1_0::IPluginV3; +namespace v_1_0 +{ +class IStreamReader; +} // namespace v_1_0 +using IStreamReader = v_1_0::IStreamReader; + class IPluginV3Layer; class IPoolingLayer; class IQuantizeLayer; @@ -1199,13 +1207,14 @@ class VBuilder : public VRoot virtual IErrorRecorder* getErrorRecorder() const noexcept = 0; virtual void reset() noexcept = 0; virtual bool platformHasTf32() const noexcept = 0; - virtual nvinfer1::IHostMemory* buildSerializedNetwork(INetworkDefinition& network, IBuilderConfig& config) noexcept - = 0; + virtual nvinfer1::IHostMemory* buildSerializedNetwork( + INetworkDefinition& network, IBuilderConfig& config) noexcept = 0; virtual bool isNetworkSupported(INetworkDefinition const& network, IBuilderConfig const& config) const noexcept = 0; virtual ILogger* getLogger() const noexcept = 0; virtual bool setMaxThreads(int32_t maxThreads) noexcept = 0; virtual int32_t getMaxThreads() const noexcept = 0; virtual IPluginRegistry& getPluginRegistry() noexcept = 0; + virtual ICudaEngine* buildEngineWithConfig(INetworkDefinition& network, IBuilderConfig& config) noexcept = 0; }; } // namespace apiv diff --git a/include/NvInferLegacyDims.h b/include/NvInferLegacyDims.h index 2725d184..ecedf55a 100644 --- a/include/NvInferLegacyDims.h +++ b/include/NvInferLegacyDims.h @@ -18,9 +18,9 @@ #ifndef NV_INFER_LEGACY_DIMS_H #define NV_INFER_LEGACY_DIMS_H -#define NV_INFER_INTERNAL_INCLUDE_RUNTIME_BASE 1 +#define NV_INFER_INTERNAL_INCLUDE 1 #include "NvInferRuntimeBase.h" -#undef NV_INFER_INTERNAL_INCLUDE_RUNTIME_BASE +#undef NV_INFER_INTERNAL_INCLUDE //! //! \file NvInferLegacyDims.h diff --git a/include/NvInferPluginBase.h b/include/NvInferPluginBase.h new file mode 100644 index 00000000..d337f48a --- /dev/null +++ b/include/NvInferPluginBase.h @@ -0,0 +1,372 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 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. + */ + +#ifndef NV_INFER_PLUGIN_BASE_H +#define NV_INFER_PLUGIN_BASE_H + +#if !defined(NV_INFER_INTERNAL_INCLUDE) +static_assert(false, "Do not directly include this file. Include NvInferRuntime.h or NvInferPluginUtils.h"); +#endif + +#define NV_INFER_INTERNAL_INCLUDE 1 +#include "NvInferRuntimeBase.h" +#undef NV_INFER_INTERNAL_INCLUDE +namespace nvinfer1 +{ + +//! +//! \enum PluginFieldType +//! +//! \brief The possible field types for custom layer. +//! +enum class PluginFieldType : int32_t +{ + //! FP16 field type. + kFLOAT16 = 0, + //! FP32 field type. + kFLOAT32 = 1, + //! FP64 field type. + kFLOAT64 = 2, + //! INT8 field type. + kINT8 = 3, + //! INT16 field type. + kINT16 = 4, + //! INT32 field type. + kINT32 = 5, + //! char field type. + kCHAR = 6, + //! nvinfer1::Dims field type. + kDIMS = 7, + //! Unknown field type. + kUNKNOWN = 8, + //! BF16 field type. + kBF16 = 9, + //! INT64 field type. + kINT64 = 10, + //! FP8 field type. + kFP8 = 11, + //! INT4 field type. + kINT4 = 12, +}; + +//! +//! \class PluginField +//! +//! \brief Structure containing plugin attribute field names and associated data +//! This information can be parsed to decode necessary plugin metadata +//! +//! +class PluginField +{ +public: + //! Plugin field attribute name + AsciiChar const* name; + //! Plugin field attribute data + void const* data; + //! Plugin field attribute type + PluginFieldType type; + //! Number of data entries in the Plugin attribute + int32_t length; + + PluginField(AsciiChar const* const name_ = nullptr, void const* const data_ = nullptr, + PluginFieldType const type_ = PluginFieldType::kUNKNOWN, int32_t const length_ = 0) noexcept + : name(name_) + , data(data_) + , type(type_) + , length(length_) + { + } +}; + +//! +//! \struct PluginFieldCollection +//! +//! \brief Plugin field collection struct. +//! +struct PluginFieldCollection +{ + //! Number of PluginField entries. + int32_t nbFields{}; + //! Pointer to PluginField entries. + PluginField const* fields{}; +}; + +//! +//! \enum TensorRTPhase +//! +//! \brief Indicates a phase of operation of TensorRT +//! +enum class TensorRTPhase : int32_t +{ + //! Build phase of TensorRT + kBUILD = 0, + //! Execution phase of TensorRT + kRUNTIME = 1 +}; + +//! +//! \enum PluginCapabilityType +//! +//! \brief Enumerates the different capability types a IPluginV3 object may have +//! +enum class PluginCapabilityType : int32_t +{ + //! Core capability. Every IPluginV3 object must have this. + kCORE = 0, + //! Build capability. IPluginV3 objects provided to TensorRT build phase must have this. + kBUILD = 1, + //! Runtime capability. IPluginV3 objects provided to TensorRT build and execution phases must have this. + kRUNTIME = 2 +}; + +namespace v_1_0 +{ +class IPluginCapability : public IVersionedInterface +{ +}; + +class IPluginResource : public IVersionedInterface +{ +public: + //! + //! \brief Return version information associated with this interface. Applications must not override this method. + //! + InterfaceInfo getInterfaceInfo() const noexcept override + { + return InterfaceInfo{"IPluginResource", 1, 0}; + } + //! + //! \brief Free the underlying resource + //! + //! This will only be called for IPluginResource objects that were produced from IPluginResource::clone() + //! + //! The IPluginResource object on which release() is called must still be in a clone-able state + //! after release() returns + //! + //! \return 0 for success, else non-zero + //! \usage + //! - Allowed context for the API call + //! - Thread-safe: No; this method is not required to be thread-safe + //! + virtual int32_t release() noexcept = 0; + + //! + //! \brief Clone the resource object + //! + //! \note Resource initialization (if any) may be skipped for non-cloned objects since only clones will be + //! registered by TensorRT + //! + //! \return Pointer to cloned object. nullptr if there was an issue. + //! + //! \usage + //! - Allowed context for the API call + //! - Thread-safe: Yes; this method is required to be thread-safe and may be called from multiple threads. + //! + virtual IPluginResource* clone() noexcept = 0; + + ~IPluginResource() noexcept override = default; + + IPluginResource() = default; + IPluginResource(IPluginResource const&) = default; + IPluginResource(IPluginResource&&) = default; + IPluginResource& operator=(IPluginResource const&) & = default; + IPluginResource& operator=(IPluginResource&&) & = default; +}; // class IPluginResource + +class IPluginCreatorInterface : public IVersionedInterface +{ +public: + ~IPluginCreatorInterface() noexcept override = default; + +protected: + IPluginCreatorInterface() = default; + IPluginCreatorInterface(IPluginCreatorInterface const&) = default; + IPluginCreatorInterface(IPluginCreatorInterface&&) = default; + IPluginCreatorInterface& operator=(IPluginCreatorInterface const&) & = default; + IPluginCreatorInterface& operator=(IPluginCreatorInterface&&) & = default; +}; + +class IPluginV3 : public IVersionedInterface +{ +public: + //! + //! \brief Return version information associated with this interface. Applications must not override this method. + //! + InterfaceInfo getInterfaceInfo() const noexcept override + { + return InterfaceInfo{"PLUGIN", 1, 0}; + } + + //! \brief Return a pointer to plugin object implementing the specified PluginCapabilityType. + //! + //! \note IPluginV3 objects added for the build phase (through addPluginV3()) must return valid objects for + //! PluginCapabilityType::kCORE, PluginCapabilityType::kBUILD and PluginCapabilityType::kRUNTIME. + //! + //! \note IPluginV3 objects added for the runtime phase must return valid objects for + //! PluginCapabilityType::kCORE and PluginCapabilityType::kRUNTIME. + //! + //! \see TensorRTPhase + //! \see IPluginCreatorV3One::createPlugin() + //! + virtual IPluginCapability* getCapabilityInterface(PluginCapabilityType type) noexcept = 0; + + //! + //! \brief Clone the plugin object. This copies over internal plugin parameters and returns a new plugin object with + //! these parameters. The cloned object must be in a fully initialized state. + //! + //! \note The cloned object must return valid objects through getCapabilityInterface() for at least the same + //! PluginCapabilityTypes as the original object. + //! + //! \return A cloned plugin object in an initialized state with the same parameters as the current object. + //! nullptr must be returned if the cloning fails. + //! + virtual IPluginV3* clone() noexcept = 0; +}; + +class IPluginCreatorV3One : public IPluginCreatorInterface +{ +public: + //! + //! \brief Return version information associated with this interface. Applications must not override this method. + //! + InterfaceInfo getInterfaceInfo() const noexcept override + { + return InterfaceInfo{"PLUGIN CREATOR_V3ONE", 1, 0}; + } + + //! + //! \brief Return a plugin object. Return nullptr in case of error. + //! + //! \param name A NULL-terminated name string of length 1024 or less, including the NULL terminator. + //! \param fc A pointer to a collection of fields needed for constructing the plugin. + //! \param phase The TensorRT phase in which the plugin is being created + //! + //! When the phase is TensorRTPhase::kRUNTIME, the PluginFieldCollection provided for serialization by the plugin's + //! runtime interface will be passed as fc. + //! + //! \note The returned plugin object must be in an initialized state + //! + //! \note If invoked by the user (e.g. with TensorRTPhase::kBUILD, to add to the network defintion with + //! addPluginV3()), it is the user's responsibility to delete the plugin object. If invoked by TensorRT (e.g. during + //! engine deserialization), TensorRT will delete any objects it creates. + //! + virtual IPluginV3* createPlugin( + AsciiChar const* name, PluginFieldCollection const* fc, TensorRTPhase phase) noexcept = 0; + + //! + //! \brief Return a list of fields that need to be passed to createPlugin() when creating a plugin for use in the + //! TensorRT build phase. + //! + //! \see PluginFieldCollection + //! + virtual PluginFieldCollection const* getFieldNames() noexcept = 0; + + //! + //! \brief Return the plugin name. + //! + //! \warning The string returned must be NULL-terminated and have a length of 1024 bytes or less including + //! the NULL terminator. + //! + virtual AsciiChar const* getPluginName() const noexcept = 0; + + //! + //! \brief Return the plugin version. + //! + //! \warning The string returned must be NULL-terminated and have a length of 1024 bytes or less including + //! the NULL terminator. + //! + virtual AsciiChar const* getPluginVersion() const noexcept = 0; + + //! + //! \brief Return the plugin namespace. + //! + //! \warning The string returned must be NULL-terminated and have a length of 1024 bytes or less including + //! the NULL terminator. + //! + virtual AsciiChar const* getPluginNamespace() const noexcept = 0; + + IPluginCreatorV3One() = default; + virtual ~IPluginCreatorV3One() = default; + +protected: + IPluginCreatorV3One(IPluginCreatorV3One const&) = default; + IPluginCreatorV3One(IPluginCreatorV3One&&) = default; + IPluginCreatorV3One& operator=(IPluginCreatorV3One const&) & = default; + IPluginCreatorV3One& operator=(IPluginCreatorV3One&&) & = default; +}; + +} // namespace v_1_0 + +//! +//! \class IPluginCreatorV3One +//! +//! \brief A plugin creator class capable of producing IPluginV3 objects +//! +//! \see IPluginV3 +//! \see IPluginRegistry +//! +using IPluginCreatorV3One = v_1_0::IPluginCreatorV3One; + +//! +//! \class IPluginResource +//! +//! \brief Interface for plugins to define custom resources that could be shared through the plugin registry +//! +//! \see IPluginRegistry::acquirePluginResource +//! \see IPluginRegistry::releasePluginResource +//! +using IPluginResource = v_1_0::IPluginResource; + +//! +//! \class IPluginCreatorInterface +//! +//! \brief Base class for all plugin creator versions. +//! +//! \see IPluginCreator and IPluginRegistry +//! +using IPluginCreatorInterface = v_1_0::IPluginCreatorInterface; + +//! +//! \class IPluginV3 +//! +//! \brief Plugin class for the V3 generation of user-implemented layers. +//! +//! IPluginV3 acts as a wrapper around the plugin capability interfaces that define the actual behavior of the plugin. +//! +//! \see IPluginCapability +//! \see IPluginCreatorV3One +//! \see IPluginRegistry +//! +using IPluginV3 = v_1_0::IPluginV3; + +//! +//! \class IPluginCapability +//! +//! \brief Base class for plugin capability interfaces +//! +//! IPluginCapability represents a split in TensorRT V3 plugins to sub-objects that expose different types of +//! capabilites a plugin may have, as opposed to a single interface which defines all capabilities and behaviors of a +//! plugin. +//! +//! \warning Do not inherit from this class, as doing so will break forward-compatibility of the API and ABI. +//! +//! \see PluginCapabilityType +//! +using IPluginCapability = v_1_0::IPluginCapability; +} // namespace nvinfer1 + +#endif /* NV_INFER_PLUGIN_BASE_H */ diff --git a/include/NvInferRuntime.h b/include/NvInferRuntime.h index 485628a6..a9e60719 100644 --- a/include/NvInferRuntime.h +++ b/include/NvInferRuntime.h @@ -25,6 +25,9 @@ //! #include "NvInferImpl.h" +#define NV_INFER_INTERNAL_INCLUDE 1 +#include "NvInferPluginBase.h" +#undef NV_INFER_INTERNAL_INCLUDE #include "NvInferRuntimeCommon.h" namespace nvinfer1 @@ -622,6 +625,55 @@ class TRT_DEPRECATED IPluginV2DynamicExt : public nvinfer1::IPluginV2Ext } }; +namespace v_1_0 +{ +class IStreamReader : public IVersionedInterface +{ +public: + //! + //! TensorRT never calls the destructor for an IStreamReader defined by the + //! application. + //! + ~IStreamReader() override = default; + IStreamReader() = default; + + //! + //! \brief Return version information associated with this interface. Applications must not override this method. + //! + InterfaceInfo getInterfaceInfo() const noexcept override + { + return InterfaceInfo{"IStreamReader", 1, 0}; + } + + //! + //! \brief Read the next number of bytes in the stream. + //! + //! \param destination The memory to write to + //! \param nbBytes The number of bytes to read + //! + //! \returns The number of bytes read. Negative values will be considered an automatic error. + //! + virtual int64_t read(void* destination, int64_t nbBytes) = 0; + +protected: + IStreamReader(IStreamReader const&) = default; + IStreamReader(IStreamReader&&) = default; + IStreamReader& operator=(IStreamReader const&) & = default; + IStreamReader& operator=(IStreamReader&&) & = default; +}; +} // namespace v_1_0 + +//! +//! \class IStreamReader +//! +//! \brief Application-implemented class for reading data in a stream-based manner. +//! +//! \note To ensure compatibility of source code with future versions of TensorRT, use IStreamReader, not +//! v_1_0::IStreamReader +//! +using IStreamReader = v_1_0::IStreamReader; + + //! //! \class IPluginResourceContext //! @@ -659,82 +711,6 @@ class IPluginResourceContext IPluginResourceContext& operator=(IPluginResourceContext&&) & = default; }; -namespace v_1_0 -{ -class IPluginCapability : public IVersionedInterface -{ -}; -} // namespace v_1_0 - -//! -//! \class IPluginCapability -//! -//! \brief Base class for plugin capability interfaces -//! -//! IPluginCapability represents a split in TensorRT V3 plugins to sub-objects that expose different types of -//! capabilites a plugin may have, as opposed to a single interface which defines all capabilities and behaviors of a -//! plugin. -//! -//! \warning Do not inherit from this class, as doing so will break forward-compatibility of the API and ABI. -//! -//! \see PluginCapabilityType -//! -using IPluginCapability = v_1_0::IPluginCapability; - -namespace v_1_0 -{ -class IPluginV3 : public IVersionedInterface -{ -public: - //! - //! \brief Return version information associated with this interface. Applications must not override this method. - //! - InterfaceInfo getInterfaceInfo() const noexcept override - { - return InterfaceInfo{"PLUGIN", 1, 0}; - } - - //! \brief Return a pointer to plugin object implementing the specified PluginCapabilityType. - //! - //! \note IPluginV3 objects added for the build phase (through addPluginV3()) must return valid objects for - //! PluginCapabilityType::kCORE, PluginCapabilityType::kBUILD and PluginCapabilityType::kRUNTIME. - //! - //! \note IPluginV3 objects added for the runtime phase must return valid objects for - //! PluginCapabilityType::kCORE and PluginCapabilityType::kRUNTIME. - //! - //! \see TensorRTPhase - //! \see IPluginCreatorV3One::createPlugin() - //! - virtual IPluginCapability* getCapabilityInterface(PluginCapabilityType type) noexcept = 0; - - //! - //! \brief Clone the plugin object. This copies over internal plugin parameters and returns a new plugin object with - //! these parameters. The cloned object must be in a fully initialized state. - //! - //! \note The cloned object must return valid objects through getCapabilityInterface() for at least the same - //! PluginCapabilityTypes as the original object. - //! - //! \return A cloned plugin object in an initialized state with the same parameters as the current object. - //! nullptr must be returned if the cloning fails. - //! - virtual IPluginV3* clone() noexcept = 0; -}; - -} // namespace v_1_0 - -//! -//! \class IPluginV3 -//! -//! \brief Plugin class for the V3 generation of user-implemented layers. -//! -//! IPluginV3 acts as a wrapper around the plugin capability interfaces that define the actual behavior of the plugin. -//! -//! \see IPluginCapability -//! \see IPluginCreatorV3One -//! \see IPluginRegistry -//! -using IPluginV3 = v_1_0::IPluginV3; - namespace v_1_0 { class IPluginV3OneCore : public IPluginCapability @@ -815,6 +791,8 @@ class IPluginV3OneBuild : public IPluginCapability //! \param out The output tensors attributes that are used for configuration. //! \param nbOutputs Number of output tensors. //! + //! \return 0 for success, else non-zero (which will cause engine termination, if invoked by TensorRT). + //! virtual int32_t configurePlugin(DynamicPluginTensorDesc const* in, int32_t nbInputs, DynamicPluginTensorDesc const* out, int32_t nbOutputs) noexcept = 0; @@ -1185,87 +1163,6 @@ using IPluginV3OneRuntime = v_1_0::IPluginV3OneRuntime; //! using IPluginV3OneBuildV2 = v_2_0::IPluginV3OneBuild; -namespace v_1_0 -{ -class IPluginCreatorV3One : public IPluginCreatorInterface -{ -public: - //! - //! \brief Return version information associated with this interface. Applications must not override this method. - //! - InterfaceInfo getInterfaceInfo() const noexcept override - { - return InterfaceInfo{"PLUGIN CREATOR_V3ONE", 1, 0}; - } - - //! - //! \brief Return a plugin object. Return nullptr in case of error. - //! - //! \param name A NULL-terminated name string of length 1024 or less, including the NULL terminator. - //! \param fc A pointer to a collection of fields needed for constructing the plugin. - //! \param phase The TensorRT phase in which the plugin is being created - //! - //! When the phase is TensorRTPhase::kRUNTIME, the PluginFieldCollection provided for serialization by the plugin's - //! runtime interface will be passed as fc. - //! - //! \note The returned plugin object must be in an initialized state - //! - virtual IPluginV3* createPlugin( - AsciiChar const* name, PluginFieldCollection const* fc, TensorRTPhase phase) noexcept = 0; - - //! - //! \brief Return a list of fields that need to be passed to createPlugin() when creating a plugin for use in the - //! TensorRT build phase. - //! - //! \see PluginFieldCollection - //! - virtual PluginFieldCollection const* getFieldNames() noexcept = 0; - - //! - //! \brief Return the plugin name. - //! - //! \warning The string returned must be NULL-terminated and have a length of 1024 bytes or less including - //! the NULL terminator. - //! - virtual AsciiChar const* getPluginName() const noexcept = 0; - - //! - //! \brief Return the plugin version. - //! - //! \warning The string returned must be NULL-terminated and have a length of 1024 bytes or less including - //! the NULL terminator. - //! - virtual AsciiChar const* getPluginVersion() const noexcept = 0; - - //! - //! \brief Return the plugin namespace. - //! - //! \warning The string returned must be NULL-terminated and have a length of 1024 bytes or less including - //! the NULL terminator. - //! - virtual AsciiChar const* getPluginNamespace() const noexcept = 0; - - IPluginCreatorV3One() = default; - virtual ~IPluginCreatorV3One() = default; - -protected: - IPluginCreatorV3One(IPluginCreatorV3One const&) = default; - IPluginCreatorV3One(IPluginCreatorV3One&&) = default; - IPluginCreatorV3One& operator=(IPluginCreatorV3One const&) & = default; - IPluginCreatorV3One& operator=(IPluginCreatorV3One&&) & = default; -}; -} // namespace v_1_0 - -//! -//! \class IPluginCreatorV3One -//! -//! \brief A plugin creator class capable of producing IPluginV3 objects -//! -//! \see IPluginV3 -//! \see IPluginRegistry -//! -using IPluginCreatorV3One = v_1_0::IPluginCreatorV3One; - namespace v_1_0 { class IProfiler @@ -1375,6 +1272,464 @@ constexpr inline int32_t EnumMax() noexcept //! IRuntime::getTempfileControlFlags() using TempfileControlFlags = uint32_t; +//! +//! \enum TensorFormat +//! +//! \brief Format of the input/output tensors. +//! +//! This enum is used by both plugins and network I/O tensors. +//! +//! \see IPluginV2::supportsFormat(), safe::ICudaEngine::getBindingFormat() +//! +//! Many of the formats are **vector-major** or **vector-minor**. These formats specify +//! a vector dimension and scalars per vector. +//! For example, suppose that the tensor has has dimensions [M,N,C,H,W], +//! the vector dimension is C and there are V scalars per vector. +//! +//! * A **vector-major** format splits the vectorized dimension into two axes in the +//! memory layout. The vectorized dimension is replaced by an axis of length ceil(C/V) +//! and a new dimension of length V is appended. For the example tensor, the memory layout +//! is equivalent to an array with dimensions [M][N][ceil(C/V)][H][W][V]. +//! Tensor coordinate (m,n,c,h,w) maps to array location [m][n][c/V][h][w][c\%V]. +//! +//! * A **vector-minor** format moves the vectorized dimension to become the last axis +//! in the memory layout. For the example tensor, the memory layout is equivalent to an +//! array with dimensions [M][N][H][W][ceil(C/V)*V]. Tensor coordinate (m,n,c,h,w) maps +//! array location subscript [m][n][h][w][c]. +//! +//! In interfaces that refer to "components per element", that's the value of V above. +//! +//! For more information about data formats, see the topic "Data Format Description" located in the +//! TensorRT Developer Guide. https://docs.nvidia.com/deeplearning/tensorrt/developer-guide/index.html#data-format-desc +//! +enum class TensorFormat : int32_t +{ + //! Memory layout is similar to an array in C or C++. + //! The stride of each dimension is the product of the dimensions after it. + //! The last dimension has unit stride. + //! + //! For DLA usage, the tensor sizes are limited to C,H,W in the range [1,8192]. + kLINEAR = 0, + + //! Vector-major format with two scalars per vector. + //! Vector dimension is third to last. + //! + //! This format requires FP16 and at least three dimensions. + kCHW2 = 1, + + //! Vector-minor format with eight scalars per vector. + //! Vector dimension is third to last. + //! This format requires FP16 or BF16 and at least three dimensions. + kHWC8 = 2, + + //! Vector-major format with four scalars per vector. + //! Vector dimension is third to last. + //! + //! This format requires INT8 or FP16 and at least three dimensions. + //! For INT8, the length of the vector dimension must be a build-time constant. + //! + //! Deprecated usage: + //! + //! If running on the DLA, this format can be used for acceleration + //! with the caveat that C must be less than or equal to 4. + //! If used as DLA input and the build option kGPU_FALLBACK is not specified, + //! it needs to meet line stride requirement of DLA format. Column stride in + //! bytes must be a multiple of 64 on Orin. + kCHW4 = 3, + + //! Vector-major format with 16 scalars per vector. + //! Vector dimension is third to last. + //! + //! This format requires FP16 and at least three dimensions. + //! + //! For DLA usage, this format maps to the native feature format for FP16, + //! and the tensor sizes are limited to C,H,W in the range [1,8192]. + kCHW16 = 4, + + //! Vector-major format with 32 scalars per vector. + //! Vector dimension is third to last. + //! + //! This format requires at least three dimensions. + //! + //! For DLA usage, this format maps to the native feature format for INT8, + //! and the tensor sizes are limited to C,H,W in the range [1,8192]. + kCHW32 = 5, + + //! Vector-minor format with eight scalars per vector. + //! Vector dimension is fourth to last. + //! + //! This format requires FP16 or BF16 and at least four dimensions. + kDHWC8 = 6, + + //! Vector-major format with 32 scalars per vector. + //! Vector dimension is fourth to last. + //! + //! This format requires FP16 or INT8 and at least four dimensions. + kCDHW32 = 7, + + //! Vector-minor format where channel dimension is third to last and unpadded. + //! + //! This format requires either FP32, FP16, UINT8, INT64 or BF16 and at least three dimensions. + kHWC = 8, + + //! DLA planar format. For a tensor with dimension {N, C, H, W}, the W axis + //! always has unit stride. The stride for stepping along the H axis is + //! rounded up to 64 bytes. + //! + //! The memory layout is equivalent to a C array with dimensions + //! [N][C][H][roundUp(W, 64/elementSize)] where elementSize is + //! 2 for FP16 and 1 for Int8, with the tensor coordinates (n, c, h, w) + //! mapping to array subscript [n][c][h][w]. + kDLA_LINEAR = 9, + + //! DLA image format. For a tensor with dimension {N, C, H, W} the C axis + //! always has unit stride. The stride for stepping along the H axis is rounded up + //! to 64 bytes on Orin. C can only be 1, 3 or 4. + //! If C == 1, it will map to grayscale format. + //! If C == 3 or C == 4, it will map to color image format. And if C == 3, + //! the stride for stepping along the W axis needs to be padded to 4 in elements. + //! + //! When C is {1, 3, 4}, then C' is {1, 4, 4} respectively, + //! the memory layout is equivalent to a C array with dimensions + //! [N][H][roundUp(W, 64/C'/elementSize)][C'] on Orin + //! where elementSize is 2 for FP16 + //! and 1 for Int8. The tensor coordinates (n, c, h, w) mapping to array + //! subscript [n][h][w][c]. + kDLA_HWC4 = 10, + + //! Vector-minor format with 16 scalars per vector. + //! Vector dimension is third to last. + //! + //! This requires FP16 or INT8 and at least three dimensions. + kHWC16 = 11, + + //! Vector-minor format with one scalar per vector. + //! Vector dimension is fourth to last. + //! + //! This format requires FP32 and at least four dimensions. + kDHWC = 12 +}; + +namespace impl +{ +//! Maximum number of elements in TensorFormat enum. \see TensorFormat +template <> +struct EnumMaxImpl +{ + //! Declaration of kVALUE that represents the maximum number of elements in the TensorFormat enum. + static constexpr int32_t kVALUE = 13; +}; +} // namespace impl + +//! +//! \enum AllocatorFlag +//! +//! \brief Allowed type of memory allocation. +//! +enum class AllocatorFlag : int32_t +{ + //! TensorRT may call realloc() on this allocation. + kRESIZABLE = 0, +}; + +namespace impl +{ +//! Maximum number of elements in AllocatorFlag enum. \see AllocatorFlag +template <> +struct EnumMaxImpl +{ + //! Declaration of kVALUE that represents the maximum number of elements in the AllocatorFlag enum. + static constexpr int32_t kVALUE = 1; +}; +} // namespace impl + +using AllocatorFlags = uint32_t; + +//! DO NOT REFER TO namespace v_1_0 IN CODE. ALWAYS USE nvinfer1 INSTEAD. +//! The name v_1_0 may change in future versions of TensoRT. + +//! +//! \class ILogger +//! +//! \brief Application-implemented logging interface for the builder, refitter and runtime. +//! +//! The logger used to create an instance of IBuilder, IRuntime or IRefitter is used for all objects created through +//! that interface. The logger must be valid until all objects created are released. +//! +//! The Logger object implementation must be thread safe. All locking and synchronization is pushed to the +//! interface implementation and TensorRT does not hold any synchronization primitives when calling the interface +//! functions. +//! +class ILogger +{ +public: + //! + //! \enum Severity + //! + //! \brief The severity corresponding to a log message. + //! + enum class Severity : int32_t + { + //! An internal error has occurred. Execution is unrecoverable. + kINTERNAL_ERROR = 0, + //! An application error has occurred. + kERROR = 1, + //! An application error has been discovered, but TensorRT has recovered or fallen back to a default. + kWARNING = 2, + //! Informational messages with instructional information. + kINFO = 3, + //! Verbose messages with debugging information. + kVERBOSE = 4, + }; + + //! + //! \brief A callback implemented by the application to handle logging messages; + //! + //! \param severity The severity of the message. + //! \param msg A null-terminated log message. + //! + //! \warning Loggers used in the safety certified runtime must set a maximum message length and truncate + //! messages exceeding this length. It is up to the implementer of the derived class to define + //! a suitable limit that will prevent buffer overruns, resource exhaustion, and other security + //! vulnerabilities in their implementation. The TensorRT safety certified runtime will never + //! emit messages longer than 1024 bytes. + //! + //! \usage + //! - Allowed context for the API call + //! - Thread-safe: Yes, this method is required to be thread-safe and may be called from multiple threads + //! when multiple execution contexts are used during runtime, or if the same logger is used + //! for multiple runtimes, builders, or refitters. + //! + virtual void log(Severity severity, AsciiChar const* msg) noexcept = 0; + + ILogger() = default; + virtual ~ILogger() = default; + +protected: + // @cond SuppressDoxyWarnings + ILogger(ILogger const&) = default; + ILogger(ILogger&&) = default; + ILogger& operator=(ILogger const&) & = default; + ILogger& operator=(ILogger&&) & = default; + // @endcond +}; + +namespace impl +{ +//! Maximum number of elements in ILogger::Severity enum. \see ILogger::Severity +template <> +struct EnumMaxImpl +{ + //! Declaration of kVALUE that represents the maximum number of elements in the ILogger::Severity enum. + static constexpr int32_t kVALUE = 5; +}; +} // namespace impl + +namespace v_1_0 +{ + +class IGpuAllocator : public IVersionedInterface +{ +public: + //! + //! \brief A thread-safe callback implemented by the application to handle acquisition of GPU memory. + //! + //! \param size The size of the memory block required (in bytes). + //! \param alignment The required alignment of memory. Alignment will be zero + //! or a power of 2 not exceeding the alignment guaranteed by cudaMalloc. + //! Thus this allocator can be safely implemented with cudaMalloc/cudaFree. + //! An alignment value of zero indicates any alignment is acceptable. + //! \param flags Reserved for future use. In the current release, 0 will be passed. + //! + //! \return If the allocation was successful, the start address of a device memory block of the requested size. + //! If an allocation request of size 0 is made, nullptr must be returned. + //! If an allocation request cannot be satisfied, nullptr must be returned. + //! If a non-null address is returned, it is guaranteed to have the specified alignment. + //! + //! \note The implementation must guarantee thread safety for concurrent allocate/reallocate/deallocate + //! requests. + //! + //! \usage + //! - Allowed context for the API call + //! - Thread-safe: Yes, this method is required to be thread-safe and may be called from multiple threads. + //! + //! \deprecated Deprecated in TensorRT 10.0. Superseded by allocateAsync + //! + TRT_DEPRECATED virtual void* allocate( + uint64_t const size, uint64_t const alignment, AllocatorFlags const flags) noexcept = 0; + + ~IGpuAllocator() override = default; + IGpuAllocator() = default; + + //! + //! \brief A thread-safe callback implemented by the application to resize an existing allocation. + //! + //! Only allocations which were allocated with AllocatorFlag::kRESIZABLE will be resized. + //! + //! Options are one of: + //! * resize in place leaving min(oldSize, newSize) bytes unchanged and return the original address + //! * move min(oldSize, newSize) bytes to a new location of sufficient size and return its address + //! * return nullptr, to indicate that the request could not be fulfilled. + //! + //! If nullptr is returned, TensorRT will assume that resize() is not implemented, and that the + //! allocation at baseAddr is still valid. + //! + //! This method is made available for use cases where delegating the resize + //! strategy to the application provides an opportunity to improve memory management. + //! One possible implementation is to allocate a large virtual device buffer and + //! progressively commit physical memory with cuMemMap. CU_MEM_ALLOC_GRANULARITY_RECOMMENDED + //! is suggested in this case. + //! + //! TensorRT may call realloc to increase the buffer by relatively small amounts. + //! + //! \param baseAddr the address of the original allocation, which will have been returned by previously calling + //! allocate() or reallocate() on the same object. + //! \param alignment The alignment used by the original allocation. This will be the same value that was previously + //! passed to the allocate() or reallocate() call that returned baseAddr. + //! \param newSize The new memory size required (in bytes). + //! + //! \return The address of the reallocated memory, or nullptr. If a non-null address is returned, it is + //! guaranteed to have the specified alignment. + //! + //! \note The implementation must guarantee thread safety for concurrent allocate/reallocate/deallocate + //! requests. + //! + //! \usage + //! - Allowed context for the API call + //! - Thread-safe: Yes, this method is required to be thread-safe and may be called from multiple threads. + //! + virtual void* reallocate(void* const /*baseAddr*/, uint64_t /*alignment*/, uint64_t /*newSize*/) noexcept + { + return nullptr; + } + + //! + //! \brief A thread-safe callback implemented by the application to handle release of GPU memory. + //! + //! TensorRT may pass a nullptr to this function if it was previously returned by allocate(). + //! + //! \param memory A memory address that was previously returned by an allocate() or reallocate() call of the same + //! allocator object. + //! + //! \return True if the acquired memory is released successfully. + //! + //! \note The implementation must guarantee thread safety for concurrent allocate/reallocate/deallocate + //! requests. + //! + //! \usage + //! - Allowed context for the API call + //! - Thread-safe: Yes, this method is required to be thread-safe and may be called from multiple threads. + //! \deprecated Deprecated in TensorRT 10.0. Superseded by deallocateAsync + //! + TRT_DEPRECATED virtual bool deallocate(void* const memory) noexcept = 0; + + //! + //! \brief A thread-safe callback implemented by the application to handle stream-ordered acquisition of GPU memory. + //! + //! The default behavior is to call method allocate(), which is synchronous and thus loses + //! any performance benefits of asynchronous allocation. If you want the benefits of asynchronous + //! allocation, see discussion of IGpuAsyncAllocator vs. IGpuAllocator in the documentation + //! for nvinfer1::IGpuAllocator. + //! + //! \param size The size of the memory block required (in bytes). + //! \param alignment The required alignment of memory. Alignment will be zero + //! or a power of 2 not exceeding the alignment guaranteed by cudaMalloc. + //! Thus this allocator can be safely implemented with cudaMalloc/cudaFree. + //! An alignment value of zero indicates any alignment is acceptable. + //! \param flags Reserved for future use. In the current release, 0 will be passed. + //! \param stream specifies the cudaStream for asynchronous usage. + //! + //! \return If the allocation was successful, the start address of a device memory block of the requested size. + //! If an allocation request of size 0 is made, nullptr must be returned. + //! If an allocation request cannot be satisfied, nullptr must be returned. + //! If a non-null address is returned, it is guaranteed to have the specified alignment. + //! + //! \note The implementation must guarantee thread safety for concurrent allocate/reallocate/deallocate + //! requests. + //! + //! \usage + //! - Allowed context for the API call + //! - Thread-safe: Yes, this method is required to be thread-safe and may be called from multiple threads. + //! + virtual void* allocateAsync( + uint64_t const size, uint64_t const alignment, AllocatorFlags const flags, cudaStream_t /*stream*/) noexcept + { + return allocate(size, alignment, flags); + } + //! + //! \brief A thread-safe callback implemented by the application to handle stream-ordered release of GPU memory. + //! + //! The default behavior is to call method deallocate(), which is synchronous and thus loses + //! any performance benefits of asynchronous deallocation. If you want the benefits of asynchronous + //! deallocation, see discussion of IGpuAsyncAllocator vs. IGpuAllocator in the documentation + //! for nvinfer1::IGpuAllocator. + //! + //! TensorRT may pass a nullptr to this function if it was previously returned by allocate(). + //! + //! \param memory A memory address that was previously returned by an allocate() or reallocate() call of the same + //! allocator object. + //! \param stream specifies the cudaStream for asynchronous usage. + //! + //! \return True if the acquired memory is released successfully. + //! + //! \note The implementation must guarantee thread safety for concurrent allocate/reallocate/deallocate + //! requests. + //! + //! \note The implementation is not required to be asynchronous. It is permitted to synchronize, + //! albeit doing so will lose the performance advantage of asynchronous deallocation. + //! Either way, it is critical that it not actually free the memory until the current + //! stream position is reached. + //! + //! \usage + //! - Allowed context for the API call + //! - Thread-safe: Yes, this method is required to be thread-safe and may be called from multiple threads. + //! + virtual bool deallocateAsync(void* const memory, cudaStream_t /*stream*/) noexcept + { + return deallocate(memory); + } + + //! + //! \brief Return version information associated with this interface. Applications must not override this method. + //! + InterfaceInfo getInterfaceInfo() const noexcept override + { + return {"IGpuAllocator", 1, 0}; + } + +protected: + // @cond SuppressDoxyWarnings + IGpuAllocator(IGpuAllocator const&) = default; + IGpuAllocator(IGpuAllocator&&) = default; + IGpuAllocator& operator=(IGpuAllocator const&) & = default; + IGpuAllocator& operator=(IGpuAllocator&&) & = default; + // @endcond +}; + +} // namespace v_1_0 + +//! +//! \class IGpuAllocator +//! +//! \brief Application-implemented class for controlling allocation on the GPU. +//! +//! \warning The lifetime of an IGpuAllocator object must exceed that of all objects that use it. +//! +//! This class is intended as a base class for allocators that implement synchronous allocation. +//! If you want the benefits of asynchronous allocation, you can do either of: +//! +//! * Derive your class from IGpuAllocator and override all four of its virtual methods +//! for allocation/deallocation, including the two deprecated methods. +//! +//! * Derive your class from IGpuAsyncAllocator and override its two pure virtual +//! methods for allocation/deallocation. +//! +//! The latter style is preferred because it does not tie code to deprecated methods. +//! +//! \see IGpuAsyncAllocator. +//! +using IGpuAllocator = v_1_0::IGpuAllocator; + //! //! \class IRuntime //! @@ -1503,6 +1858,7 @@ class IRuntime : public INoCopy return mImpl->deserializeCudaEngine(streamReader); } + //! //! \brief get the logger with which the runtime was created //! @@ -3533,7 +3889,7 @@ class IDebugListener : public IVersionedInterface //! \param type data Type of the tensor. //! \param shape shape of the tensor. //! \param name name of the tensor. - //! \param stream Cuda stream object. + //! \param stream CUDA stream object. //! //! \return True on success, false otherwise. //! @@ -3647,7 +4003,7 @@ class IExecutionContext : public INoCopy //! //! \brief Set the device memory for use by this execution context. //! - //! The memory must be aligned with cuda memory alignment property (using cudaGetDeviceProperties()), and its size + //! The memory must be aligned with CUDA memory alignment property (using cudaGetDeviceProperties()), and its size //! must be large enough for performing inference with the given network inputs. getDeviceMemorySize() and //! getDeviceMemorySizeForProfile() report upper bounds of the size. Setting memory to nullptr is acceptable if the //! reported size is 0. If using enqueueV3() to run the network, the memory is in use from the invocation of @@ -3674,7 +4030,7 @@ class IExecutionContext : public INoCopy //! //! \brief Set the device memory and its corresponding size for use by this execution context. //! - //! The memory must be aligned with cuda memory alignment property (using cudaGetDeviceProperties()), and its size + //! The memory must be aligned with CUDA memory alignment property (using cudaGetDeviceProperties()), and its size //! must be large enough for performing inference with the given network inputs. getDeviceMemorySize() and //! getDeviceMemorySizeForProfile() report upper bounds of the size. Setting memory to nullptr is acceptable if the //! reported size is 0. If using enqueueV3() to run the network, the memory is in use from the invocation of @@ -3875,7 +4231,7 @@ class IExecutionContext : public INoCopy //! \param profileIndex Index of the profile. The value must lie between 0 and //! getEngine().getNbOptimizationProfiles() - 1 //! - //! \param stream A cuda stream on which the cudaMemcpyAsyncs may be + //! \param stream A CUDA stream on which the cudaMemcpyAsyncs may be //! enqueued //! //! When an optimization profile is switched via this API, TensorRT may @@ -4145,7 +4501,7 @@ class IExecutionContext : public INoCopy //! //! \brief Mark input as consumed. //! - //! \param event The cuda event that is triggered after all input tensors have been consumed. + //! \param event The CUDA event that is triggered after all input tensors have been consumed. //! //! \warning The set event must be valid during the inferece. //! @@ -4161,7 +4517,7 @@ class IExecutionContext : public INoCopy //! //! \brief The event associated with consuming the input. //! - //! \return The cuda event. Nullptr will be returned if the event is not set yet. + //! \return The CUDA event. Nullptr will be returned if the event is not set yet. //! cudaEvent_t getInputConsumedEvent() const noexcept { @@ -4251,7 +4607,7 @@ class IExecutionContext : public INoCopy //! //! \brief Enqueue inference on a stream. //! - //! \param stream A cuda stream on which the inference kernels will be enqueued. + //! \param stream A CUDA stream on which the inference kernels will be enqueued. //! //! \return True if the kernels were enqueued successfully, false otherwise. //! @@ -4844,7 +5200,6 @@ class IGpuAsyncAllocator : public IGpuAllocator //! //! \see IGpuAllocator using IGpuAsyncAllocator = v_1_0::IGpuAsyncAllocator; - } // namespace nvinfer1 //! diff --git a/include/NvInferRuntimeBase.h b/include/NvInferRuntimeBase.h index b6652c07..bde3d1dd 100644 --- a/include/NvInferRuntimeBase.h +++ b/include/NvInferRuntimeBase.h @@ -66,12 +66,10 @@ //! //! \warning Do not directly include this file. Instead include one of: //! * NvInferRuntime.h (for the standard runtime) -//! * NvInferSafeRuntime.h (for the safety runtime) -//! * NvInferConsistency.h (for consistency checker) //! * NvInferPluginUtils.h (for plugin utilities) //! -#if !defined(NV_INFER_INTERNAL_INCLUDE_RUNTIME_BASE) -static_assert(false, "Do not directly include this file. Include NvInferRuntime.h or NvInferSafeRuntime.h or NvInferConsistency.h or NvInferPluginUtils.h"); +#if !defined(NV_INFER_INTERNAL_INCLUDE) +static_assert(false, "Do not directly include this file. Include NvInferRuntime.h or NvInferPluginUtils.h"); #endif //! Forward declare some CUDA types to avoid an include dependency. @@ -216,144 +214,6 @@ class Dims64 //! using Dims = Dims64; -//! -//! \enum TensorFormat -//! -//! \brief Format of the input/output tensors. -//! -//! This enum is used by both plugins and network I/O tensors. -//! -//! \see IPluginV2::supportsFormat(), safe::ICudaEngine::getBindingFormat() -//! -//! Many of the formats are **vector-major** or **vector-minor**. These formats specify -//! a vector dimension and scalars per vector. -//! For example, suppose that the tensor has has dimensions [M,N,C,H,W], -//! the vector dimension is C and there are V scalars per vector. -//! -//! * A **vector-major** format splits the vectorized dimension into two axes in the -//! memory layout. The vectorized dimension is replaced by an axis of length ceil(C/V) -//! and a new dimension of length V is appended. For the example tensor, the memory layout -//! is equivalent to an array with dimensions [M][N][ceil(C/V)][H][W][V]. -//! Tensor coordinate (m,n,c,h,w) maps to array location [m][n][c/V][h][w][c\%V]. -//! -//! * A **vector-minor** format moves the vectorized dimension to become the last axis -//! in the memory layout. For the example tensor, the memory layout is equivalent to an -//! array with dimensions [M][N][H][W][ceil(C/V)*V]. Tensor coordinate (m,n,c,h,w) maps -//! array location subscript [m][n][h][w][c]. -//! -//! In interfaces that refer to "components per element", that's the value of V above. -//! -//! For more information about data formats, see the topic "Data Format Description" located in the -//! TensorRT Developer Guide. https://docs.nvidia.com/deeplearning/tensorrt/developer-guide/index.html#data-format-desc -//! -enum class TensorFormat : int32_t -{ - //! Memory layout is similar to an array in C or C++. - //! The stride of each dimension is the product of the dimensions after it. - //! The last dimension has unit stride. - //! - //! For DLA usage, the tensor sizes are limited to C,H,W in the range [1,8192]. - kLINEAR = 0, - - //! Vector-major format with two scalars per vector. - //! Vector dimension is third to last. - //! - //! This format requires FP16 and at least three dimensions. - kCHW2 = 1, - - //! Vector-minor format with eight scalars per vector. - //! Vector dimension is third to last. - //! This format requires FP16 or BF16 and at least three dimensions. - kHWC8 = 2, - - //! Vector-major format with four scalars per vector. - //! Vector dimension is third to last. - //! - //! This format requires INT8 or FP16 and at least three dimensions. - //! For INT8, the length of the vector dimension must be a build-time constant. - //! - //! Deprecated usage: - //! - //! If running on the DLA, this format can be used for acceleration - //! with the caveat that C must be less than or equal to 4. - //! If used as DLA input and the build option kGPU_FALLBACK is not specified, - //! it needs to meet line stride requirement of DLA format. Column stride in - //! bytes must be a multiple of 64 on Orin. - kCHW4 = 3, - - //! Vector-major format with 16 scalars per vector. - //! Vector dimension is third to last. - //! - //! This format requires FP16 and at least three dimensions. - //! - //! For DLA usage, this format maps to the native feature format for FP16, - //! and the tensor sizes are limited to C,H,W in the range [1,8192]. - kCHW16 = 4, - - //! Vector-major format with 32 scalars per vector. - //! Vector dimension is third to last. - //! - //! This format requires at least three dimensions. - //! - //! For DLA usage, this format maps to the native feature format for INT8, - //! and the tensor sizes are limited to C,H,W in the range [1,8192]. - kCHW32 = 5, - - //! Vector-minor format with eight scalars per vector. - //! Vector dimension is fourth to last. - //! - //! This format requires FP16 or BF16 and at least four dimensions. - kDHWC8 = 6, - - //! Vector-major format with 32 scalars per vector. - //! Vector dimension is fourth to last. - //! - //! This format requires FP16 or INT8 and at least four dimensions. - kCDHW32 = 7, - - //! Vector-minor format where channel dimension is third to last and unpadded. - //! - //! This format requires either FP32, FP16, UINT8, INT64 or BF16 and at least three dimensions. - kHWC = 8, - - //! DLA planar format. For a tensor with dimension {N, C, H, W}, the W axis - //! always has unit stride. The stride for stepping along the H axis is - //! rounded up to 64 bytes. - //! - //! The memory layout is equivalent to a C array with dimensions - //! [N][C][H][roundUp(W, 64/elementSize)] where elementSize is - //! 2 for FP16 and 1 for Int8, with the tensor coordinates (n, c, h, w) - //! mapping to array subscript [n][c][h][w]. - kDLA_LINEAR = 9, - - //! DLA image format. For a tensor with dimension {N, C, H, W} the C axis - //! always has unit stride. The stride for stepping along the H axis is rounded up - //! to 64 bytes on Orin. C can only be 1, 3 or 4. - //! If C == 1, it will map to grayscale format. - //! If C == 3 or C == 4, it will map to color image format. And if C == 3, - //! the stride for stepping along the W axis needs to be padded to 4 in elements. - //! - //! When C is {1, 3, 4}, then C' is {1, 4, 4} respectively, - //! the memory layout is equivalent to a C array with dimensions - //! [N][H][roundUp(W, 64/C'/elementSize)][C'] on Orin - //! where elementSize is 2 for FP16 - //! and 1 for Int8. The tensor coordinates (n, c, h, w) mapping to array - //! subscript [n][h][w][c]. - kDLA_HWC4 = 10, - - //! Vector-minor format with 16 scalars per vector. - //! Vector dimension is third to last. - //! - //! This requires FP16 or INT8 and at least three dimensions. - kHWC16 = 11, - - //! Vector-minor format with one scalar per vector. - //! Vector dimension is fourth to last. - //! - //! This format requires FP32 and at least four dimensions. - kDHWC = 12 -}; - using InterfaceKind = char const*; //! @@ -424,326 +284,6 @@ class IVersionedInterface IVersionedInterface& operator=(IVersionedInterface&&) & = default; }; -namespace impl -{ -//! Maximum number of elements in TensorFormat enum. \see TensorFormat -template <> -struct EnumMaxImpl -{ - //! Declaration of kVALUE that represents the maximum number of elements in the TensorFormat enum. - static constexpr int32_t kVALUE = 13; -}; -} // namespace impl - - -//! -//! \enum AllocatorFlag -//! -//! \brief Allowed type of memory allocation. -//! -enum class AllocatorFlag : int32_t -{ - //! TensorRT may call realloc() on this allocation. - kRESIZABLE = 0, -}; - -namespace impl -{ -//! Maximum number of elements in AllocatorFlag enum. \see AllocatorFlag -template <> -struct EnumMaxImpl -{ - //! Declaration of kVALUE that represents the maximum number of elements in the AllocatorFlag enum. - static constexpr int32_t kVALUE = 1; -}; -} // namespace impl - -using AllocatorFlags = uint32_t; - -//! DO NOT REFER TO namespace v_1_0 IN CODE. ALWAYS USE nvinfer1 INSTEAD. -//! The name v_1_0 may change in future versions of TensoRT. -namespace v_1_0 -{ - -class IGpuAllocator : public IVersionedInterface -{ -public: - //! - //! \brief A thread-safe callback implemented by the application to handle acquisition of GPU memory. - //! - //! \param size The size of the memory block required (in bytes). - //! \param alignment The required alignment of memory. Alignment will be zero - //! or a power of 2 not exceeding the alignment guaranteed by cudaMalloc. - //! Thus this allocator can be safely implemented with cudaMalloc/cudaFree. - //! An alignment value of zero indicates any alignment is acceptable. - //! \param flags Reserved for future use. In the current release, 0 will be passed. - //! - //! \return If the allocation was successful, the start address of a device memory block of the requested size. - //! If an allocation request of size 0 is made, nullptr must be returned. - //! If an allocation request cannot be satisfied, nullptr must be returned. - //! If a non-null address is returned, it is guaranteed to have the specified alignment. - //! - //! \note The implementation must guarantee thread safety for concurrent allocate/reallocate/deallocate - //! requests. - //! - //! \usage - //! - Allowed context for the API call - //! - Thread-safe: Yes, this method is required to be thread-safe and may be called from multiple threads. - //! - //! \deprecated Deprecated in TensorRT 10.0. Superseded by allocateAsync - //! - TRT_DEPRECATED virtual void* allocate( - uint64_t const size, uint64_t const alignment, AllocatorFlags const flags) noexcept = 0; - - ~IGpuAllocator() override = default; - IGpuAllocator() = default; - - //! - //! \brief A thread-safe callback implemented by the application to resize an existing allocation. - //! - //! Only allocations which were allocated with AllocatorFlag::kRESIZABLE will be resized. - //! - //! Options are one of: - //! * resize in place leaving min(oldSize, newSize) bytes unchanged and return the original address - //! * move min(oldSize, newSize) bytes to a new location of sufficient size and return its address - //! * return nullptr, to indicate that the request could not be fulfilled. - //! - //! If nullptr is returned, TensorRT will assume that resize() is not implemented, and that the - //! allocation at baseAddr is still valid. - //! - //! This method is made available for use cases where delegating the resize - //! strategy to the application provides an opportunity to improve memory management. - //! One possible implementation is to allocate a large virtual device buffer and - //! progressively commit physical memory with cuMemMap. CU_MEM_ALLOC_GRANULARITY_RECOMMENDED - //! is suggested in this case. - //! - //! TensorRT may call realloc to increase the buffer by relatively small amounts. - //! - //! \param baseAddr the address of the original allocation, which will have been returned by previously calling - //! allocate() or reallocate() on the same object. - //! \param alignment The alignment used by the original allocation. This will be the same value that was previously - //! passed to the allocate() or reallocate() call that returned baseAddr. - //! \param newSize The new memory size required (in bytes). - //! - //! \return The address of the reallocated memory, or nullptr. If a non-null address is returned, it is - //! guaranteed to have the specified alignment. - //! - //! \note The implementation must guarantee thread safety for concurrent allocate/reallocate/deallocate - //! requests. - //! - //! \usage - //! - Allowed context for the API call - //! - Thread-safe: Yes, this method is required to be thread-safe and may be called from multiple threads. - //! - virtual void* reallocate(void* const /*baseAddr*/, uint64_t /*alignment*/, uint64_t /*newSize*/) noexcept - { - return nullptr; - } - - //! - //! \brief A thread-safe callback implemented by the application to handle release of GPU memory. - //! - //! TensorRT may pass a nullptr to this function if it was previously returned by allocate(). - //! - //! \param memory A memory address that was previously returned by an allocate() or reallocate() call of the same - //! allocator object. - //! - //! \return True if the acquired memory is released successfully. - //! - //! \note The implementation must guarantee thread safety for concurrent allocate/reallocate/deallocate - //! requests. - //! - //! \usage - //! - Allowed context for the API call - //! - Thread-safe: Yes, this method is required to be thread-safe and may be called from multiple threads. - //! \deprecated Deprecated in TensorRT 10.0. Superseded by deallocateAsync - //! - TRT_DEPRECATED virtual bool deallocate(void* const memory) noexcept = 0; - - //! - //! \brief A thread-safe callback implemented by the application to handle stream-ordered acquisition of GPU memory. - //! - //! The default behavior is to call method allocate(), which is synchronous and thus loses - //! any performance benefits of asynchronous allocation. If you want the benefits of asynchronous - //! allocation, see discussion of IGpuAsyncAllocator vs. IGpuAllocator in the documentation - //! for nvinfer1::IGpuAllocator. - //! - //! \param size The size of the memory block required (in bytes). - //! \param alignment The required alignment of memory. Alignment will be zero - //! or a power of 2 not exceeding the alignment guaranteed by cudaMalloc. - //! Thus this allocator can be safely implemented with cudaMalloc/cudaFree. - //! An alignment value of zero indicates any alignment is acceptable. - //! \param flags Reserved for future use. In the current release, 0 will be passed. - //! \param stream specifies the cudaStream for asynchronous usage. - //! - //! \return If the allocation was successful, the start address of a device memory block of the requested size. - //! If an allocation request of size 0 is made, nullptr must be returned. - //! If an allocation request cannot be satisfied, nullptr must be returned. - //! If a non-null address is returned, it is guaranteed to have the specified alignment. - //! - //! \note The implementation must guarantee thread safety for concurrent allocate/reallocate/deallocate - //! requests. - //! - //! \usage - //! - Allowed context for the API call - //! - Thread-safe: Yes, this method is required to be thread-safe and may be called from multiple threads. - //! - virtual void* allocateAsync( - uint64_t const size, uint64_t const alignment, AllocatorFlags const flags, cudaStream_t /*stream*/) noexcept - { - return allocate(size, alignment, flags); - } - //! - //! \brief A thread-safe callback implemented by the application to handle stream-ordered release of GPU memory. - //! - //! The default behavior is to call method deallocate(), which is synchronous and thus loses - //! any performance benefits of asynchronous deallocation. If you want the benefits of asynchronous - //! deallocation, see discussion of IGpuAsyncAllocator vs. IGpuAllocator in the documentation - //! for nvinfer1::IGpuAllocator. - //! - //! TensorRT may pass a nullptr to this function if it was previously returned by allocate(). - //! - //! \param memory A memory address that was previously returned by an allocate() or reallocate() call of the same - //! allocator object. - //! \param stream specifies the cudaStream for asynchronous usage. - //! - //! \return True if the acquired memory is released successfully. - //! - //! \note The implementation must guarantee thread safety for concurrent allocate/reallocate/deallocate - //! requests. - //! - //! \note The implementation is not required to be asynchronous. It is permitted to synchronize, - //! albeit doing so will lose the performance advantage of asynchronous deallocation. - //! Either way, it is critical that it not actually free the memory until the current - //! stream position is reached. - //! - //! \usage - //! - Allowed context for the API call - //! - Thread-safe: Yes, this method is required to be thread-safe and may be called from multiple threads. - //! - virtual bool deallocateAsync(void* const memory, cudaStream_t /*stream*/) noexcept - { - return deallocate(memory); - } - - //! - //! \brief Return version information associated with this interface. Applications must not override this method. - //! - InterfaceInfo getInterfaceInfo() const noexcept override - { - return {"IGpuAllocator", 1, 0}; - } - -protected: - // @cond SuppressDoxyWarnings - IGpuAllocator(IGpuAllocator const&) = default; - IGpuAllocator(IGpuAllocator&&) = default; - IGpuAllocator& operator=(IGpuAllocator const&) & = default; - IGpuAllocator& operator=(IGpuAllocator&&) & = default; - // @endcond -}; - -} // namespace v_1_0 - -//! -//! \class IGpuAllocator -//! -//! \brief Application-implemented class for controlling allocation on the GPU. -//! -//! \warning The lifetime of an IGpuAllocator object must exceed that of all objects that use it. -//! -//! This class is intended as a base class for allocators that implement synchronous allocation. -//! If you want the benefits of asynchronous allocation, you can do either of: -//! -//! * Derive your class from IGpuAllocator and override all four of its virtual methods -//! for allocation/deallocation, including the two deprecated methods. -//! -//! * Derive your class from IGpuAsyncAllocator and override its two pure virtual -//! methods for allocation/deallocation. -//! -//! The latter style is preferred because it does not tie code to deprecated methods. -//! -//! \see IGpuAsyncAllocator. -//! -using IGpuAllocator = v_1_0::IGpuAllocator; - -//! -//! \class ILogger -//! -//! \brief Application-implemented logging interface for the builder, refitter and runtime. -//! -//! The logger used to create an instance of IBuilder, IRuntime or IRefitter is used for all objects created through -//! that interface. The logger must be valid until all objects created are released. -//! -//! The Logger object implementation must be thread safe. All locking and synchronization is pushed to the -//! interface implementation and TensorRT does not hold any synchronization primitives when calling the interface -//! functions. -//! -class ILogger -{ -public: - //! - //! \enum Severity - //! - //! \brief The severity corresponding to a log message. - //! - enum class Severity : int32_t - { - //! An internal error has occurred. Execution is unrecoverable. - kINTERNAL_ERROR = 0, - //! An application error has occurred. - kERROR = 1, - //! An application error has been discovered, but TensorRT has recovered or fallen back to a default. - kWARNING = 2, - //! Informational messages with instructional information. - kINFO = 3, - //! Verbose messages with debugging information. - kVERBOSE = 4, - }; - - //! - //! \brief A callback implemented by the application to handle logging messages; - //! - //! \param severity The severity of the message. - //! \param msg A null-terminated log message. - //! - //! \warning Loggers used in the safety certified runtime must set a maximum message length and truncate - //! messages exceeding this length. It is up to the implementer of the derived class to define - //! a suitable limit that will prevent buffer overruns, resource exhaustion, and other security - //! vulnerabilities in their implementation. The TensorRT safety certified runtime will never - //! emit messages longer than 1024 bytes. - //! - //! \usage - //! - Allowed context for the API call - //! - Thread-safe: Yes, this method is required to be thread-safe and may be called from multiple threads - //! when multiple execution contexts are used during runtime, or if the same logger is used - //! for multiple runtimes, builders, or refitters. - //! - virtual void log(Severity severity, AsciiChar const* msg) noexcept = 0; - - ILogger() = default; - virtual ~ILogger() = default; - -protected: -// @cond SuppressDoxyWarnings - ILogger(ILogger const&) = default; - ILogger(ILogger&&) = default; - ILogger& operator=(ILogger const&) & = default; - ILogger& operator=(ILogger&&) & = default; -// @endcond -}; - -namespace impl -{ -//! Maximum number of elements in ILogger::Severity enum. \see ILogger::Severity -template <> -struct EnumMaxImpl -{ - //! Declaration of kVALUE that represents the maximum number of elements in the ILogger::Severity enum. - static constexpr int32_t kVALUE = 5; -}; -} // namespace impl - //! //! \enum ErrorCode //! @@ -1108,116 +648,6 @@ enum class TensorIOMode : int32_t kOUTPUT = 2 }; -namespace v_1_0 -{ -class IStreamReader : public IVersionedInterface -{ -public: - //! - //! TensorRT never calls the destructor for an IStreamReader defined by the - //! application. - //! - ~IStreamReader() override = default; - IStreamReader() = default; - - //! - //! \brief Return version information associated with this interface. Applications must not override this method. - //! - InterfaceInfo getInterfaceInfo() const noexcept override - { - return InterfaceInfo{"IStreamReader", 1, 0}; - } - - //! - //! \brief Read the next number of bytes in the stream. - //! - //! \param destination The memory to write to - //! \param nbBytes The number of bytes to read - //! - //! \returns The number of bytes read. Negative values will be considered an automatic error. - //! - virtual int64_t read(void* destination, int64_t nbBytes) = 0; - -protected: - IStreamReader(IStreamReader const&) = default; - IStreamReader(IStreamReader&&) = default; - IStreamReader& operator=(IStreamReader const&) & = default; - IStreamReader& operator=(IStreamReader&&) & = default; -}; -} // namespace v_1_0 - -//! -//! \class IStreamReader -//! -//! \brief Application-implemented class for reading data in a stream-based manner. -//! -//! \note To ensure compatibility of source code with future versions of TensorRT, use IStreamReader, not -//! v_1_0::IStreamReader -//! -using IStreamReader = v_1_0::IStreamReader; - -namespace v_1_0 -{ - -class IPluginResource : public IVersionedInterface -{ -public: - //! - //! \brief Return version information associated with this interface. Applications must not override this method. - //! - InterfaceInfo getInterfaceInfo() const noexcept override - { - return InterfaceInfo{"IPluginResource", 1, 0}; - } - //! - //! \brief Free the underlying resource - //! - //! This will only be called for IPluginResource objects that were produced from IPluginResource::clone() - //! - //! The IPluginResource object on which release() is called must still be in a clone-able state - //! after release() returns - //! - //! \return 0 for success, else non-zero - //! \usage - //! - Allowed context for the API call - //! - Thread-safe: No; this method is not required to be thread-safe - //! - virtual int32_t release() noexcept = 0; - - //! - //! \brief Clone the resource object - //! - //! \note Resource initialization (if any) may be skipped for non-cloned objects since only clones will be - //! registered by TensorRT - //! - //! \return Pointer to cloned object. nullptr if there was an issue. - //! - //! \usage - //! - Allowed context for the API call - //! - Thread-safe: Yes; this method is required to be thread-safe and may be called from multiple threads. - //! - virtual IPluginResource* clone() noexcept = 0; - - ~IPluginResource() noexcept override = default; - - IPluginResource() = default; - IPluginResource(IPluginResource const&) = default; - IPluginResource(IPluginResource&&) = default; - IPluginResource& operator=(IPluginResource const&) & = default; - IPluginResource& operator=(IPluginResource&&) & = default; -}; // class IPluginResource -} // namespace v_1_0 - -//! -//! \class IPluginResource -//! -//! \brief Interface for plugins to define custom resources that could be shared through the plugin registry -//! -//! \see IPluginRegistry::acquirePluginResource -//! \see IPluginRegistry::releasePluginResource -//! -using IPluginResource = v_1_0::IPluginResource; - namespace impl { //! Maximum number of elements in TensorIOMode enum. \see TensorIOMode diff --git a/include/NvInferRuntimeCommon.h b/include/NvInferRuntimeCommon.h index 13e42f4f..19b83b36 100644 --- a/include/NvInferRuntimeCommon.h +++ b/include/NvInferRuntimeCommon.h @@ -28,9 +28,9 @@ //! //! \warning Do not directly include this file. Instead include NvInferRuntime.h //! -#define NV_INFER_INTERNAL_INCLUDE_RUNTIME_BASE 1 -#include "NvInferRuntimeBase.h" -#undef NV_INFER_INTERNAL_INCLUDE_RUNTIME_BASE +#define NV_INFER_INTERNAL_INCLUDE 1 +#include "NvInferPluginBase.h" +#undef NV_INFER_INTERNAL_INCLUDE #include "NvInferRuntimePlugin.h" namespace nvinfer1 diff --git a/include/NvInferRuntimePlugin.h b/include/NvInferRuntimePlugin.h index dffdd901..dbe5bb49 100644 --- a/include/NvInferRuntimePlugin.h +++ b/include/NvInferRuntimePlugin.h @@ -18,9 +18,9 @@ #ifndef NV_INFER_RUNTIME_PLUGIN_H #define NV_INFER_RUNTIME_PLUGIN_H -#define NV_INFER_INTERNAL_INCLUDE_RUNTIME_BASE 1 -#include "NvInferRuntimeBase.h" -#undef NV_INFER_INTERNAL_INCLUDE_RUNTIME_BASE +#define NV_INFER_INTERNAL_INCLUDE 1 +#include "NvInferPluginBase.h" +#undef NV_INFER_INTERNAL_INCLUDE //! //! \file NvInferRuntimePlugin.h @@ -28,8 +28,7 @@ //! This file contains common definitions, data structures and interfaces that relate to plugins and are shared //! between the standard and safe runtime. //! -//! \warning Do not directly include this file. Instead include either NvInferRuntime.h (for the standard runtime) or -//! NvInferSafeRuntime.h (for the safety runtime). +//! \warning Do not directly include this file. Instead include NvInferRuntime.h //! //! @@ -40,6 +39,13 @@ namespace nvinfer1 { +enum class TensorFormat : int32_t; +namespace v_1_0 +{ +class IGpuAllocator; +} +using IGpuAllocator = v_1_0::IGpuAllocator; + //! //! \brief PluginFormat is reserved for backward compatibility. //! @@ -824,126 +830,8 @@ class TRT_DEPRECATED IPluginV2IOExt : public IPluginV2Ext } }; -//! -//! \enum PluginFieldType -//! -//! \brief The possible field types for custom layer. -//! -enum class PluginFieldType : int32_t -{ - //! FP16 field type. - kFLOAT16 = 0, - //! FP32 field type. - kFLOAT32 = 1, - //! FP64 field type. - kFLOAT64 = 2, - //! INT8 field type. - kINT8 = 3, - //! INT16 field type. - kINT16 = 4, - //! INT32 field type. - kINT32 = 5, - //! char field type. - kCHAR = 6, - //! nvinfer1::Dims field type. - kDIMS = 7, - //! Unknown field type. - kUNKNOWN = 8, - //! BF16 field type. - kBF16 = 9, - //! INT64 field type. - kINT64 = 10, - //! FP8 field type. - kFP8 = 11, - //! INT4 field type. - kINT4 = 12, -}; - -//! -//! \class PluginField -//! -//! \brief Structure containing plugin attribute field names and associated data -//! This information can be parsed to decode necessary plugin metadata -//! -//! -class PluginField -{ -public: - //! Plugin field attribute name - AsciiChar const* name; - //! Plugin field attribute data - void const* data; - //! Plugin field attribute type - PluginFieldType type; - //! Number of data entries in the Plugin attribute - int32_t length; - - PluginField(AsciiChar const* const name_ = nullptr, void const* const data_ = nullptr, - PluginFieldType const type_ = PluginFieldType::kUNKNOWN, int32_t const length_ = 0) noexcept - : name(name_) - , data(data_) - , type(type_) - , length(length_) - { - } -}; - -//! -//! \struct PluginFieldCollection -//! -//! \brief Plugin field collection struct. -//! -struct PluginFieldCollection -{ - //! Number of PluginField entries. - int32_t nbFields; - //! Pointer to PluginField entries. - PluginField const* fields; -}; - -//! -//! \enum PluginCapabilityType -//! -//! \brief Enumerates the different capability types a IPluginV3 object may have -//! -enum class PluginCapabilityType : int32_t -{ - //! Core capability. Every IPluginV3 object must have this. - kCORE = 0, - //! Build capability. IPluginV3 objects provided to TensorRT build phase must have this. - kBUILD = 1, - //! Runtime capability. IPluginV3 objects provided to TensorRT build and execution phases must have this. - kRUNTIME = 2 -}; - -//! -//! \enum TensorRTPhase -//! -//! \brief Indicates a phase of operation of TensorRT -//! -enum class TensorRTPhase : int32_t -{ - //! Build phase of TensorRT - kBUILD = 0, - //! Execution phase of TensorRT - kRUNTIME = 1 -}; - namespace v_1_0 { -class IPluginCreatorInterface : public IVersionedInterface -{ -public: - ~IPluginCreatorInterface() noexcept override = default; - -protected: - IPluginCreatorInterface() = default; - IPluginCreatorInterface(IPluginCreatorInterface const&) = default; - IPluginCreatorInterface(IPluginCreatorInterface&&) = default; - IPluginCreatorInterface& operator=(IPluginCreatorInterface const&) & = default; - IPluginCreatorInterface& operator=(IPluginCreatorInterface&&) & = default; -}; - class TRT_DEPRECATED IPluginCreator : public IPluginCreatorInterface { public: @@ -1071,15 +959,6 @@ class TRT_DEPRECATED IPluginCreator : public IPluginCreatorInterface }; } // namespace v_1_0 -//! -//! \class IPluginCreatorInterface -//! -//! \brief Base class for all plugin creator versions. -//! -//! \see IPluginCreator and IPluginRegistry -//! -using IPluginCreatorInterface = v_1_0::IPluginCreatorInterface; - //! //! \class IPluginCreator //! diff --git a/include/NvInferVersion.h b/include/NvInferVersion.h index 084b4ee1..d0d78512 100644 --- a/include/NvInferVersion.h +++ b/include/NvInferVersion.h @@ -24,9 +24,9 @@ #define NV_INFER_VERSION_H #define NV_TENSORRT_MAJOR 10 //!< TensorRT major version. -#define NV_TENSORRT_MINOR 5 //!< TensorRT minor version. +#define NV_TENSORRT_MINOR 6 //!< TensorRT minor version. #define NV_TENSORRT_PATCH 0 //!< TensorRT patch version. -#define NV_TENSORRT_BUILD 18 //!< TensorRT build number. +#define NV_TENSORRT_BUILD 26 //!< TensorRT build number. #define NV_TENSORRT_LWS_MAJOR 0 //!< TensorRT LWS major version. #define NV_TENSORRT_LWS_MINOR 0 //!< TensorRT LWS minor version. diff --git a/parsers/onnx b/parsers/onnx index 886aff91..4442153a 160000 --- a/parsers/onnx +++ b/parsers/onnx @@ -1 +1 @@ -Subproject commit 886aff917b63f10a81c5f31e89752a3b46169623 +Subproject commit 4442153a4483c29e109241eb11752f3e59be62f8 diff --git a/plugin/README.md b/plugin/README.md index 8f024104..4f13c98a 100644 --- a/plugin/README.md +++ b/plugin/README.md @@ -19,8 +19,8 @@ | [efficientNMSPlugin](efficientNMSPlugin) | EfficientNMS_TRT | 1 | | [efficientNMSONNXPlugin](efficientNMSPlugin) [DEPRECATED] | EfficientNMS_ONNX_TRT | 1 | | [embLayerNormPlugin](embLayerNormPlugin) [DEPRECATED]| CustomEmbLayerNormPluginDynamic | 1, 2, 3 | -| [embLayerNormPlugin](embLayerNormPlugin) | CustomEmbLayerNormPluginDynamic | 4, 5 | -| [fcPlugin](fcPlugin) | CustomFCPluginDynamic | 1 | +| [embLayerNormPlugin](embLayerNormPlugin) | CustomEmbLayerNormPluginDynamic | 4, 5, 6 | +| [fcPlugin](fcPlugin) [DEPRECATED] | CustomFCPluginDynamic | 1 | | [flattenConcat](flattenConcat) | FlattenConcat_TRT | 1 | | [geluPlugin](geluPlugin) [DEPRECATED] | CustomGeluPluginDynamic | 1 | | [generateDetectionPlugin](generateDetectionPlugin) | GenerateDetection_TRT | 1 | diff --git a/plugin/api/inferPlugin.cpp b/plugin/api/inferPlugin.cpp index 28c42cae..5067963e 100644 --- a/plugin/api/inferPlugin.cpp +++ b/plugin/api/inferPlugin.cpp @@ -16,11 +16,13 @@ */ #include "NvInfer.h" #include "NvInferPlugin.h" +#include "common/checkMacrosPlugin.h" +#include "common/plugin.h" +#include "roiAlignPlugin/roiAlignPlugin.h" +#if !TRT_WINML #include "batchTilePlugin/batchTilePlugin.h" #include "batchedNMSPlugin/batchedNMSPlugin.h" #include "clipPlugin/clipPlugin.h" -#include "common/checkMacrosPlugin.h" -#include "common/plugin.h" #include "coordConvACPlugin/coordConvACPlugin.h" #include "cropAndResizePlugin/cropAndResizePlugin.h" #include "decodeBbox3DPlugin/decodeBbox3D.h" @@ -57,7 +59,7 @@ #include "specialSlicePlugin/specialSlicePlugin.h" #include "splitPlugin/split.h" #include "voxelGeneratorPlugin/voxelGenerator.h" - +#endif #include #include #include @@ -181,6 +183,8 @@ extern "C" { bool initLibNvInferPlugins(void* logger, char const* libNamespace) { + initializePlugin(logger, libNamespace); +#if !TRT_WINML initializePlugin(logger, libNamespace); initializePlugin(logger, libNamespace); initializePlugin(logger, libNamespace); @@ -220,14 +224,14 @@ extern "C" initializePlugin(logger, libNamespace); initializePlugin(logger, libNamespace); initializePlugin(logger, libNamespace); - initializePlugin(logger, libNamespace); initializePlugin(logger, libNamespace); - initializePlugin(logger, libNamespace); initializePlugin(logger, libNamespace); + initializePlugin(logger, libNamespace); initializePlugin(logger, libNamespace); initializePlugin(logger, libNamespace); initializePlugin(logger, libNamespace); initializePlugin(logger, libNamespace); +#endif return true; } } // extern "C" diff --git a/plugin/embLayerNormPlugin/CustomEmbLayerNormPluginDynamic_PluginConfig.yaml b/plugin/embLayerNormPlugin/CustomEmbLayerNormPluginDynamic_PluginConfig.yaml index 6d8d1125..62b95b35 100644 --- a/plugin/embLayerNormPlugin/CustomEmbLayerNormPluginDynamic_PluginConfig.yaml +++ b/plugin/embLayerNormPlugin/CustomEmbLayerNormPluginDynamic_PluginConfig.yaml @@ -16,9 +16,9 @@ # --- name: CustomEmbLayerNormPluginDynamic -interface: "IPluginV2DynamicExt" +interface: "IPluginV3" versions: - "1": + "6": inputs: - token_id - segment_id diff --git a/plugin/embLayerNormPlugin/README.md b/plugin/embLayerNormPlugin/README.md index ca0ed259..46e9595f 100644 --- a/plugin/embLayerNormPlugin/README.md +++ b/plugin/embLayerNormPlugin/README.md @@ -31,7 +31,7 @@ Assuming contiguous input masks, encodes the masks as a single number denoting t The version 1 `embLayerNormPlugin` takes three inputs; `token_id`, `segment_id`, and `input_mask`. The subsequent versions 2,3,4,5 (variable seqlen) take four inputs; `token_id`, `segment_id`, `cu_seqlen`, and `max_seqlen`. -### Version 1 +### Version 1 & 6 Inputs: - `token_id` An input sequence containing token ids. token_id is an `int32` tensor with shape `[S, B,]` where `S` is the sequence length and `B` is the batch size. @@ -56,7 +56,7 @@ The final output embedding is the sum of embeddings for the token, the segment a The `maskIdx` is a more compact representation of the input mask, consisting of the number of valid elements, assuming that the original mask was contiguous. For fixed sequence length version 1, the `maskIdx` is an `int32` tensor with shape `[B, packSize]` where `B` is batch size, `packSize` is the packed mask size that depends on the sequence length. -### Version >= 2 +### 6 > Version >= 2 Inputs: - `token_id` @@ -95,17 +95,17 @@ The final output embedding is the sum of embeddings for the token, the segment a The parameters are defined below and consists of the following attributes: -| Type | Parameter | Version | Description -|----------|----------------------------------------|----------------|-------------------------------------------------------- -|`int` |`output_fp16` | 1, 2, 3, 4, 5 |Integer encoding the DataType, set 0 when build FP32 network and set 1 when build FP32/INT8 network (0: FP32, 1: FP16) -|`int` |`full_mask` | 1 |Whether to output the full mask that works with the specialized multi-head-attention plugin kernels (this is deprecated, please use mha_type_id) -|`int` |`mha_type_id` | 1 |Integer encoding the multi-head-attention plugin DataType (0: FP32, 1: FP16, 2: INT8) -|`Weights` |`bert_embeddings_layernorm_beta` | 1, 2, 3, 4, 5 |Beta parameter for layer norm. Shape: `[E,]` where `E` is hidden size -|`Weights` |`bert_embeddings_layernorm_gamma` | 1, 2, 3, 4, 5 |Gamma parameter for layer norm. Shape: `[E,]` where `E` is hidden size -|`Weights` |`bert_embeddings_word_embeddings` | 1, 2, 3, 4, 5 |Token embedding matrix. Shape: `[word_vocab_size, E]` where `E` is hidden size -|`Weights` |`bert_embeddings_token_type_embeddings` | 1, 2, 3, 4, 5 |Token type embedding matrix. Shape: `[type_vocab_size, E]` where `E` is hidden size -|`Weights` |`bert_embeddings_position_embeddings` | 1, 2, 3, 4, 5 |Positional embedding matrix. Shape: `[S, E]` where `S` is the maximum sequence length and `E` is hidden size - +| Type | Parameter | Version | Description +|----------|----------------------------------------|-------------------|-------------------------------------------------------- +|`int` |`output_fp16` | 1, 2, 3, 4, 5, 6 |Integer encoding the DataType, set 0 when build FP32 network and set 1 when build FP32/INT8 network (0: FP32, 1: FP16) +|`int` |`full_mask` | 1, 6 |Whether to output the full mask that works with the specialized multi-head-attention plugin kernels (this is deprecated, please use mha_type_id) +|`int` |`mha_type_id` | 1, 6 |Integer encoding the multi-head-attention plugin DataType (0: FP32, 1: FP16, 2: INT8) +|`Weights` |`bert_embeddings_layernorm_beta` | 1, 2, 3, 4, 5, 6 |Beta parameter for layer norm. Shape: `[E,]` where `E` is hidden size +|`Weights` |`bert_embeddings_layernorm_gamma` | 1, 2, 3, 4, 5, 6 |Gamma parameter for layer norm. Shape: `[E,]` where `E` is hidden size +|`Weights` |`bert_embeddings_word_embeddings` | 1, 2, 3, 4, 5, 6 |Token embedding matrix. Shape: `[word_vocab_size, E]` where `E` is hidden size +|`Weights` |`bert_embeddings_token_type_embeddings` | 1, 2, 3, 4, 5, 6 |Token type embedding matrix. Shape: `[type_vocab_size, E]` where `E` is hidden size +|`Weights` |`bert_embeddings_position_embeddings` | 1, 2, 3, 4, 5, 6 |Positional embedding matrix. Shape: `[S, E]` where `S` is the maximum sequence length and `E` is hidden size +Note: version 1, 2, 3 are deprecated and will be removed in a future release; please use their corresponding updated versions: 6, 4, 5 respectively. ## Additional resources @@ -123,6 +123,9 @@ documentation. ## Changelog +September 2024: +Added `EmblayerNormPlugin` version 6 that mirrors version 1 in IO and attributes (but uses underlying `IPluginV3` implementation instead of the deprecated `IPluginV2DynamicExt` interface) + July 2024: Add `EmbLayerNormPlugin` versions 3 & 4 that duplicate the behavior of v2 and v3 plugins respectively, but implement the `IPluginV3` interface instead of the deprecated `IPluginV2DynamicExt` interface. Update this README with updated description of I/O and structure. diff --git a/plugin/embLayerNormPlugin/embLayerNormPlugin.cpp b/plugin/embLayerNormPlugin/embLayerNormPlugin.cpp index ab523971..c682d48f 100644 --- a/plugin/embLayerNormPlugin/embLayerNormPlugin.cpp +++ b/plugin/embLayerNormPlugin/embLayerNormPlugin.cpp @@ -32,8 +32,8 @@ using namespace nvinfer1::plugin::bert; namespace { -char const* EMB_LAYER_NORM_VERSION{"1"}; -char const* EMB_LAYER_NORM_NAME{"CustomEmbLayerNormPluginDynamic"}; +char const* gEmbLayerNormVersion{"6"}; +char const* gEmbLayerNormName{"CustomEmbLayerNormPluginDynamic"}; } // namespace // Static class fields initialization @@ -48,7 +48,6 @@ EmbLayerNormPluginDynamic::EmbLayerNormPluginDynamic(std::string const& name, Da : mLayerName(name) , mLd(beta.count) , mType(type) - , mUseFullMask(useFullMask) , mMhaType(mhaType) { // Assuming Weights.count is the number of elements and not bytes @@ -61,8 +60,9 @@ EmbLayerNormPluginDynamic::EmbLayerNormPluginDynamic(std::string const& name, Da mPosVocabSize = posEmb.count / mLd; mTokVocabSize = tokEmb.count / mLd; mSM = getSMVersion(); - // mS is set during configure - + mOutputFp16 = mType == DataType::kHALF ? 1 : 0; + mUseFullMask = static_cast(useFullMask); + // NOTE: mS is set during configure mBeta.convertAndCopy(beta, nvinfer1::DataType::kFLOAT); mGamma.convertAndCopy(gamma, nvinfer1::DataType::kFLOAT); mWordEmb.convertAndCopy(wordEmb, mType); @@ -76,235 +76,179 @@ EmbLayerNormPluginDynamic::EmbLayerNormPluginDynamic(std::string const& name, Da copyToDevice(mTokEmb, getWeightsSize(mTokEmb, mType), mTokEmbDev); } -EmbLayerNormPluginDynamic::EmbLayerNormPluginDynamic(std::string const& name, void const* data, size_t length) - : mLayerName(name) - , mGammaDev(nullptr) - , mBetaDev(nullptr) - , mWordEmbDev(nullptr) - , mTokEmbDev(nullptr) - , mPosEmbDev(nullptr) -{ - BERT_DEBUG_MSG("EmbLayerNormPluginDynamic deserialize."); - - // Deserialize in the same order as serialization - deserialize_value(&data, &length, &mType); - deserialize_value(&data, &length, &mMhaType); - deserialize_value(&data, &length, &mLd); - deserialize_value(&data, &length, &mS); - deserialize_value(&data, &length, &mWordVocabSize); - deserialize_value(&data, &length, &mPosVocabSize); - deserialize_value(&data, &length, &mTokVocabSize); - deserialize_value(&data, &length, &mUseFullMask); - deserialize_value(&data, &length, &mSM); - - char const* d = static_cast(data); - mBeta.convertAndCopy(d, mLd, nvinfer1::DataType::kFLOAT); - mGamma.convertAndCopy(d, mLd, nvinfer1::DataType::kFLOAT); - mWordEmb.convertAndCopy(d, mLd * mWordVocabSize, mType); - mPosEmb.convertAndCopy(d, mLd * mPosVocabSize, mType); - mTokEmb.convertAndCopy(d, mLd * mTokVocabSize, mType); - - copyToDevice(mGamma, sizeof(float) * mGamma.count, mGammaDev); - copyToDevice(mBeta, sizeof(float) * mBeta.count, mBetaDev); - copyToDevice(mWordEmb, getWeightsSize(mWordEmb, mType), mWordEmbDev); - copyToDevice(mPosEmb, getWeightsSize(mPosEmb, mType), mPosEmbDev); - copyToDevice(mTokEmb, getWeightsSize(mTokEmb, mType), mTokEmbDev); -} - -// IPluginV2DynamicExt Methods -IPluginV2DynamicExt* EmbLayerNormPluginDynamic::clone() const noexcept +EmbLayerNormPluginDynamic::~EmbLayerNormPluginDynamic() { try { - BERT_DEBUG_MSG("EmbLayerNormPluginDynamic clone."); - - auto p = new EmbLayerNormPluginDynamic( - mLayerName, mType, mMhaType, mBeta, mGamma, mWordEmb, mPosEmb, mTokEmb, mUseFullMask); - p->mS = mS; - p->setPluginNamespace(mNamespace.c_str()); - - return p; + // This gets called when the network containing plugin is destroyed + mGammaDev.reset(nullptr); + mBetaDev.reset(nullptr); + mWordEmbDev.reset(nullptr); + mPosEmbDev.reset(nullptr); + mTokEmbDev.reset(nullptr); + // delete this; TRT or the creator of the plugin will delete this plugin object } catch (std::exception const& e) { caughtError(e); } - return nullptr; } -DimsExprs EmbLayerNormPluginDynamic::getOutputDimensions( - int32_t outputIndex, DimsExprs const* inputs, int32_t nbInputs, IExprBuilder& exprBuilder) noexcept +////// +// IPluginV3 method definitions: +// - getCapabilityInterface() (Base) +// - clone() (HFace, MTron) +////// +IPluginCapability* EmbLayerNormPluginDynamic::getCapabilityInterface(PluginCapabilityType type) noexcept { try { - // Input should be input ids and token ids and the input mask - // Output should be the embeddings tensor and mask indices - PLUGIN_ASSERT(nbInputs == 3); - - PLUGIN_ASSERT(inputs[0].nbDims == 2); // BxS - PLUGIN_ASSERT(inputs[0].nbDims == inputs[1].nbDims); - PLUGIN_ASSERT(inputs[0].nbDims == inputs[2].nbDims); - - PLUGIN_ASSERT(outputIndex == 0 || outputIndex == 1); - - if (outputIndex == 0) + if (type == PluginCapabilityType::kBUILD) { - DimsExprs ret; - ret.nbDims = 5; - ret.d[0] = inputs[0].d[0]; - ret.d[1] = inputs[0].d[1]; - ret.d[2] = exprBuilder.constant(mLd); - ret.d[3] = exprBuilder.constant(1); - ret.d[4] = exprBuilder.constant(1); - return ret; - } - - DimsExprs ret; - ret.nbDims = 2; - ret.d[0] = inputs[0].d[BDIM]; - auto cms0 = exprBuilder.constant(unfusedMaskSize); - - // this code must match getMHAMaskPackedSize in bertCommon.h - bool const isSmOK - = (mSM == kSM_75 || mSM == kSM_80 || mSM == kSM_86 || mSM == kSM_87 || mSM == kSM_89 || mSM == kSM_90); - bool const isPrecisionOK = (mMhaType == nvinfer1::DataType::kHALF || mMhaType == nvinfer1::DataType::kINT8); - if (mUseFullMask || (isSmOK && isPrecisionOK)) - { - // support 128, 384 in both int8 and fp16 - auto cms128 = exprBuilder.constant(packedMaskSize128); - auto cms384 = exprBuilder.constant(packedMaskSize384); - auto c128 = exprBuilder.constant(128); - auto c384 = exprBuilder.constant(384); - auto is128 = exprBuilder.operation(DimensionOperation::kEQUAL, *inputs[0].d[SDIM], *c128); - auto is384 = exprBuilder.operation(DimensionOperation::kEQUAL, *inputs[0].d[SDIM], *c384); - auto sel128 = exprBuilder.operation(DimensionOperation::kPROD, *is128, *cms128); - auto sel384 = exprBuilder.operation(DimensionOperation::kPROD, *is384, *cms384); - auto maskSize = exprBuilder.operation(DimensionOperation::kSUM, *sel384, *sel128); - - // support 64, 96 in both int8 and fp16 - auto cms64 = exprBuilder.constant(packedMaskSize64); - auto cms96 = exprBuilder.constant(packedMaskSize96); - auto c64 = exprBuilder.constant(64); - auto c96 = exprBuilder.constant(96); - - auto is64 = exprBuilder.operation(DimensionOperation::kEQUAL, *inputs[0].d[SDIM], *c64); - auto is96 = exprBuilder.operation(DimensionOperation::kEQUAL, *inputs[0].d[SDIM], *c96); - auto sel64 = exprBuilder.operation(DimensionOperation::kPROD, *is64, *cms64); - auto sel96 = exprBuilder.operation(DimensionOperation::kPROD, *is96, *cms96); - auto maskSize2 = exprBuilder.operation(DimensionOperation::kSUM, *sel64, *sel96); - maskSize = exprBuilder.operation(DimensionOperation::kSUM, *maskSize, *maskSize2); - - auto is0 = exprBuilder.operation(DimensionOperation::kEQUAL, *maskSize, *exprBuilder.constant(0)); - auto sel0 = exprBuilder.operation(DimensionOperation::kPROD, *is0, *cms0); - auto combinedMaskSize = exprBuilder.operation(DimensionOperation::kSUM, *maskSize, *sel0); - ret.d[1] = combinedMaskSize; + return static_cast(this); } - else + if (type == PluginCapabilityType::kRUNTIME) { - ret.d[1] = cms0; + return static_cast(this); } - - return ret; + PLUGIN_ASSERT(type == PluginCapabilityType::kCORE); + return static_cast(this); } catch (std::exception const& e) { caughtError(e); } - return DimsExprs{}; + return nullptr; } -bool EmbLayerNormPluginDynamic::supportsFormatCombination( - int32_t pos, PluginTensorDesc const* inOut, int32_t nbInputs, int32_t nbOutputs) noexcept +IPluginV3* EmbLayerNormPluginDynamic::clone() noexcept { - // 3 inputs of size BxS - PLUGIN_ASSERT(nbInputs == 3); - PLUGIN_ASSERT(nbOutputs == 2); - - PluginTensorDesc const& desc = inOut[pos]; - if (desc.format != TensorFormat::kLINEAR) + try { - return false; + BERT_DEBUG_MSG("EmbLayerNormPluginDynamic clone."); + + auto p = new EmbLayerNormPluginDynamic( + mLayerName, mType, mMhaType, mBeta, mGamma, mWordEmb, mPosEmb, mTokEmb, mUseFullMask == 1); + p->mS = mS; + p->setPluginNamespace(mNamespace.c_str()); + + return p; } - if (pos == 0) + catch (std::exception const& e) { - return desc.type == DataType::kINT32 && desc.dims.nbDims == 2; + caughtError(e); } + return nullptr; +} - PluginTensorDesc const& prev = inOut[pos - 1]; - if (pos == 1 || pos == 2) +// End IPluginV3 method definitions + +////// +// IPluginV3OneRuntime method definitions: +// - getFieldsToSerialize() +// - onShapeChange() +// - attachToContext() +// - enqueue() +///// + +PluginFieldCollection const* EmbLayerNormPluginDynamic::getFieldsToSerialize() noexcept +{ + mDataToSerialize.clear(); + mDataToSerialize.emplace_back("output_fp16", &mOutputFp16, PluginFieldType::kINT32, 1); + mDataToSerialize.emplace_back("full_mask", &mUseFullMask, PluginFieldType::kINT32, 1); + mDataToSerialize.emplace_back("mha_type_id", &mMhaType, PluginFieldType::kINT32, 1); + mDataToSerialize.emplace_back("bert_embeddings_layernorm_beta", static_cast(mBeta.values), + PluginFieldType::kFLOAT32, mBeta.count); + mDataToSerialize.emplace_back("bert_embeddings_layernorm_gamma", static_cast(mGamma.values), + PluginFieldType::kFLOAT32, mGamma.count); + if (mOutputFp16) { - return desc.type == DataType::kINT32 && desc.dims.nbDims == 2 && desc.dims.d[BDIM] == prev.dims.d[BDIM] - && desc.dims.d[SDIM] == prev.dims.d[SDIM]; + mDataToSerialize.emplace_back("bert_embeddings_word_embeddings", static_cast(mWordEmb.values), + PluginFieldType::kFLOAT16, mWordEmb.count); + mDataToSerialize.emplace_back("bert_embeddings_token_type_embeddings", static_cast(mTokEmb.values), + PluginFieldType::kFLOAT16, mTokEmb.count); + mDataToSerialize.emplace_back("bert_embeddings_position_embeddings", static_cast(mPosEmb.values), + PluginFieldType::kFLOAT16, mPosEmb.count); } - - // embedded sequence - if (pos == 3) + else { - return desc.type == mType && desc.dims.nbDims == 5 && desc.dims.d[BDIM] == prev.dims.d[BDIM] - && desc.dims.d[SDIM] == prev.dims.d[SDIM] && desc.dims.d[3] == 1 && desc.dims.d[4] == 1; + mDataToSerialize.emplace_back("bert_embeddings_word_embeddings", static_cast(mWordEmb.values), + PluginFieldType::kFLOAT32, mWordEmb.count); + mDataToSerialize.emplace_back("bert_embeddings_token_type_embeddings", + static_cast(mTokEmb.values), PluginFieldType::kFLOAT32, mTokEmb.count); + mDataToSerialize.emplace_back("bert_embeddings_position_embeddings", static_cast(mPosEmb.values), + PluginFieldType::kFLOAT32, mPosEmb.count); } - // mask - return desc.type == DataType::kINT32; + mFCToSerialize.nbFields = mDataToSerialize.size(); + mFCToSerialize.fields = mDataToSerialize.data(); + return &mFCToSerialize; } -void EmbLayerNormPluginDynamic::configurePlugin(DynamicPluginTensorDesc const* inputs, int32_t nbInputs, - DynamicPluginTensorDesc const* outputs, int32_t nbOutputs) noexcept +int32_t EmbLayerNormPluginDynamic::onShapeChange( + PluginTensorDesc const* inputs, int32_t nbInputs, PluginTensorDesc const* outputs, int32_t nbOutputs) noexcept { BERT_DEBUG_MSG("EmbLayerNormPluginDynamic configurePlugin."); + try + { + // Validate input arguments + PLUGIN_ASSERT(nbOutputs == 2); + PLUGIN_ASSERT(nbInputs == 3); - // Validate input arguments - PLUGIN_ASSERT(nbOutputs == 2); - PLUGIN_ASSERT(nbInputs == 3); + PLUGIN_ASSERT(inputs[0].dims.nbDims == 2); + int32_t const S = inputs[0].dims.d[SDIM]; + mS = S; + int32_t const B = inputs[0].dims.d[BDIM]; + TRT_UNUSED B; + PLUGIN_ASSERT(mS == static_cast(inputs[1].dims.d[SDIM])); + PLUGIN_ASSERT(B == inputs[1].dims.d[BDIM]); + PLUGIN_ASSERT(mS == static_cast(inputs[2].dims.d[SDIM])); + PLUGIN_ASSERT(B == inputs[2].dims.d[BDIM]); + + PLUGIN_ASSERT(outputs[0].dims.nbDims == 5); + PLUGIN_ASSERT(static_cast(outputs[0].dims.d[SDIM]) == mS); + PLUGIN_ASSERT(outputs[0].dims.d[BDIM] == B); + PLUGIN_ASSERT(static_cast(outputs[0].dims.d[2]) == mLd); + PLUGIN_ASSERT(outputs[0].dims.d[3] == 1); + PLUGIN_ASSERT(outputs[0].dims.d[4] == 1); + + if (mUseFullMask) + { + // user force full_mask + PLUGIN_ASSERT(outputs[1].dims.nbDims == 2); + PLUGIN_ASSERT(outputs[1].dims.d[0] == B); + PLUGIN_ASSERT((outputs[1].dims.d[1] == -1) || (outputs[1].dims.d[1] == packedMaskSize384) + || (outputs[1].dims.d[1] == packedMaskSize128)); + } + else + { + // auto detect using mhatype + if (S != -1 && B != -1) + { + PLUGIN_ASSERT(outputs[1].dims.nbDims == 2); + PLUGIN_ASSERT(outputs[1].dims.d[0] == B); + int32_t packedSize = getMHAMaskPackedSize(mSM, mMhaType, S); + TRT_UNUSED packedSize; + PLUGIN_ASSERT(outputs[1].dims.d[1] == -1 || outputs[1].dims.d[1] == packedSize); + } + } - PLUGIN_ASSERT(inputs[0].desc.dims.nbDims == 2); - int32_t const S = inputs[0].desc.dims.d[SDIM]; - mS = S; - int32_t const B = inputs[0].desc.dims.d[BDIM]; - TRT_UNUSED B; - PLUGIN_ASSERT(mS == static_cast(inputs[1].desc.dims.d[SDIM])); - PLUGIN_ASSERT(B == inputs[1].desc.dims.d[BDIM]); - PLUGIN_ASSERT(mS == static_cast(inputs[2].desc.dims.d[SDIM])); - PLUGIN_ASSERT(B == inputs[2].desc.dims.d[BDIM]); - - PLUGIN_ASSERT(outputs[0].desc.dims.nbDims == 5); - PLUGIN_ASSERT(static_cast(outputs[0].desc.dims.d[SDIM]) == mS); - PLUGIN_ASSERT(outputs[0].desc.dims.d[BDIM] == B); - PLUGIN_ASSERT(static_cast(outputs[0].desc.dims.d[2]) == mLd); - PLUGIN_ASSERT(outputs[0].desc.dims.d[3] == 1); - PLUGIN_ASSERT(outputs[0].desc.dims.d[4] == 1); - - if (mUseFullMask) - { - // user force full_mask - PLUGIN_ASSERT(outputs[1].desc.dims.nbDims == 2); - PLUGIN_ASSERT(outputs[1].desc.dims.d[0] == B); - PLUGIN_ASSERT((outputs[1].desc.dims.d[1] == -1) || (outputs[1].desc.dims.d[1] == packedMaskSize384) - || (outputs[1].desc.dims.d[1] == packedMaskSize128)); + PLUGIN_ASSERT(inputs[0].type == DataType::kINT32); + PLUGIN_ASSERT(inputs[1].type == DataType::kINT32); + PLUGIN_ASSERT(inputs[2].type == DataType::kINT32); + PLUGIN_ASSERT(outputs[0].type == mType); + PLUGIN_ASSERT(outputs[1].type == DataType::kINT32); + return pluginStatus_t::STATUS_SUCCESS; } - else + catch (std::exception const& e) { - // auto detect using mhatype - if (S != -1 && B != -1) - { - PLUGIN_ASSERT(outputs[1].desc.dims.nbDims == 2); - PLUGIN_ASSERT(outputs[1].desc.dims.d[0] == B); - int32_t packedSize = getMHAMaskPackedSize(mSM, mMhaType, S); - TRT_UNUSED packedSize; - PLUGIN_ASSERT(outputs[1].desc.dims.d[1] == -1 || outputs[1].desc.dims.d[1] == packedSize); - } + caughtError(e); } - - PLUGIN_ASSERT(inputs[0].desc.type == DataType::kINT32); - PLUGIN_ASSERT(inputs[1].desc.type == DataType::kINT32); - PLUGIN_ASSERT(inputs[2].desc.type == DataType::kINT32); - PLUGIN_ASSERT(outputs[0].desc.type == mType); - PLUGIN_ASSERT(outputs[1].desc.type == DataType::kINT32); + return pluginStatus_t::STATUS_FAILURE; } -size_t EmbLayerNormPluginDynamic::getWorkspaceSize( - PluginTensorDesc const* inputs, int32_t nbInputs, PluginTensorDesc const* outputs, int32_t nbOutputs) const noexcept +IPluginV3* EmbLayerNormPluginDynamic::attachToContext(IPluginResourceContext* context) noexcept { - return 0; + return clone(); } int32_t EmbLayerNormPluginDynamic::enqueue(PluginTensorDesc const* inputDesc, PluginTensorDesc const* /* outputDesc */, @@ -394,92 +338,185 @@ int32_t EmbLayerNormPluginDynamic::enqueue(PluginTensorDesc const* inputDesc, Pl return STATUS_FAILURE; } -// IPluginV2Ext Methods -DataType EmbLayerNormPluginDynamic::getOutputDataType( - int32_t index, DataType const* inputTypes, int32_t nbInputs) const noexcept -{ +// end IPluginV3OneRuntime method definitions - PLUGIN_ASSERT(index == 0 || index == 1); - if (index == 0) - { - PLUGIN_ASSERT(mType == DataType::kHALF || mType == DataType::kFLOAT); - return mType; - } - return DataType::kINT32; -} +/////// +// IPluginV3OneBuild method definitions +// - getNbOutputs() +// - supportsFormatCombination() +// - getOutputShapes +// - getOutputDataTypes() +// - configurePlugin() +// - getWorkSpaceSize() +////// -// IPluginV2 Methods -char const* EmbLayerNormPluginDynamic::getPluginType() const noexcept +int32_t EmbLayerNormPluginDynamic::getNbOutputs() const noexcept { - return EMB_LAYER_NORM_NAME; + return 2; } -char const* EmbLayerNormPluginDynamic::getPluginVersion() const noexcept +bool EmbLayerNormPluginDynamic::supportsFormatCombination( + int32_t pos, DynamicPluginTensorDesc const* inOut, int32_t nbInputs, int32_t nbOutputs) noexcept { - return EMB_LAYER_NORM_VERSION; + // 3 inputs of size BxS + PLUGIN_ASSERT(nbInputs == 3); + PLUGIN_ASSERT(nbOutputs == 2); + + PluginTensorDesc const& desc = inOut[pos].desc; + if (desc.format != TensorFormat::kLINEAR) + { + return false; + } + if (pos == 0) + { + return desc.type == DataType::kINT32 && desc.dims.nbDims == 2; + } + + PluginTensorDesc const& prev = inOut[pos - 1].desc; + if (pos == 1 || pos == 2) + { + return desc.type == DataType::kINT32 && desc.dims.nbDims == 2 && desc.dims.d[BDIM] == prev.dims.d[BDIM] + && desc.dims.d[SDIM] == prev.dims.d[SDIM]; + } + + // embedded sequence + if (pos == 3) + { + return desc.type == mType && desc.dims.nbDims == 5 && desc.dims.d[BDIM] == prev.dims.d[BDIM] + && desc.dims.d[SDIM] == prev.dims.d[SDIM] && desc.dims.d[3] == 1 && desc.dims.d[4] == 1; + } + // mask + return desc.type == DataType::kINT32; } -int32_t EmbLayerNormPluginDynamic::getNbOutputs() const noexcept +int32_t EmbLayerNormPluginDynamic::getOutputShapes(DimsExprs const* inputs, int32_t nbInputs, + DimsExprs const* shapeInputs, int32_t nbShapeInputs, DimsExprs* outputs, int32_t nbOutputs, + IExprBuilder& exprBuilder) noexcept { - return 2; + try + { + // Input should be input ids and token ids and the input mask + // Output should be the embeddings tensor and mask indices + PLUGIN_ASSERT(nbInputs == 3); + PLUGIN_ASSERT(inputs != nullptr); + PLUGIN_ASSERT(inputs[0].nbDims == 2); // BxS + PLUGIN_ASSERT(inputs[0].nbDims == inputs[1].nbDims); + PLUGIN_ASSERT(inputs[0].nbDims == inputs[2].nbDims); + + PLUGIN_ASSERT(nbOutputs == 2); + PLUGIN_ASSERT(outputs != nullptr); + + // output 0: embeddings tensor + outputs[0].nbDims = 5; + outputs[0].d[0] = inputs[0].d[0]; + outputs[0].d[1] = inputs[0].d[1]; + outputs[0].d[2] = exprBuilder.constant(mLd); + outputs[0].d[3] = exprBuilder.constant(1); + outputs[0].d[4] = exprBuilder.constant(1); + + // output 1: mask indices + outputs[1].nbDims = 2; + outputs[1].d[0] = inputs[0].d[BDIM]; + auto cms0 = exprBuilder.constant(unfusedMaskSize); + + // this code must match getMHAMaskPackedSize in bertCommon.h + bool const isSmOK + = (mSM == kSM_75 || mSM == kSM_80 || mSM == kSM_86 || mSM == kSM_87 || mSM == kSM_89 || mSM == kSM_90); + bool const isPrecisionOK = (mMhaType == nvinfer1::DataType::kHALF || mMhaType == nvinfer1::DataType::kINT8); + if (mUseFullMask || (isSmOK && isPrecisionOK)) + { + // support 128, 384 in both int8 and fp16 + auto cms128 = exprBuilder.constant(packedMaskSize128); + auto cms384 = exprBuilder.constant(packedMaskSize384); + auto c128 = exprBuilder.constant(128); + auto c384 = exprBuilder.constant(384); + auto is128 = exprBuilder.operation(DimensionOperation::kEQUAL, *inputs[0].d[SDIM], *c128); + auto is384 = exprBuilder.operation(DimensionOperation::kEQUAL, *inputs[0].d[SDIM], *c384); + auto sel128 = exprBuilder.operation(DimensionOperation::kPROD, *is128, *cms128); + auto sel384 = exprBuilder.operation(DimensionOperation::kPROD, *is384, *cms384); + auto maskSize = exprBuilder.operation(DimensionOperation::kSUM, *sel384, *sel128); + + // support 64, 96 in both int8 and fp16 + auto cms64 = exprBuilder.constant(packedMaskSize64); + auto cms96 = exprBuilder.constant(packedMaskSize96); + auto c64 = exprBuilder.constant(64); + auto c96 = exprBuilder.constant(96); + + auto is64 = exprBuilder.operation(DimensionOperation::kEQUAL, *inputs[0].d[SDIM], *c64); + auto is96 = exprBuilder.operation(DimensionOperation::kEQUAL, *inputs[0].d[SDIM], *c96); + auto sel64 = exprBuilder.operation(DimensionOperation::kPROD, *is64, *cms64); + auto sel96 = exprBuilder.operation(DimensionOperation::kPROD, *is96, *cms96); + auto maskSize2 = exprBuilder.operation(DimensionOperation::kSUM, *sel64, *sel96); + maskSize = exprBuilder.operation(DimensionOperation::kSUM, *maskSize, *maskSize2); + + auto is0 = exprBuilder.operation(DimensionOperation::kEQUAL, *maskSize, *exprBuilder.constant(0)); + auto sel0 = exprBuilder.operation(DimensionOperation::kPROD, *is0, *cms0); + auto combinedMaskSize = exprBuilder.operation(DimensionOperation::kSUM, *maskSize, *sel0); + outputs[1].d[1] = combinedMaskSize; + } + else + { + outputs[1].d[1] = cms0; + } + return pluginStatus_t::STATUS_SUCCESS; + } + catch (std::exception const& e) + { + caughtError(e); + } + return pluginStatus_t::STATUS_FAILURE; } -int32_t EmbLayerNormPluginDynamic::initialize() noexcept +int32_t EmbLayerNormPluginDynamic::getOutputDataTypes( + DataType* outputTypes, int32_t nbOutputs, DataType const* inputTypes, int32_t nbInputs) const noexcept { - return 0; + try + { + PLUGIN_ASSERT(outputTypes != nullptr); + PLUGIN_ASSERT(nbOutputs == 2); + PLUGIN_ASSERT(inputTypes != nullptr); + PLUGIN_ASSERT(nbInputs == 3); + PLUGIN_ASSERT(mType == DataType::kHALF || mType == DataType::kFLOAT); + outputTypes[0] = mType; + outputTypes[1] = DataType::kINT32; + return pluginStatus_t::STATUS_SUCCESS; + } + catch (std::exception const& e) + { + caughtError(e); + } + return pluginStatus_t::STATUS_FAILURE; } -void EmbLayerNormPluginDynamic::terminate() noexcept +int32_t EmbLayerNormPluginDynamic::configurePlugin(DynamicPluginTensorDesc const* inputs, int32_t nbInputs, + DynamicPluginTensorDesc const* outputs, int32_t nbOutputs) noexcept { - BERT_DEBUG_MSG("EmbLayerNormPluginDynamic terminate."); + return pluginStatus_t::STATUS_SUCCESS; } -size_t EmbLayerNormPluginDynamic::getSerializationSize() const noexcept +size_t EmbLayerNormPluginDynamic::getWorkspaceSize(DynamicPluginTensorDesc const* inputs, int32_t nbInputs, + DynamicPluginTensorDesc const* outputs, int32_t nbOutputs) const noexcept { - size_t const wordSize = getElementSize(mType); - return sizeof(mType) // type - + sizeof(mMhaType) // mha plugin datatype - + sizeof(mLd) * 5 // mLd, mS, m*VocabSize - + sizeof(mUseFullMask) // mask type - + sizeof(mSM) // smversion - + 2 * sizeof(float) * mLd // beta + gamma - + wordSize * mLd * mWordVocabSize // word emb - + wordSize * mLd * mPosVocabSize // pos emb - + wordSize * mLd * mTokVocabSize // tok emb - ; + return 0; } -void EmbLayerNormPluginDynamic::serialize(void* buffer) const noexcept +// End IPluginV3OneBuild method definitions + +////// +// IPluginV3OneCore method definitions +// - getPluginVersion() +// - getPluginName() +// - getPluginNamespace() +// - setPluginNamespace() +////// +char const* EmbLayerNormPluginDynamic::getPluginVersion() const noexcept { - serialize_value(&buffer, mType); - serialize_value(&buffer, mMhaType); - serialize_value(&buffer, mLd); - serialize_value(&buffer, mS); - serialize_value(&buffer, mWordVocabSize); - serialize_value(&buffer, mPosVocabSize); - serialize_value(&buffer, mTokVocabSize); - serialize_value(&buffer, mUseFullMask); - serialize_value(&buffer, mSM); - - char* d = static_cast(buffer); - serFromDev(d, mBetaDev.get(), mLd); - serFromDev(d, mGammaDev.get(), mLd); - size_t const wordSize = getElementSize(mType); - serFromDev(d, static_cast(mWordEmbDev.get()), mLd * mWordVocabSize * wordSize); - serFromDev(d, static_cast(mPosEmbDev.get()), mLd * mPosVocabSize * wordSize); - serFromDev(d, static_cast(mTokEmbDev.get()), mLd * mTokVocabSize * wordSize); + return gEmbLayerNormVersion; } -void EmbLayerNormPluginDynamic::destroy() noexcept +char const* EmbLayerNormPluginDynamic::getPluginName() const noexcept { - BERT_DEBUG_MSG("EmbLayerNormPluginDynamic destroy."); - // This gets called when the network containing plugin is destroyed - mGammaDev.reset(nullptr); - mBetaDev.reset(nullptr); - mWordEmbDev.reset(nullptr); - mPosEmbDev.reset(nullptr); - mTokEmbDev.reset(nullptr); - delete this; + return gEmbLayerNormName; } void EmbLayerNormPluginDynamic::setPluginNamespace(char const* libNamespace) noexcept @@ -499,10 +536,14 @@ char const* EmbLayerNormPluginDynamic::getPluginNamespace() const noexcept return mNamespace.c_str(); } -/////////////////////// +// End IPluginV3OneCore method definitions + +//////////////////////////// Plugin Creator member definitions ///////////////////////////// EmbLayerNormPluginDynamicCreator::EmbLayerNormPluginDynamicCreator() { + static std::mutex sMutex; + std::lock_guard lock(sMutex); mPluginAttributes.clear(); mPluginAttributes.emplace_back(PluginField("bert_embeddings_layernorm_beta")); mPluginAttributes.emplace_back(PluginField("bert_embeddings_layernorm_gamma")); @@ -518,12 +559,12 @@ EmbLayerNormPluginDynamicCreator::EmbLayerNormPluginDynamicCreator() char const* EmbLayerNormPluginDynamicCreator::getPluginName() const noexcept { - return EMB_LAYER_NORM_NAME; + return gEmbLayerNormName; } char const* EmbLayerNormPluginDynamicCreator::getPluginVersion() const noexcept { - return EMB_LAYER_NORM_VERSION; + return gEmbLayerNormVersion; } PluginFieldCollection const* EmbLayerNormPluginDynamicCreator::getFieldNames() noexcept @@ -531,7 +572,8 @@ PluginFieldCollection const* EmbLayerNormPluginDynamicCreator::getFieldNames() n return &mFC; } -IPluginV2* EmbLayerNormPluginDynamicCreator::createPlugin(char const* name, PluginFieldCollection const* fc) noexcept +IPluginV3* EmbLayerNormPluginDynamicCreator::createPlugin( + char const* name, PluginFieldCollection const* fc, TensorRTPhase phase) noexcept { try { @@ -630,22 +672,6 @@ IPluginV2* EmbLayerNormPluginDynamicCreator::createPlugin(char const* name, Plug return nullptr; } -IPluginV2* EmbLayerNormPluginDynamicCreator::deserializePlugin( - char const* name, void const* serialData, size_t serialLength) noexcept -{ - try - { - // This object will be deleted when the network is destroyed, which will - // call EmbLayerNormPluginDynamic::destroy() - return new EmbLayerNormPluginDynamic(name, serialData, serialLength); - } - catch (std::exception const& e) - { - caughtError(e); - } - return nullptr; -} - void EmbLayerNormPluginDynamicCreator::setPluginNamespace(char const* libNamespace) noexcept { try diff --git a/plugin/embLayerNormPlugin/embLayerNormPlugin.h b/plugin/embLayerNormPlugin/embLayerNormPlugin.h index 5eb40958..06c2bb11 100644 --- a/plugin/embLayerNormPlugin/embLayerNormPlugin.h +++ b/plugin/embLayerNormPlugin/embLayerNormPlugin.h @@ -45,52 +45,76 @@ int32_t embSkipLayerNorm(cudaStream_t stream, int32_t ld, int32_t B, int32_t S, cudaError_t convertMask(uint32_t const S, uint32_t const B, uint32_t const warps_m, uint32_t const warps_n, uint32_t const warps_k, int32_t const* inputMaskSB, uint32_t* inputMaskX, cudaStream_t stream); -class EmbLayerNormPluginDynamic : public nvinfer1::IPluginV2DynamicExt +class EmbLayerNormPluginDynamic : public IPluginV3, + public IPluginV3OneCore, + public IPluginV3OneBuild, + public IPluginV3OneRuntime { public: EmbLayerNormPluginDynamic(std::string const& name, nvinfer1::DataType const type, nvinfer1::DataType const mhaType, nvinfer1::Weights const& beta, nvinfer1::Weights const& gamma, nvinfer1::Weights const& word_emb, nvinfer1::Weights const& pos_emb, nvinfer1::Weights const& tok_emb, bool const useFullMask); - EmbLayerNormPluginDynamic(std::string const& name, void const* data, size_t length); - // It doesn't make sense to make EmbLayerNormPluginDynamic without arguments, so we // delete default constructor. EmbLayerNormPluginDynamic() = delete; - // IPluginV2DynamicExt Methods - nvinfer1::IPluginV2DynamicExt* clone() const noexcept override; - nvinfer1::DimsExprs getOutputDimensions(int32_t outputIndex, nvinfer1::DimsExprs const* inputs, int32_t nbInputs, - nvinfer1::IExprBuilder& exprBuilder) noexcept override; + ~EmbLayerNormPluginDynamic() override; + + // IPluginV3 Methods + // NOTE: since this is itself is an abstract class, the rest of virtual methods defined in its children classes + IPluginCapability* getCapabilityInterface(PluginCapabilityType type) noexcept override; + // end of IPluginV3 Methods + + // IPluginV3OneCore Methods + char const* getPluginName() const noexcept override; + + char const* getPluginNamespace() const noexcept override; + + void setPluginNamespace(char const* pluginNamespace) noexcept; + + char const* getPluginVersion() const noexcept override; + // end of IPluginV3OneCore Methods + + // IPluginV3Build Methods bool supportsFormatCombination( - int32_t pos, nvinfer1::PluginTensorDesc const* inOut, int32_t nbInputs, int32_t nbOutputs) noexcept override; - void configurePlugin(nvinfer1::DynamicPluginTensorDesc const* in, int32_t nbInputs, - nvinfer1::DynamicPluginTensorDesc const* out, int32_t nbOutputs) noexcept override; - size_t getWorkspaceSize(nvinfer1::PluginTensorDesc const* inputs, int32_t nbInputs, - nvinfer1::PluginTensorDesc const* outputs, int32_t nbOutputs) const noexcept override; + int32_t pos, DynamicPluginTensorDesc const* inOut, int32_t nbInputs, int32_t nbOutputs) noexcept override; + + int32_t configurePlugin(DynamicPluginTensorDesc const* in, int32_t nbInputs, DynamicPluginTensorDesc const* out, + int32_t nbOutputs) noexcept override; + + size_t getWorkspaceSize(DynamicPluginTensorDesc const* inputs, int32_t nbInputs, + DynamicPluginTensorDesc const* outputs, int32_t nbOutputs) const noexcept override; + + int32_t getOutputDataTypes( + DataType* outputTypes, int32_t nbOutputs, DataType const* inputTypes, int32_t nbInputs) const noexcept override; + + int32_t getNbOutputs() const noexcept override; + + int32_t getOutputShapes(DimsExprs const* inputs, int32_t nbInputs, DimsExprs const* shapeInputs, + int32_t nbShapeInputs, DimsExprs* outputs, int32_t nbOutputs, IExprBuilder& exprBuilder) noexcept override; + // end IPluginV3Build Methods + + // IPluginV3Runtime Methods + IPluginV3* clone() noexcept; + + int32_t onShapeChange( + PluginTensorDesc const* in, int32_t nbInputs, PluginTensorDesc const* out, int32_t nbOutputs) noexcept override; + int32_t enqueue(nvinfer1::PluginTensorDesc const* inputDesc, nvinfer1::PluginTensorDesc const* outputDesc, void const* const* inputs, void* const* outputs, void* workspace, cudaStream_t stream) noexcept override; - // IPluginV2Ext Methods - nvinfer1::DataType getOutputDataType( - int32_t index, nvinfer1::DataType const* inputTypes, int32_t nbInputs) const noexcept override; + IPluginV3* attachToContext(IPluginResourceContext* context) noexcept override; - // IPluginV2 Methods - char const* getPluginType() const noexcept override; - char const* getPluginVersion() const noexcept override; - int32_t getNbOutputs() const noexcept override; - int32_t initialize() noexcept override; - void terminate() noexcept override; - size_t getSerializationSize() const noexcept override; - void serialize(void* buffer) const noexcept override; - void destroy() noexcept override; - void setPluginNamespace(char const* pluginNamespace) noexcept override; - char const* getPluginNamespace() const noexcept override; + PluginFieldCollection const* getFieldsToSerialize() noexcept override; + // end IPluginV3Runtime Methods private: + // metadata fields std::string const mLayerName; std::string mNamespace; + // device-side bert::cuda_unique_ptr mGammaDev; bert::cuda_unique_ptr mBetaDev; bert::cuda_unique_ptr mWordEmbDev; @@ -101,26 +125,29 @@ class EmbLayerNormPluginDynamic : public nvinfer1::IPluginV2DynamicExt size_t mWordVocabSize; size_t mPosVocabSize; size_t mTokVocabSize; + + // members that partcipate in ser/deserialization bert::WeightsWithOwnership mBeta; bert::WeightsWithOwnership mGamma; bert::WeightsWithOwnership mWordEmb; bert::WeightsWithOwnership mTokEmb; bert::WeightsWithOwnership mPosEmb; nvinfer1::DataType mType; - bool mUseFullMask; + int32_t mOutputFp16; + int32_t mUseFullMask; nvinfer1::DataType mMhaType; int32_t mSM; - using IPluginV2::getOutputDimensions; - using IPluginV2::getWorkspaceSize; - using IPluginV2::enqueue; - using IPluginV2Ext::configurePlugin; + // IPluginV3 serialization related + std::vector mDataToSerialize; + nvinfer1::PluginFieldCollection mFCToSerialize; }; -class EmbLayerNormPluginDynamicCreator : public nvinfer1::IPluginCreator +class EmbLayerNormPluginDynamicCreator : public nvinfer1::IPluginCreatorV3One { public: EmbLayerNormPluginDynamicCreator(); + ~EmbLayerNormPluginDynamicCreator() override = default; char const* getPluginName() const noexcept override; @@ -128,12 +155,9 @@ class EmbLayerNormPluginDynamicCreator : public nvinfer1::IPluginCreator nvinfer1::PluginFieldCollection const* getFieldNames() noexcept override; - nvinfer1::IPluginV2* createPlugin(char const* name, nvinfer1::PluginFieldCollection const* fc) noexcept override; - - nvinfer1::IPluginV2* deserializePlugin( - char const* name, void const* serialData, size_t serialLength) noexcept override; + IPluginV3* createPlugin(char const* name, PluginFieldCollection const* fc, TensorRTPhase phase) noexcept override; - void setPluginNamespace(char const* pluginNamespace) noexcept override; + void setPluginNamespace(char const* pluginNamespace) noexcept; char const* getPluginNamespace() const noexcept override; diff --git a/plugin/embLayerNormPlugin/embLayerNormPluginLegacy.cpp b/plugin/embLayerNormPlugin/embLayerNormPluginLegacy.cpp new file mode 100644 index 00000000..6c037189 --- /dev/null +++ b/plugin/embLayerNormPlugin/embLayerNormPluginLegacy.cpp @@ -0,0 +1,669 @@ +/* + * 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. + */ + +#include +#if CUDA_VERSION >= 10010 + +#include +#include +#include + +#include "NvInfer.h" +#include "common/serialize.hpp" +#include "embLayerNormPluginLegacy.h" + +using namespace nvinfer1; +using namespace nvinfer1::plugin; +using namespace nvinfer1::plugin::bert; + +namespace +{ +char const* gEmbLayerNormVersion{"1"}; +char const* gEmbLayerNormName{"CustomEmbLayerNormPluginDynamic"}; +} // namespace + +// Static class fields initialization +PluginFieldCollection EmbLayerNormPluginDynamicLegacyCreator::mFC{}; +std::vector EmbLayerNormPluginDynamicLegacyCreator::mPluginAttributes; + +REGISTER_TENSORRT_PLUGIN(EmbLayerNormPluginDynamicLegacyCreator); + +EmbLayerNormPluginDynamicLegacy::EmbLayerNormPluginDynamicLegacy(std::string const& name, DataType const type, + DataType const mhaType, Weights const& beta, Weights const& gamma, Weights const& wordEmb, Weights const& posEmb, + Weights const& tokEmb, bool const useFullMask) + : mLayerName(name) + , mLd(beta.count) + , mType(type) + , mUseFullMask(useFullMask) + , mMhaType(mhaType) +{ + // Assuming Weights.count is the number of elements and not bytes + PLUGIN_VALIDATE(beta.count == gamma.count); + PLUGIN_VALIDATE(mLd > 0U); + PLUGIN_VALIDATE(wordEmb.count % mLd == 0); + PLUGIN_VALIDATE(posEmb.count % mLd == 0); + PLUGIN_VALIDATE(tokEmb.count % mLd == 0); + mWordVocabSize = wordEmb.count / mLd; + mPosVocabSize = posEmb.count / mLd; + mTokVocabSize = tokEmb.count / mLd; + mSM = getSMVersion(); + // mS is set during configure + + mBeta.convertAndCopy(beta, nvinfer1::DataType::kFLOAT); + mGamma.convertAndCopy(gamma, nvinfer1::DataType::kFLOAT); + mWordEmb.convertAndCopy(wordEmb, mType); + mTokEmb.convertAndCopy(tokEmb, mType); + mPosEmb.convertAndCopy(posEmb, mType); + + copyToDevice(mGamma, sizeof(float) * mGamma.count, mGammaDev); + copyToDevice(mBeta, sizeof(float) * mBeta.count, mBetaDev); + copyToDevice(mWordEmb, getWeightsSize(mWordEmb, mType), mWordEmbDev); + copyToDevice(mPosEmb, getWeightsSize(mPosEmb, mType), mPosEmbDev); + copyToDevice(mTokEmb, getWeightsSize(mTokEmb, mType), mTokEmbDev); +} + +EmbLayerNormPluginDynamicLegacy::EmbLayerNormPluginDynamicLegacy( + std::string const& name, void const* data, size_t length) + : mLayerName(name) + , mGammaDev(nullptr) + , mBetaDev(nullptr) + , mWordEmbDev(nullptr) + , mTokEmbDev(nullptr) + , mPosEmbDev(nullptr) +{ + BERT_DEBUG_MSG("EmbLayerNormPluginDynamicLegacy deserialize."); + + // Deserialize in the same order as serialization + deserialize_value(&data, &length, &mType); + deserialize_value(&data, &length, &mMhaType); + deserialize_value(&data, &length, &mLd); + deserialize_value(&data, &length, &mS); + deserialize_value(&data, &length, &mWordVocabSize); + deserialize_value(&data, &length, &mPosVocabSize); + deserialize_value(&data, &length, &mTokVocabSize); + deserialize_value(&data, &length, &mUseFullMask); + deserialize_value(&data, &length, &mSM); + + char const* d = static_cast(data); + mBeta.convertAndCopy(d, mLd, nvinfer1::DataType::kFLOAT); + mGamma.convertAndCopy(d, mLd, nvinfer1::DataType::kFLOAT); + mWordEmb.convertAndCopy(d, mLd * mWordVocabSize, mType); + mPosEmb.convertAndCopy(d, mLd * mPosVocabSize, mType); + mTokEmb.convertAndCopy(d, mLd * mTokVocabSize, mType); + + copyToDevice(mGamma, sizeof(float) * mGamma.count, mGammaDev); + copyToDevice(mBeta, sizeof(float) * mBeta.count, mBetaDev); + copyToDevice(mWordEmb, getWeightsSize(mWordEmb, mType), mWordEmbDev); + copyToDevice(mPosEmb, getWeightsSize(mPosEmb, mType), mPosEmbDev); + copyToDevice(mTokEmb, getWeightsSize(mTokEmb, mType), mTokEmbDev); +} + +// IPluginV2DynamicExt Methods +IPluginV2DynamicExt* EmbLayerNormPluginDynamicLegacy::clone() const noexcept +{ + try + { + BERT_DEBUG_MSG("EmbLayerNormPluginDynamicLegacy clone."); + + auto p = new EmbLayerNormPluginDynamicLegacy( + mLayerName, mType, mMhaType, mBeta, mGamma, mWordEmb, mPosEmb, mTokEmb, mUseFullMask); + p->mS = mS; + p->setPluginNamespace(mNamespace.c_str()); + + return p; + } + catch (std::exception const& e) + { + caughtError(e); + } + return nullptr; +} + +DimsExprs EmbLayerNormPluginDynamicLegacy::getOutputDimensions( + int32_t outputIndex, DimsExprs const* inputs, int32_t nbInputs, IExprBuilder& exprBuilder) noexcept +{ + try + { + // Input should be input ids and token ids and the input mask + // Output should be the embeddings tensor and mask indices + PLUGIN_ASSERT(nbInputs == 3); + + PLUGIN_ASSERT(inputs[0].nbDims == 2); // BxS + PLUGIN_ASSERT(inputs[0].nbDims == inputs[1].nbDims); + PLUGIN_ASSERT(inputs[0].nbDims == inputs[2].nbDims); + + PLUGIN_ASSERT(outputIndex == 0 || outputIndex == 1); + + if (outputIndex == 0) + { + DimsExprs ret; + ret.nbDims = 5; + ret.d[0] = inputs[0].d[0]; + ret.d[1] = inputs[0].d[1]; + ret.d[2] = exprBuilder.constant(mLd); + ret.d[3] = exprBuilder.constant(1); + ret.d[4] = exprBuilder.constant(1); + return ret; + } + + DimsExprs ret; + ret.nbDims = 2; + ret.d[0] = inputs[0].d[BDIM]; + auto cms0 = exprBuilder.constant(unfusedMaskSize); + + // this code must match getMHAMaskPackedSize in bertCommon.h + bool const isSmOK + = (mSM == kSM_75 || mSM == kSM_80 || mSM == kSM_86 || mSM == kSM_87 || mSM == kSM_89 || mSM == kSM_90); + bool const isPrecisionOK = (mMhaType == nvinfer1::DataType::kHALF || mMhaType == nvinfer1::DataType::kINT8); + if (mUseFullMask || (isSmOK && isPrecisionOK)) + { + // support 128, 384 in both int8 and fp16 + auto cms128 = exprBuilder.constant(packedMaskSize128); + auto cms384 = exprBuilder.constant(packedMaskSize384); + auto c128 = exprBuilder.constant(128); + auto c384 = exprBuilder.constant(384); + auto is128 = exprBuilder.operation(DimensionOperation::kEQUAL, *inputs[0].d[SDIM], *c128); + auto is384 = exprBuilder.operation(DimensionOperation::kEQUAL, *inputs[0].d[SDIM], *c384); + auto sel128 = exprBuilder.operation(DimensionOperation::kPROD, *is128, *cms128); + auto sel384 = exprBuilder.operation(DimensionOperation::kPROD, *is384, *cms384); + auto maskSize = exprBuilder.operation(DimensionOperation::kSUM, *sel384, *sel128); + + // support 64, 96 in both int8 and fp16 + auto cms64 = exprBuilder.constant(packedMaskSize64); + auto cms96 = exprBuilder.constant(packedMaskSize96); + auto c64 = exprBuilder.constant(64); + auto c96 = exprBuilder.constant(96); + + auto is64 = exprBuilder.operation(DimensionOperation::kEQUAL, *inputs[0].d[SDIM], *c64); + auto is96 = exprBuilder.operation(DimensionOperation::kEQUAL, *inputs[0].d[SDIM], *c96); + auto sel64 = exprBuilder.operation(DimensionOperation::kPROD, *is64, *cms64); + auto sel96 = exprBuilder.operation(DimensionOperation::kPROD, *is96, *cms96); + auto maskSize2 = exprBuilder.operation(DimensionOperation::kSUM, *sel64, *sel96); + maskSize = exprBuilder.operation(DimensionOperation::kSUM, *maskSize, *maskSize2); + + auto is0 = exprBuilder.operation(DimensionOperation::kEQUAL, *maskSize, *exprBuilder.constant(0)); + auto sel0 = exprBuilder.operation(DimensionOperation::kPROD, *is0, *cms0); + auto combinedMaskSize = exprBuilder.operation(DimensionOperation::kSUM, *maskSize, *sel0); + ret.d[1] = combinedMaskSize; + } + else + { + ret.d[1] = cms0; + } + + return ret; + } + catch (std::exception const& e) + { + caughtError(e); + } + return DimsExprs{}; +} + +bool EmbLayerNormPluginDynamicLegacy::supportsFormatCombination( + int32_t pos, PluginTensorDesc const* inOut, int32_t nbInputs, int32_t nbOutputs) noexcept +{ + // 3 inputs of size BxS + PLUGIN_ASSERT(nbInputs == 3); + PLUGIN_ASSERT(nbOutputs == 2); + + PluginTensorDesc const& desc = inOut[pos]; + if (desc.format != TensorFormat::kLINEAR) + { + return false; + } + if (pos == 0) + { + return desc.type == DataType::kINT32 && desc.dims.nbDims == 2; + } + + PluginTensorDesc const& prev = inOut[pos - 1]; + if (pos == 1 || pos == 2) + { + return desc.type == DataType::kINT32 && desc.dims.nbDims == 2 && desc.dims.d[BDIM] == prev.dims.d[BDIM] + && desc.dims.d[SDIM] == prev.dims.d[SDIM]; + } + + // embedded sequence + if (pos == 3) + { + return desc.type == mType && desc.dims.nbDims == 5 && desc.dims.d[BDIM] == prev.dims.d[BDIM] + && desc.dims.d[SDIM] == prev.dims.d[SDIM] && desc.dims.d[3] == 1 && desc.dims.d[4] == 1; + } + // mask + return desc.type == DataType::kINT32; +} + +void EmbLayerNormPluginDynamicLegacy::configurePlugin(DynamicPluginTensorDesc const* inputs, int32_t nbInputs, + DynamicPluginTensorDesc const* outputs, int32_t nbOutputs) noexcept +{ + BERT_DEBUG_MSG("EmbLayerNormPluginDynamicLegacy configurePlugin."); + + // Validate input arguments + PLUGIN_ASSERT(nbOutputs == 2); + PLUGIN_ASSERT(nbInputs == 3); + + PLUGIN_ASSERT(inputs[0].desc.dims.nbDims == 2); + int32_t const S = inputs[0].desc.dims.d[SDIM]; + mS = S; + int32_t const B = inputs[0].desc.dims.d[BDIM]; + TRT_UNUSED B; + PLUGIN_ASSERT(mS == static_cast(inputs[1].desc.dims.d[SDIM])); + PLUGIN_ASSERT(B == inputs[1].desc.dims.d[BDIM]); + PLUGIN_ASSERT(mS == static_cast(inputs[2].desc.dims.d[SDIM])); + PLUGIN_ASSERT(B == inputs[2].desc.dims.d[BDIM]); + + PLUGIN_ASSERT(outputs[0].desc.dims.nbDims == 5); + PLUGIN_ASSERT(static_cast(outputs[0].desc.dims.d[SDIM]) == mS); + PLUGIN_ASSERT(outputs[0].desc.dims.d[BDIM] == B); + PLUGIN_ASSERT(static_cast(outputs[0].desc.dims.d[2]) == mLd); + PLUGIN_ASSERT(outputs[0].desc.dims.d[3] == 1); + PLUGIN_ASSERT(outputs[0].desc.dims.d[4] == 1); + + if (mUseFullMask) + { + // user force full_mask + PLUGIN_ASSERT(outputs[1].desc.dims.nbDims == 2); + PLUGIN_ASSERT(outputs[1].desc.dims.d[0] == B); + PLUGIN_ASSERT((outputs[1].desc.dims.d[1] == -1) || (outputs[1].desc.dims.d[1] == packedMaskSize384) + || (outputs[1].desc.dims.d[1] == packedMaskSize128)); + } + else + { + // auto detect using mhatype + if (S != -1 && B != -1) + { + PLUGIN_ASSERT(outputs[1].desc.dims.nbDims == 2); + PLUGIN_ASSERT(outputs[1].desc.dims.d[0] == B); + int32_t packedSize = getMHAMaskPackedSize(mSM, mMhaType, S); + TRT_UNUSED packedSize; + PLUGIN_ASSERT(outputs[1].desc.dims.d[1] == -1 || outputs[1].desc.dims.d[1] == packedSize); + } + } + + PLUGIN_ASSERT(inputs[0].desc.type == DataType::kINT32); + PLUGIN_ASSERT(inputs[1].desc.type == DataType::kINT32); + PLUGIN_ASSERT(inputs[2].desc.type == DataType::kINT32); + PLUGIN_ASSERT(outputs[0].desc.type == mType); + PLUGIN_ASSERT(outputs[1].desc.type == DataType::kINT32); +} + +size_t EmbLayerNormPluginDynamicLegacy::getWorkspaceSize( + PluginTensorDesc const* inputs, int32_t nbInputs, PluginTensorDesc const* outputs, int32_t nbOutputs) const noexcept +{ + return 0; +} + +int32_t EmbLayerNormPluginDynamicLegacy::enqueue(PluginTensorDesc const* inputDesc, + PluginTensorDesc const* /* outputDesc */, void const* const* inputs, void* const* outputs, void* /* workspace */, + cudaStream_t stream) noexcept +{ + try + { + PLUGIN_VALIDATE(inputDesc != nullptr && inputs != nullptr && outputs != nullptr); + + int32_t const batchSize = inputDesc->dims.d[BDIM]; + int32_t const S = inputDesc->dims.d[SDIM]; + int32_t status = STATUS_FAILURE; + + // Our plugin outputs only one tensor + auto const inputIds = static_cast(inputs[0]); + auto const segmentIds = static_cast(inputs[1]); + auto const inputMask = static_cast(inputs[2]); + + float const* beta = mBetaDev.get(); + float const* gamma = mGammaDev.get(); + if (mType == DataType::kFLOAT) + { + auto output = static_cast(outputs[0]); + auto const wordEmb = static_cast(mWordEmbDev.get()); + auto const tokEmb = static_cast(mTokEmbDev.get()); + auto const posEmb = static_cast(mPosEmbDev.get()); + status = embSkipLayerNorm(stream, static_cast(mLd), batchSize, S, inputIds, segmentIds, + beta, gamma, wordEmb, posEmb, tokEmb, mWordVocabSize, mTokVocabSize, output); + + if (status != cudaSuccess) + { + return status; + } + } + else if (mType == DataType::kHALF) + { + auto output = static_cast(outputs[0]); + auto const wordEmb = static_cast(mWordEmbDev.get()); + auto const tokEmb = static_cast(mTokEmbDev.get()); + auto const posEmb = static_cast(mPosEmbDev.get()); + status = embSkipLayerNorm(stream, static_cast(mLd), batchSize, S, inputIds, segmentIds, beta, + gamma, wordEmb, posEmb, tokEmb, mWordVocabSize, mTokVocabSize, output); + + if (status != cudaSuccess) + { + return status; + } + } + else + { + gLogError << "Unsupported type error, expected [kHALF,kFLOAT], but received " << static_cast(mType) + << std::endl; + + return STATUS_NOT_SUPPORTED; + } + + // check mha use fused kernel + if (mUseFullMask || unfusedMaskSize != getMHAMaskPackedSize(mSM, mMhaType, S)) + { + size_t warps_m = 0, warps_n = 0, warps_k = 1; + if (S == 64 || S == 96 || S == 128) + { + warps_m = 2; + warps_n = 2; + } + else if (S == 384) + { + warps_m = 1; + warps_n = 8; + } + uint32_t* inputMaskX = static_cast(outputs[1]); + + status = convertMask(S, batchSize, warps_m, warps_n, warps_k, inputMask, inputMaskX, stream); + } + else + { + int32_t* maskIdx = static_cast(outputs[1]); + status = computeMaskIdx(stream, S, batchSize, inputMask, maskIdx); + } + + return status; + } + catch (std::exception const& e) + { + caughtError(e); + } + return STATUS_FAILURE; +} + +// IPluginV2Ext Methods +DataType EmbLayerNormPluginDynamicLegacy::getOutputDataType( + int32_t index, DataType const* inputTypes, int32_t nbInputs) const noexcept +{ + + PLUGIN_ASSERT(index == 0 || index == 1); + if (index == 0) + { + PLUGIN_ASSERT(mType == DataType::kHALF || mType == DataType::kFLOAT); + return mType; + } + return DataType::kINT32; +} + +// IPluginV2 Methods +char const* EmbLayerNormPluginDynamicLegacy::getPluginType() const noexcept +{ + return gEmbLayerNormName; +} + +char const* EmbLayerNormPluginDynamicLegacy::getPluginVersion() const noexcept +{ + return gEmbLayerNormVersion; +} + +int32_t EmbLayerNormPluginDynamicLegacy::getNbOutputs() const noexcept +{ + return 2; +} + +int32_t EmbLayerNormPluginDynamicLegacy::initialize() noexcept +{ + return 0; +} + +void EmbLayerNormPluginDynamicLegacy::terminate() noexcept +{ + BERT_DEBUG_MSG("EmbLayerNormPluginDynamicLegacy terminate."); +} + +size_t EmbLayerNormPluginDynamicLegacy::getSerializationSize() const noexcept +{ + size_t const wordSize = getElementSize(mType); + return sizeof(mType) // type + + sizeof(mMhaType) // mha plugin datatype + + sizeof(mLd) * 5 // mLd, mS, m*VocabSize + + sizeof(mUseFullMask) // mask type + + sizeof(mSM) // smversion + + 2 * sizeof(float) * mLd // beta + gamma + + wordSize * mLd * mWordVocabSize // word emb + + wordSize * mLd * mPosVocabSize // pos emb + + wordSize * mLd * mTokVocabSize // tok emb + ; +} + +void EmbLayerNormPluginDynamicLegacy::serialize(void* buffer) const noexcept +{ + serialize_value(&buffer, mType); + serialize_value(&buffer, mMhaType); + serialize_value(&buffer, mLd); + serialize_value(&buffer, mS); + serialize_value(&buffer, mWordVocabSize); + serialize_value(&buffer, mPosVocabSize); + serialize_value(&buffer, mTokVocabSize); + serialize_value(&buffer, mUseFullMask); + serialize_value(&buffer, mSM); + + char* d = static_cast(buffer); + serFromDev(d, mBetaDev.get(), mLd); + serFromDev(d, mGammaDev.get(), mLd); + size_t const wordSize = getElementSize(mType); + serFromDev(d, static_cast(mWordEmbDev.get()), mLd * mWordVocabSize * wordSize); + serFromDev(d, static_cast(mPosEmbDev.get()), mLd * mPosVocabSize * wordSize); + serFromDev(d, static_cast(mTokEmbDev.get()), mLd * mTokVocabSize * wordSize); +} + +void EmbLayerNormPluginDynamicLegacy::destroy() noexcept +{ + BERT_DEBUG_MSG("EmbLayerNormPluginDynamicLegacy destroy."); + // This gets called when the network containing plugin is destroyed + mGammaDev.reset(nullptr); + mBetaDev.reset(nullptr); + mWordEmbDev.reset(nullptr); + mPosEmbDev.reset(nullptr); + mTokEmbDev.reset(nullptr); + delete this; +} + +void EmbLayerNormPluginDynamicLegacy::setPluginNamespace(char const* libNamespace) noexcept +{ + try + { + mNamespace = libNamespace; + } + catch (std::exception const& e) + { + caughtError(e); + } +} + +char const* EmbLayerNormPluginDynamicLegacy::getPluginNamespace() const noexcept +{ + return mNamespace.c_str(); +} + +/////////////////////// + +EmbLayerNormPluginDynamicLegacyCreator::EmbLayerNormPluginDynamicLegacyCreator() +{ + mPluginAttributes.clear(); + mPluginAttributes.emplace_back(PluginField("bert_embeddings_layernorm_beta")); + mPluginAttributes.emplace_back(PluginField("bert_embeddings_layernorm_gamma")); + mPluginAttributes.emplace_back(PluginField("bert_embeddings_word_embeddings")); + mPluginAttributes.emplace_back(PluginField("bert_embeddings_token_type_embeddings")); + mPluginAttributes.emplace_back(PluginField("bert_embeddings_position_embeddings")); + mPluginAttributes.emplace_back(PluginField("output_fp16")); + mPluginAttributes.emplace_back(PluginField("full_mask")); + mPluginAttributes.emplace_back(PluginField("mha_type_id")); + mFC.nbFields = mPluginAttributes.size(); + mFC.fields = mPluginAttributes.data(); +} + +char const* EmbLayerNormPluginDynamicLegacyCreator::getPluginName() const noexcept +{ + return gEmbLayerNormName; +} + +char const* EmbLayerNormPluginDynamicLegacyCreator::getPluginVersion() const noexcept +{ + return gEmbLayerNormVersion; +} + +PluginFieldCollection const* EmbLayerNormPluginDynamicLegacyCreator::getFieldNames() noexcept +{ + return &mFC; +} + +IPluginV2* EmbLayerNormPluginDynamicLegacyCreator::createPlugin( + char const* name, PluginFieldCollection const* fc) noexcept +{ + try + { + BERT_DEBUG_MSG("EmbLayerNormPluginDynamicLegacy createPlugin."); + + bool output_fp16 = false; + bool useFullMask = false; + Weights beta{}; // required attribute - validateRequiredAttributesExist() will verify existence + Weights gamma{}; // required attribute - validateRequiredAttributesExist() will verify existence + Weights word_emb{}; // required attribute - validateRequiredAttributesExist() will verify existence + Weights pos_emb{}; // required attribute - validateRequiredAttributesExist() will verify existence + Weights tok_emb{}; // required attribute - validateRequiredAttributesExist() will verify existence + int32_t mhaTypeId = 0; + std::set const requiredAttributes{ + "bert_embeddings_layernorm_beta", + "bert_embeddings_layernorm_gamma", + "bert_embeddings_word_embeddings", + "bert_embeddings_token_type_embeddings", + "bert_embeddings_position_embeddings", + }; + plugin::validateRequiredAttributesExist(requiredAttributes, fc); + + for (int32_t i = 0; i < fc->nbFields; i++) + { + std::string field_name(fc->fields[i].name); + if (field_name.compare("bert_embeddings_layernorm_beta") == 0) + { + BERT_DEBUG_MSG("Building bert_embeddings_layernorm_beta..."); + beta.values = fc->fields[i].data; + beta.count = fc->fields[i].length; + beta.type = fieldTypeToDataType(fc->fields[i].type); + } + + if (field_name.compare("bert_embeddings_layernorm_gamma") == 0) + { + BERT_DEBUG_MSG("Building bert_embeddings_layernorm_gamma..."); + gamma.values = fc->fields[i].data; + gamma.count = fc->fields[i].length; + gamma.type = fieldTypeToDataType(fc->fields[i].type); + } + + if (field_name.compare("bert_embeddings_word_embeddings") == 0) + { + BERT_DEBUG_MSG("Building bert_embeddings_word_embeddings..."); + word_emb.values = fc->fields[i].data; + word_emb.count = fc->fields[i].length; + word_emb.type = fieldTypeToDataType(fc->fields[i].type); + } + + if (field_name.compare("bert_embeddings_token_type_embeddings") == 0) + { + BERT_DEBUG_MSG("Building bert_embeddings_token_type_embeddings..."); + tok_emb.values = fc->fields[i].data; + tok_emb.count = fc->fields[i].length; + tok_emb.type = fieldTypeToDataType(fc->fields[i].type); + } + + if (field_name.compare("bert_embeddings_position_embeddings") == 0) + { + BERT_DEBUG_MSG("Building bert_embeddings_position_embeddings..."); + pos_emb.values = fc->fields[i].data; + pos_emb.count = fc->fields[i].length; + pos_emb.type = fieldTypeToDataType(fc->fields[i].type); + } + if (field_name.compare("output_fp16") == 0) + { + BERT_DEBUG_MSG("Building output_fp16..."); + PLUGIN_VALIDATE(fc->fields[i].type == PluginFieldType::kINT32); + output_fp16 = static_cast(fc->fields[i].data)[0] != 0; + } + if (field_name.compare("full_mask") == 0) + { + BERT_DEBUG_MSG("Building full_mask..."); + PLUGIN_VALIDATE(fc->fields[i].type == PluginFieldType::kINT32); + useFullMask = static_cast(fc->fields[i].data)[0] != 0; + } + if (field_name.compare("mha_type_id") == 0) + { + mhaTypeId = *static_cast(fc->fields[i].data); + PLUGIN_VALIDATE(mhaTypeId >= 0 && mhaTypeId <= 3); + BERT_DEBUG_VALUE("Building mha typeId: ", mhaTypeId); + } + } + + BERT_DEBUG_MSG("Building the Plugin..."); + DataType mhaType = static_cast(mhaTypeId); + EmbLayerNormPluginDynamicLegacy* p + = new EmbLayerNormPluginDynamicLegacy(name, output_fp16 ? DataType::kHALF : DataType::kFLOAT, mhaType, beta, + gamma, word_emb, pos_emb, tok_emb, useFullMask); + return p; + } + catch (std::exception const& e) + { + caughtError(e); + } + return nullptr; +} + +IPluginV2* EmbLayerNormPluginDynamicLegacyCreator::deserializePlugin( + char const* name, void const* serialData, size_t serialLength) noexcept +{ + try + { + // This object will be deleted when the network is destroyed, which will + // call EmbLayerNormPluginDynamicLegacy::destroy() + return new EmbLayerNormPluginDynamicLegacy(name, serialData, serialLength); + } + catch (std::exception const& e) + { + caughtError(e); + } + return nullptr; +} + +void EmbLayerNormPluginDynamicLegacyCreator::setPluginNamespace(char const* libNamespace) noexcept +{ + try + { + mNamespace = libNamespace; + } + catch (std::exception const& e) + { + caughtError(e); + } +} + +char const* EmbLayerNormPluginDynamicLegacyCreator::getPluginNamespace() const noexcept +{ + return mNamespace.c_str(); +} + +#endif // CUDA_VERSION >= 10010 diff --git a/plugin/embLayerNormPlugin/embLayerNormPluginLegacy.h b/plugin/embLayerNormPlugin/embLayerNormPluginLegacy.h new file mode 100644 index 00000000..cb8f8165 --- /dev/null +++ b/plugin/embLayerNormPlugin/embLayerNormPluginLegacy.h @@ -0,0 +1,151 @@ +/* + * 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. + */ + +#include +#if CUDA_VERSION >= 10010 + +#ifndef TRT_EMB_LAYER_NORM_PLUGIN_LEGACY_H +#define TRT_EMB_LAYER_NORM_PLUGIN_LEGACY_H + +#include "NvInferPlugin.h" +#include "NvInferRuntime.h" + +#include "common/bertCommon.h" +#include +#include + +namespace nvinfer1 +{ +namespace plugin +{ +namespace bert +{ + +int32_t computeMaskIdx(cudaStream_t stream, int32_t const S, int32_t const B, int32_t const* mask, int32_t* maskIdx); + +template +int32_t embSkipLayerNorm(cudaStream_t stream, int32_t ld, int32_t B, int32_t S, int32_t const* inputIds, + int32_t const* token_ids, float const* beta, float const* gamma, T const* wordEmb, T const* posEmb, T const* tokEmb, + int32_t const wordSize, int32_t const tokSize, T* output); + +cudaError_t convertMask(uint32_t const S, uint32_t const B, uint32_t const warps_m, uint32_t const warps_n, + uint32_t const warps_k, int32_t const* inputMaskSB, uint32_t* inputMaskX, cudaStream_t stream); + +class EmbLayerNormPluginDynamicLegacy : public nvinfer1::IPluginV2DynamicExt +{ +public: + EmbLayerNormPluginDynamicLegacy(std::string const& name, nvinfer1::DataType const type, + nvinfer1::DataType const mhaType, nvinfer1::Weights const& beta, nvinfer1::Weights const& gamma, + nvinfer1::Weights const& word_emb, nvinfer1::Weights const& pos_emb, nvinfer1::Weights const& tok_emb, + bool const useFullMask); + + EmbLayerNormPluginDynamicLegacy(std::string const& name, void const* data, size_t length); + + // It doesn't make sense to make EmbLayerNormPluginDynamicLegacy without arguments, so we + // delete default constructor. + EmbLayerNormPluginDynamicLegacy() = delete; + + // IPluginV2DynamicExt Methods + nvinfer1::IPluginV2DynamicExt* clone() const noexcept override; + nvinfer1::DimsExprs getOutputDimensions(int32_t outputIndex, nvinfer1::DimsExprs const* inputs, int32_t nbInputs, + nvinfer1::IExprBuilder& exprBuilder) noexcept override; + bool supportsFormatCombination( + int32_t pos, nvinfer1::PluginTensorDesc const* inOut, int32_t nbInputs, int32_t nbOutputs) noexcept override; + void configurePlugin(nvinfer1::DynamicPluginTensorDesc const* in, int32_t nbInputs, + nvinfer1::DynamicPluginTensorDesc const* out, int32_t nbOutputs) noexcept override; + size_t getWorkspaceSize(nvinfer1::PluginTensorDesc const* inputs, int32_t nbInputs, + nvinfer1::PluginTensorDesc const* outputs, int32_t nbOutputs) const noexcept override; + int32_t enqueue(nvinfer1::PluginTensorDesc const* inputDesc, nvinfer1::PluginTensorDesc const* outputDesc, + void const* const* inputs, void* const* outputs, void* workspace, cudaStream_t stream) noexcept override; + + // IPluginV2Ext Methods + nvinfer1::DataType getOutputDataType( + int32_t index, nvinfer1::DataType const* inputTypes, int32_t nbInputs) const noexcept override; + + // IPluginV2 Methods + char const* getPluginType() const noexcept override; + char const* getPluginVersion() const noexcept override; + int32_t getNbOutputs() const noexcept override; + int32_t initialize() noexcept override; + void terminate() noexcept override; + size_t getSerializationSize() const noexcept override; + void serialize(void* buffer) const noexcept override; + void destroy() noexcept override; + void setPluginNamespace(char const* pluginNamespace) noexcept override; + char const* getPluginNamespace() const noexcept override; + +private: + std::string const mLayerName; + std::string mNamespace; + + bert::cuda_unique_ptr mGammaDev; + bert::cuda_unique_ptr mBetaDev; + bert::cuda_unique_ptr mWordEmbDev; + bert::cuda_unique_ptr mTokEmbDev; + bert::cuda_unique_ptr mPosEmbDev; + size_t mLd; // leading dim = hidden size + size_t mS; // sequence length + size_t mWordVocabSize; + size_t mPosVocabSize; + size_t mTokVocabSize; + bert::WeightsWithOwnership mBeta; + bert::WeightsWithOwnership mGamma; + bert::WeightsWithOwnership mWordEmb; + bert::WeightsWithOwnership mTokEmb; + bert::WeightsWithOwnership mPosEmb; + nvinfer1::DataType mType; + bool mUseFullMask; + nvinfer1::DataType mMhaType; + int32_t mSM; + + using IPluginV2::getOutputDimensions; + using IPluginV2::getWorkspaceSize; + using IPluginV2::enqueue; + using IPluginV2Ext::configurePlugin; +}; + +class EmbLayerNormPluginDynamicLegacyCreator : public nvinfer1::IPluginCreator +{ +public: + EmbLayerNormPluginDynamicLegacyCreator(); + + char const* getPluginName() const noexcept override; + + char const* getPluginVersion() const noexcept override; + + nvinfer1::PluginFieldCollection const* getFieldNames() noexcept override; + + nvinfer1::IPluginV2* createPlugin(char const* name, nvinfer1::PluginFieldCollection const* fc) noexcept override; + + nvinfer1::IPluginV2* deserializePlugin( + char const* name, void const* serialData, size_t serialLength) noexcept override; + + void setPluginNamespace(char const* pluginNamespace) noexcept override; + + char const* getPluginNamespace() const noexcept override; + +private: + static nvinfer1::PluginFieldCollection mFC; + static std::vector mPluginAttributes; + std::string mNamespace; +}; +} // namespace bert +} // namespace plugin +} // namespace nvinfer1 +#endif // TRT_EMB_LAYER_NORM_PLUGIN_LEGACY_H + +#endif // CUDA_VERSION >= 10010 diff --git a/plugin/fcPlugin/README.md b/plugin/fcPlugin/README.md index 913dfead..500bc05f 100644 --- a/plugin/fcPlugin/README.md +++ b/plugin/fcPlugin/README.md @@ -11,6 +11,8 @@ ## Description +> NOTE: This plugin is deprecated since TensorRT 10.6. Its functionality has been superseded by the [`IMatrixMultiplyLayer`](https://docs.nvidia.com/deeplearning/tensorrt/api/c_api/classnvinfer1_1_1_i_matrix_multiply_layer.html) (Can be added to the network definition using [`addMatrixMultiply()`](https://docs.nvidia.com/deeplearning/tensorrt/api/c_api/classnvinfer1_1_1_i_network_definition.html#acf109d93e91c86afbd263f5fea29ffe8)) + Performs a matrix multiplication similar to the FullyConnected Layer in TensorRT, but without bias. The main difference is that the weights are not transposed. Always dispatches to cuBLAS. At engine build time, the plugin runs a search over the parameters of the available algorithms to find the fastest one available. @@ -51,8 +53,8 @@ documentation. ## Changelog -November 2019 -This is the first release of this `README.md` file. +- October 2024: Add deprecation note. +- November 2019: This is the first release of this `README.md` file. ## Known issues diff --git a/python/docstrings/infer/pyCoreDoc.h b/python/docstrings/infer/pyCoreDoc.h index 541e66b8..ceb4f363 100644 --- a/python/docstrings/infer/pyCoreDoc.h +++ b/python/docstrings/infer/pyCoreDoc.h @@ -713,7 +713,8 @@ constexpr char const* descr = R"trtdoc( :ivar streamable_weights_size: Returns the size of the streamable weights in the engine. This may not include all the weights. :ivar weight_streaming_budget_v2: Set and get the current weight streaming budget for inference. The budget may be set any non-negative value. A value of 0 streams the most weights. Values equal to streamable_weights_size (default) or larger will disable weight streaming. :ivar weight_streaming_scratch_memory_size: The amount of scratch memory required by a TensorRT ExecutionContext to perform inference. This value may change based on the current weight streaming budget. Please use the V2 memory APIs, engine.device_memory_size_v2 and ExecutionContext.set_device_memory() to provide memory which includes the current weight streaming scratch memory. Not specifying these APIs or using the V1 APIs will not include this memory, so TensorRT will resort to allocating itself. - )trtdoc"; + )trtdoc" + ; // Documentation bug with parameters on these three functions because they are overloaded. constexpr char const* serialize = R"trtdoc( @@ -955,13 +956,13 @@ constexpr char const* read = R"trtdoc( If an allocation request cannot be satisfied, ``0`` should be returned. - :arg destination: The host memory address to copy read memory to. :arg size: The number of bytes required. - :returns: The number of bytes read. + :returns: A buffer containing the bytes read. )trtdoc"; } // namespace StreamReaderDoc + namespace BuilderFlagDoc { constexpr char const* descr @@ -1010,6 +1011,9 @@ constexpr char const* REFIT_INDIVIDUAL constexpr char const* WEIGHT_STREAMING = R"trtdoc(Enable building with the ability to stream varying amounts of weights during Runtime. This decreases GPU memory of TRT at the expense of performance.)trtdoc"; constexpr char const* INT4 = R"trtdoc(Enable plugins with INT4 input/output)trtdoc"; +constexpr char const* STRICT_NANS + = R"trtdoc(Disable floating-point optimizations: 0*x => 0, x-x => 0, or x/x => 1. These identities are not true when x is a NaN or Inf, and thus might hide propagation or generation of NaNs.)trtdoc"; +constexpr char const* MONITOR_MEMORY = R"trtdoc(Enable memory monitor during build time.)trtdoc"; } // namespace BuilderFlagDoc namespace MemoryPoolTypeDoc @@ -1599,6 +1603,17 @@ constexpr char const* build_serialized_network = R"trtdoc( :returns: A pointer to a :class:`IHostMemory` object that contains a serialized network. )trtdoc"; +constexpr char const* build_engine_with_config = R"trtdoc( + Builds a network for the given :class:`INetworkDefinition` and :class:`IBuilderConfig` . + + This function allows building a network and creating an engine. + + :arg network: Network definition. + :arg config: Builder configuration. + + :returns: A pointer to a :class:`ICudaEngine` object that contains a built engine. +)trtdoc"; + constexpr char const* is_network_supported = R"trtdoc( Checks that a network is within the scope of the :class:`IBuilderConfig` settings. @@ -1664,7 +1679,8 @@ constexpr char const* deserialize_cuda_engine = R"trtdoc( constexpr char const* deserialize_cuda_engine_reader = R"trtdoc( Deserialize an :class:`ICudaEngine` from a stream reader. - :arg stream_reader: The :class:`PyStreamReader` that will read the serialized :class:`ICudaEngine`. This enables deserialization from a file directly. + :arg stream_reader: The :class:`PyStreamReader` that will read the serialized :class:`ICudaEngine`. This enables + deserialization from a file directly. :returns: The :class:`ICudaEngine`, or None if it could not be deserialized. )trtdoc"; diff --git a/python/docstrings/infer/pyGraphDoc.h b/python/docstrings/infer/pyGraphDoc.h index 74a651d3..32a23068 100644 --- a/python/docstrings/infer/pyGraphDoc.h +++ b/python/docstrings/infer/pyGraphDoc.h @@ -198,7 +198,8 @@ constexpr const char* descr = R"trtdoc( :ivar dynamic_range: :class:`Tuple[float, float]` [DEPRECATED] Deprecated in TensorRT 10.1. Superseded by explicit quantization. A tuple containing the [minimum, maximum] of the dynamic range, or :class:`None` if the range was not set. :ivar is_shape: :class:`bool` Whether the tensor is a shape tensor. :ivar allowed_formats: :class:`int32` The allowed set of TensorFormat candidates. This should be an integer consisting of one or more :class:`TensorFormat` s, combined via bitwise OR after bit shifting. For example, ``1 << int(TensorFormat.CHW4) | 1 << int(TensorFormat.CHW32)``. -)trtdoc"; +)trtdoc" + ; constexpr const char* set_dynamic_range = R"trtdoc( [DEPRECATED] Deprecated in TensorRT 10.1. Superseded by explicit quantization. @@ -1653,6 +1654,7 @@ constexpr const char* descr = R"trtdoc( )trtdoc"; } // namespace IDequantizeLayerDoc + namespace IIfConditionalBoundaryLayerDoc { constexpr const char* descr = R"trtdoc( @@ -2372,6 +2374,16 @@ constexpr const char* add_plugin_v2 = R"trtdoc( :returns: The new plugin layer, or :class:`None` if it could not be created. )trtdoc"; +constexpr const char* add_plugin = R"trtdoc( + Add a plugin layer to the network with a tuple of (inputs, shape_inputs, plugin). :func:`add_plugin_v3` can be thought of as an "unpacked tuple" version of this function. + + Primarily intended to be used when using the `tensorrt.plugin` module to implement the plugin. + + :arg tuple: A tuple of (inputs, shape_inputs, plugin). + + :returns: The new plugin layer, or :class:`None` if it could not be created. +)trtdoc"; + constexpr const char* add_plugin_v3 = R"trtdoc( Add a plugin layer to the network using an :class:`IPluginV3` interface. See :class:`IPluginV3` for more information. @@ -2441,6 +2453,7 @@ constexpr const char* add_dequantize = R"trtdoc( :returns: The new dequantization layer, or :class:`None` if it could not be created. )trtdoc"; + constexpr const char* add_if_conditional = R"trtdoc( Adds an if-conditional to the network, which provides a way to specify subgraphs that will be conditionally executed using lazy evaluation. See :class:`IIfConditional` for more information. diff --git a/python/include/impl/plugin.h b/python/include/impl/plugin.h new file mode 100644 index 00000000..0c22e297 --- /dev/null +++ b/python/include/impl/plugin.h @@ -0,0 +1,290 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 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. + */ + +#ifndef TRT_PYTHON_IMPL_PLUGIN_H +#define TRT_PYTHON_IMPL_PLUGIN_H + +#include "NvInfer.h" + +//! +//! \file plugin.h +//! +//! This file contains definitions for supporting the `tensorrt.plugin` Python module +//! +//! \warning None of the defintions here are part of the TensorRT C++ API and may not follow semantic versioning rules. +//! TensorRT clients must not utilize them directly. +//! + +namespace nvinfer1 +{ +namespace v_1_0 +{ + +class IPluginV3QuickCore : public IPluginCapability +{ +public: + InterfaceInfo getInterfaceInfo() const noexcept override + { + return InterfaceInfo{"PLUGIN_V3QUICK_CORE", 1, 0}; + } + + virtual AsciiChar const* getPluginName() const noexcept = 0; + + virtual AsciiChar const* getPluginVersion() const noexcept = 0; + + virtual AsciiChar const* getPluginNamespace() const noexcept = 0; +}; + +class IPluginV3QuickBuild : public IPluginCapability +{ +public: + InterfaceInfo getInterfaceInfo() const noexcept override + { + return InterfaceInfo{"PLUGIN_V3QUICK_BUILD", 1, 0}; + } + + //! + //! \brief Provide the data types of the plugin outputs if the input tensors have the data types provided. + //! + //! \param outputTypes Pre-allocated array to which the output data types should be written. + //! \param nbOutputs The number of output tensors. This matches the value returned from getNbOutputs(). + //! \param inputTypes The input data types. + //! \param inputRanks Ranks of the input tensors + //! \param nbInputs The number of input tensors. + //! + //! \return 0 for success, else non-zero + //! + virtual int32_t getOutputDataTypes(DataType* outputTypes, int32_t nbOutputs, DataType const* inputTypes, + int32_t const* inputRanks, int32_t nbInputs) const noexcept = 0; + + //! + //! \brief Provide expressions for computing dimensions of the output tensors from dimensions of the input tensors. + //! + //! \param inputs Expressions for dimensions of the input tensors + //! \param nbInputs The number of input tensors + //! \param shapeInputs Expressions for values of the shape tensor inputs + //! \param nbShapeInputs The number of shape tensor inputs + //! \param outputs Pre-allocated array to which the output dimensions must be written + //! \param exprBuilder Object for generating new dimension expressions + //! + //! \return 0 for success, else non-zero + //! + virtual int32_t getOutputShapes(DimsExprs const* inputs, int32_t nbInputs, DimsExprs const* shapeInputs, + int32_t nbShapeInputs, DimsExprs* outputs, int32_t nbOutputs, IExprBuilder& exprBuilder) noexcept = 0; + + //! + //! \brief Configure the plugin. Behaves similarly to `IPluginV3OneBuild::configurePlugin()` + //! + //! \return 0 for success, else non-zero + //! + virtual int32_t configurePlugin(DynamicPluginTensorDesc const* in, int32_t nbInputs, + DynamicPluginTensorDesc const* out, int32_t nbOutputs) noexcept = 0; + + //! + //! \brief Get number of format combinations supported by the plugin for the I/O characteristics indicated by + //! `inOut`. + //! + virtual int32_t getNbSupportedFormatCombinations( + DynamicPluginTensorDesc const* inOut, int32_t nbInputs, int32_t nbOutputs) noexcept = 0; + + //! + //! \brief Write all format combinations supported by the plugin for the I/O characteristics indicated by `inOut` to + //! `supportedCombinations`. It is guaranteed to have sufficient memory allocated for (nbInputs + nbOutputs) * + //! getNbSupportedFormatCombinations() `PluginTensorDesc`s. + //! + //! \return 0 for success, else non-zero + //! + virtual int32_t getSupportedFormatCombinations(DynamicPluginTensorDesc const* inOut, int32_t nbInputs, + int32_t nbOutputs, PluginTensorDesc* supportedCombinations, int32_t nbFormatCombinations) noexcept = 0; + + //! + //! \brief Get the number of outputs from the plugin. + //! + virtual int32_t getNbOutputs() const noexcept = 0; + + //! + //! \brief Communicates to TensorRT that the output at the specified output index is aliased to the input at the + //! returned index. Behaves similary to `v_2_0::IPluginV3OneBuild.getAliasedInput()`. + //! + virtual int32_t getAliasedInput(int32_t outputIndex) noexcept + { + return -1; + } + + //! + //! \brief Query for any custom tactics that the plugin intends to use specific to the I/O characteristics indicated + //! by the immediately preceding call to `configurePlugin()`. + //! + //! \return 0 for success, else non-zero + //! + virtual int32_t getValidTactics(int32_t* tactics, int32_t nbTactics) noexcept + { + return 0; + } + + //! + //! \brief Query for number of custom tactics related to the `getValidTactics()` call. + //! + virtual int32_t getNbTactics() noexcept + { + return 0; + } + + //! + //! \brief Called to query the suffix to use for the timing cache ID. May be called anytime after plugin creation. + //! + virtual char const* getTimingCacheID() noexcept + { + return nullptr; + } + + //! + //! \brief Query for a string representing the configuration of the plugin. May be called anytime after + //! plugin creation. + //! + virtual char const* getMetadataString() noexcept + { + return nullptr; + } +}; + +class IPluginV3QuickRuntime : public IPluginCapability +{ +public: + InterfaceInfo getInterfaceInfo() const noexcept override + { + return InterfaceInfo{"PLUGIN_V3QUICK_RUNTIME", 1, 0}; + } + + //! + //! \brief Set the tactic to be used in the subsequent call to enqueue(). Behaves similar to + //! `IPluginV3OneRuntime::setTactic()`. + //! + //! \return 0 for success, else non-zero + //! + virtual int32_t setTactic(int32_t tactic) noexcept + { + return 0; + } + + //! + //! \brief Execute the plugin. + //! + //! \param inputDesc how to interpret the memory for the input tensors. + //! \param outputDesc how to interpret the memory for the output tensors. + //! \param inputs The memory for the input tensors. + //! \param inputStrides Strides for input tensors. + //! \param outputStrides Strides for output tensors. + //! \param outputs The memory for the output tensors. + //! \param nbInputs Number of input tensors. + //! \param nbOutputs Number of output tensors. + //! \param stream The stream in which to execute the kernels. + //! + //! \return 0 for success, else non-zero + //! + virtual int32_t enqueue(PluginTensorDesc const* inputDesc, PluginTensorDesc const* outputDesc, + void const* const* inputs, void* const* outputs, Dims const* inputStrides, Dims const* outputStrides, + int32_t nbInputs, int32_t nbOutputs, cudaStream_t stream) noexcept = 0; + + //! + //! \brief Get the plugin fields which should be serialized. + //! + virtual PluginFieldCollection const* getFieldsToSerialize() noexcept = 0; +}; + +class IPluginCreatorV3Quick : public IPluginCreatorInterface +{ +public: + InterfaceInfo getInterfaceInfo() const noexcept override + { + return InterfaceInfo{"PLUGIN CREATOR_V3QUICK", 1, 0}; + } + + //! + //! \brief Return a plugin object. Return nullptr in case of error. + //! + //! \param name A NULL-terminated name string of length 1024 or less, including the NULL terminator. + //! \param namespace A NULL-terminated name string of length 1024 or less, including the NULL terminator. + //! \param fc A pointer to a collection of fields needed for constructing the plugin. + //! \param phase The TensorRT phase in which the plugin is being created + //! + virtual IPluginV3* createPlugin(AsciiChar const* name, AsciiChar const* nspace, PluginFieldCollection const* fc, + TensorRTPhase phase) noexcept = 0; + + //! + //! \brief Return a list of fields that need to be passed to createPlugin() when creating a plugin for use in the + //! TensorRT build phase. + //! + virtual PluginFieldCollection const* getFieldNames() noexcept = 0; + + virtual AsciiChar const* getPluginName() const noexcept = 0; + + virtual AsciiChar const* getPluginVersion() const noexcept = 0; + + virtual AsciiChar const* getPluginNamespace() const noexcept = 0; + + IPluginCreatorV3Quick() = default; + virtual ~IPluginCreatorV3Quick() = default; + +protected: + IPluginCreatorV3Quick(IPluginCreatorV3Quick const&) = default; + IPluginCreatorV3Quick(IPluginCreatorV3Quick&&) = default; + IPluginCreatorV3Quick& operator=(IPluginCreatorV3Quick const&) & = default; + IPluginCreatorV3Quick& operator=(IPluginCreatorV3Quick&&) & = default; +}; + +} // namespace v_1_0 + +//! +//! \class IPluginV3QuickCore +//! +//! \brief Provides core capability (`IPluginCapability::kCORE` for quickly-deployable TRT plugins) +//! +//! \warning This class is strictly for the purpose of supporting quickly-deployable TRT Python plugins and is not part +//! of the public TensorRT C++ API. Users must not inherit from this class. +//! +using IPluginV3QuickCore = v_1_0::IPluginV3QuickCore; + +//! +//! \class IPluginV3QuickBuild +//! +//! \brief Provides build capability (`IPluginCapability::kBUILD` for quickly-deployable TRT plugins) +//! +//! \warning This class is strictly for the purpose of supporting quickly-deployable TRT Python plugins and is not part +//! of the public TensorRT C++ API. Users must not inherit from this class. +//! +using IPluginV3QuickBuild = v_1_0::IPluginV3QuickBuild; + +//! +//! \class IPluginV3QuickRuntime +//! +//! \warning This class is strictly for the purpose of supporting quickly-deployable TRT Python plugins and is not part +//! of the public TensorRT C++ API. Users must not inherit from this class. +//! +using IPluginV3QuickRuntime = v_1_0::IPluginV3QuickRuntime; + +//! +//! \class IPluginCreatorV3Quick +//! +//! \warning This class is strictly for the purpose of supporting quickly-deployable TRT Python plugins and is not part +//! of the public TensorRT C++ API. Users must not inherit from this class. +//! +using IPluginCreatorV3Quick = v_1_0::IPluginCreatorV3Quick; + +} // namespace nvinfer1 + +#endif // TRT_PYTHON_IMPL_PLUGIN_H diff --git a/python/packaging/bindings_wheel/setup.cfg b/python/packaging/bindings_wheel/setup.cfg index 9e20a94e..b6f5905e 100644 --- a/python/packaging/bindings_wheel/setup.cfg +++ b/python/packaging/bindings_wheel/setup.cfg @@ -1,12 +1,19 @@ -# SPDX-FileCopyrightText: Copyright (c) 2019-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: LicenseRef-NvidiaProprietary -# -# NVIDIA CORPORATION, its affiliates and licensors retain all intellectual -# property and proprietary rights in and to this material, related -# documentation and any modifications thereto. Any use, reproduction, -# disclosure or distribution of this material and related documentation -# without an express license agreement from NVIDIA CORPORATION or -# its affiliates is strictly prohibited. +# +# SPDX-FileCopyrightText: Copyright (c) 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. +# [metadata] license_files = LICENSE.txt diff --git a/python/packaging/bindings_wheel/setup.py b/python/packaging/bindings_wheel/setup.py index 32b9a730..19184de7 100644 --- a/python/packaging/bindings_wheel/setup.py +++ b/python/packaging/bindings_wheel/setup.py @@ -29,6 +29,8 @@ tensorrt_module += "-cu##CUDA_MAJOR##_bindings" package_name += "_bindings" +plugin_subpackage_name = f"{package_name}.plugin" + setup( name=tensorrt_module, version="##TENSORRT_PYTHON_VERSION##", @@ -41,12 +43,12 @@ "Intended Audience :: Developers", "Programming Language :: Python :: 3", ], - packages=[package_name], + packages=[package_name, plugin_subpackage_name], extras_require={"numpy": "numpy"}, package_data={package_name: ["*.so*", "*.pyd", "*.pdb", "*.dll*"]}, include_package_data=True, zip_safe=True, keywords="nvidia tensorrt deeplearning inference", - url="https://developer.nvidia.com/tensorrt", - download_url="https://github.com/nvidia/tensorrt/tags", + url="https://github.com/nvidia/tensorrt", + download_url="https://developer.nvidia.com/tensorrt", ) diff --git a/python/packaging/bindings_wheel/tensorrt/plugin/__init__.py b/python/packaging/bindings_wheel/tensorrt/plugin/__init__.py new file mode 100644 index 00000000..c6d55190 --- /dev/null +++ b/python/packaging/bindings_wheel/tensorrt/plugin/__init__.py @@ -0,0 +1,46 @@ +# +# SPDX-FileCopyrightText: Copyright (c) 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. +# + +import tensorrt as trt + +logger = trt.Logger() +logger.log(trt.Logger.WARNING, "Functionality provided through tensorrt.plugin module is experimental in TensorRT 10.6.") + +# export.public_api() will expose things here. To make sure that happens, we just need to +# import all the submodules so that the decorator is actually executed (__discover_modules() below). +__all__ = [] + +def __discover_modules(): + import importlib + import pkgutil + + mods = [importlib.import_module(__package__)] + while mods: + mod = mods.pop(0) + + yield mod + + if hasattr(mod, "__path__"): + mods.extend( + [ + importlib.import_module(f"{mod.__name__}.{submod.name}") + for submod in pkgutil.iter_modules(mod.__path__) + ] + ) + + +_ = list(__discover_modules()) diff --git a/python/packaging/bindings_wheel/tensorrt/plugin/_autotune.py b/python/packaging/bindings_wheel/tensorrt/plugin/_autotune.py new file mode 100644 index 00000000..6b9a6aac --- /dev/null +++ b/python/packaging/bindings_wheel/tensorrt/plugin/_autotune.py @@ -0,0 +1,270 @@ +# +# SPDX-FileCopyrightText: Copyright (c) 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. +# + +import builtins +import tensorrt as trt +from typing import List, Iterable +import copy + +from ._utils import _str_to_data_type +from ._export import public_api + + +# "onesided" means either type or format combinations. After combinations for each are separately generated, we will combine them later. +# e.g. io_variants = ["FP32|FP16", "FP32|FP16", "FP32*FP16"] for a plugin with 3 I/Os. i.e. I/O indices 0 and 1 are dependently either FP32/FP16 and index 2 is independently FP32/FP16. +# There will be 2 * 2 = 4 combinations here: ["FP32", "FP32", "FP32"], ["FP16", "FP16", "FP32"], ["FP32", "FP32", "FP16"], ["FP16", "FP16", "FP16"] +def _gen_onesided_combinations(io_variants): + + # Algorithm: + # (1) Ignore independent variants and count the (max) number of dependent variants `mx_poly` + # (2) Compile initial list of #`mx_poly` combinations using the first option (option 0) for any independent variants + # (3) For each independent variant IO index, add combinations with that index replaced by option 1, 2, ... + + combinations = [] + mx_poly = 0 # This is the number of dependent variants + + for io_variant in io_variants: + io_variant_list = io_variant.split("|") + + if len(io_variant_list) > 1: + if "*" in io_variant: + raise ValueError( + f"Type/Format '{io_variant}' contains both '|' and '*'" + ) + if mx_poly > 1: + if mx_poly != len(io_variant_list): + raise ValueError( + f"Type/Format combinations {io_variants} contain illegal dependent lengths" + ) + + mx_poly = builtins.max(mx_poly, len(io_variant_list)) + + for _ in range(mx_poly): + combinations.append([None] * len(io_variants)) + + for j, io_variant in enumerate(io_variants): + io_variant_list = io_variant.split("|") + + if len(io_variant_list) == 1: + if "*" in io_variant: + io_variant_list = io_variant.split("*") + for i in range(len(combinations)): + combinations[i][j] = io_variant_list[0] + else: + for k in range(len(io_variant_list)): + combinations[k][j] = io_variant_list[k] + + for j, io_variant in enumerate(io_variants): + new_combs = [] + if "*" in io_variant: + io_variant_list = io_variant.split("*") + for k in range(1, len(io_variant_list)): + for c in combinations: + new_c = copy.deepcopy(c) + new_c[j] = io_variant_list[k] + new_combs.append(new_c) + combinations.extend(new_combs) + + return combinations + + +class _TypeFormatCombination: + def __init__(self, num=0): + self.types = [None] * num + self.layouts = [None] * num + self.tactics = [] + + def set_types(self, types): + self.types = types + + def set_layouts(self, layouts=None): + if isinstance(layouts, List): + self.layouts = layouts + else: + self.layouts = [layouts] * len(self.types) + + def __hash__(self): + return hash((tuple(self.types), tuple(self.layouts))) + + def __eq__(self, other): + return ( + isinstance(other, _TypeFormatCombination) + and self.types == other.types + and self.layouts == other.layouts + ) + + def __str__(self) -> str: + return "{" + str(self.types) + ", " + str(self.layouts) + "}" + + +@public_api() +class AutoTuneCombination: + def __init__( + self, io_types: str = None, layouts: str = None, tactics: Iterable[int] = None + ): + """ + Construct a set of supported type/format combinations of a plugin's I/O. + + Any custom *tactic* s per each such type/format combination can also be advertised. A tactic is simply another way to + calculate the output of a plugin for the same type/format combination of the I/O (e.g. if there are multiple kernels available). + + Args: + io_types (str, optional): A string representation of a type combination. + + Valid format is "type0,type1,...,type#io" where 'type' is of the form "TYPE0[sep]TYPE1[sep]...". + + TYPE is a valid string representation of a `trt.DataType`. These include "FP32" for trt.float32, "FP16" for trt.float16. The string representation of other data types is the same as their name in the trt.DataType enum. + + + [sep] is a valid separator, which is either '|' or '*'. Only one of these separators can appear in a given `io_types`. + + (1). '|' indicates a dependent combination: the dependence of the type of one I/O to another I/O. e.g. "FP32|FP16,FP32|FP16" indicates the IO can only be both FP32 or both FP16. + + (2). '*' indicates an independent combination. e.g. "FP32*FP16,FP32|FP16,FP32|FP16" indicates that the first input is independently either FP32 or FP16 regardless of the rest of the IO. + + layouts (str, optional): A string representation of a format combination. + + Valid format is "format0,format1,...,format#io" where 'format' is of the form "FORMAT0[sep]FORMAT1[sep]...". + + FORMAT is a valid string representation of a `trt.TensorFormat`. These are string versions for the enum values of `trt.TensorFormat`. e.g. "LINEAR" for `trt.TensorFormat.LINEAR`. + + [sep] is a valid separator, which is either '|' or '*'. The rules are the same as for `io_types`. + + tactics (Iterable[int], optional): Custom tactics for this type/format combination. Each custom tactic must be a positive integer. Defaults to default tactic (0). + + .. code-block:: python + :linenos: + :caption: For a plugin with 3 I/Os, I/O indices 0 and 1 are dependently either FP32/FP16 and index 2 is independently FP32/FP16. + + @trtp.autotune("my::plugin") + def autotune(inp0: trtp.TensorDesc, inp1: trtp.TensorDesc, outputs: Tuple[trtp.TensorDesc]) -> List[trtp.AutoTuneCombination]: + # The following would result in the following type combinations: + # [FP32, FP32, FP32], [FP16, FP16, FP32], [FP32, FP32, FP16], [FP16, FP16, FP16] + return [trtp.AutoTuneCombination("FP32|FP16, FP32|FP16, FP32|FP16", "LINEAR", [1, 2])] + + .. code-block:: python + :linenos: + :caption: For a plugin with 2 I/Os, the input/output supports either LINEAR or HWC format for FP32 and LINEAR format for FP16. + + @trtp.autotune("my::plugin") + def autotune(inp0: trtp.TensorDesc, outputs: Tuple[trtp.TensorDesc]) -> List[trtp.AutoTuneCombination]: + # Even though (FP16, HWC) is not a valid combination (see next example), TRT should intelligently reject those + # and pass the following combinations to the impl function: + # [{FP32, FP32}, {LINEAR, LINEAR}], [{FP32, FP32}, {HWC, LINEAR}], [{FP16, FP32}, {LINEAR, LINEAR}] + return [trtp.AutoTuneCombination("FP32*FP16, FP32", "LINEAR*HWC, LINEAR", [1, 2])] + + .. code-block:: python + :linenos: + :caption: For a plugin with 2 I/Os, the input/output supports either LINEAR or HWC format for FP32 and LINEAR format for FP16 (second method). + + @trtp.autotune("my::plugin") + def autotune(inp0: trtp.TensorDesc, outputs: Tuple[trtp.TensorDesc]) -> List[trtp.AutoTuneCombination]: + # We can use two AutoTuneCombination objects to avoid communicating illegal combinations + return [trtp.AutoTuneCombination("FP32*FP16, FP32", "LINEAR, LINEAR", [1, 2]), trtp.AutoTuneCombination("FP32, FP32", "HWC, LINEAR", [1, 2])] + """ + + if io_types is not None: + self.io_types = [s.strip() for s in io_types.split(",")] + if layouts is None: + layouts = "LINEAR" + self.layouts = [s.strip() for s in layouts.split(",")] + + if len(self.layouts) > 1: + assert len(self.io_types) == len(self.layouts) + + if len(self.io_types) > len(self.layouts): + assert len(self.layouts) == 1 + self.layouts = [self.layouts[0]] * len(self.io_types) + else: + self.io_types = [] + self.layouts = [] + + self.combinations = [] + self._tactics = tactics + + def pos(self, pos: Iterable[int], io_types: str, layouts: str = "LINEAR") -> None: + """ + Specify I/O types and formats for a specified set of I/O indices. + + Args: + pos (Iterable[int]): I/O indices. Input indices are [0, 1, ..., #inputs - 1] and output indices are [#inputs, #inputs + 1, ..., #inputs + #outputs - 1]. + io_types (str): Data types for these I/O indices. + layouts (str, optional): Tensor format(s) for these I/O indices. Defaults to "LINEAR". + Raises: + ValueError: If types or layouts for any of these I/O indices is already specified. + + .. code-block:: python + :linenos: + :caption: For a plugin with 3 I/Os, I/O indices 0 and 1 are dependently either FP32/FP16 and index 2 is independently FP32/FP16. + + @trtp.autotune("my::plugin") + def autotune(inp0: trtp.TensorDesc, inp1: trtp.TensorDesc, outputs: Tuple[trtp.TensorDesc]) -> List[trtp.AutoTuneCombination]: + c = trtp.AutoTuneCombination() + c.pos([0, 1], "FP32|FP16", "LINEAR") + c.pos(2, "FP32*FP16") # Omitting format is the same as declaring it to be LINEAR. + c.tactics([1, 2]) + return [c] + """ + if max(pos) >= len(self.io_types): + self.io_types.extend([None] * (max(pos) + 1 - len(self.io_types))) + self.layouts.extend([None] * (max(pos) + 1 - len(self.layouts))) + assert len(self.io_types) == len(self.layouts) + + for p in pos: + if self.io_types[p] is not None: + raise ValueError(f"Type(s) for position {p} already specified") + if self.layouts[p] is not None: + raise ValueError(f"Layout(s) for position {p} already specified") + self.io_types[p] = io_types + self.layouts[p] = layouts + + def tactics(self, tactics: Iterable[int]) -> None: + """ + Specify custom tactics for this type/format combination + + Args: + tactics (Iterable[int]): Custom tactics. These must be positive integers. + """ + self._tactics = tactics + + def _generate_combinations(self): + + self.combinations = [] + + type_combinations = _gen_onesided_combinations(self.io_types) + layout_combinations = _gen_onesided_combinations(self.layouts) + + for t in type_combinations: + for l in layout_combinations: + c = _TypeFormatCombination(len(self.io_types)) + c.types = [_str_to_data_type(tt) for tt in t] + c.layouts = [getattr(trt.TensorFormat, ff) for ff in l] + c.tactics = self._tactics + self.combinations.append(c) + + def _get_combinations(self): + self._generate_combinations() + return self.combinations + + def _check(self, pos, type, layout): + for i in range(len(self.combinations)): + if ( + self.combinations[i].types[pos] == _str_to_data_type(type) + and self.combinations[i].layouts[pos] == layout.name + ): + return True + return False diff --git a/python/packaging/bindings_wheel/tensorrt/plugin/_export.py b/python/packaging/bindings_wheel/tensorrt/plugin/_export.py new file mode 100644 index 00000000..d4ce2398 --- /dev/null +++ b/python/packaging/bindings_wheel/tensorrt/plugin/_export.py @@ -0,0 +1,36 @@ +# +# SPDX-FileCopyrightText: Copyright (c) 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. +# + +from types import ModuleType +import importlib + +def public_api(module: ModuleType = None, symbol: str = None): + def export_impl(obj): + nonlocal module, symbol + + module = module or importlib.import_module(__package__) + symbol = symbol or obj.__name__ + + if not hasattr(module, "__all__"): + module.__all__ = [] + + module.__all__.append(symbol) + setattr(module, symbol, obj) + + return obj + + return export_impl diff --git a/python/packaging/bindings_wheel/tensorrt/plugin/_lib.py b/python/packaging/bindings_wheel/tensorrt/plugin/_lib.py new file mode 100644 index 00000000..eabc437a --- /dev/null +++ b/python/packaging/bindings_wheel/tensorrt/plugin/_lib.py @@ -0,0 +1,523 @@ +# +# SPDX-FileCopyrightText: Copyright (c) 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. +# + +import tensorrt as trt +import types +import typing +from typing import Callable, Tuple, List +import numpy as np + +from ._plugin_class import _TemplatePlugin +from ._validate import ( + _parse_register_inputs, + _parse_register_return, + _validate_autotune, + _validate_impl, + _validate_name_and_namespace, +) +from ._utils import ( + _built_in_to_plugin_field_type, + _join_with, + _numpy_to_plugin_field_type, + _is_numpy_array, + _infer_numpy_type, +) + +from ._export import public_api + +# Namespace to which plugins are dynamically bound +# A namespace can be thought of as a library of plugins from the same author/common objective +class _PluginNamespace(types.ModuleType): + def __init__(self, namespace): + super().__init__("tensorrt.plugin.op." + namespace) + self._namespace = namespace + + def define(self, name, plugin_def): + assert not hasattr(self, name) + setattr(self, name, plugin_def) + + def __getattr__(self, name): + raise AttributeError( + f"'{self.__class__.__name__}' object '{self._namespace}' has no attribute '{name}'" + ) + + def __repr__(self): + return f'_PluginNamespace(namespace="{self._namespace}")' + + +# `tensorrt.plugin.op` module to which plugin namespaces are dynamically bound +class _Op(types.ModuleType): + def __init__(self): + super().__init__("tensorrt.plugin.op") + + def define_or_get(self, namespace): + if hasattr(self, namespace): + return getattr(self, namespace) + + ns = _PluginNamespace(namespace) + setattr(self, namespace, ns) + + return ns + + def __getattr__(self, name): + raise AttributeError( + f"'{self.__class__.__name__}' object has no attribute '{name}'" + ) + + +op = _Op() +public_api(symbol="op")(op) + +QDP_CREATORS = {} +QDP_REGISTRY = {} + +# Contains metadata about a registered plugin and `__call__()`` that allows for a plugin instance to be created +class PluginDef: + def __init__(self): + self.plugin_id = None # includes namespace (format is ns::name) + self.register_func = None + self.impl_func = None + self.autotune_func = None + self.autotune_attr_names = None + self.input_tensor_names = None + self.input_attrs = None # map name -> type + self.impl_attr_names = None + self.num_outputs = None + self.input_arg_schema = None + self.expects_tactic = None + + def __call__( + self, *args, **kwargs + ) -> Tuple[List[trt.ITensor], List[trt.ITensor], trt.IPluginV3]: + namespace, name = self.plugin_id.split("::") + + input_tensors = [] + schema_chunks = [] + + for t in args: + if not isinstance(t, trt.ITensor): + raise ValueError( + f"Expected trt.ITensor but got input of type {type(t)}" + ) + + schema_chunks.append("ITensor") + input_tensors.append(t) + + attrs = {} + for key, value in kwargs.items(): + if key not in self.input_attrs: + raise ValueError( + f"Unexpected attribute {key} provided. Expected one of {self.input_attrs.keys()}." + ) + attrs[key] = value + attr_annotation = self.input_attrs[key] + if isinstance(value, np.ndarray): + if typing.get_origin(attr_annotation) == np.ndarray: + np_dtype = typing.get_args(typing.get_args(attr_annotation)[1])[0] + if np.dtype(np_dtype) != np.dtype(value.dtype): + raise ValueError( + f"Unexpected dtype '{np.dtype(value.dtype)}' for attribute '{key}'. Expected '{np_dtype}'." + ) + else: + if attr_annotation is not type(value): + raise ValueError( + f"Unexpected type '{type(value)}' for attribute '{key}'. Expected '{attr_annotation}'." + ) + + schema_chunks.append(key) + + expected_schema = ( + f"({_join_with(['ITensor'] * len(self.input_tensor_names))}" + + _join_with(self.input_attrs.keys(), True) + + ")" + ) + schema = f"({', '.join(schema_chunks)})" + + if schema != expected_schema: + raise ValueError( + f"Unexpected schema {schema} received. Expected {expected_schema}." + ) + + if self.plugin_id in QDP_CREATORS: + plg_creator = trt.get_plugin_registry().get_creator(name, "1", namespace) + else: + attrs_types = {} + for key, value in kwargs.items(): + if isinstance(value, np.ndarray): + attrs_types[key] = (False, value.dtype) # (builtin?, type) + else: + attrs_types[key] = (True, type(value)) # (builtin?, type) + + plg_creator = _register_plugin_creator(name, namespace, attrs_types) + + fields = [] + for key, value in attrs.items(): + if isinstance(value, np.ndarray): + np_type = np.dtype(value.dtype) + if np_type == np.float16: + fields.append( + trt.PluginField( + key, value.tobytes(), trt.PluginFieldType.UNKNOWN + ) + ) + else: + fields.append( + trt.PluginField( + key, value, _numpy_to_plugin_field_type[np_type] + ) + ) + elif isinstance(value, str): + fields.append( + trt.PluginField(key, value.encode(), trt.PluginFieldType.CHAR) + ) + elif isinstance(value, bytes): + fields.append(trt.PluginField(key, value, trt.PluginFieldType.UNKNOWN)) + else: + fields.append( + trt.PluginField( + key, + np.array([value]), + _built_in_to_plugin_field_type[type(value)], + ) + ) + + plg = plg_creator.create_plugin( + name, + namespace, + trt.PluginFieldCollection(fields), + trt.TensorRTPhase.BUILD, + ) + plg.init( + self.register_func, + attrs, + self.impl_attr_names, + self.impl_func, + self.autotune_attr_names, + self.autotune_func, + self.expects_tactic, + ) + + return input_tensors, [], plg + + +class _TemplatePluginCreator(trt.IPluginCreatorV3Quick): + def __init__(self, name, namespace, attrs): + trt.IPluginCreatorV3Quick.__init__(self) + self.name = name + self.plugin_namespace = namespace + self.plugin_version = "1" + field_names = [] + for name, (builtin, type_) in attrs.items(): + if builtin: + if type_ is str: + field_names.append( + trt.PluginField(name, b"", trt.PluginFieldType.CHAR) + ) + elif type_ is bytes: + field_names.append( + trt.PluginField(name, b"", trt.PluginFieldType.UNKNOWN) + ) + else: + field_names.append( + trt.PluginField( + name, np.array([]), _built_in_to_plugin_field_type[type_] + ) + ) + else: + field_names.append( + trt.PluginField( + name, np.array([]), _numpy_to_plugin_field_type[np.dtype(type_)] + ) + ) + + self.field_names = trt.PluginFieldCollection(field_names) + + def create_plugin(self, name, namespace, fc, phase): + desc = QDP_REGISTRY[f"{namespace}::{name}"] + name = name + namespace = namespace + + attrs = {} + for f in fc: + if f.name not in desc.input_attrs: + raise AssertionError( + f"Unexpected attribute {f.name} provided to create_plugin. Expected one of {desc.input_attrs.keys()}." + ) + + attr_type_annot = desc.input_attrs[f.name] + if _is_numpy_array(attr_type_annot): + np_type = _infer_numpy_type(attr_type_annot) + if np_type == np.float16: + attrs[f.name] = np.frombuffer(f.data.tobytes(), dtype=np.float16) + else: + attrs[f.name] = f.data.astype(np_type) + else: + if issubclass(attr_type_annot, str): + attrs[f.name] = f.data.tobytes().decode("utf-8") + else: + attrs[f.name] = attr_type_annot(f.data) + + plg = _TemplatePlugin(name, namespace, desc.num_outputs) + plg.init( + desc.register_func, + attrs, + desc.impl_attr_names, + desc.impl_func, + desc.autotune_attr_names, + desc.autotune_func, + desc.expects_tactic, + ) + return plg + + +def _register_plugin_creator(name: str, namespace: str, attrs_types): + plg_registry = trt.get_plugin_registry() + plg_creator = _TemplatePluginCreator(name, namespace, attrs_types) + plg_registry.register_creator(plg_creator, namespace) + plg_creator = plg_registry.get_creator(name, "1", namespace) + QDP_CREATORS[f"{name}::{namespace}"] = plg_creator + return plg_creator + + +# Decorator for `tensorrt.plugin.register` +# By default, the plugin will be immediately registered in the TRT plugin registry +# During plugin development/when building engine, lazy registration may be used to delay plugin registration until the plugin is explicitly instantiated using `trt.plugin.op.ns.plugin_name(...)` +@public_api() +def register(plugin_id: str, lazy_register: bool = False) -> Callable: + """ + Wraps a function to register and describe a TensorRT plugin's IO characteristics. In addition, a complete plugin at least needs an `trt.plugin.impl` function to be registered. + + This API is only intended to be used as a decorator. The decorated function must have type hints for all inputs as well as return value. + + .. code-block:: text + + (inp0: TensorDesc, inp1: TensorDesc, ..., attr0: SupportedAttrType, attr1: SupportedAttrType, ...) -> Union[TensorDesc, Tuple[TensorDesc]] + + * Input tensors are declared first, each described by a tensor descriptor TensorDesc. + * Plugin attributes are declared next. "SupportedAttrType" must be one of: + * Supported built-in types: int, float, str, bool, bytes (Note: Lists/tuples of these types are not supported) + * 1-D Numpy arrays of the following types: int8, int16, int32, int64, float16, float32, float64, bool. These must be annotated with 'numpy.typing.NDArray[dtype]', where 'dtype' is the expected numpy dtype. + * If the plugin has only one output, the return annotation could be TensorDesc. Tuple[TensorDesc] could be used for any number of outputs. + + By default, the plugin will be immediately registered in the TRT plugin registry. Use the lazy_register argument to change this. + + Args: + plugin_id: An ID for the plugin in the form "{namespace}::{name}", + e.g. "my_project::add_plugin". The namespace is used to avoid collisions + so using your product/project name is recommended. + + lazy_register: During plugin development/when building engine, lazy registration may be used to delay plugin registration until the plugin is explicitly instantiated using `trt.plugin.op.ns.plugin_name(...)` + + .. code-block:: python + :linenos: + :caption: Registration of an elementwise plugin (output has same characteristics as the input) + + import tensorrt.plugin as trtp + + @trtp.register("my::add_plugin") + def add_plugin_desc(inp0: trtp.TensorDesc, block_size: int) -> Tuple[trtp.TensorDesc]: + return inp0.like() + + """ + + def decorator(register_func: Callable): + + plugin_ns, plugin_name = plugin_id.split("::") + _validate_name_and_namespace(plugin_ns, plugin_name) + + op_namespace = op.define_or_get(plugin_ns) + + if hasattr(op_namespace, plugin_name): + raise ValueError( + f"'{op.__class__.__name__}' already has a defintion for '{plugin_name}'" + ) + + ( + tensor_names, + input_attrs, + input_arg_schema, + attrs_types, + ) = _parse_register_inputs(register_func, lazy_register) + + plugin_def = PluginDef() + plugin_def.plugin_id = plugin_id + plugin_def.register_func = register_func + plugin_def.input_tensor_names = tensor_names + plugin_def.input_attrs = input_attrs + plugin_def.input_arg_schema = input_arg_schema + + num_outputs = _parse_register_return(register_func) + + plugin_def.num_outputs = num_outputs + QDP_REGISTRY[plugin_id] = plugin_def + + if not lazy_register: + _register_plugin_creator(plugin_name, plugin_ns, attrs_types) + + op_namespace.define(plugin_name, plugin_def) + + return register_func + + return decorator + + +# Decorator for `tensorrt.plugin.impl` +@public_api() +def impl(plugin_id: str) -> Callable: + """ + Wraps a function to define an implementation for a plugin already registered through `trt.plugin.register`. + + This API is only intended to be used as a decorator. The decorated function is not required to have type hints for input arguments or return value; + however, any type hints specified will be validated against the `trt.plugin.register` signature for consistency. + + The schema for the function is as follows: + + .. code-block:: text + + (inp0: Tensor, inp1: Tensor, ..., attr0: SupportedAttrType, attr1: SupportedAttrType, outputs: Tuple[Tensor], stream: int, tactic: Optional[int]) -> None + + * Input tensors are passed first, each described by a `Tensor`. + * Plugin attributes are declared next. + * Not all attributes included in `trt.plugin.register` must be specified here -- they could be a subset. + * Included attributes will be serialized to the TRT engine. Therefore, only attributes the plugin actually needs to perform inference (within the body of `trt.plugin.impl`) should be included. + * `tactic` is an optional argument. If the plugin is using custom tactics, it must be specified to receive the tactic value to use for the current execution of the plugin. + + Args: + plugin_id: The ID for the plugin in the form "{namespace}::{name}", which must match that used during `trt.plugin.register` + + .. code-block:: python + :linenos: + :caption: Implementation of an elementwise plugin with an OpenAI Triton kernel + + import tensorrt.plugin as trtp + import triton + import triton.language as tl + + @triton.jit + def add_kernel(x_ptr, y_ptr, n_elements, BLOCK_SIZE: tl.constexpr): + pid = tl.program_id(0) + offsets = pid * BLOCK_SIZE + tl.arange(0, BLOCK_SIZE) + mask = offsets < n_elements + x = tl.load(x_ptr + offsets, mask=mask) + tl.store(y_ptr + offsets, x + 1, mask=mask) + + @trtp.register("my::add_plugin") + def add_plugin_desc(inp0: trtp.TensorDesc, block_size: int) -> Tuple[trtp.TensorDesc]: + return inp0.like() + + @trtp.impl("my::add_plugin") + def add_plugin_impl(inp0: trtp.Tensor, block_size: int, outputs: Tuple[trtp.Tensor], stream: int) -> None: + + n = inp0.numel() + inp0_t = torch.as_tensor(inp0, device="cuda") + out_t = torch.as_tensor(outputs[0], device="cuda") + + add_kernel[(triton.cdiv(n, block_size),)](inp0_t, out_t, n, BLOCK_SIZE = block_size) + """ + + def decorator(impl_func: Callable): + if plugin_id not in QDP_REGISTRY: + raise ValueError( + f"Plugin {plugin_id} is not registered. Did you register it with tensorrt.plugin.register API?" + ) + + plugin_def = QDP_REGISTRY[plugin_id] + impl_attr_names, found_tactic = _validate_impl(impl_func, plugin_def) + + plugin_def.impl_func = impl_func + plugin_def.impl_attr_names = impl_attr_names + plugin_def.expects_tactic = found_tactic + return impl_func + + return decorator + + +# Decorator for `tensorrt.plugin.autotune` +@public_api() +def autotune(plugin_id: str) -> Callable: + """ + Wraps a function to define autotune logic for a plugin already registered through `trt.plugin.register`. + + Autotuning is the process by which TensorRT executes the plugin over IO type/format combinations, and any custom tactics advertised as being supported by the plugin. + The (type, format, tactic) combination with the lowest latency is used to execute the plugin once the engine is built. + + .. note:: An autotune function is optional. If not specified, TensorRT will assume the plugin only supports input types specified at network creation, output types specifeid through `trt.plugin.register`, and linear formats for all I/O. + + This API is only intended to be used as a decorator. The decorated function is not required to have type hints for input arguments or return value; however, any type hints specified will be validated against the `trt.plugin.register` signature for consistency. + + The schema for the function is as follows: + + .. code-block:: text + + (inp0: TensorDesc, inp1: TensorDesc, ..., attr0: SupportedAttrType, attr1: SupportedAttrType, outputs: Tuple[TensorDesc]) -> List[AutoTuneCombination] + + * Input tensors are passed first, each described by a :class:`TensorDesc`. + * Plugin attributes are declared next. Not all attributes included in `trt.plugin.register` must be specified here -- they could be a subset. + * The function should return a list of :class:`AutoTuneCombination`\s. + + Args: + plugin_id: The ID for the plugin in the form "{namespace}::{name}", which must match that used during `trt.plugin.register` + + .. code-block:: python + :linenos: + :caption: An elementwise add plugin which supports both FP32 and FP16 linear I/O and wants to be tuned over 2 custom tactics. + + import tensorrt.plugin as trtp + + @trtp.register("my::add_plugin") + def add_plugin_desc(inp0: trtp.TensorDesc, block_size: int) -> Tuple[trtp.TensorDesc]: + return inp0.like() + + @trtp.autotune("my::add_plugin") + def add_plugin_autotune(inp0: trtp.TensorDesc, block_size: int, outputs: Tuple[trtp.TensorDesc]) -> List[trtp.AutoTuneCombination]: + + return [trtp.AutoTuneCombination("FP32|FP16, FP32|FP16", "LINEAR", [1, 2])] + + .. code-block:: python + :linenos: + :caption: Same as above example but using index-by-index construction of an `AutoTuneCombination` + + import tensorrt.plugin as trtp + + @trtp.register("my::add_plugin") + def add_plugin_desc(inp0: trtp.TensorDesc, block_size: int) -> Tuple[trtp.TensorDesc]: + return inp0.like() + + @trtp.autotune("my::add_plugin") + def add_plugin_autotune(inp0: trtp.TensorDesc, block_size: int, outputs: Tuple[trtp.TensorDesc]) -> List[trtp.AutoTuneCombination]: + c = trtp.AutoTuneCombination() + c.pos(0, "FP32|FP16", "LINEAR") + c.pos(1, "FP32|FP16") # index 1 is the output. Omitting format is the same as declaring it to be LINEAR. + c.tactics([1, 2]) + return [c] + """ + + def decorator(autotune_func: Callable): + if plugin_id not in QDP_REGISTRY: + raise ValueError( + f"Plugin {plugin_id} is not registered. Did you register it with tensorrt.plugin.register API?" + ) + + plugin_def = QDP_REGISTRY[plugin_id] + autotune_attr_names = _validate_autotune(autotune_func, plugin_def) + + plugin_def.autotune_func = autotune_func + plugin_def.autotune_attr_names = autotune_attr_names + + return autotune_func + + return decorator diff --git a/python/packaging/bindings_wheel/tensorrt/plugin/_plugin_class.py b/python/packaging/bindings_wheel/tensorrt/plugin/_plugin_class.py new file mode 100644 index 00000000..c0910421 --- /dev/null +++ b/python/packaging/bindings_wheel/tensorrt/plugin/_plugin_class.py @@ -0,0 +1,322 @@ +# +# SPDX-FileCopyrightText: Copyright (c) 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. +# + +import tensorrt as trt +from typing import Tuple + +import numpy as np +from ._utils import _numpy_to_plugin_field_type, _built_in_to_plugin_field_type +from ._tensor import TensorDesc, Tensor, Shape, ShapeExpr, ShapeExprs +from ._autotune import _TypeFormatCombination + + +class _TemplatePlugin( + trt.IPluginV3, + trt.IPluginV3QuickCore, + trt.IPluginV3QuickBuild, + trt.IPluginV3QuickRuntime, +): + def __init__(self, name, namespace, num_outputs): + trt.IPluginV3.__init__(self) + trt.IPluginV3QuickCore.__init__(self) + trt.IPluginV3QuickBuild.__init__(self) + trt.IPluginV3QuickRuntime.__init__(self) + + self.plugin_version = "1" + self.input_types = [] + self.aliased_map = {} # output index -> input index + + self.plugin_namespace = namespace + self.plugin_name = name + self.num_outputs = num_outputs + + self.autotune_combs = [] + self.supported_combs = {} + self.curr_comb = None + self.expects_tactic = False + + def init( + self, + register_function, + attrs, + impl_attr_names, + impl_function, + autotune_attr_names, + autotune_function, + expects_tactic, + ): + self.register_function = register_function + self.impl_function = impl_function + self.attrs = attrs + self.impl_attr_names = impl_attr_names + self.autotune_attr_names = autotune_attr_names + self.autotune_function = autotune_function + self.expects_tactic = expects_tactic + + def get_capability_interface(self, type): + return self + + def get_output_data_types(self, input_types, ranks): + self.input_types = input_types + + input_descs = [None] * len(input_types) + input_desc_map = {} + for i in range(len(input_types)): + input_descs[i] = TensorDesc() + input_descs[i].dtype = input_types[i] + input_descs[i].shape_expr = ShapeExprs(ranks[i], _is_dummy=True) + input_descs[i]._immutable = True + input_desc_map[id(input_descs[i])] = i + + output_descs = self.register_function(*input_descs, **self.attrs) + if not isinstance(output_descs, Tuple): + output_descs = tuple([output_descs]) + + self.output_types = [] + + for i in range(len(output_descs)): + self.output_types.append(output_descs[i].dtype) + + if output_descs[i].get_aliased() is not None: + self.aliased_map[i] = input_desc_map[id(output_descs[i].get_aliased())] + else: + self.aliased_map[i] = -1 + + return self.output_types + + def get_fields_to_serialize(self): + fields = [] + for key, value in self.attrs.items(): + if key in self.impl_attr_names: + if isinstance(value, np.ndarray): + if np.dtype(value.dtype) == np.float16: + fields.append( + trt.PluginField( + key, value.tobytes(), trt.PluginFieldType.UNKNOWN + ) + ) + else: + fields.append( + trt.PluginField( + key, + value, + _numpy_to_plugin_field_type[np.dtype(value.dtype)], + ) + ) + elif isinstance(value, str): + fields.append( + trt.PluginField(key, value.encode(), trt.PluginFieldType.CHAR) + ) + elif isinstance(value, bytes): + fields.append( + trt.PluginField(key, value, trt.PluginFieldType.UNKNOWN) + ) + else: + fields.append( + trt.PluginField( + key, + np.array([value]), + _built_in_to_plugin_field_type[type(value)], + ) + ) + + return trt.PluginFieldCollection(fields) + + def get_output_shapes(self, inputs, shape_inputs, exprBuilder): + assert len(shape_inputs) == 0 # Shape inputs are not yet supported for QDPs + ShapeExpr._exprBuilder = exprBuilder + self.input_descs = [] + for i in range(len(inputs)): + desc = TensorDesc() + inp = inputs[i] + + desc.dtype = self.input_types[i] + desc.shape_expr = ShapeExprs(len(inp)) + for j in range(len(inp)): + desc.shape_expr[j] = ShapeExpr(inp[j]) + desc._immutable = True + + self.input_descs.append(desc) + + self.output_descs = self.register_function(*self.input_descs, **self.attrs) + if not isinstance(self.output_descs, Tuple): + self.output_descs = tuple([self.output_descs]) + + for idx, desc in enumerate(self.output_descs): + if desc.is_size_tensor: + desc._set_index(idx) + + output_exprs = [] + for i in range(len(self.output_descs)): + exprs = trt.DimsExprs(len(self.output_descs[i].shape_expr)) + for j in range(len(exprs)): + exprs[j] = self.output_descs[i].shape_expr[j]._expr + + output_exprs.append(exprs) + + return output_exprs + + def configure_plugin(self, inputs, outputs): + self.curr_comb = _TypeFormatCombination() + self.curr_comb.types = [inp.desc.type for inp in inputs] + [ + out.desc.type for out in outputs + ] + self.curr_comb.layouts = [inp.desc.format for inp in inputs] + [ + out.desc.format for out in outputs + ] + + def get_supported_format_combinations(self, in_out, num_inputs): + if self.autotune_function is not None: + if len(self.autotune_attr_names) > 0: + val = [self.attrs[k] for k in self.autotune_attr_names] + else: + val = () + + for i, desc in enumerate(in_out): + if i < num_inputs: + self.input_descs[i]._immutable = False + self.input_descs[i].shape = Shape(desc) + self.input_descs[i].format = desc.desc.format + self.input_descs[i].scale = desc.desc.scale + self.input_descs[i]._immutable = True + else: + self.output_descs[i - num_inputs]._immutable = False + self.output_descs[i - num_inputs].shape = Shape(desc) + self.output_descs[i - num_inputs].format = desc.desc.format + self.output_descs[i - num_inputs].scale = desc.desc.scale + self.output_descs[i - num_inputs]._immutable = True + + self.autotune_combs = self.autotune_function( + *self.input_descs, *val, self.output_descs + ) + + if len(self.autotune_combs) == 0: + default_comb = [None] * len(in_out) + comb = _TypeFormatCombination(len(in_out)) + for j in range(len(in_out)): + default_comb[j] = trt.PluginTensorDesc() + default_comb[j].type = ( + self.input_types[j] + if j < num_inputs + else self.output_descs[j - num_inputs].dtype + ) + default_comb[j].format = trt.TensorFormat.LINEAR + comb.types[j] = default_comb[j].type + comb.layouts[j] = default_comb[j].format + + self.supported_combs[comb] = set() + + return default_comb + + all_combs = [] + for comb in self.autotune_combs: + all_combs.extend(comb._get_combinations()) + + ret_supported_combs = [] + self.supported_combs = {} + + for i, comb in enumerate(all_combs): + value = self.supported_combs.get(comb) + if value is not None: + value.update(set(comb.tactics) if comb.tactics is not None else set()) + else: + self.supported_combs[comb] = ( + set(comb.tactics) if comb.tactics is not None else set() + ) + for j in range(len(in_out)): + curr_comb = trt.PluginTensorDesc() + curr_comb.type = comb.types[j] + curr_comb.format = comb.layouts[j] + ret_supported_combs.append(curr_comb) + + return ret_supported_combs + + def enqueue( + self, + input_desc, + output_desc, + inputs, + outputs, + in_strides, + out_strides, + stream, + ): + input_tensors = [None] * (len(inputs)) + aliased_input_idxs = list(self.aliased_map.values()) + + for i in range(len(inputs)): + input_tensors[i] = Tensor() + input_tensors[i].dtype = input_desc[i].type + input_tensors[i].shape = Shape(input_desc[i]) + input_tensors[i].format = input_desc[i].format + input_tensors[i].scale = input_desc[i].scale + input_tensors[i].data_ptr = inputs[i] + input_tensors[i]._stream = stream + input_tensors[i]._read_only = i not in aliased_input_idxs + input_tensors[i].strides = in_strides[i] + + output_tensors = [None] * (len(outputs)) + for i in range(len(outputs)): + output_tensors[i] = Tensor() + output_tensors[i].dtype = output_desc[i].type + output_tensors[i].shape = Shape(output_desc[i]) + output_tensors[i].format = output_desc[i].format + output_tensors[i].scale = output_desc[i].scale + output_tensors[i].data_ptr = outputs[i] + output_tensors[i]._stream = stream + output_tensors[i]._read_only = False + output_tensors[i].strides = out_strides[i] + + for i, j in self.aliased_map.items(): + output_tensors[i]._aliased_to = input_tensors[j] + input_tensors[j]._aliased_to = output_tensors[i] + + for t in input_tensors: + t._immutable = True + + for t in output_tensors: + t._immutable = True + + if len(self.impl_attr_names) > 0: + val = [self.attrs[k] for k in self.impl_attr_names] + else: + val = () + + if self.expects_tactic: + self.impl_function( + *input_tensors, *val, output_tensors, stream, self._tactic + ) + else: + self.impl_function(*input_tensors, *val, output_tensors, stream=stream) + + def get_aliased_input(self, output_index: int): + return self.aliased_map[output_index] + + def get_valid_tactics(self): + tactics = self.supported_combs.get(self.curr_comb) + assert tactics is not None + return list(tactics) + + def set_tactic(self, tactic): + self._tactic = tactic + + def clone(self): + cloned_plugin = _TemplatePlugin( + self.plugin_name, self.plugin_namespace, self.num_outputs + ) + cloned_plugin.__dict__.update(self.__dict__) + return cloned_plugin diff --git a/python/packaging/bindings_wheel/tensorrt/plugin/_tensor.py b/python/packaging/bindings_wheel/tensorrt/plugin/_tensor.py new file mode 100644 index 00000000..5e9e711b --- /dev/null +++ b/python/packaging/bindings_wheel/tensorrt/plugin/_tensor.py @@ -0,0 +1,808 @@ +# +# SPDX-FileCopyrightText: Copyright (c) 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. +# + +import tensorrt as trt +from typing import Tuple, Union +import numpy as np +from ._export import public_api + +# Symbolic expression for a given dimension of a tensor +@public_api() +class ShapeExpr: + """ + Symbolic expression for single dimension of a tensor + """ + _exprBuilder = None # trt.IExprBuilder instance. Populated when a shape-calculation context is entered. + + def __init__(self, value: Union[int, trt.IDimensionExpr, "ShapeExpr"] = None): + """ + Args: + value (Union[int, trt.IDimensionExpr, ShapeExpr], optional): Constant or another symbolic expression. Defaults to creating a fake shape expression. + """ + self._is_dummy = False + self._dim_expr = None + self._is_size_tensor = False + if value is None: + self._is_dummy = True + elif isinstance(value, int): + if self._exprBuilder is None: + self._dim_expr = None + self._is_dummy = True + else: + self._dim_expr = ShapeExpr._exprBuilder.constant(value) + elif isinstance(value, trt.IDimensionExpr): + self._dim_expr = value + elif isinstance(value, ShapeExpr): + self._dim_expr = value._dim_expr + self._is_dummy = value._is_dummy + self._is_size_tensor = value._is_size_tensor + + def _op(self, op: trt.DimensionOperation, other: Union[int, "ShapeExpr"]): + if self._is_size_tensor: + raise ValueError("It is not permitted to perform binary operations on size tensor expressions") # trt limitation + if self._is_dummy: + return ShapeExpr() + if isinstance(other, int): + other = ShapeExpr(other) + return ShapeExpr(ShapeExpr._exprBuilder.operation(op, self._expr, other._expr)) + + # Binary operations for +, -, *, //, ==. < + # Those for ceil_div, max and min are provided as top-level functions of tensorrt.plugin + def __add__(self, other: Union[int, "ShapeExpr"]): + return self._op(trt.DimensionOperation.SUM, other) + + def __sub__(self, other: Union[int, "ShapeExpr"]): + return self._op(trt.DimensionOperation.SUB, other) + + def __mul__(self, other: Union[int, "ShapeExpr"]): + return self._op(trt.DimensionOperation.PROD, other) + + def __floordiv__(self, other: Union[int, "ShapeExpr"]): + return self._op(trt.DimensionOperation.FLOOR_DIV, other) + + def __eq__(self, other: Union[int, "ShapeExpr"]): + return self._op(trt.DimensionOperation.EQUAL, other) + + def __lt__(self, other: Union[int, "ShapeExpr"]): + return self._op(trt.DimensionOperation.LESS, other) + + def __repr__(self): + if self._is_dummy: + return f"FakeShapeExpr[id={id(self)}]" + elif not self.is_constant: + return f"ShapeExpr[id={id(self)}]" + return f"ShapeExpr[{self._expr.get_constant_value()}]" + + # A ShapeExpr may be "fake" when it is accessed in a non-shape calculation context. Fake `ShapeExpr`s are externally indistinguishable unless `is_constant` or `constant_value` is required. + # Therefore, constant checks/access must occur conditionally after evaluating `is_fake`. + @property + def is_fake(self) -> bool: + """ + A ShapeExpr may be "fake" when it is accessed in a non-shape calculation context. + Fake `ShapeExpr`s are externally indistinguishable unless `is_constant` or `constant_value` is required. + """ + return self._is_dummy + + @property + def is_size_tensor(self) -> bool: + """ + `True` if this represents a size tensor, `False` otherwise. + """ + return self._is_size_tensor + + @property + def is_constant(self) -> bool: + """ + `True` if this shape expression is a build-time constant, `False` otherwise. + + Raises: + RuntimeError: For fake :class:`ShapeExpr`\s. Check :attr:`is_fake` to determine accessibility. + """ + if self._is_dummy: + raise RuntimeError( + "Not accessible for fake 'ShapeExpr's. Check is_fake to determine accessibility." + ) + return self._expr.is_constant() + + def constant_value(self) -> int: + """ + Return value of the constant shape expression. + + Raises: + RuntimeError: For non-constant shape expressions. Check :attr:`is_constant` to determine accessibility. + """ + if not self.is_constant: + raise RuntimeError( + "Not accessible for non-constant shape expressions. Check is_constant to determine accessibility." + ) + return self._expr.get_constant_value() + + # Evaluate the underlying trt.IDimensionExpr, if so done lazily + @property + def _expr(self): + return self._dim_expr + +@public_api() +class SizeTensorShapeExpr(ShapeExpr): + """ + Extends :class:`ShapeExpr` + + A shape expression that represent a size tensor + + """ + def __init__(self, size_tensor_desc: "SizeTensorDesc"): + """ + .. note:: It is recommended to use :attr:`SizeTensorDesc.expr` to get a :class:`SizeTensorShapeExpr` representing a size tensor + """ + super().__init__() + self._is_size_tensor = True + self._is_dummy = size_tensor_desc.opt.is_fake + self._size_tensor_desc = size_tensor_desc + + def _op(self, op: trt.DimensionOperation, other: Union[int, "ShapeExpr"]): + raise ValueError("It is not permitted to perform binary operations on size tensor expressions") # TRT limitation + + @property + def is_constant(self): + if self._is_dummy: + raise RuntimeError( + "Not accessible for fake 'ShapeExpr's. Check is_fake to determine accessibility." + ) + return False + + @property + def _expr(self): + if self._dim_expr is not None: + return self._dim_expr + + self._dim_expr = super()._exprBuilder.declare_size_tensor(self._size_tensor_desc.index, self._size_tensor_desc.opt._expr, self._size_tensor_desc.upper_bound._expr) + return self._dim_expr + + def __repr__(self): + return f"ShapeExpr[is_size_tensor = True, id={id(self)}]" + +# Iterable holding `ShapeExpr`s +@public_api() +class ShapeExprs: + def __init__(self, length: int, _is_dummy: bool = False): + """ + Iterable holding :class:`ShapeExpr`\s + + Args: + length (int): Number of dimensions of the tensor + """ + self._length = length + self._is_dummy = _is_dummy + if _is_dummy: + self._shapes = [ShapeExpr()] * length + else: + self._shapes = [None] * length + + @classmethod + def from_tuple(cls, shape_exprs: Tuple[Union[ShapeExpr, int]]) -> "ShapeExprs": + """ + Args: + shape_exprs (Tuple[Union[ShapeExpr, int]]): Tuple to construct :class:`ShapeExprs` from + """ + shape_exprs_ = tuple([e if isinstance(e, ShapeExpr) else ShapeExpr(e) for e in shape_exprs]) + inst = cls(len(shape_exprs_)) + inst._shapes = list(shape_exprs_) + return inst + + def numel(self) -> ShapeExpr: + """ + Returns a symbolic expression for the number of elements + """ + ret = ShapeExpr(1) + for s in self._shapes: + ret *= s + return ret + + def __iter__(self): + return iter(self._shapes) + + def __getitem__(self, index): + return self._shapes[index] + + def __len__(self): + return self._length + + def __setitem__(self, index, shape): + if index >= self._length: + raise IndexError("Index out of range") + self._shapes[index] = shape + + def __repr__(self): + return f"ShapeExprs[{', '.join([s.__repr__() for s in self._shapes])}]" + + +# Numerical representation of a tensor shape +@public_api() +class Shape: + """ + Numerical representation of a tensor shape + """ + def __init__( + self, tensor_desc: Union[int, trt.DynamicPluginTensorDesc, trt.PluginTensorDesc] + ): + self._desc = tensor_desc + self._is_dynamic = None # set lazily + if isinstance(tensor_desc, trt.DynamicPluginTensorDesc): + self._length = len(tensor_desc.desc.dims) + self._shapes = tensor_desc.desc.dims + elif isinstance(tensor_desc, trt.PluginTensorDesc): + self._length = len(tensor_desc.dims) + self._shapes = tensor_desc.dims + + def numel(self) -> int: + """ + Number of elements contained + + Raises: + ValueError: When :attr:`is_dynamic` is `True` + """ + if self.is_dynamic: + raise ValueError("Shape has at least one dynamic dimension.") + return int(np.prod(self._shapes)) + + def __iter__(self): + yield from self._shapes + + def __getitem__(self, index): + return self._shapes[index] + + def __len__(self): + return self._length + + def __str__(self): + return "Shape" + str(tuple(self)) + + @property + def is_dynamic(self) -> bool: + """ + `True` if this tensor has at least one dynamic dimension, `False` otherwise. + """ + if self._is_dynamic is not None: + return self._is_dynamic + + self._is_dynamic = False + for d in self._shapes: + if d == -1: + self._is_dynamic = True + + return self._is_dynamic + + @property + def opt(self) -> Tuple[int]: + """ + Optimum value of dimensions specified for auto-tuning. + """ + if not self.is_dynamic: + raise ValueError("opt property is only accessible if is_dynamic is true") + return tuple(self._desc.opt) + + @property + def min(self) -> Tuple[int]: + """ + Lower bounds on tensor's dimensions. + """ + if not self.is_dynamic: + raise ValueError("min property is only accessible if is_dynamic is true") + return tuple(self._desc.min) + + @property + def max(self) -> Tuple[int]: + """ + Upper bounds on tensor's dimensions. + """ + if not self.is_dynamic: + raise ValueError("max property is only accessible if is_dynamic is true") + return tuple(self._desc.max) + + def __setitem__(self, index, val): + if index >= self._length: + raise IndexError("Index out of range") + self._shapes.desc[index] = val + + +# Descriptor for a tensor +# A `TensorDesc` never contains nor refers to any tensor data. +@public_api() +class TensorDesc: + """ + Descriptor for a tensor + A `TensorDesc` never contains nor refers to any tensor data. + """ + def __init__(self, shape_expr: ShapeExprs = None, dtype: trt.DataType = None, format: trt.TensorFormat = None, scale: float = None): + """ + Args: + shape_expr (ShapeExprs): The data with which to initialize the tensor. + dtype (trt.DataType): The data type of the tensor. + format (trt.TensorFormat): Format (layout) of the tensor. + scale (float): Scale for INT8 data type. + + .. code-block:: python + :linenos: + :caption: Creates a TensorDesc with constant shape expressions + + tensor = trt.TensorDesc((10, 2, 32, 32), dtype=trt.float32) + + .. code-block:: python + :linenos: + :caption: Creates a TensorDesc from shape expression of another TensorDesc + + tensor = trt.from_shape_expr(other.shape_expr, dtype=trt.float32) + """ + + # `TensorDesc` may or may not have `Shape` information but always has symbolic shape expressions and dtype + self._shape_expr = shape_expr + self._dtype = dtype + + # `shape`, `format`, and `scale` are only accessible if `has_shape`. Presently, this would be inside autotune. + self._shape = None + self._format = format + self._scale = scale + + self._aliased_to = None + self._immutable = False + + def numel(self) -> int: + """ + Returns: + Returns an int with the number of elements of the tensor. + + .. warning:: + Should only be called when TensorDesc.has_shape is true. If a symbolic expression for the number of elements is required, query TensorDesc.shape_expr.numel(). + """ + if not self.has_shape: + raise ValueError( + "TensorDesc has no shape information available at this stage. Inspect TensorDesc.has_shape to determine availability." + ) + return int(np.prod(self.shape)) + + @property + def ndim(self) -> int: + """ + Number of dimensions + """ + return len(self._shape_expr) + + @property + def is_size_tensor(self): + return False + + # Return a `TensorDesc` that has identical properties to `self` but is mutable + def like(self) -> "TensorDesc": + """ + Returns: + Returns a TensorDesc which has identical properties to this tensor, and is mutable. + + .. code-block:: python + :linenos: + :caption: Communicate that output tensor has identical properties to the input tensor + + @tensorrt.plugin.register("my::plugin") + def _(inp: tensorrt.plugin.TensorDesc) -> tensorrt.plugin.TensorDesc: + return inp.like() + """ + cloned = TensorDesc() + cloned.__dict__.update(self.__dict__) + cloned._immutable = False + return cloned + + # Return a `TensorDesc` that has identical properties to `self` AND is aliased to `self` (would result in a `Tensor` during enqueue sharing the same data buffer) + def aliased(self) -> "TensorDesc": + """ + Returns: + Returns a TensorDesc which has identical properties and is aliased to this tensor (would result in a `Tensor` during enqueue sharing the same data buffer). + Returned TensorDesc is immutable. + + .. code-block:: python + :linenos: + :caption: Communicate that output tensor has identical properties to the input tensor + + @tensorrt.plugin.register("my::plugin") + def _(inp: tensorrt.plugin.TensorDesc) -> tensorrt.plugin.TensorDesc: + return inp.aliased() + """ + cloned = TensorDesc() + cloned.__dict__.update(self.__dict__) + cloned._immutable = False + cloned._aliased_to = self + cloned._immutable = True + return cloned + + def get_aliased(self) -> "TensorDesc": + """ + Returns: + Returns a TensorDesc for the tensor which this tensor is aliased to. Returns None is this tensor is not aliased to any other tensor. + """ + return self._aliased_to + + def _validate_has_shape(self) -> None: + if not self.has_shape: + raise ValueError( + "TensorDesc has no shape information available at this stage. Inspect TensorDesc.has_shape to determine availability." + ) + + def _validate_not_immutable(self): + if hasattr(self, "_immutable") and self._immutable: + raise ValueError("Cannot modify immutable TensorDesc") + + @property + def shape_expr(self) -> ShapeExprs: + """ + Symbolic expressions for the tensor shape. + """ + return self._shape_expr + + @property + def dtype(self) -> trt.DataType: + """ + Data type of the tensor. + """ + return self._dtype + + @property + def shape(self) -> Shape: + """ + The (concrete) shape of the tensor. + + .. warning:: + Only accessible when TensorDesc.has_shape is true. + """ + self._validate_has_shape() + return self._shape + + @property + def format(self) -> trt.TensorFormat: + """ + The format of the tensor. + + .. warning:: + Only accessible when TensorDesc.has_shape is true. + """ + self._validate_has_shape() + return self._format + + @property + def scale(self) -> float: + """ + Scale for INT8 data type. + + .. warning:: + Only accessible when TensorDesc.has_shape is true. + """ + self._validate_has_shape() + return self._scale + + + @shape_expr.setter + def shape_expr(self, value): + self._shape_expr = value + + @dtype.setter + def dtype(self, value): + self._dtype = value + + @shape.setter + def shape(self, value): + self._validate_not_immutable() + self._shape = value + + @format.setter + def format(self, value): + self._validate_not_immutable() + self._format = value + + @scale.setter + def scale(self, value): + self._validate_not_immutable() + self._scale = value + + @property + def is_aliased(self) -> bool: + """ + True if this tensor is aliased to another tensor, False otherwise. + """ + return self._aliased_to is not None + + @property + def has_shape(self) -> bool: + """ + True if this tensor has concrete shape information, False otherwise. + """ + return self._shape is not None + + @property + def is_dynamic(self) -> bool: + """ + `True` if this tensor has at least one dynamic dimension, `False` otherwise. + """ + if not self.has_shape: + raise ValueError( + "TensorDesc has no shape information available at this stage. Inspect TensorDesc.has_shape to determine availability." + ) + return self.shape.is_dynamic + + @property + def has_shape_expr(self) -> bool: + """ + True if this tensor has symbolic shape expressions, False otherwise. + """ + return self.shape_expr is not None + + def __setattr__(self, name, value): + if hasattr(self, "_immutable") and self._immutable and name != "_immutable": + raise ValueError("Cannot modify immutable TensorDesc properties") + super().__setattr__(name, value) + +@public_api() +class SizeTensorDesc(TensorDesc): + """ + Extends :class:`TensorDesc` + + Descriptor for a size tensor: a scalar of either INT32 or INT64 data type used to express the extent of a data-dependent dimension. + """ + def __init__(self, opt: ShapeExpr, upper_bound: ShapeExpr): + """ + Args: + opt (ShapeExpr): Symbolic expression for the extent of this size tensor to use in the autotune process of the engine build + upper_bound (ShapeExpr): Symbolic expression for the upper-bound of this size tensor + + .. note:: It is recommended to construct a size tensor using :func:`size_tensor` instead of using this constructor directly + """ + super().__init__(ShapeExprs(0), trt.int32) + self._opt = opt + self._upper_bound = upper_bound + self._index = None + self._expr = SizeTensorShapeExpr(self) + + @property + def is_size_tensor(self): + return True + + @property + def opt(self) -> ShapeExpr: + """ + Symbolic expression for the extent of this size tensor to use in the autotune process of the engine build + """ + return self._opt + + @property + def upper_bound(self) -> ShapeExpr: + """ + Symbolic expression for the upper-bound of this size tensor + """ + return self._upper_bound + + @property + def index(self) -> int: + """ + Output index at which this size tensor resides + """ + return self._index + + def _set_index(self, idx: int): + self._index = idx + + def expr(self) -> SizeTensorShapeExpr: + """ + Symbolic expression for this size tensor + """ + return self._expr + + +# A tensor representation that carries data +@public_api() +class Tensor: + """ + Representation of a tensor that carries data + + :class:`Tensor` objects are strictly *descriptors* of a tensor with an underlying data buffer. `tensorrt.plugin` does not provide any APIs that perform standard data-altering operations on :class:`Tensor`\s. + + Supports `__cuda_array_interface__` for interoperability with other frameworks. + + """ + def __init__(self): + self._data_ptr = None + self._shape = None + self._format = None + self._dtype = None + self._scale = None + self._strides = None + + self._aliased_to = None + self._stream = None + self._read_only = None + self._immutable = False + + @property + def ndim(self) -> int: + """ + Number of dimensions + """ + return len(self._shape) + + @property + def data_ptr(self) -> int: + """ + Pointer to the data buffer of this tensor + """ + return self._data_ptr + + @property + def dtype(self) -> trt.DataType: + """ + Data type of the tensor. + """ + return self._dtype + + @property + def shape(self) -> Shape: + """ + The (concrete) shape of the tensor. + """ + return self._shape + + @property + def format(self) -> trt.TensorFormat: + """ + The format of the tensor. + """ + return self._format + + @property + def scale(self) -> float: + """ + Scale for INT8 data type. + """ + return self._scale + + @property + def strides(self) -> Tuple[int]: + """ + Strides of this tensor. + """ + return self._strides + + @data_ptr.setter + def data_ptr(self, value): + self._data_ptr = value + + @dtype.setter + def dtype(self, value): + self._dtype = value + + @shape.setter + def shape(self, value): + self._shape = value + + @format.setter + def format(self, value): + self._format = value + + @scale.setter + def scale(self, value): + self._scale = value + + @strides.setter + def strides(self, value): + self._strides = value + + def numel(self) -> int: + """ + Returns the number of elements of the tensor + + Raises: + ValueError: If the tensor has a data-dependent dimension. Examine :attr:`is_data_dependent` to determine whether the tensor is data-dependent. + + Returns: + int: Number of elements of the tensor + """ + if self.is_data_dependent: + raise ValueError( + "Tensor has a data-dependent dimension. Examine Tensor.shape to determine wildcards (representing data-dependent dimensions)." + ) + return int(np.prod(self._shape)) + + @property + def __cuda_array_interface__(self): + if self._dtype in [trt.DataType.BF16, trt.DataType.FP8, trt.DataType.INT4]: + raise ValueError( + f"Handling {self._dtype} via '__cuda_array_interface__' is not supported" + ) + + desc = { + "shape": tuple(self._shape), + "typestr": np.dtype(trt.nptype(self._dtype)).str, + } + desc["stream"] = self._stream + desc["version"] = 3 + desc["data"] = ( + self._data_ptr, + False, + ) # torch does not support read_only flag. Always set to False -- it is user's responsibility to respect implied read-write restriction(s). + desc["strides"] = tuple( + [s * np.dtype(trt.nptype(self._dtype)).itemsize for s in self._strides] + ) + + return desc + + def __setattr__(self, name, value): + if hasattr(self, "_immutable") and self._immutable and name != "_immutable": + raise ValueError("Cannot modify immutable Tensor properties") + super().__setattr__(name, value) + + def get_aliased(self) -> "Tensor": + """ + Returns: + Returns :class:`Tensor` of the tensor which this tensor is aliased to. Returns None is this tensor is not aliased to any other tensor. + """ + return self._aliased_to + + @property + def is_aliased(self): + """ + True if this tensor is aliased to another tensor, False otherwise. + """ + return self._aliased_to is None + + @property + def is_data_dependent(self): + """ + True if this tensor contains at least one data-dependent dimension, False otherwise. + """ + return self._shape.is_dynamic + + # Return a `Tensor` which has the same `data_ptr` as `self` but has the provided shape. + def aliased(self, shape: Union[Shape, Tuple[int], trt.PluginTensorDesc] = None) -> "Tensor": + """ + Return a :class:`Tensor` which has the same :attr:`data_ptr` as this but has the provided `shape`. + + Args: + shape (Union[Shape, Tuple[int], trt.PluginTensorDesc], optional): Required shape of the new tensor (must have the same volume). Defaults to same shape. + + Raises: + ValueError: If `shape` is not a supported type or if it does not have the same volume + """ + cloned = Tensor() + cloned.__dict__.update(self.__dict__) + cloned._immutable = False + if isinstance(shape, trt.PluginTensorDesc): + cloned._shape = Shape(shape) + elif isinstance(shape, Shape): + cloned._shape = shape + elif isinstance(shape, tuple): + desc = trt.PluginTensorDesc() + desc.dims = shape + desc.type = self._dtype + desc.format = self._format + desc.scale = self._scale + cloned._shape = Shape(desc) + elif shape is None: + pass + else: + raise ValueError("Unsupported type for 'shape'") + + # If either the `shape` or self._shape has a wildcard, we allow aliasing + if not self.is_data_dependent and cloned.is_data_dependent: + if cloned._shape.numel() > self.numel(): + raise ValueError("Volume of this tensor is less than the provided 'shape'.") + + cloned._aliased_to = self + return cloned diff --git a/python/packaging/bindings_wheel/tensorrt/plugin/_top_level.py b/python/packaging/bindings_wheel/tensorrt/plugin/_top_level.py new file mode 100644 index 00000000..03705e0f --- /dev/null +++ b/python/packaging/bindings_wheel/tensorrt/plugin/_top_level.py @@ -0,0 +1,132 @@ +# +# SPDX-FileCopyrightText: Copyright (c) 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. +# + +from typing import Union, Tuple +import tensorrt as trt +from ._tensor import ShapeExpr, TensorDesc, ShapeExprs, SizeTensorDesc +from ._export import public_api + +# Miscellaneous top-level functions accessible through `tensorrt.plugin` + +# Performs `trt.DimensionOperation.CEIL_DIV` +@public_api() +def cdiv(first: Union[int, ShapeExpr], second: Union[int, ShapeExpr]) -> ShapeExpr: + """ + Computes symbolic ceiling division of `first` by `second` + + Args: + first (Union[int, ShapeExpr]): Dividend + second (Union[int, ShapeExpr]): Divisor + + Raises: + ValueError: If both arguments are `int`\s or if `second` evaluates to 0 + + Returns: + ShapeExpr: Symbolic expression for the ceiling division of `first` by `second` + """ + if isinstance(first, int): + if isinstance(second, int): + raise ValueError("Both arguments cannot be 'int's") + first = ShapeExpr(first) + + return first._op(trt.DimensionOperation.CEIL_DIV, second) + + +# Performs `trt.DimensionOperation.MAX` +@public_api() +def max(first: Union[int, ShapeExpr], second: Union[int, ShapeExpr]) -> ShapeExpr: + """ + Computes the maximum of `first` and `second` + + Args: + first (Union[int, ShapeExpr]): First operand + second (Union[int, ShapeExpr]): Second operand + + Raises: + ValueError: If both arguments are `int`\s + + Returns: + ShapeExpr: Symbolic expression for the maximum of `first` and `second` + """ + if isinstance(first, int): + if isinstance(second, int): + raise ValueError("Both arguments cannot be 'int's") + first = ShapeExpr(first) + + return first._op(trt.DimensionOperation.MAX, second) + + +# Performs `trt.DimensionOperation.MIN` +@public_api() +def min(first: Union[int, ShapeExpr], second: Union[int, ShapeExpr]) -> ShapeExpr: + """ + Computes the minimum of `first` and `second` + + Args: + first (Union[int, ShapeExpr]): First operand + second (Union[int, ShapeExpr]): Second operand + + Raises: + ValueError: If both arguments are `int`\s + + Returns: + ShapeExpr: Symbolic expression for the minimum of `first` and `second` + """ + if isinstance(first, int): + if isinstance(second, int): + raise ValueError("Both arguments cannot be 'int's") + first = ShapeExpr(first) + + return first._op(trt.DimensionOperation.MIN, second) + + +# Declare a size tensor descriptor with the specified autotune shape expression `opt` and `upper-bound` shape expression +@public_api() +def size_tensor(opt: ShapeExpr, upper_bound: ShapeExpr) -> SizeTensorDesc: + """ + Constructs a size tensor with the specified autotune shape expression `opt` and `upper_bound` + + Args: + opt (ShapeExpr): Symbolic expression for the extent of this size tensor to use in the autotune process of the engine build + upper_bound (ShapeExpr): Symbolic expression for the upper-bound of this size tensor + + Returns: + SizeTensorDesc: A tensor descriptor for a size tensor with the specified autotune extent and upper-bound + """ + return SizeTensorDesc(opt, upper_bound) + +# Create a TensorDesc using shape expressions and a dtype +@public_api() +def from_shape_expr(shape_expr: Union[Tuple[Union[ShapeExpr, int]], ShapeExprs], dtype: trt.DataType) -> TensorDesc: + """ + Constructs a tensor descriptor with the specified shape expression and data type + + Args: + shape_expr (Union[Tuple[Union[ShapeExpr, int]], ShapeExprs]): Expressions or constants denoting the shape of the tensor + dtype (trt.DataType): Data type of the tensor + + Returns: + TensorDesc: Tensor descriptor with the specified shape expression and data type + """ + if isinstance(shape_expr, tuple): + shape_expr_ = ShapeExprs.from_tuple(shape_expr) + else: + shape_expr_ = shape_expr + + return TensorDesc(shape_expr_, dtype) + + diff --git a/python/packaging/bindings_wheel/tensorrt/plugin/_utils.py b/python/packaging/bindings_wheel/tensorrt/plugin/_utils.py new file mode 100644 index 00000000..fd917f16 --- /dev/null +++ b/python/packaging/bindings_wheel/tensorrt/plugin/_utils.py @@ -0,0 +1,77 @@ +# +# SPDX-FileCopyrightText: Copyright (c) 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. +# + +import tensorrt as trt +import numpy as np +import typing + +_numpy_to_plugin_field_type = { + np.dtype('int32'): trt.PluginFieldType.INT32, + np.dtype('int16'): trt.PluginFieldType.INT16, + np.dtype('int8'): trt.PluginFieldType.INT8, + np.dtype('bool'): trt.PluginFieldType.INT8, + np.dtype('int64'): trt.PluginFieldType.INT64, + np.dtype('float32'): trt.PluginFieldType.FLOAT32, + np.dtype('float64'): trt.PluginFieldType.FLOAT64, + np.dtype('float16'): trt.PluginFieldType.FLOAT16 +} + +_built_in_to_plugin_field_type = { + int: trt.PluginFieldType.INT64, + float: trt.PluginFieldType.FLOAT64, + bool: trt.PluginFieldType.INT8, + # str is handled separately, so not needed here +} + +def _str_to_data_type(dtype: str) -> trt.DataType: + if dtype == "FP32": + return trt.DataType.FLOAT + if dtype == "FP16": + return trt.DataType.HALF + try: + return getattr(trt.DataType, dtype) + except KeyError: + raise ValueError(f"Unknown data type string '{dtype}'") from None + + +def _join_with(lst, middle = False, delim = ", "): + if len(lst) == 0: + return "" + + ret = "" + if middle: + ret += ", " + + ret += delim.join(lst) + + return ret + +def _is_npt_ndarray(annotation): + return (typing.get_origin(annotation) == np.ndarray) or (hasattr(annotation, "__origin__") and annotation.__origin__ == np.ndarray) + +def _is_numpy_array(annotation): + return (annotation == np.ndarray) or _is_npt_ndarray(annotation) + +def _infer_numpy_type(annotation): + assert _is_npt_ndarray(annotation) + annot_args = typing.get_args(annotation) or annotation.__args__ + if len(annot_args) >= 2: + np_type = typing.get_args(annot_args[1]) or annot_args[1].__args__ + if len(np_type) >= 1: + return np_type[0] + + raise AttributeError("Improper annotation for numpy array. Annotate numpy array attributes using 'numpy.typing.NDArray[dtype]', where 'dtype' is the expected numpy dtype of the array.") diff --git a/python/packaging/bindings_wheel/tensorrt/plugin/_validate.py b/python/packaging/bindings_wheel/tensorrt/plugin/_validate.py new file mode 100644 index 00000000..48ee4fd9 --- /dev/null +++ b/python/packaging/bindings_wheel/tensorrt/plugin/_validate.py @@ -0,0 +1,357 @@ +# +# SPDX-FileCopyrightText: Copyright (c) 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. +# + +import inspect +import numpy as np +import typing + +from ._utils import _is_numpy_array, _join_with, _infer_numpy_type, _is_npt_ndarray +from ._tensor import TensorDesc, Tensor +from ._autotune import AutoTuneCombination + +SERIALIZABLE_BUILTIN_TYPES = (int, float, bytes, bool, str) +SERIALIZABLE_NP_DTYPES = ( + np.int8, + np.int16, + np.int32, + np.int64, + np.float16, + np.float32, + np.float64, + bool, + np.bool_, +) + +# Reserve some namespaces for future use/avoid confusion +RESERVED_NAMESPACES = { + "", + "trt", + "tensorrt", + "std", +} + +DISALLOWED_ATTR_NAMES = { + "outputs", + "stream", + "tactic", +} + +def _validate_name_and_namespace(ns: str, name: str): + if "." in ns: + raise ValueError( + f"Provided namespace {ns} cannot have any '.' in trt.plugin.register(\"{ns}::{name}\", ...)" + ) + + if "." in name: + raise ValueError( + f"Provided name {name} cannot have any '.' in trt.plugin.register(\"{ns}::{name}\", ...)" + ) + + if ns in RESERVED_NAMESPACES: + raise ValueError( + f"Provided namespace {ns} is a reserved namespace" + ) + + +# Parse `tensorrt.plugin.register` schema +def _parse_register_inputs(register_func, lazy_register): + tensor_names = [] + input_attrs = ( + dict() + ) # order is important here but for Python >= 3.7, dict respects key order + + schema_chunks = [] + + # TensorDescs and attribute args cannot be interspersed, so remember when we saw the first attribute arg + saw_first_attr = False + + # Map of (attr_name: str) -> (is_builtin_type?: bool, type annotation: str) + attrs_types = {} + + sig = inspect.signature(register_func) + + for idx, (name, param) in enumerate(sig.parameters.items()): + + if param.kind not in ( + inspect.Parameter.POSITIONAL_OR_KEYWORD, + inspect.Parameter.KEYWORD_ONLY, + ): + raise ValueError( + f"Argument {name} is not a positional-or-keyword or keyword-only arg" + ) + + # Type annotations are manadatory for `tensorrt.plugin.register` args + if param.annotation == inspect.Parameter.empty: + raise ValueError( + f"Argument {name} does not have a type annotation. Please mark as TensorDesc or one of the serializable attribute types." + ) + + # Presently, we do not support default values for attributes + if param.default is not inspect.Parameter.empty: + raise ValueError( + f"Argument {name} has a default value. Default values are not supported yet." + ) + + + if issubclass(param.annotation, TensorDesc): + if saw_first_attr: + raise ValueError( + f"TensorDescs args and attribute args cannot be interspersed. Received function with signature {sig}." + ) + + tensor_names.append(name) + schema_chunks.append(f"TensorDesc {name}") + # At this point, we don't validate attribute types since we only care about the types of serializable attributes + # However, we memorize name and type so that we may validate that the autotune function maintains consistency + else: + if idx == 0: + raise ValueError( + f"TensorDescs args should come first, followed by attributes. Received function with signature {sig}." + ) + + if name in DISALLOWED_ATTR_NAMES: + raise ValueError( + f"'{name}' is not allowed as a plugin attribute name." + ) + + if param.annotation not in SERIALIZABLE_BUILTIN_TYPES: + if _is_numpy_array(param.annotation): + if not lazy_register: + if param.annotation == np.ndarray: + raise ValueError( + "If using non-lazy registration, annotate numpy array attributes using 'numpy.typing.NDArray[dtype]', where 'dtype' is the expected numpy dtype of the array." + ) + + if _is_npt_ndarray(param.annotation): + np_dtype = _infer_numpy_type(param.annotation) + if np_dtype not in SERIALIZABLE_NP_DTYPES: + raise ValueError( + f"Attribute '{name}' is not a supported numpy array type. Supported numpy arrays type are {SERIALIZABLE_NP_DTYPES}." + ) + attrs_types[name] = (False, np_dtype) + + else: + raise ValueError( + f"Attribute '{name}' of type {param.annotation} is not a supported serializable type. Supported types are {SERIALIZABLE_BUILTIN_TYPES} or numpy arrays of type {SERIALIZABLE_NP_DTYPES}." + ) + else: + attrs_types[name] = (True, param.annotation) + + saw_first_attr = True + + schema_chunks.append(f"{param.annotation} {name}") + input_attrs[name] = param.annotation + + return ( + tensor_names, + input_attrs, + f"({_join_with(schema_chunks)})", + attrs_types, + ) + + +def _parse_register_return(register_func): + sig = inspect.signature(register_func) + + ret_annotation = sig.return_annotation + + if ret_annotation == inspect.Parameter.empty: + raise ValueError( + f"No return annotation found for register function. Received signature {sig}." + ) + + if typing.get_origin(ret_annotation) is not tuple: + if not inspect.isclass(ret_annotation) or not issubclass( + ret_annotation, TensorDesc + ): + raise ValueError( + f"Return argument is of type {ret_annotation}. Return types can only be TensorDesc or Tuple[TensorDesc]." + ) + + num_outputs = 1 + else: + args = typing.get_args(ret_annotation) + + for arg in args: + if not issubclass(arg, TensorDesc): + raise ValueError( + f"Return argument is of type {ret_annotation}. Return types can only be TensorDesc or Tuple[TensorDesc]." + ) + + num_outputs = len(args) + + return num_outputs + + +def _validate_impl(impl_func, plugin_def): + impl_attr_names = [] + found_tactic = False + + sig = inspect.signature(impl_func) + registered_attr_names = plugin_def.input_attrs.keys() + + # input arg annotations are optional, but we will validate if provided + for name, param in sig.parameters.items(): + # tactic arg is optional in impl function. If specified, remember so that we can pass it during enqueue. + if name == "tactic": + found_tactic = True + if param.annotation != inspect.Parameter.empty: + if name == "outputs": + if typing.get_origin(param.annotation) is not tuple: + raise ValueError( + f"'outputs' should be of type Tuple[Tensor]. Received {param.annotation}." + ) + args = typing.get_args(param.annotation) + for arg in args: + if not issubclass(arg, Tensor): + raise ValueError( + f"Argument for receiving output Tensor, '{name}' contains a {param.annotation}. '{name}' should be a Tuple[Tensor]." + ) + elif name == "stream": + if not issubclass(param.annotation, int): + raise ValueError("'stream' input argument should be an int") + elif name == "tactic": + if not issubclass(param.annotation, int): + raise ValueError("'tactic' input argument should be an int") + elif issubclass(param.annotation, Tensor): + if name not in plugin_def.input_tensor_names: + raise ValueError( + f"Unexpected tensor '{name}' specified in autotune function. Expected one of {plugin_def.input_tensor_names}." + ) + else: + if name not in plugin_def.input_attrs: + raise ValueError( + f"Unexpected attribute '{name}' specified in impl function. Expected one of {list(registered_attr_names)}." + ) + + if param.annotation != plugin_def.input_attrs[name]: + raise ValueError( + f"Attribute '{name}' has a type annotation different from the one specified at registration. Expected '{plugin_def.input_attrs[name]}'." + ) + + impl_attr_names.append(name) + else: + if name in plugin_def.input_attrs: + impl_attr_names.append(name) + + # Expected attribute schema should be constructed in the order they appeared in the register function + expected_attr_schema_chunks = [ + n for n in registered_attr_names if n in impl_attr_names + ] + + expected_schema = ( + "(" + + _join_with(plugin_def.input_tensor_names) + + _join_with(expected_attr_schema_chunks, True) + + ", outputs, stream" + ) + if found_tactic: + expected_schema += ", tactic)" + else: + expected_schema += ")" + + if f"({', '.join(sig.parameters.keys())})" != expected_schema: + raise ValueError( + f"Signature of the impl function '{sig}' does not match the expected input arg schema: {expected_schema}" + ) + + # Return annotation is optional, but we will validate if one is specified + if sig.return_annotation != inspect.Parameter.empty and sig.return_annotation is not None: + raise ValueError("Return annotation should be None.") + + return impl_attr_names, found_tactic + + +def _validate_autotune(autotune_func, plugin_def): + + sig = inspect.signature(autotune_func) + registered_attr_names = plugin_def.input_attrs.keys() + + autotune_attr_names = [] + + # input arg annotations are optional, but we will validate if provided + for name, param in sig.parameters.items(): + if param.annotation != inspect.Parameter.empty: + if name == "outputs": + if typing.get_origin(param.annotation) is not tuple: + raise ValueError( + f"'outputs' should be of type Tuple[TensorDesc]. Received {param.annotation}." + ) + args = typing.get_args(param.annotation) + for arg in args: + if not issubclass(arg, TensorDesc): + raise ValueError( + f"Argument for receiving output TensorDescs, '{name}' contains a {param.annotation}. '{name}' should be a Tuple[TensorDesc]." + ) + elif issubclass(param.annotation, TensorDesc): + if name not in plugin_def.input_tensor_names: + raise ValueError( + f"Unexpected tensor '{name}' specified in autotune function. Expected one of {plugin_def.input_tensor_names}." + ) + else: + if name not in plugin_def.input_attrs: + raise ValueError( + f"Unexpected attribute '{name}' specified in autotune function. Expected one of {list(registered_attr_names)}." + ) + if param.annotation != plugin_def.input_attrs[name]: + raise ValueError( + f"Attribute '{name}' has a type annotation different from the one specified at registration. Expected '{plugin_def.input_attrs[name]}'." + ) + + autotune_attr_names.append(name) + else: + if name in plugin_def.input_attrs: + autotune_attr_names.append(name) + + # Expected attribute schema should be constructed in the order they appeared in the register function + expected_attr_schema_chunks = [ + n for n in registered_attr_names if n in autotune_attr_names + ] + + expected_schema = ( + "(" + + _join_with(plugin_def.input_tensor_names) + + _join_with(expected_attr_schema_chunks, True) + + ", outputs)" + ) + + if f"({', '.join(sig.parameters.keys())})" != expected_schema: + raise ValueError( + f"Specified autotune function signature {sig} is not consistent with the expected input arg schema {expected_schema}." + ) + + ret_annotation = sig.return_annotation + + # Return annotation is optional, but we will validate if one is specified + if ret_annotation != inspect.Parameter.empty: + if typing.get_origin(ret_annotation) is not list: + if not inspect.isclass(ret_annotation) or not issubclass( + ret_annotation, AutoTuneCombination + ): + raise ValueError( + f"Return argument is of type {ret_annotation}. Return types can only be AutoTuneCombination or List[AutoTuneCombination]." + ) + else: + args = typing.get_args(ret_annotation) + + for arg in args: + if not issubclass(arg, AutoTuneCombination): + raise ValueError( + f"Return argument is of type {ret_annotation}. Return types can only be AutoTuneCombination or List[AutoTuneCombination]." + ) + + return autotune_attr_names diff --git a/python/packaging/frontend_sdist/setup.cfg b/python/packaging/frontend_sdist/setup.cfg index dea8290c..5b78c91c 100644 --- a/python/packaging/frontend_sdist/setup.cfg +++ b/python/packaging/frontend_sdist/setup.cfg @@ -1,12 +1,19 @@ -# SPDX-FileCopyrightText: Copyright (c) 2019-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: LicenseRef-NvidiaProprietary -# -# NVIDIA CORPORATION, its affiliates and licensors retain all intellectual -# property and proprietary rights in and to this material, related -# documentation and any modifications thereto. Any use, reproduction, -# disclosure or distribution of this material and related documentation -# without an express license agreement from NVIDIA CORPORATION or -# its affiliates is strictly prohibited. +# +# SPDX-FileCopyrightText: Copyright (c) 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. +# [metadata] license_files = LICENSE.txt diff --git a/python/packaging/frontend_sdist/setup.py b/python/packaging/frontend_sdist/setup.py index bb4d9172..512d3ce1 100644 --- a/python/packaging/frontend_sdist/setup.py +++ b/python/packaging/frontend_sdist/setup.py @@ -64,7 +64,9 @@ def find_pip(): if sys.implementation.name != "cpython": raise RuntimeError("TensorRT currently only builds wheels for CPython") if platform.machine() not in ("x86_64", "AMD64", "aarch64"): - raise RuntimeError("TensorRT currently only builds wheels for x86_64 and ARM SBSA processors") + raise RuntimeError( + "TensorRT currently only builds wheels for x86_64 and ARM SBSA processors" + ) if "tegra" in platform.release(): raise RuntimeError("TensorRT does not currently build wheels for Tegra systems") @@ -167,6 +169,6 @@ def parent_command_line(): include_package_data=True, zip_safe=True, keywords="nvidia tensorrt deeplearning inference", - url="https://developer.nvidia.com/tensorrt", - download_url="https://github.com/nvidia/tensorrt/tags", + url="https://github.com/nvidia/tensorrt", + download_url="https://developer.nvidia.com/tensorrt", ) diff --git a/python/packaging/libs_wheel/setup.cfg b/python/packaging/libs_wheel/setup.cfg index dea8290c..5b78c91c 100644 --- a/python/packaging/libs_wheel/setup.cfg +++ b/python/packaging/libs_wheel/setup.cfg @@ -1,12 +1,19 @@ -# SPDX-FileCopyrightText: Copyright (c) 2019-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: LicenseRef-NvidiaProprietary -# -# NVIDIA CORPORATION, its affiliates and licensors retain all intellectual -# property and proprietary rights in and to this material, related -# documentation and any modifications thereto. Any use, reproduction, -# disclosure or distribution of this material and related documentation -# without an express license agreement from NVIDIA CORPORATION or -# its affiliates is strictly prohibited. +# +# SPDX-FileCopyrightText: Copyright (c) 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. +# [metadata] license_files = LICENSE.txt diff --git a/python/packaging/libs_wheel/setup.py b/python/packaging/libs_wheel/setup.py index b9f7af76..2d20f09f 100644 --- a/python/packaging/libs_wheel/setup.py +++ b/python/packaging/libs_wheel/setup.py @@ -45,6 +45,6 @@ def get_requirements(): include_package_data=True, zip_safe=True, keywords="nvidia tensorrt deeplearning inference", - url="https://developer.nvidia.com/tensorrt", - download_url="https://github.com/nvidia/tensorrt/tags", + url="https://github.com/nvidia/tensorrt", + download_url="https://developer.nvidia.com/tensorrt", ) diff --git a/python/packaging/metapackage/setup.py b/python/packaging/metapackage/setup.py index 43e563e6..11a972d6 100644 --- a/python/packaging/metapackage/setup.py +++ b/python/packaging/metapackage/setup.py @@ -40,6 +40,6 @@ include_package_data=True, zip_safe=True, keywords="nvidia tensorrt deeplearning inference", - url="https://developer.nvidia.com/tensorrt", - download_url="https://github.com/nvidia/tensorrt/tags", + url="https://github.com/nvidia/tensorrt", + download_url="https://developer.nvidia.com/tensorrt", ) diff --git a/python/src/infer/pyCore.cpp b/python/src/infer/pyCore.cpp index c284d1d1..ae913bac 100644 --- a/python/src/infer/pyCore.cpp +++ b/python/src/infer/pyCore.cpp @@ -165,6 +165,8 @@ static const auto runtime_deserialize_cuda_engine = [](IRuntime& self, py::buffe return self.deserializeCudaEngine(info.ptr, info.size * info.itemsize); }; + + // For ICudaEngine // TODO: Add slicing support? static const auto engine_getitem = [](ICudaEngine& self, int32_t pyIndex) { @@ -628,6 +630,7 @@ class PyStreamReader : public IStreamReader } }; + class PyDebugListener : public IDebugListener { public: @@ -1096,7 +1099,8 @@ void bindCore(py::module& m) IExecutionContextDoc::set_tensor_debug_state) .def("get_debug_state", &IExecutionContext::getDebugState, "name"_a, IExecutionContextDoc::get_debug_state) .def("set_all_tensors_debug_state", &IExecutionContext::setAllTensorsDebugState, "flag"_a, - IExecutionContextDoc::set_all_tensors_debug_state); + IExecutionContextDoc::set_all_tensors_debug_state) + ; py::enum_(m, "ExecutionContextAllocationStrategy", py::arithmetic{}, ExecutionContextAllocationStrategyDoc::descr, py::module_local()) @@ -1292,6 +1296,7 @@ void bindCore(py::module& m) "weight_streaming_scratch_memory_size", &ICudaEngine::getWeightStreamingScratchMemorySize) // End weight streaming APIs .def("is_debug_tensor", &ICudaEngine::isDebugTensor, "name"_a, ICudaEngineDoc::is_debug_tensor) + .def("__del__", &utils::doNothingDel); py::enum_(m, "AllocatorFlag", py::arithmetic{}, AllocatorFlagDoc::descr, py::module_local()) @@ -1333,6 +1338,7 @@ void bindCore(py::module& m) .def(py::init<>()) .def("read", &IStreamReader::read, "destination"_a, "size"_a, StreamReaderDoc::read); + py::enum_(m, "BuilderFlag", py::arithmetic{}, BuilderFlagDoc::descr, py::module_local()) .value("FP16", BuilderFlag::kFP16, BuilderFlagDoc::FP16) .value("BF16", BuilderFlag::kBF16, BuilderFlagDoc::BF16) @@ -1364,6 +1370,8 @@ void bindCore(py::module& m) .value("WEIGHT_STREAMING", BuilderFlag::kWEIGHT_STREAMING, BuilderFlagDoc::WEIGHT_STREAMING) .value("INT4", BuilderFlag::kINT4, BuilderFlagDoc::INT4) .value("REFIT_INDIVIDUAL", BuilderFlag::kREFIT_INDIVIDUAL, BuilderFlagDoc::REFIT_INDIVIDUAL) + .value("STRICT_NANS", BuilderFlag::kSTRICT_NANS, BuilderFlagDoc::STRICT_NANS) + .value("MONITOR_MEMORY", BuilderFlag::kMONITOR_MEMORY, BuilderFlagDoc::MONITOR_MEMORY) ; py::enum_(m, "MemoryPoolType", MemoryPoolTypeDoc::descr, py::module_local()) @@ -1526,6 +1534,8 @@ void bindCore(py::module& m) py::keep_alive<0, 1>{}) .def("build_serialized_network", &IBuilder::buildSerializedNetwork, "network"_a, "config"_a, BuilderDoc::build_serialized_network, py::call_guard{}) + .def("build_engine_with_config", &IBuilder::buildEngineWithConfig, "network"_a, "config"_a, + BuilderDoc::build_engine_with_config, py::call_guard{}) .def("is_network_supported", &IBuilder::isNetworkSupported, "network"_a, "config"_a, BuilderDoc::is_network_supported, py::call_guard{}) .def_property_readonly("logger", &IBuilder::getLogger) diff --git a/python/src/infer/pyGraph.cpp b/python/src/infer/pyGraph.cpp index e2d8564a..b4fc21c6 100644 --- a/python/src/infer/pyGraph.cpp +++ b/python/src/infer/pyGraph.cpp @@ -19,6 +19,7 @@ #include "ForwardDeclarations.h" #include "utils.h" #include +#include #if ENABLE_INETWORK_SERIALIZE #include "NvInferSerialize.h" @@ -114,6 +115,14 @@ namespace tensorrt return self.addPluginV3(inputs.data(), inputs.size(), shapeInputs.data(), shapeInputs.size(), plugin); }; + static const auto add_plugin = [] (INetworkDefinition& self, std::tuple const&, std::vector const&, IPluginV3&> tupleInput) + { + std::vector const& inputs = std::get<0>(tupleInput); + std::vector const& shapeInputs = std::get<1>(tupleInput); + IPluginV3& plugin = std::get<2>(tupleInput); + return self.addPluginV3(inputs.data(), inputs.size(), shapeInputs.data(), shapeInputs.size(), plugin); + }; + static const auto add_convolution_nd = [](INetworkDefinition& self, ITensor& input, int32_t numOutputMaps, Dims kernelSize, Weights kernel, Weights* bias) { return self.addConvolutionNd(input, numOutputMaps, kernelSize, kernel, optionalWeights(bias)); @@ -244,6 +253,8 @@ namespace tensorrt else return py::cast(self.getBeta()); }; + + } /* lambdas */ void bindGraph(py::module& m) @@ -818,6 +829,7 @@ namespace tensorrt .def_property("compute_precision", &INormalizationLayer::getComputePrecision, &INormalizationLayer::setComputePrecision) ; + // Weights must be kept alive for the duration of the network. py::keep_alive is critical here! // Additionally, we use reference_internal so that pybind11 does not free layers when they go out of scope. py::class_(m, "INetworkDefinition", INetworkDefinitionDoc::descr, py::module_local()) @@ -893,6 +905,8 @@ namespace tensorrt INetworkDefinitionDoc::add_plugin_v2, py::return_value_policy::reference_internal) .def("add_plugin_v3", lambdas::add_plugin_v3, "inputs"_a, "shape_inputs"_a, "plugin"_a, INetworkDefinitionDoc::add_plugin_v3, py::return_value_policy::reference_internal) + .def("add_plugin", lambdas::add_plugin, "tuple"_a, + INetworkDefinitionDoc::add_plugin, py::return_value_policy::reference_internal) .def("add_parametric_relu", &INetworkDefinition::addParametricReLU, "input"_a, "slopes"_a, INetworkDefinitionDoc::add_parametric_relu, py::return_value_policy::reference_internal) .def("add_resize", &INetworkDefinition::addResize, "input"_a, INetworkDefinitionDoc::add_resize, diff --git a/python/src/infer/pyPlugin.cpp b/python/src/infer/pyPlugin.cpp index 0396fee7..72b5d9cb 100644 --- a/python/src/infer/pyPlugin.cpp +++ b/python/src/infer/pyPlugin.cpp @@ -17,6 +17,7 @@ // This file contains all bindings related to plugins. #include "ForwardDeclarations.h" +#include "impl/plugin.h" #include "infer/pyPluginDoc.h" #include "utils.h" #include @@ -782,7 +783,18 @@ class PyIPluginV3Impl : public IPluginV3 { if (type == PluginCapabilityType::kCORE) { - return pyResult.cast(); + try + { + return pyResult.cast(); + } + catch (py::cast_error const& e) + { + try + { + return pyResult.cast(); + } + PLUGIN_API_CATCH_CAST("get_capability_interface", " a valid core capability interface") + } } if (type == PluginCapabilityType::kBUILD) { @@ -796,12 +808,30 @@ class PyIPluginV3Impl : public IPluginV3 { return pyResult.cast(); } - PLUGIN_API_CATCH_CAST("get_capability_interface", " a valid build capability interface") + catch (py::cast_error const& e) + { + try + { + return pyResult.cast(); + } + PLUGIN_API_CATCH_CAST("get_capability_interface", " a valid build capability interface") + } } } if (type == PluginCapabilityType::kRUNTIME) { - return pyResult.cast(); + try + { + return pyResult.cast(); + } + catch (py::cast_error const& e) + { + try + { + return pyResult.cast(); + } + PLUGIN_API_CATCH_CAST("get_capability_interface", " a valid runtime capability interface") + } } } PLUGIN_API_CATCH_CAST("get_capability_interface", "nvinfer1::IPluginCapability") @@ -1339,69 +1369,674 @@ class PyIPluginV3OneBuildBaseImpl : public T mIsMetadataStringInitialized = true; } -private: - int32_t mNbOutputs{}; - int32_t mFormatCombinationLimit{}; - std::string mTimingCachedId{}; - std::string mMetadataString{}; - std::vector mTactics; +private: + int32_t mNbOutputs{}; + int32_t mFormatCombinationLimit{}; + std::string mTimingCachedId{}; + std::string mMetadataString{}; + std::vector mTactics; + + bool mIsNbOutputsInitialized{false}; + bool mIsTimingCachedIdInitialized{false}; + bool mIsFormatCombinationLimitInitialized{false}; + bool mIsMetadataStringInitialized{false}; + bool mIsTacticsInitialized{false}; +}; + +class PyIPluginV3OneBuildImpl : public PyIPluginV3OneBuildBaseImpl +{ +public: + PyIPluginV3OneBuildImpl() + : PyIPluginV3OneBuildBaseImpl(this) + { + } + PyIPluginV3OneBuildImpl(IPluginV3OneBuild const& a) + : PyIPluginV3OneBuildBaseImpl(this){}; +}; + +class PyIPluginV3OneBuildV2Impl : public PyIPluginV3OneBuildBaseImpl +{ +public: + PyIPluginV3OneBuildV2Impl() + : PyIPluginV3OneBuildBaseImpl(this) + { + } + PyIPluginV3OneBuildV2Impl(IPluginV3OneBuildV2 const& a) + : PyIPluginV3OneBuildBaseImpl(this){}; + + int32_t getAliasedInput(int32_t outputIndex) noexcept override + { + try + { + py::gil_scoped_acquire gil{}; + + py::function pyGetAliasedInput + = py::get_override(static_cast(this), "get_aliased_input"); + + if (!pyGetAliasedInput) + { + // if no implementation is provided for get_aliased_input(), default to no aliasing + return -1; + } + + py::object pyResult = pyGetAliasedInput(outputIndex); + + try + { + auto result = pyResult.cast(); + return result; + } + PLUGIN_API_CATCH_CAST("get_aliased_input", "int32_t") + return -1; + } + PLUGIN_API_CATCH("get_aliased_input") + return -1; + } +}; + +class PyIPluginV3QuickCoreImpl : public IPluginV3QuickCore +{ +public: + using IPluginV3QuickCore::IPluginV3QuickCore; + PyIPluginV3QuickCoreImpl() = default; + PyIPluginV3QuickCoreImpl(const IPluginV3QuickCore& a){}; + + APILanguage getAPILanguage() const noexcept final + { + return APILanguage::kPYTHON; + } + + char const* getPluginName() const noexcept override + { + try + { + py::gil_scoped_acquire gil{}; + if (!mPluginName.has_value()) + { + utils::throwPyError(PyExc_AttributeError, "plugin_name not initialized"); + } + return mPluginName.value().c_str(); + } + PLUGIN_API_CATCH("plugin_name") + return nullptr; + } + + char const* getPluginVersion() const noexcept override + { + try + { + py::gil_scoped_acquire gil{}; + if (!mPluginVersion.has_value()) + { + utils::throwPyError(PyExc_AttributeError, "plugin_version not initialized"); + } + return mPluginVersion.value().c_str(); + } + PLUGIN_API_CATCH("plugin_version") + return nullptr; + } + + char const* getPluginNamespace() const noexcept override + { + try + { + py::gil_scoped_acquire gil{}; + // getPluginNamespace() is not passed through to the Python side + if (!mPluginNamespace.has_value()) + { + utils::throwPyError(PyExc_AttributeError, "plugin_namespace not initialized"); + } + return mPluginNamespace.value().c_str(); + } + PLUGIN_API_CATCH("plugin_namespace") + return nullptr; + } + + void setPluginName(std::string pluginName) + { + mPluginName = std::move(pluginName); + } + + void setPluginNamespace(std::string pluginNamespace) + { + mPluginNamespace = std::move(pluginNamespace); + } + + void setPluginVersion(std::string pluginVersion) + { + mPluginVersion = std::move(pluginVersion); + } + +private: + std::optional mPluginNamespace; + std::optional mPluginName; + std::optional mPluginVersion; +}; + +class PyIPluginV3QuickBuildImpl : public IPluginV3QuickBuild +{ +public: + using IPluginV3QuickBuild::IPluginV3QuickBuild; + PyIPluginV3QuickBuildImpl() = default; + PyIPluginV3QuickBuildImpl(const IPluginV3QuickBuild& a){}; + + APILanguage getAPILanguage() const noexcept final + { + return APILanguage::kPYTHON; + } + + int32_t getNbOutputs() const noexcept override + { + try + { + py::gil_scoped_acquire gil{}; + if (!mNbOutputs.has_value()) + { + utils::throwPyError(PyExc_AttributeError, "num_outputs not initialized"); + } + return mNbOutputs.value(); + } + PLUGIN_API_CATCH("num_outputs") + return -1; + } + + int32_t getNbTactics() noexcept override + { + try + { + py::gil_scoped_acquire gil{}; + + try + { + py::function pyGetValidTactics + = py::get_override(static_cast(this), "get_valid_tactics"); + + if (!pyGetValidTactics) + { + // if no implementation is provided for get_valid_tactics(), communicate that no custom tactics are + // used by the plugin + return 0; + } + + py::object pyResult = pyGetValidTactics(); + mTactics = pyResult.cast>(); + return static_cast(mTactics.value().size()); + } + PLUGIN_API_CATCH_CAST("get_valid_tactics", "std::vector") + catch (py::error_already_set& e) + { + std::cerr << "[ERROR] Exception thrown from get_valid_tactics() " << e.what() << std::endl; + } + } + PLUGIN_API_CATCH("tactics") + return -1; + } + + int32_t getValidTactics(int32_t* tactics, int32_t nbTactics) noexcept override + { + try + { + py::gil_scoped_acquire gil{}; + + try + { + // getValidTactics() must immediately follow getNbTactics() + // because it is impossible to call getValidTactics() without knowing the + // correct number of tactics. So check that mTactics.has_value() is true. + // Otherwise, something has gone wrong. + if (mTactics.has_value()) + { + if (nbTactics != static_cast(mTactics.value().size())) + { + utils::throwPyError( + PyExc_RuntimeError, "number of tactics does not match cached number of tactics"); + } + std::copy(mTactics.value().begin(), mTactics.value().end(), tactics); + // Reset to catch any subsequent violations + mTactics.reset(); + return 0; + } + else + { + utils::throwPyError( + PyExc_RuntimeError, "Internal error. getValidTactics() called before getNbTactics()."); + } + return -1; + } + PLUGIN_API_CATCH_CAST("get_valid_tactics", "std::vector") + catch (py::error_already_set& e) + { + std::cerr << "[ERROR] Exception thrown from get_valid_tactics() " << e.what() << std::endl; + } + } + PLUGIN_API_CATCH("tactics") + return -1; + } + + int32_t configurePlugin(DynamicPluginTensorDesc const* in, int32_t nbInputs, DynamicPluginTensorDesc const* out, + int32_t nbOutputs) noexcept override + { + try + { + py::gil_scoped_acquire gil{}; + + py::function pyConfigurePlugin + = utils::getOverride(static_cast(this), "configure_plugin"); + + if (!pyConfigurePlugin) + { + utils::throwPyError(PyExc_RuntimeError, "no implementation provided for configure_plugin()"); + } + + std::vector inVector; + std::vector outVector; + std::copy_n(in, nbInputs, std::back_inserter(inVector)); + std::copy_n(out, nbOutputs, std::back_inserter(outVector)); + + try + { + pyConfigurePlugin(inVector, outVector); + return 0; + } + catch (py::error_already_set& e) + { + std::cerr << "[ERROR] Exception thrown from configure_plugin() " << e.what() << std::endl; + } + } + PLUGIN_API_CATCH("configure_plugin") + return -1; + } + + int32_t getNbSupportedFormatCombinations( + DynamicPluginTensorDesc const* inOut, int32_t nbInputs, int32_t nbOutputs) noexcept override + { + try + { + py::gil_scoped_acquire gil{}; + + py::function pySupportsFormatCombination + = utils::getOverride(static_cast(this), "get_supported_format_combinations"); + if (!pySupportsFormatCombination) + { + utils::throwPyError( + PyExc_RuntimeError, "no implementation provided for get_supported_format_combinations()"); + } + + std::vector inOutVector; + std::copy_n(inOut, nbInputs + nbOutputs, std::back_inserter(inOutVector)); + + py::object pyResult = pySupportsFormatCombination(inOutVector, nbInputs); + try + { + mSupportedFormatCombinations = pyResult.cast>(); + if (static_cast(mSupportedFormatCombinations.value().size()) % (nbInputs + nbOutputs) != 0) + { + utils::throwPyError( + PyExc_ValueError, "Number of supported format combinations not a multiple of number of IO."); + } + return static_cast(mSupportedFormatCombinations.value().size()) / (nbInputs + nbOutputs); + } + PLUGIN_API_CATCH_CAST("get_nb_supported_format_combinations", "int32_t") + catch (py::error_already_set& e) + { + std::cerr << "[ERROR] Exception thrown from get_supported_format_combinations() " << e.what() + << std::endl; + } + return -1; + } + PLUGIN_API_CATCH("get_nb_supported_format_combinations") + return -1; + } + + int32_t getSupportedFormatCombinations(DynamicPluginTensorDesc const* inOut, int32_t nbInputs, int32_t nbOutputs, + PluginTensorDesc* supportedCombinations, int32_t nbFormatCombinations) noexcept override + { + py::gil_scoped_acquire gil{}; + + py::function pySupportsFormatCombination + = utils::getOverride(static_cast(this), "get_supported_format_combinations"); + if (!pySupportsFormatCombination) + { + utils::throwPyError( + PyExc_RuntimeError, "no implementation provided for get_supported_format_combinations()"); + } + + std::vector inOutVector; + std::copy_n(inOut, nbInputs + nbOutputs, std::back_inserter(inOutVector)); + + py::object pyResult = pySupportsFormatCombination(inOutVector, nbInputs); + + try + { + // getSupportedFormatCombinations() must immediately follow getNbSupportedFormatCombinations() + // because it is impossible to call getSupportedFormatCombinations() without knowing the + // correct number of tactics. So check that mSupportedFormatCombinations.has_value(). + // Otherwise, something has gone wrong. + if (mSupportedFormatCombinations.has_value()) + { + std::copy(mSupportedFormatCombinations.value().begin(), mSupportedFormatCombinations.value().end(), + supportedCombinations); + // Reset to catch any subsequent violations + mSupportedFormatCombinations.reset(); + return 0; + } + else + { + utils::throwPyError(PyExc_RuntimeError, + "Internal error. getSupportedFormatCombinations() called before " + "getNbSupportedFormatCombinations()."); + } + return -1; + } + PLUGIN_API_CATCH_CAST("get_supported_format_combinations", "std::vector") + catch (py::error_already_set& e) + { + std::cerr << "[ERROR] Exception thrown from get_supported_format_combinations() " << e.what() << std::endl; + } + return -1; + } + + int32_t getOutputDataTypes(DataType* outputTypes, int32_t nbOutputs, DataType const* inputTypes, + int32_t const* inputRanks, int32_t nbInputs) const noexcept override + { + try + { + py::gil_scoped_acquire gil{}; + + py::function pyGetOutputDataTypes + = utils::getOverride(static_cast(this), "get_output_data_types"); + if (!pyGetOutputDataTypes) + { + utils::throwPyError(PyExc_RuntimeError, "no implementation provided for get_output_data_types()"); + } + + std::vector inVector; + std::vector ranksVector; + std::copy_n(inputTypes, nbInputs, std::back_inserter(inVector)); + std::copy_n(inputRanks, nbInputs, std::back_inserter(ranksVector)); + + try + { + py::object pyResult = pyGetOutputDataTypes(inVector, ranksVector); + auto result = pyResult.cast>(); + + if (static_cast(result.size()) != nbOutputs) + { + utils::throwPyError(PyExc_RuntimeError, + "get_output_data_types() returned a list with a different length than num_outputs"); + } + + std::copy(result.begin(), result.end(), outputTypes); + return 0; + } + PLUGIN_API_CATCH_CAST("get_output_data_types", "std::vector") + catch (py::error_already_set& e) + { + std::cerr << "[ERROR] Exception thrown from get_output_data_types() " << e.what() << std::endl; + } + } + PLUGIN_API_CATCH("get_output_data_types") + return -1; + } + + int32_t getOutputShapes(DimsExprs const* inputs, int32_t nbInputs, DimsExprs const* shapeInputs, + int32_t nbShapeInputs, DimsExprs* outputs, int32_t nbOutputs, IExprBuilder& exprBuilder) noexcept override + { + try + { + py::gil_scoped_acquire gil{}; + + py::function pyGetOutputShapes + = utils::getOverride(static_cast(this), "get_output_shapes"); + if (!pyGetOutputShapes) + { + utils::throwPyError(PyExc_RuntimeError, "no implementation provided for get_output_shapes()"); + } + + std::vector inVector; + std::vector shapeInVector; + std::copy_n(inputs, nbInputs, std::back_inserter(inVector)); + std::copy_n(shapeInputs, nbShapeInputs, std::back_inserter(shapeInVector)); + + py::object pyResult = pyGetOutputShapes(inVector, shapeInVector, &exprBuilder); + + try + { + auto result = pyResult.cast>(); + if (static_cast(result.size()) != nbOutputs) + { + utils::throwPyError(PyExc_RuntimeError, + "get_output_shapes() returned a list with a different length than num_outputs"); + } + std::copy(result.begin(), result.end(), outputs); + return 0; + } + PLUGIN_API_CATCH_CAST("get_output_shapes", "std::vector") + catch (py::error_already_set& e) + { + std::cerr << "[ERROR] Exception thrown from get_output_shapes() " << e.what() << std::endl; + } + return -1; + } + PLUGIN_API_CATCH("get_output_shapes") + return -1; + } + + int32_t getAliasedInput(int32_t outputIndex) noexcept override + { + try + { + py::gil_scoped_acquire gil{}; + + py::function pyGetAliasedInput + = py::get_override(static_cast(this), "get_aliased_input"); + + if (!pyGetAliasedInput) + { + // if no implementation is provided for get_aliased_input(), default to no aliasing + return -1; + } + + py::object pyResult = pyGetAliasedInput(outputIndex); + + try + { + auto result = pyResult.cast(); + return result; + } + PLUGIN_API_CATCH_CAST("get_aliased_input", "int32_t") + return -1; + } + PLUGIN_API_CATCH("get_aliased_input") + return -1; + } + + char const* getTimingCacheID() noexcept override + { + try + { + py::gil_scoped_acquire gil{}; + if (!mTimingCachedId.has_value()) + { + return nullptr; + } + return mTimingCachedId.value().c_str(); + } + PLUGIN_API_CATCH("timing_cache_id") + return nullptr; + } + + char const* getMetadataString() noexcept override + { + try + { + py::gil_scoped_acquire gil{}; + if (!mMetadataString.has_value()) + { + return nullptr; + } + return mMetadataString.value().c_str(); + } + PLUGIN_API_CATCH("metadata_string") + return nullptr; + } + + void setNbOutputs(int32_t nbOutputs) + { + mNbOutputs = nbOutputs; + } + + void setTimingCachedId(std::string timingCachedId) + { + mTimingCachedId = std::move(timingCachedId); + } + + void setMetadataString(std::string metadataString) + { + mMetadataString = std::move(metadataString); + } + +private: + std::optional mNbOutputs{}; + std::optional mTimingCachedId{}; + std::optional mMetadataString{}; + std::optional> mTactics; + std::optional> mSupportedFormatCombinations{}; +}; + +class PyIPluginV3QuickRuntimeImpl : public IPluginV3QuickRuntime +{ +public: + using IPluginV3QuickRuntime::IPluginV3QuickRuntime; + PyIPluginV3QuickRuntimeImpl() = default; + PyIPluginV3QuickRuntimeImpl(const IPluginV3QuickRuntime& a){}; + + APILanguage getAPILanguage() const noexcept final + { + return APILanguage::kPYTHON; + } + + int32_t enqueue(PluginTensorDesc const* inputDesc, PluginTensorDesc const* outputDesc, void const* const* inputs, + void* const* outputs, Dims const* inputStrides, Dims const* outputStrides, int32_t nbInputs, int32_t nbOutputs, + cudaStream_t stream) noexcept override + { + try + { + py::gil_scoped_acquire gil{}; + + py::function pyEnqueue = utils::getOverride(static_cast(this), "enqueue"); + if (!pyEnqueue) + { + utils::throwPyError(PyExc_RuntimeError, "no implementation provided for enqueue()"); + } + + std::vector inVector; + std::vector outVector; + std::copy_n(inputDesc, nbInputs, std::back_inserter(inVector)); + std::copy_n(outputDesc, nbOutputs, std::back_inserter(outVector)); + + std::vector inPtrs; + for (int32_t idx = 0; idx < nbInputs; ++idx) + { + inPtrs.push_back(reinterpret_cast(inputs[idx])); + } + std::vector outPtrs; + for (int32_t idx = 0; idx < nbOutputs; ++idx) + { + outPtrs.push_back(reinterpret_cast(outputs[idx])); + } + + intptr_t cudaStreamPtr = reinterpret_cast(stream); - bool mIsNbOutputsInitialized{false}; - bool mIsTimingCachedIdInitialized{false}; - bool mIsFormatCombinationLimitInitialized{false}; - bool mIsMetadataStringInitialized{false}; - bool mIsTacticsInitialized{false}; -}; + std::vector inStrides; + std::vector outStrides; + std::copy_n(inputStrides, nbInputs, std::back_inserter(inStrides)); + std::copy_n(outputStrides, nbOutputs, std::back_inserter(outStrides)); -class PyIPluginV3OneBuildImpl : public PyIPluginV3OneBuildBaseImpl -{ -public: - PyIPluginV3OneBuildImpl() - : PyIPluginV3OneBuildBaseImpl(this) - { + try + { + pyEnqueue(inVector, outVector, inPtrs, outPtrs, inStrides, outStrides, cudaStreamPtr); + } + catch (py::error_already_set& e) + { + std::cerr << "[ERROR] Exception thrown from enqueue() " << e.what() << std::endl; + return -1; + } + return 0; + } + PLUGIN_API_CATCH("enqueue") + return -1; } - PyIPluginV3OneBuildImpl(IPluginV3OneBuild const& a) - : PyIPluginV3OneBuildBaseImpl(this){}; -}; -class PyIPluginV3OneBuildV2Impl : public PyIPluginV3OneBuildBaseImpl -{ -public: - PyIPluginV3OneBuildV2Impl() - : PyIPluginV3OneBuildBaseImpl(this) + int32_t setTactic(int32_t tactic) noexcept override { + try + { + py::gil_scoped_acquire gil{}; + + py::function pySetTactic = utils::getOverride(static_cast(this), "set_tactic"); + if (!pySetTactic) + { + utils::throwPyError(PyExc_RuntimeError, "no implementation provided for set_tactic()"); + } + + try + { + pySetTactic(tactic); + } + catch (py::error_already_set& e) + { + std::cerr << "[ERROR] Exception thrown from set_tactic() " << e.what() << std::endl; + return -1; + } + return 0; + } + PLUGIN_API_CATCH("set_tactic") + return -1; } - PyIPluginV3OneBuildV2Impl(IPluginV3OneBuildV2 const& a) - : PyIPluginV3OneBuildBaseImpl(this){}; - int32_t getAliasedInput(int32_t outputIndex) noexcept override + PluginFieldCollection const* getFieldsToSerialize() noexcept override { try { py::gil_scoped_acquire gil{}; - py::function pyGetAliasedInput - = py::get_override(static_cast(this), "get_aliased_input"); - - if (!pyGetAliasedInput) + py::function pyGetFieldsToSerialize + = utils::getOverride(static_cast(this), "get_fields_to_serialize"); + if (!pyGetFieldsToSerialize) { - // if no implementation is provided for get_aliased_input(), default to no aliasing - return -1; + utils::throwPyError(PyExc_RuntimeError, "no implementation provided for get_fields_to_serialize()"); } - py::object pyResult = pyGetAliasedInput(outputIndex); + py::object result = pyGetFieldsToSerialize(); try { - auto result = pyResult.cast(); - return result; + mFC = result.cast(); + return &mFC; } - PLUGIN_API_CATCH_CAST("get_aliased_input", "int32_t") - return 0U; + PLUGIN_API_CATCH_CAST("get_fields_to_serialize", "nvinfer1::PluginFieldCollection") + return nullptr; } - PLUGIN_API_CATCH("get_aliased_input") - return -1; + PLUGIN_API_CATCH("get_fields_to_serialize") + return nullptr; + } + + void setPluginType(std::string pluginType) + { + mPluginType = std::move(pluginType); + } + + void setPluginVersion(std::string pluginVersion) + { + mPluginVersion = std::move(pluginVersion); } + +private: + PluginFieldCollection mFC; + std::optional mNamespace; + std::optional mPluginType; + std::optional mPluginVersion; }; class PyIPluginV3OneRuntimeImpl : public IPluginV3OneRuntime @@ -1583,7 +2218,8 @@ class PyIPluginV3OneRuntimeImpl : public IPluginV3OneRuntime try { - return result.cast(); + mFC = result.cast(); + return &mFC; } PLUGIN_API_CATCH_CAST("get_fields_to_serialize", "nvinfer1::PluginFieldCollection") return nullptr; @@ -1610,6 +2246,7 @@ class PyIPluginV3OneRuntimeImpl : public IPluginV3OneRuntime std::string mNamespace; std::string mPluginType; std::string mPluginVersion; + PluginFieldCollection mFC; bool mIsNbOutputsInitialized{false}; bool mIsNamespaceInitialized{false}; @@ -1835,6 +2472,132 @@ class IPluginCreatorV3OneImpl : public IPluginCreatorV3One bool mIsPluginVersionInitialized{false}; }; +class IPluginCreatorV3QuickImpl : public IPluginCreatorV3Quick +{ +public: + IPluginCreatorV3QuickImpl() = default; + + APILanguage getAPILanguage() const noexcept final + { + return APILanguage::kPYTHON; + } + + char const* getPluginName() const noexcept override + { + try + { + py::gil_scoped_acquire gil{}; + if (!mName.has_value()) + { + utils::throwPyError(PyExc_AttributeError, "name not initialized"); + } + return mName.value().c_str(); + } + PLUGIN_API_CATCH("name") + return nullptr; + } + + char const* getPluginVersion() const noexcept override + { + try + { + py::gil_scoped_acquire gil{}; + if (!mPluginVersion.has_value()) + { + utils::throwPyError(PyExc_AttributeError, "plugin_version not initialized"); + } + return mPluginVersion.value().c_str(); + } + PLUGIN_API_CATCH("plugin_version") + return nullptr; + } + + PluginFieldCollection const* getFieldNames() noexcept override + { + try + { + py::gil_scoped_acquire gil{}; + if (!mFC.has_value()) + { + utils::throwPyError(PyExc_AttributeError, "field_names not initialized"); + } + return &mFC.value(); + } + PLUGIN_API_CATCH("field_names") + return nullptr; + } + + IPluginV3* createPlugin( + char const* name, char const* nspace, const PluginFieldCollection* fc, TensorRTPhase phase) noexcept override + { + try + { + py::gil_scoped_acquire gil{}; + + py::function pyCreatePlugin + = utils::getOverride(static_cast(this), "create_plugin"); + if (!pyCreatePlugin) + { + utils::throwPyError(PyExc_RuntimeError, "no implementation provided for create_plugin()"); + } + + std::string nameString{name}; + std::string namespaceString{nspace}; + + py::handle handle = pyCreatePlugin(nameString, namespaceString, fc, phase).release(); + try + { + return handle.cast(); + } + PLUGIN_API_CATCH_CAST("create_plugin", "IPluginV3*") + return nullptr; + } + PLUGIN_API_CATCH("create_plugin") + return nullptr; + } + + char const* getPluginNamespace() const noexcept override + { + try + { + py::gil_scoped_acquire gil{}; + if (!mNamespace.has_value()) + { + utils::throwPyError(PyExc_AttributeError, "plugin_namespace not initialized"); + } + return mNamespace.value().c_str(); + } + PLUGIN_API_CATCH("plugin_namespace") + return nullptr; + } + + void setFieldNames(PluginFieldCollection fc) + { + mFC = fc; + } + + void setPluginName(std::string name) + { + mName = std::move(name); + } + + void setPluginVersion(std::string pluginVersion) + { + mPluginVersion = std::move(pluginVersion); + } + + void setPluginNamespace(std::string pluginNamespace) + { + mNamespace = std::move(pluginNamespace); + } + +private: + std::optional mFC; + std::optional mNamespace; + std::optional mName; + std::optional mPluginVersion; +}; + namespace { bool isPython(IVersionedInterface const& versionedInterface) @@ -1988,6 +2751,10 @@ static const auto get_all_creators = [](IPluginRegistry& self) -> std::vector(ptr[i++])); } + if (std::strcmp(ptr[i]->getInterfaceInfo().kind, "PLUGIN CREATOR_V3QUICK") == 0) + { + return py::cast(static_cast(ptr[i++])); + } utils::throwPyError(PyExc_RuntimeError, "Unknown plugin creator type"); return py::none{}; }); @@ -2017,7 +2784,14 @@ static const auto get_capability_interface = [](IPluginV3& self, PluginCapabilit { if (type == PluginCapabilityType::kCORE) { - return py::cast(static_cast(capability_interface)); + try + { + return py::cast(static_cast(capability_interface)); + } + catch (py::cast_error const& e) + { + return py::cast(static_cast(capability_interface)); + } } if (type == PluginCapabilityType::kBUILD) { @@ -2031,12 +2805,26 @@ static const auto get_capability_interface = [](IPluginV3& self, PluginCapabilit { return py::cast(static_cast(capability_interface)); } - PLUGIN_API_CATCH_CAST("get_capability_interface", " a valid build capability interface") + catch (py::cast_error const& e) + { + try + { + return py::cast(static_cast(capability_interface)); + } + PLUGIN_API_CATCH_CAST("get_capability_interface", " a valid build capability interface") + } } } if (type == PluginCapabilityType::kRUNTIME) { - return py::cast(static_cast(capability_interface)); + try + { + return py::cast(static_cast(capability_interface)); + } + catch (py::cast_error const& e) + { + return py::cast(static_cast(capability_interface)); + } } } PLUGIN_API_CATCH_CAST("get_capability_interface", "nvinfer1::IPluginCapability") @@ -2063,6 +2851,10 @@ static const auto get_creator = [](IPluginRegistry& self, char const* pluginType { return py::cast(static_cast(creator)); } + if (std::strcmp(creator->getInterfaceInfo().kind, "PLUGIN CREATOR_V3QUICK") == 0) + { + return py::cast(static_cast(creator)); + } utils::throwPyError(PyExc_RuntimeError, "Unknown plugin creator type"); return py::none{}; } @@ -2079,6 +2871,10 @@ static const auto creator_create_plugin_v3 return self.createPlugin(name.c_str(), fc, phase); }; +static const auto creator_create_plugin_v3_quick = + [](IPluginCreatorV3Quick& self, std::string const& name, std::string const& nspace, PluginFieldCollection const* fc, + TensorRTPhase phase) { return self.createPlugin(name.c_str(), nspace.c_str(), fc, phase); }; + static const auto deserialize_plugin = [](IPluginCreator& self, std::string const& name, py::buffer& serializedPlugin) { py::buffer_info info = serializedPlugin.request(); return self.deserializePlugin(name.c_str(), info.ptr, info.size * info.itemsize); @@ -2170,6 +2966,16 @@ static const auto IPluginV3_set_num_outputs = [](IPluginV3OneBuild& self, int32_ utils::throwPyError(PyExc_AttributeError, "Can't set attribute: num_outputs is read-only for C++ plugins"); }; +static const auto IPluginV3_quick_set_num_outputs = [](IPluginV3QuickBuild& self, int32_t numOutputs) { + if (isPython(self)) + { + auto plugin = static_cast(&self); + plugin->setNbOutputs(numOutputs); + return; + } + utils::throwPyError(PyExc_AttributeError, "Can't set attribute: num_outputs is read-only for C++ plugins"); +}; + } // namespace lambdas namespace helpers @@ -2635,6 +3441,35 @@ void bindPlugin(py::module& m) // The following defs are only for documenting the API for Python-based plugins .def("get_aliased_input", &pluginDoc::getAliasedInput, IPluginV3Doc::get_valid_tactics); + py::class_>(m, "IPluginV3QuickCore", py::module_local()) + .def(py::init<>()) + .def(py::init()) + .def_property("plugin_name", &IPluginV3QuickCore::getPluginName, + py::cpp_function( + &helpers::setPluginName, py::keep_alive<1, 2>{})) + .def_property("plugin_version", &IPluginV3QuickCore::getPluginVersion, + py::cpp_function( + &helpers::setPluginVersion, py::keep_alive<1, 2>{})) + .def_property("plugin_namespace", &IPluginV3QuickCore::getPluginNamespace, + py::cpp_function( + &helpers::setPluginNamespace, py::keep_alive<1, 2>{})); + + py::class_>(m, "IPluginV3QuickBuild", py::module_local()) + .def(py::init<>()) + .def(py::init()) + .def_property("num_outputs", &IPluginV3QuickBuild::getNbOutputs, lambdas::IPluginV3_quick_set_num_outputs) + .def_property("metadata_string", &IPluginV3QuickBuild::getMetadataString, + py::cpp_function(lambdas::IPluginV3_get_metadata_string, py::keep_alive<1, 2>{})) + .def_property("timing_cache_id", &IPluginV3QuickBuild::getTimingCacheID, + py::cpp_function(lambdas::IPluginV3_get_timing_cache_id, py::keep_alive<1, 2>{})); + + py::class_>(m, "IPluginV3QuickRuntime", py::module_local()) + .def(py::init<>()) + .def(py::init()); + py::class_>( m, "IPluginV3OneRuntime", IPluginV3Doc::ipluginv3oneruntime_descr, py::module_local()) @@ -2692,6 +3527,25 @@ void bindPlugin(py::module& m) .def("create_plugin", lambdas::creator_create_plugin_v3, "name"_a, "field_collection"_a, "phase"_a, IPluginCreatorV3OneDoc::create_plugin); + py::class_( + m, "IPluginCreatorV3Quick", py::module_local()) + .def(py::init<>()) + .def_property("name", &IPluginCreatorV3Quick::getPluginName, + py::cpp_function( + &helpers::setPluginName, py::keep_alive<1, 2>{})) + .def_property("plugin_version", &IPluginCreatorV3Quick::getPluginVersion, + py::cpp_function( + &helpers::setPluginVersion, py::keep_alive<1, 2>{})) + .def_property("field_names", &helpers::getFieldNames, + py::cpp_function(&helpers::setPluginCreatorFieldNames, + py::keep_alive<1, 2>{}), + py::return_value_policy::reference_internal) + .def_property("plugin_namespace", &IPluginCreatorV3Quick::getPluginNamespace, + py::cpp_function( + &helpers::setPluginNamespace, py::keep_alive<1, 2>{})) + .def("create_plugin", lambdas::creator_create_plugin_v3_quick, "name"_a, "namespace"_a, "field_collection"_a, + "phase"_a); + py::class_>( m, "IPluginResourceContext", IPluginResourceContextDoc::descr, py::module_local()) // return_value_policy::reference_internal is default for the following @@ -2742,8 +3596,18 @@ void bindPlugin(py::module& m) .value("V1", PluginCreatorVersion::kV1) .value("V1_PYTHON", PluginCreatorVersion::kV1_PYTHON); - m.def("get_plugin_registry", &getPluginRegistry, py::return_value_policy::reference, - FreeFunctionsDoc::get_plugin_registry); + m.add_object("_plugin_registry", py::none()); + + m.def( + "get_plugin_registry", + [m]() { + if (m.attr("_plugin_registry").is_none()) + { + m.attr("_plugin_registry") = py::cast(getPluginRegistry()); + } + return m.attr("_plugin_registry"); + }, + py::return_value_policy::reference, FreeFunctionsDoc::get_plugin_registry); py::enum_( m, "PluginCapabilityType", py::arithmetic{}, PluginCapabilityTypeDoc::descr, py::module_local()) diff --git a/samples/common/sampleDevice.cpp b/samples/common/sampleDevice.cpp index 7964aeb5..e9ad78dd 100644 --- a/samples/common/sampleDevice.cpp +++ b/samples/common/sampleDevice.cpp @@ -31,6 +31,7 @@ void cudaCheck(cudaError_t ret, std::ostream& err) } } +#if !TRT_WINML // Construct GPU UUID string in the same format as nvidia-smi does. std::string getUuidString(cudaUUID_t uuid) { @@ -54,7 +55,6 @@ std::string getUuidString(cudaUUID_t uuid) void setCudaDevice(int32_t device, std::ostream& os) { -#if !TRT_WINML os << "=== Device Information ===" << std::endl; // Get the number of visible GPUs. @@ -113,7 +113,6 @@ void setCudaDevice(int32_t device, std::ostream& os) os << "Note: The application clock rates do not reflect the actual clock rates that the GPU is " << "currently running at." << std::endl; // clang-format on -#endif } int32_t getCudaDriverVersion() @@ -129,5 +128,6 @@ int32_t getCudaRuntimeVersion() cudaCheck(cudaRuntimeGetVersion(&version)); return version; } +#endif } // namespace sample diff --git a/samples/common/sampleDevice.h b/samples/common/sampleDevice.h index 986dccb4..ef6a00a2 100644 --- a/samples/common/sampleDevice.h +++ b/samples/common/sampleDevice.h @@ -532,6 +532,7 @@ class OutputAllocator : public nvinfer1::IOutputAllocator nvinfer1::Dims mFinalDims; }; +#if !TRT_WINML //! Set the GPU to run the inference on. void setCudaDevice(int32_t device, std::ostream& os); @@ -541,6 +542,8 @@ int32_t getCudaDriverVersion(); //! Get the CUDA version of the current CUDA runtime. int32_t getCudaRuntimeVersion(); +#endif + } // namespace sample #endif // TRT_SAMPLE_DEVICE_H diff --git a/samples/common/sampleEngines.cpp b/samples/common/sampleEngines.cpp index 51d1329c..5dddceeb 100644 --- a/samples/common/sampleEngines.cpp +++ b/samples/common/sampleEngines.cpp @@ -909,6 +909,11 @@ bool setupNetworkAndConfig(BuildOptions const& build, SystemOptions const& sys, } } + if (build.enableMonitorMemory) + { + config.setFlag(BuilderFlag::kMONITOR_MEMORY); + } + config.setProfilingVerbosity(build.profilingVerbosity); config.setAvgTimingIterations(build.avgTiming); @@ -1305,6 +1310,7 @@ bool loadStreamingEngineToBuildEnv(std::string const& filepath, BuildEnvironment return true; } + bool loadEngineToBuildEnv(std::string const& filepath, BuildEnvironment& env, std::ostream& err) { auto const tBegin = std::chrono::high_resolution_clock::now(); @@ -1644,9 +1650,9 @@ namespace void* initSafeRuntime() { void* handle{nullptr}; - // Currently libsafe_executor_debug.so for samplesCommon::isDebug() is not ready. + // Currently libnvinfer_safe_debug.so for samplesCommon::isDebug() is not ready. #if !defined(_WIN32) - std::string const dllName{"libsafe_executor.so"}; + std::string const dllName{"libnvinfer_safe.so"}; #if SANITIZER_BUILD handle = dlopen(dllName.c_str(), RTLD_LAZY | RTLD_NODELETE); #else diff --git a/samples/common/sampleEngines.h b/samples/common/sampleEngines.h index ec02e909..d1d88319 100644 --- a/samples/common/sampleEngines.h +++ b/samples/common/sampleEngines.h @@ -159,6 +159,7 @@ class LazilyDeserializedEngine return *mFileReader; } + //! //! \brief Get if safe mode is enabled. //! diff --git a/samples/common/sampleInference.cpp b/samples/common/sampleInference.cpp index ca0098d4..77a99c1d 100644 --- a/samples/common/sampleInference.cpp +++ b/samples/common/sampleInference.cpp @@ -256,6 +256,11 @@ bool setUpInference(InferenceEnvironment& iEnv, InferenceOptions const& inferenc // Release serialized blob to save memory space. iEnv.engine.releaseBlob(); +#if TRT_WINML + // Start JIT Compilation time after engine deserialization + auto jitCompileBegin = std::chrono::high_resolution_clock::now(); +#endif + // Setup weight streaming if enabled if (engine->getStreamableWeightsSize() > 0) { @@ -502,8 +507,17 @@ bool setUpInference(InferenceEnvironment& iEnv, InferenceOptions const& inferenc } auto const* context = iEnv.contexts.front().get(); - return FillStdBindings( + bool fillBindingsSuccess = FillStdBindings( engine, context, inference.inputs, iEnv.bindings, 1, endBindingIndex, inference.optProfileIndex)(); + +#if TRT_WINML + // Stop JIT Compile Time when setup for inference is complete + auto jitCompileEnd = std::chrono::high_resolution_clock::now(); + sample::gLogInfo << "JIT Compilation in " << std::chrono::duration(jitCompileEnd - jitCompileBegin).count() + << " sec." << std::endl; +#endif + + return fillBindingsSuccess; } TaskInferenceEnvironment::TaskInferenceEnvironment( @@ -1169,18 +1183,22 @@ bool timeDeserialize(InferenceEnvironment& iEnv, SystemOptions const& sys) bool deserializeOK{false}; engine.reset(nullptr); auto startClock = std::chrono::high_resolution_clock::now(); + SMP_RETVAL_IF_FALSE(!iEnv.safe, "Safe inference is not supported!", false, sample::gLogError); - auto& reader = iEnv.engine.getFileReader(); - reader.reset(); - ASSERT(reader.isOpen()); #if !TRT_WINML for (auto const& pluginPath : sys.dynamicPlugins) { rt->getPluginRegistry().loadLibrary(pluginPath.c_str()); } #endif + + auto& reader = iEnv.engine.getFileReader(); + ASSERT(reader.isOpen()); + reader.reset(); engine.reset(rt->deserializeCudaEngine(reader)); + deserializeOK = (engine != nullptr); + deserializeOK = (engine != nullptr); auto endClock = std::chrono::high_resolution_clock::now(); // return NAN if deserialization failed. diff --git a/samples/common/sampleOptions.cpp b/samples/common/sampleOptions.cpp index bdb1b21c..283091f1 100644 --- a/samples/common/sampleOptions.cpp +++ b/samples/common/sampleOptions.cpp @@ -1222,6 +1222,7 @@ void BuildOptions::parse(Arguments& arguments) getAndDelOption(arguments, "--excludeLeanRuntime", excludeLeanRuntime); getAndDelOption(arguments, "--noCompilationCache", disableCompilationCache); + getAndDelOption(arguments, "--monitorMemory", enableMonitorMemory); getAndDelNegOption(arguments, "--noTF32", tf32); getAndDelOption(arguments, "--fp16", fp16); getAndDelOption(arguments, "--bf16", bf16); @@ -2175,6 +2176,7 @@ std::ostream& operator<<(std::ostream& os, const BuildOptions& options) "timingCacheMode: "; printTimingCache(os, options.timingCacheMode) << std::endl << "timingCacheFile: " << options.timingCacheFile << std::endl << "Enable Compilation Cache: "<< boolToEnabled(!options.disableCompilationCache) << std::endl << + "Enable Monitor Memory: "<< boolToEnabled(options.enableMonitorMemory) << std::endl << "errorOnTimingCacheMiss: " << boolToEnabled(options.errorOnTimingCacheMiss) << std::endl << "Preview Features: "; printPreviewFlags(os, options) << std::endl << "MaxAuxStreams: " << options.maxAuxStreams << std::endl << @@ -2475,6 +2477,7 @@ void BuildOptions::help(std::ostream& os) " --excludeLeanRuntime When --versionCompatible is enabled, this flag indicates that the generated engine should" "\n" " not include an embedded lean runtime. If this is set, the user must explicitly specify a" "\n" " valid lean runtime to use when loading the engine." "\n" + " --monitorMemory Enable memory monitor report for debugging usage. (default = disabled)" "\n" " --sparsity=spec Control sparsity (default = disabled). " "\n" R"( Sparsity: spec ::= "disable", "enable", "force")" "\n" " Note: Description about each of these options is as below" "\n" diff --git a/samples/common/sampleOptions.h b/samples/common/sampleOptions.h index 8ca0a655..83e11fc4 100644 --- a/samples/common/sampleOptions.h +++ b/samples/common/sampleOptions.h @@ -238,6 +238,7 @@ class BuildOptions : public Options bool pluginInstanceNorm{false}; bool excludeLeanRuntime{false}; bool disableCompilationCache{false}; + bool enableMonitorMemory{false}; int32_t builderOptimizationLevel{defaultBuilderOptimizationLevel}; int32_t maxTactics{defaultMaxTactics}; SparsityFlag sparsity{SparsityFlag::kDISABLE}; diff --git a/samples/common/sampleUtils.h b/samples/common/sampleUtils.h index 6cd4280b..5d191219 100644 --- a/samples/common/sampleUtils.h +++ b/samples/common/sampleUtils.h @@ -78,9 +78,11 @@ std::vector splitToStringVec(std::string const& option, char separa bool broadcastIOFormats(std::vector const& formats, size_t nbBindings, bool isInput = true); +#if !TRT_WINML int32_t getCudaDriverVersion(); int32_t getCudaRuntimeVersion(); +#endif void sparsify(nvinfer1::INetworkDefinition& network, std::vector>& sparseWeights); void sparsify(nvinfer1::Weights const& weights, int32_t k, int32_t rs, std::vector& sparseWeights); diff --git a/samples/common/streamReader.h b/samples/common/streamReader.h index 7d4aa1c6..8d7f78ff 100644 --- a/samples/common/streamReader.h +++ b/samples/common/streamReader.h @@ -60,7 +60,7 @@ class FileStreamReader final : public nvinfer1::IStreamReader void reset() { - assert(mFile.good()); + ASSERT(mFile.good()); mFile.seekg(0); } @@ -73,6 +73,7 @@ class FileStreamReader final : public nvinfer1::IStreamReader std::ifstream mFile; }; + } // namespace samplesCommon #endif // STREAM_READER_H diff --git a/samples/python/detectron2/requirements.txt b/samples/python/detectron2/requirements.txt index d355b491..d9dcdc99 100644 --- a/samples/python/detectron2/requirements.txt +++ b/samples/python/detectron2/requirements.txt @@ -5,7 +5,7 @@ Pillow>=10.0.0 git+https://github.com/facebookresearch/detectron2.git git+https://github.com/NVIDIA/TensorRT#subdirectory=tools/onnx-graphsurgeon cuda-python==12.2.0; python_version <= "3.10" -cuda-python==12.5.0; python_version >= "3.11" +cuda-python==12.6.0; python_version >= "3.11" pywin32; platform_system == "Windows" pyyaml==6.0.1 requests==2.32.2 diff --git a/samples/python/downloader.py b/samples/python/downloader.py index c4240b3d..3c1e1e04 100755 --- a/samples/python/downloader.py +++ b/samples/python/downloader.py @@ -94,7 +94,7 @@ def _downloadFile(path, url): session.mount("http://", HTTPAdapter(max_retries=retries)) session.mount("https://", HTTPAdapter(max_retries=retries)) try: - r = session.get(url, stream=True, timeout=30) + r = session.get(url, stream=True, timeout=60) if r.status_code == 200: logger.info("Connecting to %s is successful.", url) @@ -108,11 +108,18 @@ def _downloadFile(path, url): progress_bar.update(len(chunk)) fd.write(chunk) progress_bar.close() + return True else: logger.info("Failed to connect to %s with status code: %s.", url, r.status_code) - + return False + + except requests.exceptions.ConnectionError as e: + logger.debug("Connection failed after retries:", e) + except requests.exceptions.Timeout as e: + logger.debug("A timeout occurred:", e) except requests.exceptions.RequestException as e: logger.debug("Error occurred while requesting connection to %s: %s.", url, e) + return False allGood = True for f in sample_data.files: @@ -130,7 +137,7 @@ def _downloadFile(path, url): allGood = False continue _createDirIfNeeded(fpath) - _downloadFile(fpath, f.url) + assert _downloadFile(fpath, f.url) if not _checkMD5(fpath, f.checksum): logger.error("The downloaded file %s has a different checksum!", fpath) allGood = False diff --git a/samples/python/efficientdet/requirements.txt b/samples/python/efficientdet/requirements.txt index c9e040ec..dfed86b8 100644 --- a/samples/python/efficientdet/requirements.txt +++ b/samples/python/efficientdet/requirements.txt @@ -6,7 +6,7 @@ onnxruntime==1.18.1; python_version >= "3.11" tf2onnx==1.8.1; python_version <= "3.10" tf2onnx==1.16.0; python_version >= "3.11" cuda-python==12.2.0; python_version <= "3.10" -cuda-python==12.5.0; python_version >= "3.11" +cuda-python==12.6.0; python_version >= "3.11" pywin32; platform_system == "Windows" pyyaml==6.0.1 requests==2.32.2 diff --git a/samples/python/efficientnet/requirements.txt b/samples/python/efficientnet/requirements.txt index 751c0789..83b9ea7c 100644 --- a/samples/python/efficientnet/requirements.txt +++ b/samples/python/efficientnet/requirements.txt @@ -5,7 +5,7 @@ tensorrt>=7.1.0.0 tf2onnx==1.8.1; python_version <= "3.10" tf2onnx==1.16.0; python_version >= "3.11" cuda-python==12.2.0; python_version <= "3.10" -cuda-python==12.5.0; python_version >= "3.11" +cuda-python==12.6.0; python_version >= "3.11" pywin32; platform_system == "Windows" pyyaml==6.0.1 requests==2.32.2 diff --git a/samples/python/engine_refit_onnx_bidaf/data_processing.py b/samples/python/engine_refit_onnx_bidaf/data_processing.py index f6740bc5..7eb052ad 100644 --- a/samples/python/engine_refit_onnx_bidaf/data_processing.py +++ b/samples/python/engine_refit_onnx_bidaf/data_processing.py @@ -24,9 +24,9 @@ def preprocess(text): try: - nltk.data.find("tokenizers/punkt") + nltk.data.find("tokenizers/punkt_tab") except LookupError: - nltk.download("punkt") + nltk.download("punkt_tab") tokens = word_tokenize(text) # split into lower-case word tokens, in numpy array with shape of (seq, 1) words = np.asarray([w.lower() for w in tokens]).reshape(-1, 1) diff --git a/samples/python/engine_refit_onnx_bidaf/requirements.txt b/samples/python/engine_refit_onnx_bidaf/requirements.txt index 84469f7a..c1c9c715 100644 --- a/samples/python/engine_refit_onnx_bidaf/requirements.txt +++ b/samples/python/engine_refit_onnx_bidaf/requirements.txt @@ -1,8 +1,8 @@ onnx==1.16.0 -nltk==3.8.1 +nltk==3.9.1 wget==3.2 cuda-python==12.2.0; python_version <= "3.10" -cuda-python==12.5.0; python_version >= "3.11" +cuda-python==12.6.0; python_version >= "3.11" pywin32; platform_system == "Windows" pyyaml==6.0.1 requests==2.32.2 diff --git a/samples/python/introductory_parser_samples/requirements.txt b/samples/python/introductory_parser_samples/requirements.txt index 01b57c06..fc537473 100644 --- a/samples/python/introductory_parser_samples/requirements.txt +++ b/samples/python/introductory_parser_samples/requirements.txt @@ -1,6 +1,6 @@ Pillow>=10.0.0 cuda-python==12.2.0; python_version <= "3.10" -cuda-python==12.5.0; python_version >= "3.11" +cuda-python==12.6.0; python_version >= "3.11" pywin32; platform_system == "Windows" pyyaml==6.0.1 requests==2.32.2 diff --git a/samples/python/network_api_pytorch_mnist/README.md b/samples/python/network_api_pytorch_mnist/README.md index 1f8dba76..c5fdfb0c 100644 --- a/samples/python/network_api_pytorch_mnist/README.md +++ b/samples/python/network_api_pytorch_mnist/README.md @@ -15,7 +15,7 @@ ## Description -This sample, `network_api_pytorch_mnist`, trains a convolutional model on the [MNIST](http://yann.lecun.com/exdb/mnist/) dataset and runs inference with a TensorRT engine. +This sample, `network_api_pytorch_mnist`, trains a convolutional model on the [MNIST](https://ossci-datasets.s3.amazonaws.com/mnist/) dataset and runs inference with a TensorRT engine. ## How does this sample work? @@ -79,7 +79,7 @@ The following resources provide a deeper understanding about getting started wit - [MNIST model](https://github.com/pytorch/examples/tree/master/mnist) **Dataset** -- [MNIST database](http://yann.lecun.com/exdb/mnist/) +- [MNIST database](https://ossci-datasets.s3.amazonaws.com/mnist/) **Documentation** - [Introduction To NVIDIA’s TensorRT Samples](https://docs.nvidia.com/deeplearning/sdk/tensorrt-sample-support-guide/index.html#samples) diff --git a/samples/python/network_api_pytorch_mnist/requirements.txt b/samples/python/network_api_pytorch_mnist/requirements.txt index 153cbfbd..71ef1a17 100644 --- a/samples/python/network_api_pytorch_mnist/requirements.txt +++ b/samples/python/network_api_pytorch_mnist/requirements.txt @@ -1,12 +1,8 @@ Pillow>=10.0.0 --f https://download.pytorch.org/whl/torch_stable.html -torch==2.0.0; (platform_machine=="aarch64" and sys.platform=="linux") -torch==2.2.1+cpu; ((platform_machine=="x86_64" and sys.platform=="linux") or sys.platform=="win32") --f https://download.pytorch.org/whl/torch_stable.html -torchvision==0.15.1; (platform_machine=="aarch64" and sys.platform=="linux") -torchvision==0.17.1+cpu; ((platform_machine=="x86_64" and sys.platform=="linux") or sys.platform=="win32") +torch +torchvision cuda-python==12.2.0; python_version <= "3.10" -cuda-python==12.5.0; python_version >= "3.11" +cuda-python==12.6.0; python_version >= "3.11" pywin32; platform_system == "Windows" pyyaml==6.0.1 requests==2.32.2 diff --git a/samples/python/non_zero_plugin/requirements.txt b/samples/python/non_zero_plugin/requirements.txt index 8f84ea54..595c3d8d 100644 --- a/samples/python/non_zero_plugin/requirements.txt +++ b/samples/python/non_zero_plugin/requirements.txt @@ -1,5 +1,5 @@ cuda-python==12.2.0; python_version <= "3.10" -cuda-python==12.5.0; python_version >= "3.11" +cuda-python==12.6.0; python_version >= "3.11" cupy-cuda12x torch --extra-index-url https://pypi.ngc.nvidia.com diff --git a/samples/python/onnx_custom_plugin/requirements.txt b/samples/python/onnx_custom_plugin/requirements.txt index 4be5b282..34c96c85 100644 --- a/samples/python/onnx_custom_plugin/requirements.txt +++ b/samples/python/onnx_custom_plugin/requirements.txt @@ -1,10 +1,10 @@ -nltk==3.8.1 +nltk==3.9.1 onnx==1.16.0 --extra-index-url https://pypi.ngc.nvidia.com onnx-graphsurgeon>=0.3.20 wget>=3.2 cuda-python==12.2.0; python_version <= "3.10" -cuda-python==12.5.0; python_version >= "3.11" +cuda-python==12.6.0; python_version >= "3.11" pywin32; platform_system == "Windows" pyyaml==6.0.1 requests==2.32.2 diff --git a/samples/python/onnx_packnet/requirements.txt b/samples/python/onnx_packnet/requirements.txt index 1cb16357..d672ad99 100644 --- a/samples/python/onnx_packnet/requirements.txt +++ b/samples/python/onnx_packnet/requirements.txt @@ -1,12 +1,8 @@ onnx==1.16.0 --extra-index-url https://pypi.ngc.nvidia.com onnx-graphsurgeon>=0.3.20 --f https://download.pytorch.org/whl/torch_stable.html -torch==2.0.0; (platform_machine=="aarch64" and sys.platform=="linux") -torch==2.2.1+cpu; ((platform_machine=="x86_64" and sys.platform=="linux") or sys.platform=="win32") --f https://download.pytorch.org/whl/torch_stable.html -torchvision==0.15.1; (platform_machine=="aarch64" and sys.platform=="linux") -torchvision==0.17.1+cpu; ((platform_machine=="x86_64" and sys.platform=="linux") or sys.platform=="win32") +torch +torchvision pyyaml==6.0.1 requests==2.32.2 tqdm==4.66.4 diff --git a/samples/python/python_plugin/CMakeLists.txt b/samples/python/python_plugin/CMakeLists.txt index 6338ea50..f31d0d34 100644 --- a/samples/python/python_plugin/CMakeLists.txt +++ b/samples/python/python_plugin/CMakeLists.txt @@ -39,6 +39,90 @@ if(NOT MSVC) set_ifndef(TRT_INCLUDE /usr/include/x86_64-linux-gnu) set_ifndef(CUDA_INC_DIR /usr/local/cuda/include) set_ifndef(CUDA_LIB_DIR /usr/local/cuda) + + find_program(NVCC_EXECUTABLE nvcc HINTS "${CUDA_LIB_DIR}/bin") + + # extract CUDA version + if(NVCC_EXECUTABLE) + execute_process( + COMMAND "${NVCC_EXECUTABLE}" --version + OUTPUT_VARIABLE NVCC_VERSION_OUTPUT + ERROR_VARIABLE NVCC_VERSION_ERROR + OUTPUT_STRIP_TRAILING_WHITESPACE + ) + # Parse the version number from the output + string(REGEX MATCH "release ([0-9]+)\\.([0-9]+)" CUDA_VERSION_MATCH "${NVCC_VERSION_OUTPUT}") + if(CUDA_VERSION_MATCH) + set(CUDA_VERSION_MAJOR "${CMAKE_MATCH_1}") + set(CUDA_VERSION_MINOR "${CMAKE_MATCH_2}") + set(CUDA_VER "${CUDA_VERSION_MAJOR}.${CUDA_VERSION_MINOR}") + else() + message(FATAL_ERROR "Could not parse CUDA version from nvcc output.") + endif() + else() + message(FATAL_ERROR "nvcc not found in ${CUDA_INST_DIR}/bin") + endif() + + # Function to check if the current CUDA version is greater than or equal to a specified version + function(cuda_ge major minor result_var) + set(VERSION_TO_COMPARE "${major}.${minor}") + if(CUDA_VER VERSION_GREATER_EQUAL "${VERSION_TO_COMPARE}") + set(${result_var} 1 PARENT_SCOPE) + else() + set(${result_var} 0 PARENT_SCOPE) + endif() + endfunction() + + # Loop through minor versions from 0 to 9 + foreach(minor RANGE 0 9) + set(result_var "CUDA_GE_11_${minor}") + cuda_ge(11 ${minor} ${result_var}) + endforeach() + + set(SAMPLE_SMS "75") + + if(CUDA_GE_11_0) + list(APPEND SAMPLE_SMS "80") + endif() + + if(CUDA_GE_11_1) + list(APPEND SAMPLE_SMS "86") + endif() + + if(CUDA_GE_11_4) + list(APPEND SAMPLE_SMS "87") + endif() + + if(CUDA_GE_11_8) + list(APPEND SAMPLE_SMS "89" "90") + endif() + + set(NON_HFC_SMS "89" "90") + + if(NOT DEFINED GENCODES) + set(GENCODES "") + + # Add -gencode flags for each SM in SAMPLE_SMS + foreach(sm ${SAMPLE_SMS}) + list(APPEND GENCODES "-gencode=arch=compute_${sm},code=sm_${sm}") + endforeach() + + # Filter out NON_HFC_SMS from SAMPLE_SMS to get HFC_SMS + set(HFC_SMS ${SAMPLE_SMS}) + foreach(sm ${NON_HFC_SMS}) + list(REMOVE_ITEM HFC_SMS "${sm}") + endforeach() + + # Get the highest supported forward compatible SM + if(HFC_SMS) + list(SORT HFC_SMS) + list(GET HFC_SMS -1 GEN_PTX_SM) + # Add PTX generation flag + list(APPEND GENCODES "-gencode=arch=compute_${GEN_PTX_SM},code=compute_${GEN_PTX_SM}") + else() + message(WARNING "No hardware forward compatible SMs found. PTX generation skipped.") + endif() + endif() endif() message("\nThe following variables are derived from the values of the previous variables unless provided explicitly:\n") @@ -49,11 +133,13 @@ set_ifndef(NVINFER_LIB ${_NVINFER_LIB}) find_library(_CUDA_LIB cuda HINTS ${CUDA_LIB_DIR} PATH_SUFFIXES lib/stubs lib64/stubs) set_ifndef(CUDA_LIB ${_CUDA_LIB}) + # -------- BUILDING -------- add_library(circ_pad_plugin SHARED ${CMAKE_SOURCE_DIR}/circ_plugin_cpp/circ_pad_plugin.cu ) +target_compile_options(circ_pad_plugin PRIVATE ${GENCODES}) target_include_directories(circ_pad_plugin PUBLIC ${CUDA_INC_DIR} diff --git a/samples/python/python_plugin/requirements.txt b/samples/python/python_plugin/requirements.txt index 0299b6b0..7c11ebb8 100644 --- a/samples/python/python_plugin/requirements.txt +++ b/samples/python/python_plugin/requirements.txt @@ -1,5 +1,5 @@ cuda-python==12.2.0; python_version <= "3.10" -cuda-python==12.5.0; python_version >= "3.11" +cuda-python==12.6.0; python_version >= "3.11" cupy-cuda12x numba triton; platform_system != "Windows" diff --git a/samples/python/quickly_deployable_plugins/README.md b/samples/python/quickly_deployable_plugins/README.md new file mode 100644 index 00000000..06533248 --- /dev/null +++ b/samples/python/quickly_deployable_plugins/README.md @@ -0,0 +1,221 @@ +# Quickly Deployable TRT Python Plugins [Experimental in TensorRT 10.6] + +This is a sample to showcase quickly deployable Python-based plugin definitions (QDPs) in TRT. QDPs are able to support a large majority of use cases for adding custom operators to TRT, and will be the recommended option when it becomes a stable feature in TRT 10.7. + +## Introduction + +While the regular TRT plugin interfaces are powerful in the flexibility and tunability they provide, for the vast majority of use cases, users will benefit from the simplicity offered by the QDP workflow. + - The `tensorrt.plugin` module provides many intuitive APIs that drastically reduces the amount of boilerplate required to implement a plugin + - The concept of plugin registration, plugin creators and the plugin registry is abstracted away + - The stateless nature of QDPs all but eliminates the complications of having to comply with a predefined plugin lifecycle + +This sample contains several mini-samples that demonstrate a few common use cases. + +## Setting Up The Environment + +To build and install the bindings, follow the instructions in `$TRT_OSSPATH/python/README.md`. + +Then install the requisite packages +```bash +cd $TRT_OSSPATH/samples/python/quickly_deployable_plugins +pip3 install -r requirements.txt +``` + +# Implementing a quickly deployable Python plugin + +QDP definitions consist of a set of decorated functions that define properties and behaviors of the plugin. + - `@tensorrt.plugin.register`: Returns shape and type characteristics of output tensors, and any attributes the plugin needs to function. + - `@tensorrt.plugin.impl`: Performs the plugin computation + - (Optional) `@tensorrt.plugin.autotune`: Defines the different data types and formats (tensor layouts) supported by the plugin's IO and any tactics supported by the plugin. Defining this function allows TensorRT to "tune" the plugin during the engine build to find the most performant type/format and tactic combination on the target system. + +The specifics of these functions will become clear through the following mini-samples. + +# A Simple Plugin: Elementwise-Add + +This mini-sample contains an elementwise addition plugin, where the computation is being performed with an OpenAI Triton kernel. Let's first take a look at the `tensorrt.plugin.register` function. + +```python +import tensorrt.plugin as trtp + +@trtp.register("sample::elemwise_add_plugin") +def add_plugin_desc(inp0: trtp.TensorDesc, block_size: int) -> trtp.TensorDesc: + return inp0.like() +``` + +The argument "sample::elemwise_add_plugin" defines the namespace ("sample") and name ("elemwise_add_plugin") of the plugin. Input arguments to the decorated function (`plugin_desc`) annotated with `trt.plugin.TensorDesc` denote the input tensors; all others are interpreted as plugin attributes (see the [TRT API Reference](https://docs.nvidia.com/deeplearning/tensorrt/api/python_api/infer/tensorrt.plugin/trt_plugin_register.html) for a full list of allowed attribute types). The output signature is a `trt.plugin.TensorDesc` describing the output. `inp0.like()` returns a tensor descriptor with identical shape and type characteristics to `inp0`. + +The computation function, decorated with `trt.plugin.impl`, receives `trt.plugin.Tensor`s for each input and output. In contrast to `TensorDesc`s, a `Tensor` references an underlying data buffer, directly accessible through `Tensor.data_ptr`. When working with Torch and OpenAI Triton kernels, it is easier to use `torch.as_tensor()` to zero-copy construct a `torch.Tensor` corresponding to the `trt.plugin.Tensor`. + +This sample also showcases the effect of omitting/defining a `trt.plugin.autotune` function, which must return a list of `trt.plugin.AutoTuneCombination`s. In this case, we define a single combination `AutoTuneCombination("FP32|FP16, FP32|FP16")`; this indicates that the input and output must be either both FP32 or both FP16. See the TRT API Reference for a detailed description of the grammar underlying `AutoTuneCombination`s. + +## Running the sample + +```bash +python3 qdp_runner.py add [--autotune] [-v] +``` + +`--autotune` simulates having defined a `trt.plugin.autotune` function. Enabling verbose logging (`-v`) is recommended to see the effect of autotuning. It can be observed that the `trt.plugin.impl` function is invoked several times during the engine build process when autotune is enabled. With autotuning turned off, `trt.plugin.impl` is invoked only once (when inference is run after building the engine). + +```bash +$ python3 qdp_runner.py add --autotune -v +... +Executing for inp0.dtype=DataType.FLOAT and output[0].dtype=DataType.FLOAT +Executing for inp0.dtype=DataType.FLOAT and output[0].dtype=DataType.FLOAT +Executing for inp0.dtype=DataType.FLOAT and output[0].dtype=DataType.FLOAT +Executing for inp0.dtype=DataType.FLOAT and output[0].dtype=DataType.FLOAT +Executing for inp0.dtype=DataType.HALF and output[0].dtype=DataType.HALF +Executing for inp0.dtype=DataType.HALF and output[0].dtype=DataType.HALF +Executing for inp0.dtype=DataType.HALF and output[0].dtype=DataType.HALF +Executing for inp0.dtype=DataType.HALF and output[0].dtype=DataType.HALF +[I] Finished engine building in 1.073 seconds +Executing for inp0.dtype=DataType.HALF and output[0].dtype=DataType.HALF +``` + +# Implementing in-place custom ops with I/O aliasing + +In-place computations can be accomplished with TRT plugins via aliased I/O. i.e. An input that needs to be modified in-place can be represented by an input-output pair, where the output is aliased to the input. For example, if in-place addition is needed (instead of the out-of-place addition of the above sample), that can be achieved as below: +```python +import tensorrt.plugin as trtp + +@trtp.register("sample::elemwise_add_plugin_") +def add_plugin_desc_(inp0: trtp.TensorDesc) -> trtp.TensorDesc: + return inp0.aliased() +``` + +Note the use of `trt.plugin.TensorDesc.aliased()` to produce an output `TensorDesc` that is aliased to `inp0`. + +To appreciate the effect of aliasing better, this sample adds two in-place add plugins chained together. + +## Running the sample + +Enabling verbose logging (`-v`) is recommended to see the effect of autotuning, which is always enabled. + +```bash +python3 qdp_runner.py inplace_add [--autotune] [-v] +``` + +# An op with data-dependent output shapes: Non-zero + +Non-zero is an operation where the indices of the non-zero elements of the input tensor is found -- it has data-dependent output shapes (DDS). As such, typical shape calculations cannot be done with input shapes. + +To handle DDS, the extent of each data-dependent output dimension must be expressed in terms of a *_size tensor_*, which is a scalar that communicates to TRT an upper-bound and an autotune value for that dimension, in terms of the input shapes. The TRT engine build may be optimized for the autotune value, but the extent of that dimension may stretch up to the upper-bound at runtime. + +In this sample, we consider a 2D input tensor `inp0`; the output will be an $N x 2$ tensor (a set of $N$ 2D indices), where $N$ is the number of non-zero indices. At maximum, all elements could be non-zero, and so the upper-bound could be expressed as `upper_bound = inp0.shape_expr[0] * inp0.shape_expr[1]`. Note that `trt.plugin.TensorDesc.shape_expr` returns symbolic shape expressions for that tensor. Arithmetic operations on shape expressions are supported through standard Python binary operators (see [TRT Python API reference](https://docs.nvidia.com/deeplearning/tensorrt/api/python_api/infer/tensorrt.plugin/Shape/ShapeExpr.html) for full list of supported operations). + +On average, we can expect half of the input to be filled with zero, so a size tensor can be constructed with that as the autotune value: +```python +st = trtp.size_tensor(opt = upper_bound // 2, upper_bound = upper_bound) +``` + +Now we're ready to construct the output shape. `st.expr()` returns a shape expression for the size tensor, so a tensor descriptor for the output shape can be constructed as `trt.plugin.from_shape_expr((st.expr(), 2), dtype=trt.int32)`. TRT requires that any size tensors also be made outputs of the plugin. Putting things together, we arrive at the following: + +```python +import tensorrt.plugin as trtp + +@trtp.register("sample::non_zero_plugin") +def non_zero_plugin_reg( + inp0: trtp.TensorDesc, +) -> Tuple[trtp.TensorDesc, trtp.TensorDesc]: + upper_bound = inp0.shape_expr[0] * inp0.shape_expr[1] + st = trtp.size_tensor(upper_bound // 2, upper_bound) + return trtp.from_shape_expr((st.expr(), 2), dtype=trt.int32), st +``` + +## Running the sample + +Enabling verbose logging (`-v`) is recommended to see the effect of autotuning, which is always enabled. + +```bash +python3 qdp_runner.py non_zero [-v] +``` + +# Using multiple tactics and ONNX: Cirular padding + +This sample contains a circular padding plugin, which is useful for ops like circular convolution. + +## ONNX model with a plugin + +It is often useful to run an ONNX node with a custom op through a TRT plugin that you have written. To allow the TRT ONNX parser to correctly recognize your plugin as being mapped to an ONNX node, ensure that + - The `op` property of the node is exactly the same as your plugin name. + - The node contains a string attribute called "plugin_namespace" with the namespace of your plugin. + +In this sample, we define a plugin with the ID "sample::circ_pad_plugin", so if using ONNX Graphsurgeon, the custom op node can be constructed as follows: + +```python +import onnx_graphsurgeon as gs + +var_x = gs.Variable(name="x", shape=inp_shape, dtype=np.float32) +var_y = gs.Variable(name="y", dtype=np.float32) + +circ_pad_node = gs.Node( + name="circ_pad_plugin", + op="circ_pad_plugin", + inputs=[var_x], + outputs=[var_y], + attrs={"pads": pads, "plugin_namespace": "sample"}, +) +``` + +## Multiple tactics + +Sometimes, you may have multiple kernels (or backends) that can be used to perform the computation of the plugin -- these are typically called *_tactics_*. If it cannot be predetermined which of these tactics may perform the fastest, it is possible to let TRT time the plugin for each tactic and determine which one is fastest. + +Communicating the availability of multiple tactics can simply be done through the `trt.plugin.autotune` function. +```python +import tensorrt.plugin as trtp +from enum import IntEnum + +class Tactic(IntEnum): + TORCH = 1 + TRITON = 2 + +@trt.plugin.autotune("sample::circ_pad_plugin") +def circ_pad_plugin_autotune(inp0: trtp.TensorDesc, pads: npt.NDArray[np.int32], outputs: Tuple[trtp.TensorDesc]) -> List[trtp.AutoTuneCombination]: + c = trtp.AutoTuneCombination() + c.pos([0, 1], "FP32|FP16") + c.tactics([int(Tactic.TORCH), int(Tactic.TRITON)]) + return [c] +``` + +Note that we're using another way of constructing a `trt.plugin.AutoTuneCombination` here -- namely, through `pos(...)` to populate the type/format information and `tactics(...)` to specify the tactics. In this sample, we use an OpenAI Triton kernel and `torch.nn.functional.pad` as two methods to compute the circular padding. + +## Loading and running a TRT engine containing a plugin + +If you have a TRT engine built with a plugin, executing that engine only requires the plugin definitions for `trt.plugin.register` and `trt.plugin.impl` to be available in the module where the engine is being deserialized (note: the `trt.plugin.autotune` definition is not required to be present). + +To simulate the loading of an engine, first run this sample with the `--save_engine` flag, followed by `--artifacts_dir [dir]` with a directory in which you wish the engine to be saved. Then run the sample again with `--load engine` and `--artifacts_dir` set to the same directory. + +## Running the sample + +```bash +python3 qdp_runner.py circ_pad [--multi_tactic] [--save_engine] [--load_engine] --mode {onnx,inetdef} [--artifacts_dir ARTIFACTS_DIR] [-v] + +options: + --multi_tactic Enable multiple tactics. + --save_engine Save engine to the artifacts_dir. + --load_engine Load engine from the artifacts_dir. Ignores all other options. + --artifacts_dir ARTIFACTS_DIR + Whether to store (or retrieve) artifacts. + --mode {onnx,inetdef} Whether to use ONNX parser or INetworkDefinition APIs to construct the network. + -v, --verbose Enable verbose log output. +``` + +# Additional resources + +**`tensorrt.plugin` API reference** +- [`tensorrt.plugin` module API reference](https://docs.nvidia.com/deeplearning/tensorrt/api/python_api/infer/tensorrt.plugin/index.html) + +**Guide to TensorRT plugins** +- [Extending TensorRT with Custom Layers](https://docs.nvidia.com/deeplearning/tensorrt/developer-guide/index.html#extending) + +# License + +For terms and conditions for use, reproduction, and distribution, see the [TensorRT Software License Agreement](https://docs.nvidia.com/deeplearning/sdk/tensorrt-sla/index.html) documentation. + +# Changelog + +October 2024: Initial release of this sample + +# Known issues + +There are no known issues in this sample diff --git a/samples/python/quickly_deployable_plugins/oait_kernels.py b/samples/python/quickly_deployable_plugins/oait_kernels.py new file mode 100644 index 00000000..fa6ecfe8 --- /dev/null +++ b/samples/python/quickly_deployable_plugins/oait_kernels.py @@ -0,0 +1,74 @@ +# +# SPDX-FileCopyrightText: Copyright (c) 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. +# + +import triton +import triton.language as tl + +@triton.jit +def add_kernel(x_ptr, y_ptr, n_elements, BLOCK_SIZE: tl.constexpr): + pid = tl.program_id(0) + offsets = pid * BLOCK_SIZE + tl.arange(0, BLOCK_SIZE) + mask = offsets < n_elements + x = tl.load(x_ptr + offsets, mask=mask) + tl.store(y_ptr + offsets, x + 1, mask=mask) + + +@triton.jit +def circ_pad( + X, + all_pads_0, + all_pads_2, + all_pads_4, + all_pads_6, + orig_dims_0, + orig_dims_1, + orig_dims_2, + orig_dims_3, + Y, + Y_shape_1, + Y_shape_2, + Y_shape_3, + X_len, + Y_len, + BLOCK_SIZE: tl.constexpr, +): + pid = tl.program_id(0) + i = pid * BLOCK_SIZE + tl.arange(0, BLOCK_SIZE) + + mask_y = i < Y_len + + i3 = i % Y_shape_3 + i2 = (i // Y_shape_3) % Y_shape_2 + i1 = (i // Y_shape_3 // Y_shape_2) % Y_shape_1 + i0 = i // Y_shape_3 // Y_shape_2 // Y_shape_1 + + j0 = (i0 - all_pads_0 + orig_dims_0) % orig_dims_0 + j1 = (i1 - all_pads_2 + orig_dims_1) % orig_dims_1 + j2 = (i2 - all_pads_4 + orig_dims_2) % orig_dims_2 + j3 = (i3 - all_pads_6 + orig_dims_3) % orig_dims_3 + + load_idx = ( + orig_dims_3 * orig_dims_2 * orig_dims_1 * j0 + + orig_dims_3 * orig_dims_2 * j1 + + orig_dims_3 * j2 + + j3 + ) + mask_x = load_idx < X_len + + x = tl.load(X + load_idx, mask=mask_x) + + tl.store(Y + i, x, mask=mask_y) diff --git a/samples/python/quickly_deployable_plugins/qdp_defs.py b/samples/python/quickly_deployable_plugins/qdp_defs.py new file mode 100644 index 00000000..19f60a27 --- /dev/null +++ b/samples/python/quickly_deployable_plugins/qdp_defs.py @@ -0,0 +1,248 @@ +# +# SPDX-FileCopyrightText: Copyright (c) 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. +# + +import tensorrt as trt +import torch +import numpy as np + +from typing import Tuple, List + +import tensorrt.plugin as trtp +import numpy.typing as npt + +import logging + +logging.basicConfig(level=logging.INFO) +logging.getLogger("QuicklyDeployablePlugins").setLevel(logging.INFO) + +########## Elemwise-add plugin definition ########## + + +@trtp.register("sample::elemwise_add_plugin") +def add_plugin_desc(inp0: trtp.TensorDesc, block_size: int) -> trtp.TensorDesc: + return inp0.like() + + +# Helper to simulate defining/omitting an autotune definition for the plugin +def register_autotune(): + # Type annotations can be omitted for autotune and impl definitions, but will be checked for consistency if added + @trtp.autotune("sample::elemwise_add_plugin") + def add_plugin_autotune( + inp0: trtp.TensorDesc, outputs: Tuple[trtp.TensorDesc] + ) -> List[trtp.AutoTuneCombination]: + return [trtp.AutoTuneCombination("FP32|FP16, FP32|FP16")] + + +@trtp.impl("sample::elemwise_add_plugin") +def add_plugin_impl( + inp0: trtp.Tensor, block_size: int, outputs: Tuple[trtp.Tensor], stream: int +) -> None: + + log = logging.getLogger("QuicklyDeployablePlugins") + log.debug( + f"Executing for inp0: dtype={inp0.dtype},format={inp0.format} and output[0]: dtype={outputs[0].dtype},format={outputs[0].format}" + ) + + n = inp0.numel() + inp0_t = torch.as_tensor(inp0, device="cuda") + out_t = torch.as_tensor(outputs[0], device="cuda") + + import triton + from oait_kernels import add_kernel + + add_kernel[(triton.cdiv(n, block_size),)](inp0_t, out_t, n, BLOCK_SIZE=block_size) + + +########## In-place elemwise-add plugin definition ########## + + +@trtp.register("sample::elemwise_add_plugin_") +def add_plugin_desc_(inp0: trtp.TensorDesc, delta: int) -> trtp.TensorDesc: + return inp0.aliased() + + +@trtp.autotune("sample::elemwise_add_plugin_") +def add_plugin_autotune_(inp0, outputs) -> List[trtp.AutoTuneCombination]: + return [ + trtp.AutoTuneCombination("FP32, FP32", "LINEAR*HWC"), + trtp.AutoTuneCombination("FP32|FP16, FP32|FP16", "LINEAR"), + ] + + +@trtp.impl("sample::elemwise_add_plugin_") +def add_plugin_impl_(inp0, delta: int, outputs, stream) -> None: + + log = logging.getLogger("QuicklyDeployablePlugins") + log.debug( + f"Executing for inp0: dtype={inp0.dtype},format={inp0.format} and output[0]: dtype={outputs[0].dtype},format={outputs[0].format}" + ) + + inp0_t = torch.as_tensor(inp0, device="cuda") + inp0_t.add_(delta) + + +########## Non-zero plugin (DDS) ########## + + +@trtp.register("sample::non_zero_plugin") +def non_zero_plugin_reg( + inp0: trtp.TensorDesc, +) -> Tuple[trtp.TensorDesc, trtp.TensorDesc]: + upper_bound = inp0.shape_expr[0] * inp0.shape_expr[1] + st = trtp.size_tensor(upper_bound // 2, upper_bound) + return trtp.from_shape_expr((st.expr(), 2), dtype=trt.int32), st + + +@trtp.autotune("sample::non_zero_plugin") +def non_zero_plugin_autotune(inp0, outputs) -> List[trtp.AutoTuneCombination]: + return [trtp.AutoTuneCombination("FP32|FP16, INT32, INT32")] + + +@trtp.impl("sample::non_zero_plugin") +def non_zero_plugin_impl(inp0, outputs, stream) -> None: + + log = logging.getLogger("QuicklyDeployablePlugins") + log.debug( + f"Executing for inp0: dtype={inp0.dtype},format={inp0.format} and output[0]: dtype={outputs[0].dtype},format={outputs[0].format}" + ) + + inp0_t = torch.as_tensor(inp0, device="cuda") + out_1 = torch.as_tensor(outputs[1], device="cuda").reshape((-1,)) + + out = torch.nonzero(inp0_t) + + out0 = torch.as_tensor(outputs[0].aliased(out.shape), device="cuda") + out0.copy_(out) + out_1.copy_(torch.Tensor([out.shape[0]])) + + +########## Circular padding plugin ######## + + +@trtp.register("sample::circ_pad_plugin") +def circ_pad_plugin_desc( + inp0: trtp.TensorDesc, pads: npt.NDArray[np.int32] +) -> trtp.TensorDesc: + ndim = inp0.ndim + out_desc = inp0.like() + + for i in range(np.size(pads) // 2): + out_desc.shape_expr[ndim - i - 1] += int(pads[i * 2] + pads[i * 2 + 1]) + + return out_desc + + +# Helper to define a multi-tactic implementation of the plugin +def enable_multi_tactic_circ_pad(): + + from enum import IntEnum + + class Tactic(IntEnum): + TORCH = 1 + TRITON = 2 + + @trtp.autotune("sample::circ_pad_plugin") + def circ_pad_plugin_autotune( + inp0: trtp.TensorDesc, + outputs: Tuple[trtp.TensorDesc], + ) -> List[trtp.AutoTuneCombination]: + c = trtp.AutoTuneCombination() + c.pos([0, 1], "FP32|FP16") + c.tactics([int(Tactic.TORCH), int(Tactic.TRITON)]) + return [c] + + @trtp.impl("sample::circ_pad_plugin") + def circ_pad_plugin_impl( + inp0: trtp.Tensor, + pads: npt.NDArray[np.int32], + outputs: Tuple[trtp.Tensor], + stream: int, + tactic: int, + ) -> None: + + log = logging.getLogger("QuicklyDeployablePlugins") + log.debug( + f"Executing for inp0: dtype={inp0.dtype},format={inp0.format} and output[0]: dtype={outputs[0].dtype},format={outputs[0].format}" + ) + + inp_t = torch.as_tensor(inp0, device="cuda") + out_t = torch.as_tensor(outputs[0], device="cuda") + + if tactic == Tactic.TORCH: + out = torch.nn.functional.pad(inp_t, pads.tolist(), mode="circular") + out_t.copy_(out) + elif tactic == Tactic.TRITON: + N = inp0.ndim + all_pads = np.zeros((N * 2,), dtype=np.int32) + out_dims = trtp.Shape(tuple(inp0.shape)) + + for i in range(np.size(pads) // 2): + out_dims[N - i - 1] += pads[i * 2] + pads[i * 2 + 1] + all_pads[N * 2 - 2 * i - 2] = pads[i * 2] + all_pads[N * 2 - 2 * i - 1] = pads[i * 2 + 1] + + all_pads = all_pads.tolist() + + block_size = 256 + num_blocks = tuple( + [int((np.prod(out_dims) + block_size - 1) // block_size)] + ) + + from oait_kernels import circ_pad + + circ_pad[num_blocks]( + inp_t, + all_pads[0], + all_pads[2], + all_pads[4], + all_pads[6], + inp0.shape[0], + inp0.shape[1], + inp0.shape[2], + inp0.shape[3], + out_t, + int(out_dims[1]), + int(out_dims[2]), + int(out_dims[3]), + inp0.numel(), + out_dims.numel(), + BLOCK_SIZE=block_size, + ) + + +# Helper to define a single tactic implementation of the plugin +def enable_single_tactic_circ_pad(): + @trtp.autotune("sample::circ_pad_plugin") + def circ_pad_plugin_autotune( + inp0: trtp.TensorDesc, + outputs: Tuple[trtp.TensorDesc], + ) -> List[trtp.AutoTuneCombination]: + + return [trtp.AutoTuneCombination("FP32|FP16, FP32|FP16")] + + @trtp.impl("sample::circ_pad_plugin") + def circ_pad_plugin_impl( + inp0: trtp.Tensor, + pads: npt.NDArray[np.int32], + outputs: Tuple[trtp.Tensor], + stream: int, + ) -> None: + inp_t = torch.as_tensor(inp0, device="cuda") + out_t = torch.as_tensor(outputs[0], device="cuda") + + out = torch.nn.functional.pad(inp_t, pads.tolist(), mode="circular") + out_t.copy_(out) diff --git a/samples/python/quickly_deployable_plugins/qdp_runner.py b/samples/python/quickly_deployable_plugins/qdp_runner.py new file mode 100644 index 00000000..f2949ef5 --- /dev/null +++ b/samples/python/quickly_deployable_plugins/qdp_runner.py @@ -0,0 +1,359 @@ +# +# SPDX-FileCopyrightText: Copyright (c) 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. +# + +import tensorrt as trt +import torch +import numpy as np + +from polygraphy.backend.trt import ( + CreateConfig, + TrtRunner, + create_network, + engine_from_network, + network_from_onnx_path, + bytes_from_engine, + engine_from_bytes, +) + +from polygraphy.backend.common import bytes_from_path +from polygraphy import cuda + +import onnx_graphsurgeon as gs +import onnx +import os +import argparse + +import tensorrt.plugin as trtp + +import qdp_defs +import logging + +def run_add(enable_autotune=False): + + if enable_autotune: + qdp_defs.register_autotune() + + BLOCK_SIZE = 256 + + builder, network = create_network() + x = torch.randint(10, (10, 3, 32, 32), dtype=torch.float32, device="cuda") + + # Populate network + i_x = network.add_input(name="x", dtype=trt.DataType.FLOAT, shape=x.shape) + + out = network.add_plugin( + trtp.op.sample.elemwise_add_plugin(i_x, block_size=BLOCK_SIZE) + ) + out.get_output(0).name = "y" + network.mark_output(tensor=out.get_output(0)) + + builder.create_builder_config() + + engine = engine_from_network( + (builder, network), + CreateConfig(fp16=True), + ) + + with TrtRunner(engine, "trt_runner") as runner: + outputs = runner.infer( + { + "x": x, + }, + copy_outputs_to_host=False, + ) + + if torch.allclose(x + 1, outputs["y"]): + print("Inference result is correct!") + else: + print("Inference result is incorrect!") + + +def run_inplace_add(): + builder, network = create_network() + x = torch.ones((10, 3, 32, 32), dtype=torch.float32, device="cuda") + + x_clone = x.clone() + + i_x = network.add_input(name="x", dtype=trt.DataType.FLOAT, shape=x.shape) + + # Amounts to elementwise-add in the first and second plugins + deltas = (2, 4) + + out0 = network.add_plugin(trtp.op.sample.elemwise_add_plugin_(i_x, delta=deltas[0])) + out1 = network.add_plugin( + trtp.op.sample.elemwise_add_plugin_(out0.get_output(0), delta=deltas[1]) + ) + out1.get_output(0).name = "y" + network.mark_output(tensor=out1.get_output(0)) + + builder.create_builder_config() + + # Enable preview feature for aliasing plugin I/O + config = CreateConfig( + fp16=True, preview_features=[trt.PreviewFeature.ALIASED_PLUGIN_IO_10_03] + ) + + engine = engine_from_network( + (builder, network), + config, + ) + + context = engine.create_execution_context() + + stream = cuda.Stream() + + context.set_tensor_address("x", x.data_ptr()) + context.set_tensor_address("y", x.data_ptr()) + context.execute_async_v3(stream.ptr) + stream.synchronize() + + if torch.allclose(x, x_clone + sum(deltas), atol=1e-2): + print("Inference result is correct!") + else: + print("Inference result is incorrect!") + print(x[0][0][0][:10]) + print(x_clone[0][0][0][:10]) + + +def run_non_zero(): + builder, network = create_network() + inp_shape = (128, 128) + + X = np.random.normal(size=inp_shape).astype(trt.nptype(trt.DataType.FLOAT)) + + # Zero out some random indices + indices = np.random.choice( + np.prod(inp_shape), + replace=False, + size=np.random.randint(0, np.prod(inp_shape) + 1), + ) + X[np.unravel_index(indices, inp_shape)] = 0 + + # Populate network + i_x = network.add_input(name="X", dtype=trt.DataType.FLOAT, shape=inp_shape) + + out = network.add_plugin(trtp.op.sample.non_zero_plugin(i_x)) + out.get_output(0).name = "Y" + network.mark_output(tensor=out.get_output(0)) + + builder.create_builder_config() + + engine = engine_from_network( + (builder, network), + config=CreateConfig(fp16=True), + ) + + Y_ref = np.transpose(np.nonzero(X)) + + with TrtRunner(engine, "trt_runner") as runner: + outputs = runner.infer({"X": X}) + Y = outputs["Y"] + Y = Y[np.lexsort(np.fliplr(Y).T)] + + if np.allclose(Y, Y_ref, atol=1e-3): + print("Inference result is correct!") + else: + print("Inference result is incorrect!") + + +def check_artifacts_dir_exists(artifacts_dir): + if not os.path.exists(artifacts_dir): + raise ValueError(f"artifacts_dir '{artifacts_dir}' does not exist") + + +def run_circ_pad( + enable_multi_tactic=False, mode="onnx", artifacts_dir=None, save_or_load_engine=None +): + + if enable_multi_tactic: + qdp_defs.enable_multi_tactic_circ_pad() + else: + qdp_defs.enable_single_tactic_circ_pad() + + inp_shape = (10, 3, 32, 32) + x = np.random.normal(size=inp_shape).astype(trt.nptype(trt.DataType.FLOAT)) + + pads = np.array((1, 1, 1, 1), dtype=np.int32) + + if save_or_load_engine is not None and save_or_load_engine is False: + check_artifacts_dir_exists(artifacts_dir) + engine_path = os.path.join(artifacts_dir, "circ_pad.engine") + engine = engine_from_bytes(bytes_from_path(engine_path)) + else: + if mode == "inetdef": + builder, network = create_network() + i_x = network.add_input(name="x", dtype=trt.DataType.FLOAT, shape=x.shape) + out = network.add_plugin(trtp.op.sample.circ_pad_plugin(i_x, pads=pads)) + out.get_output(0).name = "y" + network.mark_output(tensor=out.get_output(0)) + + engine = engine_from_network( + (builder, network), + CreateConfig(fp16=True), + ) + elif mode == "onnx": + if artifacts_dir is None: + raise ValueError("'artifacts_dir' must be specified in onnx mode") + + check_artifacts_dir_exists(artifacts_dir) + + onnx_path = os.path.join(artifacts_dir, "circ_pad.onnx") + var_x = gs.Variable(name="x", shape=inp_shape, dtype=np.float32) + var_y = gs.Variable(name="y", dtype=np.float32) + circ_pad_node = gs.Node( + name="circ_pad_plugin", + op="circ_pad_plugin", + inputs=[var_x], + outputs=[var_y], + attrs={"pads": pads, "plugin_namespace": "sample"}, + ) + graph = gs.Graph( + nodes=[circ_pad_node], inputs=[var_x], outputs=[var_y], opset=16 + ) + onnx.save(gs.export_onnx(graph), onnx_path) + + engine = engine_from_network( + network_from_onnx_path(onnx_path), CreateConfig(fp16=True) + ) + else: + raise ValueError(f"Unknown mode {mode}") + + if save_or_load_engine is not None and save_or_load_engine is True: + check_artifacts_dir_exists(artifacts_dir) + engine_path = os.path.join(artifacts_dir, "circ_pad.engine") + with open(engine_path, "wb") as f: + f.write(bytes_from_engine(engine)) + + Y_ref = np.pad(x, [[0, 0], [0, 0], [pads[0], pads[1]], [pads[2], pads[3]]], "wrap") + + with TrtRunner(engine, "trt_runner") as runner: + outputs = runner.infer({"x": x}) + Y = outputs["y"] + + if np.allclose(Y, Y_ref, atol=1e-2): + print("Inference result is correct!") + else: + print("Inference result is incorrect!") + + +def setup_add_sample(subparsers): + subparser = subparsers.add_parser("add", help="'add' sample help") + subparser.add_argument("--autotune", action="store_true", help="Enable autotuning") + subparser.add_argument( + "-v", "--verbose", action="store_true", help="Enable more verbose log output" + ) + + +def setup_inplace_add_sample(subparsers): + subparser = subparsers.add_parser("inplace_add", help="inplace_add sample help") + subparser.add_argument( + "-v", "--verbose", action="store_true", help="Enable more verbose log output" + ) + + +def setup_non_zero_sample(subparsers): + subparser = subparsers.add_parser("non_zero", help="non_zero sample help") + subparser.add_argument( + "-v", "--verbose", action="store_true", help="Enable more verbose log output" + ) + + +def setup_circ_pad_sample(subparsers): + subparser = subparsers.add_parser("circ_pad", help="circ_pad sample help") + subparser.add_argument( + "--multi_tactic", action="store_true", help="Enable multiple tactics" + ) + subparser.add_argument( + "--save_engine", action="store_true", help="Save engine to the artifacts_dir" + ) + subparser.add_argument( + "--load_engine", + action="store_true", + help="Load engine from the artifacts_dir. Ignores all other options.", + ) + subparser.add_argument( + "--artifacts_dir", + type=str, + help="Whether to store (or retrieve) artifacts.", + ) + subparser.add_argument( + "--mode", + type=str, + choices=["onnx", "inetdef"], + help="Whether to use ONNX parser or INetworkDefinition APIs to construct the network.", + ) + subparser.add_argument( + "-v", "--verbose", action="store_true", help="Enable verbose log output" + ) + + return subparser + + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + + parser = argparse.ArgumentParser(description="Main script help") + subparsers = parser.add_subparsers(dest="sample", help="Mode help", required=True) + + setup_add_sample(subparsers) + setup_inplace_add_sample(subparsers) + circ_pad_subparser = setup_circ_pad_sample(subparsers) + setup_non_zero_sample(subparsers) + + args = parser.parse_args() + + if args.verbose: + logging.getLogger("QuicklyDeployablePlugins").setLevel(logging.DEBUG) + + if args.sample == "add": + run_add(args.autotune) + if args.sample == "inplace_add": + run_inplace_add() + if args.sample == "non_zero": + run_non_zero() + if args.sample == "circ_pad": + if args.mode == "onnx": + if args.artifacts_dir is None: + parser.error( + "circ_pad: argument --mode: When mode is 'onnx', artifacts_dir is required" + ) + + save_or_load_engine = None + + if args.load_engine is True: + if args.save_engine is True: + parser.error( + "circ_pad: save_engine and load_engine cannot be specified at the same time. First save_engine and load_engine separately." + ) + else: + if args.multi_tactic is True or args.mode is not None: + print( + "warning circ_pad: when load_engine is specified, all other options except 'artifacts_dir' is ignored." + ) + + save_or_load_engine = False + else: + if args.mode is None: + circ_pad_subparser.print_help() + parser.error( + "circ_pad: '--mode' option is required." + ) + + if args.save_engine is True: + save_or_load_engine = True + + run_circ_pad(args.multi_tactic, args.mode, args.artifacts_dir, save_or_load_engine) diff --git a/samples/python/quickly_deployable_plugins/requirements.txt b/samples/python/quickly_deployable_plugins/requirements.txt new file mode 100644 index 00000000..1b40b0c2 --- /dev/null +++ b/samples/python/quickly_deployable_plugins/requirements.txt @@ -0,0 +1,13 @@ +triton; platform_system != "Windows" +torch +--extra-index-url https://pypi.ngc.nvidia.com +polygraphy +colored +numpy==1.23.5; (platform_system != "Windows" and python_version <= "3.10") +numpy==1.26.4; (platform_system != "Windows" and python_version >= "3.11") +onnx==1.16.0; platform_system == "Windows" +--extra-index-url https://pypi.ngc.nvidia.com +onnx-graphsurgeon +pyyaml==6.0.1 +requests==2.32.2 +tqdm==4.66.4 diff --git a/samples/python/quickly_deployable_plugins/requirements.yml b/samples/python/quickly_deployable_plugins/requirements.yml new file mode 100644 index 00000000..39a43539 --- /dev/null +++ b/samples/python/quickly_deployable_plugins/requirements.yml @@ -0,0 +1,33 @@ +# SPDX-FileCopyrightText: Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: LicenseRef-NvidiaProprietary +# +# NVIDIA CORPORATION, its affiliates and licensors retain all intellectual +# property and proprietary rights in and to this material, related +# documentation and any modifications thereto. Any use, reproduction, +# disclosure or distribution of this material and related documentation +# without an express license agreement from NVIDIA CORPORATION or +# its affiliates is strictly prohibited. + +--- +args: + polygraphy: + - '--extra-index-url https://pypi.ngc.nvidia.com' +conditions: + onnx-graphsurgeon: + - onnx-graphsurgeon + onnx: + - onnx==1.16.0; platform_system == "Windows" + triton: + - triton; platform_system != "Windows" + numpy: + - 'numpy==1.23.5; (platform_system != "Windows" and python_version <= "3.10")' + - 'numpy==1.26.4; (platform_system != "Windows" and python_version >= "3.11")' +packages: + - triton + - torch + - polygraphy + - colored + - numpy + - onnx + - onnx-graphsurgeon +... diff --git a/samples/python/sample_weight_stripping/requirements.txt b/samples/python/sample_weight_stripping/requirements.txt index 01b57c06..fc537473 100644 --- a/samples/python/sample_weight_stripping/requirements.txt +++ b/samples/python/sample_weight_stripping/requirements.txt @@ -1,6 +1,6 @@ Pillow>=10.0.0 cuda-python==12.2.0; python_version <= "3.10" -cuda-python==12.5.0; python_version >= "3.11" +cuda-python==12.6.0; python_version >= "3.11" pywin32; platform_system == "Windows" pyyaml==6.0.1 requests==2.32.2 diff --git a/samples/python/simple_progress_monitor/requirements.txt b/samples/python/simple_progress_monitor/requirements.txt index 01b57c06..fc537473 100644 --- a/samples/python/simple_progress_monitor/requirements.txt +++ b/samples/python/simple_progress_monitor/requirements.txt @@ -1,6 +1,6 @@ Pillow>=10.0.0 cuda-python==12.2.0; python_version <= "3.10" -cuda-python==12.5.0; python_version >= "3.11" +cuda-python==12.6.0; python_version >= "3.11" pywin32; platform_system == "Windows" pyyaml==6.0.1 requests==2.32.2 diff --git a/samples/python/tensorflow_object_detection_api/requirements.txt b/samples/python/tensorflow_object_detection_api/requirements.txt index eb6d1ce3..e38c8ef9 100644 --- a/samples/python/tensorflow_object_detection_api/requirements.txt +++ b/samples/python/tensorflow_object_detection_api/requirements.txt @@ -7,7 +7,7 @@ tf2onnx==1.15.0 pycocotools; platform_system != "Windows" pycocotools-windows; platform_system == "Windows" cuda-python==12.2.0; python_version <= "3.10" -cuda-python==12.5.0; python_version >= "3.11" +cuda-python==12.6.0; python_version >= "3.11" pywin32; platform_system == "Windows" Cython<3.0 pyyaml==6.0.1 diff --git a/samples/python/yolov3_onnx/requirements.txt b/samples/python/yolov3_onnx/requirements.txt index 9a9e9a27..32c7e45c 100644 --- a/samples/python/yolov3_onnx/requirements.txt +++ b/samples/python/yolov3_onnx/requirements.txt @@ -1,5 +1,5 @@ cuda-python==12.2.0; python_version <= "3.10" -cuda-python==12.5.0; python_version >= "3.11" +cuda-python==12.6.0; python_version >= "3.11" pywin32; platform_system == "Windows" numpy==1.24.4; python_version <= "3.10" numpy==1.26.4; python_version >= "3.11" diff --git a/samples/trtexec/README.md b/samples/trtexec/README.md index 9b65c8e1..3d733160 100644 --- a/samples/trtexec/README.md +++ b/samples/trtexec/README.md @@ -53,12 +53,37 @@ Compile the sample by following build instructions in [TensorRT README](https:// ### Example 1: Profiling a custom layer -You can profile a custom layer using the `IPluginRegistry` for the plugins and `trtexec`. You’ll need to first register the plugin with `IPluginRegistry`. +You can profile a custom layer, implemented as a [TensorRT plugin](https://github.com/NVIDIA/TensorRT/tree/main/plugin#tensorrt-plugins), by leveraging `trtexec`. Plugins need to be registered in the plugin registry (instance of `IPluginRegistry`) to be visible to TensorRT. `trtexec` will load the TensorRT standard plugin library (`libnvinfer_plugin.so` / `nvinfer_plugin.dll`) that provides plugin support to TensorRT. Checkout the [Non-Zero Plugins Sample](../sampleNonZeroPlugin/) for a quick sample, or the [Plugins section](https://docs.nvidia.com/deeplearning/tensorrt/developer-guide/index.html#extending) of the TensorRT Developer Guide for a more detailed walkthrough. -If you are using TensorRT shipped plugins, you should load the `libnvinfer_plugin.so` file, as these plugins are pre-registered. +Plugins can be used with `trtexec` in the following 2 ways: -If you have your own plugin, then it has to be registered explicitly. The following macro can be used to register the plugin creator `YourPluginCreator` with the `IPluginRegistry`. -`REGISTER_TENSORRT_PLUGIN(YourPluginCreator);` +

+ Using TensorRT-shipped Plugins + + +- If you are using TensorRT-shipped plugins (included in `libnvinfer_plugin.so` / `nvinfer_plugin.dll`), no extra steps are required from the user as these plugins are pre-registered with the plugin registry. +
+ +
+ Using your own Plugin + + - If you want to define your own plugin and have `trtexec` use it as part of the network, you should define your own _Plugin Shared library_ with specific entry-points recognized by TensorRT. Then, provide the shared plugin library path to `trtexec` using the `--dynamicPlugins` flag. + - More information on Plugin Shared Libraries and how to define them can be seen in the [Plugin Shared Libraries](https://docs.nvidia.com/deeplearning/tensorrt/developer-guide/index.html#plugin-serialization) section of the [TensorRT Developer Guide](https://docs.nvidia.com/deeplearning/tensorrt/developer-guide/index.html). + + In summary, there are two methods: + 1. The `REGISTER_TENSORRT_PLUGIN` macro can be applied to the plugin creator for each plugin that needs to be statically registered. i.e. Registered at load-time of the plugin library. + 2. For dynamic registration, the plugin shared library must expose the below symbols which will be the entry points for TensorRT: + + ```cpp + extern "C" void setLoggerFinder(ILoggerFinder* finder); + extern "C" IPluginCreatorInterface* const* getCreators(int32_t& nbCreators) + ``` + In the above, `setLoggerFinder()` should accept a pointer to an `ILoggerFinder`, through which an `ILogger` instance can be retrieved for the purpose of logging inside the library code. `getCreators()` should return an array of plugin creators the library contains. Example implementations of these entry points can be found in [plugin/vc/vfcCommon.cpp](../../plugin/vc/vfcCommon.cpp) and [plugin/vc/vfcCommon.h](../../plugin/vc/vfcCommon.h). + + **Note**: Usage of `getPluginCreators` instead of `getCreators` is also valid, but deprecated. + - If the user wants to build a TensorRT engine first and run later, the user has the option to serialize the shared plugin library as part of the engine itself by specifying `--setPluginsToSerialize`. By doing so, the user does not have to specify `--dynamicPlugins` to `trtexec` when running the built engine. + - For more information on these flags, run `./trtexec --help`. +
### Example 2: Running a network on DLA diff --git a/samples/trtexec/trtexec.cpp b/samples/trtexec/trtexec.cpp index a701c149..96b1b8e1 100644 --- a/samples/trtexec/trtexec.cpp +++ b/samples/trtexec/trtexec.cpp @@ -272,8 +272,9 @@ int main(int argc, char** argv) { sample::setReportableSeverity(ILogger::Severity::kVERBOSE); } - +#if !TRT_WINML setCudaDevice(options.system.device, sample::gLogInfo); +#endif sample::gLogInfo << std::endl; sample::gLogInfo << "TensorRT version: " << NV_TENSORRT_MAJOR << "." << NV_TENSORRT_MINOR << "." << NV_TENSORRT_PATCH << std::endl; @@ -433,6 +434,7 @@ int main(int argc, char** argv) if (profilerEnabled && !options.inference.rerun) { iEnv->profiler.reset(new Profiler); +#if !TRT_WINML if (options.inference.graph && (getCudaDriverVersion() < 11010 || getCudaRuntimeVersion() < 11000)) { options.inference.graph = false; @@ -441,6 +443,7 @@ int main(int argc, char** argv) "and disabled CUDA graph." << std::endl; } +#endif } if (!setUpInference(*iEnv, options.inference, options.system)) @@ -486,6 +489,7 @@ int main(int argc, char** argv) iEnv->profiler.reset(profiler); iEnv->contexts.front()->setProfiler(profiler); iEnv->contexts.front()->setEnqueueEmitsProfile(false); +#if !TRT_WINML if (options.inference.graph && (getCudaDriverVersion() < 11010 || getCudaRuntimeVersion() < 11000)) { options.inference.graph = false; @@ -494,6 +498,7 @@ int main(int argc, char** argv) "and disabled CUDA graph." << std::endl; } +#endif if (!runInference(options.inference, *iEnv, options.system.device, trace)) { sample::gLogError << "Error occurred during inference" << std::endl; diff --git a/tools/Polygraphy/CHANGELOG.md b/tools/Polygraphy/CHANGELOG.md index 5196870f..eda347fc 100644 --- a/tools/Polygraphy/CHANGELOG.md +++ b/tools/Polygraphy/CHANGELOG.md @@ -3,6 +3,11 @@ Dates are in YYYY-MM-DD format. +## v0.49.14 (2024-09-10) +### Added +- Added `DataType.FLOAT4` for 4-bit floats (E2M1). + + ## v0.49.13 (2024-07-15) ### Added - Added option to emit logs using python `logging` module. diff --git a/tools/Polygraphy/polygraphy/__init__.py b/tools/Polygraphy/polygraphy/__init__.py index 5d394952..9fddaeac 100644 --- a/tools/Polygraphy/polygraphy/__init__.py +++ b/tools/Polygraphy/polygraphy/__init__.py @@ -1,3 +1,3 @@ import polygraphy.config -__version__ = "0.49.13" +__version__ = "0.49.14" diff --git a/tools/Polygraphy/polygraphy/datatype/datatype.py b/tools/Polygraphy/polygraphy/datatype/datatype.py index 22a1bb9a..918e177f 100644 --- a/tools/Polygraphy/polygraphy/datatype/datatype.py +++ b/tools/Polygraphy/polygraphy/datatype/datatype.py @@ -86,6 +86,7 @@ class DataType: "FLOAT64": DataTypeEntry("float64", 8, _DataTypeKind.FLOATING_POINT), "FLOAT32": DataTypeEntry("float32", 4, _DataTypeKind.FLOATING_POINT), "FLOAT16": DataTypeEntry("float16", 2, _DataTypeKind.FLOATING_POINT), + "FLOAT4": DataTypeEntry("float4", 0.5, _DataTypeKind.FLOATING_POINT), "INT16": DataTypeEntry("int16", 2, _DataTypeKind.INTEGRAL), "INT32": DataTypeEntry("int32", 4, _DataTypeKind.INTEGRAL), "INT64": DataTypeEntry("int64", 8, _DataTypeKind.INTEGRAL), diff --git a/tools/Polygraphy/polygraphy/datatype/tensorrt.py b/tools/Polygraphy/polygraphy/datatype/tensorrt.py index f59f8086..47dae6a7 100644 --- a/tools/Polygraphy/polygraphy/datatype/tensorrt.py +++ b/tools/Polygraphy/polygraphy/datatype/tensorrt.py @@ -37,6 +37,7 @@ def _get_mapping(): util.try_getattr(trt, "bfloat16"): DataType.BFLOAT16, util.try_getattr(trt, "fp8"): DataType.FLOAT8E4M3FN, util.try_getattr(trt, "int4"): DataType.INT4, + util.try_getattr(trt, "fp4"): DataType.FLOAT4, } if None in DATATYPE_FROM_TENSORRT: del DATATYPE_FROM_TENSORRT[None] diff --git a/tools/Polygraphy/tests/common/test_datatype.py b/tools/Polygraphy/tests/common/test_datatype.py index c1515e18..36df7d77 100644 --- a/tools/Polygraphy/tests/common/test_datatype.py +++ b/tools/Polygraphy/tests/common/test_datatype.py @@ -45,6 +45,7 @@ def test_numpy(self, dtype): DataType.FLOAT8E5M2, DataType.FLOAT8E5M2FNUZ, DataType.INT4, + DataType.FLOAT4, ]: pytest.xfail("Type not supported by NumPy") @@ -61,6 +62,7 @@ def test_numpy(self, dtype): def test_onnxrt(self, dtype): if dtype in [ DataType.INT4, + DataType.FLOAT4, ]: pytest.skip("Type not supported by ONNX-RT") @@ -84,6 +86,7 @@ def test_onnxrt(self, dtype): def test_onnx(self, dtype): if dtype in [ DataType.INT4, + DataType.FLOAT4, ]: pytest.skip("Type not supported by ONNX") @@ -137,6 +140,7 @@ def test_tensorrt(self, dtype): "float": "float32", "half": "float16", "fp8": "float8e4m3fn", + "fp4": "float4", "bf16": "bfloat16", }, ) @@ -162,6 +166,7 @@ def test_torch(self, dtype): DataType.UINT64, DataType.STRING, DataType.INT4, + DataType.FLOAT4, ]: pytest.xfail("Type not supported by Torch") diff --git a/tools/onnx-graphsurgeon/CHANGELOG.md b/tools/onnx-graphsurgeon/CHANGELOG.md index 7eaed393..ee3000b5 100644 --- a/tools/onnx-graphsurgeon/CHANGELOG.md +++ b/tools/onnx-graphsurgeon/CHANGELOG.md @@ -2,7 +2,7 @@ Dates are in YYYY-MM-DD format. -## v0.5.3 (TBD) +## v0.5.3 (2024-10-14) ### Added - Added `export_dtype` field to `gs.Constant` to allow numpy-unsupported dtypes such as BFloat16. diff --git a/tools/onnx-graphsurgeon/onnx_graphsurgeon/__init__.py b/tools/onnx-graphsurgeon/onnx_graphsurgeon/__init__.py index 6756daa1..32367d7e 100644 --- a/tools/onnx-graphsurgeon/onnx_graphsurgeon/__init__.py +++ b/tools/onnx-graphsurgeon/onnx_graphsurgeon/__init__.py @@ -7,4 +7,4 @@ from onnx_graphsurgeon.ir.tensor import Constant, Tensor, Variable from onnx_graphsurgeon.util.exception import OnnxGraphSurgeonException -__version__ = "0.5.2" +__version__ = "0.5.3" diff --git a/tools/onnx-graphsurgeon/tests/test_examples.py b/tools/onnx-graphsurgeon/tests/test_examples.py index fd86fbb0..345b9f38 100644 --- a/tools/onnx-graphsurgeon/tests/test_examples.py +++ b/tools/onnx-graphsurgeon/tests/test_examples.py @@ -51,9 +51,11 @@ def __init__(self, name, infer=True): ("09_shape_operations_with_the_layer_api", [Artifact("model.onnx")]), ("10_dynamic_batch_size", [Artifact("model.onnx"), Artifact("dynamic.onnx")]), ("11_creating_a_local_function", [Artifact("model.onnx")]), - # Skipping inference test as bf16 is not supported in ORT yet. - ("12_using_bf16", [Artifact("test_conv_bf16.onnx", infer=False)]), + ( + "12_using_numpy_unsupported_dtypes", + [Artifact("test_conv_bf16.onnx", infer=False)], + ), ]