chore: import upstream snapshot with attribution
Docker Image CI / build-ubuntu2004 (push) Has been cancelled
Docker Image CI / build-ubuntu2004 (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
*.onnx
|
||||
*.engine
|
||||
*.profile
|
||||
|
||||
**/__pycache__/
|
||||
@@ -0,0 +1,194 @@
|
||||
# DeBERTa Model Inference with TensorRT Disentangled Attention Optimizations
|
||||
|
||||
- [DeBERTa Model Inference with TensorRT Disentangled Attention Optimizations](#deberta-model-inference-with-tensorrt-disentangled-attention-optimizations)
|
||||
- [Background](#background)
|
||||
- [Performance Benchmark](#performance-benchmark)
|
||||
- [Environment Setup](#environment-setup)
|
||||
- [Step 1: PyTorch model to ONNX model](#step-1-pytorch-model-to-onnx-model)
|
||||
- [Step 2: Modify ONNX model for TensorRT engine building](#step-2-modify-onnx-model-for-tensorrt-engine-building)
|
||||
- [Step 3: Model inference with TensorRT (using Python TensorRT API or `trtexec`)](#step-3-model-inference-with-tensorrt-using-python-tensorrt-api-or-trtexec)
|
||||
- [Optional Step: Correctness check of model with and without plugin](#optional-step-correctness-check-of-model-with-and-without-plugin)
|
||||
- [Optional Step: Model inference with ONNX Runtime and TensorRT Execution Provider (Python API)](#optional-step-model-inference-with-onnx-runtime-and-tensorrt-execution-provider-python-api)
|
||||
|
||||
***
|
||||
|
||||
## Background
|
||||
A performance gap has been observed between Google's [BERT](https://arxiv.org/abs/1810.04805) design and Microsoft's [DeBERTa](https://arxiv.org/abs/2006.03654) design. The main reason of the gap is the disentangled attention design in DeBERTa triples the attention computation over BERT's regular attention. In addition to the extra matrix multiplications, the disentangled attention design also involves indirect memory accesses during the gather operations. In this regard, a [TensorRT plugin](https://github.com/NVIDIA/TensorRT/tree/main/plugin/disentangledAttentionPlugin) has been implemented to optimize DeBERTa's disentangled attention module, which is built-in since TensorRT 8.4 GA Update 2 (8.4.3) release.
|
||||
|
||||
This DeBERTa demo includes code and scripts for (i) exporting ONNX model from PyTorch, (ii) modifying ONNX model by inserting the plugin nodes, (iii) model inference options with TensorRT `trtexec` executable, TensorRT Python API, or ONNX Runtime with TensorRT execution provider, and (iv) measuring the correctness and performance of the optimized model.
|
||||
|
||||
The demo works with the [HuggingFace implementation](https://github.com/huggingface/transformers/tree/main/src/transformers/models/deberta_v2) of DeBERTa.
|
||||
|
||||
## Performance Benchmark
|
||||
Experiments of inference performance are measured on NVIDIA A100-80GB, T4, and V100-16GB, using TensorRT 8.4 GA Update 2 (8.4.3) + CUDA 11.6 and TensorRT 8.2 GA Update 3 (8.2.4) + CUDA 11.4. The application-based model configuration is: sequence length = 512/1024/2048, hidden size = 384, intermediate size = 1536, number of layers = 12, number of attention heads = 6, maximum relative distance = 256, vocabulary size = 128K, batch size = 1, with randomly initialized weights. Numbers are average latency of 100 inference runs. Speedup numbers are fastest TensorRT number over the PyTorch baseline.
|
||||
|
||||
| Sequence Length | Model Latency (ms) | | TensorRT 8.4 GA Update 2 (8.4.3), CUDA 11.6 | | | TensorRT 8.2 GA Update 3 (8.2.4), CUDA 11.4 | |
|
||||
| :-------------- | :----------------------------- | :-------: | :-----------------------------------------: | :-------: | :-------: | :-----------------------------------------: | :-------: |
|
||||
| | | A100-80GB | T4 | V100-16GB | A100-80GB | T4 | V100-16GB |
|
||||
| 512 | PyTorch (FP32/TF32) | 23.7 | 35.6 | 53.1 | 24.6 | 34.0 | 53.1 |
|
||||
| | PyTorch (FP16) | 21.5 | 22.7 | 43.5 | 22.6 | 20.6 | 43.5 |
|
||||
| | TensorRT (FP32/TF32) | 3.9 | 12.4 | 7.2 | 4.3 | 12.3 | 5.6 |
|
||||
| | TensorRT (FP16) | **1.6** | 6.2 | **3.9** | **1.8** | 6.1 | **3.1** |
|
||||
| | TensorRT w/ plugin (FP32/TF32) | 4.3 | 12.9 | 6.9 | 4.8 | 13.1 | 6.9 |
|
||||
| | TensorRT w/ plugin (FP16) | 1.9 | **5.6** | 4.0 | 2.1 | **6.0** | 4.2 |
|
||||
| | **Speedup** | **14.8** | **6.4** | **13.6** | **13.7** | **5.7** | **17.1** |
|
||||
| | | | | | | | |
|
||||
| 1024 | PyTorch (FP32/TF32) | 35.7 | 82.8 | 65.4 | 35.8 | 83.7 | 65.4 |
|
||||
| | PyTorch (FP16) | 35.1 | 53.8 | 59.4 | 35.6 | 52.9 | 59.4 |
|
||||
| | TensorRT (FP32/TF32) | 8.3 | 31.3 | 15.7 | 8.8 | 34.7 | 12.8 |
|
||||
| | TensorRT (FP16) | 3.8 | 16.3 | 8.3 | 3.7 | 18.4 | 7.4 |
|
||||
| | TensorRT w/ plugin (FP32/TF32) | 7.9 | 31.1 | 14.3 | 9.0 | 32.8 | 13.5 |
|
||||
| | TensorRT w/ plugin (FP16) | **2.8** | **12.4** | **7.3** | **3.3** | **13.6** | **6.8** |
|
||||
| | **Speedup** | **12.8** | **6.7** | **9.0** | **10.8** | **6.2** | **9.6** |
|
||||
| | | | | | | | |
|
||||
| 2048 | PyTorch (FP32/TF32) | 84.8 | 261.3 | 236.3 | 84.8 | 263.1 | 236.3 |
|
||||
| | PyTorch (FP16) | 79.4 | 178.2 | 205.5 | 76.0 | 181.8 | 205.5 |
|
||||
| | TensorRT (FP32/TF32) | 22.2 | 109.1 | 39.1 | 22.6 | 109.8 | 35.0 |
|
||||
| | TensorRT (FP16) | 10.2 | 56.2 | 23.5 | 10.8 | 62.9 | 21.0 |
|
||||
| | TensorRT w/ plugin (FP32/TF32) | 20.4 | 96.7 | 38.6 | 21.2 | 95.7 | 35.6 |
|
||||
| | TensorRT w/ plugin (FP16) | **7.6** | **44.1** | **21.0** | **8.3** | **44.6** | **18.1** |
|
||||
| | **Speedup** | **11.2** | **5.9** | **11.3** | **10.2** | **5.9** | **13.1** |
|
||||
|
||||
In addition, a pre-trained model `microsoft/deberta-v3-xsmall` was tested, which configuration is similar to sequencen length = 512 model above. And `microsoft/deberta-v3-large` (sequence length = 512) performance on TensorRT 8.4 GA Update 2 (8.4.3) is also added from recent experiments.
|
||||
|
||||
| Variant | Model Latency (ms) | | TensorRT 8.4 GA Update 2 (8.4.3), CUDA 11.6 | | | TensorRT 8.2 GA Update 3 (8.2.4), CUDA 11.4 | |
|
||||
| :------------------ | :----------------------------- | :-------: | :-----------------------------------------: | :-------: | :-------: | :-----------------------------------------: | :-------: |
|
||||
| | | A100-80GB | T4 | V100-16GB | A100-80GB | T4 | V100-16GB |
|
||||
| `deberta-v3-xsmall` | PyTorch (FP32/TF32) | 30.1 | 40.6 | 57.1 | 28.9 | 39.2 | 57.1 |
|
||||
| | PyTorch (FP16) | 27.6 | 26.3 | 47.9 | 26.3 | 24.8 | 47.9 |
|
||||
| | TensorRT (FP32/TF32) | 4.1 | 12.4 | 7.6 | 4.4 | 12.8 | 5.8 |
|
||||
| | TensorRT (FP16) | 1.8 | 6.2 | **3.7** | **1.9** | 6.4 | **3.1** |
|
||||
| | TensorRT w/ plugin (FP32/TF32) | 4.3 | 12.9 | 7.7 | 4.8 | 13.6 | 6.9 |
|
||||
| | TensorRT w/ plugin (FP16) | **1.8** | **5.6** | 4.7 | 2.1 | **6.0** | 4.1 |
|
||||
| | **Speedup** | **16.7** | **7.3** | **15.4** | **15.2** | **6.5** | **18.4** |
|
||||
| | | | | | | | |
|
||||
| `deberta-v3-large` | PyTorch (FP32/TF32) | 51.6 | 40.6 | 100.0 | - | - | - |
|
||||
| | PyTorch (FP16) | 52.6 | 26.3 | 96.5 | - | - | - |
|
||||
| | TensorRT (FP32/TF32) | 31.1 | 32.3 | 43.2 | - | - | - |
|
||||
| | TensorRT (FP16) | 7.8 | 16.0 | **13.9** | - | - | - |
|
||||
| | TensorRT w/ plugin (FP32/TF32) | 30.9 | 32.8 | 44.9 | - | - | - |
|
||||
| | TensorRT w/ plugin (FP16) | **7.3** | **13.5** | 14.4 | - | - | - |
|
||||
| | **Speedup** | **7.1** | **3.0** | **7.2** | - | - | - |
|
||||
|
||||
Note that the performance gap between BERT's self-attention and DeBERTa's disentangled attention mainly comes from the additional `Gather` and `Transpose` operations in the attention design, and such gap becomes most significant when the maximum input sequence length is long (e.g., 2048). The fastest inference times are labeled as bold in the table above. In summary, for short sequence length applications, regular TensorRT inference might be sufficient, while for longer sequence length applications, the plugin optimizations are preferred and can be utilized to further improve the inference latency. Also, to get maximum speedup, using FP16 precision for inference is recommended.
|
||||
|
||||
## Environment Setup
|
||||
It is recommended to use docker for reproducing the following steps. Follow the setup steps in TensorRT OSS [README](https://github.com/NVIDIA/TensorRT#setting-up-the-build-environment) to build and launch the container and build OSS:
|
||||
|
||||
**Example: Ubuntu 20.04 on x86-64 with cuda-12.8 (default)**
|
||||
```bash
|
||||
# Download this TensorRT OSS repo
|
||||
git clone -b main https://github.com/nvidia/TensorRT TensorRT
|
||||
cd TensorRT
|
||||
git submodule update --init --recursive
|
||||
|
||||
## at root of TensorRT OSS
|
||||
# build container
|
||||
./docker/build.sh --file docker/ubuntu-20.04.Dockerfile --tag tensorrt-ubuntu20.04-cuda12.8
|
||||
|
||||
# launch container
|
||||
./docker/launch.sh --tag tensorrt-ubuntu20.04-cuda12.8 --gpus all
|
||||
|
||||
## now inside container
|
||||
# build OSS (only required for pre-8.4.3 TensorRT versions)
|
||||
cd $TRT_OSSPATH
|
||||
mkdir -p build && cd build
|
||||
cmake .. -DTRT_LIB_DIR=$TRT_LIBPATH -DTRT_OUT_DIR=`pwd`/out
|
||||
make -j$(nproc)
|
||||
|
||||
# polygraphy bin location & trtexec bin location
|
||||
export PATH="~/.local/bin":"${TRT_OSSPATH}/build/out":$PATH
|
||||
|
||||
# navigate to the demo folder install additional dependencies (note PyTorch 1.11.0 is recommended for onnx to export properly)
|
||||
cd $TRT_OSSPATH/demo/DeBERTa
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
> NOTE:
|
||||
1. `sudo` password for Ubuntu build containers is 'nvidia'.
|
||||
2. The DeBERTa plugin is only built-in after TensorRT 8.4 GA Update 2 (8.4.3) release. For pre-8.4.3 versions, you need to build TensorRT OSS from source and link the shared libraries with TensorRT build.
|
||||
3. For ONNX Runtime deployment, the associated changes for the plugin are only built-in after 1.12 release. For pre-1.12 versions, you need to [build ONNX Runtime from source with TensorRT execution provider](https://onnxruntime.ai/docs/build/eps.html#tensorrt).
|
||||
4. TensorRT OSS docker container is designed for use cases when you need to build OSS repository from source. After TensorRT 8.4 GA Update 2 (8.4.3) release, the most convenient way is to use the corresponding `22.08-py3` docker image at [TensorRT NGC container](https://catalog.ngc.nvidia.com/orgs/nvidia/containers/tensorrt) when it is released, without the need to build from dockerfile. But for now, please follow the instructions above to use the OSS docker image.
|
||||
|
||||
## Step 1: PyTorch model to ONNX model
|
||||
```bash
|
||||
python deberta_pytorch2onnx.py --filename ./test/deberta.onnx [--variant microsoft/deberta-v3-xsmall] [--seq-len 2048]
|
||||
```
|
||||
|
||||
This will export the DeBERTa model from HuggingFace's DeBERTa-V2 implementation into ONNX format, with user given file name. Optionally, specify DeBERTa variant to export, such as `--variant microsoft/deberta-v3-xsmall`, or specify the maximum sequence length configuration for testing, such as `--seq-len 2048`. Models specified by `--variant` are with pre-trained weights, while models specified by `--seq-len` are with randomly initialized weights. Note that `--variant` and `--seq-len` cannot be used together because pre-trained models have pre-defined max sequence length.
|
||||
|
||||
## Step 2: Modify ONNX model for TensorRT engine building
|
||||
```bash
|
||||
python deberta_onnx_modify.py ./test/deberta.onnx # generates original TRT-compatible model, `*_original.onnx`
|
||||
|
||||
python deberta_onnx_modify.py ./test/deberta.onnx --plugin # generates TRT-compatible model with plugin nodes, `*_plugin.onnx`
|
||||
```
|
||||
|
||||
The original HuggingFace implementation has uint8 Cast operations that TensorRT doesn't support, which needs to be removed from the ONNX model. After this step, the ONNX model can run in TensorRT. Without passing any flags, the script will save the model with Cast nodes removed, by default named with `_original` suffix.
|
||||
|
||||
Further, to use the DeBERTa plugin optimizations, the disentangled attention subgraph needs to be replaced by node named `DisentangledAttention_TRT`. By passing `--plugin` flag, the script will save the model with Cast nodes removed and plugin nodes replaced, by default named with `_plugin` suffix.
|
||||
|
||||
The benefits of the DeBERTa plugin optimizations can be demonstrated by comparing the latency of original model and plugin model.
|
||||
|
||||
## Step 3: Model inference with TensorRT (using Python TensorRT API or `trtexec`)
|
||||
```bash
|
||||
# build and test the original DeBERTa model (baseline)
|
||||
python deberta_tensorrt_inference.py --onnx=./test/deberta_original.onnx --build fp16 --test fp16
|
||||
|
||||
# build and test the optimized DeBERTa model with plugin
|
||||
python deberta_tensorrt_inference.py --onnx=./test/deberta_plugin.onnx --build fp16 --test fp16
|
||||
```
|
||||
|
||||
This will build and test the original and optimized DeBERTa models. `--build` to build the engine from ONNX model, and `--test` to measure the optimized latency. TensorRT engine of different precisions (`--fp32/--tf32/--fp16`) can be built. Engine files are saved as `**/[Model name]_[GPU name]_[Precision].engine`.
|
||||
|
||||
> NOTE:
|
||||
1. To get maximum speedup, using `--fp16` is recommended. Also, it was observed the plugin optimizations demonstrate more speedup when the input sequence length is long, such as 2048.
|
||||
2. TF32 is only effective on Ampere and later devices.
|
||||
3. TensorRT optimization profile in `deberta_tensorrt_inference.py` is set by default for batch size of 1 usage. For batch usage, the optional and maximum profile should be changed.
|
||||
4. TensorRT engines are specific to the exact GPU device they were built on, as well as the platform and the TensorRT version. On the same machine, building is needed only once and the engine can be used for repeated testing.
|
||||
|
||||
For `trtexec` test, it is recommended to used the [TensorRT NGC container](https://catalog.ngc.nvidia.com/orgs/nvidia/containers/tensorrt) where the executable is built-in. The DeBERTa plugin support is available since docker image `22.08-py3`, which contains TensorRT 8.4 GA Update 2 (8.4.3) pre-installed. For earlier images before `22.08`, skip this `trtexec` test.
|
||||
|
||||
`trtexec` command for sequence length = 2048 model is given as an example:
|
||||
```bash
|
||||
trtexec --onnx=./test/deberta_plugin.onnx --workspace=4096 --explicitBatch --optShapes=input_ids:1x2048,attention_mask:1x2048 --iterations=10 --warmUp=10 --noDataTransfers --fp16
|
||||
```
|
||||
|
||||
## Optional Step: Correctness check of model with and without plugin
|
||||
```bash
|
||||
# prepare the ONNX models with intermediate output nodes (this will save two new onnx models with suffix `*_correctness_check_original.onnx` and `*_correctness_check_plugin.onnx`)
|
||||
python deberta_onnx_modify.py ./test/deberta.onnx --correctness-check
|
||||
|
||||
# build the ONNX models with intermediate outputs for comparison
|
||||
python deberta_tensorrt_inference.py --onnx=./test/deberta_correctness_check_original.onnx --build fp16
|
||||
python deberta_tensorrt_inference.py --onnx=./test/deberta_correctness_check_plugin.onnx --build fp16
|
||||
|
||||
# run correctness check (specify the root model name with --onnx)
|
||||
python deberta_tensorrt_inference.py --onnx=./test/deberta --correctness-check fp16
|
||||
```
|
||||
|
||||
Correctness check requires intermediate outputs from the model, thus it is necessary to modify the ONNX graph and add intermediate output nodes. The correctness check was added at the location of plugin outputs in each layer. The metrics are average and maximum of the element-wise absolute error. Note that for FP16 precision with 10 significance bits, absolute error in the order of 1e-2 and 1e-3 is expected, and for FP32 precision with 23 significance bits, 1e-6 to 1e-7 is expected. Refer to [Machine Epsilon](https://en.wikipedia.org/wiki/Machine_epsilon) for details.
|
||||
|
||||
Example output for FP16 correctness check between original DeBERTa model and plugin optimized DeBERTa model:
|
||||
```bash
|
||||
[Layer 0 Element-wise Check] Avgerage absolute error: 2.152580e-03, Maximum absolute error: 3.010559e-02. 1e-2~1e-3 expected for FP16 (10 significance bits) and 1e-6~1e-7 expected for FP32 (23 significance bits)
|
||||
...
|
||||
...
|
||||
[Layer 12 Element-wise Check] Avgerage absolute error: 6.198883e-05, Maximum absolute error: 1.220703e-04. 1e-2~1e-3 expected for FP16 (10 significance bits) and 1e-6~1e-7 expected for FP32 (23 significance bits)
|
||||
```
|
||||
|
||||
## Optional Step: Model inference with ONNX Runtime and TensorRT Execution Provider (Python API)
|
||||
```bash
|
||||
python deberta_ort_inference.py --onnx=./test/deberta_original.onnx --test fp16
|
||||
|
||||
python deberta_ort_inference.py --onnx=./test/deberta_plugin.onnx --test fp16
|
||||
|
||||
python deberta_ort_inference.py --onnx=./test/deberta --correctness-check fp16 # for correctness check
|
||||
```
|
||||
|
||||
In addition to TensorRT inference, [ONNX Runtime](https://github.com/microsoft/onnxruntime) with TensorRT Execution Provider can also be used as the inference framework. The DeBERTa TensorRT plugin is officially supported in onnxruntime since version 1.12. For earlier releases of onnxruntime, skip this step.
|
||||
|
||||
The results can be cross-validated between TensorRT and onnxruntime. For example, the [Polygraphy](https://github.com/NVIDIA/TensorRT/tree/master/tools/Polygraphy) tool can be used for easy comparison:
|
||||
|
||||
```bash
|
||||
polygraphy run ./test/deberta_original.onnx --trt --onnxrt --workspace=4000000000
|
||||
```
|
||||
@@ -0,0 +1,128 @@
|
||||
#!/usr/bin/env python3
|
||||
#
|
||||
# SPDX-FileCopyrightText: Copyright (c) 1993-2025 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 numpy as np
|
||||
import logging
|
||||
from cuda.bindings import driver as cuda, runtime as cudart
|
||||
|
||||
class CudaStreamContext:
|
||||
"""CUDA stream lifecycle management with context manager support"""
|
||||
def __init__(self):
|
||||
"""Initialize CUDA stream"""
|
||||
self._stream = cuda_call(cudart.cudaStreamCreate())
|
||||
|
||||
def __enter__(self):
|
||||
"""Create CUDA stream when entering context (if not already created)"""
|
||||
if self._stream is None:
|
||||
self._stream = cuda_call(cudart.cudaStreamCreate())
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
"""Destroy CUDA stream when exiting context"""
|
||||
self.free()
|
||||
|
||||
@property
|
||||
def stream(self) -> cudart.cudaStream_t:
|
||||
if self._stream is None:
|
||||
raise RuntimeError("Stream not created. Use 'with' statement.")
|
||||
return self._stream
|
||||
|
||||
def synchronize(self):
|
||||
"""Synchronize the stream"""
|
||||
if self._stream is None:
|
||||
raise RuntimeError("Stream not created. Use 'with' statement.")
|
||||
cuda_call(cudart.cudaStreamSynchronize(self._stream))
|
||||
|
||||
def free(self):
|
||||
"""Explicitly free the CUDA stream"""
|
||||
if self._stream is not None:
|
||||
try:
|
||||
cuda_call(cudart.cudaStreamDestroy(self._stream))
|
||||
self._stream = None
|
||||
except Exception as e:
|
||||
logging.warning(f"Failed to destroy CUDA stream: {e}")
|
||||
|
||||
def __del__(self):
|
||||
"""Cleanup stream on destruction"""
|
||||
if hasattr(self, '_stream') and self._stream is not None:
|
||||
self.free()
|
||||
|
||||
def __str__(self):
|
||||
return f"CudaStreamContext: {self._stream}"
|
||||
|
||||
def __repr__(self):
|
||||
return self.__str__()
|
||||
|
||||
def cuda_call(call):
|
||||
"""Helper function to make CUDA calls and check for errors"""
|
||||
def _cudaGetErrorEnum(error):
|
||||
if isinstance(error, cuda.CUresult):
|
||||
err, name = cuda.cuGetErrorName(error)
|
||||
return name if err == cuda.CUresult.CUDA_SUCCESS else "<unknown>"
|
||||
elif isinstance(error, cudart.cudaError_t):
|
||||
return cudart.cudaGetErrorName(error)[1]
|
||||
else:
|
||||
raise RuntimeError("Unknown error type: {}".format(error))
|
||||
|
||||
err, res = call[0], call[1:]
|
||||
if err.value:
|
||||
raise RuntimeError(
|
||||
"CUDA error code={}({})".format(
|
||||
err.value, _cudaGetErrorEnum(err)
|
||||
)
|
||||
)
|
||||
if len(res) == 1:
|
||||
return res[0]
|
||||
elif len(res) == 0:
|
||||
return None
|
||||
else:
|
||||
return res
|
||||
|
||||
def getComputeCapacity(devID=0):
|
||||
major = cuda_call(cudart.cudaDeviceGetAttribute(cudart.cudaDeviceAttr.cudaDevAttrComputeCapabilityMajor, devID))
|
||||
minor = cuda_call(cudart.cudaDeviceGetAttribute(cudart.cudaDeviceAttr.cudaDevAttrComputeCapabilityMinor, devID))
|
||||
return (major, minor)
|
||||
|
||||
def memcpy_host_to_device_async(device_ptr: int, host_arr: np.ndarray, stream):
|
||||
"""Wrapper for async host-to-device memory copy"""
|
||||
cuda_call(cudart.cudaMemcpyAsync(device_ptr, host_arr.ctypes.data, host_arr.nbytes, cudart.cudaMemcpyKind.cudaMemcpyHostToDevice, stream))
|
||||
|
||||
|
||||
def memcpy_device_to_host_async(host_arr: np.ndarray, device_ptr: int, stream):
|
||||
"""Wrapper for async device-to-host memory copy"""
|
||||
cuda_call(cudart.cudaMemcpyAsync(host_arr.ctypes.data, device_ptr, host_arr.nbytes, cudart.cudaMemcpyKind.cudaMemcpyDeviceToHost, stream))
|
||||
|
||||
def memcpy_device_to_device_async(dst_device_ptr: int, src_device_ptr: int, nbytes: int, stream):
|
||||
"""Wrapper for async device-to-device memory copy"""
|
||||
cuda_call(cudart.cudaMemcpyAsync(dst_device_ptr, src_device_ptr, nbytes, cudart.cudaMemcpyKind.cudaMemcpyDeviceToDevice, stream))
|
||||
|
||||
def memcpy_host_to_device(device_ptr: int, host_arr: np.ndarray):
|
||||
"""Wrapper for synchronous host-to-device memory copy"""
|
||||
cuda_call(cudart.cudaMemcpy(device_ptr, host_arr.ctypes.data, host_arr.nbytes, cudart.cudaMemcpyKind.cudaMemcpyHostToDevice))
|
||||
|
||||
|
||||
def memcpy_device_to_host(host_arr: np.ndarray, device_ptr: int):
|
||||
"""Wrapper for synchronous device-to-host memory copy"""
|
||||
cuda_call(cudart.cudaMemcpy(host_arr.ctypes.data, device_ptr, host_arr.nbytes, cudart.cudaMemcpyKind.cudaMemcpyDeviceToHost))
|
||||
|
||||
def memcpy_device_to_device(dst_device_ptr: int, src_device_ptr: int, nbytes: int):
|
||||
"""Wrapper for synchronous device-to-device memory copy"""
|
||||
cuda_call(cudart.cudaMemcpy(dst_device_ptr, src_device_ptr, nbytes, cudart.cudaMemcpyKind.cudaMemcpyDeviceToDevice))
|
||||
|
||||
# Initialize CUDA
|
||||
cuda_call(cudart.cudaFree(0))
|
||||
@@ -0,0 +1,227 @@
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
|
||||
'''
|
||||
Modify original ONNX exported from HuggingFace to for TensorRT engine building.
|
||||
|
||||
The original HuggingFace implementation has uint8 Cast operations that TensorRT doesn't support, which needs to be removed from the ONNX model. After this step, the ONNX model can run in TensorRT.
|
||||
|
||||
Further, to use the DeBERTa plugin optimizations, the disentangled attention module needs to be replaced by node named `DisentangledAttention_TRT`.
|
||||
|
||||
Optional: generate model that has per-layer intermediate outputs for correctness check purpose.
|
||||
|
||||
These modifications are automated in this script.
|
||||
|
||||
Usage:
|
||||
python deberta_onnx_modify.py xx.onnx # for original TRT-compatible model, `xx_original.onnx`
|
||||
python deberta_onnx_modify.py xx.onnx --plugin # for TRT-compatible model with plugin nodes, `xx_plugin.onnx`
|
||||
python deberta_onnx_modify.py xx.onnx --correctness-check # for correctness check
|
||||
'''
|
||||
|
||||
import onnx
|
||||
from onnx import TensorProto
|
||||
import onnx_graphsurgeon as gs
|
||||
import argparse, os
|
||||
import numpy as np
|
||||
|
||||
parser = argparse.ArgumentParser(description="Modify DeBERTa ONNX model to prepare for TensorRT engine building. If none of --plugin or --correctness-check flag is passed, it will just save the uint8 cast removed model.")
|
||||
parser.add_argument('input', type=str, help='Path to the input ONNX model')
|
||||
parser.add_argument('--plugin', action='store_true', help="Generate model with plugin")
|
||||
parser.add_argument('--correctness-check', action='store_true', help="Generate model that has per-layer intermediate outputs for correctness check purpose")
|
||||
parser.add_argument('--output', type=str, help="Path to the output ONNX model. If not set, default to the input file name with a suffix of '_original' or '_plugin' ")
|
||||
args = parser.parse_args()
|
||||
|
||||
model_input = args.input
|
||||
use_plugin = args.plugin
|
||||
correctness_check = args.correctness_check
|
||||
|
||||
if args.output is None:
|
||||
model_output = os.path.splitext(model_input)[0] + ("_plugin" if use_plugin else "_original") + os.path.splitext(model_input)[-1]
|
||||
else:
|
||||
model_output = args.output
|
||||
|
||||
def remove_uint8_cast(graph):
|
||||
'''
|
||||
Remove all uint8 Cast nodes since TRT doesn't support UINT8 cast op.
|
||||
Ref: https://github.com/NVIDIA/TensorRT/tree/main/tools/onnx-graphsurgeon/examples/06_removing_nodes
|
||||
'''
|
||||
nodes = [node for node in graph.nodes if node.op == 'Cast' and node.attrs["to"] == TensorProto.UINT8] # find by op name and attribute
|
||||
|
||||
for node in nodes:
|
||||
# [ONNX's Cast operator](https://github.com/onnx/onnx/blob/main/docs/Operators.md#Cast) will exactly have 1 input and 1 output
|
||||
# reconnect tensors
|
||||
input_node = node.i()
|
||||
input_node.outputs = node.outputs
|
||||
node.outputs.clear()
|
||||
|
||||
# an alternative way is to just not cast to uint8
|
||||
# node.attrs["to"] = TensorProto.INT64
|
||||
|
||||
return graph
|
||||
|
||||
@gs.Graph.register()
|
||||
def insert_disentangled_attention(self, inputs, outputs, factor, span):
|
||||
'''
|
||||
Fuse disentangled attention module (Add + Gather + Gather + Transpose + Add + Div)
|
||||
|
||||
inputs: list of plugin inputs
|
||||
outputs: list of plugin outputs
|
||||
factor: scaling factor of disentangled attention, sqrt(3d), converted from a division factor to a multiplying factor
|
||||
span: relative distance span, k
|
||||
'''
|
||||
# disconnect previous output from flow (the previous subgraph still exists but is effectively dead since it has no link to an output tensor, and thus will be cleaned up)
|
||||
[out.inputs.clear() for out in outputs]
|
||||
# add plugin layer
|
||||
attrs = {
|
||||
"factor": 1/factor,
|
||||
"span": span
|
||||
}
|
||||
self.layer(op='DisentangledAttention_TRT', inputs=inputs, outputs=outputs, attrs=attrs)
|
||||
|
||||
def insert_disentangled_attention_all(graph):
|
||||
'''
|
||||
Insert disentangled attention plugin nodes for all layers
|
||||
'''
|
||||
nodes = [node for node in graph.nodes if node.op == 'GatherElements'] # find entry points by gatherelements op
|
||||
assert len(nodes) % 2 == 0, "No. of GatherElements nodes is not an even number!"
|
||||
|
||||
layers = [(nodes[2*i+0], nodes[2*i+1]) for i in range(len(nodes)//2)] # 2 gatherelements in 1 layer
|
||||
for l, (left,right) in enumerate(layers):
|
||||
print(f"Fusing layer {l}")
|
||||
|
||||
# CAVEAT! MUST cast to list() when setting the inputs & outputs. graphsurgeon's default for X.inputs and X.outputs is `onnx_graphsurgeon.util.misc.SynchronizedList`, i.e. 2-way node-tensor updating mechanism. If not cast, when we remove the input nodes of a tensor, the tensor itself will be removed as well...
|
||||
|
||||
# inputs: (data0, data1, data2), input tensors for c2c add and 2 gathers
|
||||
inputs = list(left.o().o().o().o().i().inputs)[0:1] + list(left.inputs)[0:1] + list(right.inputs)[0:1]
|
||||
|
||||
# outputs: (result), output tensors after adding 3 gather results
|
||||
outputs = list(left.o().o().o().o().outputs)
|
||||
|
||||
# constants: scaling factor, relative distance span
|
||||
factor = left.o().inputs[1].inputs[0].attrs["value"].values.item()
|
||||
span = right.i(1,0).i().i().i().inputs[1].inputs[0].attrs["value"].values.item()
|
||||
|
||||
# insert plugin layer
|
||||
graph.insert_disentangled_attention(inputs, outputs, factor, span)
|
||||
|
||||
return graph
|
||||
|
||||
def correctness_check_models(graph):
|
||||
'''
|
||||
Add output nodes at the plugin exit point for both the original model and the model with plugin
|
||||
'''
|
||||
|
||||
seq_len = graph.inputs[0].shape[1]
|
||||
|
||||
## for original graph
|
||||
# make a copy of the graph first
|
||||
graph_raw = graph.copy()
|
||||
nodes = [node for node in graph_raw.nodes if node.op == 'GatherElements'] # find by gatherelements op
|
||||
assert len(nodes) % 2 == 0, "No. of GatherElements nodes is not an even number!"
|
||||
|
||||
layers = [(nodes[2*i+0], nodes[2*i+1]) for i in range(len(nodes)//2)] # 2 gatherelements in 1 layer
|
||||
original_output_all = []
|
||||
for l, (left,right) in enumerate(layers):
|
||||
# outputs: (result), output tensors after adding 3 gather results
|
||||
# add the output tensor to the graph outputs list. Don't create any new tensor!
|
||||
end_node = left.o().o().o().o()
|
||||
end_node.outputs[0].dtype = graph_raw.outputs[0].dtype # need to explicitly specify dtype and shape of graph output tensor
|
||||
end_node.outputs[0].shape = ['batch_size*num_heads', seq_len, seq_len]
|
||||
original_output_all.append(end_node.outputs[0])
|
||||
|
||||
graph_raw.outputs = graph_raw.outputs + original_output_all # add plugin outputs to graph output
|
||||
|
||||
## for modified graph with plugin
|
||||
nodes = [node for node in graph.nodes if node.op == 'GatherElements'] # find by gatherelements op
|
||||
assert len(nodes) % 2 == 0, "No. of GatherElements nodes is not an even number!"
|
||||
|
||||
layers = [(nodes[2*i+0], nodes[2*i+1]) for i in range(len(nodes)//2)] # 2 gatherelements in 1 layer
|
||||
plugin_output_all = []
|
||||
for l, (left,right) in enumerate(layers):
|
||||
# inputs: (data0, data1, data2), input tensors for c2c add and 2 gathers
|
||||
inputs = list(left.o().o().o().o().i().inputs)[0:1] + list(left.inputs)[0:1] + list(right.inputs)[0:1]
|
||||
# outputs: (result), output tensors after adding 3 gather results
|
||||
outputs = list(left.o().o().o().o().outputs)
|
||||
end_node = left.o().o().o().o()
|
||||
end_node.outputs[0].dtype = graph.outputs[0].dtype # need to explicitly specify dtype and shape of graph output tensor
|
||||
end_node.outputs[0].shape = ['batch_size*num_heads', seq_len, seq_len]
|
||||
plugin_output_all.append(end_node.outputs[0]) # add to graph output (outside this loop)
|
||||
|
||||
# constants: scaling factor, relative distance span
|
||||
factor = left.o().inputs[1].inputs[0].attrs["value"].values.item()
|
||||
span = right.i(1,0).i().i().i().inputs[1].inputs[0].attrs["value"].values.item()
|
||||
|
||||
# insert plugin layer
|
||||
graph.insert_disentangled_attention(inputs, outputs, factor, span)
|
||||
|
||||
graph.outputs = graph.outputs + plugin_output_all # add plugin outputs to graph output
|
||||
|
||||
return graph_raw, graph
|
||||
|
||||
def check_model(model_name):
|
||||
# Load the ONNX model
|
||||
model = onnx.load(model_name)
|
||||
|
||||
# Check that the model is well formed
|
||||
onnx.checker.check_model(model)
|
||||
|
||||
# load onnx
|
||||
graph = gs.import_onnx(onnx.load(model_input))
|
||||
|
||||
# first, remove uint8 cast nodes
|
||||
graph = remove_uint8_cast(graph)
|
||||
|
||||
if use_plugin:
|
||||
# save the modified model with plugin nodes
|
||||
|
||||
# replace Add + Gather + Gather + Transpose + Add + Div (c2c and c2p and p2c) with DisentangledAttention_TRT node
|
||||
graph = insert_disentangled_attention_all(graph)
|
||||
|
||||
# remove unused nodes, and topologically sort the graph.
|
||||
graph.cleanup().toposort()
|
||||
|
||||
# export the onnx graph from graphsurgeon
|
||||
onnx.save_model(gs.export_onnx(graph), model_output)
|
||||
print(f"Saving modified model to {model_output}")
|
||||
|
||||
# don't check model because 'DisentangledAttention_TRT' is not a registered op
|
||||
|
||||
elif correctness_check:
|
||||
# correctness check, save two models (original and w/ plugin) with intermediate output nodes inserted
|
||||
graph_raw, graph = correctness_check_models(graph)
|
||||
|
||||
# remove unused nodes, and topologically sort the graph.
|
||||
graph_raw.cleanup().toposort()
|
||||
graph.cleanup().toposort()
|
||||
|
||||
# export the onnx graph from graphsurgeon
|
||||
model_output1 = os.path.splitext(model_input)[0] + "_correctness_check_original" + os.path.splitext(model_input)[-1]
|
||||
model_output2 = os.path.splitext(model_input)[0] + "_correctness_check_plugin" + os.path.splitext(model_input)[-1]
|
||||
onnx.save_model(gs.export_onnx(graph_raw), model_output1)
|
||||
onnx.save_model(gs.export_onnx(graph), model_output2)
|
||||
|
||||
print(f"Saving models for correctness check to {model_output1} (original) and {model_output2} (with plugin)")
|
||||
|
||||
check_model(model_output1)
|
||||
# don't check model_output2 because 'DisentangledAttention_TRT' is not a registered op
|
||||
|
||||
else:
|
||||
# no flag passed, save model with just uint8 cast removed
|
||||
graph.cleanup().toposort()
|
||||
onnx.save_model(gs.export_onnx(graph), model_output)
|
||||
print(f"Saving modified model to {model_output}")
|
||||
check_model(model_output)
|
||||
@@ -0,0 +1,201 @@
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
|
||||
"""
|
||||
Test ORT-TRT engine of DeBERTa model. Different precisions are supported.
|
||||
|
||||
Usage:
|
||||
Test model inference time:
|
||||
- python deberta_ort_inference.py --onnx=./test/deberta.onnx --test fp16
|
||||
|
||||
Correctness check by comparing original model and model with plugin:
|
||||
- python deberta_ort_inference.py --onnx=./test/deberta --correctness-check fp16
|
||||
|
||||
Notes:
|
||||
- supported precisions are fp32/fp16. For test, you can specify more than one precisions, and TensorRT engine of each precision will be built sequentially.
|
||||
- engine files are saved at `./engine_cache/[Model name]_[GPU name]_[Precision]/`. Note that TensorRT engine is specific to both GPU architecture and TensorRT version.
|
||||
- if in --correctness-check mode, the argument for --onnx is the stem name for the model without .onnx extension.
|
||||
"""
|
||||
|
||||
import os, argparse
|
||||
import onnxruntime as ort
|
||||
import numpy as np
|
||||
import torch
|
||||
from time import time
|
||||
|
||||
ENGINE_PATH = './test'
|
||||
if not os.path.exists(ENGINE_PATH):
|
||||
os.makedirs(ENGINE_PATH)
|
||||
|
||||
def GPU_ABBREV(name):
|
||||
'''
|
||||
Map GPU device query name to abbreviation.
|
||||
|
||||
::param str name Device name from torch.cuda.get_device_name().
|
||||
::return str GPU abbreviation.
|
||||
'''
|
||||
|
||||
GPU_LIST = [
|
||||
'V100',
|
||||
'TITAN',
|
||||
'T4',
|
||||
'A100',
|
||||
'A10G',
|
||||
'A10'
|
||||
]
|
||||
# Partial list, can be extended. The order of A100, A10G, A10 matters. They're put in a way to not detect substring A10 as A100
|
||||
|
||||
for i in GPU_LIST:
|
||||
if i in name:
|
||||
return i
|
||||
|
||||
return 'GPU' # for names not in the partial list, use 'GPU' as default
|
||||
|
||||
gpu_name = GPU_ABBREV(torch.cuda.get_device_name())
|
||||
|
||||
VALID_PRECISION = [
|
||||
'fp32',
|
||||
'fp16',
|
||||
]
|
||||
|
||||
parser = argparse.ArgumentParser(description="Build and test TensorRT engine.")
|
||||
parser.add_argument('--onnx', required=True, help='ONNX model path (or filename stem if in correctness check mode).')
|
||||
parser.add_argument('--test', nargs='+', help='Test ORT-TRT engine in precision fp32/fp16. You can list multiple precisions to test all of them.')
|
||||
parser.add_argument('--correctness-check', nargs='+', help='Correctness check for original & plugin TRT engines in precision fp32/fp16. You can list multiple precisions to check all of them.')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
ONNX_MODEL = args.onnx
|
||||
MODEL_STEM = os.path.splitext(args.onnx)[0].split('/')[-1]
|
||||
TEST = args.test
|
||||
CORRECTNESS = args.correctness_check
|
||||
|
||||
if TEST:
|
||||
for i in TEST:
|
||||
if i not in VALID_PRECISION:
|
||||
parser.error(f'Unsupported precision {i}')
|
||||
if CORRECTNESS:
|
||||
for i in CORRECTNESS:
|
||||
if i not in VALID_PRECISION:
|
||||
parser.error(f'Unsupported precision {i}')
|
||||
|
||||
def test_engine():
|
||||
|
||||
for precision in TEST:
|
||||
|
||||
engine_cachepath = '/'.join([ENGINE_PATH, '_'.join([MODEL_STEM, gpu_name, precision, 'ort'])])
|
||||
|
||||
providers = [
|
||||
('TensorrtExecutionProvider', {
|
||||
'trt_max_workspace_size': 2147483648,
|
||||
'trt_fp16_enable': precision == 'fp16',
|
||||
'trt_engine_cache_enable': True,
|
||||
'trt_engine_cache_path': engine_cachepath
|
||||
}),
|
||||
'CUDAExecutionProvider'] # EP order indicates priority
|
||||
|
||||
so = ort.SessionOptions()
|
||||
|
||||
sess = ort.InferenceSession(ONNX_MODEL, sess_options=so, providers=providers)
|
||||
|
||||
print(f'Running inference on engine {engine_cachepath}')
|
||||
|
||||
## psuedo-random input test
|
||||
batch_size = 1
|
||||
seq_len = sess.get_inputs()[0].shape[1]
|
||||
vocab = 128203
|
||||
input_ids = torch.randint(0, vocab, (batch_size, seq_len), dtype=torch.long)
|
||||
attention_mask = torch.randint(0, 2, (batch_size, seq_len), dtype=torch.long)
|
||||
inputs = {
|
||||
'input_ids': input_ids.numpy(),
|
||||
'attention_mask': attention_mask.numpy()
|
||||
}
|
||||
|
||||
outputs = sess.run(None, inputs)
|
||||
|
||||
nreps = 100
|
||||
start_time = time()
|
||||
for _ in range(nreps):
|
||||
sess.run(None, inputs)
|
||||
end_time = time()
|
||||
|
||||
duration = end_time - start_time
|
||||
print(f'Average Inference time (ms) of {nreps} runs: {duration/nreps*1000:.3f}. For more accurate test, please use the onnxruntime_perf_test commands.')
|
||||
|
||||
def correctness_check_engines():
|
||||
|
||||
for precision in CORRECTNESS:
|
||||
|
||||
engine_cachepath1 = '/'.join([ENGINE_PATH, '_'.join([MODEL_STEM, 'original', gpu_name, precision, 'ort'])])
|
||||
engine_cachepath2 = '/'.join([ENGINE_PATH, '_'.join([MODEL_STEM, 'plugin', gpu_name, precision, 'ort'])])
|
||||
|
||||
if not os.path.exists(engine_cachepath1) or not os.path.exists(engine_cachepath2):
|
||||
print('At least one of the original and/or plugin engines do not exist. Please build them first by --test')
|
||||
return
|
||||
|
||||
print(f'Running inference on original engine {engine_cachepath1} and plugin engine {engine_cachepath2}')
|
||||
|
||||
so = ort.SessionOptions()
|
||||
|
||||
providers1 = [
|
||||
('TensorrtExecutionProvider', {
|
||||
'trt_max_workspace_size': 2147483648,
|
||||
'trt_fp16_enable': precision == 'fp16',
|
||||
'trt_engine_cache_enable': True,
|
||||
'trt_engine_cache_path': engine_cachepath1
|
||||
}),
|
||||
'CUDAExecutionProvider']
|
||||
|
||||
providers2 = [
|
||||
('TensorrtExecutionProvider', {
|
||||
'trt_max_workspace_size': 2147483648,
|
||||
'trt_fp16_enable': precision == 'fp16',
|
||||
'trt_engine_cache_enable': True,
|
||||
'trt_engine_cache_path': engine_cachepath2
|
||||
}),
|
||||
'CUDAExecutionProvider']
|
||||
|
||||
sess1 = ort.InferenceSession(ONNX_MODEL+'_original.onnx', sess_options=so, providers=providers1)
|
||||
sess2 = ort.InferenceSession(ONNX_MODEL+'_plugin.onnx', sess_options=so, providers=providers2)
|
||||
|
||||
## psuedo-random input test
|
||||
batch_size = 1
|
||||
seq_len = sess1.get_inputs()[0].shape[1]
|
||||
vocab = 128203
|
||||
input_ids = torch.randint(0, vocab, (batch_size, seq_len), dtype=torch.long)
|
||||
attention_mask = torch.randint(0, 2, (batch_size, seq_len), dtype=torch.long)
|
||||
inputs = {
|
||||
'input_ids': input_ids.numpy(),
|
||||
'attention_mask': attention_mask.numpy()
|
||||
}
|
||||
|
||||
outputs1 = sess1.run(None, inputs)
|
||||
outputs2 = sess2.run(None, inputs)
|
||||
|
||||
for i in range(len(outputs1)):
|
||||
avg_abs_error = np.sum(np.abs(outputs1[i] - outputs2[i])) / outputs1[i].size
|
||||
max_abs_error = np.max(np.abs(outputs1[i] - outputs2[i]))
|
||||
print(f"Output {i}:")
|
||||
print("onnx model (original): ", outputs1[i])
|
||||
print("onnx model (plugin): ", outputs2[i])
|
||||
print(f"[Output {i} Element-wise Check] Avgerage absolute error: {avg_abs_error:e}, Maximum absolute error: {max_abs_error:e}. 1e-2~1e-3 expected for FP16 (10 significance bits) and 1e-6~1e-7 expected for FP32 (23 significance bits) " ) # machine epsilon for different precisions: https://en.wikipedia.org/wiki/Machine_epsilon
|
||||
|
||||
if TEST:
|
||||
test_engine()
|
||||
|
||||
if CORRECTNESS:
|
||||
correctness_check_engines()
|
||||
@@ -0,0 +1,134 @@
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
|
||||
'''
|
||||
Generate HuggingFace DeBERTa (V2) model with different configurations (e.g., sequence length, hidden size, No. of layers, No. of heads, etc.) and export in ONNX format
|
||||
|
||||
Usage:
|
||||
python deberta_pytorch2onnx.py [--filename xx.onnx] [--variant microsoft/deberta-xx] [--seq-len xx]
|
||||
'''
|
||||
|
||||
import os, time, argparse
|
||||
from transformers import DebertaV2Tokenizer, DebertaV2Config, DebertaV2ForSequenceClassification
|
||||
# DEBERTA V2 implementation, https://github.com/huggingface/transformers/blob/master/src/transformers/models/deberta_v2/modeling_deberta_v2.py
|
||||
import torch, onnxruntime as ort, numpy as np
|
||||
|
||||
parser = argparse.ArgumentParser(description="Generate HuggingFace DeBERTa (V2) model with different configurations and export in ONNX format. This will save the model under the same directory as 'deberta_seqxxx_hf.onnx'.")
|
||||
parser.add_argument('--filename', type=str, help='Path to the save the ONNX model')
|
||||
parser.add_argument('--variant', type=str, default=None, help='DeBERTa variant name. Such as microsoft/deberta-v3-xsmall')
|
||||
parser.add_argument('--seq-len', type=int, default=None, help='Specify maximum sequence length. Note: --variant and --seq-len cannot be used together. Pre-trained models have pre-defined sequence length')
|
||||
|
||||
args = parser.parse_args()
|
||||
onnx_filename = args.filename
|
||||
model_variant = args.variant
|
||||
sequence_length = args.seq_len
|
||||
|
||||
assert not args.variant or (args.variant and not args.seq_len), "--variant and --seq-len cannot be used together!"
|
||||
assert torch.cuda.is_available(), "CUDA not available!"
|
||||
|
||||
def randomize_model(model):
|
||||
for module_ in model.named_modules():
|
||||
if isinstance(module_[1],(torch.nn.Linear, torch.nn.Embedding)):
|
||||
module_[1].weight.data.normal_(mean=0.0, std=model.config.initializer_range)
|
||||
elif isinstance(module_[1], torch.nn.LayerNorm):
|
||||
module_[1].bias.data.zero_()
|
||||
module_[1].weight.data.fill_(1.0)
|
||||
if isinstance(module_[1], torch.nn.Linear) and module_[1].bias is not None:
|
||||
module_[1].bias.data.zero_()
|
||||
return model
|
||||
|
||||
def export():
|
||||
parent_dir = os.path.dirname(onnx_filename)
|
||||
if not os.path.exists(parent_dir):
|
||||
os.makedirs(parent_dir)
|
||||
|
||||
if model_variant is None:
|
||||
# default model hyper-params
|
||||
batch_size = 1
|
||||
seq_len = 2048 if sequence_length is None else sequence_length
|
||||
max_position_embeddings = 512 if seq_len <= 512 else seq_len # maximum sequence length that this model might ever be used with. By default 512. otherwise error https://github.com/huggingface/transformers/issues/4542
|
||||
vocab_size = 128203
|
||||
hidden_size = 384
|
||||
layers = 12
|
||||
heads = 6
|
||||
intermediate_size = hidden_size*4 # feed forward layer dimension
|
||||
type_vocab_size = 0
|
||||
# relative attention
|
||||
relative_attention=True
|
||||
max_relative_positions = 256 # k
|
||||
pos_att_type = ["p2c", "c2p"]
|
||||
|
||||
deberta_config = DebertaV2Config(vocab_size=vocab_size, hidden_size=hidden_size, num_hidden_layers=layers, num_attention_heads=heads, intermediate_size=intermediate_size, type_vocab_size=type_vocab_size, max_position_embeddings=max_position_embeddings, relative_attention=relative_attention, max_relative_positions=max_relative_positions, pos_att_type=pos_att_type)
|
||||
deberta_model = DebertaV2ForSequenceClassification(deberta_config)
|
||||
deberta_model = randomize_model(deberta_model)
|
||||
else:
|
||||
deberta_model = DebertaV2ForSequenceClassification.from_pretrained(model_variant)
|
||||
deberta_config = DebertaV2Config.from_pretrained(model_variant)
|
||||
|
||||
batch_size = 1
|
||||
seq_len = deberta_config.max_position_embeddings
|
||||
vocab_size = deberta_config.vocab_size
|
||||
|
||||
deberta_model.cuda().eval()
|
||||
|
||||
# input/output
|
||||
gpu = torch.device('cuda')
|
||||
input_ids = torch.randint(0, vocab_size, (batch_size, seq_len), dtype=torch.long, device=gpu)
|
||||
attention_mask = torch.randint(0, 2, (batch_size, seq_len), dtype=torch.long, device=gpu)
|
||||
input_names = ['input_ids', 'attention_mask']
|
||||
output_names = ['output']
|
||||
dynamic_axes={'input_ids' : {0 : 'batch_size'},
|
||||
'attention_mask' : {0 : 'batch_size'},
|
||||
'output' : {0 : 'batch_size'}}
|
||||
|
||||
# ONNX export
|
||||
torch.onnx.export(deberta_model, # model
|
||||
(input_ids, attention_mask), # model inputs
|
||||
onnx_filename,
|
||||
export_params=True,
|
||||
opset_version=13,
|
||||
do_constant_folding=True,
|
||||
input_names = input_names,
|
||||
output_names = output_names,
|
||||
dynamic_axes = dynamic_axes)
|
||||
|
||||
# full precision inference
|
||||
num_trials = 10
|
||||
|
||||
start = time.time()
|
||||
for i in range(num_trials):
|
||||
results = deberta_model(input_ids, attention_mask)
|
||||
end = time.time()
|
||||
|
||||
print("Average PyTorch FP32(TF32) time: {:.2f} ms".format((end - start)/num_trials*1000))
|
||||
|
||||
# half precision inference (do this after onnx export, otherwise the export ONNX model is with FP16 weights...)
|
||||
deberta_model_fp16 = deberta_model.half()
|
||||
start = time.time()
|
||||
for i in range(num_trials):
|
||||
results = deberta_model_fp16(input_ids, attention_mask)
|
||||
end = time.time()
|
||||
|
||||
print("Average PyTorch FP16 time: {:.2f} ms".format((end - start)/num_trials*1000))
|
||||
|
||||
# model size
|
||||
total_params = sum(param.numel() for param in deberta_model.parameters())
|
||||
print("Total # of params: ", total_params)
|
||||
print("Maximum sequence length: ", seq_len)
|
||||
|
||||
if __name__ == "__main__":
|
||||
export()
|
||||
@@ -0,0 +1,401 @@
|
||||
#
|
||||
# SPDX-FileCopyrightText: Copyright (c) 1993-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
"""
|
||||
Build and test TensorRT engines generated from the DeBERTa model. Different precisions are supported.
|
||||
|
||||
Usage:
|
||||
Build and test a model:
|
||||
- build: python deberta_tensorrt_inference.py --onnx=xx.onnx --build fp16 # build TRT engines
|
||||
- test: python deberta_tensorrt_inference.py --onnx=xx.onnx --test fp16 # test will measure the inference time
|
||||
- build and test: python deberta_tensorrt_inference.py --onnx=xx.onnx --build fp16 --test fp16
|
||||
|
||||
Correctness check is done by comparing engines generated from the original model and the plugin model:
|
||||
- [1] export ONNX model with extra output nodes: python deberta_onnx_modify.py xx.onnx --correctness-check
|
||||
- [2] build original model: python deberta_tensorrt_inference.py --onnx=xx_correctness_check_original.onnx --build fp16
|
||||
- [3] build plugin model: python deberta_tensorrt_inference.py --onnx=xx_correctness_check_plugin.onnx --build fp16
|
||||
- [4] correctness check: python deberta_tensorrt_inference.py --onnx=deberta --correctness_check fp16
|
||||
|
||||
Notes:
|
||||
- supported precisions are fp32/tf32/fp16. For both --build and --test, you can specify more than one precisions, and TensorRT engines of each precision will be built sequentially.
|
||||
- engine files are saved as `**/[Model name]_[GPU name]_[Precision].engine`. Note that TensorRT engines are specific to both GPU architecture and TensorRT version, and therefore are not compatible cross-version nor cross-device.
|
||||
- in --correctness-check mode, the argument for --onnx is the `root` name for the models [root]_correctness_check_original/plugin.onnx
|
||||
"""
|
||||
|
||||
import torch
|
||||
import tensorrt as trt
|
||||
import os, sys, argparse
|
||||
import numpy as np
|
||||
from time import time
|
||||
from cuda.bindings import driver as cuda, runtime as cudart
|
||||
from cuda_utils import (
|
||||
cuda_call,
|
||||
CudaStreamContext,
|
||||
memcpy_host_to_device_async,
|
||||
memcpy_device_to_host_async,
|
||||
memcpy_host_to_device,
|
||||
memcpy_device_to_device_async,
|
||||
memcpy_device_to_device,
|
||||
)
|
||||
|
||||
TRT_VERSION = int(trt.__version__[:3].replace('.','')) # e.g., version 8.4.1.5 becomes 84
|
||||
|
||||
def GPU_ABBREV(name):
|
||||
'''
|
||||
Map GPU device query name to abbreviation.
|
||||
|
||||
::param str name Device name from torch.cuda.get_device_name().
|
||||
::return str GPU abbreviation.
|
||||
'''
|
||||
|
||||
GPU_LIST = [
|
||||
'V100',
|
||||
'TITAN',
|
||||
'T4',
|
||||
'A100',
|
||||
'A10G',
|
||||
'A10'
|
||||
]
|
||||
# Partial list, can be extended. The order of A100, A10G, A10 matters. They're put in a way to not detect substring A10 as A100
|
||||
|
||||
for i in GPU_LIST:
|
||||
if i in name:
|
||||
return i
|
||||
|
||||
return 'GPU' # for names not in the partial list, use 'GPU' as default
|
||||
|
||||
gpu_name = GPU_ABBREV(torch.cuda.get_device_name())
|
||||
|
||||
VALID_PRECISION = [
|
||||
'fp32',
|
||||
'tf32',
|
||||
'fp16'
|
||||
]
|
||||
|
||||
parser = argparse.ArgumentParser(description="Build and test TensorRT engine.")
|
||||
parser.add_argument('--onnx', required=True, help='ONNX model path (or filename stem if in correctness check mode).')
|
||||
parser.add_argument('--build', nargs='+', help='Build TRT engine in precision fp32/tf32/fp16. You can list multiple precisions to build all of them.')
|
||||
parser.add_argument('--test', nargs='+', help='Test TRT engine in precision fp32/tf32/fp16. You can list multiple precisions to test all of them.')
|
||||
parser.add_argument('--correctness-check', nargs='+', help='Correctness check for original & plugin TRT engines in precision fp32/tf32/fp16. You can list multiple precisions to check all of them.')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
ONNX_MODEL = args.onnx
|
||||
MODEL_NAME = os.path.splitext(args.onnx)[0]
|
||||
BUILD = args.build
|
||||
TEST = args.test
|
||||
CORRECTNESS = args.correctness_check
|
||||
|
||||
if not (args.build or args.test or args.correctness_check):
|
||||
parser.error('Please specify --build and/or --test and/or --correctness-check' )
|
||||
|
||||
if BUILD:
|
||||
for i in BUILD:
|
||||
if i not in VALID_PRECISION:
|
||||
parser.error(f'Unsupported precision {i}')
|
||||
if TEST:
|
||||
for i in TEST:
|
||||
if i not in VALID_PRECISION:
|
||||
parser.error(f'Unsupported precision {i}')
|
||||
if CORRECTNESS:
|
||||
for i in CORRECTNESS:
|
||||
if i not in VALID_PRECISION:
|
||||
parser.error(f'Unsupported precision {i}')
|
||||
|
||||
class TRTModel:
|
||||
'''
|
||||
Generic class to run a TRT engine by specifying engine path and giving input data.
|
||||
'''
|
||||
class HostDeviceMem(object):
|
||||
'''
|
||||
Helper class to record host-device memory pointer pairs
|
||||
'''
|
||||
def __init__(self, host_mem, device_mem):
|
||||
self.host = host_mem
|
||||
self.device = device_mem
|
||||
|
||||
def __str__(self):
|
||||
return "Host:\n" + str(self.host) + "\nDevice:\n" + str(self.device)
|
||||
|
||||
def __repr__(self):
|
||||
return self.__str__()
|
||||
|
||||
def __init__(self, engine_path):
|
||||
self.engine_path = engine_path
|
||||
self.logger = trt.Logger(trt.Logger.WARNING)
|
||||
self.runtime = trt.Runtime(self.logger)
|
||||
|
||||
# load and deserialize TRT engine
|
||||
self.engine = self.load_engine()
|
||||
|
||||
# allocate input/output memory buffers
|
||||
self.inputs, self.outputs, self.bindings, self.stream = self.allocate_buffers(self.engine)
|
||||
|
||||
# create context
|
||||
self.context = self.engine.create_execution_context()
|
||||
|
||||
# Dict of NumPy dtype -> torch dtype (when the correspondence exists). From: https://github.com/pytorch/pytorch/blob/e180ca652f8a38c479a3eff1080efe69cbc11621/torch/testing/_internal/common_utils.py#L349
|
||||
self.numpy_to_torch_dtype_dict = {
|
||||
bool : torch.bool,
|
||||
np.uint8 : torch.uint8,
|
||||
np.int8 : torch.int8,
|
||||
np.int16 : torch.int16,
|
||||
np.int32 : torch.int32,
|
||||
np.int64 : torch.int64,
|
||||
np.float16 : torch.float16,
|
||||
np.float32 : torch.float32,
|
||||
np.float64 : torch.float64,
|
||||
np.complex64 : torch.complex64,
|
||||
np.complex128 : torch.complex128
|
||||
}
|
||||
|
||||
def load_engine(self):
|
||||
with open(self.engine_path, 'rb') as f:
|
||||
engine = self.runtime.deserialize_cuda_engine(f.read())
|
||||
return engine
|
||||
|
||||
def allocate_buffers(self, engine):
|
||||
'''
|
||||
Allocates all buffers required for an engine, i.e. host/device inputs/outputs.
|
||||
'''
|
||||
inputs = []
|
||||
outputs = []
|
||||
bindings = []
|
||||
stream = CudaStreamContext()
|
||||
|
||||
for i in range(engine.num_io_tensors):
|
||||
tensor_name = engine.get_tensor_name(i)
|
||||
size = trt.volume(engine.get_tensor_shape(tensor_name))
|
||||
dtype = trt.nptype(engine.get_tensor_dtype(tensor_name))
|
||||
|
||||
# Allocate host and device buffers
|
||||
host_mem = np.empty(size, dtype)
|
||||
device_mem = cuda_call(cudart.cudaMalloc(host_mem.nbytes))
|
||||
|
||||
# Append the device buffer address to device bindings. When cast to int, it's a linear index into the context's memory (like memory address).
|
||||
bindings.append(int(device_mem))
|
||||
|
||||
# Append to the appropriate input/output list.
|
||||
if engine.get_tensor_mode(tensor_name) == trt.TensorIOMode.INPUT:
|
||||
inputs.append(self.HostDeviceMem(host_mem, device_mem))
|
||||
else:
|
||||
outputs.append(self.HostDeviceMem(host_mem, device_mem))
|
||||
|
||||
return inputs, outputs, bindings, stream
|
||||
|
||||
def __call__(self, model_inputs: list, timing=False):
|
||||
'''
|
||||
Inference step (like forward() in PyTorch).
|
||||
|
||||
model_inputs: list of numpy array or list of torch.Tensor (on GPU)
|
||||
'''
|
||||
NUMPY = False
|
||||
TORCH = False
|
||||
if isinstance(model_inputs[0], np.ndarray):
|
||||
NUMPY = True
|
||||
elif torch.is_tensor(model_inputs[0]):
|
||||
TORCH = True
|
||||
else:
|
||||
assert False, 'Unsupported input data format!'
|
||||
|
||||
# batch size consistency check
|
||||
if NUMPY:
|
||||
batch_size = np.unique(np.array([i.shape[0] for i in model_inputs]))
|
||||
elif TORCH:
|
||||
batch_size = np.unique(np.array([i.size(dim=0) for i in model_inputs]))
|
||||
assert len(batch_size) == 1, 'Input batch sizes are not consistent!'
|
||||
batch_size = batch_size[0]
|
||||
|
||||
for i, model_input in enumerate(model_inputs):
|
||||
binding_name = self.engine.get_tensor_name(i) # i-th input/output name
|
||||
binding_dtype = trt.nptype(self.engine.get_tensor_dtype(binding_name)) # trt can only tell to numpy dtype
|
||||
|
||||
# input type cast
|
||||
if NUMPY:
|
||||
model_input = model_input.astype(binding_dtype)
|
||||
elif TORCH:
|
||||
model_input = model_input.to(self.numpy_to_torch_dtype_dict[binding_dtype])
|
||||
|
||||
if NUMPY:
|
||||
# fill host memory with flattened input data
|
||||
np.copyto(self.inputs[i].host, model_input.ravel())
|
||||
elif TORCH:
|
||||
nbytes = model_input.element_size() * model_input.nelement()
|
||||
if timing:
|
||||
memcpy_device_to_device(self.inputs[i].device, model_input.data_ptr(), nbytes)
|
||||
else:
|
||||
# for Torch GPU tensor it's easier, can just do Device to Device copy
|
||||
memcpy_device_to_device_async(self.inputs[i].device, model_input.data_ptr(), nbytes, self.stream.stream)
|
||||
|
||||
if NUMPY:
|
||||
if timing:
|
||||
[memcpy_host_to_device(inp.device, inp.host) for inp in self.inputs]
|
||||
else:
|
||||
# input, Host to Device
|
||||
[memcpy_host_to_device_async(inp.device, inp.host, self.stream.stream) for inp in self.inputs]
|
||||
|
||||
for i in range(self.engine.num_io_tensors):
|
||||
self.context.set_tensor_address(self.engine.get_tensor_name(i), self.bindings[i])
|
||||
|
||||
duration = 0
|
||||
if timing:
|
||||
start_time = time()
|
||||
self.context.execute_v2(bindings=self.bindings)
|
||||
end_time = time()
|
||||
duration = end_time - start_time
|
||||
else:
|
||||
# run inference
|
||||
self.context.execute_async_v3(stream_handle=self.stream.stream)
|
||||
|
||||
if timing:
|
||||
[cuda_call(cudart.cudaMemcpy(out.host.ctypes.data, out.device, out.host.nbytes, cudart.cudaMemcpyKind.cudaMemcpyDeviceToHost)) for out in self.outputs]
|
||||
else:
|
||||
# output, Device to Host
|
||||
[memcpy_device_to_host_async(out.host, out.device, self.stream.stream) for out in self.outputs]
|
||||
|
||||
if not timing:
|
||||
# synchronize to ensure completion of async calls
|
||||
self.stream.synchronize()
|
||||
|
||||
if NUMPY:
|
||||
return [out.host.reshape(batch_size,-1) for out in self.outputs], duration
|
||||
elif TORCH:
|
||||
return [torch.from_numpy(out.host.reshape(batch_size,-1)) for out in self.outputs], duration
|
||||
|
||||
def build_engine():
|
||||
|
||||
TRT_LOGGER = trt.Logger(trt.Logger.INFO)
|
||||
TRT_BUILDER = trt.Builder(TRT_LOGGER)
|
||||
|
||||
for precision in BUILD:
|
||||
engine_filename = '_'.join([MODEL_NAME, gpu_name, precision]) + '.engine'
|
||||
if os.path.exists(engine_filename):
|
||||
print(f'Engine file {engine_filename} exists. Skip building...')
|
||||
continue
|
||||
|
||||
print(f'Building {precision} engine of {MODEL_NAME} model on {gpu_name} GPU...')
|
||||
|
||||
## parse ONNX model
|
||||
network_creation_flag = 0
|
||||
if "EXPLICIT_BATCH" in trt.NetworkDefinitionCreationFlag.__members__.keys():
|
||||
network_creation_flag = 1 << int(trt.NetworkDefinitionCreationFlag.EXPLICIT_BATCH)
|
||||
network = TRT_BUILDER.create_network(network_creation_flag)
|
||||
onnx_parser = trt.OnnxParser(network, TRT_LOGGER)
|
||||
parse_success = onnx_parser.parse_from_file(ONNX_MODEL)
|
||||
for idx in range(onnx_parser.num_errors):
|
||||
print(onnx_parser.get_error(idx))
|
||||
if not parse_success:
|
||||
sys.exit('ONNX model parsing failed')
|
||||
|
||||
## build TRT engine (configuration options at: https://docs.nvidia.com/deeplearning/tensorrt/api/python_api/infer/Core/BuilderConfig.html#ibuilderconfig)
|
||||
config = TRT_BUILDER.create_builder_config()
|
||||
|
||||
seq_len = network.get_input(0).shape[1]
|
||||
|
||||
# handle dynamic shape (min/opt/max): https://docs.nvidia.com/deeplearning/tensorrt/developer-guide/index.html#work_dynamic_shapes
|
||||
# by default batch dim set as 1 for all min/opt/max. If there are batch need, change the value for opt and max accordingly
|
||||
profile = TRT_BUILDER.create_optimization_profile()
|
||||
profile.set_shape("input_ids", (1,seq_len), (1,seq_len), (1,seq_len))
|
||||
profile.set_shape("attention_mask", (1,seq_len), (1,seq_len), (1,seq_len))
|
||||
config.add_optimization_profile(profile)
|
||||
config.set_memory_pool_limit(trt.MemoryPoolType.WORKSPACE, 4096 * (1 << 20)) # 4096 MiB
|
||||
|
||||
# precision
|
||||
if precision == 'fp32':
|
||||
config.clear_flag(trt.BuilderFlag.TF32) # TF32 enabled by default, need to clear flag
|
||||
elif precision == 'tf32':
|
||||
pass
|
||||
elif precision == 'fp16':
|
||||
config.set_flag(trt.BuilderFlag.FP16)
|
||||
|
||||
# build
|
||||
serialized_engine = TRT_BUILDER.build_serialized_network(network, config)
|
||||
|
||||
## save TRT engine
|
||||
with open(engine_filename, 'wb') as f:
|
||||
f.write(serialized_engine)
|
||||
print(f'Engine is saved to {engine_filename}')
|
||||
|
||||
def test_engine():
|
||||
|
||||
for precision in TEST:
|
||||
## load and deserialize TRT engine
|
||||
engine_filename = '_'.join([MODEL_NAME, gpu_name, precision]) + '.engine'
|
||||
print(f'Running inference on engine {engine_filename}')
|
||||
|
||||
model = TRTModel(engine_filename)
|
||||
|
||||
## psuedo-random input test
|
||||
batch_size = 1
|
||||
seq_len = model.engine.get_tensor_shape(model.engine.get_tensor_name(0))[1]
|
||||
vocab = 128203
|
||||
gpu = torch.device('cuda')
|
||||
torch.manual_seed(0) # make sure in each test the seed are the same
|
||||
input_ids = torch.randint(0, vocab, (batch_size, seq_len), dtype=torch.long, device=gpu)
|
||||
attention_mask = torch.randint(0, 2, (batch_size, seq_len), dtype=torch.long, device=gpu)
|
||||
inputs = [input_ids, attention_mask]
|
||||
outputs, duration = model(inputs, timing=True)
|
||||
|
||||
nreps = 100
|
||||
duration_total = 0
|
||||
for _ in range(nreps):
|
||||
outputs, duration = model(inputs, timing=True)
|
||||
duration_total += duration
|
||||
|
||||
print(f'Average Inference time (ms) of {nreps} runs: {duration_total/nreps*1000:.3f}')
|
||||
|
||||
def correctness_check_engines():
|
||||
for precision in CORRECTNESS:
|
||||
## load and deserialize TRT engine
|
||||
engine_filename1 = '_'.join([ONNX_MODEL, 'correctness_check_original', gpu_name, precision]) + '.engine'
|
||||
engine_filename2 = '_'.join([ONNX_MODEL, 'correctness_check_plugin', gpu_name, precision]) + '.engine'
|
||||
|
||||
assert os.path.exists(engine_filename1), f'Engine file {engine_filename1} does not exist. Please build the engine first by --build'
|
||||
assert os.path.exists(engine_filename2), f'Engine file {engine_filename2} does not exist. Please build the engine first by --build'
|
||||
|
||||
print(f'Running inference on original engine {engine_filename1} and plugin engine {engine_filename2}')
|
||||
|
||||
model1 = TRTModel(engine_filename1)
|
||||
model2 = TRTModel(engine_filename2)
|
||||
|
||||
## psuedo-random input test
|
||||
batch_size = 1
|
||||
seq_len = model1.engine.get_tensor_shape(model1.engine.get_tensor_name(0))[1]
|
||||
vocab = 128203
|
||||
gpu = torch.device('cuda')
|
||||
# torch.manual_seed(0) # make sure in each test the seed are the same
|
||||
input_ids = torch.randint(0, vocab, (batch_size, seq_len), dtype=torch.long, device=gpu)
|
||||
attention_mask = torch.randint(0, 2, (batch_size, seq_len), dtype=torch.long, device=gpu)
|
||||
inputs = [input_ids, attention_mask]
|
||||
|
||||
outputs1, _ = model1(inputs)
|
||||
outputs2, _ = model2(inputs)
|
||||
|
||||
# element-wise and layer-wise output comparison
|
||||
for i in range(len(outputs1)):
|
||||
avg_abs_error = torch.sum(torch.abs(torch.sub(outputs1[i], outputs2[i]))) / torch.numel(outputs1[i])
|
||||
max_abs_error = torch.max(torch.abs(torch.sub(outputs1[i], outputs2[i])))
|
||||
print(f"[Layer {i} Element-wise Check] Avgerage absolute error: {avg_abs_error.item():e}, Maximum absolute error: {max_abs_error.item():e}. 1e-2~1e-3 expected for FP16 (10 significance bits) and 1e-6~1e-7 expected for FP32 (23 significance bits)" ) # machine epsilon for different precisions: https://en.wikipedia.org/wiki/Machine_epsilon
|
||||
|
||||
if BUILD:
|
||||
build_engine()
|
||||
|
||||
if TEST:
|
||||
test_engine()
|
||||
|
||||
if CORRECTNESS:
|
||||
correctness_check_engines()
|
||||
@@ -0,0 +1,24 @@
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
|
||||
-f https://download.pytorch.org/whl/cu113/torch_stable.html
|
||||
torch==1.11.0+cu113
|
||||
transformers==4.18.0
|
||||
onnxruntime-gpu>=1.12
|
||||
argparse
|
||||
--extra-index-url https://pypi.ngc.nvidia.com
|
||||
polygraphy
|
||||
@@ -0,0 +1,6 @@
|
||||
__pycache__/
|
||||
onnx/
|
||||
engine/
|
||||
output/
|
||||
pytorch_model/
|
||||
artifacts_cache/
|
||||
Executable
+538
@@ -0,0 +1,538 @@
|
||||
# Introduction
|
||||
|
||||
This demo application ("demoDiffusion") showcases the acceleration of Stable Diffusion and ControlNet pipeline using TensorRT.
|
||||
|
||||
# Setup
|
||||
|
||||
### Clone the TensorRT OSS repository
|
||||
|
||||
```bash
|
||||
git clone git@github.com:NVIDIA/TensorRT.git -b release/11.0 --single-branch
|
||||
cd TensorRT
|
||||
```
|
||||
|
||||
### Launch NVIDIA pytorch container
|
||||
|
||||
Install nvidia-docker using [these intructions](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/install-guide.html#docker).
|
||||
|
||||
```bash
|
||||
# Create a directory for persistent dependencies
|
||||
mkdir -p deps
|
||||
|
||||
# Launch container with volume mounts
|
||||
docker run --rm -it --gpus all \
|
||||
-v $PWD:/workspace \
|
||||
-v $PWD/deps:/workspace/deps \
|
||||
nvcr.io/nvidia/pytorch:26.03-py3 /bin/bash
|
||||
```
|
||||
|
||||
> **NOTE:** Mounting `/workspace/deps` as a volume ensures dependencies persist across container restarts. After initial installation, subsequent container launches will reuse the installed dependencies.
|
||||
|
||||
NOTE: The demo supports CUDA>=13.0
|
||||
|
||||
### Install the required packages
|
||||
|
||||
This demo uses a family-based dependency management system. Install dependencies for the model families you want to use:
|
||||
|
||||
**Install all dependencies (recommended for first-time users):**
|
||||
```bash
|
||||
python3 setup.py all
|
||||
```
|
||||
|
||||
**Or install specific model families:**
|
||||
```bash
|
||||
# SD family: SD 1.4, SDXL, SD3, SD3.5, SVD (Stable Video Diffusion), Stable Cascade
|
||||
python3 setup.py sd
|
||||
|
||||
# Flux family: Flux.1-dev, Flux.1-schnell, Flux.1-Canny, Flux.1-Depth, Flux.1-Kontext
|
||||
python3 setup.py flux
|
||||
|
||||
# Cosmos family: Cosmos-Predict2 text2image, video2world
|
||||
python3 setup.py cosmos
|
||||
```
|
||||
|
||||
**Additional options:**
|
||||
```bash
|
||||
# Force reinstall even if already installed
|
||||
python3 setup.py all --force
|
||||
|
||||
# Install dependencies to a custom location
|
||||
# Option 1 (recommended): set the env var and install
|
||||
export TENSORRT_DIFFUSION_DEPS_ROOT=/custom/path/deps
|
||||
python3 setup.py all
|
||||
|
||||
# Option 2: install to a path without changing this shell
|
||||
# Remember to export the env var in the environment that runs the demos,
|
||||
# so deps.configure() can find the custom path.
|
||||
python3 setup.py all --deps-root /custom/path/deps
|
||||
# Then, before running any demo scripts:
|
||||
export TENSORRT_DIFFUSION_DEPS_ROOT=/custom/path/deps
|
||||
```
|
||||
|
||||
**Check installation status:**
|
||||
```bash
|
||||
python3 -c "from demo_diffusion import deps; deps.print_status()"
|
||||
```
|
||||
|
||||
Check your installed TensorRT version using:
|
||||
```bash
|
||||
python3 -c 'import tensorrt; print(tensorrt.__version__)'
|
||||
```
|
||||
|
||||
> NOTE: Alternatively, you can download and install TensorRT packages from [NVIDIA TensorRT Developer Zone](https://developer.nvidia.com/tensorrt).
|
||||
|
||||
> NOTE: demoDiffusion has been tested on systems with NVIDIA H100, A100, L40, T4, and RTX4090 GPUs, and the following software configuration.
|
||||
|
||||
|
||||
# Running demoDiffusion
|
||||
|
||||
### Review usage instructions for the supported pipelines
|
||||
|
||||
```bash
|
||||
python3 demo_txt2img.py --help
|
||||
python3 demo_img2img.py --help
|
||||
python3 demo_controlnet.py --help
|
||||
python3 demo_txt2img_xl.py --help
|
||||
python3 demo_txt2img_flux.py --help
|
||||
python3 demo_txt2vid_wan.py --help
|
||||
```
|
||||
|
||||
### HuggingFace user access token
|
||||
|
||||
To download model checkpoints for the Stable Diffusion pipelines, obtain a `read` access token to HuggingFace Hub. See [instructions](https://huggingface.co/docs/hub/security-tokens).
|
||||
|
||||
```bash
|
||||
export HF_TOKEN=<your access token>
|
||||
```
|
||||
|
||||
### Generate an image guided by a text prompt
|
||||
|
||||
```bash
|
||||
python3 demo_txt2img.py "a beautiful photograph of Mt. Fuji during cherry blossom" --hf-token=$HF_TOKEN
|
||||
```
|
||||
|
||||
### Faster Text-to-image using SD1.4 INT8 & FP8 quantization using ModelOpt
|
||||
|
||||
Run the below command to generate an image with SD1.4 in INT8
|
||||
|
||||
```bash
|
||||
python3 demo_txt2img.py "a beautiful photograph of Mt. Fuji during cherry blossom" --hf-token=$HF_TOKEN --int8
|
||||
```
|
||||
|
||||
Run the below command to generate an image with SD1.4 in FP8. (FP8 is only supported on Hopper and Ada.)
|
||||
|
||||
```bash
|
||||
python3 demo_txt2img.py "a beautiful photograph of Mt. Fuji during cherry blossom" --hf-token=$HF_TOKEN --fp8
|
||||
```
|
||||
|
||||
### Generate an image guided by an initial image and a text prompt
|
||||
|
||||
```bash
|
||||
wget https://raw.githubusercontent.com/CompVis/stable-diffusion/main/assets/stable-samples/img2img/sketch-mountains-input.jpg -O sketch-mountains-input.jpg
|
||||
|
||||
python3 demo_img2img.py "A fantasy landscape, trending on artstation" --hf-token=$HF_TOKEN --input-image=sketch-mountains-input.jpg
|
||||
```
|
||||
|
||||
### Generate an image with ControlNet guided by image(s) and text prompt(s)
|
||||
|
||||
```bash
|
||||
python3 demo_controlnet.py "Stormtrooper's lecture in beautiful lecture hall" --controlnet-type depth --hf-token=$HF_TOKEN --denoising-steps 20 --onnx-dir=onnx-cnet-depth --engine-dir=engine-cnet-depth
|
||||
```
|
||||
|
||||
> NOTE: `--input-image` must be a pre-processed image corresponding to `--controlnet-type`. If unspecified, a sample image will be downloaded. Supported controlnet types include: `canny`, `depth`, `hed`, `mlsd`, `normal`, `openpose`, `scribble`, and `seg`.
|
||||
|
||||
Examples:
|
||||
<img src="https://drive.google.com/uc?export=view&id=17ub3MVSQHp26ty-wioNX6iQQ-nAveYSV" alt= “” width="800" height="400">
|
||||
|
||||
#### Combining multiple conditionings
|
||||
|
||||
Multiple ControlNet types can also be specified to combine the conditionings. While specifying multiple conditionings, controlnet scales should also be provided. The scales signify the importance of each conditioning in relation with the other. For example, to condition using `openpose` and `canny` with scales of 1.0 and 0.8 respectively, the arguments provided would be `--controlnet-type openpose canny` and `--controlnet-scale 1.0 0.8`. Note that the number of controlnet scales provided should match the number of controlnet types.
|
||||
|
||||
### Generate an image with Stable Diffusion XL guided by a single text prompt
|
||||
|
||||
> **NOTE:** SDXL and later Stable Diffusion models require sd dependencies to be installed. Install with: `python3 setup.py sd`
|
||||
|
||||
Run the below command to generate an image with Stable Diffusion XL
|
||||
|
||||
```bash
|
||||
python3 demo_txt2img_xl.py "a photo of an astronaut riding a horse on mars" --hf-token=$HF_TOKEN --version=xl-1.0
|
||||
```
|
||||
|
||||
The optional refiner model may be enabled by specifying `--enable-refiner` and separate directories for storing refiner onnx and engine files using `--onnx-refiner-dir` and `--engine-refiner-dir` respectively.
|
||||
|
||||
```bash
|
||||
python3 demo_txt2img_xl.py "a photo of an astronaut riding a horse on mars" --hf-token=$HF_TOKEN --version=xl-1.0 --enable-refiner --onnx-refiner-dir=onnx-refiner --engine-refiner-dir=engine-refiner
|
||||
```
|
||||
|
||||
### Generate an image with Stable Diffusion XL with ControlNet guided by an image and a text prompt
|
||||
|
||||
```bash
|
||||
python3 demo_controlnet.py "A beautiful bird with rainbow colors" --controlnet-type canny --hf-token=$HF_TOKEN --denoising-steps 20 --onnx-dir=onnx-cnet --engine-dir=engine-cnet --version xl-1.0
|
||||
```
|
||||
|
||||
> NOTE: Currently only `--controlnet-type canny` is supported. `--input-image` must be a pre-processed image corresponding to `--controlnet-type canny`. If unspecified, a sample image will be downloaded.
|
||||
|
||||
> NOTE: FP8 quantization (`--fp8`) is supported.
|
||||
|
||||
### Generate an image guided by a text prompt, and using specified LoRA model weight updates
|
||||
|
||||
```bash
|
||||
# FP16
|
||||
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
|
||||
|
||||
# FP8
|
||||
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 --fp8
|
||||
```
|
||||
|
||||
### Faster Text-to-image using SDXL INT8 & FP8 quantization using ModelOpt
|
||||
|
||||
Run the below command to generate an image with Stable Diffusion XL in INT8
|
||||
|
||||
```bash
|
||||
python3 demo_txt2img_xl.py "a photo of an astronaut riding a horse on mars" --version xl-1.0 --onnx-dir onnx-sdxl --engine-dir engine-sdxl --int8
|
||||
```
|
||||
|
||||
Run the below command to generate an image with Stable Diffusion XL in FP8. (FP8 is only supported on Hopper and Ada.)
|
||||
|
||||
```bash
|
||||
python3 demo_txt2img_xl.py "a photo of an astronaut riding a horse on mars" --version xl-1.0 --onnx-dir onnx-sdxl --engine-dir engine-sdxl --fp8
|
||||
```
|
||||
|
||||
> Note that INT8 & FP8 quantization is only supported for SDXL, and won't work with LoRA weights. FP8 quantization is only supported on Hopper and Ada. Some prompts may produce better inputs with fewer denoising steps (e.g. `--denoising-steps 20`) but this will repeat the calibration, ONNX export, and engine building processes for the U-Net.
|
||||
|
||||
For step-by-step tutorials to run INT8 & FP8 inference on stable diffusion models, please refer to examples in [TensorRT ModelOpt diffusers sample](https://github.com/NVIDIA/TensorRT-Model-Optimizer/tree/main/diffusers).
|
||||
|
||||
### Faster Text-to-Image using SDXL Turbo
|
||||
|
||||
Produce coherent images in just 1 step. Note: SDXL Turbo works best for 512x512 resolution, EulerA scheduler and classifier-free-guidance disabled.
|
||||
|
||||
```bash
|
||||
python3 demo_txt2img_xl.py "Einstein" --version xl-turbo --onnx-dir onnx-sdxl-turbo --engine-dir engine-sdxl-turbo --denoising-steps 1 --scheduler EulerA --guidance-scale 0.0 --width 512 --height 512
|
||||
```
|
||||
|
||||
### Generate an image guided by a text prompt using Stable Diffusion 3 and its variants
|
||||
|
||||
Run the command below to generate an image using Stable Diffusion 3 and Stable Diffusion 3.5
|
||||
|
||||
```bash
|
||||
# Stable Diffusion 3
|
||||
python3 demo_txt2img_sd3.py "A vibrant street wall covered in colorful graffiti, the centerpiece spells \"SD3 MEDIUM\", in a storm of colors" --version sd3 --hf-token=$HF_TOKEN
|
||||
|
||||
# Stable Diffusion 3.5-medium
|
||||
python3 demo_txt2img_sd35.py "a beautiful photograph of Mt. Fuji during cherry blossom" --version=3.5-medium --denoising-steps=30 --guidance-scale 3.5 --hf-token=$HF_TOKEN --bf16 --download-onnx-models
|
||||
|
||||
# Stable Diffusion 3.5-large
|
||||
python3 demo_txt2img_sd35.py "a beautiful photograph of Mt. Fuji during cherry blossom" --version=3.5-large --denoising-steps=30 --guidance-scale 3.5 --hf-token=$HF_TOKEN --bf16 --download-onnx-models
|
||||
|
||||
# Stable Diffusion 3.5-large FP8
|
||||
python3 demo_txt2img_sd35.py "a beautiful photograph of Mt. Fuji during cherry blossom" --version=3.5-large --denoising-steps=30 --guidance-scale 3.5 --hf-token=$HF_TOKEN --fp8 --download-onnx-models --onnx-dir onnx_35_fp8/ --engine-dir engine_35_fp8/
|
||||
```
|
||||
|
||||
You can also specify an input image conditioning as shown below
|
||||
|
||||
```bash
|
||||
wget https://raw.githubusercontent.com/CompVis/latent-diffusion/main/data/inpainting_examples/overture-creations-5sI6fQgYIuo.png -O dog-on-bench.png
|
||||
|
||||
# Stable Diffusion 3
|
||||
python3 demo_txt2img_sd3.py "dog wearing a sweater and a blue collar" --version sd3 --input-image dog-on-bench.png --hf-token=$HF_TOKEN
|
||||
```
|
||||
|
||||
Note that a denosing-percentage is applied to the number of denoising-steps when an input image conditioning is provided. Its default value is set to 0.6. This parameter can be updated using `--denoising-percentage`
|
||||
|
||||
### Generate an image with Stable Diffusion v3.5-large with ControlNet guided by an image and a text prompt
|
||||
|
||||
```bash
|
||||
# Depth BF16
|
||||
python3 demo_controlnet_sd35.py "a photo of a man" --controlnet-type depth --hf-token=$HF_TOKEN --denoising-steps 40 --guidance-scale 4.5 --bf16 --download-onnx-models --low-vram
|
||||
|
||||
# Depth FP8
|
||||
python3 demo_controlnet_sd35.py "a photo of a man" --version=3.5-large --fp8 --controlnet-type depth --download-onnx-models --denoising-steps=40 --guidance-scale 4.5 --hf-token=$HF_TOKEN --low-vram
|
||||
|
||||
# Canny BF16
|
||||
python3 demo_controlnet_sd35.py "A Night time photo taken by Leica M11, portrait of a Japanese woman in a kimono, looking at the camera, Cherry blossoms" --controlnet-type canny --hf-token=$HF_TOKEN --denoising-steps 60 --guidance-scale 3.5 --bf16 --download-onnx-models --low-vram
|
||||
|
||||
# Canny FP8
|
||||
python3 demo_controlnet_sd35.py "A Night time photo taken by Leica M11, portrait of a Japanese woman in a kimono, looking at the camera, Cherry blossoms" --version=3.5-large --fp8 --controlnet-type canny --hf-token=$HF_TOKEN --denoising-steps 60 --guidance-scale 3.5 --low-vram --download-onnx-models
|
||||
|
||||
# Blur
|
||||
python3 demo_controlnet_sd35.py "generated ai art, a tiny, lost rubber ducky in an action shot close-up, surfing the humongous waves, inside the tube, in the style of Kelly Slater" --controlnet-type blur --hf-token=$HF_TOKEN --denoising-steps 60 --guidance-scale 3.5 --bf16 --download-onnx-models --low-vram
|
||||
```
|
||||
|
||||
### Generate a video guided by an initial image using Stable Video Diffusion
|
||||
|
||||
Download the pre-exported ONNX model
|
||||
|
||||
```bash
|
||||
pip install -U "huggingface_hub[cli]"
|
||||
hf download stabilityai/stable-video-diffusion-img2vid-xt-1-1-tensorrt --local-dir onnx-svd-xt-1-1
|
||||
```
|
||||
|
||||
SVD-XT-1.1 (25 frames at resolution 576x1024)
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
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
|
||||
```
|
||||
|
||||
NOTE: The min and max guidance scales are configured using --min-guidance-scale and --max-guidance-scale respectively.
|
||||
|
||||
### Generate an image guided by a text prompt using Stable Cascade
|
||||
|
||||
Run the below command to generate an image using Stable Cascade
|
||||
|
||||
```bash
|
||||
python3 demo_stable_cascade.py --onnx-opset=16 "Anthropomorphic cat dressed as a pilot" --onnx-dir onnx-sc --engine-dir engine-sc
|
||||
```
|
||||
|
||||
The lite versions of the models are also supported using the command below
|
||||
|
||||
```bash
|
||||
python3 demo_stable_cascade.py --onnx-opset=16 "Anthropomorphic cat dressed as a pilot" --onnx-dir onnx-sc-lite --engine-dir engine-sc-lite --lite
|
||||
```
|
||||
|
||||
> NOTE: The pipeline is only enabled for the BF16 model weights
|
||||
|
||||
> NOTE: The pipeline only supports ONNX export using Opset 16.
|
||||
|
||||
> NOTE: The denoising steps and guidance scale for the Prior and Decoder models are configured using --prior-denoising-steps, --prior-guidance-scale, --decoder-denoising-steps, and --decoder-guidance-scale respectively.
|
||||
|
||||
### Generating Images with Flux
|
||||
|
||||
> **NOTE:** Flux models require Flux family dependencies. Install with: `python3 setup.py flux`
|
||||
|
||||
#### 1. Generate an Image from a Text Prompt
|
||||
|
||||
##### Run Flux.1-Dev
|
||||
|
||||
NOTE: Pass `--download-onnx-models` to avoid native ONNX export and download the ONNX models from [Black Forest Labs' collection](https://huggingface.co/collections/black-forest-labs/flux1-onnx-679d06b7579583bd84c8ef83). It is only supported for BF16, FP8, and FP4 pipelines.
|
||||
|
||||
```bash
|
||||
# FP16 (requires >48GB VRAM for native export)
|
||||
python3 demo_txt2img_flux.py "a beautiful photograph of Mt. Fuji during cherry blossom" --hf-token=$HF_TOKEN
|
||||
|
||||
# BF16
|
||||
python3 demo_txt2img_flux.py "a beautiful photograph of Mt. Fuji during cherry blossom" --hf-token=$HF_TOKEN --bf16 --download-onnx-models
|
||||
|
||||
# FP8
|
||||
python3 demo_txt2img_flux.py "a beautiful photograph of Mt. Fuji during cherry blossom" --hf-token=$HF_TOKEN --quantization-level 4 --fp8 --download-onnx-models
|
||||
|
||||
# FP4
|
||||
python3 demo_txt2img_flux.py "a beautiful photograph of Mt. Fuji during cherry blossom" --hf-token=$HF_TOKEN --fp4 --download-onnx-models
|
||||
```
|
||||
|
||||
##### Run Flux.1-Schnell
|
||||
|
||||
```bash
|
||||
# FP16 (requires >48GB VRAM for native export)
|
||||
python3 demo_txt2img_flux.py "a beautiful photograph of Mt. Fuji during cherry blossom" --hf-token=$HF_TOKEN --version="flux.1-schnell"
|
||||
|
||||
# BF16
|
||||
python3 demo_txt2img_flux.py "a beautiful photograph of Mt. Fuji during cherry blossom" --hf-token=$HF_TOKEN --version="flux.1-schnell" --bf16 --download-onnx-models
|
||||
|
||||
# FP8
|
||||
python3 demo_txt2img_flux.py "a beautiful photograph of Mt. Fuji during cherry blossom" --hf-token=$HF_TOKEN --version="flux.1-schnell" --quantization-level 4 --fp8 --download-onnx-models
|
||||
|
||||
# FP4
|
||||
python3 demo_txt2img_flux.py "a beautiful photograph of Mt. Fuji during cherry blossom" --hf-token=$HF_TOKEN --version="flux.1-schnell" --fp4 --download-onnx-models
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### 2. Generate an Image from an Initial Image + Text Prompt
|
||||
|
||||
Download an example input image:
|
||||
|
||||
```bash
|
||||
wget "https://miro.medium.com/v2/resize:fit:640/format:webp/1*iD8mUonHMgnlP0qrSx3qPg.png" -O yellow.png
|
||||
```
|
||||
|
||||
Run the image-to-image pipeline:
|
||||
|
||||
```bash
|
||||
python3 demo_img2img_flux.py "A home with 2 floors and windows. The front door is purple" --hf-token=$HF_TOKEN --input-image yellow.png --image-strength 0.95 --bf16 --onnx-dir onnx-flux-dev/bf16 --engine-dir engine-flux-dev/
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### 3. Generate an Image Using Flux ControlNet
|
||||
|
||||
##### Download the Control Image
|
||||
|
||||
```bash
|
||||
wget https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/robot.png
|
||||
```
|
||||
|
||||
##### Calibration Data for native ONNX export (FP8 Pipeline)
|
||||
|
||||
FP8 ControlNet pipelines require downloading a calibration dataset and providing the path. You can use the datasets provided by Black Forest Labs here: [depth](https://drive.google.com/file/d/1DFfhOSrTlKfvBFLcD2vAALwwH4jSGdGk/view) | [canny](https://drive.google.com/file/d/1dRoxOL-vy3tSAesyqBSJoUWsbkMwv3en/view)
|
||||
|
||||
You can use the `--calibraton-dataset` flag to specify the path, which is set to `./{depth/canny}-eval/benchmark` by default if not provided. Note that the dataset should have `inputs/` and `prompts/` underneath the provided path, matching the format of the BFL dataset.
|
||||
|
||||
##### Depth ControlNet
|
||||
|
||||
```bash
|
||||
# BF16
|
||||
python3 demo_img2img_flux.py "A robot made of exotic candies and chocolates of different kinds. The background is filled with confetti and celebratory gifts." --version="flux.1-dev-depth" --hf-token=$HF_TOKEN --guidance-scale 10 --control-image robot.png --bf16 --denoising-steps 30 --download-onnx-models
|
||||
|
||||
# FP8 using pre-exported ONNX models
|
||||
python3 demo_img2img_flux.py "A robot made of exotic candies" --version="flux.1-dev-depth" --hf-token=$HF_TOKEN --guidance-scale 10 --control-image robot.png --fp8 --denoising-steps 30 --download-onnx-models --build-static-batch --quantization-level 4
|
||||
|
||||
# FP8 using native ONNX export
|
||||
rm -rf onnx/* engine/* && python3 demo_img2img_flux.py "A robot made of exotic candies" --version="flux.1-dev-depth" --hf-token=$HF_TOKEN --guidance-scale 10 --control-image robot.png --quantization-level 4 --fp8 --denoising-steps 30
|
||||
|
||||
# FP4
|
||||
python3 demo_img2img_flux.py "A robot made of exotic candies" --version="flux.1-dev-depth" --hf-token=$HF_TOKEN --guidance-scale 10 --control-image robot.png --fp4 --denoising-steps 30 --download-onnx-models --build-static-batch
|
||||
```
|
||||
|
||||
##### Canny ControlNet
|
||||
|
||||
```bash
|
||||
# BF16
|
||||
python3 demo_img2img_flux.py "a robot made out of gold" --version="flux.1-dev-canny" --hf-token=$HF_TOKEN --guidance-scale 30 --control-image robot.png --bf16 --denoising-steps 30 --download-onnx-models
|
||||
|
||||
# FP8 using pre-exported ONNX models
|
||||
python3 demo_img2img_flux.py "a robot made out of gold" --version="flux.1-dev-canny" --hf-token=$HF_TOKEN --guidance-scale 30 --control-image robot.png --fp8 --denoising-steps 30 --download-onnx-models --build-static-batch --quantization-level 4
|
||||
|
||||
# FP8 using native ONNX export
|
||||
rm -rf onnx/* engine/* && python3 demo_img2img_flux.py "a robot made out of gold" --version="flux.1-dev-canny" --hf-token=$HF_TOKEN --guidance-scale 30 --control-image robot.png --quantization-level 4 --fp8 --denoising-steps 30 --calibration-dataset {custom/dataset/path}
|
||||
|
||||
# FP4
|
||||
python3 demo_img2img_flux.py "a robot made out of gold" --version="flux.1-dev-canny" --hf-token=$HF_TOKEN --guidance-scale 30 --control-image robot.png --fp4 --denoising-steps 30 --download-onnx-models --build-static-batch
|
||||
```
|
||||
|
||||
#### 4. Generate an Image Using Flux LoRA
|
||||
|
||||
FLUX supports loading LoRA for Flux.1-Dev and Flux.1-Schnell. Make sure the target lora is compatible with the transformer model. Below is an example of using a [water color Flux LoRA](https://huggingface.co/SebastianBodza/flux_lora_aquarel_watercolor)
|
||||
|
||||
```bash
|
||||
# FP16
|
||||
python3 demo_txt2img_flux.py "A painting of a barista creating an intricate latte art design, with the 'Coffee Creations' logo skillfully formed within the latte foam. In a watercolor style, AQUACOLTOK. White background." --hf-token=$HF_TOKEN --lora-path "SebastianBodza/flux_lora_aquarel_watercolor" --lora-weight 1.0 --onnx-dir=onnx-flux-lora --engine-dir=engine-flux-lora
|
||||
|
||||
# FP8
|
||||
python3 demo_txt2img_flux.py "A painting of a barista creating an intricate latte art design, with the 'Coffee Creations' logo skillfully formed within the latte foam. In a watercolor style, AQUACOLTOK. White background." --hf-token=$HF_TOKEN --lora-path "SebastianBodza/flux_lora_aquarel_watercolor" --lora-weight 1.0 --onnx-dir=onnx-flux-lora --engine-dir=engine-flux-lora --fp8
|
||||
```
|
||||
|
||||
#### 5. Edit an Image using Flux Kontext
|
||||
|
||||
```bash
|
||||
wget https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/diffusers/cat.png
|
||||
|
||||
# BF16
|
||||
python3 demo_img2img_flux.py "Add a hat to the cat" --version="flux.1-kontext-dev" --hf-token=$HF_TOKEN --guidance-scale 2.5 --kontext-image cat.png --denoising-steps 28 --bf16 --onnx-dir onnx-kontext --engine-dir engine-kontext --download-onnx-models
|
||||
|
||||
# FP8
|
||||
python3 demo_img2img_flux.py "Add a hat to the cat" --version="flux.1-kontext-dev" --hf-token=$HF_TOKEN --guidance-scale 2.5 --kontext-image cat.png --denoising-steps 28 --fp8 --onnx-dir onnx-kontext-fp8 --engine-dir engine-kontext-fp8 --download-onnx-models --quantization-level 4
|
||||
|
||||
# FP4
|
||||
python3 demo_img2img_flux.py "Add a hat to the cat" --version="flux.1-kontext-dev" --hf-token=$HF_TOKEN --guidance-scale 2.5 --kontext-image cat.png --denoising-steps 28 --fp4 --onnx-dir onnx-kontext-fp4 --engine-dir engine-kontext-fp4 --download-onnx-models
|
||||
```
|
||||
---
|
||||
|
||||
#### 5. Export ONNX Models Only (Skip Inference)
|
||||
|
||||
Use the `--onnx-export-only` flag to export ONNX models on a higher-VRAM device. The exported ONNX models can be used on a device with lower VRAM for the engine build and inference steps.
|
||||
|
||||
```bash
|
||||
python3 demo_txt2img_flux.py "a beautiful photograph of Mt. Fuji during cherry blossom" --hf-token=$HF_TOKEN --onnx-export-only
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### 6. Running Flux on GPUs with Limited Memory
|
||||
|
||||
##### Optimization Flags
|
||||
|
||||
- `--low-vram`: Enables model-offloading for reduced VRAM usage.
|
||||
- `--ws`: Enables weight streaming in TensorRT engines.
|
||||
- `--t5-ws-percentage` and `--transformer-ws-percentage`: Set runtime weight streaming budgets.
|
||||
- `--build-static-batch`: Build all engines using static batch sizes to lower the required activation memory. This will limit supported batch size of these engines for inference to the value specified by `--batch-size`.
|
||||
|
||||
##### FLUX VRAM Requirements Table
|
||||
|
||||
Memory usage captured below excludes the ONNX export step, and assumes use of the `--build-static-batch` flag to reduce activation VRAM usage. Users can either use [pre-exported ONNX models](README.md#download-pre-exported-models-recommended-for-48gb-vram) or export the models separately on a higher-VRAM device using [--onnx-export-only](README.md#4-export-onnx-models-only-skip-inference).
|
||||
|
||||
| Precision | Default VRAM Usage | With `--low-vram` |
|
||||
| --------- | ------------------ | ----------------- |
|
||||
| FP16 | 39.3 GB | 23.9 GB |
|
||||
| BF16 | 35.7 GB | 23.9 GB |
|
||||
| FP8 | 24.6 GB | 14.9 GB |
|
||||
| FP4 | 21.67 GB | 11.1 GB |
|
||||
|
||||
NOTE: The FP8 and FP4 Pipelines are supported on Hopper/Ada/Blackwell devices only. The FP4 pipeline is most performant on Blackwell devices.
|
||||
|
||||
|
||||
### Run Cosmos2 World Foundation Models
|
||||
|
||||
> **NOTE:** Cosmos models require Cosmos family dependencies. Install with: `python3 setup.py cosmos`
|
||||
|
||||
Select the prompts and export them as below
|
||||
|
||||
```bash
|
||||
export PROMPT="A close-up shot captures a vibrant yellow scrubber vigorously working on a grimy plate, its bristles moving in circular motions to lift stubborn grease and food residue. The dish, once covered in remnants of a hearty meal, gradually reveals its original glossy surface. Suds form and bubble around the scrubber, creating a satisfying visual of cleanliness in progress. The sound of scrubbing fills the air, accompanied by the gentle clinking of the dish against the sink. As the scrubber continues its task, the dish transforms, gleaming under the bright kitchen lights, symbolizing the triumph of cleanliness over mess."
|
||||
|
||||
export NEGATIVE_PROMPT="The video captures a series of frames showing ugly scenes, static with no motion, motion blur, over-saturation, shaky footage, low resolution, grainy texture, pixelated images, poorly lit areas, underexposed and overexposed scenes, poor color balance, washed out colors, choppy sequences, jerky movements, low frame rate, artifacting, color banding, unnatural transitions, outdated special effects, fake elements, unconvincing visuals, poorly edited content, jump cuts, visual noise, and flickering. Overall, the video is of poor quality."
|
||||
```
|
||||
|
||||
#### 1. Generate an Image from a Text Prompt
|
||||
|
||||
##### Run Cosmos-Predict2-2B-Text2Image
|
||||
|
||||
```bash
|
||||
# BF16
|
||||
python3 demo_txt2image_cosmos.py "$PROMPT" --negative-prompt="$NEGATIVE_PROMPT" --hf-token=$HF_TOKEN
|
||||
```
|
||||
|
||||
#### 2. Generate a Video guided by an Initial Video Conditioning and a Text Prompt
|
||||
|
||||
##### Run Cosmos-Predict2-2B-Video2World (only PyTorch backend enabled)
|
||||
|
||||
```bash
|
||||
# BF16
|
||||
python3 demo_vid2world_cosmos.py "$PROMPT" --negative-prompt="$NEGATIVE_PROMPT" --hf-token=$HF_TOKEN
|
||||
```
|
||||
|
||||
|
||||
### Specify Custom Paths for ONNX models and TensorRT engines (FLUX, Stable Diffusion 3.5 and Cosmos only)
|
||||
|
||||
Custom override paths to pre-exported ONNX model files can be provided using `--custom-onnx-paths`. These ONNX models are directly used to build TRT engines without further optimization on the ONNX graphs. Paths should be a comma-separated list of <model_name>:<path> pairs. For example: `--custom-onnx-paths=transformer:/path/to/transformer.onnx,vae:/path/to/vae.onnx`. Call <PipelineClass>.get_model_names(...) for the list of supported model names.
|
||||
|
||||
Custom override paths to pre-built engine files can be provided using `--custom-engine-paths`. Paths should be a comma-separated list of <model_name>:<path> pairs. For example: `--custom-onnx-paths=transformer:/path/to/transformer.plan,vae:/path/to/vae.plan`.
|
||||
|
||||
### Generate a video from a text prompt using Wan
|
||||
|
||||
Run the below command to generate 81 frames of video at 720×1280 resolution using Wan 2.2. Due to the high memory requirements of this model, it is recommended to enable `--low-vram` and use a Blackwell device.
|
||||
|
||||
```bash
|
||||
# Default (81 frames, 720x1280) with --low-vram enabled
|
||||
python3 demo_txt2vid_wan.py "A serene bamboo forest with sunlight filtering through the leaves" --hf-token=$HF_TOKEN --low-vram
|
||||
|
||||
# Adjust denoising steps, guidance scales, negative prompt, seed, warmup runs
|
||||
python3 demo_txt2vid_wan.py "Ocean waves crashing on a beach at sunset" --hf-token=$HF_TOKEN --low-vram --denoising-steps 50 --guidance-scale 4.5 --guidance-scale-2 3.5 --negative-prompt "blurry, low quality, static" --seed 42 --num-warmup-runs 0
|
||||
```
|
||||
|
||||
## Configuration options
|
||||
|
||||
- Noise scheduler can be set using `--scheduler <scheduler>`. Note: not all schedulers are available for every version.
|
||||
- To accelerate engine building time use `--timing-cache <path to cache file>`. The cache file will be created if it does not already exist. Note that performance may degrade if cache files are used across multiple GPU targets. It is recommended to use timing caches only during development. To achieve the best perfromance in deployment, please build engines without timing cache.
|
||||
- Specify new directories for storing onnx and engine files when switching between versions, LoRAs, ControlNets, etc. This can be done using `--onnx-dir <new onnx dir>` and `--engine-dir <new 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`.
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 482 KiB |
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,161 @@
|
||||
#
|
||||
# SPDX-FileCopyrightText: Copyright (c) 1993-2022 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.
|
||||
#
|
||||
|
||||
# Configure dependencies before any external imports
|
||||
from demo_diffusion import deps
|
||||
deps.configure("sd")
|
||||
|
||||
import argparse
|
||||
|
||||
import controlnet_aux
|
||||
import torch
|
||||
from cuda.bindings import runtime as cudart
|
||||
from PIL import Image
|
||||
|
||||
from demo_diffusion import dd_argparse
|
||||
from demo_diffusion import image as image_module
|
||||
from demo_diffusion import pipeline as pipeline_module
|
||||
|
||||
|
||||
def parseArgs():
|
||||
parser = argparse.ArgumentParser(description="Options for Stable Diffusion ControlNet Demo", conflict_handler='resolve')
|
||||
parser = dd_argparse.add_arguments(parser)
|
||||
parser.add_argument('--scheduler', type=str, default="UniPC", choices=["DDIM", "DPM", "EulerA", "LMSD", "PNDM", "UniPC"], help="Scheduler for diffusion process")
|
||||
parser.add_argument('--input-image', nargs = '+', type=str, default=[], help="Path to the input image/images already prepared for ControlNet modality. For example: canny edged image for canny ControlNet, not just regular rgb image")
|
||||
parser.add_argument('--controlnet-type', nargs='+', type=str, default=["canny"], help="Controlnet type, can be `None`, `str` or `str` list from ['canny', 'depth', 'hed', 'mlsd', 'normal', 'openpose', 'scribble', 'seg']")
|
||||
parser.add_argument('--controlnet-scale', nargs='+', type=float, default=[1.0], help="The outputs of the controlnet are multiplied by `controlnet_scale` before they are added to the residual in the original unet, can be `None`, `float` or `float` list")
|
||||
return parser.parse_args()
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("[I] Initializing StableDiffusion controlnet demo using TensorRT")
|
||||
args = parseArgs()
|
||||
|
||||
# Controlnet configuration
|
||||
if not isinstance(args.controlnet_type, list):
|
||||
raise ValueError(f"`--controlnet-type` must be of type `str` or `str` list, but is {type(args.controlnet_type)}")
|
||||
|
||||
# Controlnet configuration
|
||||
if not isinstance(args.controlnet_scale, list):
|
||||
raise ValueError(f"`--controlnet-scale`` must be of type `float` or `float` list, but is {type(args.controlnet_scale)}")
|
||||
|
||||
# Check number of ControlNets to ControlNet scales
|
||||
if len(args.controlnet_type) != len(args.controlnet_scale):
|
||||
raise ValueError(f"Numbers of ControlNets {len(args.controlnet_type)} should be equal to number of ControlNet scales {len(args.controlnet_scale)}.")
|
||||
|
||||
# Convert controlnet scales to tensor
|
||||
controlnet_scale = torch.FloatTensor(args.controlnet_scale)
|
||||
|
||||
# Check images
|
||||
input_images = []
|
||||
if len(args.input_image) > 0:
|
||||
for image in args.input_image:
|
||||
input_images.append(Image.open(image))
|
||||
else:
|
||||
for controlnet in args.controlnet_type:
|
||||
if controlnet == "canny":
|
||||
if args.version == "xl-1.0":
|
||||
canny_image = image_module.download_image(
|
||||
"https://huggingface.co/diffusers/controlnet-canny-sdxl-1.0/resolve/main/out_bird.png"
|
||||
)
|
||||
# "out_bird.png" has 5 images combined in a row. We pick the first image which is the input image.
|
||||
canny_image = canny_image.crop((0, 0, canny_image.width / 5, canny_image.height))
|
||||
elif args.version == "1.5":
|
||||
canny_image = image_module.download_image(
|
||||
"https://hf.co/datasets/huggingface/documentation-images/resolve/main/diffusers/input_image_vermeer.png"
|
||||
)
|
||||
canny_image = controlnet_aux.CannyDetector()(canny_image)
|
||||
else:
|
||||
raise ValueError(
|
||||
f"This demo supports ControlNets for v1.4 and SDXL base pipelines only. Version provided: {args.version}"
|
||||
)
|
||||
input_images.append(canny_image.resize((args.width, args.height)))
|
||||
elif controlnet == "normal":
|
||||
normal_image = image_module.download_image(
|
||||
"https://huggingface.co/lllyasviel/sd-controlnet-normal/resolve/main/images/toy.png"
|
||||
)
|
||||
normal_image = controlnet_aux.NormalBaeDetector.from_pretrained("lllyasviel/Annotators")(normal_image)
|
||||
input_images.append(normal_image.resize((args.width, args.height)))
|
||||
elif controlnet == "depth":
|
||||
depth_image = image_module.download_image(
|
||||
"https://huggingface.co/lllyasviel/sd-controlnet-depth/resolve/main/images/stormtrooper.png"
|
||||
)
|
||||
depth_image = controlnet_aux.LeresDetector.from_pretrained("lllyasviel/Annotators")(depth_image)
|
||||
input_images.append(depth_image.resize((args.width, args.height)))
|
||||
elif controlnet == "hed":
|
||||
hed_image = image_module.download_image(
|
||||
"https://huggingface.co/lllyasviel/sd-controlnet-hed/resolve/main/images/man.png"
|
||||
)
|
||||
hed_image = controlnet_aux.HEDdetector.from_pretrained("lllyasviel/Annotators")(hed_image)
|
||||
input_images.append(hed_image.resize((args.width, args.height)))
|
||||
elif controlnet == "mlsd":
|
||||
mlsd_image = image_module.download_image(
|
||||
"https://huggingface.co/lllyasviel/sd-controlnet-mlsd/resolve/main/images/room.png"
|
||||
)
|
||||
mlsd_image = controlnet_aux.MLSDdetector.from_pretrained("lllyasviel/Annotators")(mlsd_image)
|
||||
input_images.append(mlsd_image.resize((args.width, args.height)))
|
||||
elif controlnet == "openpose":
|
||||
openpose_image = image_module.download_image(
|
||||
"https://huggingface.co/lllyasviel/sd-controlnet-openpose/resolve/main/images/pose.png"
|
||||
)
|
||||
openpose_image = controlnet_aux.OpenposeDetector.from_pretrained("lllyasviel/Annotators")(openpose_image)
|
||||
input_images.append(openpose_image.resize((args.width, args.height)))
|
||||
elif controlnet == "scribble":
|
||||
scribble_image = image_module.download_image(
|
||||
"https://huggingface.co/lllyasviel/sd-controlnet-scribble/resolve/main/images/bag.png"
|
||||
)
|
||||
scribble_image = controlnet_aux.HEDdetector.from_pretrained("lllyasviel/Annotators")(scribble_image, scribble=True)
|
||||
input_images.append(scribble_image.resize((args.width, args.height)))
|
||||
elif controlnet == "seg":
|
||||
seg_image = image_module.download_image(
|
||||
"https://huggingface.co/lllyasviel/sd-controlnet-seg/resolve/main/images/house.png"
|
||||
)
|
||||
seg_image = controlnet_aux.SamDetector.from_pretrained("ybelkada/segment-anything", subfolder="checkpoints")(seg_image)
|
||||
input_images.append(seg_image.resize((args.width, args.height)))
|
||||
else:
|
||||
raise ValueError(f"You should implement the conditonal image of this controlnet: {controlnet}")
|
||||
assert len(input_images) > 0
|
||||
|
||||
kwargs_init_pipeline, kwargs_load_engine, args_run_demo = dd_argparse.process_pipeline_args(args)
|
||||
|
||||
# Initialize demo
|
||||
demo = pipeline_module.StableDiffusionPipeline(
|
||||
pipeline_type=(
|
||||
pipeline_module.PIPELINE_TYPE.CONTROLNET
|
||||
if args.version != "xl-1.0"
|
||||
else pipeline_module.PIPELINE_TYPE.XL_CONTROLNET
|
||||
),
|
||||
controlnets=args.controlnet_type,
|
||||
**kwargs_init_pipeline,
|
||||
)
|
||||
|
||||
# Load TensorRT engines and pytorch modules
|
||||
demo.loadEngines(
|
||||
args.engine_dir,
|
||||
args.framework_model_dir,
|
||||
args.onnx_dir,
|
||||
**kwargs_load_engine)
|
||||
|
||||
# Load resources
|
||||
_, shared_device_memory = cudart.cudaMalloc(demo.calculateMaxDeviceMemory())
|
||||
demo.activateEngines(shared_device_memory)
|
||||
demo.loadResources(args.height, args.width, args.batch_size, args.seed)
|
||||
|
||||
# Run inference
|
||||
demo_kwargs = {'input_image': input_images, 'controlnet_scales': controlnet_scale}
|
||||
demo.run(*args_run_demo, **demo_kwargs)
|
||||
|
||||
demo.teardown()
|
||||
@@ -0,0 +1,191 @@
|
||||
#
|
||||
# SPDX-FileCopyrightText: Copyright (c) 1993-2025 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.
|
||||
#
|
||||
|
||||
# Configure dependencies before any external imports
|
||||
from demo_diffusion import deps
|
||||
deps.configure("sd")
|
||||
|
||||
import argparse
|
||||
|
||||
import torch
|
||||
from cuda.bindings import runtime as cudart
|
||||
from PIL import Image
|
||||
|
||||
from demo_diffusion import dd_argparse
|
||||
from demo_diffusion import image as image_module
|
||||
from demo_diffusion import pipeline as pipeline_module
|
||||
|
||||
|
||||
def parseArgs():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Options for Stable Diffusion 3.5-large ControlNet Demo", conflict_handler="resolve"
|
||||
)
|
||||
parser = dd_argparse.add_arguments(parser)
|
||||
parser.add_argument(
|
||||
"--version",
|
||||
type=str,
|
||||
default="3.5-large",
|
||||
choices={"3.5-large"},
|
||||
help="Version of Stable Diffusion 3.5",
|
||||
)
|
||||
parser.add_argument("--height", type=int, default=1024, help="Height of image to generate (must be multiple of 8)")
|
||||
parser.add_argument("--width", type=int, default=1024, help="Height of image to generate (must be multiple of 8)")
|
||||
parser.add_argument(
|
||||
"--max-sequence-length",
|
||||
type=int,
|
||||
default=256,
|
||||
help="Maximum sequence length to use with the prompt.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--control-image",
|
||||
nargs="+",
|
||||
type=str,
|
||||
default=[],
|
||||
help="Path to the input image/images already prepared for ControlNet modality. For example: canny edged image for canny ControlNet, not just regular rgb image",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--controlnet-type",
|
||||
type=str,
|
||||
default="canny",
|
||||
help="Controlnet type (single type only), can be 'canny', 'depth', 'blur', etc.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--controlnet-scale",
|
||||
type=float,
|
||||
default=1.0,
|
||||
help="The outputs of the controlnet are multiplied by `controlnet_scale` before they are added to the residual in the original Transformer",
|
||||
)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def process_demo_args(args):
|
||||
batch_size = args.batch_size
|
||||
prompt = args.prompt
|
||||
negative_prompt = args.negative_prompt
|
||||
# Process prompt
|
||||
if not isinstance(prompt, list):
|
||||
raise ValueError(f"`prompt` must be of type `str` list, but is {type(prompt)}")
|
||||
prompt = prompt * batch_size
|
||||
|
||||
if not isinstance(negative_prompt, list):
|
||||
raise ValueError(f"`--negative-prompt` must be of type `str` list, but is {type(negative_prompt)}")
|
||||
if len(negative_prompt) == 1:
|
||||
negative_prompt = negative_prompt * batch_size
|
||||
|
||||
if args.height % 8 != 0 or args.width % 8 != 0:
|
||||
raise ValueError(
|
||||
f"Image height and width have to be divisible by 8 but specified as: {args.image_height} and {args.width}."
|
||||
)
|
||||
|
||||
max_batch_size = 4
|
||||
if args.batch_size > max_batch_size:
|
||||
raise ValueError(f"Batch size {args.batch_size} is larger than allowed {max_batch_size}.")
|
||||
|
||||
if args.use_cuda_graph and (not args.build_static_batch or args.build_dynamic_shape):
|
||||
raise ValueError(
|
||||
"Using CUDA graph requires static dimensions. Enable `--build-static-batch` and do not specify `--build-dynamic-shape`"
|
||||
)
|
||||
|
||||
# Controlnet configuration
|
||||
if not isinstance(args.controlnet_type, str):
|
||||
raise ValueError(f"`--controlnet-type` must be of type `str`, but is {type(args.controlnet_type)}")
|
||||
|
||||
# Controlnet configuration
|
||||
if not isinstance(args.controlnet_scale, float):
|
||||
raise ValueError(f"`--controlnet-scale` must be of type `float`, but is {type(args.controlnet_scale)}")
|
||||
|
||||
# Convert controlnet scales to tensor
|
||||
controlnet_scale = torch.tensor(args.controlnet_scale)
|
||||
|
||||
# Check images
|
||||
input_images = []
|
||||
if len(args.control_image) > 0:
|
||||
for image in args.control_image:
|
||||
input_images.append(Image.open(image))
|
||||
else:
|
||||
if args.controlnet_type == "canny":
|
||||
canny_image = image_module.download_image(
|
||||
"https://huggingface.co/datasets/diffusers/diffusers-images-docs/resolve/main/canny.png"
|
||||
)
|
||||
input_images.append(canny_image.resize((args.width, args.height)))
|
||||
elif args.controlnet_type == "depth":
|
||||
depth_image = image_module.download_image(
|
||||
"https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/marigold/marigold_einstein_lcm_depth.png"
|
||||
)
|
||||
input_images.append(depth_image.resize((args.width, args.height)))
|
||||
elif args.controlnet_type == "blur":
|
||||
blur_image = image_module.download_image(
|
||||
"https://huggingface.co/datasets/diffusers/diffusers-images-docs/resolve/main/blur.png"
|
||||
)
|
||||
input_images.append(blur_image.resize((args.width, args.height)))
|
||||
else:
|
||||
raise ValueError(f"You should implement the conditonal image of this controlnet: {args.controlnet_type}")
|
||||
assert len(input_images) > 0
|
||||
|
||||
kwargs_run_demo = {
|
||||
"prompt": prompt,
|
||||
"negative_prompt": negative_prompt,
|
||||
"height": args.height,
|
||||
"width": args.width,
|
||||
"control_image": input_images,
|
||||
"controlnet_scale": controlnet_scale,
|
||||
"batch_count": args.batch_count,
|
||||
"num_warmup_runs": args.num_warmup_runs,
|
||||
"use_cuda_graph": args.use_cuda_graph,
|
||||
}
|
||||
|
||||
return kwargs_run_demo
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("[I] Initializing StableDiffusion ControlNet demo using TensorRT")
|
||||
args = parseArgs()
|
||||
|
||||
# Initialize demo
|
||||
_, kwargs_load_engine, _ = dd_argparse.process_pipeline_args(args)
|
||||
kwargs_run_demo = process_demo_args(args)
|
||||
|
||||
# Initialize demo
|
||||
demo = pipeline_module.StableDiffusion35Pipeline.FromArgs(
|
||||
args,
|
||||
pipeline_type=pipeline_module.PIPELINE_TYPE.CONTROLNET,
|
||||
)
|
||||
|
||||
# Load TensorRT engines and pytorch modules
|
||||
demo.load_engines(
|
||||
framework_model_dir=args.framework_model_dir,
|
||||
**kwargs_load_engine,
|
||||
)
|
||||
|
||||
if demo.low_vram:
|
||||
demo.device_memory_sizes = demo.get_device_memory_sizes()
|
||||
else:
|
||||
_, shared_device_memory = cudart.cudaMalloc(demo.calculate_max_device_memory())
|
||||
demo.activate_engines(shared_device_memory)
|
||||
|
||||
# Load resources
|
||||
demo.load_resources(
|
||||
image_height=args.height,
|
||||
image_width=args.width,
|
||||
batch_size=args.batch_size,
|
||||
seed=args.seed,
|
||||
)
|
||||
|
||||
# Run inference
|
||||
demo.run(**kwargs_run_demo)
|
||||
|
||||
demo.teardown()
|
||||
@@ -0,0 +1,465 @@
|
||||
#
|
||||
# SPDX-FileCopyrightText: Copyright (c) 1993-2025 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 __future__ import annotations
|
||||
|
||||
import argparse
|
||||
from typing import Any, Dict, Tuple
|
||||
|
||||
import torch
|
||||
|
||||
# Define valid optimization levels for TensorRT engine build
|
||||
VALID_OPTIMIZATION_LEVELS = list(range(6))
|
||||
|
||||
|
||||
def parse_key_value_pairs(string: str) -> Dict[str, str]:
|
||||
"""Parse a string of comma-separated key-value pairs into a dictionary.
|
||||
|
||||
Args:
|
||||
string (str): A string of comma-separated key-value pairs.
|
||||
|
||||
Returns:
|
||||
Dict[str, str]: Parsed dictionary of key-value pairs.
|
||||
|
||||
Example:
|
||||
>>> parse_key_value_pairs("key1:value1,key2:value2")
|
||||
{"key1": "value1", "key2": "value2"}
|
||||
"""
|
||||
parsed = {}
|
||||
|
||||
for key_value_pair in string.split(","):
|
||||
if not key_value_pair:
|
||||
continue
|
||||
|
||||
key_value_pair = key_value_pair.split(":")
|
||||
if len(key_value_pair) != 2:
|
||||
raise argparse.ArgumentTypeError(f"Invalid key-value pair: {key_value_pair}. Must have length 2.")
|
||||
key, value = key_value_pair
|
||||
parsed[key] = value
|
||||
|
||||
return parsed
|
||||
|
||||
|
||||
def add_arguments(parser):
|
||||
# Stable Diffusion configuration
|
||||
parser.add_argument(
|
||||
"--version",
|
||||
type=str,
|
||||
default="1.4",
|
||||
choices=(
|
||||
"1.4",
|
||||
"dreamshaper-7",
|
||||
"xl-1.0",
|
||||
"xl-turbo",
|
||||
"svd-xt-1.1",
|
||||
"sd3",
|
||||
"3.5-medium",
|
||||
"3.5-large",
|
||||
"cascade",
|
||||
"flux.1-dev",
|
||||
"flux.1-schnell",
|
||||
"flux.1-dev-canny",
|
||||
"flux.1-dev-depth",
|
||||
"flux.1-kontext-dev",
|
||||
"cosmos-predict2-2b-text2image",
|
||||
"cosmos-predict2-14b-text2image",
|
||||
"cosmos-predict2-2b-video2world",
|
||||
"cosmos-predict2-14b-video2world",
|
||||
),
|
||||
help="Version of Stable Diffusion",
|
||||
)
|
||||
parser.add_argument("prompt", nargs="*", help="Text prompt(s) to guide image generation")
|
||||
parser.add_argument(
|
||||
"--negative-prompt", nargs="*", default=[""], help="The negative prompt(s) to guide the image generation."
|
||||
)
|
||||
parser.add_argument("--batch-size", type=int, default=1, choices=[1, 2, 4], help="Batch size (repeat prompt)")
|
||||
parser.add_argument(
|
||||
"--batch-count", type=int, default=1, help="Number of images to generate in sequence, one at a time."
|
||||
)
|
||||
parser.add_argument("--height", type=int, default=512, help="Height of image to generate (must be multiple of 8)")
|
||||
parser.add_argument("--width", type=int, default=512, help="Height of image to generate (must be multiple of 8)")
|
||||
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,
|
||||
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'",
|
||||
)
|
||||
parser.add_argument("--bf16", action="store_true", help="Run pipeline in BFloat16 precision")
|
||||
|
||||
# ONNX export
|
||||
parser.add_argument(
|
||||
"--onnx-opset",
|
||||
type=int,
|
||||
default=19,
|
||||
choices=range(7, 24),
|
||||
help="Select ONNX opset version to target for exported models",
|
||||
)
|
||||
parser.add_argument("--onnx-dir", default="onnx", help="Output directory for ONNX export")
|
||||
parser.add_argument(
|
||||
"--custom-onnx-paths",
|
||||
type=parse_key_value_pairs,
|
||||
help=(
|
||||
"[FLUX, Stable Diffusion 3.5-large, Cosmos only] Custom override paths to pre-exported ONNX model files. These ONNX models are directly used to "
|
||||
"build TRT engines without further optimization on the ONNX graphs. Paths should be a comma-separated list "
|
||||
"of <model_name>:<path> pairs. For example: "
|
||||
"--custom-onnx-paths=transformer:/path/to/transformer.onnx,vae:/path/to/vae.onnx. Call "
|
||||
"<PipelineClass>.get_model_names(...) for the list of supported model names."
|
||||
),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--onnx-export-only",
|
||||
action="store_true",
|
||||
help="If set, only performs the export of models to ONNX, skipping engine build and inference.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--download-onnx-models",
|
||||
action="store_true",
|
||||
help=("[FLUX and Stable Diffusion 3.5-large only] Download pre-exported ONNX models"),
|
||||
)
|
||||
|
||||
# Framework model ckpt
|
||||
parser.add_argument("--framework-model-dir", default="pytorch_model", help="Directory for HF saved models")
|
||||
|
||||
# TensorRT engine build
|
||||
parser.add_argument("--engine-dir", default="engine", help="Output directory for TensorRT engines")
|
||||
parser.add_argument(
|
||||
"--custom-engine-paths",
|
||||
type=parse_key_value_pairs,
|
||||
help=(
|
||||
"[FLUX only] Custom override paths to pre-built engine files. Paths should be a comma-separated list of "
|
||||
"<model_name>:<path> pairs. For example: "
|
||||
"--custom-onnx-paths=transformer:/path/to/transformer.plan,vae:/path/to/vae.plan. Call "
|
||||
"<PipelineClass>.get_model_names(...) for the list of supported model names."
|
||||
),
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--optimization-level",
|
||||
type=int,
|
||||
default=None,
|
||||
help=f"Set the builder optimization level to build the engine with. A higher level allows TensorRT to spend more building time for more optimization options. Must be one of {VALID_OPTIMIZATION_LEVELS}.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--build-static-batch", action="store_true", help="Build TensorRT engines with fixed batch size."
|
||||
)
|
||||
parser.add_argument(
|
||||
"--build-dynamic-shape", action="store_true", help="Build TensorRT engines with dynamic image shapes."
|
||||
)
|
||||
parser.add_argument(
|
||||
"--build-enable-refit", action="store_true", help="Enable Refit option in TensorRT engines during build."
|
||||
)
|
||||
parser.add_argument(
|
||||
"--build-all-tactics", action="store_true", help="Build TensorRT engines using all tactic sources."
|
||||
)
|
||||
parser.add_argument(
|
||||
"--timing-cache", default=None, type=str, help="Path to the precached timing measurements to accelerate build."
|
||||
)
|
||||
parser.add_argument("--ws", action="store_true", help="Build TensorRT engines with weight streaming enabled.")
|
||||
|
||||
# Quantization configuration.
|
||||
parser.add_argument("--int8", action="store_true", help="Apply int8 quantization.")
|
||||
parser.add_argument("--fp8", action="store_true", help="Apply fp8 quantization.")
|
||||
parser.add_argument("--fp4", action="store_true", help="Apply fp4 quantization.")
|
||||
parser.add_argument(
|
||||
"--quantization-level",
|
||||
type=float,
|
||||
default=0.0,
|
||||
choices=[0.0, 1.0, 2.0, 2.5, 3.0, 4.0],
|
||||
help="int8/fp8 quantization level, 1: CNN, 2: CNN + FFN, 2.5: CNN + FFN + QKV, 3: CNN + Almost all Linear (Including FFN, QKV, Proj and others), 4: CNN + Almost all Linear + fMHA, 0: Default to 2.5 for int8 and 4.0 for fp8.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--quantization-percentile",
|
||||
type=float,
|
||||
default=1.0,
|
||||
help="Control quantization scaling factors (amax) collecting range, where the minimum amax in range(n_steps * percentile) will be collected. Recommendation: 1.0.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--quantization-alpha",
|
||||
type=float,
|
||||
default=0.8,
|
||||
help="The alpha parameter for SmoothQuant quantization used for linear layers. Recommendation: 0.8 for SDXL.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--calibration-size",
|
||||
type=int,
|
||||
default=32,
|
||||
help="The number of steps to use for calibrating the model for quantization. Recommendation: 32, 64, 128 for SDXL",
|
||||
)
|
||||
|
||||
# Inference
|
||||
parser.add_argument(
|
||||
"--num-warmup-runs", type=int, default=5, help="Number of warmup runs before benchmarking performance"
|
||||
)
|
||||
parser.add_argument("--use-cuda-graph", action="store_true", help="Enable cuda graph")
|
||||
parser.add_argument("--nvtx-profile", action="store_true", help="Enable NVTX markers for performance profiling")
|
||||
parser.add_argument(
|
||||
"--torch-inference",
|
||||
default="",
|
||||
help="Run inference with PyTorch (using specified compilation mode) instead of TensorRT.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--torch-fallback",
|
||||
default=None,
|
||||
type=str,
|
||||
help="[FLUX, SD3.5, and Wan] Comma separated list of models to be inferenced using PyTorch instead of TRT. For example --torch-fallback text_encoder,transformer,transformer_2. If --torch-inference set, this parameter will be ignored.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--low-vram",
|
||||
action="store_true",
|
||||
help="[FLUX, SD3.5, and Wan] Optimize for low VRAM usage, possibly at the expense of inference performance. Disabled by default.",
|
||||
)
|
||||
parser.add_argument("--seed", type=int, default=None, help="Seed for random generator to get consistent results")
|
||||
parser.add_argument("--output-dir", default="output", help="Output directory for logs and image artifacts")
|
||||
parser.add_argument("--hf-token", type=str, help="HuggingFace API access token for downloading model checkpoints")
|
||||
parser.add_argument("-v", "--verbose", action="store_true", help="Show verbose output")
|
||||
return parser
|
||||
|
||||
|
||||
def process_pipeline_args(args: argparse.Namespace) -> Tuple[Dict[str, Any], Dict[str, Any], Tuple]:
|
||||
"""Validate parsed arguments and process argument values.
|
||||
|
||||
Some argument values are resolved or overwritten during processing.
|
||||
|
||||
Args:
|
||||
args (argparse.Namespace): Parsed argument. This is modified in-place.
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: Keyword arguments for initializing a pipeline. This is only used in legacy pipelines that do not
|
||||
have factory methods `FromArgs` that construct the pipeline directly from the parsed argument.
|
||||
Dict[str, Any]: Keyword arguments for calling the `.load_engine` method of the pipeline.
|
||||
Tuple: Arguments for calling the `.run` method of the pipeline.
|
||||
"""
|
||||
|
||||
# GPU device info
|
||||
device_info = torch.cuda.get_device_properties(0)
|
||||
sm_version = device_info.major * 10 + device_info.minor
|
||||
|
||||
is_flux = args.version.startswith("flux")
|
||||
is_sd35 = args.version.startswith("3.5")
|
||||
is_wan = args.version.startswith("wan")
|
||||
is_cosmos = args.version.startswith("cosmos")
|
||||
|
||||
if args.height % 8 != 0 or args.width % 8 != 0:
|
||||
raise ValueError(
|
||||
f"Image height and width have to be divisible by 8 but specified as: {args.image_height} and {args.width}."
|
||||
)
|
||||
|
||||
# Handle batch size
|
||||
max_batch_size = 4
|
||||
if args.batch_size > max_batch_size:
|
||||
raise ValueError(f"Batch size {args.batch_size} is larger than allowed {max_batch_size}.")
|
||||
|
||||
if args.use_cuda_graph and (not args.build_static_batch or args.build_dynamic_shape):
|
||||
raise ValueError(
|
||||
"Using CUDA graph requires static dimensions. Enable `--build-static-batch` and do not specify `--build-dynamic-shape`"
|
||||
)
|
||||
|
||||
# TensorRT builder optimization level
|
||||
if args.optimization_level is None:
|
||||
# optimization level set to 3 for all Flux pipelines to reduce GPU memory usage
|
||||
if args.int8 or args.fp8 and not is_flux:
|
||||
args.optimization_level = 4
|
||||
else:
|
||||
args.optimization_level = 3
|
||||
|
||||
if args.optimization_level not in VALID_OPTIMIZATION_LEVELS:
|
||||
raise ValueError(
|
||||
f"Optimization level {args.optimization_level} not valid. Valid values are: {VALID_OPTIMIZATION_LEVELS}"
|
||||
)
|
||||
|
||||
# Quantized pipeline
|
||||
# int8 support
|
||||
if args.int8 and not any(args.version.startswith(prefix) for prefix in ("xl", "1.4")):
|
||||
raise ValueError("int8 quantization is only supported for SDXL and SD1.4 pipelines.")
|
||||
|
||||
# fp8 support validation
|
||||
if args.fp8:
|
||||
# Check version compatibility
|
||||
supported_versions = ("xl", "1.4", "3.5-large")
|
||||
if not (any(args.version.startswith(prefix) for prefix in supported_versions) or is_flux):
|
||||
raise ValueError(
|
||||
"fp8 quantization is only supported for SDXL, SD1.4, SD3.5-large and FLUX pipelines."
|
||||
)
|
||||
|
||||
# Check controlnet compatibility
|
||||
if getattr(args, "controlnet_type", None) is not None:
|
||||
if args.version not in ("xl-1.0", "3.5-large"):
|
||||
raise ValueError("fp8 controlnet quantization is only supported for SDXL and SD3.5-large.")
|
||||
if args.version == "3.5-large" and args.controlnet_type == "blur":
|
||||
raise ValueError("Blur controlnet type is not supported for SD3.5.")
|
||||
# Check for conflicting quantization
|
||||
if args.int8:
|
||||
raise ValueError("Cannot apply both int8 and fp8 quantization, please choose only one.")
|
||||
|
||||
# Check GPU compute capability
|
||||
if sm_version < 89:
|
||||
raise ValueError(
|
||||
f"Cannot apply FP8 quantization for GPU with compute capability {sm_version / 10.0}. A minimum compute capability of 8.9 is required."
|
||||
)
|
||||
|
||||
# Check SD3.5-large specific requirement
|
||||
if args.version == "3.5-large" and not args.download_onnx_models:
|
||||
raise ValueError(
|
||||
"Native FP8 quantization is not supported for SD3.5-large. Please pass --download-onnx-models."
|
||||
)
|
||||
|
||||
# TensorRT ModelOpt quantization level
|
||||
if args.quantization_level == 0.0:
|
||||
def override_quant_level(level: float, dtype_str: str):
|
||||
args.quantization_level = level
|
||||
print(f"[W] The default quantization level has been set to {level} for {dtype_str}.")
|
||||
|
||||
if args.fp8:
|
||||
# L4 fp8 fMHA on Hopper not yet enabled.
|
||||
if sm_version == 90 and is_flux:
|
||||
override_quant_level(3.0, "FP8")
|
||||
else:
|
||||
override_quant_level(3.0 if args.version == "1.4" else 4.0, "FP8")
|
||||
|
||||
elif args.int8:
|
||||
override_quant_level(3.0, "INT8")
|
||||
|
||||
if args.version.startswith("flux") and args.quantization_level == 3.0 and args.download_onnx_models:
|
||||
raise ValueError(
|
||||
"Transformer ONNX model for Quantization level 3 is not available for download. Please export the quantized Transformer model natively with the removal of --download-onnx-models."
|
||||
)
|
||||
if args.fp4:
|
||||
# FP4 precision is only supported for the Flux pipeline
|
||||
assert is_flux, "FP4 precision is only supported for the Flux pipeline"
|
||||
|
||||
# Handle LoRA
|
||||
# FLUX canny and depth official LoRAs are not supported because they modify the transformer architecture, conflicting with refit
|
||||
if args.lora_path and not any(args.version.startswith(prefix) for prefix in ("xl", "flux.1-dev", "flux.1-schnell")):
|
||||
raise ValueError("LoRA adapter support is only supported for SDXL, FLUX.1-dev and FLUX.1-schnell pipelines")
|
||||
|
||||
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}")
|
||||
|
||||
# Force lora merge when fp8 or int8 is used with LoRA
|
||||
if args.build_enable_refit and args.lora_path and (args.int8 or args.fp8):
|
||||
raise ValueError(
|
||||
"Engine refit should not be enabled for quantized models with LoRA. ModelOpt recommends fusing the LoRA to the model before quantization. \
|
||||
See https://github.com/NVIDIA/TensorRT-Model-Optimizer/tree/main/examples/diffusers/quantization#lora"
|
||||
)
|
||||
|
||||
# Torch-fallback and Torch-inference
|
||||
if args.torch_fallback and not args.torch_inference:
|
||||
assert (
|
||||
is_flux or is_sd35 or is_wan or is_cosmos
|
||||
), "PyTorch Fallback is only supported for Flux, Stable Diffusion 3.5, Wan and Cosmos pipelines."
|
||||
args.torch_fallback = args.torch_fallback.split(",")
|
||||
|
||||
if args.torch_fallback and args.torch_inference:
|
||||
print(
|
||||
"[W] All models will run in PyTorch when --torch-inference is set. Parameter --torch-fallback will be ignored."
|
||||
)
|
||||
args.torch_fallback = None
|
||||
|
||||
# low-vram
|
||||
if args.low_vram:
|
||||
assert (
|
||||
is_flux or is_sd35 or is_wan or is_cosmos
|
||||
), "low-vram mode is only supported for Flux, Stable Diffusion 3.5, Wan and Cosmos pipelines."
|
||||
|
||||
# Disable SDXL LCM pipeline
|
||||
if args.version == "xl-1.0" and args.scheduler == "LCM":
|
||||
raise ValueError("SDXL pipeline does not support the LCM scheduler currently. Please use a different scheduler.")
|
||||
|
||||
# Pack arguments
|
||||
kwargs_init_pipeline = {
|
||||
"version": args.version,
|
||||
"max_batch_size": max_batch_size,
|
||||
"denoising_steps": args.denoising_steps,
|
||||
"scheduler": args.scheduler,
|
||||
"guidance_scale": args.guidance_scale,
|
||||
"output_dir": args.output_dir,
|
||||
"hf_token": args.hf_token,
|
||||
"verbose": args.verbose,
|
||||
"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,
|
||||
}
|
||||
|
||||
kwargs_load_engine = {
|
||||
"onnx_opset": args.onnx_opset,
|
||||
"opt_batch_size": args.batch_size,
|
||||
"opt_image_height": args.height,
|
||||
"opt_image_width": args.width,
|
||||
"optimization_level": args.optimization_level,
|
||||
"static_batch": args.build_static_batch,
|
||||
"static_shape": not args.build_dynamic_shape,
|
||||
"enable_all_tactics": args.build_all_tactics,
|
||||
"enable_refit": args.build_enable_refit,
|
||||
"timing_cache": args.timing_cache,
|
||||
"int8": args.int8,
|
||||
"fp8": args.fp8,
|
||||
"fp4": args.fp4,
|
||||
"quantization_level": args.quantization_level,
|
||||
"quantization_percentile": args.quantization_percentile,
|
||||
"quantization_alpha": args.quantization_alpha,
|
||||
"calibration_size": args.calibration_size,
|
||||
"onnx_export_only": args.onnx_export_only,
|
||||
"download_onnx_models": args.download_onnx_models,
|
||||
}
|
||||
|
||||
args_run_demo = (
|
||||
args.prompt,
|
||||
args.negative_prompt,
|
||||
args.height,
|
||||
args.width,
|
||||
args.batch_size,
|
||||
args.batch_count,
|
||||
args.num_warmup_runs,
|
||||
args.use_cuda_graph,
|
||||
)
|
||||
|
||||
return kwargs_init_pipeline, kwargs_load_engine, args_run_demo
|
||||
@@ -0,0 +1,270 @@
|
||||
#
|
||||
# SPDX-FileCopyrightText: Copyright (c) 1993-2025 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.
|
||||
#
|
||||
|
||||
"""Dependency management for TensorRT diffusion demos.
|
||||
|
||||
Adds the right group's site-packages to sys.path so imports work.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
# Marker file to indicate successful installation
|
||||
# Must match the marker file used by setup.py
|
||||
INSTALL_COMPLETE_MARKER = ".install_complete"
|
||||
|
||||
# Descriptions for user-facing messages
|
||||
GROUP_DESCRIPTIONS = {
|
||||
"sd": "SD family (SD 1.4, SDXL, SD3, SD3.5, SVD, Stable Cascade)",
|
||||
"flux": "Flux family (Black Forest Labs)",
|
||||
"cosmos": "Cosmos family (NVIDIA), Wan2.2 T2V",
|
||||
}
|
||||
|
||||
# Valid dependency groups
|
||||
VALID_GROUPS = list(GROUP_DESCRIPTIONS.keys())
|
||||
|
||||
__all__ = [
|
||||
"configure",
|
||||
"get_configured_groups",
|
||||
"print_status",
|
||||
]
|
||||
|
||||
|
||||
def _resolve_deps_root(deps_root: str | None) -> str:
|
||||
"""Resolve the dependency root path with precedence: arg > env > default."""
|
||||
if deps_root is not None:
|
||||
return deps_root
|
||||
return os.environ.get("TENSORRT_DIFFUSION_DEPS_ROOT", "/workspace/deps")
|
||||
|
||||
def _prepend_env_path(var: str, path: str, clean_root: str | None = None):
|
||||
"""Prepend `path` to an os.pathsep-separated env var so child processes
|
||||
(e.g. the `polygraphy` CLI) inherit the group's dependencies. If
|
||||
`clean_root` is given, drop existing entries under it first."""
|
||||
def _norm(p: str) -> str:
|
||||
return os.path.abspath(os.path.expanduser(p))
|
||||
|
||||
existing = [p for p in os.environ.get(var, "").split(os.pathsep) if p]
|
||||
if clean_root is not None:
|
||||
root_abs = _norm(clean_root).rstrip(os.sep) + os.sep
|
||||
existing = [p for p in existing if not _norm(p).startswith(root_abs)]
|
||||
existing = [p for p in existing if _norm(p) != _norm(path)]
|
||||
os.environ[var] = os.pathsep.join([path, *existing])
|
||||
|
||||
|
||||
def _clean_diffusion_paths(deps_root: str = "/workspace/deps"):
|
||||
"""Drop any existing paths under deps_root from sys.path."""
|
||||
# Filter out any paths that live under deps_root (robust to path forms)
|
||||
root_abs = os.path.abspath(os.path.expanduser(deps_root)).rstrip(os.sep) + os.sep
|
||||
sys.path[:] = [
|
||||
p for p in sys.path
|
||||
if not os.path.abspath(os.path.expanduser(p)).startswith(root_abs)
|
||||
]
|
||||
|
||||
|
||||
def configure(
|
||||
group: str,
|
||||
deps_root: str | None = None,
|
||||
verbose: bool = False,
|
||||
clean: bool = True,
|
||||
fallback: bool = False
|
||||
):
|
||||
"""
|
||||
Configure sys.path to use dependencies from the specified group.
|
||||
|
||||
This function should be called at the top of each demo script, before
|
||||
any other imports that depend on external packages.
|
||||
|
||||
Args:
|
||||
group: Dependency group name ("sd", "flux", or "cosmos")
|
||||
deps_root: Root directory where dependencies are installed.
|
||||
If None, uses TENSORRT_DIFFUSION_DEPS_ROOT environment variable,
|
||||
or defaults to "/workspace/deps"
|
||||
verbose: Print configuration info (default: False)
|
||||
clean: Remove other dependency group paths first (default: True)
|
||||
This prevents sys.path inflation in long-running processes.
|
||||
fallback: If True, gracefully handle missing dependencies instead of raising
|
||||
RuntimeError. Useful for test environments using requirements.txt.
|
||||
If False but USE_REQUIREMENTS=1 is set,
|
||||
fallback will be automatically enabled.
|
||||
|
||||
Example:
|
||||
from demo_diffusion import deps
|
||||
deps.configure("flux")
|
||||
|
||||
# Or with custom location
|
||||
deps.configure("flux", deps_root="/custom/path/deps")
|
||||
|
||||
# Or via environment variable
|
||||
# export TENSORRT_DIFFUSION_DEPS_ROOT=/custom/path/deps
|
||||
# deps.configure("flux")
|
||||
|
||||
# For test environments using requirements.txt (installed via setup.sh)
|
||||
# export USE_REQUIREMENTS=1
|
||||
# deps.configure("sd") # Will automatically use fallback mode
|
||||
"""
|
||||
if group not in VALID_GROUPS:
|
||||
raise ValueError(
|
||||
f"Invalid dependency group: '{group}'\n"
|
||||
f"Valid groups: {', '.join(sorted(VALID_GROUPS))}"
|
||||
)
|
||||
|
||||
# Auto-enable fallback mode if using requirements.txt installation
|
||||
if not fallback and os.environ.get("USE_REQUIREMENTS") == "1":
|
||||
fallback = True
|
||||
if verbose:
|
||||
print("Detected USE_REQUIREMENTS=1, enabling fallback mode")
|
||||
|
||||
# Resolve deps_root consistently across the module
|
||||
deps_root = _resolve_deps_root(deps_root)
|
||||
|
||||
# Clean old dependency paths first (prevents inflation)
|
||||
if clean:
|
||||
_clean_diffusion_paths(deps_root)
|
||||
|
||||
# Determine Python version
|
||||
python_version = f"{sys.version_info.major}.{sys.version_info.minor}"
|
||||
|
||||
# Construct path to site-packages for this group
|
||||
deps_path = os.path.join(
|
||||
deps_root,
|
||||
group,
|
||||
"lib",
|
||||
f"python{python_version}",
|
||||
"site-packages"
|
||||
)
|
||||
|
||||
# Check if installation is complete
|
||||
group_dir = os.path.join(deps_root, group)
|
||||
marker_file = os.path.join(group_dir, INSTALL_COMPLETE_MARKER)
|
||||
|
||||
if os.path.exists(deps_path) and os.path.exists(marker_file):
|
||||
# Insert at the beginning to override any system packages
|
||||
sys.path.insert(0, deps_path)
|
||||
|
||||
# Mirror onto the environment so child processes (e.g. the polygraphy
|
||||
# CLI that engine builds shell out to) use these deps too. When clean,
|
||||
# drop other groups' entries under deps_root, matching sys.path above.
|
||||
clean_root = deps_root if clean else None
|
||||
_prepend_env_path("PYTHONPATH", deps_path, clean_root=clean_root)
|
||||
_prepend_env_path("PATH", os.path.join(group_dir, "bin"), clean_root=clean_root)
|
||||
|
||||
if verbose:
|
||||
description = GROUP_DESCRIPTIONS.get(group, group)
|
||||
print(f"Configured dependencies: {description}")
|
||||
print(f" Path: {deps_path}")
|
||||
else:
|
||||
# Dependencies not found or installation incomplete
|
||||
description = GROUP_DESCRIPTIONS.get(group, group)
|
||||
|
||||
# Check if it's an incomplete installation
|
||||
if os.path.exists(group_dir) and not os.path.exists(marker_file):
|
||||
error_msg = (
|
||||
f"Dependencies for '{group}' are incomplete!\n"
|
||||
f" Location: {group_dir}\n"
|
||||
f" Description: {description}\n"
|
||||
f" A previous installation failed or was interrupted.\n"
|
||||
f" To fix, run:\n"
|
||||
f" python setup.py {group}\n"
|
||||
f" (This will clean up and reinstall)"
|
||||
)
|
||||
if fallback:
|
||||
if verbose:
|
||||
print(f"Warning: {error_msg}")
|
||||
print("Continuing with fallback mode...")
|
||||
return
|
||||
else:
|
||||
raise RuntimeError(error_msg)
|
||||
else:
|
||||
# Not installed at all
|
||||
error_msg = (
|
||||
f"Dependencies for '{group}' not found!\n"
|
||||
f" Expected at: {deps_path}\n"
|
||||
f" Description: {description}\n"
|
||||
f" To install, run: python setup.py {group}\n"
|
||||
f" If you installed elsewhere, set TENSORRT_DIFFUSION_DEPS_ROOT or pass deps_root."
|
||||
)
|
||||
if fallback:
|
||||
if verbose:
|
||||
print(f"Warning: {error_msg}")
|
||||
print("Continuing with fallback mode (assuming requirements.txt setup)...")
|
||||
return
|
||||
else:
|
||||
raise RuntimeError(error_msg)
|
||||
|
||||
|
||||
def get_configured_groups(deps_root: str | None = None) -> list[str]:
|
||||
"""
|
||||
Get list of dependency groups that are currently installed.
|
||||
|
||||
A group is considered installed only if both:
|
||||
1. The group directory exists
|
||||
2. The .install_complete marker file exists in that directory
|
||||
|
||||
Args:
|
||||
deps_root: Root directory where dependencies are installed.
|
||||
If None, uses TENSORRT_DIFFUSION_DEPS_ROOT environment variable,
|
||||
or defaults to "/workspace/deps"
|
||||
|
||||
Returns:
|
||||
List of installed group names (e.g., ["sd", "flux"])
|
||||
"""
|
||||
# Resolve deps_root
|
||||
deps_root = _resolve_deps_root(deps_root)
|
||||
|
||||
installed = []
|
||||
for group in VALID_GROUPS:
|
||||
# Check if the group directory exists and has the completion marker
|
||||
group_dir = os.path.join(deps_root, group)
|
||||
marker_file = os.path.join(group_dir, INSTALL_COMPLETE_MARKER)
|
||||
|
||||
# Only consider it installed if marker file exists
|
||||
if os.path.exists(group_dir) and os.path.isdir(group_dir) and os.path.exists(marker_file):
|
||||
installed.append(group)
|
||||
return sorted(installed)
|
||||
|
||||
|
||||
def print_status(deps_root: str | None = None):
|
||||
"""
|
||||
Print the status of all dependency groups.
|
||||
|
||||
Args:
|
||||
deps_root: Root directory where dependencies are installed.
|
||||
If None, uses TENSORRT_DIFFUSION_DEPS_ROOT environment variable,
|
||||
or defaults to "/workspace/deps"
|
||||
"""
|
||||
# Resolve deps_root
|
||||
deps_root = _resolve_deps_root(deps_root)
|
||||
|
||||
print("Dependency Groups Status:")
|
||||
print("-" * 60)
|
||||
print(f"Location: {deps_root}")
|
||||
print("-" * 60)
|
||||
|
||||
installed = get_configured_groups(deps_root)
|
||||
|
||||
for group in sorted(VALID_GROUPS):
|
||||
description = GROUP_DESCRIPTIONS.get(group, group)
|
||||
status = "Installed" if group in installed else "Not installed"
|
||||
print(f" {group:8} - {status:15} - {description}")
|
||||
|
||||
print("-" * 60)
|
||||
|
||||
if not installed:
|
||||
print("No dependency groups installed.")
|
||||
print("Run: python setup.py all")
|
||||
else:
|
||||
print(f"{len(installed)} group(s) installed: {', '.join(installed)}")
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
#
|
||||
# SPDX-FileCopyrightText: Copyright (c) 1993-2025 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 warnings
|
||||
from importlib import import_module
|
||||
|
||||
|
||||
def import_from_diffusers(model_name, module_name):
|
||||
try:
|
||||
module = import_module(module_name)
|
||||
return getattr(module, model_name)
|
||||
except ImportError:
|
||||
warnings.warn(f"Failed to import {module_name}. The {model_name} model will not be available.", ImportWarning)
|
||||
except AttributeError:
|
||||
warnings.warn(f"The {model_name} model is not available in the installed version of diffusers.", ImportWarning)
|
||||
return None
|
||||
@@ -0,0 +1,326 @@
|
||||
#
|
||||
# SPDX-FileCopyrightText: Copyright (c) 1993-2025 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 gc
|
||||
import os
|
||||
import subprocess
|
||||
import warnings
|
||||
from collections import OrderedDict, defaultdict
|
||||
|
||||
import numpy as np
|
||||
import onnx
|
||||
import tensorrt as trt
|
||||
import torch
|
||||
from cuda.bindings import runtime as cudart
|
||||
from onnx import numpy_helper
|
||||
from polygraphy.backend.common import bytes_from_path
|
||||
from polygraphy.backend.trt import (
|
||||
engine_from_bytes,
|
||||
)
|
||||
|
||||
TRT_LOGGER = trt.Logger(trt.Logger.ERROR)
|
||||
|
||||
|
||||
# Map of TensorRT dtype -> torch dtype
|
||||
trt_to_torch_dtype_dict = {
|
||||
trt.DataType.BOOL: torch.bool,
|
||||
trt.DataType.UINT8: torch.uint8,
|
||||
trt.DataType.INT8: torch.int8,
|
||||
trt.DataType.INT32: torch.int32,
|
||||
trt.DataType.INT64: torch.int64,
|
||||
trt.DataType.HALF: torch.float16,
|
||||
trt.DataType.FLOAT: torch.float32,
|
||||
trt.DataType.BF16: torch.bfloat16,
|
||||
}
|
||||
|
||||
|
||||
def _CUASSERT(cuda_ret):
|
||||
err = cuda_ret[0]
|
||||
if err != cudart.cudaError_t.cudaSuccess:
|
||||
raise RuntimeError(
|
||||
f"CUDA ERROR: {err}, error code reference: https://nvidia.github.io/cuda-python/module/cudart.html#cuda.cudart.cudaError_t"
|
||||
)
|
||||
if len(cuda_ret) > 1:
|
||||
return cuda_ret[1]
|
||||
return None
|
||||
|
||||
|
||||
def get_refit_weights(state_dict, onnx_opt_path, weight_name_mapping, weight_shape_mapping):
|
||||
onnx_opt_dir = os.path.dirname(onnx_opt_path)
|
||||
onnx_opt_model = onnx.load(onnx_opt_path)
|
||||
# Create initializer data hashes
|
||||
initializer_hash_mapping = {}
|
||||
for initializer in onnx_opt_model.graph.initializer:
|
||||
initializer_data = numpy_helper.to_array(initializer, base_dir=onnx_opt_dir).astype(np.float16)
|
||||
initializer_hash = hash(initializer_data.data.tobytes())
|
||||
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]
|
||||
initializer_hash = initializer_hash_mapping[initializer_name]
|
||||
|
||||
# get shape transform info
|
||||
initializer_shape, is_transpose = weight_shape_mapping[wt_name]
|
||||
if is_transpose:
|
||||
wt = torch.transpose(wt, 0, 1)
|
||||
else:
|
||||
wt = torch.reshape(wt, initializer_shape)
|
||||
|
||||
# include weight if hashes differ
|
||||
wt_hash = hash(wt.cpu().detach().numpy().astype(np.float16).data.tobytes())
|
||||
if initializer_hash != wt_hash:
|
||||
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
|
||||
|
||||
|
||||
class Engine:
|
||||
def __init__(
|
||||
self,
|
||||
engine_path,
|
||||
):
|
||||
self.engine_path = engine_path
|
||||
self.engine = None
|
||||
self.context = None
|
||||
self.buffers = OrderedDict()
|
||||
self.tensors = OrderedDict()
|
||||
self.cuda_graph_instance = None # cuda graph
|
||||
|
||||
def __del__(self):
|
||||
del self.engine
|
||||
del self.context
|
||||
del self.buffers
|
||||
del self.tensors
|
||||
|
||||
def refit(self, refit_weights, updated_weight_names):
|
||||
# Initialize refitter
|
||||
refitter = trt.Refitter(self.engine, TRT_LOGGER)
|
||||
refitted_weights = set()
|
||||
|
||||
def refit_single_weight(trt_weight_name):
|
||||
# get weight from state dict
|
||||
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])
|
||||
)
|
||||
trt_wt_location = (
|
||||
trt.TensorLocation.DEVICE if refit_weights[trt_weight_name].is_cuda else trt.TensorLocation.HOST
|
||||
)
|
||||
|
||||
# apply refit
|
||||
refitter.set_named_weights(trt_weight_name, trt_wt_tensor, trt_wt_location)
|
||||
refitted_weights.add(trt_weight_name)
|
||||
|
||||
# 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)
|
||||
|
||||
print(f"[I] Total refitted weights {len(refitted_weights)}.")
|
||||
|
||||
def build(
|
||||
self,
|
||||
onnx_path,
|
||||
tf32=False,
|
||||
input_profile=None,
|
||||
enable_refit=False,
|
||||
enable_all_tactics=False,
|
||||
timing_cache=None,
|
||||
update_output_names=None,
|
||||
native_instancenorm=True,
|
||||
verbose=False,
|
||||
weight_streaming=False,
|
||||
builder_optimization_level=3,
|
||||
precision_constraints='none',
|
||||
):
|
||||
print(f"Building TensorRT engine for {onnx_path}: {self.engine_path}")
|
||||
|
||||
# Base command
|
||||
build_command = [f"polygraphy convert {onnx_path} --convert-to trt --output {self.engine_path}"]
|
||||
|
||||
# Build arguments
|
||||
build_args = [
|
||||
"--strongly-typed",
|
||||
"--tf32" if tf32 else "",
|
||||
"--weight-streaming" if weight_streaming else "",
|
||||
"--refittable" if enable_refit else "",
|
||||
"--tactic-sources" if not enable_all_tactics else "",
|
||||
"--onnx-flags native_instancenorm" if native_instancenorm else "",
|
||||
f"--builder-optimization-level {builder_optimization_level}",
|
||||
f"--precision-constraints {precision_constraints}",
|
||||
]
|
||||
|
||||
# Timing cache
|
||||
if timing_cache:
|
||||
build_args.extend([
|
||||
f"--load-timing-cache {timing_cache}",
|
||||
f"--save-timing-cache {timing_cache}"
|
||||
])
|
||||
|
||||
# Verbosity setting
|
||||
verbosity = "extra_verbose" if verbose else "error"
|
||||
build_args.append(f"--verbosity {verbosity}")
|
||||
|
||||
# Output names
|
||||
if update_output_names:
|
||||
print(f"Updating network outputs to {update_output_names}")
|
||||
build_args.append(f"--trt-outputs {' '.join(update_output_names)}")
|
||||
|
||||
# Input profiles
|
||||
if input_profile:
|
||||
profile_args = defaultdict(str)
|
||||
for name, dims in input_profile.items():
|
||||
assert len(dims) == 3
|
||||
profile_args["--trt-min-shapes"] += f"{name}:{str(list(dims[0])).replace(' ', '')} "
|
||||
profile_args["--trt-opt-shapes"] += f"{name}:{str(list(dims[1])).replace(' ', '')} "
|
||||
profile_args["--trt-max-shapes"] += f"{name}:{str(list(dims[2])).replace(' ', '')} "
|
||||
|
||||
build_args.extend(f"{k} {v}" for k, v in profile_args.items())
|
||||
|
||||
# Filter out empty strings and join command
|
||||
build_args = [arg for arg in build_args if arg]
|
||||
final_command = ' '.join(build_command + build_args)
|
||||
|
||||
# Execute command with improved error handling
|
||||
try:
|
||||
print(f"Engine build command: {final_command}")
|
||||
subprocess.run(final_command, check=True, shell=True)
|
||||
except subprocess.CalledProcessError as exc:
|
||||
error_msg = (
|
||||
f"Failed to build TensorRT engine. Error details:\n"
|
||||
f"Command: {exc.cmd}\n"
|
||||
)
|
||||
raise RuntimeError(error_msg) from exc
|
||||
|
||||
def load(self, weight_streaming=False, weight_streaming_budget_percentage=None):
|
||||
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)
|
||||
if weight_streaming:
|
||||
if weight_streaming_budget_percentage is None:
|
||||
warnings.warn(
|
||||
f"Weight streaming budget is not set for {self.engine_path}. Weights will not be streamed."
|
||||
)
|
||||
else:
|
||||
self.engine.weight_streaming_budget_v2 = int(
|
||||
weight_streaming_budget_percentage / 100 * self.engine.streamable_weights_size
|
||||
)
|
||||
|
||||
def unload(self, verbose=True):
|
||||
if self.engine is not None:
|
||||
if verbose:
|
||||
print(f"Unloading TensorRT engine: {self.engine_path}")
|
||||
del self.engine
|
||||
self.engine = None
|
||||
gc.collect()
|
||||
else:
|
||||
if verbose:
|
||||
print(f"[W]: Unload an unloaded engine {self.engine_path}, skip unloading")
|
||||
|
||||
def activate(self, device_memory=None):
|
||||
if device_memory is not None:
|
||||
self.context = self.engine.create_execution_context(
|
||||
trt.ExecutionContextAllocationStrategy.USER_MANAGED
|
||||
)
|
||||
self.context.device_memory = device_memory
|
||||
else:
|
||||
self.context = self.engine.create_execution_context()
|
||||
|
||||
def reactivate(self, device_memory):
|
||||
assert self.context
|
||||
self.context.device_memory = device_memory
|
||||
|
||||
def deactivate(self):
|
||||
del self.context
|
||||
self.context = None
|
||||
|
||||
def allocate_buffers(self, shape_dict=None, device="cuda"):
|
||||
for binding in range(self.engine.num_io_tensors):
|
||||
name = self.engine.get_tensor_name(binding)
|
||||
if shape_dict and name in shape_dict:
|
||||
shape = shape_dict[name]
|
||||
else:
|
||||
shape = self.engine.get_tensor_shape(name)
|
||||
print(
|
||||
f"[W]: {self.engine_path}: Could not find '{name}' in shape dict {shape_dict}. Using shape {shape} inferred from the engine."
|
||||
)
|
||||
if self.engine.get_tensor_mode(name) == trt.TensorIOMode.INPUT:
|
||||
self.context.set_input_shape(name, shape)
|
||||
dtype = trt_to_torch_dtype_dict[self.engine.get_tensor_dtype(name)]
|
||||
tensor = torch.empty(tuple(shape), dtype=dtype).to(device=device)
|
||||
self.tensors[name] = tensor
|
||||
|
||||
def deallocate_buffers(self):
|
||||
if not self.engine:
|
||||
return
|
||||
for idx in range(self.engine.num_io_tensors):
|
||||
binding = self.engine[idx]
|
||||
del self.tensors[binding]
|
||||
|
||||
def infer(self, feed_dict, stream, use_cuda_graph=False):
|
||||
for name, buf in feed_dict.items():
|
||||
self.tensors[name].copy_(buf)
|
||||
|
||||
for name, tensor in self.tensors.items():
|
||||
self.context.set_tensor_address(name, tensor.data_ptr())
|
||||
|
||||
if use_cuda_graph:
|
||||
if self.cuda_graph_instance is not None:
|
||||
_CUASSERT(cudart.cudaGraphLaunch(self.cuda_graph_instance, stream))
|
||||
_CUASSERT(cudart.cudaStreamSynchronize(stream))
|
||||
else:
|
||||
# do inference before CUDA graph capture
|
||||
noerror = self.context.execute_async_v3(stream)
|
||||
if not noerror:
|
||||
raise ValueError(f"ERROR: inference of {self.engine_path} failed.")
|
||||
# capture cuda graph
|
||||
_CUASSERT(
|
||||
cudart.cudaStreamBeginCapture(stream, cudart.cudaStreamCaptureMode.cudaStreamCaptureModeGlobal)
|
||||
)
|
||||
self.context.execute_async_v3(stream)
|
||||
self.graph = _CUASSERT(cudart.cudaStreamEndCapture(stream))
|
||||
self.cuda_graph_instance = _CUASSERT(cudart.cudaGraphInstantiate(self.graph, 0))
|
||||
else:
|
||||
noerror = self.context.execute_async_v3(stream)
|
||||
if not noerror:
|
||||
raise ValueError(f"ERROR: inference of {self.engine_path} failed.")
|
||||
|
||||
return self.tensors
|
||||
@@ -0,0 +1,34 @@
|
||||
#
|
||||
# SPDX-FileCopyrightText: Copyright (c) 1993-2025 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 demo_diffusion.image.load import (
|
||||
download_image,
|
||||
prepare_mask_and_masked_image,
|
||||
preprocess_image,
|
||||
save_image,
|
||||
)
|
||||
from demo_diffusion.image.resize import resize_with_antialiasing
|
||||
from demo_diffusion.image.video import tensor2vid
|
||||
|
||||
__all__ = [
|
||||
"preprocess_image",
|
||||
"prepare_mask_and_masked_image",
|
||||
"download_image",
|
||||
"save_image",
|
||||
"resize_with_antialiasing",
|
||||
"tensor2vid",
|
||||
]
|
||||
@@ -0,0 +1,78 @@
|
||||
#
|
||||
# SPDX-FileCopyrightText: Copyright (c) 1993-2022 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 os
|
||||
import random
|
||||
from io import BytesIO
|
||||
|
||||
import numpy as np
|
||||
import requests
|
||||
import torch
|
||||
from PIL import Image
|
||||
|
||||
|
||||
def preprocess_image(image):
|
||||
"""
|
||||
image: torch.Tensor
|
||||
"""
|
||||
w, h = image.size
|
||||
w, h = map(lambda x: x - x % 32, (w, h)) # resize to integer multiple of 32
|
||||
image = image.resize((w, h))
|
||||
image = np.array(image).astype(np.float32) / 255.0
|
||||
image = image[None].transpose(0, 3, 1, 2)
|
||||
image = torch.from_numpy(image).contiguous()
|
||||
return 2.0 * image - 1.0
|
||||
|
||||
|
||||
def prepare_mask_and_masked_image(image, mask):
|
||||
"""
|
||||
image: PIL.Image.Image
|
||||
mask: PIL.Image.Image
|
||||
"""
|
||||
if isinstance(image, Image.Image):
|
||||
image = np.array(image.convert("RGB"))
|
||||
image = image[None].transpose(0, 3, 1, 2)
|
||||
image = torch.from_numpy(image).to(dtype=torch.float32).contiguous() / 127.5 - 1.0
|
||||
if isinstance(mask, Image.Image):
|
||||
mask = np.array(mask.convert("L"))
|
||||
mask = mask.astype(np.float32) / 255.0
|
||||
mask = mask[None, None]
|
||||
mask[mask < 0.5] = 0
|
||||
mask[mask >= 0.5] = 1
|
||||
mask = torch.from_numpy(mask).to(dtype=torch.float32).contiguous()
|
||||
|
||||
masked_image = image * (mask < 0.5)
|
||||
|
||||
return mask, masked_image
|
||||
|
||||
|
||||
def download_image(url):
|
||||
response = requests.get(url)
|
||||
return Image.open(BytesIO(response.content)).convert("RGB")
|
||||
|
||||
|
||||
def save_image(images, image_path_dir, image_name_prefix, image_name_suffix):
|
||||
"""
|
||||
Save the generated images to png files.
|
||||
"""
|
||||
for i in range(images.shape[0]):
|
||||
image_path = os.path.join(
|
||||
image_path_dir,
|
||||
f"{image_name_prefix}{i + 1}-{random.randint(1000, 9999)}-{image_name_suffix}.png",
|
||||
)
|
||||
print(f"Saving image {i+1} / {images.shape[0]} to: {image_path}")
|
||||
Image.fromarray(images[i]).save(image_path)
|
||||
@@ -0,0 +1,123 @@
|
||||
# Copyright 2024 The HuggingFace Team. All rights reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import torch
|
||||
|
||||
|
||||
# Taken from https://github.com/huggingface/diffusers/blob/be62c85cd973f2001ab8c5d8919a9a6811fc7e43/src/diffusers/pipelines/stable_video_diffusion/pipeline_stable_video_diffusion.py#L633
|
||||
def resize_with_antialiasing(input, size, interpolation="bicubic", align_corners=True):
|
||||
h, w = input.shape[-2:]
|
||||
factors = (h / size[0], w / size[1])
|
||||
|
||||
# First, we have to determine sigma
|
||||
# Taken from skimage: https://github.com/scikit-image/scikit-image/blob/v0.19.2/skimage/transform/_warps.py#L171
|
||||
sigmas = (
|
||||
max((factors[0] - 1.0) / 2.0, 0.001),
|
||||
max((factors[1] - 1.0) / 2.0, 0.001),
|
||||
)
|
||||
|
||||
# Now kernel size. Good results are for 3 sigma, but that is kind of slow. Pillow uses 1 sigma
|
||||
# https://github.com/python-pillow/Pillow/blob/master/src/libImaging/Resample.c#L206
|
||||
# But they do it in the 2 passes, which gives better results. Let's try 2 sigmas for now
|
||||
ks = int(max(2.0 * 2 * sigmas[0], 3)), int(max(2.0 * 2 * sigmas[1], 3))
|
||||
|
||||
# Make sure it is odd
|
||||
if (ks[0] % 2) == 0:
|
||||
ks = ks[0] + 1, ks[1]
|
||||
|
||||
if (ks[1] % 2) == 0:
|
||||
ks = ks[0], ks[1] + 1
|
||||
|
||||
input = _gaussian_blur2d(input, ks, sigmas)
|
||||
|
||||
output = torch.nn.functional.interpolate(input, size=size, mode=interpolation, align_corners=align_corners)
|
||||
return output
|
||||
|
||||
|
||||
def _compute_padding(kernel_size):
|
||||
"""Compute padding tuple."""
|
||||
# 4 or 6 ints: (padding_left, padding_right,padding_top,padding_bottom)
|
||||
# https://pytorch.org/docs/stable/nn.html#torch.nn.functional.pad
|
||||
if len(kernel_size) < 2:
|
||||
raise AssertionError(kernel_size)
|
||||
computed = [k - 1 for k in kernel_size]
|
||||
|
||||
# for even kernels we need to do asymmetric padding :(
|
||||
out_padding = 2 * len(kernel_size) * [0]
|
||||
|
||||
for i in range(len(kernel_size)):
|
||||
computed_tmp = computed[-(i + 1)]
|
||||
|
||||
pad_front = computed_tmp // 2
|
||||
pad_rear = computed_tmp - pad_front
|
||||
|
||||
out_padding[2 * i + 0] = pad_front
|
||||
out_padding[2 * i + 1] = pad_rear
|
||||
|
||||
return out_padding
|
||||
|
||||
|
||||
def _filter2d(input, kernel):
|
||||
# prepare kernel
|
||||
b, c, h, w = input.shape
|
||||
tmp_kernel = kernel[:, None, ...].to(device=input.device, dtype=input.dtype)
|
||||
|
||||
tmp_kernel = tmp_kernel.expand(-1, c, -1, -1)
|
||||
|
||||
height, width = tmp_kernel.shape[-2:]
|
||||
|
||||
padding_shape: list[int] = _compute_padding([height, width])
|
||||
input = torch.nn.functional.pad(input, padding_shape, mode="reflect")
|
||||
|
||||
# kernel and input tensor reshape to align element-wise or batch-wise params
|
||||
tmp_kernel = tmp_kernel.reshape(-1, 1, height, width)
|
||||
input = input.view(-1, tmp_kernel.size(0), input.size(-2), input.size(-1))
|
||||
|
||||
# convolve the tensor with the kernel.
|
||||
output = torch.nn.functional.conv2d(input, tmp_kernel, groups=tmp_kernel.size(0), padding=0, stride=1)
|
||||
|
||||
out = output.view(b, c, h, w)
|
||||
return out
|
||||
|
||||
|
||||
def _gaussian(window_size: int, sigma):
|
||||
if isinstance(sigma, float):
|
||||
sigma = torch.tensor([[sigma]])
|
||||
|
||||
batch_size = sigma.shape[0]
|
||||
|
||||
x = (torch.arange(window_size, device=sigma.device, dtype=sigma.dtype) - window_size // 2).expand(batch_size, -1)
|
||||
|
||||
if window_size % 2 == 0:
|
||||
x = x + 0.5
|
||||
|
||||
gauss = torch.exp(-x.pow(2.0) / (2 * sigma.pow(2.0)))
|
||||
|
||||
return gauss / gauss.sum(-1, keepdim=True)
|
||||
|
||||
|
||||
def _gaussian_blur2d(input, kernel_size, sigma):
|
||||
if isinstance(sigma, tuple):
|
||||
sigma = torch.tensor([sigma], dtype=input.dtype)
|
||||
else:
|
||||
sigma = sigma.to(dtype=input.dtype)
|
||||
|
||||
ky, kx = int(kernel_size[0]), int(kernel_size[1])
|
||||
bs = sigma.shape[0]
|
||||
kernel_x = _gaussian(kx, sigma[:, 1].view(bs, 1))
|
||||
kernel_y = _gaussian(ky, sigma[:, 0].view(bs, 1))
|
||||
out_x = _filter2d(input, kernel_x[..., None, :])
|
||||
out = _filter2d(out_x, kernel_y[..., None])
|
||||
|
||||
return out
|
||||
@@ -0,0 +1,37 @@
|
||||
#
|
||||
# Copyright (c) Alibaba, Inc. and its affiliates.
|
||||
# SPDX-FileCopyrightText: Copyright (c) 1993-2025 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 torch
|
||||
|
||||
|
||||
# Not a contribution
|
||||
# Changes made by NVIDIA CORPORATION & AFFILIATES enabling tensor2vid or otherwise documented as
|
||||
# NVIDIA-proprietary are not a contribution and subject to the terms and conditions at the top of the file
|
||||
def tensor2vid(video: torch.Tensor, processor, output_type="np"):
|
||||
# Based on:
|
||||
# https://github.com/modelscope/modelscope/blob/1509fdb973e5871f37148a4b5e5964cafd43e64d/modelscope/pipelines/multi_modal/text_to_video_synthesis_pipeline.py#L78
|
||||
|
||||
batch_size, channels, num_frames, height, width = video.shape
|
||||
outputs = []
|
||||
for batch_idx in range(batch_size):
|
||||
batch_vid = video[batch_idx].permute(1, 0, 2, 3)
|
||||
batch_output = processor.postprocess(batch_vid, output_type)
|
||||
|
||||
outputs.append(batch_output)
|
||||
|
||||
return outputs
|
||||
@@ -0,0 +1,111 @@
|
||||
#
|
||||
# SPDX-FileCopyrightText: Copyright (c) 1993-2025 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 demo_diffusion.model.base_model import BaseModel
|
||||
from demo_diffusion.model.clip import (
|
||||
CLIPImageProcessorModel,
|
||||
CLIPModel,
|
||||
CLIPVisionWithProjModel,
|
||||
CLIPWithProjModel,
|
||||
SD3_CLIPGModel,
|
||||
SD3_CLIPLModel,
|
||||
SD3_T5XXLModel,
|
||||
get_clip_embedding_dim,
|
||||
)
|
||||
from demo_diffusion.model.controlnet import SD3ControlNet
|
||||
from demo_diffusion.model.diffusion_transformer import (
|
||||
CosmosTransformerModel,
|
||||
FluxTransformerModel,
|
||||
SD3_MMDiTModel,
|
||||
SD3TransformerModel,
|
||||
WanTransformerModel,
|
||||
)
|
||||
from demo_diffusion.model.gan import VQGANModel
|
||||
from demo_diffusion.model.load import unload_torch_model
|
||||
from demo_diffusion.model.lora import FLUXLoraLoader, SDLoraLoader, merge_loras
|
||||
from demo_diffusion.model.scheduler import make_scheduler
|
||||
from demo_diffusion.model.t5 import T5Model
|
||||
from demo_diffusion.model.tokenizer import make_tokenizer
|
||||
from demo_diffusion.model.unet import (
|
||||
UNet2DConditionControlNetModel,
|
||||
UNetCascadeModel,
|
||||
UNetModel,
|
||||
UNetTemporalModel,
|
||||
UNetXLModel,
|
||||
UNetXLModelControlNet,
|
||||
)
|
||||
from demo_diffusion.model.vae import (
|
||||
AutoencoderKLWanEncoderModel,
|
||||
AutoencoderKLWanModel,
|
||||
SD3_VAEDecoderModel,
|
||||
SD3_VAEEncoderModel,
|
||||
TorchVAEEncoder,
|
||||
VAEDecTemporalModel,
|
||||
VAEEncoderModel,
|
||||
VAEModel,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
# base_model
|
||||
"BaseModel",
|
||||
# clip
|
||||
"get_clip_embedding_dim",
|
||||
"CLIPModel",
|
||||
"CLIPWithProjModel",
|
||||
"SD3_CLIPGModel",
|
||||
"SD3_CLIPLModel",
|
||||
"SD3_T5XXLModel",
|
||||
"CLIPVisionWithProjModel",
|
||||
"CLIPImageProcessorModel",
|
||||
"CosmosTransformerModel",
|
||||
# diffusion_transformer
|
||||
"SD3_MMDiTModel",
|
||||
"FluxTransformerModel",
|
||||
"SD3TransformerModel",
|
||||
"SD3ControlNet",
|
||||
"WanTransformerModel",
|
||||
# gan
|
||||
"VQGANModel",
|
||||
# lora
|
||||
"SDLoraLoader",
|
||||
"FLUXLoraLoader",
|
||||
"merge_loras",
|
||||
# scheduler
|
||||
"make_scheduler",
|
||||
# t5
|
||||
"T5Model",
|
||||
# tokenizer
|
||||
"make_tokenizer",
|
||||
# unet
|
||||
"UNetModel",
|
||||
"UNetXLModel",
|
||||
"UNetXLModelControlNet",
|
||||
"UNet2DConditionControlNetModel",
|
||||
"UNetTemporalModel",
|
||||
"UNetCascadeModel",
|
||||
# vae
|
||||
"VAEModel",
|
||||
"SD3_VAEDecoderModel",
|
||||
"VAEDecTemporalModel",
|
||||
"TorchVAEEncoder",
|
||||
"VAEEncoderModel",
|
||||
"SD3_VAEEncoderModel",
|
||||
"AutoencoderKLWanModel",
|
||||
"AutoencoderKLWanEncoderModel",
|
||||
# load
|
||||
"unload_torch_model",
|
||||
]
|
||||
@@ -0,0 +1,320 @@
|
||||
#
|
||||
# SPDX-FileCopyrightText: Copyright (c) 1993-2025 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 gc
|
||||
import json
|
||||
import os
|
||||
|
||||
import numpy as np
|
||||
import onnx
|
||||
import torch
|
||||
from diffusers import DiffusionPipeline
|
||||
from onnx import numpy_helper
|
||||
|
||||
from demo_diffusion.model import load, optimizer
|
||||
from demo_diffusion.model.lora import merge_loras
|
||||
|
||||
|
||||
class BaseModel:
|
||||
def __init__(
|
||||
self,
|
||||
version="1.4",
|
||||
pipeline=None,
|
||||
device="cuda",
|
||||
hf_token="",
|
||||
verbose=True,
|
||||
framework_model_dir="pytorch_model",
|
||||
fp16=False,
|
||||
tf32=False,
|
||||
bf16=False,
|
||||
int8=False,
|
||||
fp8=False,
|
||||
fp4=False,
|
||||
max_batch_size=16,
|
||||
text_maxlen=77,
|
||||
embedding_dim=768,
|
||||
compression_factor=8,
|
||||
):
|
||||
|
||||
self.name = self.__class__.__name__
|
||||
self.pipeline_type = pipeline
|
||||
self.pipeline = pipeline.name
|
||||
self.version = version
|
||||
self.path = load.get_path(version, pipeline)
|
||||
self.device = device
|
||||
self.hf_token = hf_token
|
||||
self.hf_safetensor = True
|
||||
self.verbose = verbose
|
||||
self.framework_model_dir = framework_model_dir
|
||||
|
||||
self.fp16 = fp16
|
||||
self.tf32 = tf32
|
||||
self.bf16 = bf16
|
||||
self.int8 = int8
|
||||
self.fp8 = fp8
|
||||
self.fp4 = fp4
|
||||
|
||||
self.compression_factor = compression_factor
|
||||
self.min_batch = 1
|
||||
self.max_batch = max_batch_size
|
||||
self.min_image_shape = 256 # min image resolution: 256x256
|
||||
self.max_image_shape = 1360 # max image resolution: 1360x1360
|
||||
self.min_latent_shape = self.min_image_shape // self.compression_factor
|
||||
self.max_latent_shape = self.max_image_shape // self.compression_factor
|
||||
|
||||
self.text_maxlen = text_maxlen
|
||||
self.embedding_dim = embedding_dim
|
||||
self.extra_output_names = []
|
||||
|
||||
self.do_constant_folding = True
|
||||
|
||||
def get_pipeline(self):
|
||||
model_opts = {"variant": "fp16", "torch_dtype": torch.float16} if self.fp16 else {}
|
||||
model_opts = {"torch_dtype": torch.bfloat16} if self.bf16 else model_opts
|
||||
return DiffusionPipeline.from_pretrained(
|
||||
self.path,
|
||||
use_safetensors=self.hf_safetensor,
|
||||
token=self.hf_token,
|
||||
**model_opts,
|
||||
).to(self.device)
|
||||
|
||||
def get_model(self, torch_inference=""):
|
||||
pass
|
||||
|
||||
def get_input_names(self):
|
||||
pass
|
||||
|
||||
def get_output_names(self):
|
||||
pass
|
||||
|
||||
def get_dynamic_axes(self):
|
||||
return None
|
||||
|
||||
def get_sample_input(self, batch_size, image_height, image_width, static_shape):
|
||||
pass
|
||||
|
||||
def get_input_profile(self, batch_size, image_height, image_width, static_batch, static_shape):
|
||||
return None
|
||||
|
||||
def get_shape_dict(self, batch_size, image_height, image_width):
|
||||
return None
|
||||
|
||||
# Helper utility for ONNX export
|
||||
def export_onnx(
|
||||
self,
|
||||
onnx_path,
|
||||
onnx_opt_path,
|
||||
onnx_opset,
|
||||
opt_image_height,
|
||||
opt_image_width,
|
||||
opt_num_frames=None,
|
||||
custom_model=None,
|
||||
enable_lora_merge=False,
|
||||
static_shape=False,
|
||||
lora_loader=None,
|
||||
dynamo=False,
|
||||
):
|
||||
onnx_opt_graph = None
|
||||
# Export optimized ONNX model (if missing)
|
||||
if not os.path.exists(onnx_opt_path):
|
||||
if not os.path.exists(onnx_path):
|
||||
print(f"[I] Exporting ONNX model: {onnx_path}")
|
||||
|
||||
def export_onnx(model):
|
||||
if enable_lora_merge:
|
||||
assert lora_loader is not None
|
||||
model = merge_loras(model, lora_loader)
|
||||
|
||||
export_kwargs = {}
|
||||
if dynamo:
|
||||
export_kwargs["dynamic_shapes"] = self.get_dynamic_axes()
|
||||
else:
|
||||
export_kwargs["dynamic_axes"] = self.get_dynamic_axes()
|
||||
|
||||
inputs = self.get_sample_input(
|
||||
1, opt_image_height, opt_image_width, static_shape,
|
||||
**({'num_frames': opt_num_frames} if opt_num_frames else {})
|
||||
)
|
||||
|
||||
with torch.no_grad():
|
||||
torch.onnx.export(
|
||||
model,
|
||||
inputs,
|
||||
onnx_path,
|
||||
export_params=True,
|
||||
do_constant_folding=self.do_constant_folding,
|
||||
input_names=self.get_input_names(),
|
||||
output_names=self.get_output_names(),
|
||||
verbose=False,
|
||||
dynamo=dynamo,
|
||||
opset_version=onnx_opset,
|
||||
**export_kwargs,
|
||||
)
|
||||
|
||||
if custom_model:
|
||||
with torch.inference_mode():
|
||||
export_onnx(custom_model)
|
||||
else:
|
||||
# WAR: Enable autocast for BF16 Stable Cascade pipeline
|
||||
do_autocast = True if self.version == "cascade" and self.bf16 else False
|
||||
model = self.get_model()
|
||||
with torch.inference_mode(), torch.autocast("cuda", enabled=do_autocast):
|
||||
export_onnx(model)
|
||||
del model
|
||||
gc.collect()
|
||||
torch.cuda.empty_cache()
|
||||
else:
|
||||
print(f"[I] Found cached ONNX model: {onnx_path}")
|
||||
|
||||
print(f"[I] Optimizing ONNX model: {onnx_opt_path}")
|
||||
onnx_opt_graph = self.optimize(onnx.load(onnx_path))
|
||||
if load.onnx_graph_needs_external_data(onnx_opt_graph):
|
||||
onnx.save_model(
|
||||
onnx_opt_graph,
|
||||
onnx_opt_path,
|
||||
save_as_external_data=True,
|
||||
all_tensors_to_one_file=True,
|
||||
convert_attribute=False,
|
||||
)
|
||||
else:
|
||||
onnx.save(onnx_opt_graph, onnx_opt_path)
|
||||
else:
|
||||
print(f"[I] Found cached optimized ONNX model: {onnx_opt_path} ")
|
||||
|
||||
# Helper utility for weights map
|
||||
def export_weights_map(self, onnx_opt_path, weights_map_path):
|
||||
if not os.path.exists(weights_map_path):
|
||||
onnx_opt_dir = os.path.dirname(onnx_opt_path)
|
||||
onnx_opt_model = onnx.load(onnx_opt_path)
|
||||
state_dict = self.get_model().state_dict()
|
||||
# Create initializer data hashes
|
||||
initializer_hash_mapping = {}
|
||||
for initializer in onnx_opt_model.graph.initializer:
|
||||
initializer_data = numpy_helper.to_array(initializer, base_dir=onnx_opt_dir).astype(np.float16)
|
||||
initializer_hash = hash(initializer_data.data.tobytes())
|
||||
initializer_hash_mapping[initializer.name] = (initializer_hash, initializer_data.shape)
|
||||
|
||||
weights_name_mapping = {}
|
||||
weights_shape_mapping = {}
|
||||
# set to keep track of initializers already added to the name_mapping dict
|
||||
initializers_mapped = set()
|
||||
for wt_name, wt in state_dict.items():
|
||||
# get weight hash
|
||||
wt = wt.cpu().detach().numpy().astype(np.float16)
|
||||
wt_hash = hash(wt.data.tobytes())
|
||||
wt_t_hash = hash(np.transpose(wt).data.tobytes())
|
||||
|
||||
for initializer_name, (initializer_hash, initializer_shape) in initializer_hash_mapping.items():
|
||||
# Due to constant folding, some weights are transposed during export
|
||||
# To account for the transpose op, we compare the initializer hash to the
|
||||
# hash for the weight and its transpose
|
||||
if wt_hash == initializer_hash or wt_t_hash == initializer_hash:
|
||||
# The assert below ensures there is a 1:1 mapping between
|
||||
# PyTorch and ONNX weight names. It can be removed in cases where 1:many
|
||||
# mapping is found and name_mapping[wt_name] = list()
|
||||
assert initializer_name not in initializers_mapped
|
||||
weights_name_mapping[wt_name] = initializer_name
|
||||
initializers_mapped.add(initializer_name)
|
||||
is_transpose = False if wt_hash == initializer_hash else True
|
||||
weights_shape_mapping[wt_name] = (initializer_shape, is_transpose)
|
||||
|
||||
# Sanity check: Were any weights not matched
|
||||
if wt_name not in weights_name_mapping:
|
||||
print(f"[I] PyTorch weight {wt_name} not matched with any ONNX initializer")
|
||||
print(f"[I] {len(weights_name_mapping.keys())} PyTorch weights were matched with ONNX initializers")
|
||||
assert weights_name_mapping.keys() == weights_shape_mapping.keys()
|
||||
with open(weights_map_path, "w") as fp:
|
||||
json.dump([weights_name_mapping, weights_shape_mapping], fp)
|
||||
else:
|
||||
print(f"[I] Found cached weights map: {weights_map_path} ")
|
||||
|
||||
def optimize(self, onnx_graph, return_onnx=True, **kwargs):
|
||||
opt = optimizer.Optimizer(onnx_graph, verbose=self.verbose, version=self.version)
|
||||
opt.info(self.name + ": original")
|
||||
opt.cleanup()
|
||||
opt.info(self.name + ": cleanup")
|
||||
if kwargs.get("modify_fp8_graph", False):
|
||||
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")
|
||||
elif self.bf16:
|
||||
# Cast Resize I/O for strongly-typed TRT builds: BF16 -> FP32 inputs, FP32 -> BF16 outputs.
|
||||
# TRT does not support BF16 for the Resize operator.
|
||||
opt.infer_shapes()
|
||||
opt.cast_resize_io(output_dtype=onnx.TensorProto.BFLOAT16)
|
||||
opt.info(self.name + ": cast resize I/O for bf16")
|
||||
if self.version.startswith("flux.1") and self.fp8:
|
||||
opt.flux_convert_rope_weight_type()
|
||||
opt.info(self.name + ": convert rope weight type for fp8 flux")
|
||||
opt.fold_constants()
|
||||
opt.info(self.name + ": fold constants")
|
||||
opt.infer_shapes()
|
||||
opt.info(self.name + ": shape inference")
|
||||
if kwargs.get("modify_int8_graph", False):
|
||||
opt.modify_int8_graph()
|
||||
opt.info(self.name + ": modify int8 graph")
|
||||
onnx_opt_graph = opt.cleanup(return_onnx=return_onnx)
|
||||
opt.info(self.name + ": finished")
|
||||
return onnx_opt_graph
|
||||
|
||||
def check_dims(self, batch_size, image_height, image_width, num_frames=None):
|
||||
assert batch_size >= self.min_batch and batch_size <= self.max_batch
|
||||
latent_height = image_height // self.compression_factor
|
||||
latent_width = image_width // self.compression_factor
|
||||
assert latent_height >= self.min_latent_shape and latent_height <= self.max_latent_shape
|
||||
assert latent_width >= self.min_latent_shape and latent_width <= self.max_latent_shape
|
||||
|
||||
if num_frames:
|
||||
latent_frames = (self.num_frames - 1) // self.temporal_compression_factor + 1
|
||||
return (latent_height, latent_width, latent_frames)
|
||||
|
||||
return (latent_height, latent_width)
|
||||
|
||||
def get_minmax_dims(self, batch_size, image_height, image_width, static_batch, static_shape, num_frames=None):
|
||||
min_batch = batch_size if static_batch else self.min_batch
|
||||
max_batch = batch_size if static_batch else self.max_batch
|
||||
latent_height = image_height // self.compression_factor
|
||||
latent_width = image_width // self.compression_factor
|
||||
min_image_height = image_height if static_shape else self.min_image_shape
|
||||
max_image_height = image_height if static_shape else self.max_image_shape
|
||||
min_image_width = image_width if static_shape else self.min_image_shape
|
||||
max_image_width = image_width if static_shape else self.max_image_shape
|
||||
min_latent_height = latent_height if static_shape else self.min_latent_shape
|
||||
max_latent_height = latent_height if static_shape else self.max_latent_shape
|
||||
min_latent_width = latent_width if static_shape else self.min_latent_shape
|
||||
max_latent_width = latent_width if static_shape else self.max_latent_shape
|
||||
|
||||
frame_dims = ()
|
||||
if num_frames:
|
||||
latent_frames = (num_frames - 1) // self.temporal_compression_factor + 1
|
||||
min_latent_frames = latent_frames if static_shape else self.min_latent_frames
|
||||
max_latent_frames = latent_frames if static_shape else self.max_latent_frames
|
||||
frame_dims = (min_latent_frames, max_latent_frames)
|
||||
|
||||
return (
|
||||
min_batch,
|
||||
max_batch,
|
||||
min_image_height,
|
||||
max_image_height,
|
||||
min_image_width,
|
||||
max_image_width,
|
||||
min_latent_height,
|
||||
max_latent_height,
|
||||
min_latent_width,
|
||||
max_latent_width,
|
||||
*frame_dims
|
||||
)
|
||||
@@ -0,0 +1,555 @@
|
||||
#
|
||||
# SPDX-FileCopyrightText: Copyright (c) 1993-2025 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 os
|
||||
|
||||
import torch
|
||||
from huggingface_hub import hf_hub_download
|
||||
from safetensors import safe_open
|
||||
from transformers import (
|
||||
CLIPImageProcessor,
|
||||
CLIPTextModel,
|
||||
CLIPTextModelWithProjection,
|
||||
CLIPVisionModelWithProjection,
|
||||
)
|
||||
|
||||
from demo_diffusion.model import base_model, load, optimizer
|
||||
from demo_diffusion.utils_sd3.other_impls import (
|
||||
SDClipModel,
|
||||
SDXLClipG,
|
||||
T5XXLModel,
|
||||
load_into,
|
||||
)
|
||||
|
||||
|
||||
def get_clipwithproj_embedding_dim(version: str, subfolder: str) -> int:
|
||||
"""Return the embedding dimension of a CLIP with projection model."""
|
||||
if version in ("xl-1.0", "xl-turbo", "cascade"):
|
||||
return 1280
|
||||
elif version in {"3.5-medium", "3.5-large"} and subfolder == "text_encoder":
|
||||
return 768
|
||||
elif version in {"3.5-medium", "3.5-large"} and subfolder == "text_encoder_2":
|
||||
return 1280
|
||||
else:
|
||||
raise ValueError(f"Invalid version {version} + subfolder {subfolder}")
|
||||
|
||||
|
||||
def get_clip_embedding_dim(version, pipeline):
|
||||
if version in (
|
||||
"1.4",
|
||||
"dreamshaper-7",
|
||||
"flux.1-dev",
|
||||
"flux.1-schnell",
|
||||
"flux.1-dev-canny",
|
||||
"flux.1-dev-depth",
|
||||
"flux.1-kontext-dev",
|
||||
):
|
||||
return 768
|
||||
elif version in ("xl-1.0", "xl-turbo") and pipeline.is_sd_xl_base():
|
||||
return 768
|
||||
elif version in ("sd3"):
|
||||
return 4096
|
||||
else:
|
||||
raise ValueError(f"Invalid version {version} + pipeline {pipeline}")
|
||||
|
||||
|
||||
class CLIPModel(base_model.BaseModel):
|
||||
def __init__(
|
||||
self,
|
||||
version,
|
||||
pipeline,
|
||||
device,
|
||||
hf_token,
|
||||
verbose,
|
||||
framework_model_dir,
|
||||
max_batch_size,
|
||||
embedding_dim,
|
||||
fp16=False,
|
||||
tf32=False,
|
||||
bf16=False,
|
||||
output_hidden_states=False,
|
||||
keep_pooled_output=False,
|
||||
subfolder="text_encoder",
|
||||
):
|
||||
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
|
||||
self.hidden_layer_offset = 0 if pipeline.is_cascade() else -1
|
||||
self.keep_pooled_output = keep_pooled_output
|
||||
|
||||
# Output the final hidden state
|
||||
if output_hidden_states:
|
||||
self.extra_output_names = ["hidden_states"]
|
||||
|
||||
def get_model(self, torch_inference=""):
|
||||
model_opts = (
|
||||
{"torch_dtype": torch.float16} if self.fp16 else {"torch_dtype": torch.bfloat16} if self.bf16 else {}
|
||||
)
|
||||
clip_model_dir = load.get_checkpoint_dir(self.framework_model_dir, self.version, self.pipeline, self.subfolder)
|
||||
if not load.is_model_cached(clip_model_dir, model_opts, self.hf_safetensor, model_name="model"):
|
||||
model = CLIPTextModel.from_pretrained(
|
||||
self.path,
|
||||
subfolder=self.subfolder,
|
||||
use_safetensors=self.hf_safetensor,
|
||||
token=self.hf_token,
|
||||
attn_implementation="eager",
|
||||
**model_opts,
|
||||
).to(self.device)
|
||||
model.save_pretrained(clip_model_dir, **model_opts)
|
||||
else:
|
||||
print(f"[I] Load CLIPTextModel model from: {clip_model_dir}")
|
||||
model = CLIPTextModel.from_pretrained(clip_model_dir, **model_opts).to(self.device)
|
||||
model = optimizer.optimize_checkpoint(model, torch_inference)
|
||||
return model
|
||||
|
||||
def get_input_names(self):
|
||||
return ["input_ids"]
|
||||
|
||||
def get_output_names(self):
|
||||
output_names = ["text_embeddings"]
|
||||
if self.keep_pooled_output:
|
||||
output_names += ["pooled_embeddings"]
|
||||
return output_names
|
||||
|
||||
def get_dynamic_axes(self):
|
||||
dynamic_axes = {
|
||||
"input_ids": {0: "B"},
|
||||
"text_embeddings": {0: "B"},
|
||||
}
|
||||
if self.keep_pooled_output:
|
||||
dynamic_axes["pooled_embeddings"] = {0: "B"}
|
||||
return dynamic_axes
|
||||
|
||||
def get_input_profile(self, batch_size, image_height, image_width, static_batch, static_shape):
|
||||
self.check_dims(batch_size, image_height, image_width)
|
||||
min_batch, max_batch, _, _, _, _, _, _, _, _ = self.get_minmax_dims(
|
||||
batch_size, image_height, image_width, static_batch, static_shape
|
||||
)
|
||||
return {
|
||||
"input_ids": [(min_batch, self.text_maxlen), (batch_size, self.text_maxlen), (max_batch, self.text_maxlen)]
|
||||
}
|
||||
|
||||
def get_shape_dict(self, batch_size, image_height, image_width):
|
||||
self.check_dims(batch_size, image_height, image_width)
|
||||
output = {
|
||||
"input_ids": (batch_size, self.text_maxlen),
|
||||
"text_embeddings": (batch_size, self.text_maxlen, self.embedding_dim),
|
||||
}
|
||||
if self.keep_pooled_output:
|
||||
output["pooled_embeddings"] = (batch_size, self.embedding_dim)
|
||||
if "hidden_states" in self.extra_output_names:
|
||||
output["hidden_states"] = (batch_size, self.text_maxlen, self.embedding_dim)
|
||||
return output
|
||||
|
||||
def get_sample_input(self, batch_size, image_height, image_width, static_shape):
|
||||
self.check_dims(batch_size, image_height, image_width)
|
||||
return torch.zeros(batch_size, self.text_maxlen, dtype=torch.int32, device=self.device)
|
||||
|
||||
def optimize(self, onnx_graph):
|
||||
opt = optimizer.Optimizer(onnx_graph, verbose=self.verbose, version=self.version)
|
||||
opt.info(self.name + ": original")
|
||||
keep_outputs = [0, 1] if self.keep_pooled_output else [0]
|
||||
opt.select_outputs(keep_outputs)
|
||||
opt.cleanup()
|
||||
opt.fold_constants()
|
||||
opt.info(self.name + ": fold constants")
|
||||
opt.infer_shapes()
|
||||
opt.info(self.name + ": shape inference")
|
||||
opt.select_outputs(keep_outputs, names=self.get_output_names()) # rename network outputs
|
||||
opt.info(self.name + ": rename network output(s)")
|
||||
opt_onnx_graph = opt.cleanup(return_onnx=True)
|
||||
if "hidden_states" in self.extra_output_names:
|
||||
opt_onnx_graph = opt.clip_add_hidden_states(self.hidden_layer_offset, return_onnx=True)
|
||||
opt.info(self.name + ": added hidden_states")
|
||||
opt.info(self.name + ": finished")
|
||||
return opt_onnx_graph
|
||||
|
||||
class CLIPWithProjModel(CLIPModel):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
version,
|
||||
pipeline,
|
||||
device,
|
||||
hf_token,
|
||||
verbose,
|
||||
framework_model_dir,
|
||||
fp16=False,
|
||||
bf16=False,
|
||||
max_batch_size=16,
|
||||
output_hidden_states=False,
|
||||
subfolder="text_encoder_2",
|
||||
):
|
||||
|
||||
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, subfolder),
|
||||
output_hidden_states=output_hidden_states,
|
||||
)
|
||||
self.subfolder = subfolder
|
||||
|
||||
def get_model(self, torch_inference=""):
|
||||
model_opts = {"variant": "fp16", "torch_dtype": torch.float16} if self.fp16 else {"torch_dtype": torch.bfloat16}
|
||||
clip_model_dir = load.get_checkpoint_dir(self.framework_model_dir, self.version, self.pipeline, self.subfolder)
|
||||
if not load.is_model_cached(clip_model_dir, model_opts, self.hf_safetensor, model_name="model"):
|
||||
model = CLIPTextModelWithProjection.from_pretrained(
|
||||
self.path,
|
||||
subfolder=self.subfolder,
|
||||
use_safetensors=self.hf_safetensor,
|
||||
token=self.hf_token,
|
||||
attn_implementation="eager",
|
||||
**model_opts,
|
||||
).to(self.device)
|
||||
model.save_pretrained(clip_model_dir, **model_opts)
|
||||
else:
|
||||
print(f"[I] Load CLIPTextModelWithProjection model from: {clip_model_dir}")
|
||||
model = CLIPTextModelWithProjection.from_pretrained(clip_model_dir, **model_opts).to(self.device)
|
||||
model = optimizer.optimize_checkpoint(model, torch_inference)
|
||||
return model
|
||||
|
||||
def get_input_names(self):
|
||||
return ["input_ids", "attention_mask"]
|
||||
|
||||
def get_output_names(self):
|
||||
return ["text_embeddings"]
|
||||
|
||||
def get_dynamic_axes(self):
|
||||
return {
|
||||
"input_ids": {0: "B"},
|
||||
"attention_mask": {0: "B"},
|
||||
"text_embeddings": {0: "B"},
|
||||
}
|
||||
|
||||
def get_input_profile(self, batch_size, image_height, image_width, static_batch, static_shape):
|
||||
self.check_dims(batch_size, image_height, image_width)
|
||||
min_batch, max_batch, _, _, _, _, _, _, _, _ = self.get_minmax_dims(
|
||||
batch_size, image_height, image_width, static_batch, static_shape
|
||||
)
|
||||
return {
|
||||
"input_ids": [(min_batch, self.text_maxlen), (batch_size, self.text_maxlen), (max_batch, self.text_maxlen)],
|
||||
"attention_mask": [
|
||||
(min_batch, self.text_maxlen),
|
||||
(batch_size, self.text_maxlen),
|
||||
(max_batch, self.text_maxlen),
|
||||
],
|
||||
}
|
||||
|
||||
def get_shape_dict(self, batch_size, image_height, image_width):
|
||||
self.check_dims(batch_size, image_height, image_width)
|
||||
output = {
|
||||
"input_ids": (batch_size, self.text_maxlen),
|
||||
"attention_mask": (batch_size, self.text_maxlen),
|
||||
"text_embeddings": (batch_size, self.embedding_dim),
|
||||
}
|
||||
if "hidden_states" in self.extra_output_names:
|
||||
output["hidden_states"] = (batch_size, self.text_maxlen, self.embedding_dim)
|
||||
return output
|
||||
|
||||
def get_sample_input(self, batch_size, image_height, image_width, static_shape):
|
||||
self.check_dims(batch_size, image_height, image_width)
|
||||
return (
|
||||
torch.zeros(batch_size, self.text_maxlen, dtype=torch.int32, device=self.device),
|
||||
torch.zeros(batch_size, self.text_maxlen, dtype=torch.int32, device=self.device),
|
||||
)
|
||||
|
||||
class SD3_CLIPGModel(CLIPModel):
|
||||
def __init__(
|
||||
self,
|
||||
version,
|
||||
pipeline,
|
||||
device,
|
||||
hf_token,
|
||||
verbose,
|
||||
framework_model_dir,
|
||||
max_batch_size,
|
||||
embedding_dim=None,
|
||||
fp16=False,
|
||||
pooled_output=False,
|
||||
):
|
||||
self.CLIPG_CONFIG = {
|
||||
"hidden_act": "gelu",
|
||||
"hidden_size": 1280,
|
||||
"intermediate_size": 5120,
|
||||
"num_attention_heads": 20,
|
||||
"num_hidden_layers": 32,
|
||||
}
|
||||
super(SD3_CLIPGModel, self).__init__(
|
||||
version,
|
||||
pipeline,
|
||||
device=device,
|
||||
hf_token=hf_token,
|
||||
verbose=verbose,
|
||||
framework_model_dir=framework_model_dir,
|
||||
fp16=fp16,
|
||||
max_batch_size=max_batch_size,
|
||||
embedding_dim=self.CLIPG_CONFIG["hidden_size"] if embedding_dim is None else embedding_dim,
|
||||
)
|
||||
self.subfolder = "text_encoders"
|
||||
if pooled_output:
|
||||
self.extra_output_names = ["pooled_output"]
|
||||
|
||||
def get_model(self, torch_inference=""):
|
||||
clip_g_model_dir = load.get_checkpoint_dir(
|
||||
self.framework_model_dir, self.version, self.pipeline, self.subfolder
|
||||
)
|
||||
clip_g_filename = "clip_g.safetensors"
|
||||
clip_g_model_path = f"{clip_g_model_dir}/{clip_g_filename}"
|
||||
if not os.path.exists(clip_g_model_path):
|
||||
hf_hub_download(
|
||||
repo_id=self.path,
|
||||
filename=clip_g_filename,
|
||||
local_dir=load.get_checkpoint_dir(self.framework_model_dir, self.version, self.pipeline, ""),
|
||||
subfolder=self.subfolder,
|
||||
)
|
||||
with safe_open(clip_g_model_path, framework="pt", device=self.device) as f:
|
||||
dtype = torch.float16 if self.fp16 else torch.float32
|
||||
model = SDXLClipG(self.CLIPG_CONFIG, device=self.device, dtype=dtype)
|
||||
load_into(f, model.transformer, "", self.device, dtype)
|
||||
model = optimizer.optimize_checkpoint(model, torch_inference)
|
||||
return model
|
||||
|
||||
def get_shape_dict(self, batch_size, image_height, image_width):
|
||||
self.check_dims(batch_size, image_height, image_width)
|
||||
output = {
|
||||
"input_ids": (batch_size, self.text_maxlen),
|
||||
"text_embeddings": (batch_size, self.text_maxlen, self.embedding_dim),
|
||||
}
|
||||
if "pooled_output" in self.extra_output_names:
|
||||
output["pooled_output"] = (batch_size, self.embedding_dim)
|
||||
|
||||
return output
|
||||
|
||||
def optimize(self, onnx_graph):
|
||||
opt = optimizer.Optimizer(onnx_graph, verbose=self.verbose, version=self.version)
|
||||
opt.info(self.name + ": original")
|
||||
opt.select_outputs([0, 1])
|
||||
opt.cleanup()
|
||||
opt.fold_constants()
|
||||
opt.info(self.name + ": fold constants")
|
||||
opt.infer_shapes()
|
||||
opt.info(self.name + ": shape inference")
|
||||
opt.select_outputs([0, 1], names=["text_embeddings", "pooled_output"]) # rename network output
|
||||
opt.info(self.name + ": rename output[0] and output[1]")
|
||||
opt_onnx_graph = opt.cleanup(return_onnx=True)
|
||||
opt.info(self.name + ": finished")
|
||||
return opt_onnx_graph
|
||||
|
||||
|
||||
class SD3_CLIPLModel(SD3_CLIPGModel):
|
||||
def __init__(
|
||||
self,
|
||||
version,
|
||||
pipeline,
|
||||
device,
|
||||
hf_token,
|
||||
verbose,
|
||||
framework_model_dir,
|
||||
max_batch_size,
|
||||
fp16=False,
|
||||
pooled_output=False,
|
||||
):
|
||||
self.CLIPL_CONFIG = {
|
||||
"hidden_act": "quick_gelu",
|
||||
"hidden_size": 768,
|
||||
"intermediate_size": 3072,
|
||||
"num_attention_heads": 12,
|
||||
"num_hidden_layers": 12,
|
||||
}
|
||||
super(SD3_CLIPLModel, self).__init__(
|
||||
version,
|
||||
pipeline,
|
||||
device=device,
|
||||
hf_token=hf_token,
|
||||
verbose=verbose,
|
||||
framework_model_dir=framework_model_dir,
|
||||
fp16=fp16,
|
||||
max_batch_size=max_batch_size,
|
||||
embedding_dim=self.CLIPL_CONFIG["hidden_size"],
|
||||
)
|
||||
self.subfolder = "text_encoders"
|
||||
if pooled_output:
|
||||
self.extra_output_names = ["pooled_output"]
|
||||
|
||||
def get_model(self, torch_inference=""):
|
||||
clip_l_model_dir = load.get_checkpoint_dir(
|
||||
self.framework_model_dir, self.version, self.pipeline, self.subfolder
|
||||
)
|
||||
clip_l_filename = "clip_l.safetensors"
|
||||
clip_l_model_path = f"{clip_l_model_dir}/{clip_l_filename}"
|
||||
if not os.path.exists(clip_l_model_path):
|
||||
hf_hub_download(
|
||||
repo_id=self.path,
|
||||
filename=clip_l_filename,
|
||||
local_dir=load.get_checkpoint_dir(self.framework_model_dir, self.version, self.pipeline, ""),
|
||||
subfolder=self.subfolder,
|
||||
)
|
||||
with safe_open(clip_l_model_path, framework="pt", device=self.device) as f:
|
||||
dtype = torch.float16 if self.fp16 else torch.float32
|
||||
model = SDClipModel(
|
||||
layer="hidden",
|
||||
layer_idx=-2,
|
||||
device=self.device,
|
||||
dtype=dtype,
|
||||
layer_norm_hidden_state=False,
|
||||
return_projected_pooled=False,
|
||||
textmodel_json_config=self.CLIPL_CONFIG,
|
||||
)
|
||||
load_into(f, model.transformer, "", self.device, dtype)
|
||||
model = optimizer.optimize_checkpoint(model, torch_inference)
|
||||
return model
|
||||
|
||||
|
||||
# NOTE: For legacy reasons, even though this is a T5 model, it inherits from CLIPModel.
|
||||
class SD3_T5XXLModel(CLIPModel):
|
||||
def __init__(
|
||||
self,
|
||||
version,
|
||||
pipeline,
|
||||
device,
|
||||
hf_token,
|
||||
verbose,
|
||||
framework_model_dir,
|
||||
max_batch_size,
|
||||
embedding_dim,
|
||||
fp16=False,
|
||||
):
|
||||
super(SD3_T5XXLModel, self).__init__(
|
||||
version,
|
||||
pipeline,
|
||||
device=device,
|
||||
hf_token=hf_token,
|
||||
verbose=verbose,
|
||||
framework_model_dir=framework_model_dir,
|
||||
fp16=fp16,
|
||||
max_batch_size=max_batch_size,
|
||||
embedding_dim=embedding_dim,
|
||||
)
|
||||
self.T5_CONFIG = {"d_ff": 10240, "d_model": 4096, "num_heads": 64, "num_layers": 24, "vocab_size": 32128}
|
||||
self.subfolder = "text_encoders"
|
||||
|
||||
def get_model(self, torch_inference=""):
|
||||
t5xxl_model_dir = load.get_checkpoint_dir(self.framework_model_dir, self.version, self.pipeline, self.subfolder)
|
||||
t5xxl_filename = "t5xxl_fp16.safetensors"
|
||||
t5xxl_model_path = f"{t5xxl_model_dir}/{t5xxl_filename}"
|
||||
if not os.path.exists(t5xxl_model_path):
|
||||
hf_hub_download(
|
||||
repo_id=self.path,
|
||||
filename=t5xxl_filename,
|
||||
local_dir=load.get_checkpoint_dir(self.framework_model_dir, self.version, self.pipeline, ""),
|
||||
subfolder=self.subfolder,
|
||||
)
|
||||
with safe_open(t5xxl_model_path, framework="pt", device=self.device) as f:
|
||||
dtype = torch.float16 if self.fp16 else torch.float32
|
||||
model = T5XXLModel(self.T5_CONFIG, device=self.device, dtype=dtype)
|
||||
load_into(f, model.transformer, "", self.device, dtype)
|
||||
model = optimizer.optimize_checkpoint(model, torch_inference)
|
||||
return model
|
||||
|
||||
|
||||
class CLIPVisionWithProjModel(base_model.BaseModel):
|
||||
def __init__(
|
||||
self,
|
||||
version,
|
||||
pipeline,
|
||||
device,
|
||||
hf_token,
|
||||
verbose,
|
||||
framework_model_dir,
|
||||
max_batch_size=1,
|
||||
subfolder="image_encoder",
|
||||
):
|
||||
|
||||
super(CLIPVisionWithProjModel, self).__init__(
|
||||
version,
|
||||
pipeline,
|
||||
device=device,
|
||||
hf_token=hf_token,
|
||||
verbose=verbose,
|
||||
framework_model_dir=framework_model_dir,
|
||||
max_batch_size=max_batch_size,
|
||||
)
|
||||
self.subfolder = subfolder
|
||||
|
||||
def get_model(self, torch_inference=""):
|
||||
clip_model_dir = load.get_checkpoint_dir(self.framework_model_dir, self.version, self.pipeline, self.subfolder)
|
||||
if not os.path.exists(clip_model_dir):
|
||||
model = CLIPVisionModelWithProjection.from_pretrained(
|
||||
self.path, subfolder=self.subfolder, use_safetensors=self.hf_safetensor, token=self.hf_token
|
||||
).to(self.device)
|
||||
model.save_pretrained(clip_model_dir)
|
||||
else:
|
||||
print(f"[I] Load CLIPVisionModelWithProjection model from: {clip_model_dir}")
|
||||
model = CLIPVisionModelWithProjection.from_pretrained(clip_model_dir).to(self.device)
|
||||
model = optimizer.optimize_checkpoint(model, torch_inference)
|
||||
return model
|
||||
|
||||
|
||||
class CLIPImageProcessorModel(base_model.BaseModel):
|
||||
def __init__(
|
||||
self,
|
||||
version,
|
||||
pipeline,
|
||||
device,
|
||||
hf_token,
|
||||
verbose,
|
||||
framework_model_dir,
|
||||
max_batch_size=1,
|
||||
subfolder="feature_extractor",
|
||||
):
|
||||
|
||||
super(CLIPImageProcessorModel, self).__init__(
|
||||
version,
|
||||
pipeline,
|
||||
device=device,
|
||||
hf_token=hf_token,
|
||||
verbose=verbose,
|
||||
framework_model_dir=framework_model_dir,
|
||||
max_batch_size=max_batch_size,
|
||||
)
|
||||
self.subfolder = subfolder
|
||||
|
||||
def get_model(self, torch_inference=""):
|
||||
clip_model_dir = load.get_checkpoint_dir(self.framework_model_dir, self.version, self.pipeline, self.subfolder)
|
||||
# NOTE to(device) not supported
|
||||
if not os.path.exists(clip_model_dir):
|
||||
model = CLIPImageProcessor.from_pretrained(
|
||||
self.path, subfolder=self.subfolder, use_safetensors=self.hf_safetensor, token=self.hf_token
|
||||
)
|
||||
model.save_pretrained(clip_model_dir)
|
||||
else:
|
||||
print(f"[I] Load CLIPImageProcessor model from: {clip_model_dir}")
|
||||
model = CLIPImageProcessor.from_pretrained(clip_model_dir)
|
||||
return model
|
||||
@@ -0,0 +1,223 @@
|
||||
#
|
||||
# SPDX-FileCopyrightText: Copyright (c) 1993-2025 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 os
|
||||
|
||||
import torch
|
||||
|
||||
from demo_diffusion.dynamic_import import import_from_diffusers
|
||||
from demo_diffusion.model import base_model, load, optimizer
|
||||
|
||||
# List of models to import from diffusers.models
|
||||
models_to_import = ["SD3Transformer2DModel", "SD3ControlNetModel"]
|
||||
for model in models_to_import:
|
||||
globals()[model] = import_from_diffusers(model, "diffusers.models")
|
||||
|
||||
|
||||
class SD3ControlNetWrapper(torch.nn.Module):
|
||||
def __init__(self, controlnet):
|
||||
super().__init__()
|
||||
self.controlnet = controlnet
|
||||
|
||||
def forward(self, hidden_states, controlnet_cond, conditioning_scale, pooled_projections, timestep):
|
||||
params = {
|
||||
"hidden_states": hidden_states,
|
||||
"pooled_projections": pooled_projections,
|
||||
"timestep": timestep,
|
||||
"controlnet_cond": controlnet_cond,
|
||||
"conditioning_scale": conditioning_scale,
|
||||
}
|
||||
out = self.controlnet(**params)["controlnet_block_samples"]
|
||||
return torch.stack(out, dim=0)
|
||||
|
||||
|
||||
class SD3ControlNet(base_model.BaseModel):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
version,
|
||||
controlnet,
|
||||
pipeline,
|
||||
device,
|
||||
hf_token,
|
||||
verbose,
|
||||
framework_model_dir,
|
||||
fp16=False,
|
||||
tf32=False,
|
||||
bf16=False,
|
||||
int8=False,
|
||||
fp8=False,
|
||||
max_batch_size=16,
|
||||
do_classifier_free_guidance=False,
|
||||
):
|
||||
super(SD3ControlNet, self).__init__(
|
||||
version,
|
||||
pipeline,
|
||||
device=device,
|
||||
hf_token=hf_token,
|
||||
verbose=verbose,
|
||||
framework_model_dir=framework_model_dir,
|
||||
fp16=fp16,
|
||||
tf32=tf32,
|
||||
bf16=bf16,
|
||||
int8=int8,
|
||||
fp8=fp8,
|
||||
max_batch_size=max_batch_size,
|
||||
)
|
||||
self.path = load.get_path(version, pipeline, controlnet)
|
||||
self.subfolder = "controlnet_{}".format(controlnet)
|
||||
self.controlnet_model_dir = load.get_checkpoint_dir(
|
||||
self.framework_model_dir, self.version, self.pipeline, self.subfolder
|
||||
)
|
||||
self.transformer_model_dir = load.get_checkpoint_dir(
|
||||
self.framework_model_dir, self.version, self.pipeline, "transformer"
|
||||
)
|
||||
if not os.path.exists(self.controlnet_model_dir):
|
||||
self.config = SD3ControlNetModel.load_config(self.path, token=self.hf_token)
|
||||
else:
|
||||
print(f"[I] Load SD3ControlNetModel config from: {self.controlnet_model_dir}")
|
||||
self.config = SD3ControlNetModel.load_config(self.controlnet_model_dir)
|
||||
self.xB = 2 if do_classifier_free_guidance else 1 # batch multiplier
|
||||
|
||||
def get_model(self, torch_inference=""):
|
||||
model_opts = (
|
||||
{"torch_dtype": torch.float16} if self.fp16 else {"torch_dtype": torch.bfloat16} if self.bf16 else {}
|
||||
)
|
||||
if not load.is_model_cached(self.controlnet_model_dir, model_opts, self.hf_safetensor):
|
||||
model = SD3ControlNetModel.from_pretrained(self.path, **model_opts, use_safetensors=self.hf_safetensor).to(
|
||||
self.device
|
||||
)
|
||||
model.save_pretrained(self.controlnet_model_dir, **model_opts)
|
||||
else:
|
||||
print(f"[I] Load SD3ControlNetModel model from: {self.controlnet_model_dir}")
|
||||
model = SD3ControlNetModel.from_pretrained(self.controlnet_model_dir, **model_opts).to(self.device)
|
||||
|
||||
# Load transformer model for pos_embed
|
||||
transformer = SD3Transformer2DModel.from_pretrained(self.transformer_model_dir, **model_opts).to(self.device)
|
||||
|
||||
if hasattr(model.config, "use_pos_embed") and model.config.use_pos_embed is False:
|
||||
pos_embed = model._get_pos_embed_from_transformer(transformer)
|
||||
model.pos_embed = pos_embed.to(model.dtype).to(model.device)
|
||||
# Free transformer model
|
||||
del transformer
|
||||
|
||||
model = optimizer.optimize_checkpoint(model, torch_inference)
|
||||
model = SD3ControlNetWrapper(model)
|
||||
return model
|
||||
|
||||
def get_input_names(self):
|
||||
return ["hidden_states", "controlnet_cond", "conditioning_scale", "pooled_projections", "timestep"]
|
||||
|
||||
def get_output_names(self):
|
||||
return ["controlnet_block_samples"]
|
||||
|
||||
def get_dynamic_axes(self):
|
||||
xB = "2B" if self.xB == 2 else "B"
|
||||
dynamic_axes = {
|
||||
"hidden_states": {0: xB, 2: "H", 3: "W"},
|
||||
"controlnet_cond": {0: xB, 2: "H", 3: "W"},
|
||||
"pooled_projections": {0: xB},
|
||||
"timestep": {0: xB},
|
||||
}
|
||||
return dynamic_axes
|
||||
|
||||
def get_input_profile(
|
||||
self,
|
||||
batch_size: int,
|
||||
image_height: int,
|
||||
image_width: int,
|
||||
static_batch: bool,
|
||||
static_shape: bool,
|
||||
):
|
||||
latent_height, latent_width = self.check_dims(batch_size, image_height, image_width)
|
||||
(
|
||||
min_batch,
|
||||
max_batch,
|
||||
_,
|
||||
_,
|
||||
_,
|
||||
_,
|
||||
min_latent_height,
|
||||
max_latent_height,
|
||||
min_latent_width,
|
||||
max_latent_width,
|
||||
) = self.get_minmax_dims(batch_size, image_height, image_width, static_batch, static_shape)
|
||||
|
||||
input_profile = {
|
||||
"hidden_states": [
|
||||
(self.xB * min_batch, self.config["in_channels"], min_latent_height, min_latent_width),
|
||||
(self.xB * batch_size, self.config["in_channels"], latent_height, latent_width),
|
||||
(self.xB * max_batch, self.config["in_channels"], max_latent_height, max_latent_width),
|
||||
],
|
||||
"timestep": [(self.xB * min_batch,), (self.xB * batch_size,), (self.xB * max_batch,)],
|
||||
"pooled_projections": [
|
||||
(self.xB * min_batch, self.config["pooled_projection_dim"]),
|
||||
(self.xB * batch_size, self.config["pooled_projection_dim"]),
|
||||
(self.xB * max_batch, self.config["pooled_projection_dim"]),
|
||||
],
|
||||
"controlnet_cond": [
|
||||
(self.xB * min_batch, self.config["in_channels"], min_latent_height, min_latent_width),
|
||||
(self.xB * batch_size, self.config["in_channels"], latent_height, latent_width),
|
||||
(self.xB * max_batch, self.config["in_channels"], max_latent_height, max_latent_width),
|
||||
],
|
||||
}
|
||||
return input_profile
|
||||
|
||||
def get_shape_dict(self, batch_size, image_height, image_width):
|
||||
latent_height, latent_width = self.check_dims(batch_size, image_height, image_width)
|
||||
shape_dict = {
|
||||
"hidden_states": (self.xB * batch_size, self.config["in_channels"], latent_height, latent_width),
|
||||
"timestep": (self.xB * batch_size,),
|
||||
"pooled_projections": (self.xB * batch_size, self.config["pooled_projection_dim"]),
|
||||
"controlnet_cond": (self.xB * batch_size, self.config["in_channels"], latent_height, latent_width),
|
||||
"conditioning_scale": (),
|
||||
"controlnet_block_samples": (
|
||||
self.config["num_layers"],
|
||||
self.xB * batch_size,
|
||||
latent_height // 2 * latent_width // 2,
|
||||
self.config["num_attention_heads"] * self.config["attention_head_dim"],
|
||||
),
|
||||
}
|
||||
return shape_dict
|
||||
|
||||
def get_sample_input(self, batch_size, image_height, image_width, static_shape):
|
||||
dtype = torch.float16 if self.fp16 else torch.bfloat16 if self.bf16 else torch.float32
|
||||
latent_height, latent_width = self.check_dims(batch_size, image_height, image_width)
|
||||
sample_input = (
|
||||
torch.randn(
|
||||
self.xB * batch_size,
|
||||
self.config["in_channels"],
|
||||
latent_height,
|
||||
latent_width,
|
||||
dtype=dtype,
|
||||
device=self.device,
|
||||
),
|
||||
torch.randn(
|
||||
self.xB * batch_size,
|
||||
self.config["in_channels"],
|
||||
latent_height,
|
||||
latent_width,
|
||||
dtype=dtype,
|
||||
device=self.device,
|
||||
),
|
||||
torch.tensor(1.0, dtype=dtype, device=self.device),
|
||||
torch.randn(self.xB * batch_size, self.config["pooled_projection_dim"], dtype=dtype, device=self.device),
|
||||
torch.randn(self.xB * batch_size, dtype=torch.float32, device=self.device),
|
||||
)
|
||||
|
||||
return sample_input
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,149 @@
|
||||
#
|
||||
# SPDX-FileCopyrightText: Copyright (c) 1993-2025 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 torch
|
||||
from diffusers.pipelines.wuerstchen import PaellaVQModel
|
||||
|
||||
from demo_diffusion.model import base_model, load, optimizer
|
||||
|
||||
|
||||
class VQGANModel(base_model.BaseModel):
|
||||
def __init__(
|
||||
self,
|
||||
version,
|
||||
pipeline,
|
||||
device,
|
||||
hf_token,
|
||||
verbose,
|
||||
framework_model_dir,
|
||||
fp16=False,
|
||||
bf16=False,
|
||||
max_batch_size=16,
|
||||
compression_factor=42,
|
||||
latent_dim_scale=10.67,
|
||||
scale_factor=0.3764,
|
||||
):
|
||||
super(VQGANModel, 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,
|
||||
compression_factor=compression_factor,
|
||||
)
|
||||
self.subfolder = "vqgan"
|
||||
self.latent_dim_scale = latent_dim_scale
|
||||
self.scale_factor = scale_factor
|
||||
|
||||
def get_model(self, torch_inference=""):
|
||||
model_opts = {"variant": "bf16", "torch_dtype": torch.bfloat16} if self.bf16 else {}
|
||||
vqgan_model_dir = load.get_checkpoint_dir(self.framework_model_dir, self.version, self.pipeline, self.subfolder)
|
||||
if not load.is_model_cached(vqgan_model_dir, model_opts, self.hf_safetensor, model_name="model"):
|
||||
model = PaellaVQModel.from_pretrained(
|
||||
self.path,
|
||||
subfolder=self.subfolder,
|
||||
use_safetensors=self.hf_safetensor,
|
||||
token=self.hf_token,
|
||||
**model_opts,
|
||||
).to(self.device)
|
||||
model.save_pretrained(vqgan_model_dir, **model_opts)
|
||||
else:
|
||||
print(f"[I] Load VQGAN pytorch model from: {vqgan_model_dir}")
|
||||
model = PaellaVQModel.from_pretrained(vqgan_model_dir, **model_opts).to(self.device)
|
||||
model.forward = model.decode
|
||||
model = optimizer.optimize_checkpoint(model, torch_inference)
|
||||
return model
|
||||
|
||||
def get_input_names(self):
|
||||
return ["latent"]
|
||||
|
||||
def get_output_names(self):
|
||||
return ["images"]
|
||||
|
||||
def get_dynamic_axes(self):
|
||||
return {"latent": {0: "B", 2: "H", 3: "W"}, "images": {0: "B", 2: "8H", 3: "8W"}}
|
||||
|
||||
def get_input_profile(self, batch_size, image_height, image_width, static_batch, static_shape):
|
||||
latent_height, latent_width = self.check_dims(batch_size, image_height, image_width)
|
||||
min_batch, max_batch, _, _, _, _, min_latent_height, max_latent_height, min_latent_width, max_latent_width = (
|
||||
self.get_minmax_dims(batch_size, image_height, image_width, static_batch, static_shape)
|
||||
)
|
||||
return {
|
||||
"latent": [
|
||||
(min_batch, 4, min_latent_height, min_latent_width),
|
||||
(batch_size, 4, latent_height, latent_width),
|
||||
(max_batch, 4, max_latent_height, max_latent_width),
|
||||
]
|
||||
}
|
||||
|
||||
def get_shape_dict(self, batch_size, image_height, image_width):
|
||||
latent_height, latent_width = self.check_dims(batch_size, image_height, image_width)
|
||||
return {
|
||||
"latent": (batch_size, 4, latent_height, latent_width),
|
||||
"images": (batch_size, 3, 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.bfloat16 if self.bf16 else torch.float32
|
||||
return torch.randn(batch_size, 4, latent_height, latent_width, dtype=dtype, device=self.device)
|
||||
|
||||
def optimize(self, onnx_graph, return_onnx=True, **kwargs):
|
||||
onnx_opt_graph = super().optimize(onnx_graph, return_onnx=True, **kwargs)
|
||||
opt = optimizer.Optimizer(onnx_opt_graph, verbose=self.verbose, version=self.version)
|
||||
opt.cast_convtranspose_io()
|
||||
return opt.cleanup(return_onnx=return_onnx)
|
||||
|
||||
def check_dims(self, batch_size, image_height, image_width):
|
||||
latent_height, latent_width = super().check_dims(batch_size, image_height, image_width)
|
||||
latent_height = int(latent_height * self.latent_dim_scale)
|
||||
latent_width = int(latent_width * self.latent_dim_scale)
|
||||
return (latent_height, latent_width)
|
||||
|
||||
def get_minmax_dims(self, batch_size, image_height, image_width, static_batch, static_shape):
|
||||
(
|
||||
min_batch,
|
||||
max_batch,
|
||||
min_image_height,
|
||||
max_image_height,
|
||||
min_image_width,
|
||||
max_image_width,
|
||||
min_latent_height,
|
||||
max_latent_height,
|
||||
min_latent_width,
|
||||
max_latent_width,
|
||||
) = super().get_minmax_dims(batch_size, image_height, image_width, static_batch, static_shape)
|
||||
min_latent_height = int(min_latent_height * self.latent_dim_scale)
|
||||
min_latent_width = int(min_latent_width * self.latent_dim_scale)
|
||||
max_latent_height = int(max_latent_height * self.latent_dim_scale)
|
||||
max_latent_width = int(max_latent_width * self.latent_dim_scale)
|
||||
return (
|
||||
min_batch,
|
||||
max_batch,
|
||||
min_image_height,
|
||||
max_image_height,
|
||||
min_image_width,
|
||||
max_image_width,
|
||||
min_latent_height,
|
||||
max_latent_height,
|
||||
min_latent_width,
|
||||
max_latent_width,
|
||||
)
|
||||
@@ -0,0 +1,119 @@
|
||||
#
|
||||
# SPDX-FileCopyrightText: Copyright (c) 1993-2025 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.
|
||||
#
|
||||
"""
|
||||
Functions for loading models.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import gc
|
||||
import glob
|
||||
import os
|
||||
import sys
|
||||
from typing import List, Optional
|
||||
|
||||
import torch
|
||||
|
||||
import onnx
|
||||
|
||||
|
||||
def onnx_graph_needs_external_data(onnx_graph: onnx.ModelProto) -> bool:
|
||||
"""Return true if ONNX graph needs to store external data."""
|
||||
if sys.platform == "win32":
|
||||
# ByteSize is broken (wraps around) on Windows, so always assume external data is needed.
|
||||
return True
|
||||
else:
|
||||
TWO_GIGABYTES = 2147483648
|
||||
return onnx_graph.ByteSize() > TWO_GIGABYTES
|
||||
|
||||
|
||||
def get_path(version: str, pipeline: "pipeline.DiffusionPipeline", controlnets: Optional[List[str]] = None) -> str:
|
||||
"""Return the relative path to the model files directory."""
|
||||
if controlnets is not None:
|
||||
if version == "xl-1.0":
|
||||
return ["diffusers/controlnet-canny-sdxl-1.0"]
|
||||
if version == "3.5-large":
|
||||
return f"stabilityai/stable-diffusion-3.5-large-controlnet-{controlnets}"
|
||||
return ["lllyasviel/sd-controlnet-" + modality for modality in controlnets]
|
||||
|
||||
elif version == "1.4":
|
||||
return "CompVis/stable-diffusion-v1-4"
|
||||
elif version == "dreamshaper-7":
|
||||
return "Lykon/dreamshaper-7"
|
||||
elif version == "xl-1.0" and pipeline.is_sd_xl_base():
|
||||
return "stabilityai/stable-diffusion-xl-base-1.0"
|
||||
elif version == "xl-1.0" and pipeline.is_sd_xl_refiner():
|
||||
return "stabilityai/stable-diffusion-xl-refiner-1.0"
|
||||
# TODO SDXL turbo with refiner
|
||||
elif version == "xl-turbo" and pipeline.is_sd_xl_base():
|
||||
return "stabilityai/sdxl-turbo"
|
||||
elif version == "sd3":
|
||||
return "stabilityai/stable-diffusion-3-medium"
|
||||
elif version == "3.5-medium":
|
||||
return "stabilityai/stable-diffusion-3.5-medium"
|
||||
elif version == "3.5-large":
|
||||
return "stabilityai/stable-diffusion-3.5-large"
|
||||
elif version == "svd-xt-1.1" and pipeline.is_img2vid():
|
||||
return "stabilityai/stable-video-diffusion-img2vid-xt-1-1"
|
||||
elif version == "cascade":
|
||||
if pipeline.is_cascade_decoder():
|
||||
return "stabilityai/stable-cascade"
|
||||
else:
|
||||
return "stabilityai/stable-cascade-prior"
|
||||
elif version == "flux.1-dev":
|
||||
return "black-forest-labs/FLUX.1-dev"
|
||||
elif version == "flux.1-schnell":
|
||||
return "black-forest-labs/FLUX.1-schnell"
|
||||
elif version == "flux.1-dev-canny":
|
||||
return "black-forest-labs/FLUX.1-Canny-dev"
|
||||
elif version == "flux.1-dev-depth":
|
||||
return "black-forest-labs/FLUX.1-Depth-dev"
|
||||
elif version == "flux.1-kontext-dev":
|
||||
return "black-forest-labs/FLUX.1-Kontext-dev"
|
||||
elif version == "wan2.2-t2v-a14b":
|
||||
return "Wan-AI/Wan2.2-T2V-A14B-Diffusers"
|
||||
elif version == "cosmos-predict2-2b-text2image":
|
||||
return "nvidia/Cosmos-Predict2-2B-Text2Image"
|
||||
elif version == "cosmos-predict2-14b-text2image":
|
||||
return "nvidia/Cosmos-Predict2-14B-Text2Image"
|
||||
elif version == "cosmos-predict2-2b-video2world":
|
||||
return "nvidia/Cosmos-Predict2-2B-Video2World"
|
||||
elif version == "cosmos-predict2-14b-video2world":
|
||||
return "nvidia/Cosmos-Predict2-14B-Video2World"
|
||||
else:
|
||||
raise ValueError(f"Unsupported version {version} + pipeline {pipeline.name}")
|
||||
|
||||
|
||||
# FIXME serialization not supported for torch.compile
|
||||
def get_checkpoint_dir(framework_model_dir: str, version: str, pipeline: str, subfolder: str) -> str:
|
||||
"""Return the path to the torch model checkpoint directory."""
|
||||
return os.path.join(framework_model_dir, version, pipeline, subfolder)
|
||||
|
||||
|
||||
def is_model_cached(model_dir, model_opts, hf_safetensor, model_name="diffusion_pytorch_model") -> bool:
|
||||
"""Return True if model was cached."""
|
||||
variant = "." + model_opts.get("variant") if "variant" in model_opts else ""
|
||||
suffix = ".safetensors" if hf_safetensor else ".bin"
|
||||
# WAR with * for larger models that are split into multiple smaller ckpt files
|
||||
model_file = model_name + variant + "*" + suffix
|
||||
return bool(glob.glob(os.path.join(model_dir, model_file)))
|
||||
|
||||
|
||||
def unload_torch_model(model):
|
||||
if model:
|
||||
del model
|
||||
torch.cuda.empty_cache()
|
||||
gc.collect()
|
||||
@@ -0,0 +1,56 @@
|
||||
#
|
||||
# SPDX-FileCopyrightText: Copyright (c) 1993-2025 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 abc import ABC
|
||||
from diffusers.loaders import StableDiffusionLoraLoaderMixin, FluxLoraLoaderMixin
|
||||
|
||||
|
||||
class LoraLoader(ABC):
|
||||
def __init__(self, paths, weights, scale):
|
||||
self.paths = paths
|
||||
self.weights = weights
|
||||
self.scale = scale
|
||||
|
||||
|
||||
class SDLoraLoader(LoraLoader, StableDiffusionLoraLoaderMixin):
|
||||
def __init__(self, paths, weights, scale):
|
||||
super().__init__(paths, weights, scale)
|
||||
|
||||
|
||||
class FLUXLoraLoader(LoraLoader, FluxLoraLoaderMixin):
|
||||
def __init__(self, paths, weights, scale):
|
||||
super().__init__(paths, weights, scale)
|
||||
|
||||
|
||||
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]}")
|
||||
if isinstance(lora_loader, SDLoraLoader):
|
||||
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)
|
||||
elif isinstance(lora_loader, FLUXLoraLoader):
|
||||
state_dict, network_alphas = lora_loader.lora_state_dict(path, return_alphas=True)
|
||||
lora_loader.load_lora_into_transformer(state_dict, network_alphas=network_alphas, transformer=model, adapter_name=path)
|
||||
else:
|
||||
raise ValueError(f"Unsupported LoRA loader: {lora_loader}")
|
||||
|
||||
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
|
||||
@@ -0,0 +1,218 @@
|
||||
#
|
||||
# SPDX-FileCopyrightText: Copyright (c) 1993-2025 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 os
|
||||
import re
|
||||
import tempfile
|
||||
|
||||
import onnx
|
||||
import onnx_graphsurgeon as gs
|
||||
import torch
|
||||
from onnx import shape_inference
|
||||
from polygraphy.backend.onnx.loader import fold_constants
|
||||
|
||||
from demo_diffusion.model import load
|
||||
from demo_diffusion.utils_modelopt import (
|
||||
cast_convtranspose_io,
|
||||
cast_fp8_mha_io,
|
||||
cast_layernorm_io,
|
||||
cast_resize_io,
|
||||
convert_fp16_io,
|
||||
convert_zp_fp8,
|
||||
)
|
||||
|
||||
# FIXME update callsites after serialization support for torch.compile is added
|
||||
TORCH_INFERENCE_MODELS = ["default", "reduce-overhead", "max-autotune"]
|
||||
|
||||
|
||||
def optimize_checkpoint(model, torch_inference: str):
|
||||
"""Optimize a torch model checkpoint using torch.compile."""
|
||||
if not torch_inference or torch_inference == "eager":
|
||||
return model
|
||||
assert torch_inference in TORCH_INFERENCE_MODELS
|
||||
return torch.compile(model, mode=torch_inference, dynamic=False, fullgraph=False)
|
||||
|
||||
|
||||
class Optimizer:
|
||||
|
||||
def __init__(self, onnx_graph, verbose=False, version=None):
|
||||
self.graph = gs.import_onnx(onnx_graph)
|
||||
self.verbose = verbose
|
||||
self.version = version
|
||||
|
||||
def info(self, prefix):
|
||||
if self.verbose:
|
||||
print(
|
||||
f"{prefix} .. {len(self.graph.nodes)} nodes, {len(self.graph.tensors().keys())} tensors, {len(self.graph.inputs)} inputs, {len(self.graph.outputs)} outputs"
|
||||
)
|
||||
|
||||
def cleanup(self, return_onnx=False):
|
||||
self.graph.cleanup().toposort()
|
||||
return gs.export_onnx(self.graph) if return_onnx else self.graph
|
||||
|
||||
def select_outputs(self, keep, names=None):
|
||||
self.graph.outputs = [self.graph.outputs[o] for o in keep]
|
||||
if names:
|
||||
for i, name in enumerate(names):
|
||||
self.graph.outputs[i].name = name
|
||||
|
||||
def fold_constants(self, return_onnx=False):
|
||||
onnx_graph = fold_constants(gs.export_onnx(self.graph), allow_onnxruntime_shape_inference=True)
|
||||
self.graph = gs.import_onnx(onnx_graph)
|
||||
if return_onnx:
|
||||
return onnx_graph
|
||||
|
||||
def infer_shapes(self, return_onnx=False):
|
||||
onnx_graph = gs.export_onnx(self.graph)
|
||||
if load.onnx_graph_needs_external_data(onnx_graph):
|
||||
temp_dir = tempfile.TemporaryDirectory().name
|
||||
os.makedirs(temp_dir, exist_ok=True)
|
||||
onnx_orig_path = os.path.join(temp_dir, "model.onnx")
|
||||
onnx_inferred_path = os.path.join(temp_dir, "inferred.onnx")
|
||||
onnx.save_model(
|
||||
onnx_graph,
|
||||
onnx_orig_path,
|
||||
save_as_external_data=True,
|
||||
all_tensors_to_one_file=True,
|
||||
convert_attribute=False,
|
||||
)
|
||||
onnx.shape_inference.infer_shapes_path(onnx_orig_path, onnx_inferred_path)
|
||||
onnx_graph = onnx.load(onnx_inferred_path)
|
||||
else:
|
||||
onnx_graph = shape_inference.infer_shapes(onnx_graph)
|
||||
|
||||
self.graph = gs.import_onnx(onnx_graph)
|
||||
if return_onnx:
|
||||
return onnx_graph
|
||||
|
||||
def clip_add_hidden_states(self, hidden_layer_offset, return_onnx=False):
|
||||
hidden_layers = -1
|
||||
onnx_graph = gs.export_onnx(self.graph)
|
||||
for i in range(len(onnx_graph.graph.node)):
|
||||
for j in range(len(onnx_graph.graph.node[i].output)):
|
||||
name = onnx_graph.graph.node[i].output[j]
|
||||
if "layers" in name:
|
||||
hidden_layers = max(int(name.split(".")[1].split("/")[0]), hidden_layers)
|
||||
for i in range(len(onnx_graph.graph.node)):
|
||||
for j in range(len(onnx_graph.graph.node[i].output)):
|
||||
if onnx_graph.graph.node[i].output[j] == "/text_model/encoder/layers.{}/Add_1_output_0".format(
|
||||
hidden_layers + hidden_layer_offset
|
||||
):
|
||||
onnx_graph.graph.node[i].output[j] = "hidden_states"
|
||||
for j in range(len(onnx_graph.graph.node[i].input)):
|
||||
if onnx_graph.graph.node[i].input[j] == "/text_model/encoder/layers.{}/Add_1_output_0".format(
|
||||
hidden_layers + hidden_layer_offset
|
||||
):
|
||||
onnx_graph.graph.node[i].input[j] = "hidden_states"
|
||||
if return_onnx:
|
||||
return onnx_graph
|
||||
|
||||
def fuse_mha_qkv_int8_sq(self):
|
||||
tensors = self.graph.tensors()
|
||||
keys = tensors.keys()
|
||||
|
||||
# mha : fuse QKV QDQ nodes
|
||||
# mhca : fuse KV QDQ nodes
|
||||
q_pat = (
|
||||
"/down_blocks.\\d+/attentions.\\d+/transformer_blocks"
|
||||
".\\d+/attn\\d+/to_q/input_quantizer/DequantizeLinear_output_0"
|
||||
)
|
||||
k_pat = (
|
||||
"/down_blocks.\\d+/attentions.\\d+/transformer_blocks"
|
||||
".\\d+/attn\\d+/to_k/input_quantizer/DequantizeLinear_output_0"
|
||||
)
|
||||
v_pat = (
|
||||
"/down_blocks.\\d+/attentions.\\d+/transformer_blocks"
|
||||
".\\d+/attn\\d+/to_v/input_quantizer/DequantizeLinear_output_0"
|
||||
)
|
||||
|
||||
qs = list(
|
||||
sorted(
|
||||
map(
|
||||
lambda x: x.group(0), # type: ignore
|
||||
filter(lambda x: x is not None, [re.match(q_pat, key) for key in keys]),
|
||||
)
|
||||
)
|
||||
)
|
||||
ks = list(
|
||||
sorted(
|
||||
map(
|
||||
lambda x: x.group(0), # type: ignore
|
||||
filter(lambda x: x is not None, [re.match(k_pat, key) for key in keys]),
|
||||
)
|
||||
)
|
||||
)
|
||||
vs = list(
|
||||
sorted(
|
||||
map(
|
||||
lambda x: x.group(0), # type: ignore
|
||||
filter(lambda x: x is not None, [re.match(v_pat, key) for key in keys]),
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
removed = 0
|
||||
assert len(qs) == len(ks) == len(vs), "Failed to collect tensors"
|
||||
for q, k, v in zip(qs, ks, vs):
|
||||
is_mha = all(["attn1" in tensor for tensor in [q, k, v]])
|
||||
is_mhca = all(["attn2" in tensor for tensor in [q, k, v]])
|
||||
assert (is_mha or is_mhca) and (not (is_mha and is_mhca))
|
||||
|
||||
if is_mha:
|
||||
tensors[k].outputs[0].inputs[0] = tensors[q]
|
||||
tensors[v].outputs[0].inputs[0] = tensors[q]
|
||||
del tensors[k]
|
||||
del tensors[v]
|
||||
removed += 2
|
||||
else: # is_mhca
|
||||
tensors[k].outputs[0].inputs[0] = tensors[v]
|
||||
del tensors[k]
|
||||
removed += 1
|
||||
print(f"Removed {removed} QDQ nodes")
|
||||
return removed # expected 72 for L2.5
|
||||
|
||||
def modify_int8_graph(self):
|
||||
# Cast LayerNorm scale/bias from FP16 to FP32 to match INT8 DQ activations.
|
||||
cast_layernorm_io(self.graph)
|
||||
# Fuse QKV QDQ nodes for INT8 SmoothQuant.
|
||||
self.fuse_mha_qkv_int8_sq()
|
||||
|
||||
def cast_convtranspose_io(self):
|
||||
cast_convtranspose_io(self.graph)
|
||||
|
||||
def cast_resize_io(self, output_dtype):
|
||||
cast_resize_io(self.graph, output_dtype=output_dtype)
|
||||
|
||||
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)
|
||||
|
||||
self.graph = gs.import_onnx(onnx_graph)
|
||||
# Add cast nodes to Resize I/O.
|
||||
cast_resize_io(self.graph)
|
||||
# Convert model inputs and outputs to fp16 I/O.
|
||||
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)
|
||||
|
||||
def flux_convert_rope_weight_type(self):
|
||||
for node in self.graph.nodes:
|
||||
if node.op == "Einsum":
|
||||
print(f"Fixed RoPE (Rotary Position Embedding) weight type: {node.name}")
|
||||
return gs.export_onnx(self.graph)
|
||||
@@ -0,0 +1,33 @@
|
||||
#
|
||||
# SPDX-FileCopyrightText: Copyright (c) 1993-2025 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 os
|
||||
|
||||
from demo_diffusion.model import load
|
||||
|
||||
|
||||
def make_scheduler(cls, version, pipeline, hf_token, framework_model_dir, subfolder="scheduler"):
|
||||
scheduler_dir = os.path.join(
|
||||
framework_model_dir, version, pipeline.name, next(iter({cls.__name__})).lower(), subfolder
|
||||
)
|
||||
if not os.path.exists(scheduler_dir):
|
||||
scheduler = cls.from_pretrained(load.get_path(version, pipeline), subfolder=subfolder, token=hf_token)
|
||||
scheduler.save_pretrained(scheduler_dir)
|
||||
else:
|
||||
print(f"[I] Load Scheduler {cls.__name__} from: {scheduler_dir}")
|
||||
scheduler = cls.from_pretrained(scheduler_dir)
|
||||
return scheduler
|
||||
@@ -0,0 +1,142 @@
|
||||
#
|
||||
# SPDX-FileCopyrightText: Copyright (c) 1993-2025 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 os
|
||||
|
||||
import torch
|
||||
from transformers import (
|
||||
AutoConfig,
|
||||
T5EncoderModel,
|
||||
UMT5EncoderModel,
|
||||
)
|
||||
|
||||
from demo_diffusion.model import base_model, load, optimizer
|
||||
|
||||
|
||||
class T5Model(base_model.BaseModel):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
version,
|
||||
pipeline,
|
||||
device,
|
||||
hf_token,
|
||||
verbose,
|
||||
framework_model_dir,
|
||||
max_batch_size,
|
||||
fp16=False,
|
||||
tf32=False,
|
||||
bf16=False,
|
||||
subfolder="text_encoder",
|
||||
text_maxlen=512,
|
||||
weight_streaming=False,
|
||||
weight_streaming_budget_percentage=None,
|
||||
use_attention_mask=False,
|
||||
):
|
||||
super(T5Model, 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,
|
||||
text_maxlen=text_maxlen,
|
||||
)
|
||||
self.subfolder = subfolder
|
||||
self.t5_model_dir = load.get_checkpoint_dir(
|
||||
self.framework_model_dir, self.version, self.pipeline, self.subfolder
|
||||
)
|
||||
if not os.path.exists(self.t5_model_dir):
|
||||
self.config = AutoConfig.from_pretrained(self.path, subfolder=self.subfolder, token=self.hf_token)
|
||||
else:
|
||||
print(f"[I] Load T5Encoder Config from: {self.t5_model_dir}")
|
||||
self.config = AutoConfig.from_pretrained(self.t5_model_dir)
|
||||
self.is_umt5 = getattr(self.config, 'model_type', '') == 'umt5'
|
||||
self.weight_streaming = weight_streaming
|
||||
self.weight_streaming_budget_percentage = weight_streaming_budget_percentage
|
||||
self.use_attention_mask = use_attention_mask
|
||||
|
||||
def get_model(self, torch_inference=""):
|
||||
model_opts = (
|
||||
{"torch_dtype": torch.float16} if self.fp16 else {"torch_dtype": torch.bfloat16} if self.bf16 else {}
|
||||
)
|
||||
EncoderModelClass = UMT5EncoderModel if self.is_umt5 else T5EncoderModel
|
||||
if not load.is_model_cached(self.t5_model_dir, model_opts, self.hf_safetensor, model_name="model"):
|
||||
model = EncoderModelClass.from_pretrained(
|
||||
self.path,
|
||||
subfolder=self.subfolder,
|
||||
use_safetensors=self.hf_safetensor,
|
||||
token=self.hf_token,
|
||||
**model_opts,
|
||||
).to(self.device)
|
||||
model.save_pretrained(self.t5_model_dir, **model_opts)
|
||||
else:
|
||||
print(f"[I] Load {EncoderModelClass.__name__} model from: {self.t5_model_dir}")
|
||||
model = EncoderModelClass.from_pretrained(self.t5_model_dir, **model_opts).to(self.device)
|
||||
|
||||
model = optimizer.optimize_checkpoint(model, torch_inference)
|
||||
return model
|
||||
|
||||
def get_input_names(self):
|
||||
if self.use_attention_mask:
|
||||
return ["input_ids", "attention_mask"]
|
||||
return ["input_ids"]
|
||||
|
||||
def get_output_names(self):
|
||||
return ["text_embeddings"]
|
||||
|
||||
def get_dynamic_axes(self):
|
||||
if self.use_attention_mask:
|
||||
return {"input_ids": {0: "B"}, "attention_mask": {0: "B"}, "text_embeddings": {0: "B"}}
|
||||
return {"input_ids": {0: "B"}, "text_embeddings": {0: "B"}}
|
||||
|
||||
def get_input_profile(self, batch_size, image_height, image_width, static_batch, static_shape):
|
||||
self.check_dims(batch_size, image_height, image_width)
|
||||
min_batch, max_batch, _, _, _, _, _, _, _, _ = self.get_minmax_dims(
|
||||
batch_size, image_height, image_width, static_batch, static_shape
|
||||
)
|
||||
profile = {
|
||||
"input_ids": [(min_batch, self.text_maxlen), (batch_size, self.text_maxlen), (max_batch, self.text_maxlen)]
|
||||
}
|
||||
if self.use_attention_mask:
|
||||
profile["attention_mask"] = [
|
||||
(min_batch, self.text_maxlen),
|
||||
(batch_size, self.text_maxlen),
|
||||
(max_batch, self.text_maxlen),
|
||||
]
|
||||
return profile
|
||||
|
||||
def get_shape_dict(self, batch_size, image_height, image_width):
|
||||
self.check_dims(batch_size, image_height, image_width)
|
||||
output = {
|
||||
"input_ids": (batch_size, self.text_maxlen),
|
||||
"text_embeddings": (batch_size, self.text_maxlen, self.config.d_model),
|
||||
}
|
||||
if self.use_attention_mask:
|
||||
output["attention_mask"] = (batch_size, self.text_maxlen)
|
||||
return output
|
||||
|
||||
def get_sample_input(self, batch_size, image_height, image_width, static_shape):
|
||||
self.check_dims(batch_size, image_height, image_width)
|
||||
inputs = {"input_ids": torch.zeros(batch_size, self.text_maxlen, dtype=torch.int32, device=self.device)}
|
||||
if self.use_attention_mask:
|
||||
inputs["attention_mask"] = torch.ones(batch_size, self.text_maxlen, dtype=torch.int32, device=self.device)
|
||||
return inputs
|
||||
@@ -0,0 +1,46 @@
|
||||
#
|
||||
# SPDX-FileCopyrightText: Copyright (c) 1993-2025 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 os
|
||||
|
||||
from transformers import (
|
||||
CLIPTokenizer,
|
||||
T5TokenizerFast,
|
||||
)
|
||||
|
||||
from demo_diffusion.model import load
|
||||
|
||||
|
||||
def make_tokenizer(version, pipeline, hf_token, framework_model_dir, subfolder="tokenizer", tokenizer_type="clip"):
|
||||
if tokenizer_type == "clip":
|
||||
tokenizer_class = CLIPTokenizer
|
||||
elif tokenizer_type == "t5":
|
||||
tokenizer_class = T5TokenizerFast
|
||||
else:
|
||||
raise ValueError(
|
||||
f"Unsupported tokenizer_type {tokenizer_type}. Only tokenizer_type clip and t5 are currently supported"
|
||||
)
|
||||
tokenizer_model_dir = load.get_checkpoint_dir(framework_model_dir, version, pipeline.name, subfolder)
|
||||
if not os.path.exists(tokenizer_model_dir):
|
||||
model = tokenizer_class.from_pretrained(
|
||||
load.get_path(version, pipeline), subfolder=subfolder, use_safetensors=pipeline.is_sd_xl(), token=hf_token
|
||||
)
|
||||
model.save_pretrained(tokenizer_model_dir)
|
||||
else:
|
||||
print(f"[I] Load {tokenizer_class.__name__} model from: {tokenizer_model_dir}")
|
||||
model = tokenizer_class.from_pretrained(tokenizer_model_dir)
|
||||
return model
|
||||
@@ -0,0 +1,902 @@
|
||||
#
|
||||
# SPDX-FileCopyrightText: Copyright (c) 1993-2025 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.
|
||||
#
|
||||
|
||||
"""
|
||||
Model definitions for UNet models.
|
||||
"""
|
||||
|
||||
import torch
|
||||
|
||||
from demo_diffusion.dynamic_import import import_from_diffusers
|
||||
from demo_diffusion.model import base_model, load, optimizer
|
||||
from diffusers import StableDiffusionXLControlNetPipeline
|
||||
|
||||
# List of models to import from diffusers.models
|
||||
models_to_import = [
|
||||
"ControlNetModel",
|
||||
"UNet2DConditionModel",
|
||||
"UNetSpatioTemporalConditionModel",
|
||||
"StableCascadeUNet",
|
||||
]
|
||||
for model in models_to_import:
|
||||
globals()[model] = import_from_diffusers(model, "diffusers.models")
|
||||
|
||||
|
||||
def get_unet_embedding_dim(version, pipeline):
|
||||
if version in ("1.4", "dreamshaper-7"):
|
||||
return 768
|
||||
elif version in ("xl-1.0", "xl-turbo") and pipeline.is_sd_xl_base():
|
||||
return 2048
|
||||
elif version in ("cascade"):
|
||||
return 1280
|
||||
elif version in ("xl-1.0", "xl-turbo") and pipeline.is_sd_xl_refiner():
|
||||
return 1280
|
||||
elif pipeline.is_img2vid():
|
||||
return 1024
|
||||
else:
|
||||
raise ValueError(f"Invalid version {version} + pipeline {pipeline}")
|
||||
|
||||
|
||||
class UNet2DConditionControlNetModel(torch.nn.Module):
|
||||
def __init__(self, unet, controlnets) -> None:
|
||||
super().__init__()
|
||||
self.unet = unet
|
||||
self.controlnets = controlnets
|
||||
|
||||
def forward(self, sample, timestep, encoder_hidden_states, images, controlnet_scales, added_cond_kwargs=None):
|
||||
for i, (image, conditioning_scale, controlnet) in enumerate(zip(images, controlnet_scales, self.controlnets)):
|
||||
down_samples, mid_sample = controlnet(
|
||||
sample,
|
||||
timestep,
|
||||
encoder_hidden_states=encoder_hidden_states,
|
||||
controlnet_cond=image,
|
||||
return_dict=False,
|
||||
added_cond_kwargs=added_cond_kwargs,
|
||||
)
|
||||
|
||||
down_samples = [down_sample * conditioning_scale for down_sample in down_samples]
|
||||
mid_sample *= conditioning_scale
|
||||
|
||||
# merge samples
|
||||
if i == 0:
|
||||
down_block_res_samples, mid_block_res_sample = down_samples, mid_sample
|
||||
else:
|
||||
down_block_res_samples = [
|
||||
samples_prev + samples_curr
|
||||
for samples_prev, samples_curr in zip(down_block_res_samples, down_samples)
|
||||
]
|
||||
mid_block_res_sample += mid_sample
|
||||
|
||||
noise_pred = self.unet(
|
||||
sample,
|
||||
timestep,
|
||||
encoder_hidden_states=encoder_hidden_states,
|
||||
down_block_additional_residuals=down_block_res_samples,
|
||||
mid_block_additional_residual=mid_block_res_sample,
|
||||
added_cond_kwargs=added_cond_kwargs,
|
||||
)
|
||||
return noise_pred
|
||||
|
||||
|
||||
class UNetModel(base_model.BaseModel):
|
||||
def __init__(
|
||||
self,
|
||||
version,
|
||||
pipeline,
|
||||
device,
|
||||
hf_token,
|
||||
verbose,
|
||||
framework_model_dir,
|
||||
fp16=False,
|
||||
int8=False,
|
||||
fp8=False,
|
||||
max_batch_size=16,
|
||||
text_maxlen=77,
|
||||
controlnets=None,
|
||||
do_classifier_free_guidance=False,
|
||||
):
|
||||
|
||||
super(UNetModel, 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.controlnets = load.get_path(version, pipeline, controlnets) if controlnets else None
|
||||
self.unet_dim = 4
|
||||
self.xB = 2 if do_classifier_free_guidance else 1 # batch multiplier
|
||||
|
||||
def get_model(self, torch_inference=""):
|
||||
model_opts = {"variant": "fp16", "torch_dtype": torch.float16} if self.fp16 else {}
|
||||
if self.controlnets:
|
||||
unet_model = UNet2DConditionModel.from_pretrained(
|
||||
self.path,
|
||||
subfolder=self.subfolder,
|
||||
use_safetensors=self.hf_safetensor,
|
||||
token=self.hf_token,
|
||||
**model_opts,
|
||||
).to(self.device)
|
||||
cnet_model_opts = {"torch_dtype": torch.float16} if self.fp16 else {}
|
||||
controlnets = torch.nn.ModuleList(
|
||||
[ControlNetModel.from_pretrained(path, **cnet_model_opts).to(self.device) for path in self.controlnets]
|
||||
)
|
||||
# FIXME - cache UNet2DConditionControlNetModel
|
||||
model = UNet2DConditionControlNetModel(unet_model, controlnets)
|
||||
else:
|
||||
unet_model_dir = load.get_checkpoint_dir(
|
||||
self.framework_model_dir, self.version, self.pipeline, self.subfolder
|
||||
)
|
||||
if not load.is_model_cached(unet_model_dir, model_opts, self.hf_safetensor):
|
||||
model = UNet2DConditionModel.from_pretrained(
|
||||
self.path,
|
||||
subfolder=self.subfolder,
|
||||
use_safetensors=self.hf_safetensor,
|
||||
token=self.hf_token,
|
||||
**model_opts,
|
||||
).to(self.device)
|
||||
model.save_pretrained(unet_model_dir, **model_opts)
|
||||
else:
|
||||
print(f"[I] Load UNet2DConditionModel model from: {unet_model_dir}")
|
||||
model = UNet2DConditionModel.from_pretrained(unet_model_dir, **model_opts).to(self.device)
|
||||
if torch_inference:
|
||||
model.to(memory_format=torch.channels_last)
|
||||
model = optimizer.optimize_checkpoint(model, torch_inference)
|
||||
return model
|
||||
|
||||
def get_input_names(self):
|
||||
if self.controlnets is None:
|
||||
return ["sample", "timestep", "encoder_hidden_states"]
|
||||
else:
|
||||
return ["sample", "timestep", "encoder_hidden_states", "images", "controlnet_scales"]
|
||||
|
||||
def get_output_names(self):
|
||||
return ["latent"]
|
||||
|
||||
def get_dynamic_axes(self):
|
||||
xB = "2B" if self.xB == 2 else "B"
|
||||
if self.controlnets is None:
|
||||
return {
|
||||
"sample": {0: xB, 2: "H", 3: "W"},
|
||||
"encoder_hidden_states": {0: xB},
|
||||
"latent": {0: xB, 2: "H", 3: "W"},
|
||||
}
|
||||
else:
|
||||
return {
|
||||
"sample": {0: xB, 2: "H", 3: "W"},
|
||||
"encoder_hidden_states": {0: xB},
|
||||
"images": {1: xB, 3: "8H", 4: "8W"},
|
||||
"latent": {0: xB, 2: "H", 3: "W"},
|
||||
}
|
||||
|
||||
def get_input_profile(self, batch_size, image_height, image_width, static_batch, static_shape):
|
||||
# WAR to enable inference for H/W that are not multiples of 16
|
||||
# If building with Dynamic Shapes: ensure image height and width are not multiples of 16 for ONNX export and TensorRT engine build
|
||||
if not static_shape:
|
||||
image_height = image_height - 8 if image_height % 16 == 0 else image_height
|
||||
image_width = image_width - 8 if image_width % 16 == 0 else image_width
|
||||
latent_height, latent_width = self.check_dims(batch_size, image_height, image_width)
|
||||
(
|
||||
min_batch,
|
||||
max_batch,
|
||||
min_image_height,
|
||||
max_image_height,
|
||||
min_image_width,
|
||||
max_image_width,
|
||||
min_latent_height,
|
||||
max_latent_height,
|
||||
min_latent_width,
|
||||
max_latent_width,
|
||||
) = self.get_minmax_dims(batch_size, image_height, image_width, static_batch, static_shape)
|
||||
if self.controlnets is None:
|
||||
return {
|
||||
"sample": [
|
||||
(self.xB * min_batch, self.unet_dim, min_latent_height, min_latent_width),
|
||||
(self.xB * batch_size, self.unet_dim, latent_height, latent_width),
|
||||
(self.xB * max_batch, self.unet_dim, max_latent_height, max_latent_width),
|
||||
],
|
||||
"encoder_hidden_states": [
|
||||
(self.xB * min_batch, self.text_maxlen, self.embedding_dim),
|
||||
(self.xB * batch_size, self.text_maxlen, self.embedding_dim),
|
||||
(self.xB * max_batch, self.text_maxlen, self.embedding_dim),
|
||||
],
|
||||
}
|
||||
else:
|
||||
return {
|
||||
"sample": [
|
||||
(self.xB * min_batch, self.unet_dim, min_latent_height, min_latent_width),
|
||||
(self.xB * batch_size, self.unet_dim, latent_height, latent_width),
|
||||
(self.xB * max_batch, self.unet_dim, max_latent_height, max_latent_width),
|
||||
],
|
||||
"encoder_hidden_states": [
|
||||
(self.xB * min_batch, self.text_maxlen, self.embedding_dim),
|
||||
(self.xB * batch_size, self.text_maxlen, self.embedding_dim),
|
||||
(self.xB * max_batch, self.text_maxlen, self.embedding_dim),
|
||||
],
|
||||
"images": [
|
||||
(len(self.controlnets), self.xB * min_batch, 3, min_image_height, min_image_width),
|
||||
(len(self.controlnets), self.xB * batch_size, 3, image_height, image_width),
|
||||
(len(self.controlnets), self.xB * max_batch, 3, max_image_height, max_image_width),
|
||||
],
|
||||
}
|
||||
|
||||
def get_shape_dict(self, batch_size, image_height, image_width):
|
||||
latent_height, latent_width = self.check_dims(batch_size, image_height, image_width)
|
||||
if self.controlnets is None:
|
||||
return {
|
||||
"sample": (self.xB * batch_size, self.unet_dim, latent_height, latent_width),
|
||||
"encoder_hidden_states": (self.xB * batch_size, self.text_maxlen, self.embedding_dim),
|
||||
"latent": (self.xB * batch_size, 4, latent_height, latent_width),
|
||||
}
|
||||
else:
|
||||
return {
|
||||
"sample": (self.xB * batch_size, self.unet_dim, latent_height, latent_width),
|
||||
"encoder_hidden_states": (self.xB * batch_size, self.text_maxlen, self.embedding_dim),
|
||||
"images": (len(self.controlnets), self.xB * batch_size, 3, image_height, image_width),
|
||||
"latent": (self.xB * batch_size, 4, latent_height, latent_width),
|
||||
}
|
||||
|
||||
def get_sample_input(self, batch_size, image_height, image_width, static_shape):
|
||||
# WAR to enable inference for H/W that are not multiples of 16
|
||||
# If building with Dynamic Shapes: ensure image height and width are not multiples of 16 for ONNX export and TensorRT engine build
|
||||
if not static_shape:
|
||||
image_height = image_height - 8 if image_height % 16 == 0 else image_height
|
||||
image_width = image_width - 8 if image_width % 16 == 0 else image_width
|
||||
latent_height, latent_width = self.check_dims(batch_size, image_height, image_width)
|
||||
dtype = torch.float16 if self.fp16 else torch.float32
|
||||
if self.controlnets is None:
|
||||
return (
|
||||
torch.randn(batch_size, self.unet_dim, latent_height, latent_width, dtype=dtype, device=self.device),
|
||||
torch.tensor([1.0], dtype=dtype, device=self.device),
|
||||
torch.randn(batch_size, self.text_maxlen, self.embedding_dim, dtype=dtype, device=self.device),
|
||||
)
|
||||
else:
|
||||
return (
|
||||
torch.randn(batch_size, self.unet_dim, latent_height, latent_width, dtype=dtype, device=self.device),
|
||||
torch.tensor(999, dtype=dtype, device=self.device),
|
||||
torch.randn(batch_size, self.text_maxlen, self.embedding_dim, dtype=dtype, device=self.device),
|
||||
torch.randn(
|
||||
len(self.controlnets), batch_size, 3, image_height, image_width, dtype=dtype, device=self.device
|
||||
),
|
||||
torch.randn(len(self.controlnets), dtype=dtype, device=self.device),
|
||||
)
|
||||
|
||||
def optimize(self, onnx_graph):
|
||||
if self.fp8:
|
||||
return super().optimize(onnx_graph, modify_fp8_graph=True)
|
||||
if self.int8:
|
||||
return super().optimize(onnx_graph, modify_int8_graph=True)
|
||||
return super().optimize(onnx_graph)
|
||||
|
||||
|
||||
class UNetXLModel(base_model.BaseModel):
|
||||
def __init__(
|
||||
self,
|
||||
version,
|
||||
pipeline,
|
||||
device,
|
||||
hf_token,
|
||||
verbose,
|
||||
framework_model_dir,
|
||||
fp16=False,
|
||||
int8=False,
|
||||
fp8=False,
|
||||
max_batch_size=16,
|
||||
text_maxlen=77,
|
||||
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 = 4
|
||||
self.time_dim = 5 if pipeline.is_sd_xl_refiner() else 6
|
||||
self.xB = 2 if do_classifier_free_guidance else 1 # batch multiplier
|
||||
|
||||
def get_model(self, torch_inference=""):
|
||||
model_opts = {"variant": "fp16", "torch_dtype": torch.float16} if self.fp16 else {}
|
||||
unet_model_dir = load.get_checkpoint_dir(self.framework_model_dir, self.version, self.pipeline, self.subfolder)
|
||||
if not load.is_model_cached(unet_model_dir, model_opts, self.hf_safetensor):
|
||||
model = UNet2DConditionModel.from_pretrained(
|
||||
self.path,
|
||||
subfolder=self.subfolder,
|
||||
use_safetensors=self.hf_safetensor,
|
||||
token=self.hf_token,
|
||||
**model_opts,
|
||||
).to(self.device)
|
||||
# Use default attention processor for ONNX export
|
||||
if not torch_inference:
|
||||
model.set_default_attn_processor()
|
||||
model.save_pretrained(unet_model_dir, **model_opts)
|
||||
else:
|
||||
print(f"[I] Load UNet2DConditionModel model from: {unet_model_dir}")
|
||||
model = UNet2DConditionModel.from_pretrained(unet_model_dir, **model_opts).to(self.device)
|
||||
model = optimizer.optimize_checkpoint(model, torch_inference)
|
||||
return model
|
||||
|
||||
def get_input_names(self):
|
||||
return ["sample", "timestep", "encoder_hidden_states", "text_embeds", "time_ids"]
|
||||
|
||||
def get_output_names(self):
|
||||
return ["latent"]
|
||||
|
||||
def get_dynamic_axes(self):
|
||||
xB = "2B" if self.xB == 2 else "B"
|
||||
return {
|
||||
"sample": {0: xB, 2: "H", 3: "W"},
|
||||
"encoder_hidden_states": {0: xB},
|
||||
"latent": {0: xB, 2: "H", 3: "W"},
|
||||
"text_embeds": {0: xB},
|
||||
"time_ids": {0: xB},
|
||||
}
|
||||
|
||||
def get_input_profile(self, batch_size, image_height, image_width, static_batch, static_shape):
|
||||
# WAR to enable inference for H/W that are not multiples of 16
|
||||
# If building with Dynamic Shapes: ensure image height and width are not multiples of 16 for ONNX export and TensorRT engine build
|
||||
if not static_shape:
|
||||
image_height = image_height - 8 if image_height % 16 == 0 else image_height
|
||||
image_width = image_width - 8 if image_width % 16 == 0 else image_width
|
||||
latent_height, latent_width = self.check_dims(batch_size, image_height, image_width)
|
||||
min_batch, max_batch, _, _, _, _, min_latent_height, max_latent_height, min_latent_width, max_latent_width = (
|
||||
self.get_minmax_dims(batch_size, image_height, image_width, static_batch, static_shape)
|
||||
)
|
||||
return {
|
||||
"sample": [
|
||||
(self.xB * min_batch, self.unet_dim, min_latent_height, min_latent_width),
|
||||
(self.xB * batch_size, self.unet_dim, latent_height, latent_width),
|
||||
(self.xB * max_batch, self.unet_dim, max_latent_height, max_latent_width),
|
||||
],
|
||||
"encoder_hidden_states": [
|
||||
(self.xB * min_batch, self.text_maxlen, self.embedding_dim),
|
||||
(self.xB * batch_size, self.text_maxlen, self.embedding_dim),
|
||||
(self.xB * max_batch, self.text_maxlen, self.embedding_dim),
|
||||
],
|
||||
"text_embeds": [(self.xB * min_batch, 1280), (self.xB * batch_size, 1280), (self.xB * max_batch, 1280)],
|
||||
"time_ids": [
|
||||
(self.xB * min_batch, self.time_dim),
|
||||
(self.xB * batch_size, self.time_dim),
|
||||
(self.xB * max_batch, self.time_dim),
|
||||
],
|
||||
}
|
||||
|
||||
def get_shape_dict(self, batch_size, image_height, image_width):
|
||||
latent_height, latent_width = self.check_dims(batch_size, image_height, image_width)
|
||||
return {
|
||||
"sample": (self.xB * batch_size, self.unet_dim, latent_height, latent_width),
|
||||
"encoder_hidden_states": (self.xB * batch_size, self.text_maxlen, self.embedding_dim),
|
||||
"latent": (self.xB * batch_size, 4, latent_height, latent_width),
|
||||
"text_embeds": (self.xB * batch_size, 1280),
|
||||
"time_ids": (self.xB * batch_size, self.time_dim),
|
||||
}
|
||||
|
||||
def get_sample_input(self, batch_size, image_height, image_width, static_shape):
|
||||
# WAR to enable inference for H/W that are not multiples of 16
|
||||
# If building with Dynamic Shapes: ensure image height and width are not multiples of 16 for ONNX export and TensorRT engine build
|
||||
if not static_shape:
|
||||
image_height = image_height - 8 if image_height % 16 == 0 else image_height
|
||||
image_width = image_width - 8 if image_width % 16 == 0 else image_width
|
||||
latent_height, latent_width = self.check_dims(batch_size, image_height, image_width)
|
||||
dtype = torch.float16 if self.fp16 else torch.float32
|
||||
return (
|
||||
torch.randn(
|
||||
self.xB * batch_size, self.unet_dim, latent_height, latent_width, dtype=dtype, device=self.device
|
||||
),
|
||||
torch.tensor([1.0], dtype=dtype, device=self.device),
|
||||
torch.randn(self.xB * batch_size, self.text_maxlen, self.embedding_dim, dtype=dtype, device=self.device),
|
||||
{
|
||||
"added_cond_kwargs": {
|
||||
"text_embeds": torch.randn(self.xB * batch_size, 1280, dtype=dtype, device=self.device),
|
||||
"time_ids": torch.randn(self.xB * batch_size, self.time_dim, dtype=dtype, device=self.device),
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
def optimize(self, onnx_graph):
|
||||
if self.fp8:
|
||||
return super().optimize(onnx_graph, modify_fp8_graph=True)
|
||||
if self.int8:
|
||||
return super().optimize(onnx_graph, modify_int8_graph=True)
|
||||
return super().optimize(onnx_graph)
|
||||
|
||||
|
||||
class UNetXLModelControlNet(UNetXLModel):
|
||||
def __init__(
|
||||
self,
|
||||
version,
|
||||
pipeline,
|
||||
device,
|
||||
hf_token,
|
||||
verbose,
|
||||
framework_model_dir,
|
||||
fp16=False,
|
||||
int8=False,
|
||||
fp8=False,
|
||||
max_batch_size=16,
|
||||
text_maxlen=77,
|
||||
controlnets=None,
|
||||
do_classifier_free_guidance=False,
|
||||
):
|
||||
super().__init__(
|
||||
version=version,
|
||||
pipeline=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,
|
||||
do_classifier_free_guidance=do_classifier_free_guidance,
|
||||
)
|
||||
self.controlnets = load.get_path(version, pipeline, controlnets) if controlnets else None
|
||||
|
||||
def get_pipeline(self):
|
||||
cnet_model_opts = {"torch_dtype": torch.float16} if self.fp16 else {}
|
||||
controlnets = [
|
||||
ControlNetModel.from_pretrained(path, **cnet_model_opts).to(self.device) for path in self.controlnets
|
||||
]
|
||||
if self.bf16:
|
||||
model_opts = {"torch_dtype": torch.bfloat16}
|
||||
elif self.fp16:
|
||||
model_opts = {"variant": "fp16", "torch_dtype": torch.float16}
|
||||
else:
|
||||
model_opts = {}
|
||||
pipeline = StableDiffusionXLControlNetPipeline.from_pretrained(
|
||||
self.path,
|
||||
use_safetensors=self.hf_safetensor,
|
||||
token=self.hf_token,
|
||||
controlnet=controlnets,
|
||||
**model_opts,
|
||||
).to(self.device)
|
||||
return pipeline
|
||||
|
||||
def get_model(self, torch_inference=""):
|
||||
model_opts = {"variant": "fp16", "torch_dtype": torch.float16} if self.fp16 else {}
|
||||
unet_model = UNet2DConditionModel.from_pretrained(
|
||||
self.path,
|
||||
subfolder=self.subfolder,
|
||||
use_safetensors=self.hf_safetensor,
|
||||
token=self.hf_token,
|
||||
**model_opts,
|
||||
).to(self.device)
|
||||
cnet_model_opts = {"torch_dtype": torch.float16} if self.fp16 else {}
|
||||
controlnets = torch.nn.ModuleList(
|
||||
[ControlNetModel.from_pretrained(path, **cnet_model_opts).to(self.device) for path in self.controlnets]
|
||||
)
|
||||
# FIXME - cache UNet2DConditionControlNetModel
|
||||
model = UNet2DConditionControlNetModel(unet_model, controlnets)
|
||||
model = optimizer.optimize_checkpoint(model, torch_inference)
|
||||
return model
|
||||
|
||||
def get_input_names(self):
|
||||
return ["sample", "timestep", "encoder_hidden_states", "images", "controlnet_scales", "text_embeds", "time_ids"]
|
||||
|
||||
def get_dynamic_axes(self):
|
||||
xB = "2B" if self.xB == 2 else "B"
|
||||
result = super().get_dynamic_axes()
|
||||
result["images"] = {1: xB, 3: "8H", 4: "8W"}
|
||||
return result
|
||||
|
||||
def get_input_profile(self, batch_size, image_height, image_width, static_batch, static_shape):
|
||||
min_batch, max_batch, min_image_height, max_image_height, min_image_width, max_image_width, _, _, _, _ = (
|
||||
self.get_minmax_dims(batch_size, image_height, image_width, static_batch, static_shape)
|
||||
)
|
||||
result = super().get_input_profile(batch_size, image_height, image_width, static_batch, static_shape)
|
||||
result["images"] = [
|
||||
(len(self.controlnets), self.xB * min_batch, 3, min_image_height, min_image_width),
|
||||
(len(self.controlnets), self.xB * batch_size, 3, image_height, image_width),
|
||||
(len(self.controlnets), self.xB * max_batch, 3, max_image_height, max_image_width),
|
||||
]
|
||||
return result
|
||||
|
||||
def get_shape_dict(self, batch_size, image_height, image_width):
|
||||
result = super().get_shape_dict(batch_size, image_height, image_width)
|
||||
result["images"] = (len(self.controlnets), self.xB * batch_size, 3, image_height, image_width)
|
||||
return result
|
||||
|
||||
def get_sample_input(self, batch_size, image_height, image_width, static_shape):
|
||||
dtype = torch.float16 if self.fp16 else torch.float32
|
||||
result = super().get_sample_input(batch_size, image_height, image_width, static_shape)
|
||||
result = (
|
||||
result[:-1]
|
||||
+ (
|
||||
torch.randn(
|
||||
len(self.controlnets),
|
||||
self.xB * batch_size,
|
||||
3,
|
||||
image_height,
|
||||
image_width,
|
||||
dtype=dtype,
|
||||
device=self.device,
|
||||
), # images
|
||||
torch.randn(len(self.controlnets), dtype=dtype, device=self.device), # controlnet_scales
|
||||
)
|
||||
+ result[-1:]
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
class UNetTemporalModel(base_model.BaseModel):
|
||||
def __init__(
|
||||
self,
|
||||
version,
|
||||
pipeline,
|
||||
device,
|
||||
hf_token,
|
||||
verbose,
|
||||
framework_model_dir,
|
||||
fp16=False,
|
||||
fp8=False,
|
||||
max_batch_size=16,
|
||||
num_frames=14,
|
||||
do_classifier_free_guidance=True,
|
||||
):
|
||||
super(UNetTemporalModel, self).__init__(
|
||||
version,
|
||||
pipeline,
|
||||
device=device,
|
||||
hf_token=hf_token,
|
||||
verbose=verbose,
|
||||
framework_model_dir=framework_model_dir,
|
||||
fp16=fp16,
|
||||
fp8=fp8,
|
||||
max_batch_size=max_batch_size,
|
||||
embedding_dim=get_unet_embedding_dim(version, pipeline),
|
||||
)
|
||||
self.subfolder = "unet"
|
||||
self.unet_dim = 4
|
||||
self.num_frames = num_frames
|
||||
self.out_channels = 4
|
||||
self.cross_attention_dim = 1024
|
||||
self.xB = 2 if do_classifier_free_guidance else 1 # batch multiplier
|
||||
|
||||
def get_model(self, torch_inference=""):
|
||||
model_opts = {"torch_dtype": torch.float16} if self.fp16 else {}
|
||||
unet_model_dir = load.get_checkpoint_dir(self.framework_model_dir, self.version, self.pipeline, self.subfolder)
|
||||
if not load.is_model_cached(unet_model_dir, model_opts, self.hf_safetensor):
|
||||
model = UNetSpatioTemporalConditionModel.from_pretrained(
|
||||
self.path,
|
||||
subfolder=self.subfolder,
|
||||
use_safetensors=self.hf_safetensor,
|
||||
token=self.hf_token,
|
||||
**model_opts,
|
||||
).to(self.device)
|
||||
model.save_pretrained(unet_model_dir, **model_opts)
|
||||
else:
|
||||
print(f"[I] Load UNetSpatioTemporalConditionModel model from: {unet_model_dir}")
|
||||
model = UNetSpatioTemporalConditionModel.from_pretrained(unet_model_dir, **model_opts).to(self.device)
|
||||
model = optimizer.optimize_checkpoint(model, torch_inference)
|
||||
return model
|
||||
|
||||
def get_input_names(self):
|
||||
return ["sample", "timestep", "encoder_hidden_states", "added_time_ids"]
|
||||
|
||||
def get_output_names(self):
|
||||
return ["latent"]
|
||||
|
||||
def get_dynamic_axes(self):
|
||||
xB = str(self.xB) + "B"
|
||||
return {
|
||||
"sample": {0: xB, 1: "num_frames", 3: "H", 4: "W"},
|
||||
"encoder_hidden_states": {0: xB},
|
||||
"added_time_ids": {0: xB},
|
||||
}
|
||||
|
||||
def get_input_profile(self, batch_size, image_height, image_width, static_batch, static_shape):
|
||||
latent_height, latent_width = self.check_dims(batch_size, image_height, image_width)
|
||||
(
|
||||
min_batch,
|
||||
max_batch,
|
||||
min_image_height,
|
||||
max_image_height,
|
||||
min_image_width,
|
||||
max_image_width,
|
||||
min_latent_height,
|
||||
max_latent_height,
|
||||
min_latent_width,
|
||||
max_latent_width,
|
||||
) = self.get_minmax_dims(batch_size, image_height, image_width, static_batch, static_shape)
|
||||
return {
|
||||
"sample": [
|
||||
(self.xB * min_batch, self.num_frames, 2 * self.out_channels, min_latent_height, min_latent_width),
|
||||
(self.xB * batch_size, self.num_frames, 2 * self.out_channels, latent_height, latent_width),
|
||||
(self.xB * max_batch, self.num_frames, 2 * self.out_channels, max_latent_height, max_latent_width),
|
||||
],
|
||||
"encoder_hidden_states": [
|
||||
(self.xB * min_batch, 1, self.cross_attention_dim),
|
||||
(self.xB * batch_size, 1, self.cross_attention_dim),
|
||||
(self.xB * max_batch, 1, self.cross_attention_dim),
|
||||
],
|
||||
"added_time_ids": [(self.xB * min_batch, 3), (self.xB * batch_size, 3), (self.xB * max_batch, 3)],
|
||||
}
|
||||
|
||||
def get_shape_dict(self, batch_size, image_height, image_width):
|
||||
latent_height, latent_width = self.check_dims(batch_size, image_height, image_width)
|
||||
return {
|
||||
"sample": (self.xB * batch_size, self.num_frames, 2 * self.out_channels, latent_height, latent_width),
|
||||
"timestep": (1,),
|
||||
"encoder_hidden_states": (self.xB * batch_size, 1, self.cross_attention_dim),
|
||||
"added_time_ids": (self.xB * batch_size, 3),
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
dtype = torch.float16 if self.fp16 else torch.float32
|
||||
return (
|
||||
torch.randn(
|
||||
self.xB * batch_size,
|
||||
self.num_frames,
|
||||
2 * self.out_channels,
|
||||
latent_height,
|
||||
latent_width,
|
||||
dtype=dtype,
|
||||
device=self.device,
|
||||
),
|
||||
torch.tensor([1.0], dtype=torch.float32, device=self.device),
|
||||
torch.randn(self.xB * batch_size, 1, self.cross_attention_dim, dtype=dtype, device=self.device),
|
||||
torch.randn(self.xB * batch_size, 3, dtype=dtype, device=self.device),
|
||||
)
|
||||
|
||||
def optimize(self, onnx_graph):
|
||||
return super().optimize(onnx_graph, modify_fp8_graph=self.fp8)
|
||||
|
||||
|
||||
class UNetCascadeModel(base_model.BaseModel):
|
||||
def __init__(
|
||||
self,
|
||||
version,
|
||||
pipeline,
|
||||
device,
|
||||
hf_token,
|
||||
verbose,
|
||||
framework_model_dir,
|
||||
fp16=False,
|
||||
bf16=False,
|
||||
max_batch_size=16,
|
||||
text_maxlen=77,
|
||||
do_classifier_free_guidance=False,
|
||||
compression_factor=42,
|
||||
latent_dim_scale=10.67,
|
||||
image_embedding_dim=768,
|
||||
lite=False,
|
||||
):
|
||||
super(UNetCascadeModel, 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,
|
||||
text_maxlen=text_maxlen,
|
||||
embedding_dim=get_unet_embedding_dim(version, pipeline),
|
||||
compression_factor=compression_factor,
|
||||
)
|
||||
self.is_prior = True if pipeline.is_cascade_prior() else False
|
||||
self.subfolder = "prior" if self.is_prior else "decoder"
|
||||
if lite:
|
||||
self.subfolder += "_lite"
|
||||
self.prior_dim = 16
|
||||
self.decoder_dim = 4
|
||||
self.xB = 2 if do_classifier_free_guidance else 1 # batch multiplier
|
||||
self.latent_dim_scale = latent_dim_scale
|
||||
self.min_latent_shape = self.min_image_shape // self.compression_factor
|
||||
self.max_latent_shape = self.max_image_shape // self.compression_factor
|
||||
self.do_constant_folding = False
|
||||
self.image_embedding_dim = image_embedding_dim
|
||||
|
||||
def get_model(self, torch_inference=""):
|
||||
# FP16 variant doesn't exist
|
||||
model_opts = {"torch_dtype": torch.float16} if self.fp16 else {}
|
||||
model_opts = {"variant": "bf16", "torch_dtype": torch.bfloat16} if self.bf16 else model_opts
|
||||
unet_model_dir = load.get_checkpoint_dir(self.framework_model_dir, self.version, self.pipeline, self.subfolder)
|
||||
if not load.is_model_cached(unet_model_dir, model_opts, self.hf_safetensor):
|
||||
model = StableCascadeUNet.from_pretrained(
|
||||
self.path,
|
||||
subfolder=self.subfolder,
|
||||
use_safetensors=self.hf_safetensor,
|
||||
token=self.hf_token,
|
||||
**model_opts,
|
||||
).to(self.device)
|
||||
model.save_pretrained(unet_model_dir, **model_opts)
|
||||
else:
|
||||
print(f"[I] Load Stable Cascade UNet pytorch model from: {unet_model_dir}")
|
||||
model = StableCascadeUNet.from_pretrained(unet_model_dir, **model_opts).to(self.device)
|
||||
model = optimizer.optimize_checkpoint(model, torch_inference)
|
||||
return model
|
||||
|
||||
def get_input_names(self):
|
||||
if self.is_prior:
|
||||
return ["sample", "timestep_ratio", "clip_text_pooled", "clip_text", "clip_img"]
|
||||
else:
|
||||
return ["sample", "timestep_ratio", "clip_text_pooled", "effnet"]
|
||||
|
||||
def get_output_names(self):
|
||||
return ["latent"]
|
||||
|
||||
def get_dynamic_axes(self):
|
||||
xB = "2B" if self.xB == 2 else "B"
|
||||
if self.is_prior:
|
||||
return {
|
||||
"sample": {0: xB, 2: "H", 3: "W"},
|
||||
"timestep_ratio": {0: xB},
|
||||
"clip_text_pooled": {0: xB},
|
||||
"clip_text": {0: xB},
|
||||
"clip_img": {0: xB},
|
||||
"latent": {0: xB, 2: "H", 3: "W"},
|
||||
}
|
||||
else:
|
||||
return {
|
||||
"sample": {0: xB, 2: "H", 3: "W"},
|
||||
"timestep_ratio": {0: xB},
|
||||
"clip_text_pooled": {0: xB},
|
||||
"effnet": {0: xB, 2: "H_effnet", 3: "W_effnet"},
|
||||
"latent": {0: xB, 2: "H", 3: "W"},
|
||||
}
|
||||
|
||||
def get_input_profile(self, batch_size, image_height, image_width, static_batch, static_shape):
|
||||
latent_height, latent_width = self.check_dims(batch_size, image_height, image_width)
|
||||
min_batch, max_batch, _, _, _, _, min_latent_height, max_latent_height, min_latent_width, max_latent_width = (
|
||||
self.get_minmax_dims(batch_size, image_height, image_width, static_batch, static_shape)
|
||||
)
|
||||
if self.is_prior:
|
||||
return {
|
||||
"sample": [
|
||||
(self.xB * min_batch, self.prior_dim, min_latent_height, min_latent_width),
|
||||
(self.xB * batch_size, self.prior_dim, latent_height, latent_width),
|
||||
(self.xB * max_batch, self.prior_dim, max_latent_height, max_latent_width),
|
||||
],
|
||||
"timestep_ratio": [(self.xB * min_batch,), (self.xB * batch_size,), (self.xB * max_batch,)],
|
||||
"clip_text_pooled": [
|
||||
(self.xB * min_batch, 1, self.embedding_dim),
|
||||
(self.xB * batch_size, 1, self.embedding_dim),
|
||||
(self.xB * max_batch, 1, self.embedding_dim),
|
||||
],
|
||||
"clip_text": [
|
||||
(self.xB * min_batch, self.text_maxlen, self.embedding_dim),
|
||||
(self.xB * batch_size, self.text_maxlen, self.embedding_dim),
|
||||
(self.xB * max_batch, self.text_maxlen, self.embedding_dim),
|
||||
],
|
||||
"clip_img": [
|
||||
(self.xB * min_batch, 1, self.image_embedding_dim),
|
||||
(self.xB * batch_size, 1, self.image_embedding_dim),
|
||||
(self.xB * max_batch, 1, self.image_embedding_dim),
|
||||
],
|
||||
}
|
||||
else:
|
||||
return {
|
||||
"sample": [
|
||||
(
|
||||
self.xB * min_batch,
|
||||
self.decoder_dim,
|
||||
int(min_latent_height * self.latent_dim_scale),
|
||||
int(min_latent_width * self.latent_dim_scale),
|
||||
),
|
||||
(
|
||||
self.xB * batch_size,
|
||||
self.decoder_dim,
|
||||
int(latent_height * self.latent_dim_scale),
|
||||
int(latent_width * self.latent_dim_scale),
|
||||
),
|
||||
(
|
||||
self.xB * max_batch,
|
||||
self.decoder_dim,
|
||||
int(max_latent_height * self.latent_dim_scale),
|
||||
int(max_latent_width * self.latent_dim_scale),
|
||||
),
|
||||
],
|
||||
"timestep_ratio": [(self.xB * min_batch,), (self.xB * batch_size,), (self.xB * max_batch,)],
|
||||
"clip_text_pooled": [
|
||||
(self.xB * min_batch, 1, self.embedding_dim),
|
||||
(self.xB * batch_size, 1, self.embedding_dim),
|
||||
(self.xB * max_batch, 1, self.embedding_dim),
|
||||
],
|
||||
"effnet": [
|
||||
(self.xB * min_batch, self.prior_dim, min_latent_height, min_latent_width),
|
||||
(self.xB * batch_size, self.prior_dim, latent_height, latent_width),
|
||||
(self.xB * max_batch, self.prior_dim, max_latent_height, max_latent_width),
|
||||
],
|
||||
}
|
||||
|
||||
def get_shape_dict(self, batch_size, image_height, image_width):
|
||||
latent_height, latent_width = self.check_dims(batch_size, image_height, image_width)
|
||||
if self.is_prior:
|
||||
return {
|
||||
"sample": (self.xB * batch_size, self.prior_dim, latent_height, latent_width),
|
||||
"timestep_ratio": (self.xB * batch_size,),
|
||||
"clip_text_pooled": (self.xB * batch_size, 1, self.embedding_dim),
|
||||
"clip_text": (self.xB * batch_size, self.text_maxlen, self.embedding_dim),
|
||||
"clip_img": (self.xB * batch_size, 1, self.image_embedding_dim),
|
||||
"latent": (self.xB * batch_size, self.prior_dim, latent_height, latent_width),
|
||||
}
|
||||
else:
|
||||
return {
|
||||
"sample": (
|
||||
self.xB * batch_size,
|
||||
self.decoder_dim,
|
||||
int(latent_height * self.latent_dim_scale),
|
||||
int(latent_width * self.latent_dim_scale),
|
||||
),
|
||||
"timestep_ratio": (self.xB * batch_size,),
|
||||
"clip_text_pooled": (self.xB * batch_size, 1, self.embedding_dim),
|
||||
"effnet": (self.xB * batch_size, self.prior_dim, latent_height, latent_width),
|
||||
"latent": (
|
||||
self.xB * batch_size,
|
||||
self.decoder_dim,
|
||||
int(latent_height * self.latent_dim_scale),
|
||||
int(latent_width * self.latent_dim_scale),
|
||||
),
|
||||
}
|
||||
|
||||
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.bfloat16 if self.bf16 else torch.float32
|
||||
if self.is_prior:
|
||||
return (
|
||||
torch.randn(batch_size, self.prior_dim, latent_height, latent_width, dtype=dtype, device=self.device),
|
||||
torch.tensor([1.0] * batch_size, dtype=dtype, device=self.device),
|
||||
torch.randn(batch_size, 1, self.embedding_dim, dtype=dtype, device=self.device),
|
||||
{
|
||||
"clip_text": torch.randn(
|
||||
batch_size, self.text_maxlen, self.embedding_dim, dtype=dtype, device=self.device
|
||||
),
|
||||
"clip_img": torch.randn(batch_size, 1, self.image_embedding_dim, dtype=dtype, device=self.device),
|
||||
},
|
||||
)
|
||||
else:
|
||||
return (
|
||||
torch.randn(
|
||||
batch_size,
|
||||
self.decoder_dim,
|
||||
int(latent_height * self.latent_dim_scale),
|
||||
int(latent_width * self.latent_dim_scale),
|
||||
dtype=dtype,
|
||||
device=self.device,
|
||||
),
|
||||
torch.tensor([1.0] * batch_size, dtype=dtype, device=self.device),
|
||||
torch.randn(batch_size, 1, self.embedding_dim, dtype=dtype, device=self.device),
|
||||
{
|
||||
"effnet": torch.randn(
|
||||
batch_size, self.prior_dim, latent_height, latent_width, dtype=dtype, device=self.device
|
||||
),
|
||||
},
|
||||
)
|
||||
@@ -0,0 +1,671 @@
|
||||
#
|
||||
# SPDX-FileCopyrightText: Copyright (c) 1993-2025 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 os
|
||||
|
||||
import torch
|
||||
from huggingface_hub import hf_hub_download
|
||||
from safetensors import safe_open
|
||||
|
||||
from demo_diffusion.dynamic_import import import_from_diffusers
|
||||
from demo_diffusion.model import base_model, load, optimizer
|
||||
from demo_diffusion.utils_sd3.other_impls import load_into
|
||||
from demo_diffusion.utils_sd3.sd3_impls import SDVAE
|
||||
|
||||
# List of models to import from diffusers.models
|
||||
models_to_import = ["AutoencoderKL", "AutoencoderKLTemporalDecoder", "AutoencoderKLWan"]
|
||||
for model in models_to_import:
|
||||
globals()[model] = import_from_diffusers(model, "diffusers.models")
|
||||
|
||||
# Import FluxKontextUtil from pipeline module
|
||||
# Using a deferred import to avoid circular dependencies
|
||||
def _get_flux_kontext_util():
|
||||
from demo_diffusion.pipeline.flux_pipeline import FluxKontextUtil
|
||||
return FluxKontextUtil
|
||||
|
||||
|
||||
class VAEModel(base_model.BaseModel):
|
||||
def __init__(
|
||||
self,
|
||||
version,
|
||||
pipeline,
|
||||
device,
|
||||
hf_token,
|
||||
verbose,
|
||||
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,
|
||||
bf16=bf16,
|
||||
max_batch_size=max_batch_size,
|
||||
)
|
||||
self.do_constant_folding = False
|
||||
self.subfolder = "vae"
|
||||
self.vae_decoder_model_dir = load.get_checkpoint_dir(
|
||||
self.framework_model_dir, self.version, self.pipeline, self.subfolder
|
||||
)
|
||||
if not os.path.exists(self.vae_decoder_model_dir):
|
||||
self.config = AutoencoderKL.load_config(self.path, subfolder=self.subfolder, token=self.hf_token)
|
||||
else:
|
||||
print(f"[I] Load AutoencoderKL (decoder) config from: {self.vae_decoder_model_dir}")
|
||||
self.config = AutoencoderKL.load_config(self.vae_decoder_model_dir)
|
||||
|
||||
def get_model(self, torch_inference=""):
|
||||
model_opts = (
|
||||
{"torch_dtype": torch.float16} if self.fp16 else {"torch_dtype": torch.bfloat16} if self.bf16 else {}
|
||||
)
|
||||
if not load.is_model_cached(self.vae_decoder_model_dir, model_opts, self.hf_safetensor):
|
||||
model = AutoencoderKL.from_pretrained(
|
||||
self.path,
|
||||
subfolder=self.subfolder,
|
||||
use_safetensors=self.hf_safetensor,
|
||||
token=self.hf_token,
|
||||
**model_opts,
|
||||
).to(self.device)
|
||||
model.save_pretrained(self.vae_decoder_model_dir, **model_opts)
|
||||
else:
|
||||
print(f"[I] Load AutoencoderKL (decoder) model from: {self.vae_decoder_model_dir}")
|
||||
model = AutoencoderKL.from_pretrained(self.vae_decoder_model_dir, **model_opts).to(self.device)
|
||||
model.forward = model.decode
|
||||
model = optimizer.optimize_checkpoint(model, torch_inference)
|
||||
return model
|
||||
|
||||
def get_input_names(self):
|
||||
return ["latent"]
|
||||
|
||||
def get_output_names(self):
|
||||
return ["images"]
|
||||
|
||||
def get_dynamic_axes(self):
|
||||
return {"latent": {0: "B", 2: "H", 3: "W"}, "images": {0: "B", 2: "8H", 3: "8W"}}
|
||||
|
||||
def get_input_profile(self, batch_size, image_height, image_width, static_batch, static_shape):
|
||||
latent_height, latent_width = self.check_dims(batch_size, image_height, image_width)
|
||||
min_batch, max_batch, _, _, _, _, min_latent_height, max_latent_height, min_latent_width, max_latent_width = (
|
||||
self.get_minmax_dims(batch_size, image_height, image_width, static_batch, static_shape)
|
||||
)
|
||||
return {
|
||||
"latent": [
|
||||
(min_batch, self.config["latent_channels"], min_latent_height, min_latent_width),
|
||||
(batch_size, self.config["latent_channels"], latent_height, latent_width),
|
||||
(max_batch, self.config["latent_channels"], max_latent_height, max_latent_width),
|
||||
]
|
||||
}
|
||||
|
||||
def get_shape_dict(self, batch_size, image_height, image_width):
|
||||
latent_height, latent_width = self.check_dims(batch_size, image_height, image_width)
|
||||
return {
|
||||
"latent": (batch_size, self.config["latent_channels"], latent_height, latent_width),
|
||||
"images": (batch_size, 3, 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.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(base_model.BaseModel):
|
||||
def __init__(
|
||||
self,
|
||||
version,
|
||||
pipeline,
|
||||
device,
|
||||
hf_token,
|
||||
verbose,
|
||||
framework_model_dir,
|
||||
max_batch_size,
|
||||
fp16=False,
|
||||
):
|
||||
super(SD3_VAEDecoderModel, self).__init__(
|
||||
version,
|
||||
pipeline,
|
||||
device=device,
|
||||
hf_token=hf_token,
|
||||
verbose=verbose,
|
||||
framework_model_dir=framework_model_dir,
|
||||
fp16=fp16,
|
||||
max_batch_size=max_batch_size,
|
||||
)
|
||||
self.subfolder = "sd3"
|
||||
|
||||
def get_model(self, torch_inference=""):
|
||||
dtype = torch.float16 if self.fp16 else torch.float32
|
||||
sd3_model_dir = load.get_checkpoint_dir(self.framework_model_dir, self.version, self.pipeline, self.subfolder)
|
||||
sd3_filename = "sd3_medium.safetensors"
|
||||
sd3_model_path = f"{sd3_model_dir}/{sd3_filename}"
|
||||
if not os.path.exists(sd3_model_path):
|
||||
hf_hub_download(repo_id=self.path, filename=sd3_filename, local_dir=sd3_model_dir)
|
||||
with safe_open(sd3_model_path, framework="pt", device=self.device) as f:
|
||||
model = SDVAE(device=self.device, dtype=dtype).eval().cuda()
|
||||
prefix = ""
|
||||
if any(k.startswith("first_stage_model.") for k in f.keys()):
|
||||
prefix = "first_stage_model."
|
||||
load_into(f, model, prefix, self.device, dtype)
|
||||
model.forward = model.decode
|
||||
model = optimizer.optimize_checkpoint(model, torch_inference)
|
||||
return model
|
||||
|
||||
def get_input_names(self):
|
||||
return ["latent"]
|
||||
|
||||
def get_output_names(self):
|
||||
return ["images"]
|
||||
|
||||
def get_dynamic_axes(self):
|
||||
return {"latent": {0: "B", 2: "H", 3: "W"}, "images": {0: "B", 2: "8H", 3: "8W"}}
|
||||
|
||||
def get_input_profile(self, batch_size, image_height, image_width, static_batch, static_shape):
|
||||
latent_height, latent_width = self.check_dims(batch_size, image_height, image_width)
|
||||
min_batch, max_batch, _, _, _, _, min_latent_height, max_latent_height, min_latent_width, max_latent_width = (
|
||||
self.get_minmax_dims(batch_size, image_height, image_width, static_batch, static_shape)
|
||||
)
|
||||
return {
|
||||
"latent": [
|
||||
(min_batch, 16, min_latent_height, min_latent_width),
|
||||
(batch_size, 16, latent_height, latent_width),
|
||||
(max_batch, 16, max_latent_height, max_latent_width),
|
||||
]
|
||||
}
|
||||
|
||||
def get_shape_dict(self, batch_size, image_height, image_width):
|
||||
latent_height, latent_width = self.check_dims(batch_size, image_height, image_width)
|
||||
return {
|
||||
"latent": (batch_size, 16, latent_height, latent_width),
|
||||
"images": (batch_size, 3, 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
|
||||
return torch.randn(batch_size, 16, latent_height, latent_width, dtype=dtype, device=self.device)
|
||||
|
||||
|
||||
class VAEDecTemporalModel(base_model.BaseModel):
|
||||
def __init__(
|
||||
self,
|
||||
version,
|
||||
pipeline,
|
||||
device,
|
||||
hf_token,
|
||||
verbose,
|
||||
framework_model_dir,
|
||||
max_batch_size=16,
|
||||
decode_chunk_size=14,
|
||||
):
|
||||
super(VAEDecTemporalModel, self).__init__(
|
||||
version,
|
||||
pipeline,
|
||||
device=device,
|
||||
hf_token=hf_token,
|
||||
verbose=verbose,
|
||||
framework_model_dir=framework_model_dir,
|
||||
max_batch_size=max_batch_size,
|
||||
)
|
||||
self.subfolder = "vae"
|
||||
self.decode_chunk_size = decode_chunk_size
|
||||
|
||||
def get_model(self, torch_inference=""):
|
||||
vae_decoder_model_path = load.get_checkpoint_dir(
|
||||
self.framework_model_dir, self.version, self.pipeline, self.subfolder
|
||||
)
|
||||
if not os.path.exists(vae_decoder_model_path):
|
||||
model = AutoencoderKLTemporalDecoder.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)
|
||||
else:
|
||||
print(f"[I] Load AutoencoderKLTemporalDecoder model from: {vae_decoder_model_path}")
|
||||
model = AutoencoderKLTemporalDecoder.from_pretrained(vae_decoder_model_path).to(self.device)
|
||||
model.forward = model.decode
|
||||
model = optimizer.optimize_checkpoint(model, torch_inference)
|
||||
return model
|
||||
|
||||
def get_input_names(self):
|
||||
return ["latent", "num_frames_in"]
|
||||
|
||||
def get_output_names(self):
|
||||
return ["frames"]
|
||||
|
||||
def get_dynamic_axes(self):
|
||||
return {"latent": {0: "num_frames_in", 2: "H", 3: "W"}, "frames": {0: "num_frames_in", 2: "8H", 3: "8W"}}
|
||||
|
||||
def get_input_profile(self, batch_size, image_height, image_width, static_batch, static_shape):
|
||||
latent_height, latent_width = self.check_dims(batch_size, image_height, image_width)
|
||||
assert batch_size == 1
|
||||
_, _, _, _, _, _, min_latent_height, max_latent_height, min_latent_width, max_latent_width = (
|
||||
self.get_minmax_dims(batch_size, image_height, image_width, static_batch, static_shape)
|
||||
)
|
||||
return {
|
||||
"latent": [
|
||||
(1, 4, min_latent_height, min_latent_width),
|
||||
(self.decode_chunk_size, 4, latent_height, latent_width),
|
||||
(self.decode_chunk_size, 4, max_latent_height, max_latent_width),
|
||||
],
|
||||
"num_frames_in": [(1,), (1,), (1,)],
|
||||
}
|
||||
|
||||
def get_shape_dict(self, batch_size, image_height, image_width):
|
||||
latent_height, latent_width = self.check_dims(batch_size, image_height, image_width)
|
||||
assert batch_size == 1
|
||||
return {
|
||||
"latent": (self.decode_chunk_size, 4, latent_height, latent_width),
|
||||
#'num_frames_in': (1,),
|
||||
"frames": (self.decode_chunk_size, 3, image_height, image_width),
|
||||
}
|
||||
|
||||
def get_sample_input(self, batch_size, image_height, image_width):
|
||||
latent_height, latent_width = self.check_dims(batch_size, image_height, image_width)
|
||||
assert batch_size == 1
|
||||
return (
|
||||
torch.randn(
|
||||
self.decode_chunk_size, 4, latent_height, latent_width, dtype=torch.float32, device=self.device
|
||||
),
|
||||
self.decode_chunk_size,
|
||||
)
|
||||
|
||||
|
||||
class TorchVAEEncoder(torch.nn.Module):
|
||||
def __init__(
|
||||
self,
|
||||
version,
|
||||
pipeline,
|
||||
hf_token,
|
||||
device,
|
||||
path,
|
||||
framework_model_dir,
|
||||
subfolder,
|
||||
fp16=False,
|
||||
bf16=False,
|
||||
hf_safetensor=False,
|
||||
):
|
||||
super().__init__()
|
||||
model_opts = {"torch_dtype": torch.float16} if fp16 else {"torch_dtype": torch.bfloat16} if bf16 else {}
|
||||
vae_encoder_model_dir = load.get_checkpoint_dir(framework_model_dir, version, pipeline, subfolder)
|
||||
if not load.is_model_cached(vae_encoder_model_dir, model_opts, hf_safetensor):
|
||||
self.vae_encoder = AutoencoderKL.from_pretrained(
|
||||
path, subfolder="vae", use_safetensors=hf_safetensor, token=hf_token, **model_opts
|
||||
).to(device)
|
||||
self.vae_encoder.save_pretrained(vae_encoder_model_dir, **model_opts)
|
||||
else:
|
||||
print(f"[I] Load AutoencoderKL (encoder) model from: {vae_encoder_model_dir}")
|
||||
self.vae_encoder = AutoencoderKL.from_pretrained(vae_encoder_model_dir, **model_opts).to(device)
|
||||
|
||||
def forward(self, x):
|
||||
return self.vae_encoder.encode(x).latent_dist.sample()
|
||||
|
||||
|
||||
class VAEEncoderModel(base_model.BaseModel):
|
||||
def __init__(
|
||||
self,
|
||||
version,
|
||||
pipeline,
|
||||
device,
|
||||
hf_token,
|
||||
verbose,
|
||||
framework_model_dir,
|
||||
fp16=False,
|
||||
tf32=False,
|
||||
bf16=False,
|
||||
max_batch_size=16,
|
||||
do_classifier_free_guidance=False,
|
||||
kontext_resolution=None,
|
||||
):
|
||||
super(VAEEncoderModel, 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.kontext_resolution = kontext_resolution
|
||||
self.subfolder = "vae"
|
||||
self.vae_encoder_model_dir = load.get_checkpoint_dir(
|
||||
framework_model_dir, version, self.pipeline, self.subfolder
|
||||
)
|
||||
if not os.path.exists(self.vae_encoder_model_dir):
|
||||
self.config = AutoencoderKL.load_config(self.path, subfolder=self.subfolder, token=self.hf_token)
|
||||
else:
|
||||
print(f"[I] Load AutoencoderKL (encoder) config from: {self.vae_encoder_model_dir}")
|
||||
self.config = AutoencoderKL.load_config(self.vae_encoder_model_dir)
|
||||
self.xB = 2 if do_classifier_free_guidance else 1 # batch multiplier
|
||||
|
||||
def get_model(self, torch_inference=""):
|
||||
vae_encoder = TorchVAEEncoder(
|
||||
self.version,
|
||||
self.pipeline,
|
||||
self.hf_token,
|
||||
self.device,
|
||||
self.path,
|
||||
self.framework_model_dir,
|
||||
self.subfolder,
|
||||
self.fp16,
|
||||
self.bf16,
|
||||
hf_safetensor=self.hf_safetensor,
|
||||
)
|
||||
return vae_encoder
|
||||
|
||||
def get_input_names(self):
|
||||
return ["images"]
|
||||
|
||||
def get_output_names(self):
|
||||
return ["latent"]
|
||||
|
||||
def get_dynamic_axes(self):
|
||||
xB = "2B" if self.xB == 2 else "B"
|
||||
return {"images": {0: xB, 2: "8H", 3: "8W"}, "latent": {0: xB, 2: "H", 3: "W"}}
|
||||
|
||||
def get_input_profile(self, batch_size, image_height, image_width, static_batch, static_shape):
|
||||
assert batch_size >= self.min_batch and batch_size <= self.max_batch
|
||||
min_batch = batch_size if static_batch else self.min_batch
|
||||
max_batch = batch_size if static_batch else self.max_batch
|
||||
self.check_dims(batch_size, image_height, image_width)
|
||||
min_batch, max_batch, min_image_height, max_image_height, min_image_width, max_image_width, _, _, _, _ = (
|
||||
self.get_minmax_dims(batch_size, image_height, image_width, static_batch, static_shape)
|
||||
)
|
||||
|
||||
if self.version == "flux.1-kontext-dev":
|
||||
FluxKontextUtil = _get_flux_kontext_util()
|
||||
min_latent_dim, max_latent_dim = FluxKontextUtil.get_min_max_kontext_dimensions()
|
||||
return {
|
||||
"images": [
|
||||
(self.xB * min_batch, 3, min_latent_dim[1], min_latent_dim[0]),
|
||||
(self.xB * batch_size, 3, self.kontext_resolution[1], self.kontext_resolution[0]),
|
||||
(self.xB * max_batch, 3, max_latent_dim[1], max_latent_dim[0]),
|
||||
],
|
||||
}
|
||||
return {
|
||||
"images": [
|
||||
(self.xB * min_batch, 3, min_image_height, min_image_width),
|
||||
(self.xB * batch_size, 3, image_height, image_width),
|
||||
(self.xB * max_batch, 3, max_image_height, max_image_width),
|
||||
],
|
||||
}
|
||||
|
||||
def get_shape_dict(self, batch_size, image_height, image_width):
|
||||
# Determine dimensions based on version
|
||||
if self.version == "flux.1-kontext-dev":
|
||||
img_h, img_w = self.kontext_resolution[1], self.kontext_resolution[0]
|
||||
else:
|
||||
img_h, img_w = image_height, image_width
|
||||
latent_height, latent_width = self.check_dims(batch_size, img_h, img_w)
|
||||
|
||||
return {
|
||||
"images": (self.xB * batch_size, 3, img_h, img_w),
|
||||
"latent": (self.xB * batch_size, self.config["latent_channels"], latent_height, latent_width),
|
||||
}
|
||||
|
||||
def get_sample_input(self, batch_size, image_height, image_width, static_shape):
|
||||
self.check_dims(batch_size, image_height, image_width)
|
||||
dtype = torch.float16 if self.fp16 else torch.bfloat16 if self.bf16 else torch.float32
|
||||
return torch.randn(self.xB * batch_size, 3, image_height, image_width, dtype=dtype, device=self.device)
|
||||
|
||||
|
||||
class SD3_VAEEncoderModel(base_model.BaseModel):
|
||||
def __init__(
|
||||
self,
|
||||
version,
|
||||
pipeline,
|
||||
device,
|
||||
hf_token,
|
||||
verbose,
|
||||
framework_model_dir,
|
||||
max_batch_size,
|
||||
fp16=False,
|
||||
):
|
||||
super(SD3_VAEEncoderModel, self).__init__(
|
||||
version,
|
||||
pipeline,
|
||||
device=device,
|
||||
hf_token=hf_token,
|
||||
verbose=verbose,
|
||||
framework_model_dir=framework_model_dir,
|
||||
fp16=fp16,
|
||||
max_batch_size=max_batch_size,
|
||||
)
|
||||
self.subfolder = "sd3"
|
||||
|
||||
def get_model(self, torch_inference=""):
|
||||
dtype = torch.float16 if self.fp16 else torch.float32
|
||||
sd3_model_dir = load.get_checkpoint_dir(self.framework_model_dir, self.version, self.pipeline, self.subfolder)
|
||||
sd3_filename = "sd3_medium.safetensors"
|
||||
sd3_model_path = f"{sd3_model_dir}/{sd3_filename}"
|
||||
if not os.path.exists(sd3_model_path):
|
||||
hf_hub_download(repo_id=self.path, filename=sd3_filename, local_dir=sd3_model_dir)
|
||||
with safe_open(sd3_model_path, framework="pt", device=self.device) as f:
|
||||
model = SDVAE(device=self.device, dtype=dtype).eval().cuda()
|
||||
prefix = ""
|
||||
if any(k.startswith("first_stage_model.") for k in f.keys()):
|
||||
prefix = "first_stage_model."
|
||||
load_into(f, model, prefix, self.device, dtype)
|
||||
model.forward = model.encode
|
||||
model = optimizer.optimize_checkpoint(model, torch_inference)
|
||||
return model
|
||||
|
||||
def get_input_names(self):
|
||||
return ["images"]
|
||||
|
||||
def get_output_names(self):
|
||||
return ["latent"]
|
||||
|
||||
def get_dynamic_axes(self):
|
||||
return {"images": {0: "B", 2: "8H", 3: "8W"}, "latent": {0: "B", 2: "H", 3: "W"}}
|
||||
|
||||
def get_input_profile(self, batch_size, image_height, image_width, static_batch, static_shape):
|
||||
min_batch, max_batch, _, _, _, _, _, _, _, _ = self.get_minmax_dims(
|
||||
batch_size, image_height, image_width, static_batch, static_shape
|
||||
)
|
||||
return {
|
||||
"images": [
|
||||
(min_batch, 3, image_height, image_width),
|
||||
(batch_size, 3, image_height, image_width),
|
||||
(max_batch, 3, image_height, image_width),
|
||||
]
|
||||
}
|
||||
|
||||
def get_shape_dict(self, batch_size, image_height, image_width):
|
||||
latent_height, latent_width = self.check_dims(batch_size, image_height, image_width)
|
||||
return {
|
||||
"images": (batch_size, 3, image_height, image_width),
|
||||
"latent": (batch_size, 16, latent_height, latent_width),
|
||||
}
|
||||
|
||||
def get_sample_input(self, batch_size, image_height, image_width, static_shape):
|
||||
dtype = torch.float16 if self.fp16 else torch.float32
|
||||
return torch.randn(batch_size, 3, image_height, image_width, dtype=dtype, device=self.device)
|
||||
|
||||
|
||||
class AutoencoderKLWanModel(base_model.BaseModel):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
version,
|
||||
pipeline,
|
||||
device,
|
||||
hf_token,
|
||||
verbose,
|
||||
framework_model_dir,
|
||||
fp16=False,
|
||||
tf32=False,
|
||||
bf16=False,
|
||||
max_batch_size=16,
|
||||
):
|
||||
super(AutoencoderKLWanModel, 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.is_wan_pipeline = version.startswith("wan")
|
||||
self.subfolder = "vae"
|
||||
self.vae_decoder_model_dir = load.get_checkpoint_dir(
|
||||
self.framework_model_dir, self.version, self.pipeline, self.subfolder
|
||||
)
|
||||
if not os.path.exists(self.vae_decoder_model_dir):
|
||||
self.config = AutoencoderKLWan.load_config(self.path, subfolder=self.subfolder, token=self.hf_token)
|
||||
else:
|
||||
print(f"[I] Load AutoencoderKLWan (decoder) config from: {self.vae_decoder_model_dir}")
|
||||
self.config = AutoencoderKLWan.load_config(self.vae_decoder_model_dir)
|
||||
|
||||
def get_model(self, torch_inference=""):
|
||||
if self.is_wan_pipeline:
|
||||
print(f"[I] Using float32 precision for Wan 2.2 VAE decoder")
|
||||
model_opts = {"torch_dtype": torch.float32}
|
||||
else:
|
||||
model_opts = (
|
||||
{"torch_dtype": torch.float16} if self.fp16 else {"torch_dtype": torch.bfloat16} if self.bf16 else {}
|
||||
)
|
||||
if not load.is_model_cached(self.vae_decoder_model_dir, model_opts, self.hf_safetensor):
|
||||
model = AutoencoderKLWan.from_pretrained(
|
||||
self.path,
|
||||
subfolder=self.subfolder,
|
||||
use_safetensors=self.hf_safetensor,
|
||||
token=self.hf_token,
|
||||
**model_opts,
|
||||
).to(self.device)
|
||||
model.save_pretrained(self.vae_decoder_model_dir, **model_opts)
|
||||
else:
|
||||
print(f"[I] Load AutoencoderKLWan (decoder) model from: {self.vae_decoder_model_dir}")
|
||||
model = AutoencoderKLWan.from_pretrained(self.vae_decoder_model_dir, **model_opts).to(self.device)
|
||||
model.forward = model.decode
|
||||
model = optimizer.optimize_checkpoint(model, torch_inference)
|
||||
return model
|
||||
|
||||
def get_input_names(self):
|
||||
return ["latent"]
|
||||
|
||||
def get_output_names(self):
|
||||
return ["images"]
|
||||
|
||||
def get_dynamic_axes(self):
|
||||
return {"latent": {0: "B", 3: "H", 4: "W"}, "images": {0: "B", 3: "8H", 4: "8W"}}
|
||||
|
||||
def get_input_profile(self, batch_size, image_height, image_width, static_batch, static_shape):
|
||||
latent_height, latent_width = self.check_dims(batch_size, image_height, image_width)
|
||||
min_batch, max_batch, _, _, _, _, min_latent_height, max_latent_height, min_latent_width, max_latent_width = (
|
||||
self.get_minmax_dims(batch_size, image_height, image_width, static_batch, static_shape)
|
||||
)
|
||||
return {
|
||||
"latent": [
|
||||
(min_batch, self.config["z_dim"], 1, min_latent_height, min_latent_width),
|
||||
(batch_size, self.config["z_dim"], 1, latent_height, latent_width),
|
||||
(max_batch, self.config["z_dim"], 1, max_latent_height, max_latent_width),
|
||||
]
|
||||
}
|
||||
|
||||
def get_shape_dict(self, batch_size, image_height, image_width):
|
||||
latent_height, latent_width = self.check_dims(batch_size, image_height, image_width)
|
||||
return {
|
||||
"latent": (batch_size, self.config["z_dim"], 1, latent_height, latent_width),
|
||||
"images": (batch_size, 3, 1, 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.bfloat16 if self.bf16 else torch.float32
|
||||
return torch.randn(
|
||||
batch_size, self.config["z_dim"], 1, latent_height, latent_width, dtype=dtype, device=self.device
|
||||
)
|
||||
|
||||
class AutoencoderKLWanEncoderModelWrapper(torch.nn.Module):
|
||||
def __init__(self, model):
|
||||
super().__init__()
|
||||
self.model = model
|
||||
|
||||
def forward(self, x):
|
||||
return self.model.encode(x).latent_dist.sample()
|
||||
|
||||
|
||||
class AutoencoderKLWanEncoderModel(base_model.BaseModel):
|
||||
def __init__(
|
||||
self,
|
||||
version,
|
||||
pipeline,
|
||||
device,
|
||||
hf_token,
|
||||
verbose,
|
||||
framework_model_dir,
|
||||
fp16=False,
|
||||
tf32=False,
|
||||
bf16=False,
|
||||
max_batch_size=16,
|
||||
):
|
||||
super(AutoencoderKLWanEncoderModel, 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.vae_encoder_model_dir = load.get_checkpoint_dir(
|
||||
self.framework_model_dir, self.version, self.pipeline, self.subfolder
|
||||
)
|
||||
if not os.path.exists(self.vae_encoder_model_dir):
|
||||
self.config = AutoencoderKLWan.load_config(self.path, subfolder=self.subfolder, token=self.hf_token)
|
||||
else:
|
||||
print(f"[I] Load AutoencoderKLWan (encoder) config from: {self.vae_encoder_model_dir}")
|
||||
self.config = AutoencoderKLWan.load_config(self.vae_encoder_model_dir)
|
||||
|
||||
def get_model(self, torch_inference=""):
|
||||
model_opts = (
|
||||
{"torch_dtype": torch.float16} if self.fp16 else {"torch_dtype": torch.bfloat16} if self.bf16 else {}
|
||||
)
|
||||
if not load.is_model_cached(self.vae_encoder_model_dir, model_opts, self.hf_safetensor):
|
||||
model = AutoencoderKLWan.from_pretrained(
|
||||
self.path,
|
||||
subfolder=self.subfolder,
|
||||
use_safetensors=self.hf_safetensor,
|
||||
token=self.hf_token,
|
||||
**model_opts,
|
||||
).to(self.device)
|
||||
model.save_pretrained(self.vae_encoder_model_dir, **model_opts)
|
||||
else:
|
||||
print(f"[I] Load AutoencoderKLWan (encoder) model from: {self.vae_encoder_model_dir}")
|
||||
model = AutoencoderKLWan.from_pretrained(self.vae_encoder_model_dir, **model_opts).to(self.device)
|
||||
model = AutoencoderKLWanEncoderModelWrapper(model)
|
||||
model = optimizer.optimize_checkpoint(model, torch_inference)
|
||||
return model
|
||||
@@ -0,0 +1,21 @@
|
||||
#
|
||||
# SPDX-FileCopyrightText: Copyright (c) 1993-2025 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 demo_diffusion.path.dd_path import DDPath
|
||||
from demo_diffusion.path.resolve_path import resolve_path
|
||||
|
||||
__all__ = ["DDPath", "resolve_path"]
|
||||
@@ -0,0 +1,50 @@
|
||||
#
|
||||
# SPDX-FileCopyrightText: Copyright (c) 1993-2025 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.
|
||||
#
|
||||
|
||||
"""
|
||||
Define a data structure for storing various paths used in DemoDiffusion.
|
||||
"""
|
||||
|
||||
import dataclasses
|
||||
import os
|
||||
from typing import Dict
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class DDPath:
|
||||
"""Data class that stores various paths used in DemoDiffusion."""
|
||||
|
||||
model_name_to_optimized_onnx_path: Dict[str, str] = dataclasses.field(default_factory=dict)
|
||||
model_name_to_engine_path: Dict[str, str] = dataclasses.field(default_factory=dict)
|
||||
|
||||
# Artifact paths.
|
||||
model_name_to_unoptimized_onnx_path: Dict[str, str] = dataclasses.field(default_factory=dict)
|
||||
model_name_to_weights_map_path: Dict[str, str] = dataclasses.field(default_factory=dict)
|
||||
model_name_to_refit_weights_path: Dict[str, str] = dataclasses.field(default_factory=dict)
|
||||
model_name_to_quantized_model_state_dict_path: Dict[str, str] = dataclasses.field(default_factory=dict)
|
||||
|
||||
def create_directory(self) -> None:
|
||||
"""Create directories for all paths, if they do not exist."""
|
||||
all_paths = [value for name_to_path in dataclasses.astuple(self) for value in name_to_path.values()]
|
||||
|
||||
for path in all_paths:
|
||||
directory = os.path.dirname(path)
|
||||
|
||||
# If `path` does not have a directory component, `directory` will be an empty string.
|
||||
# Only proceed if `directory` is non-empty.
|
||||
if directory:
|
||||
os.makedirs(directory, exist_ok=True)
|
||||
@@ -0,0 +1,168 @@
|
||||
#
|
||||
# SPDX-FileCopyrightText: Copyright (c) 1993-2025 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 __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import os
|
||||
from typing import Dict, List
|
||||
|
||||
import tensorrt as trt
|
||||
|
||||
from demo_diffusion import pipeline
|
||||
from demo_diffusion.path import dd_path
|
||||
|
||||
ARTIFACT_CACHE_DIRECTORY = os.path.join(os.getcwd(), "artifacts_cache")
|
||||
|
||||
|
||||
def resolve_path(
|
||||
model_names: List[str],
|
||||
args: argparse.Namespace,
|
||||
pipeline_type: pipeline.PIPELINE_TYPE,
|
||||
pipeline_uid: str,
|
||||
) -> dd_path.DDPath:
|
||||
"""Resolve all paths and store them in a newly constructed dd_path.DDPath object.
|
||||
|
||||
Args:
|
||||
model_names (List[str]): List of model names.
|
||||
args (argparse.Namespace): Parsed arguments.
|
||||
|
||||
Returns:
|
||||
dd_path.DDPath: Path object containing all the resolved paths.
|
||||
"""
|
||||
path = dd_path.DDPath()
|
||||
model_name_to_model_uri = {
|
||||
model_name: _resolve_model_uri(model_name, args, pipeline_type, pipeline_uid) for model_name in model_names
|
||||
}
|
||||
|
||||
_resolve_default_path(model_name_to_model_uri, args, path)
|
||||
_resolve_custom_path(args, path)
|
||||
|
||||
path.create_directory()
|
||||
|
||||
return path
|
||||
|
||||
|
||||
def _resolve_model_uri(
|
||||
model_name: str, args: argparse.Namespace, pipeline_type: pipeline.PIPELINE_TYPE, pipeline_uid: str
|
||||
) -> str:
|
||||
"""Resolve and return the model URI.
|
||||
|
||||
The model URI is a partial path that uniquely identifies the model. It is used to construct various model paths like
|
||||
artifact cache path, checkpoint path, etc.
|
||||
"""
|
||||
# Lora unique ID represents the lora configuration.
|
||||
if args.lora_path and args.lora_weight:
|
||||
lora_config_uid = "-".join(
|
||||
sorted(
|
||||
[
|
||||
f"{hashlib.sha256(lora_path.encode()).hexdigest()}-{lora_weight}-{args.lora_scale}"
|
||||
for lora_path, lora_weight in zip(args.lora_path, args.lora_weight)
|
||||
if args.lora_path
|
||||
]
|
||||
)
|
||||
)
|
||||
else:
|
||||
lora_config_uid = ""
|
||||
|
||||
# Quantization config unique ID represents the quantization configuration.
|
||||
def _is_quantized() -> bool:
|
||||
"""Return True if model is quantized, False if otherwise.
|
||||
|
||||
When quantization flags are set in `args`, only a subset of the models are actually quantized.
|
||||
"""
|
||||
is_unet = model_name == "unet"
|
||||
is_unetxl_base = pipeline_type.is_sd_xl_base() and model_name == "unetxl"
|
||||
is_flux_transformer = args.version.startswith("flux.1") and model_name == "transformer"
|
||||
|
||||
if args.int8:
|
||||
return is_unet or is_unetxl_base
|
||||
elif args.fp8:
|
||||
return is_unet or is_unetxl_base or is_flux_transformer
|
||||
elif args.fp4:
|
||||
return is_flux_transformer
|
||||
else:
|
||||
return False
|
||||
|
||||
if _is_quantized():
|
||||
if args.int8 or args.fp8:
|
||||
quantization_config_uid = (
|
||||
f"{'int8' if args.int8 else 'fp8'}.l{args.quantization_level}.bs2"
|
||||
f".c{args.calibration_size}.p{args.quantization_percentile}.a{args.quantization_alpha}"
|
||||
)
|
||||
else:
|
||||
quantization_config_uid = "fp4"
|
||||
else:
|
||||
quantization_config_uid = ""
|
||||
|
||||
# Model unique ID represents the model name and its configuration. It is unique under the same pipeline.
|
||||
model_uid = "_".join([s for s in [model_name, lora_config_uid, quantization_config_uid] if s])
|
||||
|
||||
# Model URI is the concatenation of pipeline unique ID and model unique ID.
|
||||
model_uri = os.path.join(pipeline_uid, model_uid)
|
||||
|
||||
return model_uri
|
||||
|
||||
|
||||
def _resolve_default_path(
|
||||
model_name_to_model_uri: Dict[str, str], args: argparse.Namespace, path: dd_path.DDPath
|
||||
) -> None:
|
||||
"""Resolve the default paths.
|
||||
|
||||
Args:
|
||||
model_name_to_model_uri (Dict[str, str]): Dictionary of model name to model URI.
|
||||
args (argparse.Namespace): Parsed arguments.
|
||||
path (dd_path.DDPath): Path object. This object is modified in-place to store all resolved default paths.
|
||||
"""
|
||||
for model_name, model_uri in model_name_to_model_uri.items():
|
||||
path.model_name_to_optimized_onnx_path[model_name] = os.path.join(
|
||||
args.onnx_dir, model_uri, "model_optimized.onnx"
|
||||
)
|
||||
path.model_name_to_engine_path[model_name] = os.path.join(
|
||||
args.engine_dir, model_uri, f"engine_trt{trt.__version__}.plan"
|
||||
)
|
||||
|
||||
# Resolve artifact paths.
|
||||
artifact_dir = os.path.join(ARTIFACT_CACHE_DIRECTORY, model_uri)
|
||||
|
||||
path.model_name_to_unoptimized_onnx_path[model_name] = os.path.join(artifact_dir, "model_unoptimized.onnx")
|
||||
path.model_name_to_weights_map_path[model_name] = os.path.join(artifact_dir, "weights_map.json")
|
||||
path.model_name_to_refit_weights_path[model_name] = os.path.join(artifact_dir, "refit_weights.json")
|
||||
path.model_name_to_quantized_model_state_dict_path[model_name] = os.path.join(
|
||||
artifact_dir, "quantized_model_state_dict.json"
|
||||
)
|
||||
|
||||
|
||||
def _resolve_custom_path(args: argparse.Namespace, path: dd_path.DDPath) -> None:
|
||||
"""Resolve the custom paths.
|
||||
|
||||
If a different path already exists in `path`, it will be overridden.
|
||||
|
||||
Args:
|
||||
args (argparse.Namespace): Parsed arguments.
|
||||
path (dd_path.DDPath): Path object. This object is modified in-place to store or override all resolved paths.
|
||||
"""
|
||||
# Resolve and override custom ONNX paths.
|
||||
if args.custom_onnx_paths:
|
||||
for model_name, optimized_onnx_path in args.custom_onnx_paths.items():
|
||||
path.model_name_to_optimized_onnx_path[model_name] = optimized_onnx_path
|
||||
|
||||
# Resolve and override custom engine paths.
|
||||
if args.custom_engine_paths:
|
||||
for model_name, engine_path in args.custom_engine_paths.items():
|
||||
path.model_name_to_engine_path[model_name] = engine_path
|
||||
@@ -0,0 +1,104 @@
|
||||
#
|
||||
# SPDX-FileCopyrightText: Copyright (c) 1993-2025 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 importlib
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
# Expose public API while avoiding importing optional dependencies at module import time.
|
||||
# Each attribute is imported on first access via __getattr__.
|
||||
|
||||
__all__ = [
|
||||
"DiffusionPipeline",
|
||||
"FluxPipeline",
|
||||
"FluxKontextPipeline",
|
||||
"StableCascadePipeline",
|
||||
"StableDiffusion3Pipeline",
|
||||
"StableDiffusion35Pipeline",
|
||||
"StableDiffusionPipeline",
|
||||
"CosmosPipeline",
|
||||
"StableVideoDiffusionPipeline",
|
||||
"WanPipeline",
|
||||
"PIPELINE_TYPE",
|
||||
]
|
||||
|
||||
_LAZY_ATTRS = {
|
||||
# Core/base
|
||||
"DiffusionPipeline": ("demo_diffusion.pipeline.diffusion_pipeline", "DiffusionPipeline"),
|
||||
"PIPELINE_TYPE": ("demo_diffusion.pipeline.type", "PIPELINE_TYPE"),
|
||||
# Stable Diffusion family
|
||||
"StableDiffusionPipeline": ("demo_diffusion.pipeline.stable_diffusion_pipeline", "StableDiffusionPipeline"),
|
||||
"StableDiffusion3Pipeline": ("demo_diffusion.pipeline.stable_diffusion_3_pipeline", "StableDiffusion3Pipeline"),
|
||||
"StableDiffusion35Pipeline": ("demo_diffusion.pipeline.stable_diffusion_35_pipeline", "StableDiffusion35Pipeline"),
|
||||
# Stable Cascade
|
||||
"StableCascadePipeline": ("demo_diffusion.pipeline.stable_cascade_pipeline", "StableCascadePipeline"),
|
||||
# Stable Video Diffusion
|
||||
"StableVideoDiffusionPipeline": ("demo_diffusion.pipeline.stable_video_diffusion_pipeline", "StableVideoDiffusionPipeline"),
|
||||
# Flux family (optional dependency: `flux`)
|
||||
"FluxPipeline": ("demo_diffusion.pipeline.flux_pipeline", "FluxPipeline"),
|
||||
"FluxKontextPipeline": ("demo_diffusion.pipeline.flux_pipeline", "FluxKontextPipeline"),
|
||||
# Cosmos (optional dependency: `flux`)
|
||||
"CosmosPipeline": ("demo_diffusion.pipeline.cosmos_pipeline", "CosmosPipeline"),
|
||||
# Wan
|
||||
"WanPipeline": ("demo_diffusion.pipeline.wan_pipeline", "WanPipeline"),
|
||||
}
|
||||
|
||||
def __getattr__(name):
|
||||
if name not in _LAZY_ATTRS:
|
||||
raise AttributeError(f"module 'demo_diffusion.pipeline' has no attribute {name!r}")
|
||||
|
||||
module_path, attr_name = _LAZY_ATTRS[name]
|
||||
try:
|
||||
module = importlib.import_module(module_path)
|
||||
except ModuleNotFoundError as e:
|
||||
missing_pkg = e.name or "<unknown>"
|
||||
raise ModuleNotFoundError(
|
||||
f"Optional dependency '{missing_pkg}' is required for '{name}'. "
|
||||
"Install the appropriate extras/requirements for the selected pipeline "
|
||||
"(e.g., use the non-legacy requirements for Flux/Cosmos), or install the missing package."
|
||||
) from e
|
||||
try:
|
||||
return getattr(module, attr_name)
|
||||
except AttributeError as e:
|
||||
raise AttributeError(
|
||||
f"'{module_path}' does not export attribute '{attr_name}' (while resolving '{name}')."
|
||||
) from e
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from demo_diffusion.pipeline.diffusion_pipeline import DiffusionPipeline as DiffusionPipeline
|
||||
from demo_diffusion.pipeline.type import PIPELINE_TYPE as PIPELINE_TYPE
|
||||
from demo_diffusion.pipeline.stable_diffusion_pipeline import (
|
||||
StableDiffusionPipeline as StableDiffusionPipeline,
|
||||
)
|
||||
from demo_diffusion.pipeline.stable_diffusion_3_pipeline import (
|
||||
StableDiffusion3Pipeline as StableDiffusion3Pipeline,
|
||||
)
|
||||
from demo_diffusion.pipeline.stable_diffusion_35_pipeline import (
|
||||
StableDiffusion35Pipeline as StableDiffusion35Pipeline,
|
||||
)
|
||||
from demo_diffusion.pipeline.stable_cascade_pipeline import (
|
||||
StableCascadePipeline as StableCascadePipeline,
|
||||
)
|
||||
from demo_diffusion.pipeline.stable_video_diffusion_pipeline import (
|
||||
StableVideoDiffusionPipeline as StableVideoDiffusionPipeline,
|
||||
)
|
||||
# Optional pipelines
|
||||
from demo_diffusion.pipeline.flux_pipeline import (
|
||||
FluxPipeline as FluxPipeline,
|
||||
FluxKontextPipeline as FluxKontextPipeline,
|
||||
)
|
||||
from demo_diffusion.pipeline.cosmos_pipeline import CosmosPipeline as CosmosPipeline
|
||||
from demo_diffusion.pipeline.wan_pipeline import WanPipeline as WanPipeline
|
||||
@@ -0,0 +1,37 @@
|
||||
#
|
||||
# SPDX-FileCopyrightText: Copyright (c) 1993-2025 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 os
|
||||
|
||||
from diffusers.utils import load_image
|
||||
|
||||
|
||||
def load_calib_prompts(batch_size, calib_data_path):
|
||||
with open(calib_data_path, "r", encoding="utf-8") 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
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,997 @@
|
||||
#
|
||||
# SPDX-FileCopyrightText: Copyright (c) 1993-2025 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 __future__ import annotations
|
||||
|
||||
import abc
|
||||
import argparse
|
||||
import gc
|
||||
import json
|
||||
import os
|
||||
import pathlib
|
||||
import sys
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Any, List
|
||||
|
||||
import modelopt.torch.opt as mto
|
||||
import modelopt.torch.quantization as mtq
|
||||
import nvtx
|
||||
import torch
|
||||
from cuda.bindings import runtime as cudart
|
||||
from diffusers import (
|
||||
DDIMScheduler,
|
||||
DDPMScheduler,
|
||||
DDPMWuerstchenScheduler,
|
||||
EulerAncestralDiscreteScheduler,
|
||||
EulerDiscreteScheduler,
|
||||
FlowMatchEulerDiscreteScheduler,
|
||||
LCMScheduler,
|
||||
LMSDiscreteScheduler,
|
||||
PNDMScheduler,
|
||||
UniPCMultistepScheduler,
|
||||
)
|
||||
from torch.utils.data import DataLoader
|
||||
|
||||
import demo_diffusion.engine as engine_module
|
||||
import demo_diffusion.image as image_module
|
||||
from demo_diffusion.model import (
|
||||
make_scheduler,
|
||||
merge_loras,
|
||||
unload_torch_model,
|
||||
)
|
||||
from demo_diffusion.pipeline.calibrate import load_calib_prompts
|
||||
from demo_diffusion.pipeline.model_memory_manager import ModelMemoryManager
|
||||
from demo_diffusion.pipeline.type import PIPELINE_TYPE
|
||||
from demo_diffusion.utils_modelopt import (
|
||||
SD_FP8_BF16_FLUX_MMDIT_BMM2_FP8_OUTPUT_CONFIG,
|
||||
SD_FP8_FP16_DEFAULT_CONFIG,
|
||||
SD_FP8_FP32_DEFAULT_CONFIG,
|
||||
PromptImageDataset,
|
||||
SameSizeSampler,
|
||||
check_lora,
|
||||
custom_collate,
|
||||
filter_func,
|
||||
filter_func_no_proj_out,
|
||||
fp8_mha_disable,
|
||||
generate_fp8_scales,
|
||||
get_int8_config,
|
||||
infinite_dataloader,
|
||||
quantize_lvl,
|
||||
set_fmha,
|
||||
set_quant_precision,
|
||||
)
|
||||
|
||||
|
||||
class DiffusionPipeline(ABC):
|
||||
"""
|
||||
Application showcasing the acceleration of Stable Diffusion pipelines using NVidia TensorRT.
|
||||
"""
|
||||
VALID_DIFFUSION_PIPELINES = (
|
||||
"1.4",
|
||||
"dreamshaper-7",
|
||||
"xl-1.0",
|
||||
"xl-turbo",
|
||||
"svd-xt-1.1",
|
||||
"sd3",
|
||||
"3.5-medium",
|
||||
"3.5-large",
|
||||
"cascade",
|
||||
"flux.1-dev",
|
||||
"flux.1-dev-canny",
|
||||
"flux.1-dev-depth",
|
||||
"flux.1-schnell",
|
||||
"flux.1-kontext-dev",
|
||||
"wan2.2-t2v-a14b",
|
||||
"cosmos-predict2-2b-text2image",
|
||||
"cosmos-predict2-14b-text2image",
|
||||
"cosmos-predict2-2b-video2world",
|
||||
"cosmos-predict2-14b-video2world",
|
||||
)
|
||||
SCHEDULER_DEFAULTS = {
|
||||
"1.4": "PNDM",
|
||||
"dreamshaper-7": "PNDM",
|
||||
"xl-1.0": "Euler",
|
||||
"xl-turbo": "EulerA",
|
||||
"3.5-large": "FlowMatchEuler",
|
||||
"3.5-medium": "FlowMatchEuler",
|
||||
"svd-xt-1.1": "Euler",
|
||||
"cascade": "DDPMWuerstchen",
|
||||
"flux.1-dev": "FlowMatchEuler",
|
||||
"flux.1-dev-canny": "FlowMatchEuler",
|
||||
"flux.1-dev-depth": "FlowMatchEuler",
|
||||
"flux.1-schnell": "FlowMatchEuler",
|
||||
"flux.1-kontext-dev": "FlowMatchEuler",
|
||||
"wan2.2-t2v-a14b": "UniPC",
|
||||
"cosmos-predict2-2b-text2image": "FlowMatchEuler",
|
||||
"cosmos-predict2-14b-text2image": "FlowMatchEuler",
|
||||
"cosmos-predict2-2b-video2world": "FlowMatchEuler",
|
||||
"cosmos-predict2-14b-video2world": "FlowMatchEuler",
|
||||
}
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
dd_path,
|
||||
version="1.4",
|
||||
pipeline_type=PIPELINE_TYPE.TXT2IMG,
|
||||
bf16=False,
|
||||
max_batch_size=16,
|
||||
denoising_steps=30,
|
||||
scheduler=None,
|
||||
device="cuda",
|
||||
output_dir=".",
|
||||
hf_token=None,
|
||||
verbose=False,
|
||||
nvtx_profile=False,
|
||||
use_cuda_graph=False,
|
||||
framework_model_dir="pytorch_model",
|
||||
return_latents=False,
|
||||
low_vram=False,
|
||||
torch_inference="",
|
||||
torch_fallback=None,
|
||||
weight_streaming=False,
|
||||
text_encoder_weight_streaming_budget_percentage=None,
|
||||
denoiser_weight_streaming_budget_percentage=None,
|
||||
controlnet=None,
|
||||
):
|
||||
"""
|
||||
Initializes the Diffusion pipeline.
|
||||
|
||||
Args:
|
||||
dd_path (load_module.DDPath): DDPath object that contains all paths used in DemoDiffusion.
|
||||
version (str):
|
||||
The version of the pipeline. Should be one of the values listed in DiffusionPipeline.VALID_DIFFUSION_PIPELINES.
|
||||
pipeline_type (PIPELINE_TYPE):
|
||||
Task performed by the current pipeline. Should be one of PIPELINE_TYPE.__members__.
|
||||
max_batch_size (int):
|
||||
Maximum batch size for dynamic batch engine.
|
||||
bf16 (`bool`, defaults to False):
|
||||
Whether to run the pipeline in BFloat16 precision.
|
||||
denoising_steps (int):
|
||||
The number of denoising steps.
|
||||
More denoising steps usually lead to a higher quality image at the expense of slower inference.
|
||||
scheduler (str):
|
||||
The scheduler to guide the denoising process. Must be one of the values listed in DiffusionPipeline.SCHEDULER_DEFAULTS.values().
|
||||
device (str):
|
||||
PyTorch device to run inference. Default: 'cuda'.
|
||||
output_dir (str):
|
||||
Output directory for log files and image artifacts.
|
||||
hf_token (str):
|
||||
HuggingFace User Access Token to use for downloading Stable Diffusion model checkpoints.
|
||||
verbose (bool):
|
||||
Enable verbose logging.
|
||||
nvtx_profile (bool):
|
||||
Insert NVTX profiling markers.
|
||||
use_cuda_graph (bool):
|
||||
Use CUDA graph to capture engine execution and then launch inference.
|
||||
framework_model_dir (str):
|
||||
cache directory for framework checkpoints.
|
||||
return_latents (bool):
|
||||
Skip decoding the image and return latents instead.
|
||||
low_vram (bool):
|
||||
[FLUX only] Optimize for low VRAM usage, possibly at the expense of inference performance. Disabled by default.
|
||||
torch_inference (str):
|
||||
Run inference with PyTorch (using specified compilation mode) instead of TensorRT. The compilation mode specified should be one of ['eager', 'reduce-overhead', 'max-autotune'].
|
||||
torch_fallback (str):
|
||||
[FLUX only] Comma separated list of models to be inferenced using PyTorch instead of TRT. For example --torch-fallback t5,transformer. If --torch-inference set, this parameter will be ignored.
|
||||
weight_streaming (`bool`, defaults to False):
|
||||
Whether to enable weight streaming during TensorRT engine build.
|
||||
text_encoder_ws_budget_percentage (`int`, defaults to None):
|
||||
Weight streaming budget as a percentage of the size of total streamable weights for the text encoder model.
|
||||
denoiser_weight_streaming_budget_percentage (`int`, defaults to None):
|
||||
Weight streaming budget as a percentage of the size of total streamable weights for the denoiser model.
|
||||
controlnet (str, defaults to None):
|
||||
Type of ControlNet to use for the pipeline.
|
||||
"""
|
||||
self.bf16 = bf16
|
||||
self.dd_path = dd_path
|
||||
|
||||
self.denoising_steps = denoising_steps
|
||||
self.max_batch_size = max_batch_size
|
||||
|
||||
self.framework_model_dir = framework_model_dir
|
||||
self.output_dir = output_dir
|
||||
for directory in [self.framework_model_dir, self.output_dir]:
|
||||
if not os.path.exists(directory):
|
||||
print(f"[I] Create directory: {directory}")
|
||||
pathlib.Path(directory).mkdir(parents=True)
|
||||
|
||||
self.hf_token = hf_token
|
||||
self.device = device
|
||||
self.verbose = verbose
|
||||
self.nvtx_profile = nvtx_profile
|
||||
|
||||
self.version = version
|
||||
self.pipeline_type = pipeline_type
|
||||
self.return_latents = return_latents
|
||||
|
||||
self.low_vram = low_vram
|
||||
self.weight_streaming = weight_streaming
|
||||
self.text_encoder_weight_streaming_budget_percentage = text_encoder_weight_streaming_budget_percentage
|
||||
self.denoiser_weight_streaming_budget_percentage = denoiser_weight_streaming_budget_percentage
|
||||
|
||||
self.stages = self.get_model_names(self.pipeline_type, controlnet)
|
||||
# config to store additional info
|
||||
self.config = {}
|
||||
if torch_fallback:
|
||||
assert type(torch_fallback) is list
|
||||
for model_name in torch_fallback:
|
||||
if model_name not in self.stages:
|
||||
raise ValueError(f'Model "{model_name}" set in --torch-fallback does not exist')
|
||||
self.config[model_name.replace("-", "_") + "_torch_fallback"] = True
|
||||
print(f"[I] Setting torch_fallback for {model_name} model.")
|
||||
|
||||
if not scheduler:
|
||||
scheduler = 'UniPC' if self.pipeline_type.is_controlnet() and not self.version == "3.5-large" else self.SCHEDULER_DEFAULTS.get(version, 'DDIM')
|
||||
print(f"[I] Autoselected scheduler: {scheduler}")
|
||||
|
||||
scheduler_class_map = {
|
||||
"DDIM" : DDIMScheduler,
|
||||
"DDPM" : DDPMScheduler,
|
||||
"EulerA" : EulerAncestralDiscreteScheduler,
|
||||
"Euler" : EulerDiscreteScheduler,
|
||||
"LCM" : LCMScheduler,
|
||||
"LMSD" : LMSDiscreteScheduler,
|
||||
"PNDM" : PNDMScheduler,
|
||||
"UniPC" : UniPCMultistepScheduler,
|
||||
"DDPMWuerstchen" : DDPMWuerstchenScheduler,
|
||||
"FlowMatchEuler": FlowMatchEulerDiscreteScheduler,
|
||||
}
|
||||
try:
|
||||
scheduler_class = scheduler_class_map[scheduler]
|
||||
except KeyError:
|
||||
raise ValueError(
|
||||
f"Unsupported scheduler {scheduler}. Should be one of {list(scheduler_class_map.keys())}."
|
||||
)
|
||||
self.scheduler = make_scheduler(scheduler_class, version, pipeline_type, hf_token, framework_model_dir)
|
||||
|
||||
self.torch_inference = torch_inference
|
||||
if self.torch_inference:
|
||||
torch._inductor.config.conv_1x1_as_mm = True
|
||||
torch._inductor.config.coordinate_descent_tuning = True
|
||||
torch._inductor.config.epilogue_fusion = False
|
||||
torch._inductor.config.coordinate_descent_check_all_directions = True
|
||||
self.use_cuda_graph = use_cuda_graph
|
||||
|
||||
# initialized in load_engines()
|
||||
self.models = {}
|
||||
self.torch_models = {}
|
||||
self.engine = {}
|
||||
self.shape_dicts = {}
|
||||
self.shared_device_memory = None
|
||||
self.lora_loader = None
|
||||
|
||||
# initialized in load_resources()
|
||||
self.events = {}
|
||||
self.generator = None
|
||||
self.markers = {}
|
||||
self.seed = None
|
||||
self.stream = None
|
||||
self.tokenizer = None
|
||||
|
||||
def model_memory_manager(self, model_names, low_vram=False):
|
||||
return ModelMemoryManager(self, model_names, low_vram)
|
||||
|
||||
@classmethod
|
||||
@abc.abstractmethod
|
||||
def FromArgs(cls, args: argparse.Namespace, pipeline_type: PIPELINE_TYPE) -> DiffusionPipeline:
|
||||
"""Factory method to construct a concrete pipeline object from parsed arguments."""
|
||||
raise NotImplementedError("FromArgs cannot be called from the abstract base class.")
|
||||
|
||||
@classmethod
|
||||
@abc.abstractmethod
|
||||
def get_model_names(cls, pipeline_type: PIPELINE_TYPE, controlnet_type: str = None) -> List[str]:
|
||||
"""Return a list of model names used by this pipeline."""
|
||||
raise NotImplementedError("get_model_names cannot be called from the abstract base class.")
|
||||
|
||||
@classmethod
|
||||
def _get_pipeline_uid(cls, version: str) -> str:
|
||||
"""Return the unique ID of this pipeline.
|
||||
|
||||
This is typically used to determine the default path for things like engine files, artifacts caches, etc.
|
||||
"""
|
||||
return f"{cls.__name__}_{version}"
|
||||
|
||||
def profile_start(self, name, color="blue", domain=None):
|
||||
if self.nvtx_profile:
|
||||
self.markers[name] = nvtx.start_range(message=name, color=color, domain=domain)
|
||||
if name in self.events:
|
||||
cudart.cudaEventRecord(self.events[name][0], 0)
|
||||
|
||||
def profile_stop(self, name):
|
||||
if name in self.events:
|
||||
cudart.cudaEventRecord(self.events[name][1], 0)
|
||||
if self.nvtx_profile:
|
||||
nvtx.end_range(self.markers[name])
|
||||
|
||||
def load_resources(self, image_height, image_width, batch_size, seed):
|
||||
# Initialize noise generator
|
||||
if seed is not None:
|
||||
self.seed = seed
|
||||
self.generator = torch.Generator(device="cuda").manual_seed(seed)
|
||||
|
||||
# Create CUDA events and stream
|
||||
for stage in self.stages:
|
||||
self.events[stage] = [cudart.cudaEventCreate()[1], cudart.cudaEventCreate()[1]]
|
||||
self.stream = cudart.cudaStreamCreate()[1]
|
||||
|
||||
# Allocate TensorRT I/O buffers
|
||||
if not self.torch_inference:
|
||||
for model_name, obj in self.models.items():
|
||||
if self.torch_fallback[model_name]:
|
||||
continue
|
||||
shape_dict = obj.get_shape_dict(
|
||||
batch_size, image_height, image_width,
|
||||
**({'num_frames': obj.num_frames} if hasattr(obj, 'num_frames') and obj.num_frames else {})
|
||||
)
|
||||
|
||||
self.shape_dicts[model_name] = shape_dict
|
||||
if not self.low_vram:
|
||||
self.engine[model_name].allocate_buffers(shape_dict=shape_dict, device=self.device)
|
||||
|
||||
@abstractmethod
|
||||
def _initialize_models(self, *args, **kwargs):
|
||||
raise NotImplementedError("Please Implement the _initialize_models method")
|
||||
|
||||
def _prepare_model_configs(
|
||||
self,
|
||||
enable_refit,
|
||||
int8,
|
||||
fp8,
|
||||
fp4
|
||||
):
|
||||
model_names = self.models.keys()
|
||||
self.torch_fallback = dict(zip(model_names, [self.torch_inference or self.config.get(model_name.replace('-','_')+'_torch_fallback', False) for model_name in model_names]))
|
||||
|
||||
configs = {}
|
||||
for model_name in model_names:
|
||||
# Initialize config
|
||||
do_engine_refit = enable_refit and not self.pipeline_type.is_sd_xl_refiner() and any(model_name.startswith(prefix) for prefix in ("unet", "transformer"))
|
||||
do_lora_merge = not enable_refit and self.lora_loader and any(model_name.startswith(prefix) for prefix in ("unet", "transformer"))
|
||||
|
||||
config = {
|
||||
"do_engine_refit": do_engine_refit,
|
||||
"do_lora_merge": do_lora_merge,
|
||||
"use_int8": False,
|
||||
"use_fp8": False,
|
||||
'use_fp4': False,
|
||||
}
|
||||
|
||||
# TODO: Move this to when arguments are first being validated in dd_argparse.py
|
||||
# 8-bit/4-bit precision inference
|
||||
if int8:
|
||||
assert self.pipeline_type.is_sd_xl_base() or self.version in [
|
||||
"1.4",
|
||||
], "int8 quantization only supported for SDXL and SD1.4 pipeline"
|
||||
if (self.pipeline_type.is_sd_xl() and model_name == "unetxl") or (model_name == "unet"):
|
||||
config["use_int8"] = True
|
||||
|
||||
elif fp8:
|
||||
assert (
|
||||
self.pipeline_type.is_sd_xl()
|
||||
or self.version in ["1.4"]
|
||||
or self.version.startswith("flux.1")
|
||||
or self.version.startswith("3.5-large")
|
||||
), "fp8 quantization only supported for SDXL, SD1.4, SD3.5-large and FLUX pipelines"
|
||||
if (
|
||||
(self.pipeline_type.is_sd_xl() and model_name == "unetxl")
|
||||
or (self.version.startswith("flux.1") and model_name == "transformer")
|
||||
or (
|
||||
self.version.startswith("3.5-large")
|
||||
and ("transformer" in model_name or "controlnet" in model_name)
|
||||
)
|
||||
or (model_name == "unet")
|
||||
):
|
||||
config["use_fp8"] = True
|
||||
elif fp4:
|
||||
config['use_fp4'] = True
|
||||
|
||||
# Setup paths
|
||||
config["onnx_path"] = self.dd_path.model_name_to_unoptimized_onnx_path[model_name]
|
||||
config["onnx_opt_path"] = self.dd_path.model_name_to_optimized_onnx_path[model_name]
|
||||
config["engine_path"] = self.dd_path.model_name_to_engine_path[model_name]
|
||||
config["weights_map_path"] = (
|
||||
self.dd_path.model_name_to_weights_map_path[model_name] if config["do_engine_refit"] else None
|
||||
)
|
||||
config["state_dict_path"] = self.dd_path.model_name_to_quantized_model_state_dict_path[model_name]
|
||||
config["refit_weights_path"] = self.dd_path.model_name_to_refit_weights_path[model_name]
|
||||
|
||||
configs[model_name] = config
|
||||
|
||||
return configs
|
||||
|
||||
def _calibrate_and_save_model(
|
||||
self,
|
||||
pipeline,
|
||||
model,
|
||||
model_config,
|
||||
quantization_level,
|
||||
quantization_percentile,
|
||||
quantization_alpha,
|
||||
calibration_size,
|
||||
calib_batch_size,
|
||||
enable_lora_merge = False,
|
||||
**kwargs):
|
||||
print(f"[I] Calibrated weights not found, generating {model_config['state_dict_path']}")
|
||||
|
||||
# TODO check size > calibration_size
|
||||
def do_calibrate(pipeline, calibration_prompts, **kwargs):
|
||||
for i_th, prompts in enumerate(calibration_prompts):
|
||||
if i_th >= kwargs["calib_size"]:
|
||||
return
|
||||
if kwargs["model_id"] in ("flux.1-dev", "flux.1-schnell"):
|
||||
common_args = {
|
||||
"prompt": prompts,
|
||||
"prompt_2": prompts,
|
||||
"num_inference_steps": kwargs["n_steps"],
|
||||
"height": kwargs.get("height", 1024),
|
||||
"width": kwargs.get("width", 1024),
|
||||
"guidance_scale": 3.5,
|
||||
"max_sequence_length": 512 if kwargs["model_id"] == "flux.1-dev" else 256,
|
||||
}
|
||||
else:
|
||||
common_args = {
|
||||
"prompt": prompts,
|
||||
"num_inference_steps": kwargs["n_steps"],
|
||||
"negative_prompt": ["normal quality, low quality, worst quality, low res, blurry, nsfw, nude"]
|
||||
* len(prompts),
|
||||
}
|
||||
|
||||
pipeline(**common_args).images
|
||||
|
||||
def do_calibrate_img2img(pipeline, dataloader, **kwargs):
|
||||
for i_th, (img_conds, prompts) in enumerate(dataloader):
|
||||
if i_th >= kwargs["calib_size"]:
|
||||
return
|
||||
|
||||
common_args = {
|
||||
"prompt": list(prompts),
|
||||
"control_image": img_conds,
|
||||
"num_inference_steps": kwargs["n_steps"],
|
||||
"height": img_conds.size(2),
|
||||
"width": img_conds.size(3),
|
||||
"generator": torch.Generator().manual_seed(42),
|
||||
"guidance_scale": 3.5,
|
||||
"max_sequence_length": 512,
|
||||
}
|
||||
pipeline(**common_args).images
|
||||
|
||||
if self.version in ("flux.1-dev-depth", "flux.1-dev-canny"):
|
||||
dataset = PromptImageDataset(
|
||||
root_dir=self.calibration_dataset,
|
||||
)
|
||||
|
||||
dataloader = DataLoader(
|
||||
dataset,
|
||||
batch_size=calib_batch_size,
|
||||
shuffle=False,
|
||||
num_workers=0,
|
||||
sampler=SameSizeSampler(dataset=dataset, batch_size=calib_batch_size),
|
||||
collate_fn=custom_collate,
|
||||
)
|
||||
else:
|
||||
root_dir = os.path.dirname(os.path.abspath(sys.modules["__main__"].__file__))
|
||||
calibration_file = os.path.join(root_dir, "calibration_data", "calibration-prompts.txt")
|
||||
calibration_prompts = load_calib_prompts(calib_batch_size, calibration_file)
|
||||
|
||||
def forward_loop(model):
|
||||
if self.version not in ("sd3", "flux.1-dev", "flux.1-schnell", "flux.1-dev-depth", "flux.1-dev-canny"):
|
||||
pipeline.unet = model
|
||||
else:
|
||||
pipeline.transformer = model
|
||||
|
||||
if self.version in ("flux.1-dev-depth", "flux.1-dev-canny"):
|
||||
do_calibrate_img2img(
|
||||
pipeline=pipeline,
|
||||
dataloader=infinite_dataloader(dataloader),
|
||||
calib_size=calibration_size // calib_batch_size,
|
||||
n_steps=self.denoising_steps,
|
||||
model_id=self.version,
|
||||
)
|
||||
else:
|
||||
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.")
|
||||
if model_config['use_int8']:
|
||||
quant_config = get_int8_config(
|
||||
model,
|
||||
quantization_level,
|
||||
quantization_alpha,
|
||||
quantization_percentile,
|
||||
self.denoising_steps
|
||||
)
|
||||
elif model_config['use_fp8']:
|
||||
if self.version.startswith("flux.1"):
|
||||
quant_config = SD_FP8_BF16_FLUX_MMDIT_BMM2_FP8_OUTPUT_CONFIG
|
||||
else:
|
||||
quant_config = SD_FP8_FP16_DEFAULT_CONFIG
|
||||
|
||||
# Handle LoRA
|
||||
if enable_lora_merge:
|
||||
assert self.lora_loader is not None
|
||||
model = merge_loras(model, self.lora_loader)
|
||||
|
||||
check_lora(model)
|
||||
|
||||
if self.version.startswith("flux.1"):
|
||||
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,
|
||||
enable_lora_merge = False,
|
||||
**kwargs):
|
||||
pipeline = obj.get_pipeline()
|
||||
is_flux = self.version.startswith("flux.1")
|
||||
model = pipeline.unet if self.version not in ("sd3", "flux.1-dev", "flux.1-schnell", "flux.1-dev-depth", "flux.1-dev-canny") else pipeline.transformer
|
||||
if model_config['use_fp8'] and quantization_level == 4.0:
|
||||
set_fmha(model, is_flux=is_flux)
|
||||
|
||||
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,
|
||||
enable_lora_merge,
|
||||
**kwargs)
|
||||
else:
|
||||
mto.restore(model, model_config['state_dict_path'])
|
||||
|
||||
if not os.path.exists(model_config['onnx_path']):
|
||||
quantize_lvl(self.version, model, quantization_level)
|
||||
if self.version.startswith("flux.1"):
|
||||
mtq.disable_quantizer(model, filter_func_no_proj_out)
|
||||
else:
|
||||
mtq.disable_quantizer(model, filter_func)
|
||||
if model_config['use_fp8'] and not self.version.startswith("flux.1"):
|
||||
generate_fp8_scales(model)
|
||||
if quantization_level == 4.0:
|
||||
fp8_mha_disable(model, quantized_mha_output=False) # Remove Q/DQ after BMM2 in MHA
|
||||
else:
|
||||
model = None
|
||||
|
||||
return model
|
||||
|
||||
@abstractmethod
|
||||
def download_onnx_models(self, model_name: str, model_config: dict[str, Any]) -> None:
|
||||
"""Download pre-exported ONNX Models"""
|
||||
raise NotImplementedError("Please Implement the download_onnx_models method")
|
||||
|
||||
def is_native_export_supported(self, model_config: dict[str, Any]) -> bool:
|
||||
"""Check if pipeline supports native ONNX export"""
|
||||
# Native export is supported by default
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def _fix_bf16_resize_nodes(onnx_opt_path):
|
||||
"""Cast Resize node I/O for strongly-typed TRT engine builds.
|
||||
TRT does not support BF16 for the Resize operator, so inputs are
|
||||
cast to FP32 and outputs are cast back to BF16."""
|
||||
import onnx
|
||||
import onnx_graphsurgeon as gs
|
||||
from demo_diffusion.model import load
|
||||
from demo_diffusion.utils_modelopt import cast_resize_io
|
||||
|
||||
onnx_graph = onnx.load(onnx_opt_path, load_external_data=True)
|
||||
graph = gs.import_onnx(onnx_graph)
|
||||
|
||||
resize_nodes = [n for n in graph.nodes if n.op == "Resize"]
|
||||
if not resize_nodes:
|
||||
return
|
||||
|
||||
print(f"[I] Fixing {len(resize_nodes)} BF16 Resize node(s) in downloaded model: {onnx_opt_path}")
|
||||
cast_resize_io(graph, output_dtype=onnx.TensorProto.BFLOAT16)
|
||||
graph.cleanup().toposort()
|
||||
onnx_graph = gs.export_onnx(graph)
|
||||
|
||||
if load.onnx_graph_needs_external_data(onnx_graph):
|
||||
onnx.save_model(
|
||||
onnx_graph,
|
||||
onnx_opt_path,
|
||||
save_as_external_data=True,
|
||||
all_tensors_to_one_file=True,
|
||||
convert_attribute=False,
|
||||
)
|
||||
else:
|
||||
onnx.save(onnx_graph, onnx_opt_path)
|
||||
|
||||
def _export_onnx(
|
||||
self,
|
||||
obj,
|
||||
model_name,
|
||||
model_config,
|
||||
opt_image_height,
|
||||
opt_image_width,
|
||||
static_shape,
|
||||
onnx_opset,
|
||||
quantization_level,
|
||||
quantization_percentile,
|
||||
quantization_alpha,
|
||||
calibration_size,
|
||||
calib_batch_size,
|
||||
onnx_export_only,
|
||||
download_onnx_models,
|
||||
):
|
||||
# With onnx_export_only True, the export still happens even if the TRT engine exists. However, it will not re-run the export if the onnx exists.
|
||||
do_export_onnx = (not os.path.exists(model_config['engine_path']) or onnx_export_only) and not os.path.exists(model_config['onnx_opt_path'])
|
||||
do_export_weights_map = model_config['weights_map_path'] and not os.path.exists(model_config['weights_map_path'])
|
||||
|
||||
# If ONNX export is required, either download ONNX models or check if the pipeline supports native ONNX export
|
||||
if do_export_onnx:
|
||||
if download_onnx_models:
|
||||
self.download_onnx_models(model_name, model_config)
|
||||
# Fix Resize nodes for strongly-typed TRT builds.
|
||||
# Downloaded models bypass optimize(), so apply the fix here.
|
||||
if obj.bf16:
|
||||
self._fix_bf16_resize_nodes(model_config['onnx_opt_path'])
|
||||
do_export_onnx = False
|
||||
else:
|
||||
self.is_native_export_supported(model_config)
|
||||
|
||||
dynamo = True if (self.pipeline_type.is_video2world() and model_name == "transformer") or (self.pipeline_type.is_txt2vid() and (model_name in ["transformer", "transformer_2"])) or (self.version.startswith("flux.1") and model_name == "transformer" and obj.fp16) else False
|
||||
|
||||
export_kwargs = {
|
||||
"static_shape": static_shape,
|
||||
"dynamo": dynamo,
|
||||
**({'opt_num_frames': obj.num_frames} if hasattr(obj, 'num_frames') and obj.num_frames else {})
|
||||
}
|
||||
|
||||
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"],
|
||||
lora_loader=self.lora_loader,
|
||||
**export_kwargs,
|
||||
)
|
||||
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,
|
||||
height=opt_image_width,
|
||||
width=opt_image_width,
|
||||
enable_lora_merge=model_config["do_lora_merge"],
|
||||
)
|
||||
obj.export_onnx(
|
||||
model_config["onnx_path"],
|
||||
model_config["onnx_opt_path"],
|
||||
onnx_opset,
|
||||
opt_image_height,
|
||||
opt_image_width,
|
||||
custom_model=quantized_model,
|
||||
**export_kwargs,
|
||||
)
|
||||
|
||||
# FIXME do_export_weights_map needs ONNX graph
|
||||
if do_export_weights_map:
|
||||
print(f"[I] Saving weights map: {model_config['weights_map_path']}")
|
||||
obj.export_weights_map(model_config['onnx_opt_path'], model_config['weights_map_path'])
|
||||
|
||||
def _build_engine(self, obj, engine, model_config, opt_batch_size, opt_image_height, opt_image_width, optimization_level, static_batch, static_shape, enable_all_tactics, timing_cache):
|
||||
update_output_names = obj.get_output_names() + obj.extra_output_names if obj.extra_output_names else None
|
||||
tf32amp = obj.tf32
|
||||
weight_streaming = getattr(obj, 'weight_streaming', False)
|
||||
precision_constraints = 'none'
|
||||
input_profile = obj.get_input_profile(
|
||||
opt_batch_size, opt_image_height, opt_image_width,
|
||||
static_batch=static_batch, static_shape=static_shape,
|
||||
**({'num_frames': obj.num_frames} if hasattr(obj, 'num_frames') and obj.num_frames else {})
|
||||
)
|
||||
|
||||
engine.build(
|
||||
model_config["onnx_opt_path"],
|
||||
tf32=tf32amp,
|
||||
input_profile=input_profile,
|
||||
enable_refit=model_config["do_engine_refit"],
|
||||
enable_all_tactics=enable_all_tactics,
|
||||
timing_cache=timing_cache,
|
||||
update_output_names=update_output_names,
|
||||
weight_streaming=weight_streaming,
|
||||
verbose=self.verbose,
|
||||
builder_optimization_level=optimization_level,
|
||||
precision_constraints=precision_constraints,
|
||||
)
|
||||
|
||||
def _refit_engine(self, obj, model_name, model_config):
|
||||
assert model_config['weights_map_path']
|
||||
with open(model_config['weights_map_path'], 'r') as fp_wts:
|
||||
print(f"[I] Loading weights map: {model_config['weights_map_path']} ")
|
||||
[weights_name_mapping, weights_shape_mapping] = json.load(fp_wts)
|
||||
|
||||
if not os.path.exists(model_config['refit_weights_path']):
|
||||
model = merge_loras(obj.get_model(), self.lora_loader)
|
||||
refit_weights, updated_weight_names = engine_module.get_refit_weights(
|
||||
model.state_dict(), model_config["onnx_opt_path"], weights_name_mapping, weights_shape_mapping
|
||||
)
|
||||
print(f"[I] Saving refit weights: {model_config['refit_weights_path']}")
|
||||
torch.save((refit_weights, updated_weight_names), model_config["refit_weights_path"])
|
||||
unload_torch_model(model)
|
||||
else:
|
||||
print(f"[I] Loading refit weights: {model_config['refit_weights_path']}")
|
||||
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
|
||||
for model_name, obj in self.models.items():
|
||||
if self.torch_fallback[model_name]:
|
||||
self.torch_models[model_name] = obj.get_model(torch_inference=self.torch_inference)
|
||||
if self.low_vram:
|
||||
self.torch_models[model_name] = self.torch_models[model_name].to('cpu')
|
||||
torch.cuda.empty_cache()
|
||||
|
||||
def load_engines(
|
||||
self,
|
||||
framework_model_dir,
|
||||
onnx_opset,
|
||||
opt_batch_size,
|
||||
opt_image_height,
|
||||
opt_image_width,
|
||||
optimization_level=3,
|
||||
static_batch=False,
|
||||
static_shape=True,
|
||||
enable_refit=False,
|
||||
enable_all_tactics=False,
|
||||
timing_cache=None,
|
||||
int8=False,
|
||||
fp8=False,
|
||||
fp4=False,
|
||||
quantization_level=2.5,
|
||||
quantization_percentile=1.0,
|
||||
quantization_alpha=0.8,
|
||||
calibration_size=32,
|
||||
calib_batch_size=2,
|
||||
onnx_export_only=False,
|
||||
download_onnx_models=False,
|
||||
):
|
||||
"""
|
||||
Build and load engines for TensorRT accelerated inference.
|
||||
Export ONNX models first, if applicable.
|
||||
|
||||
Args:
|
||||
framework_model_dir (str):
|
||||
Directory to store the framework model ckpt.
|
||||
onnx_opset (int):
|
||||
ONNX opset version to export the models.
|
||||
opt_batch_size (int):
|
||||
Batch size to optimize for during engine building.
|
||||
opt_image_height (int):
|
||||
Image height to optimize for during engine building. Must be a multiple of 8.
|
||||
opt_image_width (int):
|
||||
Image width to optimize for during engine building. Must be a multiple of 8.
|
||||
optimization_level (int):
|
||||
Optimization level to build the TensorRT engine with.
|
||||
static_batch (bool):
|
||||
Build engine only for specified opt_batch_size.
|
||||
static_shape (bool):
|
||||
Build engine only for specified opt_image_height & opt_image_width. Default = True.
|
||||
enable_refit (bool):
|
||||
Build engines with refit option enabled.
|
||||
enable_all_tactics (bool):
|
||||
Enable all tactic sources during TensorRT engine builds.
|
||||
timing_cache (str):
|
||||
Path to the timing cache to speed up TensorRT build.
|
||||
int8 (bool):
|
||||
Whether to quantize to int8 format or not (SDXL, SD15 and SD21 only).
|
||||
fp8 (bool):
|
||||
Whether to quantize to fp8 format or not (SDXL, SD15 and SD21 only).
|
||||
quantization_level (float):
|
||||
Controls which layers to quantize. 1: CNN, 2: CNN+FFN, 2.5: CNN+FFN+QKV, 3: CNN+FC
|
||||
quantization_percentile (float):
|
||||
Control quantization scaling factors (amax) collecting range, where the minimum amax in
|
||||
range(n_steps * percentile) will be collected. Recommendation: 1.0
|
||||
quantization_alpha (float):
|
||||
The alpha parameter for SmoothQuant quantization used for linear layers.
|
||||
Recommendation: 0.8 for SDXL
|
||||
calibration_size (int):
|
||||
The number of steps to use for calibrating the model for quantization.
|
||||
Recommendation: 32, 64, 128 for SDXL
|
||||
calib_batch_size (int):
|
||||
The batch size to use for calibration. Defaults to 2.
|
||||
onnx_export_only (bool):
|
||||
Whether only export onnx without building the TRT engine.
|
||||
download_onnx_models (bool):
|
||||
Download pre-exported ONNX models
|
||||
"""
|
||||
|
||||
self._initialize_models(framework_model_dir, int8, fp8, fp4)
|
||||
|
||||
model_configs = self._prepare_model_configs(enable_refit, int8, fp8, fp4)
|
||||
|
||||
# Export models to ONNX
|
||||
for model_name, obj in self.models.items():
|
||||
if self.torch_fallback[model_name]:
|
||||
continue
|
||||
self._export_onnx(
|
||||
obj,
|
||||
model_name,
|
||||
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,
|
||||
onnx_export_only,
|
||||
download_onnx_models,
|
||||
)
|
||||
|
||||
# Release temp GPU memory during onnx export to avoid OOM.
|
||||
gc.collect()
|
||||
torch.cuda.empty_cache()
|
||||
|
||||
if onnx_export_only:
|
||||
return
|
||||
|
||||
# Build TensorRT engines
|
||||
for model_name, obj in self.models.items():
|
||||
if self.torch_fallback[model_name]:
|
||||
continue
|
||||
|
||||
model_config = model_configs[model_name]
|
||||
engine = engine_module.Engine(model_config["engine_path"])
|
||||
if not os.path.exists(model_config['engine_path']):
|
||||
self._build_engine(obj, engine, model_config, opt_batch_size, opt_image_height, opt_image_width, optimization_level, static_batch, static_shape, enable_all_tactics, timing_cache)
|
||||
self.engine[model_name] = engine
|
||||
|
||||
# Load and refit TensorRT engines
|
||||
for model_name, obj in self.models.items():
|
||||
if self.torch_fallback[model_name]:
|
||||
continue
|
||||
model_config = model_configs[model_name]
|
||||
|
||||
# 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:
|
||||
weight_streaming = getattr(obj, 'weight_streaming', False)
|
||||
weight_streaming_budget_percentage = getattr(obj, 'weight_streaming_budget_percentage', None)
|
||||
self.engine[model_name].load(weight_streaming, weight_streaming_budget_percentage)
|
||||
|
||||
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()
|
||||
|
||||
# Reclaim GPU memory from torch cache
|
||||
torch.cuda.empty_cache()
|
||||
|
||||
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_v2)
|
||||
if self.low_vram:
|
||||
engine.unload()
|
||||
return max_device_memory
|
||||
|
||||
def get_device_memory_sizes(self):
|
||||
device_memory_sizes = {}
|
||||
for model_name, engine in self.engine.items():
|
||||
engine.load()
|
||||
device_memory_sizes[model_name] = engine.engine.device_memory_size_v2
|
||||
engine.unload()
|
||||
return device_memory_sizes
|
||||
|
||||
def activate_engines(self, shared_device_memory=None):
|
||||
if shared_device_memory is None:
|
||||
max_device_memory = self.calculate_max_device_memory()
|
||||
_, shared_device_memory = cudart.cudaMalloc(max_device_memory)
|
||||
self.shared_device_memory = shared_device_memory
|
||||
# Load and activate TensorRT engines
|
||||
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):
|
||||
for e in self.events.values():
|
||||
cudart.cudaEventDestroy(e[0])
|
||||
cudart.cudaEventDestroy(e[1])
|
||||
|
||||
for engine in self.engine.values():
|
||||
engine.deallocate_buffers()
|
||||
engine.deactivate()
|
||||
engine.unload(verbose=False)
|
||||
del engine
|
||||
|
||||
if self.shared_device_memory:
|
||||
cudart.cudaFree(self.shared_device_memory)
|
||||
|
||||
for torch_model in self.torch_models.values():
|
||||
torch_model.to("cpu")
|
||||
del torch_model
|
||||
|
||||
cudart.cudaStreamDestroy(self.stream)
|
||||
del self.stream
|
||||
|
||||
gc.collect()
|
||||
torch.cuda.empty_cache()
|
||||
|
||||
def initialize_latents(self, batch_size, unet_channels, latent_height, latent_width, latents_dtype=torch.float32):
|
||||
latents_shape = (batch_size, unet_channels, latent_height, latent_width)
|
||||
latents = torch.randn(latents_shape, device=self.device, dtype=latents_dtype, generator=self.generator)
|
||||
# Scale the initial noise by the standard deviation required by the scheduler
|
||||
latents = latents * self.scheduler.init_noise_sigma
|
||||
return latents
|
||||
|
||||
def save_image(self, images, pipeline, prompt, seed):
|
||||
# Save image
|
||||
prompt_prefix = ''.join(set([prompt[i].replace(' ','_')[:10] for i in range(len(prompt))]))
|
||||
image_name_prefix = '-'.join([pipeline, prompt_prefix, str(seed)])
|
||||
image_name_suffix = 'torch' if self.torch_inference else 'trt'
|
||||
image_module.save_image(images, self.output_dir, image_name_prefix, image_name_suffix)
|
||||
|
||||
@abstractmethod
|
||||
def print_summary(self):
|
||||
"""Print a summary of the pipeline's configuration."""
|
||||
raise NotImplementedError("Please Implement the print_summary method")
|
||||
|
||||
@abstractmethod
|
||||
def infer(self):
|
||||
"""Perform inference using the pipeline."""
|
||||
raise NotImplementedError("Please Implement the infer method")
|
||||
|
||||
@abstractmethod
|
||||
def run(self):
|
||||
"""Run the pipeline."""
|
||||
raise NotImplementedError("Please Implement the run method")
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,74 @@
|
||||
#
|
||||
# SPDX-FileCopyrightText: Copyright (c) 1993-2025 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 torch
|
||||
from cuda.bindings import runtime as cudart
|
||||
|
||||
|
||||
class ModelMemoryManager:
|
||||
"""
|
||||
Context manager for efficiently loading and unloading models to optimize VRAM usage.
|
||||
|
||||
This class provides a context to temporarily load models into GPU memory for inference
|
||||
and automatically unload them afterward. It's especially useful in low VRAM environments
|
||||
where models need to be swapped in and out of GPU memory.
|
||||
|
||||
Args:
|
||||
parent: The parent class instance that contains the model references and resources.
|
||||
model_names (list): List of model names to load and unload.
|
||||
low_vram (bool, optional): If True, enables VRAM optimization. If False, the context manager does nothing. Defaults to False.
|
||||
"""
|
||||
|
||||
def __init__(self, parent, model_names, low_vram=False):
|
||||
self.parent = parent
|
||||
self.model_names = model_names
|
||||
self.low_vram = low_vram
|
||||
self.device_memories = {}
|
||||
|
||||
def __enter__(self):
|
||||
if not self.low_vram:
|
||||
return
|
||||
for model_name in self.model_names:
|
||||
if not self.parent.torch_fallback[model_name]:
|
||||
# creating engine object (load from plan file)
|
||||
self.parent.engine[model_name].load()
|
||||
# allocate device memory
|
||||
_, shared_device_memory = cudart.cudaMalloc(self.parent.device_memory_sizes[model_name])
|
||||
self.device_memories[model_name] = shared_device_memory
|
||||
# creating context
|
||||
self.parent.engine[model_name].activate(device_memory=shared_device_memory)
|
||||
# creating input and output buffer
|
||||
self.parent.engine[model_name].allocate_buffers(
|
||||
shape_dict=self.parent.shape_dicts[model_name], device=self.parent.device
|
||||
)
|
||||
else:
|
||||
print(f"[I] Reloading torch model {model_name} from cpu.")
|
||||
self.parent.torch_models[model_name] = self.parent.torch_models[model_name].to("cuda")
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
if not self.low_vram:
|
||||
return
|
||||
for model_name in self.model_names:
|
||||
if not self.parent.torch_fallback[model_name]:
|
||||
self.parent.engine[model_name].deallocate_buffers()
|
||||
self.parent.engine[model_name].deactivate()
|
||||
self.parent.engine[model_name].unload()
|
||||
cudart.cudaFree(self.device_memories.pop(model_name))
|
||||
else:
|
||||
print(f"[I] Offloading torch model {model_name} to cpu.")
|
||||
self.parent.torch_models[model_name] = self.parent.torch_models[model_name].to("cpu")
|
||||
torch.cuda.empty_cache()
|
||||
@@ -0,0 +1,343 @@
|
||||
#
|
||||
# SPDX-FileCopyrightText: Copyright (c) 1993-2025 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 time
|
||||
|
||||
import tensorrt as trt
|
||||
import torch
|
||||
from cuda.bindings import runtime as cudart
|
||||
from diffusers import DDPMWuerstchenScheduler
|
||||
|
||||
from demo_diffusion.model import (
|
||||
CLIPWithProjModel,
|
||||
UNetCascadeModel,
|
||||
VQGANModel,
|
||||
make_tokenizer,
|
||||
)
|
||||
from demo_diffusion.pipeline.stable_diffusion_pipeline import StableDiffusionPipeline
|
||||
from demo_diffusion.pipeline.type import PIPELINE_TYPE
|
||||
|
||||
TRT_LOGGER = trt.Logger(trt.Logger.ERROR)
|
||||
|
||||
|
||||
class StableCascadePipeline(StableDiffusionPipeline):
|
||||
"""
|
||||
Application showcasing the acceleration of Stable Cascade pipelines using NVidia TensorRT.
|
||||
"""
|
||||
def __init__(
|
||||
self,
|
||||
version='cascade',
|
||||
pipeline_type=PIPELINE_TYPE.CASCADE_PRIOR,
|
||||
latent_dim_scale=10.67,
|
||||
lite=False,
|
||||
**kwargs
|
||||
):
|
||||
"""
|
||||
Initializes the Stable Cascade pipeline.
|
||||
|
||||
Args:
|
||||
version (str):
|
||||
The version of the pipeline. Should be one of [cascade]
|
||||
pipeline_type (PIPELINE_TYPE):
|
||||
Type of current pipeline.
|
||||
latent_dim_scale (float):
|
||||
Multiplier to determine the VQ latent space size from the image embeddings. If the image embeddings are
|
||||
height=24 and width=24, the VQ latent shape needs to be height=int(24*10.67)=256 and
|
||||
width=int(24*10.67)=256 in order to match the training conditions.
|
||||
lite (bool):
|
||||
Boolean indicating if the Lite Version of the Stage B and Stage C models is to be used
|
||||
"""
|
||||
super().__init__(
|
||||
version=version,
|
||||
pipeline_type=pipeline_type,
|
||||
**kwargs
|
||||
)
|
||||
self.config['clip_hidden_states'] = True
|
||||
# from Diffusers: https://github.com/huggingface/diffusers/blob/main/src/diffusers/pipelines/stable_cascade/pipeline_stable_cascade.py#L91C9-L91C41
|
||||
self.latent_dim_scale = latent_dim_scale
|
||||
self.lite = lite
|
||||
|
||||
def initializeModels(self, framework_model_dir, int8, fp8):
|
||||
# Load text tokenizer(s)
|
||||
self.tokenizer = make_tokenizer(self.version, self.pipeline_type, self.hf_token, framework_model_dir)
|
||||
|
||||
# Load pipeline models
|
||||
models_args = {'version': self.version, 'pipeline': self.pipeline_type, 'device': self.device,
|
||||
'hf_token': self.hf_token, 'verbose': self.verbose, 'framework_model_dir': framework_model_dir,
|
||||
'max_batch_size': self.max_batch_size}
|
||||
|
||||
self.fp16 = False # TODO: enable FP16 mode for decoder model (requires strongly typed engine)
|
||||
self.bf16 = True
|
||||
if 'clip' in self.stages:
|
||||
self.models['clip'] = CLIPWithProjModel(**models_args, fp16=self.fp16, bf16=self.bf16, output_hidden_states=self.config.get('clip_hidden_states', False), subfolder='text_encoder')
|
||||
|
||||
if 'unet' in self.stages:
|
||||
self.models['unet'] = UNetCascadeModel(**models_args, fp16=self.fp16, bf16=self.bf16, lite=self.lite, do_classifier_free_guidance=self.do_classifier_free_guidance)
|
||||
|
||||
if 'vqgan' in self.stages:
|
||||
self.models['vqgan'] = VQGANModel(**models_args, fp16=self.fp16, bf16=self.bf16, latent_dim_scale = self.latent_dim_scale)
|
||||
|
||||
def encode_prompt(self, prompt, negative_prompt, encoder='clip', pooled_outputs=False, output_hidden_states=False):
|
||||
self.profile_start(encoder, color='green')
|
||||
|
||||
tokenizer = self.tokenizer
|
||||
|
||||
def tokenize(prompt, output_hidden_states):
|
||||
text_inputs = tokenizer(
|
||||
prompt,
|
||||
padding="max_length",
|
||||
max_length=tokenizer.model_max_length,
|
||||
truncation=True,
|
||||
return_tensors="pt",
|
||||
)
|
||||
text_input_ids = text_inputs.input_ids.type(torch.int32).to(self.device)
|
||||
attention_mask = text_inputs.attention_mask.type(torch.int32).to(self.device)
|
||||
|
||||
text_hidden_states = None
|
||||
if self.torch_inference:
|
||||
outputs = self.torch_models[encoder](text_input_ids, attention_mask=attention_mask, output_hidden_states=output_hidden_states)
|
||||
text_embeddings = outputs[0].clone()
|
||||
if output_hidden_states:
|
||||
hidden_state_layer = -1
|
||||
text_hidden_states = outputs['hidden_states'][hidden_state_layer].clone()
|
||||
else:
|
||||
# NOTE: output tensor for CLIP must be cloned because it will be overwritten when called again for negative prompt
|
||||
outputs = self.runEngine(encoder, {'input_ids': text_input_ids, 'attention_mask': attention_mask})
|
||||
text_embeddings = outputs['text_embeddings'].clone()
|
||||
if output_hidden_states:
|
||||
text_hidden_states = outputs['hidden_states'].clone()
|
||||
|
||||
return text_embeddings, text_hidden_states
|
||||
|
||||
# Tokenize prompt
|
||||
text_embeddings, text_hidden_states = tokenize(prompt, output_hidden_states)
|
||||
|
||||
if self.do_classifier_free_guidance:
|
||||
# Tokenize negative prompt
|
||||
uncond_embeddings, uncond_hidden_states = tokenize(negative_prompt, output_hidden_states)
|
||||
|
||||
# Concatenate the unconditional and text embeddings into a single batch to avoid doing two forward passes for classifier free guidance
|
||||
text_embeddings = torch.cat([text_embeddings, uncond_embeddings])
|
||||
|
||||
if pooled_outputs:
|
||||
pooled_output = text_embeddings
|
||||
|
||||
if output_hidden_states:
|
||||
text_embeddings = torch.cat([text_hidden_states, uncond_hidden_states]) if self.do_classifier_free_guidance else text_hidden_states
|
||||
|
||||
self.profile_stop(encoder)
|
||||
if pooled_outputs:
|
||||
return text_embeddings, pooled_output
|
||||
return text_embeddings
|
||||
|
||||
def denoise_latent(self,
|
||||
latents,
|
||||
pooled_embeddings,
|
||||
text_embeddings=None,
|
||||
image_embeds=None,
|
||||
effnet=None,
|
||||
denoiser='unet',
|
||||
timesteps=None,
|
||||
):
|
||||
|
||||
do_autocast = False
|
||||
with torch.autocast('cuda', enabled=do_autocast):
|
||||
self.profile_start(denoiser, color='blue')
|
||||
for step_index, timestep in enumerate(timesteps):
|
||||
# ratio input required for stable cascade prior
|
||||
timestep_ratio = timestep.expand(latents.size(0)).to(latents.dtype)
|
||||
# Expand the latents and timestep_ratio if we are doing classifier free guidance
|
||||
latent_model_input = torch.cat([latents] * 2) if self.do_classifier_free_guidance else latents
|
||||
timestep_ratio_input = torch.cat([timestep_ratio] * 2) if self.do_classifier_free_guidance else timestep_ratio
|
||||
|
||||
params = {"sample": latent_model_input, "timestep_ratio": timestep_ratio_input, "clip_text_pooled": pooled_embeddings}
|
||||
if text_embeddings is not None:
|
||||
params.update({'clip_text': text_embeddings})
|
||||
if image_embeds is not None:
|
||||
params.update({'clip_img': image_embeds})
|
||||
if effnet is not None:
|
||||
params.update({'effnet': effnet})
|
||||
|
||||
# Predict the noise residual
|
||||
if self.torch_inference:
|
||||
noise_pred = self.torch_models[denoiser](**params)['sample']
|
||||
else:
|
||||
noise_pred = self.runEngine(denoiser, params)['latent']
|
||||
|
||||
# Perform guidance
|
||||
if self.do_classifier_free_guidance:
|
||||
noise_pred_text, noise_pred_uncond = noise_pred.chunk(2)
|
||||
noise_pred = noise_pred_uncond + self.guidance_scale * (noise_pred_text - noise_pred_uncond)
|
||||
|
||||
# from diffusers (prepare_extra_step_kwargs)
|
||||
extra_step_kwargs = {}
|
||||
if "eta" in set(inspect.signature(self.scheduler.step).parameters.keys()):
|
||||
# TODO: configurable eta
|
||||
eta = 0.0
|
||||
extra_step_kwargs["eta"] = eta
|
||||
if "generator" in set(inspect.signature(self.scheduler.step).parameters.keys()):
|
||||
extra_step_kwargs["generator"] = self.generator
|
||||
|
||||
latents = self.scheduler.step(noise_pred, timestep_ratio, latents, **extra_step_kwargs, return_dict=False)[0]
|
||||
|
||||
latents = latents.to(dtype=torch.bfloat16 if self.bf16 else torch.float32)
|
||||
|
||||
self.profile_stop(denoiser)
|
||||
return latents
|
||||
|
||||
def decode_latent(self, latents, model_name='vqgan'):
|
||||
self.profile_start(model_name, color='red')
|
||||
latents = self.models[model_name].scale_factor * latents
|
||||
if self.torch_inference:
|
||||
images = self.torch_models[model_name](latents)['sample']
|
||||
else:
|
||||
images = self.runEngine(model_name, {'latent': latents})['images']
|
||||
self.profile_stop(model_name)
|
||||
return images
|
||||
|
||||
def print_summary(self, denoising_steps, walltime_ms, batch_size):
|
||||
print('|-----------------|--------------|')
|
||||
print('| {:^15} | {:^12} |'.format('Module', 'Latency'))
|
||||
print('|-----------------|--------------|')
|
||||
for stage in self.stages:
|
||||
stage_name = stage + ' x ' + str(denoising_steps) if stage == 'unet' else stage
|
||||
print(
|
||||
"| {:^15} | {:>9.2f} ms |".format(
|
||||
stage_name, cudart.cudaEventElapsedTime(self.events[stage][0], self.events[stage][1])[1],
|
||||
)
|
||||
)
|
||||
print('|-----------------|--------------|')
|
||||
print('| {:^15} | {:>9.2f} ms |'.format('Pipeline', walltime_ms))
|
||||
print('|-----------------|--------------|')
|
||||
print('Throughput: {:.5f} image/s'.format(batch_size*1000./walltime_ms))
|
||||
|
||||
def infer(
|
||||
self,
|
||||
prompt,
|
||||
negative_prompt,
|
||||
image_height,
|
||||
image_width,
|
||||
image_embeddings=None,
|
||||
warmup=False,
|
||||
verbose=False,
|
||||
save_image=True,
|
||||
):
|
||||
"""
|
||||
Run the diffusion pipeline.
|
||||
|
||||
Args:
|
||||
prompt (str):
|
||||
The text prompt to guide image generation.
|
||||
negative_prompt (str):
|
||||
The prompt not to guide the image generation.
|
||||
image_height (int):
|
||||
Height (in pixels) of the image to be generated. Must be a multiple of 8.
|
||||
image_width (int):
|
||||
Width (in pixels) of the image to be generated. Must be a multiple of 8.
|
||||
image_embeddings (`torch.FloatTensor` or `List[torch.FloatTensor]`):
|
||||
Image Embeddings either extracted from an image or generated by a Prior Model.
|
||||
warmup (bool):
|
||||
Indicate if this is a warmup run.
|
||||
verbose (bool):
|
||||
Verbose in logging
|
||||
save_image (bool):
|
||||
Save the generated image (if applicable)
|
||||
"""
|
||||
if self.pipeline_type.is_cascade_decoder():
|
||||
assert image_embeddings is not None, "Image Embeddings are required to run the decoder. Provided None"
|
||||
assert len(prompt) == len(negative_prompt)
|
||||
batch_size = len(prompt)
|
||||
|
||||
# Spatial dimensions of latent tensor
|
||||
latent_height = image_height // 42
|
||||
latent_width = image_width // 42
|
||||
|
||||
if image_embeddings is not None:
|
||||
assert latent_height == image_embeddings.shape[-2]
|
||||
assert latent_width == image_embeddings.shape[-1]
|
||||
|
||||
if self.generator and self.seed:
|
||||
self.generator.manual_seed(self.seed)
|
||||
|
||||
num_inference_steps = self.denoising_steps
|
||||
|
||||
with torch.inference_mode(), trt.Runtime(TRT_LOGGER):
|
||||
torch.cuda.synchronize()
|
||||
e2e_tic = time.perf_counter()
|
||||
|
||||
denoise_kwargs = {}
|
||||
# TODO: support custom timesteps
|
||||
timesteps = None
|
||||
if timesteps is not None:
|
||||
if "timesteps" not in set(inspect.signature(self.scheduler.set_timesteps).parameters.keys()):
|
||||
raise ValueError(
|
||||
f"The current scheduler class {self.scheduler.__class__}'s `set_timesteps` does not support custom"
|
||||
f" timestep schedules. Please check whether you are using the correct scheduler."
|
||||
)
|
||||
self.scheduler.set_timesteps(timesteps=timesteps, device=self.device)
|
||||
assert self.denoising_steps == len(self.scheduler.timesteps)
|
||||
else:
|
||||
self.scheduler.set_timesteps(self.denoising_steps, device=self.device)
|
||||
timesteps = self.scheduler.timesteps.to(self.device)
|
||||
if isinstance(self.scheduler, DDPMWuerstchenScheduler):
|
||||
timesteps = timesteps[:-1]
|
||||
denoise_kwargs.update({'timesteps': timesteps})
|
||||
|
||||
# Initialize latents
|
||||
latents_dtpye = torch.float16 if self.fp16 else torch.bfloat16 if self.bf16 else torch.float32
|
||||
latents = self.initialize_latents(
|
||||
batch_size=batch_size,
|
||||
unet_channels=16 if self.pipeline_type.is_cascade_prior() else 4, # TODO: can we query "in_channels" from config
|
||||
latent_height=latent_height if self.pipeline_type.is_cascade_prior() else int(latent_height * self.latent_dim_scale),
|
||||
latent_width=latent_width if self.pipeline_type.is_cascade_prior() else int(latent_width * self.latent_dim_scale),
|
||||
latents_dtype=latents_dtpye
|
||||
)
|
||||
|
||||
# CLIP text encoder(s)
|
||||
text_embeddings, pooled_embeddings = self.encode_prompt(prompt, negative_prompt,
|
||||
encoder='clip', pooled_outputs=True, output_hidden_states=True)
|
||||
|
||||
if self.pipeline_type.is_cascade_prior():
|
||||
denoise_kwargs.update({'text_embeddings': text_embeddings})
|
||||
|
||||
# image embeds
|
||||
image_embeds_pooled = torch.zeros(batch_size, 1, 768, device=self.device, dtype=latents_dtpye)
|
||||
image_embeds = (torch.cat([image_embeds_pooled, torch.zeros_like(image_embeds_pooled)]) if self.do_classifier_free_guidance else image_embeddings)
|
||||
denoise_kwargs.update({'image_embeds': image_embeds})
|
||||
else:
|
||||
effnet = (torch.cat([image_embeddings, torch.zeros_like(image_embeddings)]) if self.do_classifier_free_guidance else image_embeddings)
|
||||
denoise_kwargs.update({'effnet': effnet})
|
||||
|
||||
# UNet denoiser
|
||||
latents = self.denoise_latent(latents, pooled_embeddings.unsqueeze(1), denoiser='unet', **denoise_kwargs)
|
||||
|
||||
if not self.return_latents:
|
||||
images = self.decode_latent(latents)
|
||||
|
||||
torch.cuda.synchronize()
|
||||
e2e_toc = time.perf_counter()
|
||||
|
||||
walltime_ms = (e2e_toc - e2e_tic) * 1000.
|
||||
if not warmup:
|
||||
self.print_summary(num_inference_steps, walltime_ms, batch_size)
|
||||
if not self.return_latents and save_image:
|
||||
# post-process images
|
||||
images = ((images) * 255).clamp(0, 255).detach().permute(0, 2, 3, 1).round().type(torch.uint8).cpu().numpy()
|
||||
self.save_image(images, self.pipeline_type.name.lower(), prompt, self.seed)
|
||||
|
||||
return (latents, walltime_ms) if self.return_latents else (images, walltime_ms)
|
||||
@@ -0,0 +1,952 @@
|
||||
#
|
||||
# SPDX-FileCopyrightText: Copyright (c) 1993-2025 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 __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import inspect
|
||||
import os
|
||||
import time
|
||||
import warnings
|
||||
from typing import Any, List, Union
|
||||
|
||||
import tensorrt as trt
|
||||
import torch
|
||||
from cuda.bindings import runtime as cudart
|
||||
from diffusers.image_processor import VaeImageProcessor
|
||||
from huggingface_hub import snapshot_download
|
||||
from transformers import PreTrainedTokenizerBase
|
||||
|
||||
from demo_diffusion import path as path_module
|
||||
from demo_diffusion.model import (
|
||||
CLIPWithProjModel,
|
||||
SD3ControlNet,
|
||||
SD3TransformerModel,
|
||||
T5Model,
|
||||
VAEEncoderModel,
|
||||
VAEModel,
|
||||
load,
|
||||
make_tokenizer,
|
||||
)
|
||||
from demo_diffusion.pipeline.diffusion_pipeline import DiffusionPipeline
|
||||
from demo_diffusion.pipeline.type import PIPELINE_TYPE
|
||||
|
||||
TRT_LOGGER = trt.Logger(trt.Logger.ERROR)
|
||||
|
||||
class SD3CannyImageProcessor(VaeImageProcessor):
|
||||
def __init__(self):
|
||||
super().__init__(do_normalize=False)
|
||||
def preprocess(self, image, **kwargs):
|
||||
image = super().preprocess(image, **kwargs)
|
||||
image = image * 255 * 0.5 + 0.5
|
||||
return image
|
||||
def postprocess(self, image, do_denormalize=True, **kwargs):
|
||||
do_denormalize = [True] * image.shape[0]
|
||||
image = super().postprocess(image, **kwargs, do_denormalize=do_denormalize)
|
||||
return image
|
||||
|
||||
class StableDiffusion35Pipeline(DiffusionPipeline):
|
||||
"""
|
||||
Application showcasing the acceleration of Stable Diffusion 3.5 pipelines using Nvidia TensorRT.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
version: str,
|
||||
pipeline_type=PIPELINE_TYPE.TXT2IMG,
|
||||
guidance_scale: float = 7.0,
|
||||
max_sequence_length: int = 256,
|
||||
controlnet=None,
|
||||
**kwargs,
|
||||
):
|
||||
"""
|
||||
Initializes the Stable Diffusion 3.5 pipeline.
|
||||
|
||||
Args:
|
||||
version (str):
|
||||
The version of the pipeline. Should be one of ['3.5-medium', '3.5-large']
|
||||
pipeline_type (PIPELINE_TYPE):
|
||||
Type of current pipeline.
|
||||
guidance_scale (`float`, defaults to 7.0):
|
||||
Guidance scale is enabled by setting as > 1.
|
||||
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 256):
|
||||
Maximum sequence length to use with the `prompt`.
|
||||
controlnet (str):
|
||||
Which ControlNet to use.
|
||||
"""
|
||||
super().__init__(version=version, pipeline_type=pipeline_type, controlnet=controlnet, **kwargs)
|
||||
|
||||
self.fp16 = True if not self.bf16 else False
|
||||
|
||||
self.force_weakly_typed_t5 = False
|
||||
self.config["clip_g_torch_fallback"] = True
|
||||
self.config["clip_l_torch_fallback"] = True
|
||||
self.config["clip_hidden_states"] = True
|
||||
self.controlnet = controlnet
|
||||
|
||||
self.guidance_scale = guidance_scale
|
||||
self.do_classifier_free_guidance = self.guidance_scale > 1
|
||||
self.max_sequence_length = max_sequence_length
|
||||
|
||||
@classmethod
|
||||
def FromArgs(cls, args: argparse.Namespace, pipeline_type: PIPELINE_TYPE) -> StableDiffusion35Pipeline:
|
||||
"""Factory method to construct a `StableDiffusion35Pipeline` object from parsed arguments.
|
||||
|
||||
Overrides:
|
||||
DiffusionPipeline.FromArgs
|
||||
"""
|
||||
MAX_BATCH_SIZE = 4
|
||||
DEVICE = "cuda"
|
||||
DO_RETURN_LATENTS = False
|
||||
|
||||
# Resolve all paths.
|
||||
controlnet_type = args.controlnet_type if "controlnet_type" in args else None
|
||||
dd_path = path_module.resolve_path(
|
||||
cls.get_model_names(pipeline_type, controlnet_type),
|
||||
args,
|
||||
pipeline_type,
|
||||
cls._get_pipeline_uid(args.version),
|
||||
)
|
||||
|
||||
return cls(
|
||||
dd_path=dd_path,
|
||||
version=args.version,
|
||||
pipeline_type=pipeline_type,
|
||||
guidance_scale=args.guidance_scale,
|
||||
max_sequence_length=args.max_sequence_length,
|
||||
controlnet=controlnet_type,
|
||||
bf16=args.bf16,
|
||||
low_vram=args.low_vram,
|
||||
torch_fallback=args.torch_fallback,
|
||||
weight_streaming=args.ws,
|
||||
max_batch_size=MAX_BATCH_SIZE,
|
||||
denoising_steps=args.denoising_steps,
|
||||
scheduler=args.scheduler,
|
||||
device=DEVICE,
|
||||
output_dir=args.output_dir,
|
||||
hf_token=args.hf_token,
|
||||
verbose=args.verbose,
|
||||
nvtx_profile=args.nvtx_profile,
|
||||
use_cuda_graph=args.use_cuda_graph,
|
||||
framework_model_dir=args.framework_model_dir,
|
||||
return_latents=DO_RETURN_LATENTS,
|
||||
torch_inference=args.torch_inference,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def get_model_names(cls, pipeline_type: PIPELINE_TYPE, controlnet_type: str = None) -> List[str]:
|
||||
"""Return a list of model names used by this pipeline.
|
||||
|
||||
Overrides:
|
||||
DiffusionPipeline.get_model_names
|
||||
"""
|
||||
if pipeline_type.is_controlnet():
|
||||
assert controlnet_type, "ControlNet type must be specified for ControlNet pipelines"
|
||||
return ["clip_l", "clip_g", "t5", "transformer", "vae", "vae_encoder", f"controlnet_{controlnet_type}"]
|
||||
return ["clip_l", "clip_g", "t5", "transformer", "vae"]
|
||||
|
||||
def download_onnx_models(self, model_name: str, model_config: dict[str, Any]) -> None:
|
||||
if self.fp16:
|
||||
raise ValueError(
|
||||
"ONNX models can be downloaded only for the following precisions: BF16, FP8. This pipeline is running in FP16."
|
||||
)
|
||||
|
||||
hf_download_path = "-".join([load.get_path(self.version, self.pipeline_type.name), "tensorrt"])
|
||||
model_path = model_config["onnx_opt_path"]
|
||||
base_dir = os.path.dirname(os.path.dirname(model_config["onnx_opt_path"]))
|
||||
|
||||
if not os.path.exists(model_path):
|
||||
if model_name == "transformer":
|
||||
if model_config["use_fp8"]:
|
||||
dirname = os.path.join(model_name, "fp8")
|
||||
elif self.bf16:
|
||||
dirname = os.path.join(model_name, "bf16")
|
||||
elif "controlnet" in model_name:
|
||||
hf_download_path_cnet = hf_download_path.replace("large", "controlnets")
|
||||
dirname = f"controlnet_{self.controlnet}"
|
||||
if "blur" in model_name:
|
||||
pass
|
||||
elif model_config["use_fp8"]:
|
||||
dirname = os.path.join(dirname, "fp8")
|
||||
elif self.bf16:
|
||||
dirname = os.path.join(dirname, "bf16")
|
||||
elif model_name in self.stages:
|
||||
dirname = model_name
|
||||
else:
|
||||
raise ValueError(f"{model_name} not found in {self.stages}")
|
||||
|
||||
dirname = os.path.join("ONNX", dirname)
|
||||
snapshot_download(
|
||||
repo_id=hf_download_path if "controlnet" not in model_name else hf_download_path_cnet,
|
||||
allow_patterns=os.path.join(dirname, "*"),
|
||||
local_dir=base_dir,
|
||||
token=self.hf_token,
|
||||
)
|
||||
# Rename directory from ONNX/<model_name> to <model_name>
|
||||
saved_dir = os.path.join(base_dir, dirname)
|
||||
model_dir = os.path.dirname(model_path)
|
||||
os.rename(saved_dir, model_dir)
|
||||
|
||||
def load_resources(
|
||||
self,
|
||||
image_height: int,
|
||||
image_width: int,
|
||||
batch_size: int,
|
||||
seed: int,
|
||||
):
|
||||
super().load_resources(image_height, image_width, batch_size, seed)
|
||||
|
||||
def _initialize_models(self, framework_model_dir, int8, fp8, fp4):
|
||||
# Load text tokenizer(s)
|
||||
self.tokenizer = make_tokenizer(
|
||||
self.version,
|
||||
self.pipeline_type,
|
||||
self.hf_token,
|
||||
framework_model_dir,
|
||||
)
|
||||
self.tokenizer2 = make_tokenizer(
|
||||
self.version,
|
||||
self.pipeline_type,
|
||||
self.hf_token,
|
||||
framework_model_dir,
|
||||
subfolder="tokenizer_2",
|
||||
)
|
||||
self.tokenizer3 = make_tokenizer(
|
||||
self.version,
|
||||
self.pipeline_type,
|
||||
self.hf_token,
|
||||
framework_model_dir,
|
||||
subfolder="tokenizer_3",
|
||||
tokenizer_type="t5",
|
||||
)
|
||||
|
||||
# Load pipeline models
|
||||
models_args = {
|
||||
"version": self.version,
|
||||
"pipeline": self.pipeline_type,
|
||||
"device": self.device,
|
||||
"hf_token": self.hf_token,
|
||||
"verbose": self.verbose,
|
||||
"framework_model_dir": framework_model_dir,
|
||||
"max_batch_size": self.max_batch_size,
|
||||
}
|
||||
|
||||
self.bf16 = True if int8 or fp8 or fp4 else self.bf16
|
||||
self.fp16 = True if not self.bf16 else False
|
||||
self.tf32 = True
|
||||
self.fp8 = fp8
|
||||
self.int8 = int8
|
||||
self.fp4 = fp4
|
||||
if "clip_l" in self.stages:
|
||||
self.models["clip_l"] = CLIPWithProjModel(
|
||||
**models_args,
|
||||
fp16=self.fp16,
|
||||
bf16=self.bf16,
|
||||
subfolder="text_encoder",
|
||||
output_hidden_states=self.config.get("clip_hidden_states", False),
|
||||
)
|
||||
|
||||
if "clip_g" in self.stages:
|
||||
self.models["clip_g"] = CLIPWithProjModel(
|
||||
**models_args,
|
||||
fp16=self.fp16,
|
||||
bf16=self.bf16,
|
||||
subfolder="text_encoder_2",
|
||||
output_hidden_states=self.config.get("clip_hidden_states", False),
|
||||
)
|
||||
|
||||
if "t5" in self.stages:
|
||||
# Known accuracy issues with FP16
|
||||
self.models["t5"] = T5Model(
|
||||
**models_args,
|
||||
fp16=self.fp16,
|
||||
bf16=self.bf16,
|
||||
tf32=self.tf32,
|
||||
subfolder="text_encoder_3",
|
||||
text_maxlen=self.max_sequence_length,
|
||||
weight_streaming=self.weight_streaming,
|
||||
weight_streaming_budget_percentage=self.text_encoder_weight_streaming_budget_percentage,
|
||||
)
|
||||
|
||||
if "transformer" in self.stages:
|
||||
self.models["transformer"] = SD3TransformerModel(
|
||||
**models_args,
|
||||
fp16=self.fp16,
|
||||
tf32=self.tf32,
|
||||
bf16=self.bf16,
|
||||
fp8=self.fp8,
|
||||
int8=self.int8,
|
||||
fp4=self.fp4,
|
||||
text_maxlen=self.models["t5"].text_maxlen + self.models["clip_g"].text_maxlen,
|
||||
weight_streaming=self.weight_streaming,
|
||||
do_classifier_free_guidance=self.do_classifier_free_guidance,
|
||||
)
|
||||
|
||||
if f"controlnet_{self.controlnet}" in self.stages:
|
||||
self.models[f"controlnet_{self.controlnet}"] = SD3ControlNet(
|
||||
**models_args,
|
||||
fp16=self.fp16,
|
||||
tf32=self.tf32,
|
||||
bf16=self.bf16,
|
||||
do_classifier_free_guidance=self.do_classifier_free_guidance,
|
||||
controlnet=self.controlnet,
|
||||
)
|
||||
|
||||
if "vae" in self.stages:
|
||||
self.models["vae"] = VAEModel(**models_args, fp16=self.fp16, tf32=self.tf32, bf16=self.bf16)
|
||||
|
||||
self.vae_scale_factor = (
|
||||
2 ** (len(self.models["vae"].config["block_out_channels"]) - 1) if "vae" in self.models else 8
|
||||
)
|
||||
self.patch_size = (
|
||||
self.models["transformer"].config["patch_size"]
|
||||
if "transformer" in self.stages and self.models["transformer"] is not None
|
||||
else 2
|
||||
)
|
||||
|
||||
if "vae_encoder" in self.stages:
|
||||
self.models["vae_encoder"] = VAEEncoderModel(**models_args, fp16=False, tf32=self.tf32, bf16=self.bf16, do_classifier_free_guidance=self.do_classifier_free_guidance)
|
||||
self.vae_latent_channels = (
|
||||
self.models["vae"].config["latent_channels"]
|
||||
if "vae" in self.stages and self.models["vae"] is not None
|
||||
else 16
|
||||
)
|
||||
if self.controlnet and "canny" in self.controlnet:
|
||||
self.image_processor = SD3CannyImageProcessor()
|
||||
else:
|
||||
self.image_processor = VaeImageProcessor(vae_scale_factor=self.vae_scale_factor)
|
||||
|
||||
def print_summary(self, denoising_steps, walltime_ms):
|
||||
print("|-----------------|--------------|")
|
||||
print("| {:^15} | {:^12} |".format("Module", "Latency"))
|
||||
print("|-----------------|--------------|")
|
||||
for stage in self.stages:
|
||||
# controlnet is profiled in the denoising step
|
||||
if "controlnet" in stage:
|
||||
continue
|
||||
stage_name = stage
|
||||
if "transformer" in stage:
|
||||
if f"controlnet_{self.controlnet}" in self.stages:
|
||||
stage_name += '+cnet'
|
||||
stage_name += ' x ' + str(denoising_steps)
|
||||
print(
|
||||
"| {:^15} | {:>9.2f} ms |".format(
|
||||
stage_name, cudart.cudaEventElapsedTime(self.events[stage][0], self.events[stage][1])[1],
|
||||
)
|
||||
)
|
||||
print("|-----------------|--------------|")
|
||||
print("| {:^15} | {:>9.2f} ms |".format("Pipeline", walltime_ms))
|
||||
print("|-----------------|--------------|")
|
||||
print("Throughput: {:.5f} image/s".format(self.batch_size * 1000.0 / walltime_ms))
|
||||
|
||||
@staticmethod
|
||||
def _tokenize(
|
||||
tokenizer: PreTrainedTokenizerBase,
|
||||
prompt: list[str],
|
||||
max_sequence_length: int,
|
||||
device: torch.device,
|
||||
):
|
||||
text_input_ids = tokenizer(
|
||||
prompt,
|
||||
padding="max_length",
|
||||
max_length=max_sequence_length,
|
||||
truncation=True,
|
||||
add_special_tokens=True,
|
||||
return_tensors="pt",
|
||||
).input_ids
|
||||
text_input_ids = text_input_ids.type(torch.int32)
|
||||
|
||||
untruncated_ids = tokenizer(
|
||||
prompt,
|
||||
padding="longest",
|
||||
return_tensors="pt",
|
||||
).input_ids.type(torch.int32)
|
||||
|
||||
if untruncated_ids.shape[-1] >= text_input_ids.shape[-1] and not torch.equal(text_input_ids, untruncated_ids):
|
||||
removed_text = tokenizer.batch_decode(untruncated_ids[:, max_sequence_length - 1 : -1])
|
||||
warnings.warn(
|
||||
"The following part of your input was truncated because `max_sequence_length` is set to "
|
||||
f" {max_sequence_length} tokens: {removed_text}"
|
||||
)
|
||||
text_input_ids = text_input_ids.to(device)
|
||||
return text_input_ids
|
||||
|
||||
def _get_prompt_embed(
|
||||
self,
|
||||
prompt: list[str],
|
||||
encoder_name: str,
|
||||
domain="positive_prompt",
|
||||
):
|
||||
if encoder_name == "clip_l":
|
||||
tokenizer = self.tokenizer
|
||||
max_sequence_length = tokenizer.model_max_length
|
||||
output_hidden_states = True
|
||||
elif encoder_name == "clip_g":
|
||||
tokenizer = self.tokenizer2
|
||||
max_sequence_length = tokenizer.model_max_length
|
||||
output_hidden_states = True
|
||||
elif encoder_name == "t5":
|
||||
tokenizer = self.tokenizer3
|
||||
max_sequence_length = self.max_sequence_length
|
||||
output_hidden_states = False
|
||||
else:
|
||||
raise NotImplementedError(f"encoder not found: {encoder_name}")
|
||||
|
||||
self.profile_start(encoder_name, color="green", domain=domain)
|
||||
|
||||
text_input_ids = self._tokenize(
|
||||
tokenizer=tokenizer,
|
||||
prompt=prompt,
|
||||
device=self.device,
|
||||
max_sequence_length=max_sequence_length,
|
||||
)
|
||||
|
||||
text_hidden_states = None
|
||||
if self.torch_inference or self.torch_fallback[encoder_name]:
|
||||
outputs = self.torch_models[encoder_name](
|
||||
text_input_ids,
|
||||
output_hidden_states=output_hidden_states,
|
||||
)
|
||||
text_embeddings = outputs[0].clone()
|
||||
if output_hidden_states:
|
||||
text_hidden_states = outputs["hidden_states"][-2].clone()
|
||||
else:
|
||||
# NOTE: output tensor for the encoder must be cloned because it will be overwritten when called again for prompt2
|
||||
outputs = self.run_engine(encoder_name, {"input_ids": text_input_ids})
|
||||
text_embeddings = outputs["text_embeddings"].clone()
|
||||
if output_hidden_states:
|
||||
text_hidden_states = outputs["hidden_states"].clone()
|
||||
|
||||
self.profile_stop(encoder_name)
|
||||
return text_hidden_states, text_embeddings
|
||||
|
||||
@staticmethod
|
||||
def _duplicate_text_embed(
|
||||
prompt_embed: torch.Tensor,
|
||||
batch_size: int,
|
||||
num_images_per_prompt: int,
|
||||
pooled_prompt_embed: torch.Tensor | None = None,
|
||||
):
|
||||
_, seq_len, _ = prompt_embed.shape
|
||||
# duplicate text embeddings for each generation per prompt, using mps friendly method
|
||||
prompt_embed = prompt_embed.repeat(1, num_images_per_prompt, 1)
|
||||
prompt_embed = prompt_embed.view(batch_size * num_images_per_prompt, seq_len, -1)
|
||||
|
||||
if pooled_prompt_embed is not None:
|
||||
pooled_prompt_embed = pooled_prompt_embed.repeat(1, num_images_per_prompt, 1)
|
||||
pooled_prompt_embed = pooled_prompt_embed.view(batch_size * num_images_per_prompt, -1)
|
||||
|
||||
return prompt_embed, pooled_prompt_embed
|
||||
|
||||
def encode_prompt(
|
||||
self,
|
||||
prompt: list[str],
|
||||
negative_prompt: list[str] | None = None,
|
||||
num_images_per_prompt: int = 1,
|
||||
):
|
||||
clip_l_prompt_embed, clip_l_pooled_embed = self._get_prompt_embed(
|
||||
prompt=prompt,
|
||||
encoder_name="clip_l",
|
||||
)
|
||||
prompt_embed, pooled_prompt_embed = self._duplicate_text_embed(
|
||||
prompt_embed=clip_l_prompt_embed.clone(),
|
||||
pooled_prompt_embed=clip_l_pooled_embed.clone(),
|
||||
num_images_per_prompt=num_images_per_prompt,
|
||||
batch_size=self.batch_size,
|
||||
)
|
||||
|
||||
clip_g_prompt_embed, clip_g_pooled_embed = self._get_prompt_embed(
|
||||
prompt=prompt,
|
||||
encoder_name="clip_g",
|
||||
)
|
||||
prompt_2_embed, pooled_prompt_2_embed = self._duplicate_text_embed(
|
||||
prompt_embed=clip_g_prompt_embed.clone(),
|
||||
pooled_prompt_embed=clip_g_pooled_embed.clone(),
|
||||
batch_size=self.batch_size,
|
||||
num_images_per_prompt=num_images_per_prompt,
|
||||
)
|
||||
|
||||
_, t5_prompt_embed = self._get_prompt_embed(
|
||||
prompt=prompt,
|
||||
encoder_name="t5",
|
||||
)
|
||||
|
||||
t5_prompt_embed, _ = self._duplicate_text_embed(
|
||||
prompt_embed=t5_prompt_embed.clone(),
|
||||
batch_size=self.batch_size,
|
||||
num_images_per_prompt=num_images_per_prompt,
|
||||
)
|
||||
|
||||
clip_prompt_embeds = torch.cat([prompt_embed, prompt_2_embed], dim=-1)
|
||||
clip_prompt_embeds = torch.nn.functional.pad(
|
||||
clip_prompt_embeds, (0, t5_prompt_embed.shape[-1] - clip_prompt_embeds.shape[-1])
|
||||
)
|
||||
prompt_embeds = torch.cat([clip_prompt_embeds, t5_prompt_embed], dim=-2)
|
||||
pooled_prompt_embeds = torch.cat([pooled_prompt_embed, pooled_prompt_2_embed], dim=-1)
|
||||
|
||||
if negative_prompt is None:
|
||||
negative_prompt = ""
|
||||
|
||||
clip_l_negative_prompt_embed, clip_l_negative_pooled_embed = self._get_prompt_embed(
|
||||
prompt=negative_prompt,
|
||||
encoder_name="clip_l",
|
||||
)
|
||||
negative_prompt_embed, negative_pooled_prompt_embed = self._duplicate_text_embed(
|
||||
prompt_embed=clip_l_negative_prompt_embed.clone(),
|
||||
pooled_prompt_embed=clip_l_negative_pooled_embed.clone(),
|
||||
batch_size=self.batch_size,
|
||||
num_images_per_prompt=num_images_per_prompt,
|
||||
)
|
||||
|
||||
clip_g_negative_prompt_embed, clip_g_negative_pooled_embed = self._get_prompt_embed(
|
||||
prompt=negative_prompt,
|
||||
encoder_name="clip_g",
|
||||
)
|
||||
negative_prompt_2_embed, negative_pooled_prompt_2_embed = self._duplicate_text_embed(
|
||||
prompt_embed=clip_g_negative_prompt_embed.clone(),
|
||||
pooled_prompt_embed=clip_g_negative_pooled_embed.clone(),
|
||||
batch_size=self.batch_size,
|
||||
num_images_per_prompt=num_images_per_prompt,
|
||||
)
|
||||
|
||||
_, t5_negative_prompt_embed = self._get_prompt_embed(
|
||||
prompt=negative_prompt,
|
||||
encoder_name="t5",
|
||||
)
|
||||
|
||||
t5_negative_prompt_embed, _ = self._duplicate_text_embed(
|
||||
prompt_embed=t5_negative_prompt_embed.clone(),
|
||||
batch_size=self.batch_size,
|
||||
num_images_per_prompt=num_images_per_prompt,
|
||||
)
|
||||
|
||||
negative_clip_prompt_embeds = torch.cat([negative_prompt_embed, negative_prompt_2_embed], dim=-1)
|
||||
negative_clip_prompt_embeds = torch.nn.functional.pad(
|
||||
negative_clip_prompt_embeds,
|
||||
(0, t5_negative_prompt_embed.shape[-1] - negative_clip_prompt_embeds.shape[-1]),
|
||||
)
|
||||
negative_prompt_embeds = torch.cat([negative_clip_prompt_embeds, t5_negative_prompt_embed], dim=-2)
|
||||
negative_pooled_prompt_embeds = torch.cat(
|
||||
[negative_pooled_prompt_embed, negative_pooled_prompt_2_embed], dim=-1
|
||||
)
|
||||
|
||||
return prompt_embeds, negative_prompt_embeds, pooled_prompt_embeds, negative_pooled_prompt_embeds
|
||||
|
||||
@staticmethod
|
||||
def initialize_latents(
|
||||
batch_size: int,
|
||||
num_channels_latents: int,
|
||||
latent_height: int,
|
||||
latent_width: int,
|
||||
device: torch.device,
|
||||
generator: torch.Generator,
|
||||
dtype=torch.float32,
|
||||
layout=torch.strided,
|
||||
) -> torch.Tensor:
|
||||
latents_shape = (batch_size, num_channels_latents, latent_height, latent_width)
|
||||
latents = torch.randn(
|
||||
latents_shape,
|
||||
dtype=dtype,
|
||||
device="cuda",
|
||||
generator=generator,
|
||||
layout=layout,
|
||||
).to(device)
|
||||
return latents
|
||||
|
||||
# Copied from diffusers.pipelines.stable_diffusion.pipeline_stable_diffusion.retrieve_timesteps
|
||||
@staticmethod
|
||||
def retrieve_timesteps(
|
||||
scheduler,
|
||||
num_inference_steps: int | None = None,
|
||||
device: str | torch.device | None = None,
|
||||
timesteps: list[int] | None = None,
|
||||
sigmas: list[float] | None = None,
|
||||
**kwargs,
|
||||
):
|
||||
r"""
|
||||
Calls the scheduler's `set_timesteps` method and retrieves timesteps from the scheduler after the call. Handles
|
||||
custom timesteps. Any kwargs will be supplied to `scheduler.set_timesteps`.
|
||||
|
||||
Args:
|
||||
scheduler (`SchedulerMixin`):
|
||||
The scheduler to get timesteps from.
|
||||
num_inference_steps (`int`):
|
||||
The number of diffusion steps used when generating samples with a pre-trained model. If used, `timesteps`
|
||||
must be `None`.
|
||||
device (`str` or `torch.device`, *optional*):
|
||||
The device to which the timesteps should be moved to. If `None`, the timesteps are not moved.
|
||||
timesteps (`List[int]`, *optional*):
|
||||
Custom timesteps used to override the timestep spacing strategy of the scheduler. If `timesteps` is passed,
|
||||
`num_inference_steps` and `sigmas` must be `None`.
|
||||
sigmas (`List[float]`, *optional*):
|
||||
Custom sigmas used to override the timestep spacing strategy of the scheduler. If `sigmas` is passed,
|
||||
`num_inference_steps` and `timesteps` must be `None`.
|
||||
|
||||
Returns:
|
||||
`Tuple[torch.Tensor, int]`: A tuple where the first element is the timestep schedule from the scheduler and the
|
||||
second element is the number of inference steps.
|
||||
"""
|
||||
if timesteps is not None and sigmas is not None:
|
||||
raise ValueError(
|
||||
"Only one of `timesteps` or `sigmas` can be passed. Please choose one to set custom values"
|
||||
)
|
||||
if timesteps is not None:
|
||||
accepts_timesteps = "timesteps" in set(inspect.signature(scheduler.set_timesteps).parameters.keys())
|
||||
if not accepts_timesteps:
|
||||
raise ValueError(
|
||||
f"The current scheduler class {scheduler.__class__}'s `set_timesteps` does not support custom"
|
||||
f" timestep schedules. Please check whether you are using the correct scheduler."
|
||||
)
|
||||
scheduler.set_timesteps(timesteps=timesteps, device=device, **kwargs)
|
||||
timesteps = scheduler.timesteps
|
||||
num_inference_steps = len(timesteps)
|
||||
elif sigmas is not None:
|
||||
accept_sigmas = "sigmas" in set(inspect.signature(scheduler.set_timesteps).parameters.keys())
|
||||
if not accept_sigmas:
|
||||
raise ValueError(
|
||||
f"The current scheduler class {scheduler.__class__}'s `set_timesteps` does not support custom"
|
||||
f" sigmas schedules. Please check whether you are using the correct scheduler."
|
||||
)
|
||||
scheduler.set_timesteps(sigmas=sigmas, device=device, **kwargs)
|
||||
timesteps = scheduler.timesteps
|
||||
num_inference_steps = len(timesteps)
|
||||
else:
|
||||
scheduler.set_timesteps(num_inference_steps, device=device, **kwargs)
|
||||
timesteps = scheduler.timesteps
|
||||
return timesteps, num_inference_steps
|
||||
|
||||
def get_control_block_samples(self, params_controlnet, controlnet_name="controlnet"):
|
||||
# Predict the controlnet block samples
|
||||
if self.torch_inference or self.torch_fallback[controlnet_name]:
|
||||
block_samples = self.torch_models[controlnet_name](**params_controlnet)
|
||||
else:
|
||||
block_samples = self.run_engine(controlnet_name, params_controlnet)["controlnet_block_samples"].clone()
|
||||
|
||||
return block_samples
|
||||
|
||||
def denoise_latents(
|
||||
self,
|
||||
latents: torch.Tensor,
|
||||
prompt_embeds: torch.Tensor,
|
||||
pooled_prompt_embeds: torch.Tensor,
|
||||
timesteps: torch.FloatTensor,
|
||||
guidance_scale: float,
|
||||
denoiser="transformer",
|
||||
control_image=None,
|
||||
controlnet_scale=None,
|
||||
controlnet_keep=None,
|
||||
) -> torch.Tensor:
|
||||
do_autocast = self.torch_inference != "" and self.models[denoiser].fp16
|
||||
with torch.autocast("cuda", enabled=do_autocast):
|
||||
self.profile_start(denoiser, color="blue")
|
||||
|
||||
for step_index, timestep in enumerate(timesteps):
|
||||
# expand the latents as we are doing classifier free guidance
|
||||
latents_model_input = torch.cat([latents] * 2) if self.do_classifier_free_guidance else latents
|
||||
# broadcast to batch dimension in a way that's compatible with ONNX/Core ML
|
||||
timestep_inp = timestep.expand(latents_model_input.shape[0])
|
||||
|
||||
controlnet_name = f"controlnet_{self.controlnet}"
|
||||
if control_image is not None:
|
||||
cond_scale = controlnet_scale * controlnet_keep[step_index]
|
||||
|
||||
cast_to = (
|
||||
torch.float16
|
||||
if self.models[controlnet_name].fp16
|
||||
else torch.bfloat16 if self.models[controlnet_name].bf16 else torch.float32
|
||||
)
|
||||
params_controlnet = {
|
||||
"hidden_states": latents_model_input,
|
||||
"timestep": timestep_inp,
|
||||
"pooled_projections": pooled_prompt_embeds,
|
||||
"controlnet_cond": control_image,
|
||||
"conditioning_scale": cond_scale.to(self.device).to(cast_to),
|
||||
}
|
||||
|
||||
control_block_samples = self.get_control_block_samples(params_controlnet, controlnet_name)
|
||||
else:
|
||||
latent_shape = latents_model_input.shape
|
||||
# Initialize control block samples with zeros. Hard-coding some dimensions that can only be queried if a controlnet is used.
|
||||
control_block_samples = torch.zeros(
|
||||
self.models["transformer"].num_controlnet_layers,
|
||||
latent_shape[0], # batch size
|
||||
latent_shape[2] // 2 * latent_shape[3] // 2,
|
||||
self.models["transformer"].config["num_attention_heads"]
|
||||
* self.models["transformer"].config["attention_head_dim"],
|
||||
dtype=latents.dtype,
|
||||
device=latents.device,
|
||||
)
|
||||
params = {
|
||||
"hidden_states": latents_model_input,
|
||||
"timestep": timestep_inp,
|
||||
"encoder_hidden_states": prompt_embeds,
|
||||
"pooled_projections": pooled_prompt_embeds,
|
||||
"block_controlnet_hidden_states": control_block_samples,
|
||||
}
|
||||
|
||||
# Predict the noise residual
|
||||
if self.torch_inference or self.torch_fallback[denoiser]:
|
||||
noise_pred = self.torch_models[denoiser](**params)["sample"]
|
||||
else:
|
||||
noise_pred = self.run_engine(denoiser, params)["latent"]
|
||||
|
||||
# perform guidance
|
||||
if self.do_classifier_free_guidance:
|
||||
noise_pred_uncond, noise_pred_text = noise_pred.chunk(2)
|
||||
noise_pred = noise_pred_uncond + guidance_scale * (noise_pred_text - noise_pred_uncond)
|
||||
|
||||
# compute the previous noisy sample x_t -> x_t-1
|
||||
latents = self.scheduler.step(noise_pred, timestep, latents, return_dict=False)[0]
|
||||
|
||||
self.profile_stop(denoiser)
|
||||
return latents
|
||||
|
||||
def prepare_image(
|
||||
self,
|
||||
image,
|
||||
width,
|
||||
height,
|
||||
device,
|
||||
dtype,
|
||||
):
|
||||
image = self.image_processor.preprocess(image, height=height, width=width)
|
||||
|
||||
image = image.to(device=device, dtype=dtype)
|
||||
|
||||
if self.do_classifier_free_guidance:
|
||||
image = torch.cat([image] * 2)
|
||||
|
||||
return image
|
||||
|
||||
def encode_image(self, input_image, model_name="vae_encoder"):
|
||||
self.profile_start(model_name, color="red")
|
||||
cast_to = (
|
||||
torch.float16
|
||||
if self.models[model_name].fp16
|
||||
else torch.bfloat16 if self.models[model_name].bf16 else torch.float32
|
||||
)
|
||||
input_image = input_image.to(dtype=cast_to)
|
||||
if self.torch_inference or self.torch_fallback[model_name]:
|
||||
image_latents = self.torch_models[model_name](input_image)
|
||||
else:
|
||||
image_latents = self.run_engine(model_name, {"images": input_image})["latent"]
|
||||
image_latents = (image_latents - self.models["vae"].config["shift_factor"]) * self.models[
|
||||
"vae"
|
||||
].config["scaling_factor"]
|
||||
self.profile_stop(model_name)
|
||||
return image_latents
|
||||
|
||||
def decode_latents(self, latents: torch.Tensor, decoder="vae") -> torch.Tensor:
|
||||
cast_to = (
|
||||
torch.float16
|
||||
if self.models[decoder].fp16
|
||||
else torch.bfloat16
|
||||
if self.models[decoder].bf16
|
||||
else torch.float32
|
||||
)
|
||||
latents = latents.to(dtype=cast_to)
|
||||
self.profile_start(decoder, color="red")
|
||||
if self.torch_inference or self.torch_fallback[decoder]:
|
||||
images = self.torch_models[decoder](latents, return_dict=False)[0]
|
||||
else:
|
||||
images = self.run_engine(decoder, {"latent": latents})["images"]
|
||||
self.profile_stop(decoder)
|
||||
return images
|
||||
|
||||
def infer(
|
||||
self,
|
||||
prompt: list[str],
|
||||
negative_prompt: list[str],
|
||||
image_height: int,
|
||||
image_width: int,
|
||||
control_image=None,
|
||||
controlnet_scale=None,
|
||||
control_guidance_start: Union[float, List[float]] = 0.0,
|
||||
control_guidance_end: Union[float, List[float]] = 1.0,
|
||||
warmup=False,
|
||||
save_image=True,
|
||||
):
|
||||
"""
|
||||
Run the diffusion pipeline.
|
||||
|
||||
Args:
|
||||
prompt (list[str]):
|
||||
The text prompt to guide image generation.
|
||||
negative_prompt (list[str]):
|
||||
The prompt not to guide the image generation.
|
||||
image_height (int):
|
||||
Height (in pixels) of the image to be generated. Must be a multiple of 8.
|
||||
image_width (int):
|
||||
Width (in pixels) of the image to be generated. Must be a multiple of 8.
|
||||
control_image (PIL.Image.Image):
|
||||
The control image to guide the image generation.
|
||||
controlnet_scale (torch.Tensor):
|
||||
A tensor which contains ControlNet scale, essential for multi ControlNet.
|
||||
control_guidance_start (`float` or `List[float]`, *optional*, defaults to 0.0):
|
||||
The percentage of total steps at which the ControlNet starts applying.
|
||||
control_guidance_end (`float` or `List[float]`, *optional*, defaults to 1.0):
|
||||
The percentage of total steps at which the ControlNet stops applying.
|
||||
warmup (bool):
|
||||
Indicate if this is a warmup run.
|
||||
save_image (bool):
|
||||
Save the generated image (if applicable)
|
||||
"""
|
||||
assert len(prompt) == len(negative_prompt)
|
||||
self.batch_size = len(prompt)
|
||||
|
||||
# Spatial dimensions of latent tensor
|
||||
assert image_height % (self.vae_scale_factor * self.patch_size) == 0, (
|
||||
f"image height not supported {image_height}"
|
||||
)
|
||||
assert image_width % (self.vae_scale_factor * self.patch_size) == 0, f"image width not supported {image_width}"
|
||||
latent_height = int(image_height) // self.vae_scale_factor
|
||||
latent_width = int(image_width) // self.vae_scale_factor
|
||||
|
||||
with torch.inference_mode(), trt.Runtime(TRT_LOGGER):
|
||||
torch.cuda.synchronize()
|
||||
e2e_tic = time.perf_counter()
|
||||
|
||||
# 1. encode inputs
|
||||
with self.model_memory_manager(["clip_g", "clip_l", "t5"], low_vram=self.low_vram):
|
||||
(
|
||||
prompt_embeds,
|
||||
negative_prompt_embeds,
|
||||
pooled_prompt_embeds,
|
||||
negative_pooled_prompt_embeds,
|
||||
) = self.encode_prompt(
|
||||
prompt=prompt,
|
||||
negative_prompt=negative_prompt,
|
||||
num_images_per_prompt=1,
|
||||
)
|
||||
# do classifier free guidance
|
||||
if self.do_classifier_free_guidance:
|
||||
prompt_embeds = torch.cat([negative_prompt_embeds, prompt_embeds], dim=0)
|
||||
pooled_prompt_embeds = torch.cat([negative_pooled_prompt_embeds, pooled_prompt_embeds], dim=0)
|
||||
|
||||
# 2. Prepare latent variables
|
||||
num_channels_latents = self.models["transformer"].config["in_channels"]
|
||||
latents = self.initialize_latents(
|
||||
batch_size=self.batch_size,
|
||||
num_channels_latents=num_channels_latents,
|
||||
latent_height=latent_height,
|
||||
latent_width=latent_width,
|
||||
device=prompt_embeds.device,
|
||||
generator=self.generator,
|
||||
dtype=torch.float16 if self.fp16 else torch.bfloat16 if self.bf16 else torch.float32,
|
||||
)
|
||||
|
||||
# 3. Prepare timesteps
|
||||
timesteps, num_inference_steps = self.retrieve_timesteps(
|
||||
scheduler=self.scheduler,
|
||||
num_inference_steps=self.denoising_steps,
|
||||
device=self.device,
|
||||
sigmas=None,
|
||||
)
|
||||
|
||||
# 4. Prepare control image
|
||||
controlnet_keep = []
|
||||
if control_image is not None:
|
||||
# Process controlnet_scales
|
||||
for i in range(len(timesteps)):
|
||||
keeps = [
|
||||
1.0 - float(i / len(timesteps) < s or (i + 1) / len(timesteps) > e)
|
||||
for s, e in zip([control_guidance_start], [control_guidance_end])
|
||||
]
|
||||
controlnet_keep.append(keeps[0])
|
||||
|
||||
control_image = self.prepare_image(
|
||||
image=control_image,
|
||||
width=image_width,
|
||||
height=image_height,
|
||||
device=self.device,
|
||||
dtype=torch.float16 if self.fp16 else torch.bfloat16 if self.bf16 else torch.float32,
|
||||
)
|
||||
|
||||
with self.model_memory_manager(["vae_encoder"], low_vram=self.low_vram):
|
||||
control_image = self.encode_image(control_image)
|
||||
|
||||
# 5. Denoise
|
||||
denoiser_list = ["transformer", f"controlnet_{self.controlnet}"] if self.controlnet else ["transformer"]
|
||||
with self.model_memory_manager(denoiser_list, low_vram=self.low_vram):
|
||||
latents = self.denoise_latents(
|
||||
latents=latents,
|
||||
prompt_embeds=prompt_embeds,
|
||||
pooled_prompt_embeds=pooled_prompt_embeds,
|
||||
timesteps=timesteps,
|
||||
guidance_scale=self.guidance_scale,
|
||||
control_image=control_image,
|
||||
# TODO: support multiple controlnets
|
||||
controlnet_scale=controlnet_scale,
|
||||
controlnet_keep=controlnet_keep,
|
||||
)
|
||||
|
||||
# 6. Decode Latents
|
||||
latents = (latents / self.models["vae"].config["scaling_factor"]) + self.models["vae"].config[
|
||||
"shift_factor"
|
||||
]
|
||||
with self.model_memory_manager(["vae"], low_vram=self.low_vram):
|
||||
images = self.decode_latents(latents)
|
||||
|
||||
torch.cuda.synchronize()
|
||||
e2e_toc = time.perf_counter()
|
||||
|
||||
walltime_ms = (e2e_toc - e2e_tic) * 1000.0
|
||||
if not warmup:
|
||||
self.print_summary(
|
||||
num_inference_steps,
|
||||
walltime_ms,
|
||||
)
|
||||
if save_image:
|
||||
# post-process images
|
||||
images = (
|
||||
((images + 1) * 255 / 2)
|
||||
.clamp(0, 255)
|
||||
.detach()
|
||||
.permute(0, 2, 3, 1)
|
||||
.round()
|
||||
.type(torch.uint8)
|
||||
.cpu()
|
||||
.numpy()
|
||||
)
|
||||
self.save_image(images, self.pipeline_type.name.lower(), prompt, self.seed)
|
||||
|
||||
return images, walltime_ms
|
||||
|
||||
def run(
|
||||
self,
|
||||
prompt: list[str],
|
||||
negative_prompt: list[str],
|
||||
height: int,
|
||||
width: int,
|
||||
batch_count: int,
|
||||
num_warmup_runs: int,
|
||||
use_cuda_graph: bool,
|
||||
**kwargs,
|
||||
):
|
||||
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 ..")
|
||||
for _ in range(num_warmup_runs):
|
||||
self.infer(prompt, negative_prompt, height, width, warmup=True, **kwargs)
|
||||
|
||||
for _ in range(batch_count):
|
||||
print("[I] Running StableDiffusion 3.5 pipeline")
|
||||
if self.nvtx_profile:
|
||||
cudart.cudaProfilerStart()
|
||||
self.infer(prompt, negative_prompt, height, width, warmup=False, **kwargs)
|
||||
if self.nvtx_profile:
|
||||
cudart.cudaProfilerStop()
|
||||
@@ -0,0 +1,599 @@
|
||||
#
|
||||
# SPDX-FileCopyrightText: Copyright (c) 1993-2025 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 math
|
||||
import os
|
||||
import pathlib
|
||||
import time
|
||||
|
||||
import nvtx
|
||||
import tensorrt as trt
|
||||
import torch
|
||||
from cuda.bindings import runtime as cudart
|
||||
|
||||
import demo_diffusion.engine as engine_module
|
||||
import demo_diffusion.image as image_module
|
||||
from demo_diffusion.model import (
|
||||
SD3_CLIPGModel,
|
||||
SD3_CLIPLModel,
|
||||
SD3_MMDiTModel,
|
||||
SD3_T5XXLModel,
|
||||
SD3_VAEDecoderModel,
|
||||
SD3_VAEEncoderModel,
|
||||
get_clip_embedding_dim,
|
||||
)
|
||||
from demo_diffusion.pipeline.type import PIPELINE_TYPE
|
||||
from demo_diffusion.utils_sd3.other_impls import SD3Tokenizer
|
||||
from demo_diffusion.utils_sd3.sd3_impls import SD3LatentFormat, sample_euler
|
||||
|
||||
TRT_LOGGER = trt.Logger(trt.Logger.ERROR)
|
||||
|
||||
|
||||
class StableDiffusion3Pipeline:
|
||||
"""
|
||||
Application showcasing the acceleration of Stable Diffusion 3 pipelines using NVidia TensorRT.
|
||||
"""
|
||||
def __init__(
|
||||
self,
|
||||
version='sd3',
|
||||
pipeline_type=PIPELINE_TYPE.TXT2IMG,
|
||||
max_batch_size=16,
|
||||
shift=1.0,
|
||||
cfg_scale=5,
|
||||
denoising_steps=50,
|
||||
denoising_percentage=0.6,
|
||||
input_image=None,
|
||||
device='cuda',
|
||||
output_dir='.',
|
||||
hf_token=None,
|
||||
verbose=False,
|
||||
nvtx_profile=False,
|
||||
use_cuda_graph=False,
|
||||
framework_model_dir='pytorch_model',
|
||||
torch_inference='',
|
||||
):
|
||||
"""
|
||||
Initializes the Stable Diffusion 3 pipeline.
|
||||
|
||||
Args:
|
||||
version (str):
|
||||
The version of the pipeline. Should be one of ['sd3]
|
||||
pipeline_type (PIPELINE_TYPE):
|
||||
Type of current pipeline.
|
||||
max_batch_size (int):
|
||||
Maximum batch size for dynamic batch engine.
|
||||
shift (float):
|
||||
Shift parameter for MMDiT model. Default: 1.0
|
||||
cfg_scale (int):
|
||||
CFG Scale used for denoising. Default: 5
|
||||
denoising_steps (int):
|
||||
Number of denoising steps. Default: 1.0
|
||||
denoising_percentage (float):
|
||||
Denoising percentage. Default: 0.6
|
||||
input_image (float):
|
||||
Input image for conditioning. Default: None
|
||||
device (str):
|
||||
PyTorch device to run inference. Default: 'cuda'
|
||||
output_dir (str):
|
||||
Output directory for log files and image artifacts
|
||||
hf_token (str):
|
||||
HuggingFace User Access Token to use for downloading Stable Diffusion model checkpoints.
|
||||
verbose (bool):
|
||||
Enable verbose logging.
|
||||
nvtx_profile (bool):
|
||||
Insert NVTX profiling markers.
|
||||
use_cuda_graph (bool):
|
||||
Use CUDA graph to capture engine execution and then launch inference
|
||||
framework_model_dir (str):
|
||||
cache directory for framework checkpoints
|
||||
torch_inference (str):
|
||||
Run inference with PyTorch (using specified compilation mode) instead of TensorRT.
|
||||
"""
|
||||
|
||||
self.max_batch_size = max_batch_size
|
||||
self.shift = shift
|
||||
self.cfg_scale = cfg_scale
|
||||
self.denoising_steps = denoising_steps
|
||||
self.input_image = input_image
|
||||
self.denoising_percentage = denoising_percentage if input_image is not None else 1.0
|
||||
|
||||
self.framework_model_dir = framework_model_dir
|
||||
self.output_dir = output_dir
|
||||
for directory in [self.framework_model_dir, self.output_dir]:
|
||||
if not os.path.exists(directory):
|
||||
print(f"[I] Create directory: {directory}")
|
||||
pathlib.Path(directory).mkdir(parents=True)
|
||||
|
||||
self.hf_token = hf_token
|
||||
self.device = device
|
||||
self.verbose = verbose
|
||||
self.nvtx_profile = nvtx_profile
|
||||
|
||||
self.version = version
|
||||
|
||||
# Pipeline type
|
||||
self.pipeline_type = pipeline_type
|
||||
self.stages = ['clip_g', 'clip_l', 't5xxl', 'transformer', 'vae_decoder']
|
||||
if input_image is not None:
|
||||
self.stages = ['vae_encoder'] + self.stages
|
||||
|
||||
self.config = {}
|
||||
self.config['clip_hidden_states'] = True
|
||||
self.config['t5xxl_torch_fallback'] = True
|
||||
self.config['vae_encoder_torch_fallback'] = True
|
||||
self.torch_inference = torch_inference
|
||||
if self.torch_inference:
|
||||
torch._inductor.config.conv_1x1_as_mm = True
|
||||
torch._inductor.config.coordinate_descent_tuning = True
|
||||
torch._inductor.config.epilogue_fusion = False
|
||||
torch._inductor.config.coordinate_descent_check_all_directions = True
|
||||
self.use_cuda_graph = use_cuda_graph
|
||||
|
||||
# initialized in loadEngines()
|
||||
self.models = {}
|
||||
self.torch_models = {}
|
||||
self.engine = {}
|
||||
self.shared_device_memory = None
|
||||
|
||||
# initialized in loadResources()
|
||||
self.events = {}
|
||||
self.generator = None
|
||||
self.markers = {}
|
||||
self.seed = None
|
||||
self.stream = None
|
||||
self.tokenizer = None
|
||||
|
||||
def loadResources(self, image_height, image_width, batch_size, seed):
|
||||
# Initialize noise generator
|
||||
if seed:
|
||||
self.seed = seed
|
||||
self.generator = torch.Generator(device="cuda").manual_seed(seed)
|
||||
|
||||
# Create CUDA events and stream
|
||||
for stage in self.stages:
|
||||
self.events[stage] = [cudart.cudaEventCreate()[1], cudart.cudaEventCreate()[1]]
|
||||
self.stream = cudart.cudaStreamCreate()[1]
|
||||
|
||||
# Allocate TensorRT I/O buffers
|
||||
if not self.torch_inference:
|
||||
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)
|
||||
|
||||
def teardown(self):
|
||||
for e in self.events.values():
|
||||
cudart.cudaEventDestroy(e[0])
|
||||
cudart.cudaEventDestroy(e[1])
|
||||
|
||||
for engine in self.engine.values():
|
||||
del engine
|
||||
|
||||
if self.shared_device_memory:
|
||||
cudart.cudaFree(self.shared_device_memory)
|
||||
|
||||
cudart.cudaStreamDestroy(self.stream)
|
||||
del self.stream
|
||||
|
||||
def getOnnxPath(self, model_name, onnx_dir, opt=True, suffix=''):
|
||||
onnx_model_dir = os.path.join(onnx_dir, model_name+suffix+('.opt' if opt else ''))
|
||||
os.makedirs(onnx_model_dir, exist_ok=True)
|
||||
return os.path.join(onnx_model_dir, 'model.onnx')
|
||||
|
||||
def getEnginePath(self, model_name, engine_dir, enable_refit=False, suffix=''):
|
||||
return os.path.join(engine_dir, model_name+suffix+('.refit' if enable_refit else '')+'.trt'+trt.__version__+'.plan')
|
||||
|
||||
def loadEngines(
|
||||
self,
|
||||
engine_dir,
|
||||
framework_model_dir,
|
||||
onnx_dir,
|
||||
onnx_opset,
|
||||
opt_batch_size,
|
||||
opt_image_height,
|
||||
opt_image_width,
|
||||
static_batch=False,
|
||||
static_shape=True,
|
||||
enable_all_tactics=False,
|
||||
timing_cache=None,
|
||||
**_kwargs,
|
||||
):
|
||||
"""
|
||||
Build and load engines for TensorRT accelerated inference.
|
||||
Export ONNX models first, if applicable.
|
||||
|
||||
Args:
|
||||
engine_dir (str):
|
||||
Directory to store the TensorRT engines.
|
||||
framework_model_dir (str):
|
||||
Directory to store the framework model ckpt.
|
||||
onnx_dir (str):
|
||||
Directory to store the ONNX models.
|
||||
onnx_opset (int):
|
||||
ONNX opset version to export the models.
|
||||
opt_batch_size (int):
|
||||
Batch size to optimize for during engine building.
|
||||
opt_image_height (int):
|
||||
Image height to optimize for during engine building. Must be a multiple of 8.
|
||||
opt_image_width (int):
|
||||
Image width to optimize for during engine building. Must be a multiple of 8.
|
||||
static_batch (bool):
|
||||
Build engine only for specified opt_batch_size.
|
||||
static_shape (bool):
|
||||
Build engine only for specified opt_image_height & opt_image_width. Default = True.
|
||||
enable_all_tactics (bool):
|
||||
Enable all tactic sources during TensorRT engine builds.
|
||||
timing_cache (str):
|
||||
Path to the timing cache to speed up TensorRT build.
|
||||
"""
|
||||
# Create directories if missing
|
||||
for directory in [engine_dir, onnx_dir]:
|
||||
if not os.path.exists(directory):
|
||||
print(f"[I] Create directory: {directory}")
|
||||
pathlib.Path(directory).mkdir(parents=True)
|
||||
|
||||
# Load pipeline models
|
||||
models_args = {'version': self.version, 'pipeline': self.pipeline_type, 'device': self.device,
|
||||
'hf_token': self.hf_token, 'verbose': self.verbose, 'framework_model_dir': framework_model_dir,
|
||||
'max_batch_size': self.max_batch_size}
|
||||
|
||||
# Load text tokenizer
|
||||
self.tokenizer = SD3Tokenizer()
|
||||
|
||||
# Load text encoders
|
||||
if 'clip_g' in self.stages:
|
||||
self.models['clip_g'] = SD3_CLIPGModel(**models_args, fp16=True, pooled_output=True)
|
||||
|
||||
if 'clip_l' in self.stages:
|
||||
self.models['clip_l'] = SD3_CLIPLModel(**models_args, fp16=True, pooled_output=True)
|
||||
|
||||
if 't5xxl' in self.stages:
|
||||
self.models['t5xxl'] = SD3_T5XXLModel(**models_args, fp16=True, embedding_dim=get_clip_embedding_dim(self.version, self.pipeline_type))
|
||||
|
||||
# Load Transformer model
|
||||
if 'transformer' in self.stages:
|
||||
self.models['transformer'] = SD3_MMDiTModel(**models_args, fp16=True, shift=self.shift)
|
||||
|
||||
# Load VAE Encoder model
|
||||
if 'vae_encoder' in self.stages:
|
||||
self.models['vae_encoder'] = SD3_VAEEncoderModel(**models_args, fp16=True)
|
||||
|
||||
# Load VAE Decoder model
|
||||
if 'vae_decoder' in self.stages:
|
||||
self.models['vae_decoder'] = SD3_VAEDecoderModel(**models_args, fp16=True)
|
||||
|
||||
# Configure pipeline models to load
|
||||
model_names = self.models.keys()
|
||||
# Torch fallback
|
||||
self.torch_fallback = dict(zip(model_names, [self.torch_inference or self.config.get(model_name.replace('-','_')+'_torch_fallback', False) for model_name in model_names]))
|
||||
|
||||
onnx_path = dict(zip(model_names, [self.getOnnxPath(model_name, onnx_dir, opt=False) for model_name in model_names]))
|
||||
onnx_opt_path = dict(zip(model_names, [self.getOnnxPath(model_name, onnx_dir) for model_name in model_names]))
|
||||
engine_path = dict(zip(model_names, [self.getEnginePath(model_name, engine_dir) for model_name in model_names]))
|
||||
|
||||
for model_name, obj in self.models.items():
|
||||
if self.torch_fallback[model_name]:
|
||||
continue
|
||||
# Export models to ONNX
|
||||
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, static_shape=static_shape)
|
||||
|
||||
# Build TensorRT engines
|
||||
for model_name, obj in self.models.items():
|
||||
if self.torch_fallback[model_name]:
|
||||
continue
|
||||
engine = engine_module.Engine(engine_path[model_name])
|
||||
if not os.path.exists(engine_path[model_name]):
|
||||
update_output_names = obj.get_output_names() + obj.extra_output_names if obj.extra_output_names else None
|
||||
extra_build_args = {'verbose': self.verbose}
|
||||
engine.build(onnx_opt_path[model_name],
|
||||
input_profile=obj.get_input_profile(
|
||||
opt_batch_size, opt_image_height, opt_image_width,
|
||||
static_batch=static_batch, static_shape=static_shape
|
||||
),
|
||||
enable_all_tactics=enable_all_tactics,
|
||||
timing_cache=timing_cache,
|
||||
update_output_names=update_output_names,
|
||||
verbose=self.verbose
|
||||
)
|
||||
self.engine[model_name] = engine
|
||||
|
||||
# Load TensorRT engines
|
||||
for model_name, obj in self.models.items():
|
||||
if self.torch_fallback[model_name]:
|
||||
continue
|
||||
self.engine[model_name].load()
|
||||
|
||||
# Load torch models
|
||||
for model_name, obj in self.models.items():
|
||||
if self.torch_fallback[model_name] or model_name == 'transformer':
|
||||
self.torch_models[model_name] = obj.get_model(torch_inference=self.torch_inference)
|
||||
|
||||
def calculateMaxDeviceMemory(self):
|
||||
max_device_memory = 0
|
||||
for model_name, engine in self.engine.items():
|
||||
max_device_memory = max(max_device_memory, engine.engine.device_memory_size_v2)
|
||||
return max_device_memory
|
||||
|
||||
def activateEngines(self, shared_device_memory=None):
|
||||
if shared_device_memory is None:
|
||||
max_device_memory = self.calculateMaxDeviceMemory()
|
||||
_, 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)
|
||||
|
||||
def runEngine(self, model_name, feed_dict):
|
||||
engine = self.engine[model_name]
|
||||
return engine.infer(feed_dict, self.stream, use_cuda_graph=self.use_cuda_graph)
|
||||
|
||||
def initialize_latents(self, batch_size, unet_channels, latent_height, latent_width):
|
||||
return torch.ones(batch_size, unet_channels, latent_height, latent_width, device="cuda") * 0.0609
|
||||
|
||||
def profile_start(self, name, color='blue'):
|
||||
if self.nvtx_profile:
|
||||
self.markers[name] = nvtx.start_range(message=name, color=color)
|
||||
if name in self.events:
|
||||
cudart.cudaEventRecord(self.events[name][0], 0)
|
||||
|
||||
def profile_stop(self, name):
|
||||
if name in self.events:
|
||||
cudart.cudaEventRecord(self.events[name][1], 0)
|
||||
if self.nvtx_profile:
|
||||
nvtx.end_range(self.markers[name])
|
||||
|
||||
def print_summary(self, denoising_steps, walltime_ms, batch_size):
|
||||
print('|-----------------|--------------|')
|
||||
print('| {:^15} | {:^12} |'.format('Module', 'Latency'))
|
||||
print('|-----------------|--------------|')
|
||||
for stage in self.stages:
|
||||
stage_name = stage
|
||||
if "transformer" in stage:
|
||||
stage_name += ' x ' + str(denoising_steps)
|
||||
print(
|
||||
"| {:^15} | {:>9.2f} ms |".format(
|
||||
stage_name, cudart.cudaEventElapsedTime(self.events[stage][0], self.events[stage][1])[1],
|
||||
)
|
||||
)
|
||||
print('|-----------------|--------------|')
|
||||
print('| {:^15} | {:>9.2f} ms |'.format('Pipeline', walltime_ms))
|
||||
print('|-----------------|--------------|')
|
||||
print('Throughput: {:.5f} image/s'.format(batch_size*1000./walltime_ms))
|
||||
|
||||
def save_image(self, images, pipeline, prompt, seed):
|
||||
# Save image
|
||||
image_name_prefix = pipeline+''.join(set(['-'+prompt[i].replace(' ','_')[:10] for i in range(len(prompt))]))+'-'+str(seed)+'-'
|
||||
image_name_suffix = 'torch' if self.torch_inference else 'trt'
|
||||
image_module.save_image(images, self.output_dir, image_name_prefix, image_name_suffix)
|
||||
|
||||
def encode_prompt(self, prompt, negative_prompt):
|
||||
def encode_token_weights(model_name, token_weight_pairs):
|
||||
self.profile_start(model_name, color='green')
|
||||
|
||||
tokens = list(map(lambda a: a[0], token_weight_pairs[0]))
|
||||
tokens = torch.tensor([tokens], dtype=torch.int64, device=self.device)
|
||||
if self.torch_inference or self.torch_fallback[model_name]:
|
||||
out, pooled = self.torch_models[model_name](tokens)
|
||||
else:
|
||||
trt_out = self.runEngine(model_name, {'input_ids': tokens})
|
||||
out, pooled = trt_out['text_embeddings'], trt_out["pooled_output"]
|
||||
|
||||
self.profile_stop(model_name)
|
||||
|
||||
if pooled is not None:
|
||||
first_pooled = pooled[0:1].cuda()
|
||||
else:
|
||||
first_pooled = pooled
|
||||
output = [out[0:1]]
|
||||
return torch.cat(output, dim=-2).cuda(), first_pooled
|
||||
|
||||
def tokenize(prompt):
|
||||
tokens = self.tokenizer.tokenize_with_weights(prompt)
|
||||
l_out, l_pooled = encode_token_weights('clip_l', tokens["l"])
|
||||
g_out, g_pooled = encode_token_weights('clip_g', tokens["g"])
|
||||
t5_out, _ = encode_token_weights('t5xxl', tokens["t5xxl"])
|
||||
lg_out = torch.cat([l_out, g_out], dim=-1)
|
||||
lg_out = torch.nn.functional.pad(lg_out, (0, 4096 - lg_out.shape[-1]))
|
||||
|
||||
return torch.cat([lg_out, t5_out], dim=-2), torch.cat((l_pooled, g_pooled), dim=-1)
|
||||
|
||||
conditioning = tokenize(prompt[0])
|
||||
neg_conditioning = tokenize(negative_prompt[0])
|
||||
return conditioning, neg_conditioning
|
||||
|
||||
def denoise_latent(self, latent, conditioning, neg_conditioning, model_name='transformer'):
|
||||
def get_noise(latent):
|
||||
return torch.randn(latent.size(), dtype=torch.float32, layout=latent.layout, generator=self.generator, device="cuda").to(latent.dtype)
|
||||
|
||||
def get_sigmas(sampling, steps):
|
||||
start = sampling.timestep(sampling.sigma_max)
|
||||
end = sampling.timestep(sampling.sigma_min)
|
||||
timesteps = torch.linspace(start, end, steps)
|
||||
sigs = []
|
||||
for x in range(len(timesteps)):
|
||||
ts = timesteps[x]
|
||||
sigs.append(sampling.sigma(ts))
|
||||
sigs += [0.0]
|
||||
return torch.FloatTensor(sigs)
|
||||
|
||||
def max_denoise(sigmas):
|
||||
max_sigma = float(self.torch_models[model_name].model_sampling.sigma_max)
|
||||
sigma = float(sigmas[0])
|
||||
return math.isclose(max_sigma, sigma, rel_tol=1e-05) or sigma > max_sigma
|
||||
|
||||
def fix_cond(cond):
|
||||
cond, pooled = (cond[0].half().cuda(), cond[1].half().cuda())
|
||||
return { "c_crossattn": cond, "y": pooled }
|
||||
|
||||
def cfg_denoiser(x, timestep, cond, uncond, cond_scale):
|
||||
# Run cond and uncond in a batch together
|
||||
sample = torch.cat([x, x])
|
||||
sigma = torch.cat([timestep, timestep])
|
||||
c_crossattn = torch.cat([cond["c_crossattn"], uncond["c_crossattn"]])
|
||||
y = torch.cat([cond["y"], uncond["y"]])
|
||||
if self.torch_inference or self.torch_fallback[model_name]:
|
||||
with torch.autocast("cuda", dtype=torch.float16):
|
||||
batched = self.torch_models[model_name](sample, sigma, c_crossattn=c_crossattn, y=y)
|
||||
else:
|
||||
input_dict = {'sample': sample, 'sigma': sigma, 'c_crossattn': c_crossattn, 'y': y}
|
||||
batched = self.runEngine(model_name, input_dict)['latent']
|
||||
|
||||
# Then split and apply CFG Scaling
|
||||
pos_out, neg_out = batched.chunk(2)
|
||||
scaled = neg_out + (pos_out - neg_out) * cond_scale
|
||||
return scaled
|
||||
|
||||
self.profile_start(model_name, color='blue')
|
||||
|
||||
latent = latent.half().cuda()
|
||||
noise = get_noise(latent).cuda()
|
||||
sigmas = get_sigmas(self.torch_models[model_name].model_sampling, self.denoising_steps).cuda()
|
||||
sigmas = sigmas[int(self.denoising_steps * (1 - self.denoising_percentage)):]
|
||||
conditioning = fix_cond(conditioning)
|
||||
neg_conditioning = fix_cond(neg_conditioning)
|
||||
|
||||
noise_scaled = self.torch_models[model_name].model_sampling.noise_scaling(sigmas[0], noise, latent, max_denoise(sigmas))
|
||||
extra_args = { "cond": conditioning, "uncond": neg_conditioning, "cond_scale": self.cfg_scale }
|
||||
latent = sample_euler(cfg_denoiser, noise_scaled, sigmas, extra_args=extra_args)
|
||||
latent = SD3LatentFormat().process_out(latent)
|
||||
|
||||
self.profile_stop(model_name)
|
||||
|
||||
return latent
|
||||
|
||||
def encode_image(self, model_name='vae_encoder'):
|
||||
self.input_image = self.input_image.to(self.device)
|
||||
self.profile_start(model_name, color='orange')
|
||||
if self.torch_inference or self.torch_fallback[model_name]:
|
||||
with torch.autocast("cuda", dtype=torch.float16):
|
||||
latent = self.torch_models[model_name](self.input_image)
|
||||
else:
|
||||
latent = self.runEngine(model_name, {'images': self.input_image})['latent']
|
||||
|
||||
latent = SD3LatentFormat().process_in(latent)
|
||||
self.profile_stop(model_name)
|
||||
return latent
|
||||
|
||||
def decode_latent(self, latent, model_name='vae_decoder'):
|
||||
self.profile_start(model_name, color='red')
|
||||
if self.torch_inference or self.torch_fallback[model_name]:
|
||||
with torch.autocast("cuda", dtype=torch.float16):
|
||||
image = self.torch_models[model_name](latent)
|
||||
else:
|
||||
image = self.runEngine(model_name, {'latent': latent})['images']
|
||||
image = image.float()
|
||||
self.profile_stop(model_name)
|
||||
return image
|
||||
|
||||
def infer(
|
||||
self,
|
||||
prompt,
|
||||
negative_prompt,
|
||||
image_height,
|
||||
image_width,
|
||||
warmup=False,
|
||||
save_image=True,
|
||||
):
|
||||
"""
|
||||
Run the diffusion pipeline.
|
||||
|
||||
Args:
|
||||
prompt (str):
|
||||
The text prompt to guide image generation.
|
||||
negative_prompt (str):
|
||||
The prompt not to guide the image generation.
|
||||
image_height (int):
|
||||
Height (in pixels) of the image to be generated. Must be a multiple of 8.
|
||||
image_width (int):
|
||||
Width (in pixels) of the image to be generated. Must be a multiple of 8.
|
||||
warmup (bool):
|
||||
Indicate if this is a warmup run.
|
||||
save_image (bool):
|
||||
Save the generated image (if applicable)
|
||||
"""
|
||||
assert len(prompt) == len(negative_prompt)
|
||||
batch_size = len(prompt)
|
||||
|
||||
# Spatial dimensions of latent tensor
|
||||
latent_height = image_height // 8
|
||||
latent_width = image_width // 8
|
||||
|
||||
if self.generator and self.seed:
|
||||
self.generator.manual_seed(self.seed)
|
||||
|
||||
with torch.inference_mode(), trt.Runtime(TRT_LOGGER):
|
||||
torch.cuda.synchronize()
|
||||
e2e_tic = time.perf_counter()
|
||||
|
||||
# Initialize Latents
|
||||
latent = self.initialize_latents(batch_size=batch_size,
|
||||
unet_channels=16,
|
||||
latent_height=latent_height,
|
||||
latent_width=latent_width)
|
||||
|
||||
# Encode input image
|
||||
if self.input_image is not None:
|
||||
latent = self.encode_image()
|
||||
|
||||
# Get Conditionings
|
||||
conditioning, neg_conditioning = self.encode_prompt(prompt, negative_prompt)
|
||||
|
||||
# Denoise
|
||||
latent = self.denoise_latent(latent, conditioning, neg_conditioning)
|
||||
|
||||
# Decode Latents
|
||||
images = self.decode_latent(latent)
|
||||
|
||||
torch.cuda.synchronize()
|
||||
e2e_toc = time.perf_counter()
|
||||
|
||||
walltime_ms = (e2e_toc - e2e_tic) * 1000.
|
||||
if not warmup:
|
||||
num_inference_steps = int(self.denoising_steps * self.denoising_percentage)
|
||||
self.print_summary(num_inference_steps, walltime_ms, batch_size)
|
||||
if save_image:
|
||||
# post-process images
|
||||
images = ((images + 1) * 255 / 2).clamp(0, 255).detach().permute(0, 2, 3, 1).round().type(torch.uint8).cpu().numpy()
|
||||
self.save_image(images, self.pipeline_type.name.lower(), prompt, self.seed)
|
||||
|
||||
return images, walltime_ms
|
||||
|
||||
def run(self, prompt, negative_prompt, height, width, batch_size, batch_count, num_warmup_runs, use_cuda_graph, **kwargs):
|
||||
# Process prompt
|
||||
if not isinstance(prompt, list):
|
||||
raise ValueError(f"`prompt` must be of type `str` list, but is {type(prompt)}")
|
||||
prompt = prompt * batch_size
|
||||
|
||||
if not isinstance(negative_prompt, list):
|
||||
raise ValueError(f"`--negative-prompt` must be of type `str` list, but is {type(negative_prompt)}")
|
||||
if len(negative_prompt) == 1:
|
||||
negative_prompt = negative_prompt * batch_size
|
||||
|
||||
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 ..")
|
||||
for _ in range(num_warmup_runs):
|
||||
self.infer(prompt, negative_prompt, height, width, warmup=True, **kwargs)
|
||||
|
||||
for _ in range(batch_count):
|
||||
print("[I] Running StableDiffusion3 pipeline")
|
||||
if self.nvtx_profile:
|
||||
cudart.cudaProfilerStart()
|
||||
self.infer(prompt, negative_prompt, height, width, warmup=False, **kwargs)
|
||||
if self.nvtx_profile:
|
||||
cudart.cudaProfilerStop()
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,751 @@
|
||||
#
|
||||
# Copyright 2024 The HuggingFace Inc. team.
|
||||
# SPDX-FileCopyrightText: Copyright (c) 1993-2025 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 os
|
||||
import pathlib
|
||||
import random
|
||||
import sys
|
||||
import time
|
||||
from typing import Optional
|
||||
|
||||
import modelopt.torch.opt as mto
|
||||
import modelopt.torch.quantization as mtq
|
||||
import tensorrt as trt
|
||||
import torch
|
||||
from cuda.bindings import runtime as cudart
|
||||
from diffusers.image_processor import VaeImageProcessor
|
||||
from diffusers.utils.torch_utils import randn_tensor
|
||||
from tqdm.auto import tqdm
|
||||
|
||||
import demo_diffusion.engine as engine_module
|
||||
import demo_diffusion.image as image_module
|
||||
from demo_diffusion.model import (
|
||||
CLIPImageProcessorModel,
|
||||
CLIPVisionWithProjModel,
|
||||
UNetTemporalModel,
|
||||
VAEDecTemporalModel,
|
||||
)
|
||||
from demo_diffusion.pipeline.calibrate import load_calibration_images
|
||||
from demo_diffusion.pipeline.stable_diffusion_pipeline import StableDiffusionPipeline
|
||||
from demo_diffusion.pipeline.type import PIPELINE_TYPE
|
||||
from demo_diffusion.utils_modelopt import (
|
||||
SD_FP8_FP16_DEFAULT_CONFIG,
|
||||
check_lora,
|
||||
filter_func,
|
||||
generate_fp8_scales,
|
||||
quantize_lvl,
|
||||
)
|
||||
|
||||
TRT_LOGGER = trt.Logger(trt.Logger.ERROR)
|
||||
|
||||
|
||||
def _GiB(val):
|
||||
return val * 1 << 30
|
||||
|
||||
|
||||
def _append_dims(x, target_dims):
|
||||
"""Appends dimensions to the end of a tensor until it has target_dims dimensions."""
|
||||
dims_to_append = target_dims - x.ndim
|
||||
if dims_to_append < 0:
|
||||
raise ValueError(f"input has {x.ndim} dims but target_dims is {target_dims}, which is less")
|
||||
return x[(...,) + (None,) * dims_to_append]
|
||||
|
||||
|
||||
class StableVideoDiffusionPipeline(StableDiffusionPipeline):
|
||||
"""
|
||||
Application showcasing the acceleration of Stable Video Diffusion pipelines using NVidia TensorRT.
|
||||
"""
|
||||
def __init__(
|
||||
self,
|
||||
version='svd-xt-1.1',
|
||||
pipeline_type=PIPELINE_TYPE.IMG2VID,
|
||||
min_guidance_scale: float = 1.0,
|
||||
max_guidance_scale: float = 3.0,
|
||||
decode_chunk_size: Optional[int] = None,
|
||||
**kwargs
|
||||
):
|
||||
"""
|
||||
Initializes the Diffusion pipeline.
|
||||
|
||||
Args:
|
||||
version (str):
|
||||
The version of the pipeline. Should be one of [svd-xt-1.1]
|
||||
pipeline_type (PIPELINE_TYPE):
|
||||
Type of current pipeline.
|
||||
min_guidance_scale (`float`, *optional*, defaults to 1.0):
|
||||
The minimum guidance scale. Used for the classifier free guidance with first frame.
|
||||
max_guidance_scale (`float`, *optional*, defaults to 3.0):
|
||||
The maximum guidance scale. Used for the classifier free guidance with last frame.
|
||||
`max_guidance_scale = 1` corresponds to doing no classifier free guidance.
|
||||
decode_chunk_size (`int`, *optional*):
|
||||
The number of frames to decode at a time. The higher the chunk size, the higher the temporal consistency
|
||||
between frames, but also the higher the memory consumption. By default, the decoder will decode all frames at once
|
||||
for maximal quality. Reduce `decode_chunk_size` to reduce memory usage.
|
||||
"""
|
||||
super().__init__(
|
||||
version=version,
|
||||
pipeline_type=pipeline_type,
|
||||
**kwargs
|
||||
)
|
||||
self.min_guidance_scale = min_guidance_scale
|
||||
self.max_guidance_scale = max_guidance_scale
|
||||
self.do_classifier_free_guidance = max_guidance_scale > 1
|
||||
# FIXME vae_scale_factor = 2 ** (len(self.vae.config.block_out_channels) - 1)
|
||||
self.vae_scale_factor = 8
|
||||
# FIXME num_frames = self.config.num_frames
|
||||
select_num_frames = {
|
||||
'svd-xt-1.1': 25,
|
||||
}
|
||||
self.num_frames = select_num_frames.get(version, 14)
|
||||
# TODO decode_chunk_size from args
|
||||
self.decode_chunk_size = 8 if not decode_chunk_size else decode_chunk_size
|
||||
# TODO: scaling_factor = vae.config.scaling_factor
|
||||
self.scaling_factor = 0.18215
|
||||
|
||||
# TODO user configurable cuda_device_id
|
||||
cuda_device_id = 0
|
||||
properties = cudart.cudaGetDeviceProperties(cuda_device_id)
|
||||
if properties[0] != 0:
|
||||
total_device_count = cudart.cudaGetDeviceCount()[1]
|
||||
raise ValueError(f"Failed to get device properties for device {cuda_device_id}, total device count: {total_device_count}")
|
||||
vram_size = properties[1].totalGlobalMem
|
||||
self.low_vram = vram_size < _GiB(40)
|
||||
if self.low_vram:
|
||||
print(f"[W] WARNING low VRAM ({vram_size/_GiB(1):.2f} GB) mode selected. Certain optimizations may be skipped.")
|
||||
if self.use_cuda_graph and self.low_vram:
|
||||
print("[W] WARNING CUDA graph disabled in low VRAM mode.")
|
||||
self.use_cuda_graph = False
|
||||
|
||||
self.config = {}
|
||||
if self.pipeline_type.is_img2vid():
|
||||
self.config['clip_vis_torch_fallback'] = True
|
||||
self.config['clip_imgfe_torch_fallback'] = True
|
||||
self.config['vae_temp_torch_fallback'] = True
|
||||
|
||||
# initialized in loadEngines()
|
||||
self.max_shared_device_memory_size = 0
|
||||
|
||||
def loadResources(self, image_height, image_width, batch_size, seed):
|
||||
# Initialize noise generator
|
||||
self.seed = seed
|
||||
self.generator = torch.Generator(device="cuda").manual_seed(seed) if seed else None
|
||||
|
||||
# Create CUDA events and stream
|
||||
for stage in ['clip', 'denoise', 'vae', 'vae_encoder']:
|
||||
self.events[stage] = [cudart.cudaEventCreate()[1], cudart.cudaEventCreate()[1]]
|
||||
self.stream = cudart.cudaStreamCreate()[1]
|
||||
|
||||
# Allocate shared device memory for TensorRT engines
|
||||
if not self.low_vram and not self.torch_inference:
|
||||
for model_name in self.models.keys():
|
||||
if not self.torch_fallback[model_name]:
|
||||
self.max_shared_device_memory_size = max(self.max_shared_device_memory_size, self.engine[model_name].engine.device_memory_size_v2)
|
||||
self.shared_device_memory = cudart.cudaMalloc(self.max_shared_device_memory_size)[1]
|
||||
# Activate TensorRT engines
|
||||
for model_name in self.models.keys():
|
||||
if not self.torch_fallback[model_name]:
|
||||
self.engine[model_name].activate(device_memory=self.shared_device_memory)
|
||||
alloc_shape = self.models[model_name].get_shape_dict(batch_size, image_height, image_width)
|
||||
self.engine[model_name].allocate_buffers(shape_dict=alloc_shape, device=self.device)
|
||||
|
||||
def loadEngines(
|
||||
self,
|
||||
engine_dir,
|
||||
framework_model_dir,
|
||||
onnx_dir,
|
||||
onnx_opset,
|
||||
opt_batch_size,
|
||||
opt_image_height,
|
||||
opt_image_width,
|
||||
static_batch=False,
|
||||
static_shape=True,
|
||||
enable_refit=False,
|
||||
enable_all_tactics=False,
|
||||
timing_cache=None,
|
||||
fp8=False,
|
||||
quantization_level=0.0,
|
||||
calibration_size=32,
|
||||
calib_batch_size=2,
|
||||
**_kwargs,
|
||||
):
|
||||
"""
|
||||
Build and load engines for TensorRT accelerated inference.
|
||||
Export ONNX models first, if applicable.
|
||||
|
||||
Args:
|
||||
engine_dir (str):
|
||||
Directory to store the TensorRT engines.
|
||||
framework_model_dir (str):
|
||||
Directory to store the framework model ckpt.
|
||||
onnx_dir (str):
|
||||
Directory to store the ONNX models.
|
||||
onnx_opset (int):
|
||||
ONNX opset version to export the models.
|
||||
opt_batch_size (int):
|
||||
Batch size to optimize for during engine building.
|
||||
opt_image_height (int):
|
||||
Image height to optimize for during engine building. Must be a multiple of 8.
|
||||
opt_image_width (int):
|
||||
Image width to optimize for during engine building. Must be a multiple of 8.
|
||||
static_batch (bool):
|
||||
Build engine only for specified opt_batch_size.
|
||||
static_shape (bool):
|
||||
Build engine only for specified opt_image_height & opt_image_width. Default = True.
|
||||
enable_refit (bool):
|
||||
Build engines with refit option enabled.
|
||||
enable_all_tactics (bool):
|
||||
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]:
|
||||
if not os.path.exists(directory):
|
||||
print(f"[I] Create directory: {directory}")
|
||||
pathlib.Path(directory).mkdir(parents=True)
|
||||
|
||||
# Load pipeline models
|
||||
models_args = {'version': self.version, 'pipeline': self.pipeline_type, 'device': self.device,
|
||||
'hf_token': self.hf_token, 'verbose': self.verbose, 'framework_model_dir': framework_model_dir,
|
||||
'max_batch_size': self.max_batch_size}
|
||||
if 'clip-vis' in self.stages:
|
||||
self.models['clip-vis'] = CLIPVisionWithProjModel(**models_args, subfolder='image_encoder')
|
||||
if 'clip-imgfe' in self.stages:
|
||||
self.models['clip-imgfe'] = CLIPImageProcessorModel(**models_args, subfolder='feature_extractor')
|
||||
if 'unet-temp' in self.stages:
|
||||
self.models['unet-temp'] = UNetTemporalModel(**models_args, fp16=True, fp8=fp8, num_frames=self.num_frames, do_classifier_free_guidance=self.do_classifier_free_guidance)
|
||||
if 'vae-temp' in self.stages:
|
||||
self.models['vae-temp'] = VAEDecTemporalModel(**models_args, decode_chunk_size=self.decode_chunk_size)
|
||||
self.image_processor = VaeImageProcessor(vae_scale_factor=self.vae_scale_factor)
|
||||
|
||||
# Configure pipeline models to load
|
||||
model_names = self.models.keys()
|
||||
self.torch_fallback = dict(zip(model_names, [self.torch_inference or self.config.get(model_name.replace('-','_')+'_torch_fallback', False) for model_name in model_names]))
|
||||
onnx_path = dict(zip(model_names, [self.getOnnxPath(model_name, onnx_dir, opt=False) for model_name in model_names]))
|
||||
onnx_opt_path = dict(zip(model_names, [self.getOnnxPath(model_name, onnx_dir) for model_name in model_names]))
|
||||
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])
|
||||
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}")
|
||||
root_dir = os.path.dirname(os.path.abspath(sys.modules["__main__"].__file__))
|
||||
calibration_image_folder = os.path.join(root_dir, "calibration_data", "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(self.version, 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)
|
||||
|
||||
# Clean model cache
|
||||
torch.cuda.empty_cache()
|
||||
|
||||
# Build TensorRT engines
|
||||
for model_name, obj in self.models.items():
|
||||
if self.torch_fallback[model_name]:
|
||||
continue
|
||||
engine = engine_module.Engine(engine_path[model_name])
|
||||
if not os.path.exists(engine_path[model_name]):
|
||||
update_output_names = obj.get_output_names() + obj.extra_output_names if obj.extra_output_names else None
|
||||
engine.build(onnx_opt_path[model_name],
|
||||
input_profile=obj.get_input_profile(
|
||||
opt_batch_size, opt_image_height, opt_image_width,
|
||||
static_batch=static_batch, static_shape=static_shape
|
||||
),
|
||||
enable_refit=do_engine_refit[model_name],
|
||||
enable_all_tactics=enable_all_tactics,
|
||||
timing_cache=timing_cache,
|
||||
update_output_names=update_output_names,
|
||||
native_instancenorm=False)
|
||||
self.engine[model_name] = engine
|
||||
|
||||
# Load TensorRT engines
|
||||
for model_name in self.models.keys():
|
||||
if not self.torch_fallback[model_name]:
|
||||
self.engine[model_name].load()
|
||||
|
||||
def activateEngines(self, model_name, alloc_shape=None):
|
||||
if not self.torch_fallback[model_name]:
|
||||
device_memory_update = self.low_vram and not self.shared_device_memory
|
||||
if device_memory_update:
|
||||
assert not self.use_cuda_graph
|
||||
# Reclaim GPU memory from torch cache
|
||||
torch.cuda.empty_cache()
|
||||
self.shared_device_memory = cudart.cudaMalloc(self.max_shared_device_memory_size)[1]
|
||||
# Create TensorRT execution context
|
||||
if not self.engine[model_name].context:
|
||||
assert not self.use_cuda_graph
|
||||
self.engine[model_name].activate(device_memory=self.shared_device_memory)
|
||||
if device_memory_update:
|
||||
self.engine[model_name].reactivate(device_memory=self.shared_device_memory)
|
||||
if alloc_shape and not self.engine[model_name].tensors:
|
||||
assert not self.use_cuda_graph
|
||||
self.engine[model_name].allocate_buffers(shape_dict=alloc_shape, device=self.device)
|
||||
else:
|
||||
# Load torch model
|
||||
if model_name not in self.torch_models:
|
||||
self.torch_models[model_name] = self.models[model_name].get_model(torch_inference=self.torch_inference)
|
||||
|
||||
def deactivateEngines(self, model_name, release_model=True):
|
||||
if not release_model:
|
||||
return
|
||||
if not self.torch_fallback[model_name]:
|
||||
assert not self.use_cuda_graph
|
||||
self.engine[model_name].deallocate_buffers()
|
||||
self.engine[model_name].deactivate()
|
||||
# Shared device memory deallocated only in low VRAM mode
|
||||
if self.low_vram and self.shared_device_memory:
|
||||
cudart.cudaFree(self.shared_device_memory)
|
||||
self.shared_device_memory = None
|
||||
else:
|
||||
del self.torch_models[model_name]
|
||||
|
||||
def print_summary(self, denoising_steps, walltime_ms, batch_size, num_frames):
|
||||
print('|-----------------|--------------|')
|
||||
print('| {:^15} | {:^12} |'.format('Module', 'Latency'))
|
||||
print('|-----------------|--------------|')
|
||||
print('| {:^15} | {:>9.2f} ms |'.format('VAE-Enc', cudart.cudaEventElapsedTime(self.events['vae_encoder'][0], self.events['vae_encoder'][1])[1]))
|
||||
print('| {:^15} | {:>9.2f} ms |'.format('CLIP', cudart.cudaEventElapsedTime(self.events['clip'][0], self.events['clip'][1])[1]))
|
||||
print('| {:^15} | {:>9.2f} ms |'.format('UNet'+('+CNet' if self.pipeline_type.is_controlnet() else '')+' x '+str(denoising_steps), cudart.cudaEventElapsedTime(self.events['denoise'][0], self.events['denoise'][1])[1]))
|
||||
print('| {:^15} | {:>9.2f} ms |'.format('VAE-Dec', cudart.cudaEventElapsedTime(self.events['vae'][0], self.events['vae'][1])[1]))
|
||||
print('|-----------------|--------------|')
|
||||
print('| {:^15} | {:>9.2f} ms |'.format('Pipeline', walltime_ms))
|
||||
print('|-----------------|--------------|')
|
||||
print('Throughput: {:.5f} videos/min ({} frames)'.format(batch_size*60000./walltime_ms, num_frames))
|
||||
|
||||
def save_video(self, frames, pipeline, seed):
|
||||
video_name_prefix = '-'.join([pipeline, 'fp16', str(seed), str(random.randint(1000,9999))])
|
||||
video_name_suffix = 'torch' if self.torch_inference else 'trt'
|
||||
video_path = video_name_prefix+'-'+video_name_suffix+'.gif'
|
||||
print(f"Saving video to: {video_path}")
|
||||
frames[0].save(os.path.join(self.output_dir, video_path), save_all=True,optimize=False, append_images=frames[1:], loop=0)
|
||||
|
||||
def _encode_image(self, image, num_videos_per_prompt, do_classifier_free_guidance):
|
||||
dtype = next(self.torch_models['clip-vis'].parameters()).dtype
|
||||
|
||||
if not isinstance(image, torch.Tensor):
|
||||
image = self.image_processor.pil_to_numpy(image)
|
||||
image = self.image_processor.numpy_to_pt(image)
|
||||
|
||||
# We normalize the image before resizing to match with the original implementation.
|
||||
# Then we unnormalize it after resizing.
|
||||
image = image * 2.0 - 1.0
|
||||
image = image_module.resize_with_antialiasing(image, (224, 224))
|
||||
image = (image + 1.0) / 2.0
|
||||
|
||||
# Normalize the image with for CLIP input
|
||||
image = self.torch_models['clip-imgfe'](
|
||||
images=image,
|
||||
do_normalize=True,
|
||||
do_center_crop=False,
|
||||
do_resize=False,
|
||||
do_rescale=False,
|
||||
return_tensors="pt",
|
||||
).pixel_values
|
||||
|
||||
image = image.to(device=self.device, dtype=dtype)
|
||||
image_embeddings = self.torch_models['clip-vis'](image).image_embeds
|
||||
image_embeddings = image_embeddings.unsqueeze(1)
|
||||
|
||||
# duplicate image embeddings for each generation per prompt, using mps friendly method
|
||||
bs_embed, seq_len, _ = image_embeddings.shape
|
||||
image_embeddings = image_embeddings.repeat(1, num_videos_per_prompt, 1)
|
||||
image_embeddings = image_embeddings.view(bs_embed * num_videos_per_prompt, seq_len, -1)
|
||||
|
||||
if do_classifier_free_guidance:
|
||||
negative_image_embeddings = torch.zeros_like(image_embeddings)
|
||||
|
||||
# For classifier free guidance, we need to do two forward passes.
|
||||
# Here we concatenate the unconditional and text embeddings into a single batch
|
||||
# to avoid doing two forward passes
|
||||
image_embeddings = torch.cat([negative_image_embeddings, image_embeddings])
|
||||
|
||||
return image_embeddings
|
||||
|
||||
def _encode_vae_image(
|
||||
self,
|
||||
image: torch.Tensor,
|
||||
device,
|
||||
num_videos_per_prompt,
|
||||
do_classifier_free_guidance,
|
||||
):
|
||||
image = image.to(device=device)
|
||||
image_latents = self.torch_models['vae-temp'].encode(image).latent_dist.mode()
|
||||
|
||||
if do_classifier_free_guidance:
|
||||
negative_image_latents = torch.zeros_like(image_latents)
|
||||
|
||||
# For classifier free guidance, we need to do two forward passes.
|
||||
# Here we concatenate the unconditional and text embeddings into a single batch
|
||||
# to avoid doing two forward passes
|
||||
image_latents = torch.cat([negative_image_latents, image_latents])
|
||||
|
||||
# duplicate image_latents for each generation per prompt, using mps friendly method
|
||||
image_latents = image_latents.repeat(num_videos_per_prompt, 1, 1, 1)
|
||||
|
||||
return image_latents
|
||||
|
||||
def _get_add_time_ids(
|
||||
self,
|
||||
fps,
|
||||
motion_bucket_id,
|
||||
noise_aug_strength,
|
||||
dtype,
|
||||
batch_size,
|
||||
num_videos_per_prompt,
|
||||
do_classifier_free_guidance,
|
||||
):
|
||||
add_time_ids = [fps, motion_bucket_id, noise_aug_strength]
|
||||
add_time_ids = torch.tensor([add_time_ids], dtype=dtype)
|
||||
add_time_ids = add_time_ids.repeat(batch_size * num_videos_per_prompt, 1)
|
||||
|
||||
if do_classifier_free_guidance:
|
||||
add_time_ids = torch.cat([add_time_ids, add_time_ids])
|
||||
|
||||
return add_time_ids
|
||||
|
||||
def prepare_latents(
|
||||
self,
|
||||
batch_size,
|
||||
num_frames,
|
||||
num_channels_latents,
|
||||
height,
|
||||
width,
|
||||
dtype,
|
||||
device,
|
||||
latents=None,
|
||||
):
|
||||
shape = (
|
||||
batch_size,
|
||||
num_frames,
|
||||
num_channels_latents // 2,
|
||||
height // self.vae_scale_factor,
|
||||
width // self.vae_scale_factor,
|
||||
)
|
||||
|
||||
if latents is None:
|
||||
latents = randn_tensor(shape, generator=self.generator, device=device, dtype=dtype)
|
||||
else:
|
||||
latents = latents.to(device)
|
||||
|
||||
# scale the initial noise by the standard deviation required by the scheduler
|
||||
latents = latents * self.scheduler.init_noise_sigma
|
||||
return latents
|
||||
|
||||
def decode_latents(self, latents, num_frames, decode_chunk_size):
|
||||
# [batch, frames, channels, height, width] -> [batch*frames, channels, height, width]
|
||||
latents = latents.flatten(0, 1)
|
||||
|
||||
latents = 1 / self.scaling_factor * latents
|
||||
|
||||
# decode decode_chunk_size frames at a time to avoid OOM
|
||||
frames = []
|
||||
for i in range(0, latents.shape[0], decode_chunk_size):
|
||||
num_frames_in = latents[i : i + decode_chunk_size].shape[0]
|
||||
# TODO only pass num_frames_in if it's expected
|
||||
if self.torch_fallback['vae-temp']:
|
||||
frame = self.torch_models['vae-temp'].decode(latents[i : i + decode_chunk_size], num_frames=num_frames_in).sample
|
||||
else:
|
||||
params = {
|
||||
'latent': latents[i : i + decode_chunk_size],
|
||||
# FIXME segfault
|
||||
#'num_frames_in': torch.Tensor([num_frames_in]).to(device=latents.device, dtype=torch.int64),
|
||||
}
|
||||
frame = self.runEngine('vae-temp', params)['frames']
|
||||
frames.append(frame)
|
||||
frames = torch.cat(frames, dim=0)
|
||||
|
||||
# [batch*frames, channels, height, width] -> [batch, channels, frames, height, width]
|
||||
frames = frames.reshape(-1, num_frames, *frames.shape[1:]).permute(0, 2, 1, 3, 4)
|
||||
|
||||
# we always cast to float32 as this does not cause significant overhead and is compatible with bfloat16
|
||||
frames = frames.float()
|
||||
return frames
|
||||
|
||||
def infer(
|
||||
self,
|
||||
input_image,
|
||||
image_height: int,
|
||||
image_width: int,
|
||||
fps: int = 7,
|
||||
motion_bucket_id: int = 127,
|
||||
noise_aug_strength: int = 0.02,
|
||||
num_videos_per_prompt: Optional[int] = 1,
|
||||
warmup: bool = False,
|
||||
save_video: bool = True,
|
||||
):
|
||||
"""
|
||||
Run the video diffusion pipeline.
|
||||
|
||||
Args:
|
||||
input_image (image):
|
||||
Input image used to initialize the latents.
|
||||
image_height (int):
|
||||
Height (in pixels) of the image to be generated. Must be a multiple of 8.
|
||||
image_width (int):
|
||||
Width (in pixels) of the image to be generated. Must be a multiple of 8.
|
||||
fps (`int`, *optional*, defaults to 7):
|
||||
Frames per second. The rate at which the generated images shall be exported to a video after generation.
|
||||
Note that Stable Diffusion Video's UNet was micro-conditioned on fps-1 during training.
|
||||
motion_bucket_id (`int`, *optional*, defaults to 127):
|
||||
The motion bucket ID. Used as conditioning for the generation. The higher the number the more motion will be in the video.
|
||||
noise_aug_strength (`int`, *optional*, defaults to 0.02):
|
||||
The amount of noise added to the init image, the higher it is the less the video will look like the init image. Increase it for more motion.
|
||||
num_videos_per_prompt (`int`, *optional*, defaults to 1):
|
||||
The number of images to generate per prompt.
|
||||
warmup (bool):
|
||||
Indicate if this is a warmup run.
|
||||
save_video (bool):
|
||||
Save the video image.
|
||||
"""
|
||||
|
||||
if self.generator and self.seed:
|
||||
self.generator.manual_seed(self.seed)
|
||||
|
||||
# TODO
|
||||
batch_size = 1
|
||||
# Fast warmup
|
||||
denoising_steps = 1 if warmup else self.denoising_steps
|
||||
|
||||
torch.cuda.synchronize()
|
||||
e2e_tic = time.perf_counter()
|
||||
|
||||
class LoadModelContext:
|
||||
def __init__(ctx, model_name, alloc_shape=None, release_model=False):
|
||||
ctx.model_name = model_name
|
||||
ctx.release_model = release_model
|
||||
ctx.alloc_shape = alloc_shape
|
||||
def __enter__(ctx):
|
||||
self.activateEngines(ctx.model_name, alloc_shape=ctx.alloc_shape)
|
||||
def __exit__(ctx, exc_type, exc_val, exc_tb):
|
||||
self.deactivateEngines(ctx.model_name, release_model=ctx.release_model)
|
||||
|
||||
# Release model opportunistically in TensorRT pipeline only in low VRAM mode
|
||||
release_model = self.low_vram and not self.torch_inference
|
||||
with torch.inference_mode(), torch.autocast("cuda"), trt.Runtime(TRT_LOGGER):
|
||||
with LoadModelContext('clip-imgfe', release_model=release_model), LoadModelContext('clip-vis', release_model=release_model):
|
||||
self.profile_start('clip', color='green')
|
||||
image_embeddings = self._encode_image(input_image, num_videos_per_prompt, self.do_classifier_free_guidance)
|
||||
self.profile_stop('clip')
|
||||
# NOTE Stable Diffusion Video was conditioned on fps - 1
|
||||
fps = fps - 1
|
||||
|
||||
self.profile_start('preprocess', color='pink')
|
||||
input_image = self.image_processor.preprocess(input_image, height=image_height, width=image_width).to(self.device)
|
||||
noise = randn_tensor(input_image.shape, generator=self.generator, device=input_image.device, dtype=input_image.dtype)
|
||||
input_image = input_image + noise_aug_strength * noise
|
||||
self.profile_stop('preprocess')
|
||||
|
||||
# TODO
|
||||
# assert self.torch_models['vae-temp'].dtype == torch.float32
|
||||
|
||||
with LoadModelContext('vae-temp'):
|
||||
self.profile_start('vae_encoder', color='red')
|
||||
image_latents = self._encode_vae_image(input_image, self.device, num_videos_per_prompt, self.do_classifier_free_guidance)
|
||||
image_latents = image_latents.to(image_embeddings.dtype)
|
||||
self.profile_stop('vae_encoder')
|
||||
|
||||
# Repeat the image latents for each frame so we can concatenate them with the noise
|
||||
# image_latents [batch, channels, height, width] ->[batch, num_frames, channels, height, width]
|
||||
image_latents = image_latents.unsqueeze(1).repeat(1, self.num_frames, 1, 1, 1)
|
||||
|
||||
# Get Added Time IDs
|
||||
added_time_ids = self._get_add_time_ids(
|
||||
fps,
|
||||
motion_bucket_id,
|
||||
noise_aug_strength,
|
||||
image_embeddings.dtype,
|
||||
batch_size,
|
||||
num_videos_per_prompt,
|
||||
self.do_classifier_free_guidance,
|
||||
)
|
||||
added_time_ids = added_time_ids.to(self.device)
|
||||
|
||||
# Prepare timesteps
|
||||
self.scheduler.set_timesteps(denoising_steps, device=self.device)
|
||||
timesteps = self.scheduler.timesteps
|
||||
|
||||
# Prepare latent variables
|
||||
latents = self.prepare_latents(
|
||||
batch_size * num_videos_per_prompt,
|
||||
self.num_frames,
|
||||
8, # TODO: num_channels_latents = unet.config.in_channels
|
||||
image_height,
|
||||
image_width,
|
||||
image_embeddings.dtype,
|
||||
input_image.device,
|
||||
None, # pre-generated latents
|
||||
)
|
||||
|
||||
# Prepare guidance scale
|
||||
guidance_scale = torch.linspace(self.min_guidance_scale, self.max_guidance_scale, self.num_frames).unsqueeze(0)
|
||||
guidance_scale = guidance_scale.to(self.device, latents.dtype)
|
||||
guidance_scale = guidance_scale.repeat(batch_size * num_videos_per_prompt, 1)
|
||||
guidance_scale = _append_dims(guidance_scale, latents.ndim)
|
||||
|
||||
# Denoising loop
|
||||
num_warmup_steps = len(timesteps) - denoising_steps * self.scheduler.order
|
||||
unet_shape_dict = self.models['unet-temp'].get_shape_dict(batch_size, image_height, image_width)
|
||||
with LoadModelContext('unet-temp', alloc_shape=unet_shape_dict, release_model=release_model), tqdm(total=denoising_steps) as progress_bar:
|
||||
self.profile_start('denoise', color='blue')
|
||||
for i, t in enumerate(timesteps):
|
||||
# expand the latents if we are doing classifier free guidance
|
||||
latent_model_input = torch.cat([latents] * 2) if self.do_classifier_free_guidance else latents
|
||||
latent_model_input = self.scheduler.scale_model_input(latent_model_input, t)
|
||||
|
||||
# Concatenate image_latents over channels dimention
|
||||
latent_model_input = torch.cat([latent_model_input, image_latents], dim=2)
|
||||
|
||||
# predict the noise residual
|
||||
if self.torch_fallback['unet-temp']:
|
||||
noise_pred = self.torch_models['unet-temp'](
|
||||
latent_model_input,
|
||||
t,
|
||||
encoder_hidden_states=image_embeddings,
|
||||
added_time_ids=added_time_ids,
|
||||
return_dict=False,
|
||||
)[0]
|
||||
else:
|
||||
params = {
|
||||
"sample": latent_model_input,
|
||||
"timestep": t,
|
||||
"encoder_hidden_states": image_embeddings,
|
||||
"added_time_ids": added_time_ids,
|
||||
}
|
||||
noise_pred = self.runEngine('unet-temp', params)['latent']
|
||||
|
||||
# perform guidance
|
||||
if self.do_classifier_free_guidance:
|
||||
noise_pred_uncond, noise_pred_cond = noise_pred.chunk(2)
|
||||
noise_pred = noise_pred_uncond + guidance_scale * (noise_pred_cond - noise_pred_uncond)
|
||||
|
||||
# compute the previous noisy sample x_t -> x_t-1
|
||||
latents = self.scheduler.step(noise_pred, t, latents).prev_sample
|
||||
|
||||
if i == len(timesteps) - 1 or ((i + 1) > num_warmup_steps and (i + 1) % self.scheduler.order == 0):
|
||||
progress_bar.update()
|
||||
self.profile_stop('denoise')
|
||||
|
||||
with torch.inference_mode(), trt.Runtime(TRT_LOGGER), LoadModelContext('vae-temp'):
|
||||
self.profile_start('vae', color='red')
|
||||
self.torch_models['vae-temp'].to(dtype=torch.float16)
|
||||
frames = self.decode_latents(latents, self.num_frames, self.decode_chunk_size)
|
||||
frames = image_module.tensor2vid(frames, self.image_processor, output_type="pil")
|
||||
self.profile_stop('vae')
|
||||
|
||||
torch.cuda.synchronize()
|
||||
|
||||
if warmup:
|
||||
return
|
||||
|
||||
e2e_toc = time.perf_counter()
|
||||
walltime_ms = (e2e_toc - e2e_tic) * 1000.
|
||||
self.print_summary(denoising_steps, walltime_ms, batch_size, len(frames[0]))
|
||||
if save_video:
|
||||
self.save_video(frames[0], self.pipeline_type.name.lower(), self.seed)
|
||||
|
||||
return frames, walltime_ms
|
||||
|
||||
def run(self, input_image, height, width, batch_size, batch_count, num_warmup_runs, use_cuda_graph, **kwargs):
|
||||
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 ..")
|
||||
for _ in range(num_warmup_runs):
|
||||
self.infer(input_image, height, width, warmup=True)
|
||||
|
||||
for _ in range(batch_count):
|
||||
print("[I] Running StableDiffusion pipeline")
|
||||
if self.nvtx_profile:
|
||||
cudart.cudaProfilerStart()
|
||||
self.infer(input_image, height, width, warmup=False)
|
||||
if self.nvtx_profile:
|
||||
cudart.cudaProfilerStop()
|
||||
@@ -0,0 +1,68 @@
|
||||
#
|
||||
# SPDX-FileCopyrightText: Copyright (c) 1993-2025 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 enum
|
||||
|
||||
|
||||
class PIPELINE_TYPE(enum.Enum):
|
||||
TXT2IMG = enum.auto()
|
||||
IMG2IMG = enum.auto()
|
||||
IMG2VID = enum.auto()
|
||||
TXT2VID = enum.auto()
|
||||
CONTROLNET = enum.auto()
|
||||
XL_CONTROLNET = enum.auto()
|
||||
XL_BASE = enum.auto()
|
||||
XL_REFINER = enum.auto()
|
||||
CASCADE_PRIOR = enum.auto()
|
||||
CASCADE_DECODER = enum.auto()
|
||||
VIDEO2WORLD = enum.auto()
|
||||
|
||||
def is_txt2img(self):
|
||||
return self in (self.TXT2IMG, self.CONTROLNET)
|
||||
|
||||
def is_img2img(self):
|
||||
return self == self.IMG2IMG
|
||||
|
||||
def is_img2vid(self):
|
||||
return self == self.IMG2VID
|
||||
|
||||
def is_txt2vid(self):
|
||||
return self == self.TXT2VID
|
||||
|
||||
def is_controlnet(self):
|
||||
return self in (self.CONTROLNET, self.XL_CONTROLNET)
|
||||
|
||||
def is_sd_xl_base(self):
|
||||
return self in (self.XL_BASE, self.XL_CONTROLNET)
|
||||
|
||||
def is_sd_xl_refiner(self):
|
||||
return self == self.XL_REFINER
|
||||
|
||||
def is_sd_xl(self):
|
||||
return self.is_sd_xl_base() or self.is_sd_xl_refiner()
|
||||
|
||||
def is_cascade_prior(self):
|
||||
return self == self.CASCADE_PRIOR
|
||||
|
||||
def is_cascade_decoder(self):
|
||||
return self == self.CASCADE_DECODER
|
||||
|
||||
def is_cascade(self):
|
||||
return self.is_cascade_prior() or self.is_cascade_decoder()
|
||||
|
||||
def is_video2world(self):
|
||||
return self == self.VIDEO2WORLD
|
||||
@@ -0,0 +1,841 @@
|
||||
#
|
||||
# SPDX-FileCopyrightText: Copyright (c) 1993-2025 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 argparse
|
||||
import gc
|
||||
import html
|
||||
import os
|
||||
import pathlib
|
||||
import random
|
||||
import re
|
||||
import time
|
||||
from typing import Any, Callable, Dict, List, Optional, Union
|
||||
|
||||
import tensorrt as trt
|
||||
import torch
|
||||
from cuda.bindings import runtime as cudart
|
||||
from diffusers.utils.torch_utils import randn_tensor
|
||||
from PIL import Image
|
||||
from tqdm.auto import tqdm
|
||||
|
||||
try:
|
||||
import ftfy
|
||||
FTFY_AVAILABLE = True
|
||||
except ImportError:
|
||||
FTFY_AVAILABLE = False
|
||||
|
||||
import demo_diffusion.engine as engine_module
|
||||
import demo_diffusion.image as image_module
|
||||
import demo_diffusion.path as path_module
|
||||
from demo_diffusion.model import (
|
||||
T5Model,
|
||||
WanTransformerModel,
|
||||
AutoencoderKLWanModel,
|
||||
make_tokenizer,
|
||||
)
|
||||
from demo_diffusion.pipeline.diffusion_pipeline import DiffusionPipeline
|
||||
from demo_diffusion.pipeline.type import PIPELINE_TYPE
|
||||
|
||||
TRT_LOGGER = trt.Logger(trt.Logger.ERROR)
|
||||
|
||||
# Copied from https://github.com/huggingface/diffusers/blob/main/src/diffusers/pipelines/wan/pipeline_wan.py#L78
|
||||
def basic_clean(text):
|
||||
if FTFY_AVAILABLE:
|
||||
text = ftfy.fix_text(text)
|
||||
text = html.unescape(html.unescape(text))
|
||||
return text.strip()
|
||||
|
||||
# Copied from https://github.com/huggingface/diffusers/blob/main/src/diffusers/pipelines/wan/pipeline_wan.py#L84
|
||||
def whitespace_clean(text):
|
||||
text = re.sub(r"\s+", " ", text)
|
||||
text = text.strip()
|
||||
return text
|
||||
|
||||
# Copied from https://github.com/huggingface/diffusers/blob/main/src/diffusers/pipelines/wan/pipeline_wan.py#L90
|
||||
def prompt_clean(text):
|
||||
text = whitespace_clean(basic_clean(text))
|
||||
return text
|
||||
|
||||
|
||||
class WanPipeline(DiffusionPipeline):
|
||||
"""
|
||||
Application showcasing the acceleration of Wan 2.2 T2V pipeline using Nvidia TensorRT.
|
||||
"""
|
||||
|
||||
_callback_tensor_inputs = ["latents", "prompt_embeds", "negative_prompt_embeds"]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
dd_path,
|
||||
version='wan2.2-t2v-a14b',
|
||||
pipeline_type=PIPELINE_TYPE.TXT2VID,
|
||||
boundary_ratio: float = 0.875,
|
||||
guidance_scale: float = 4.0,
|
||||
guidance_scale_2: float = 3.0,
|
||||
t5_weight_streaming_budget_percentage=None,
|
||||
transformer_weight_streaming_budget_percentage=None,
|
||||
**kwargs
|
||||
):
|
||||
"""
|
||||
Initializes the Wan T2V pipeline.
|
||||
|
||||
Args:
|
||||
dd_path (load_module.DDPath):
|
||||
DDPath object that contains all paths used in DemoDiffusion
|
||||
version (str):
|
||||
The version of the pipeline. Should be [wan2.2-t2v-a14b]
|
||||
pipeline_type (PIPELINE_TYPE):
|
||||
Type of current pipeline (TXT2VID)
|
||||
boundary_ratio (float, defaults to 0.875):
|
||||
Ratio of total timesteps to use as the boundary for switching between transformers
|
||||
in two-stage denoising. Transformer handles high-noise stages (timesteps >= boundary)
|
||||
and transformer_2 handles low-noise stages (timesteps < boundary).
|
||||
Wan 2.2 T2V always uses two-stage denoising.
|
||||
guidance_scale (float):
|
||||
Guidance scale for high-noise stage (transformer). Wan default: 4.0
|
||||
guidance_scale_2 (float):
|
||||
Guidance scale for low-noise stage (transformer_2). Wan default: 3.0
|
||||
t5_weight_streaming_budget_percentage (`int`, defaults to None):
|
||||
Weight streaming budget as a percentage of the size of total streamable weights for the T5 model.
|
||||
transformer_weight_streaming_budget_percentage (`int`, defaults to None):
|
||||
Weight streaming budget as a percentage of the size of total streamable weights for the transformer models.
|
||||
"""
|
||||
super().__init__(
|
||||
dd_path=dd_path,
|
||||
version=version,
|
||||
pipeline_type=pipeline_type,
|
||||
scheduler="UniPC",
|
||||
bf16=True,
|
||||
text_encoder_weight_streaming_budget_percentage=t5_weight_streaming_budget_percentage,
|
||||
denoiser_weight_streaming_budget_percentage=transformer_weight_streaming_budget_percentage,
|
||||
**kwargs
|
||||
)
|
||||
|
||||
# Validate boundary_ratio (required for Wan 2.2 two-stage denoising)
|
||||
if boundary_ratio is None or not (0.0 < boundary_ratio < 1.0):
|
||||
raise ValueError(
|
||||
f"`boundary_ratio` must be between 0.0 and 1.0, got {boundary_ratio}"
|
||||
)
|
||||
|
||||
self.boundary_ratio = boundary_ratio
|
||||
self.guidance_scale = guidance_scale
|
||||
self.guidance_scale_2 = guidance_scale_2
|
||||
|
||||
self.vae_scale_factor_temporal = 4
|
||||
self.vae_scale_factor_spatial = 8
|
||||
|
||||
self.opt_image_height = 720
|
||||
self.opt_image_width = 1280
|
||||
self.opt_num_frames = 81
|
||||
self.max_sequence_length = 512
|
||||
|
||||
@classmethod
|
||||
def FromArgs(cls, args: argparse.Namespace, pipeline_type: PIPELINE_TYPE) -> 'WanPipeline':
|
||||
"""Factory method to construct a WanPipeline object from parsed arguments."""
|
||||
|
||||
MAX_BATCH_SIZE = 1 # Wan always uses batch size 1
|
||||
DEVICE = "cuda"
|
||||
|
||||
dd_path = path_module.resolve_path(
|
||||
cls.get_model_names(pipeline_type), args, pipeline_type, cls._get_pipeline_uid(args.version)
|
||||
)
|
||||
|
||||
return cls(
|
||||
dd_path=dd_path,
|
||||
version=args.version,
|
||||
pipeline_type=pipeline_type,
|
||||
boundary_ratio=args.boundary_ratio,
|
||||
denoising_steps=args.denoising_steps,
|
||||
guidance_scale=args.guidance_scale,
|
||||
guidance_scale_2=args.guidance_scale_2,
|
||||
t5_weight_streaming_budget_percentage=args.t5_ws_percentage if hasattr(args, 't5_ws_percentage') else None,
|
||||
transformer_weight_streaming_budget_percentage=args.transformer_ws_percentage if hasattr(args, 'transformer_ws_percentage') else None,
|
||||
max_batch_size=MAX_BATCH_SIZE,
|
||||
device=DEVICE,
|
||||
output_dir=args.output_dir,
|
||||
hf_token=args.hf_token,
|
||||
verbose=args.verbose,
|
||||
nvtx_profile=args.nvtx_profile,
|
||||
use_cuda_graph=args.use_cuda_graph,
|
||||
framework_model_dir=args.framework_model_dir,
|
||||
low_vram=args.low_vram,
|
||||
torch_inference=args.torch_inference,
|
||||
torch_fallback=args.torch_fallback if hasattr(args, 'torch_fallback') else None,
|
||||
weight_streaming=args.ws if hasattr(args, 'ws') else False,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def get_model_names(cls, pipeline_type: PIPELINE_TYPE, controlnet_type: str = None) -> List[str]:
|
||||
"""Return a list of model names used by this pipeline.
|
||||
|
||||
Overrides:
|
||||
DiffusionPipeline.get_model_names
|
||||
"""
|
||||
return ["text_encoder", "transformer", "transformer_2", "vae_decoder"]
|
||||
|
||||
def download_onnx_models(self, model_name: str, model_config: dict[str, Any]) -> None:
|
||||
raise NotImplementedError(
|
||||
"Pre-exported Wan ONNX models are not available for download. "
|
||||
"Export ONNX models locally using the provided export script."
|
||||
)
|
||||
|
||||
def _initialize_models(self, framework_model_dir, int8=False, fp8=False, fp4=False):
|
||||
self.tokenizer = make_tokenizer(
|
||||
self.version,
|
||||
self.pipeline_type,
|
||||
self.hf_token,
|
||||
framework_model_dir,
|
||||
subfolder='tokenizer',
|
||||
tokenizer_type='t5'
|
||||
)
|
||||
|
||||
models_args = {
|
||||
'version': self.version,
|
||||
'pipeline': self.pipeline_type,
|
||||
'device': self.device,
|
||||
'hf_token': self.hf_token,
|
||||
'verbose': self.verbose,
|
||||
'framework_model_dir': framework_model_dir,
|
||||
'max_batch_size': 1
|
||||
}
|
||||
|
||||
if "text_encoder" in self.stages:
|
||||
self.models['text_encoder'] = T5Model(
|
||||
**models_args,
|
||||
fp16=False,
|
||||
bf16=True,
|
||||
text_maxlen=self.max_sequence_length,
|
||||
weight_streaming=self.weight_streaming,
|
||||
weight_streaming_budget_percentage=self.text_encoder_weight_streaming_budget_percentage,
|
||||
use_attention_mask=True,
|
||||
)
|
||||
|
||||
if "transformer" in self.stages:
|
||||
self.models['transformer'] = WanTransformerModel(
|
||||
**models_args,
|
||||
subfolder='transformer',
|
||||
fp16=False,
|
||||
bf16=True,
|
||||
text_maxlen=self.max_sequence_length,
|
||||
num_frames=self.opt_num_frames,
|
||||
height=self.opt_image_height,
|
||||
width=self.opt_image_width,
|
||||
weight_streaming=self.weight_streaming,
|
||||
weight_streaming_budget_percentage=self.denoiser_weight_streaming_budget_percentage,
|
||||
)
|
||||
|
||||
if "transformer_2" in self.stages:
|
||||
self.models['transformer_2'] = WanTransformerModel(
|
||||
**models_args,
|
||||
subfolder='transformer_2',
|
||||
fp16=False,
|
||||
bf16=True,
|
||||
text_maxlen=self.max_sequence_length,
|
||||
num_frames=self.opt_num_frames,
|
||||
height=self.opt_image_height,
|
||||
width=self.opt_image_width,
|
||||
weight_streaming=self.weight_streaming,
|
||||
weight_streaming_budget_percentage=self.denoiser_weight_streaming_budget_percentage,
|
||||
)
|
||||
|
||||
if "vae_decoder" in self.stages:
|
||||
self.models['vae_decoder'] = AutoencoderKLWanModel(
|
||||
**models_args,
|
||||
)
|
||||
|
||||
self.config['vae_decoder_torch_fallback'] = True
|
||||
|
||||
def load_resources(self, image_height, image_width, batch_size, seed):
|
||||
"""Override to create additional 'denoise' event for combined transformer timing."""
|
||||
super().load_resources(image_height, image_width, batch_size, seed)
|
||||
# additional event for combined denoising timing (both transformers)
|
||||
self.events['denoise'] = [cudart.cudaEventCreate()[1], cudart.cudaEventCreate()[1]]
|
||||
|
||||
def print_summary(self, denoising_steps, walltime_ms, batch_size, num_frames):
|
||||
print("|----------------------|--------------|")
|
||||
print("| {:^20} | {:^12} |".format("Module", "Latency"))
|
||||
print("|----------------------|--------------|")
|
||||
|
||||
# calculate transformer timings from combined denoise event
|
||||
total_denoise_time = cudart.cudaEventElapsedTime(self.events['denoise'][0], self.events['denoise'][1])[1]
|
||||
transformer_steps_map = {
|
||||
'transformer': self.transformer_steps if (self.transformer_steps > 0 and self.transformer_2_steps > 0) else denoising_steps,
|
||||
'transformer_2': self.transformer_2_steps if self.transformer_2_steps > 0 else 0
|
||||
}
|
||||
|
||||
for stage in self.stages:
|
||||
if stage in transformer_steps_map and transformer_steps_map[stage] > 0:
|
||||
steps = transformer_steps_map[stage]
|
||||
time_ms = total_denoise_time * (steps / denoising_steps)
|
||||
stage_label = f"{stage} x {steps}"
|
||||
elif stage in transformer_steps_map:
|
||||
continue # skip transformer_2 if unused
|
||||
else:
|
||||
time_ms = cudart.cudaEventElapsedTime(self.events[stage][0], self.events[stage][1])[1]
|
||||
stage_label = stage
|
||||
|
||||
print("| {:^20} | {:>9.2f} ms |".format(stage_label, time_ms))
|
||||
|
||||
print("|----------------------|--------------|")
|
||||
print("| {:^20} | {:>9.2f} ms |".format("Pipeline", walltime_ms))
|
||||
print("|----------------------|--------------|")
|
||||
print("Throughput: {:.2f} videos/min ({} frames)".format(batch_size * 60000.0 / walltime_ms, num_frames))
|
||||
|
||||
def save_video(self, frames, pipeline, prompt, seed):
|
||||
if isinstance(prompt, list):
|
||||
prompt_prefix = ''.join(set([p.replace(' ','_')[:10] for p in prompt]))
|
||||
else:
|
||||
prompt_prefix = prompt.replace(' ','_')[:10]
|
||||
|
||||
seed_str = str(seed) if seed is not None else 'random'
|
||||
precision = 'bf16' if self.bf16 else 'fp16' if self.fp16 else 'fp32'
|
||||
video_name_prefix = '-'.join([pipeline, prompt_prefix, precision, seed_str, str(random.randint(1000,9999))])
|
||||
video_name_suffix = 'torch' if self.torch_inference else 'trt'
|
||||
video_path = video_name_prefix+'-'+video_name_suffix+'.gif'
|
||||
full_path = os.path.join(self.output_dir, video_path)
|
||||
print(f"Saving video to: {full_path}")
|
||||
frames[0].save(full_path, save_all=True, optimize=False, append_images=frames[1:], loop=0)
|
||||
|
||||
# Adapted from https://github.com/huggingface/diffusers/blob/main/src/diffusers/pipelines/wan/pipeline_wan.py#L198
|
||||
def encode_prompt(
|
||||
self,
|
||||
prompt: Union[str, List[str]],
|
||||
negative_prompt: Optional[Union[str, List[str]]] = None,
|
||||
do_classifier_free_guidance: bool = True,
|
||||
num_videos_per_prompt: int = 1,
|
||||
prompt_embeds: Optional[torch.Tensor] = None,
|
||||
negative_prompt_embeds: Optional[torch.Tensor] = None,
|
||||
max_sequence_length: int = 226,
|
||||
device: Optional[torch.device] = None,
|
||||
dtype: Optional[torch.dtype] = None,
|
||||
):
|
||||
r"""
|
||||
Encodes the prompt into text encoder hidden states.
|
||||
|
||||
Implementation modeled from diffusers Wan pipeline, adapted for TensorRT.
|
||||
|
||||
Args:
|
||||
prompt (`str` or `List[str]`, *optional*):
|
||||
prompt to be encoded
|
||||
negative_prompt (`str` or `List[str]`, *optional*):
|
||||
The prompt or prompts not to guide the image generation. If not defined, one has to pass
|
||||
`negative_prompt_embeds` instead. Ignored when not using guidance (i.e., ignored if `guidance_scale` is
|
||||
less than `1`).
|
||||
do_classifier_free_guidance (`bool`, *optional*, defaults to `True`):
|
||||
Whether to use classifier free guidance or not.
|
||||
num_videos_per_prompt (`int`, *optional*, defaults to 1):
|
||||
Number of videos that should be generated per prompt. torch device to place the resulting embeddings on
|
||||
prompt_embeds (`torch.Tensor`, *optional*):
|
||||
Pre-generated text embeddings. Can be used to easily tweak text inputs, *e.g.* prompt weighting. If not
|
||||
provided, text embeddings will be generated from `prompt` input argument.
|
||||
negative_prompt_embeds (`torch.Tensor`, *optional*):
|
||||
Pre-generated negative text embeddings. Can be used to easily tweak text inputs, *e.g.* prompt
|
||||
weighting. If not provided, negative_prompt_embeds will be generated from `negative_prompt` input
|
||||
argument.
|
||||
max_sequence_length (`int`, defaults to `226`):
|
||||
Maximum sequence length for text encoder.
|
||||
device: (`torch.device`, *optional*):
|
||||
torch device
|
||||
dtype: (`torch.dtype`, *optional*):
|
||||
torch dtype
|
||||
"""
|
||||
self.profile_start('text_encoder', color='green')
|
||||
|
||||
device = device or self._execution_device
|
||||
|
||||
prompt = [prompt] if isinstance(prompt, str) else prompt
|
||||
if prompt is not None:
|
||||
batch_size = len(prompt)
|
||||
else:
|
||||
batch_size = prompt_embeds.shape[0]
|
||||
|
||||
if prompt_embeds is None:
|
||||
prompt_embeds = self._get_t5_prompt_embeds(
|
||||
prompt=prompt,
|
||||
num_videos_per_prompt=num_videos_per_prompt,
|
||||
max_sequence_length=max_sequence_length,
|
||||
device=device,
|
||||
dtype=dtype,
|
||||
)
|
||||
|
||||
if do_classifier_free_guidance and negative_prompt_embeds is None:
|
||||
negative_prompt = negative_prompt or ""
|
||||
negative_prompt = batch_size * [negative_prompt] if isinstance(negative_prompt, str) else negative_prompt
|
||||
|
||||
if prompt is not None and type(prompt) is not type(negative_prompt):
|
||||
raise TypeError(
|
||||
f"`negative_prompt` should be the same type to `prompt`, but got {type(negative_prompt)} !="
|
||||
f" {type(prompt)}."
|
||||
)
|
||||
elif batch_size != len(negative_prompt):
|
||||
raise ValueError(
|
||||
f"`negative_prompt`: {negative_prompt} has batch size {len(negative_prompt)}, but `prompt`:"
|
||||
f" {prompt} has batch size {batch_size}. Please make sure that passed `negative_prompt` matches"
|
||||
" the batch size of `prompt`."
|
||||
)
|
||||
|
||||
negative_prompt_embeds = self._get_t5_prompt_embeds(
|
||||
prompt=negative_prompt,
|
||||
num_videos_per_prompt=num_videos_per_prompt,
|
||||
max_sequence_length=max_sequence_length,
|
||||
device=device,
|
||||
dtype=dtype,
|
||||
)
|
||||
|
||||
self.profile_stop('text_encoder')
|
||||
return prompt_embeds, negative_prompt_embeds
|
||||
|
||||
def denoise_latents(
|
||||
self,
|
||||
latents: torch.Tensor,
|
||||
prompt_embeds: torch.Tensor,
|
||||
negative_prompt_embeds: Optional[torch.Tensor],
|
||||
timesteps: torch.Tensor,
|
||||
guidance_scale: float,
|
||||
guidance_scale_2: float,
|
||||
transformer_dtype: torch.dtype,
|
||||
num_warmup_steps: int,
|
||||
attention_kwargs: Optional[Dict[str, Any]] = None,
|
||||
callback_on_step_end: Optional[Callable] = None,
|
||||
callback_on_step_end_tensor_inputs: Optional[List[str]] = None,
|
||||
warmup: bool = False,
|
||||
) -> torch.Tensor:
|
||||
boundary_timestep = self.boundary_ratio * self.scheduler.config.num_train_timesteps
|
||||
self.profile_start('denoise', color='blue')
|
||||
|
||||
timestep_stages = []
|
||||
for i, t in enumerate(timesteps):
|
||||
if t >= boundary_timestep:
|
||||
timestep_stages.append((i, t, 'transformer', guidance_scale))
|
||||
else:
|
||||
timestep_stages.append((i, t, 'transformer_2', guidance_scale_2))
|
||||
|
||||
stage_groups = []
|
||||
if timestep_stages:
|
||||
current_group = {
|
||||
'transformer': timestep_stages[0][2],
|
||||
'guidance_scale': timestep_stages[0][3],
|
||||
'timesteps': [(timestep_stages[0][0], timestep_stages[0][1])]
|
||||
}
|
||||
|
||||
for i, t, transformer_name, gs in timestep_stages[1:]:
|
||||
if transformer_name == current_group['transformer']:
|
||||
current_group['timesteps'].append((i, t))
|
||||
else:
|
||||
stage_groups.append(current_group)
|
||||
current_group = {
|
||||
'transformer': transformer_name,
|
||||
'guidance_scale': gs,
|
||||
'timesteps': [(i, t)]
|
||||
}
|
||||
stage_groups.append(current_group)
|
||||
|
||||
self.transformer_steps = sum(len(g['timesteps']) for g in stage_groups if g['transformer'] == 'transformer')
|
||||
self.transformer_2_steps = sum(len(g['timesteps']) for g in stage_groups if g['transformer'] == 'transformer_2')
|
||||
|
||||
with tqdm(total=len(timesteps)) as progress_bar:
|
||||
for stage_group in stage_groups:
|
||||
transformer_name = stage_group['transformer']
|
||||
current_guidance_scale = stage_group['guidance_scale']
|
||||
|
||||
with self.model_memory_manager([transformer_name], low_vram=self.low_vram):
|
||||
for step_index, t in stage_group['timesteps']:
|
||||
latent_model_input = latents.to(transformer_dtype)
|
||||
timestep = t.expand(latent_model_input.shape[0])
|
||||
|
||||
if self.torch_inference or self.torch_fallback[transformer_name]:
|
||||
current_model = self.torch_models[transformer_name]
|
||||
|
||||
noise_pred_cond = current_model(
|
||||
hidden_states=latent_model_input,
|
||||
timestep=timestep,
|
||||
encoder_hidden_states=prompt_embeds,
|
||||
attention_kwargs=attention_kwargs,
|
||||
return_dict=False,
|
||||
)[0]
|
||||
|
||||
if self.do_classifier_free_guidance:
|
||||
noise_pred_uncond = current_model(
|
||||
hidden_states=latent_model_input,
|
||||
timestep=timestep,
|
||||
encoder_hidden_states=negative_prompt_embeds,
|
||||
attention_kwargs=attention_kwargs,
|
||||
return_dict=False,
|
||||
)[0]
|
||||
|
||||
noise_pred = noise_pred_uncond + current_guidance_scale * (noise_pred_cond - noise_pred_uncond)
|
||||
else:
|
||||
noise_pred = noise_pred_cond
|
||||
else:
|
||||
if self.do_classifier_free_guidance:
|
||||
params_cond = {
|
||||
"hidden_states": latent_model_input,
|
||||
"timestep": timestep,
|
||||
"encoder_hidden_states": prompt_embeds,
|
||||
}
|
||||
|
||||
# conditional engine call
|
||||
output_cond = self.run_engine(transformer_name, params_cond)['denoised_latents']
|
||||
|
||||
noise_pred_cond = output_cond.clone()
|
||||
|
||||
params_uncond = {
|
||||
"hidden_states": latent_model_input,
|
||||
"timestep": timestep,
|
||||
"encoder_hidden_states": negative_prompt_embeds,
|
||||
}
|
||||
|
||||
# unconditional engine call
|
||||
output_uncond = self.run_engine(transformer_name, params_uncond)['denoised_latents']
|
||||
|
||||
noise_pred_uncond = output_uncond.clone()
|
||||
|
||||
# Apply classifier-free guidance
|
||||
noise_pred = noise_pred_uncond + current_guidance_scale * (noise_pred_cond - noise_pred_uncond)
|
||||
else:
|
||||
# No CFG
|
||||
params = {
|
||||
"hidden_states": latent_model_input,
|
||||
"timestep": timestep,
|
||||
"encoder_hidden_states": prompt_embeds,
|
||||
}
|
||||
noise_pred = self.run_engine(transformer_name, params)['denoised_latents']
|
||||
|
||||
latents = self.scheduler.step(noise_pred, t, latents, return_dict=False)[0]
|
||||
|
||||
if callback_on_step_end is not None:
|
||||
callback_kwargs = {}
|
||||
for k in callback_on_step_end_tensor_inputs:
|
||||
callback_kwargs[k] = locals()[k]
|
||||
callback_outputs = callback_on_step_end(self, step_index, t, callback_kwargs)
|
||||
|
||||
latents = callback_outputs.pop("latents", latents)
|
||||
prompt_embeds = callback_outputs.pop("prompt_embeds", prompt_embeds)
|
||||
negative_prompt_embeds = callback_outputs.pop("negative_prompt_embeds", negative_prompt_embeds)
|
||||
|
||||
if step_index == len(timesteps) - 1 or ((step_index + 1) > num_warmup_steps and (step_index + 1) % self.scheduler.order == 0):
|
||||
progress_bar.update()
|
||||
|
||||
self.profile_stop('denoise')
|
||||
return latents
|
||||
|
||||
# Adapted from https://github.com/huggingface/diffusers/blob/main/src/diffusers/pipelines/wan/pipeline_wan.py#L324
|
||||
def prepare_latents(
|
||||
self,
|
||||
batch_size: int,
|
||||
num_channels_latents: int = 16,
|
||||
height: int = 720,
|
||||
width: int = 1280,
|
||||
num_frames: int = 81,
|
||||
dtype: Optional[torch.dtype] = None,
|
||||
device: Optional[torch.device] = None,
|
||||
generator: Optional[Union[torch.Generator, List[torch.Generator]]] = None,
|
||||
latents: Optional[torch.Tensor] = None,
|
||||
) -> torch.Tensor:
|
||||
if latents is not None:
|
||||
return latents.to(device=device, dtype=dtype)
|
||||
|
||||
num_latent_frames = (num_frames - 1) // self.vae_scale_factor_temporal + 1
|
||||
shape = (
|
||||
batch_size,
|
||||
num_channels_latents,
|
||||
num_latent_frames,
|
||||
int(height) // self.vae_scale_factor_spatial,
|
||||
int(width) // self.vae_scale_factor_spatial,
|
||||
)
|
||||
if isinstance(generator, list) and len(generator) != batch_size:
|
||||
raise ValueError(
|
||||
f"You have passed a list of generators of length {len(generator)}, but requested an effective batch"
|
||||
f" size of {batch_size}. Make sure the batch size matches the length of the generators."
|
||||
)
|
||||
|
||||
if generator is None and hasattr(self, 'generator'):
|
||||
generator = self.generator
|
||||
|
||||
latents = randn_tensor(shape, generator=generator, device=device, dtype=dtype)
|
||||
return latents
|
||||
|
||||
# Copied from https://github.com/huggingface/diffusers/blob/main/src/diffusers/pipelines/wan/pipeline_wan.py#L279
|
||||
def check_inputs(
|
||||
self,
|
||||
prompt,
|
||||
negative_prompt,
|
||||
height,
|
||||
width,
|
||||
prompt_embeds=None,
|
||||
negative_prompt_embeds=None,
|
||||
callback_on_step_end_tensor_inputs=None,
|
||||
guidance_scale_2=None,
|
||||
):
|
||||
if height % 16 != 0 or width % 16 != 0:
|
||||
raise ValueError(f"`height` and `width` have to be divisible by 16 but are {height} and {width}.")
|
||||
|
||||
if callback_on_step_end_tensor_inputs is not None:
|
||||
pass
|
||||
|
||||
if prompt is not None and prompt_embeds is not None:
|
||||
raise ValueError(
|
||||
f"Cannot forward both `prompt`: {prompt} and `prompt_embeds`: {prompt_embeds}. Please make sure to"
|
||||
" only forward one of the two."
|
||||
)
|
||||
elif negative_prompt is not None and negative_prompt_embeds is not None:
|
||||
raise ValueError(
|
||||
f"Cannot forward both `negative_prompt`: {negative_prompt} and `negative_prompt_embeds`: {negative_prompt_embeds}. Please make sure to"
|
||||
" only forward one of the two."
|
||||
)
|
||||
elif prompt is None and prompt_embeds is None:
|
||||
raise ValueError(
|
||||
"Provide either `prompt` or `prompt_embeds`. Cannot leave both `prompt` and `prompt_embeds` undefined."
|
||||
)
|
||||
elif prompt is not None and (not isinstance(prompt, str) and not isinstance(prompt, list)):
|
||||
raise ValueError(f"`prompt` has to be of type `str` or `list` but is {type(prompt)}")
|
||||
elif negative_prompt is not None and (
|
||||
not isinstance(negative_prompt, str) and not isinstance(negative_prompt, list)
|
||||
):
|
||||
raise ValueError(f"`negative_prompt` has to be of type `str` or `list` but is {type(negative_prompt)}")
|
||||
|
||||
# Adapted from https://github.com/huggingface/diffusers/blob/main/src/diffusers/pipelines/wan/pipeline_wan.py#L157
|
||||
def _get_t5_prompt_embeds(
|
||||
self,
|
||||
prompt: Union[str, List[str]] = None,
|
||||
num_videos_per_prompt: int = 1,
|
||||
max_sequence_length: int = 226,
|
||||
device: Optional[torch.device] = None,
|
||||
dtype: Optional[torch.dtype] = None,
|
||||
encoder: str = "text_encoder",
|
||||
):
|
||||
device = device or self._execution_device
|
||||
dtype = dtype or (self.torch_models[encoder].dtype if self.torch_fallback.get(encoder) and encoder in self.torch_models else torch.float32)
|
||||
|
||||
prompt = [prompt] if isinstance(prompt, str) else prompt
|
||||
prompt = [prompt_clean(u) for u in prompt]
|
||||
batch_size = len(prompt)
|
||||
|
||||
text_inputs = self.tokenizer(
|
||||
prompt,
|
||||
padding="max_length",
|
||||
max_length=max_sequence_length,
|
||||
truncation=True,
|
||||
add_special_tokens=True,
|
||||
return_attention_mask=True,
|
||||
return_tensors="pt",
|
||||
)
|
||||
text_input_ids, mask = text_inputs.input_ids, text_inputs.attention_mask
|
||||
seq_lens = mask.gt(0).sum(dim=1).long()
|
||||
|
||||
if self.torch_inference or self.torch_fallback[encoder]:
|
||||
outputs = self.torch_models[encoder](text_input_ids.to(device), mask.to(device))
|
||||
prompt_embeds = outputs.last_hidden_state.clone()
|
||||
else:
|
||||
outputs = self.run_engine(encoder, {
|
||||
"input_ids": text_input_ids.to(device),
|
||||
"attention_mask": mask.to(device)
|
||||
})
|
||||
prompt_embeds = outputs['text_embeddings'].clone()
|
||||
|
||||
prompt_embeds = prompt_embeds.to(dtype=dtype, device=device)
|
||||
|
||||
prompt_embeds = [u[:v] for u, v in zip(prompt_embeds, seq_lens)]
|
||||
prompt_embeds = torch.stack(
|
||||
[torch.cat([u, u.new_zeros(max_sequence_length - u.size(0), u.size(1))]) for u in prompt_embeds], dim=0
|
||||
)
|
||||
|
||||
_, seq_len, _ = prompt_embeds.shape
|
||||
prompt_embeds = prompt_embeds.repeat(1, num_videos_per_prompt, 1)
|
||||
prompt_embeds = prompt_embeds.view(batch_size * num_videos_per_prompt, seq_len, -1)
|
||||
|
||||
return prompt_embeds
|
||||
|
||||
@property
|
||||
def _execution_device(self):
|
||||
return self.device
|
||||
|
||||
@property
|
||||
def do_classifier_free_guidance(self):
|
||||
return self.guidance_scale > 1.0
|
||||
|
||||
@property
|
||||
def num_timesteps(self):
|
||||
return self._num_timesteps
|
||||
|
||||
def decode_latents(self, latents, num_frames):
|
||||
self.profile_start('vae_decoder', color='red')
|
||||
|
||||
vae_config = self.models['vae_decoder'].config
|
||||
z_dim = vae_config.get("z_dim", 16)
|
||||
|
||||
vae_dtype = torch.float32
|
||||
latents = latents.to(vae_dtype)
|
||||
|
||||
latents_mean = (
|
||||
torch.tensor(vae_config.get("latents_mean"))
|
||||
.view(1, z_dim, 1, 1, 1)
|
||||
.to(latents.device, latents.dtype)
|
||||
)
|
||||
latents_std = 1.0 / torch.tensor(vae_config.get("latents_std")).view(1, z_dim, 1, 1, 1).to(
|
||||
latents.device, latents.dtype
|
||||
)
|
||||
|
||||
latents = latents / latents_std + latents_mean
|
||||
|
||||
frames = self.torch_models['vae_decoder'].decode(latents, return_dict=False)[0]
|
||||
|
||||
self.profile_stop('vae_decoder')
|
||||
return frames
|
||||
|
||||
def postprocess(self, video: torch.Tensor, output_type: str = "pil"):
|
||||
# Convert [F, C, H, W] -> [F, H, W, C]
|
||||
video = video.permute(0, 2, 3, 1)
|
||||
# Convert to list of PIL Images
|
||||
video = (video + 1.0) / 2.0
|
||||
video = torch.clamp(video, 0.0, 1.0)
|
||||
video = (video * 255.0).to(torch.uint8).cpu().numpy()
|
||||
pil_frames = [Image.fromarray(frame) for frame in video]
|
||||
return pil_frames
|
||||
|
||||
def infer(
|
||||
self,
|
||||
prompt: Union[str, List[str]],
|
||||
negative_prompt: Optional[Union[str, List[str]]] = None,
|
||||
height: int = 720,
|
||||
width: int = 1280,
|
||||
num_frames: int = 81,
|
||||
num_inference_steps: int = 40,
|
||||
num_videos_per_prompt: int = 1,
|
||||
generator: Optional[Union[torch.Generator, List[torch.Generator]]] = None,
|
||||
latents: Optional[torch.Tensor] = None,
|
||||
prompt_embeds: Optional[torch.Tensor] = None,
|
||||
negative_prompt_embeds: Optional[torch.Tensor] = None,
|
||||
output_type: str = "pil",
|
||||
attention_kwargs: Optional[Dict[str, Any]] = None,
|
||||
callback_on_step_end: Optional[Union[Callable, Any]] = None,
|
||||
callback_on_step_end_tensor_inputs: List[str] = ["latents"],
|
||||
max_sequence_length: int = 512,
|
||||
warmup: bool = False,
|
||||
save_video: bool = True,
|
||||
):
|
||||
"""
|
||||
Run the Wan text-to-video diffusion pipeline.
|
||||
"""
|
||||
|
||||
self.check_inputs(
|
||||
prompt,
|
||||
negative_prompt,
|
||||
height,
|
||||
width,
|
||||
prompt_embeds,
|
||||
negative_prompt_embeds,
|
||||
callback_on_step_end_tensor_inputs,
|
||||
self.guidance_scale_2,
|
||||
)
|
||||
|
||||
if num_frames % self.vae_scale_factor_temporal != 1:
|
||||
print(f"[W] `num_frames - 1` has to be divisible by {self.vae_scale_factor_temporal}. Rounding to the nearest number.")
|
||||
num_frames = num_frames // self.vae_scale_factor_temporal * self.vae_scale_factor_temporal + 1
|
||||
num_frames = max(num_frames, 1)
|
||||
|
||||
device = self._execution_device
|
||||
batch_size = 1
|
||||
|
||||
with torch.inference_mode(), trt.Runtime(TRT_LOGGER):
|
||||
torch.cuda.synchronize()
|
||||
e2e_tic = time.perf_counter()
|
||||
|
||||
with self.model_memory_manager(["text_encoder"], low_vram=self.low_vram):
|
||||
prompt_embeds, negative_prompt_embeds = self.encode_prompt(
|
||||
prompt=prompt,
|
||||
negative_prompt=negative_prompt,
|
||||
do_classifier_free_guidance=self.do_classifier_free_guidance,
|
||||
num_videos_per_prompt=num_videos_per_prompt,
|
||||
prompt_embeds=prompt_embeds,
|
||||
negative_prompt_embeds=negative_prompt_embeds,
|
||||
max_sequence_length=max_sequence_length,
|
||||
device=device,
|
||||
)
|
||||
|
||||
transformer_dtype = torch.bfloat16
|
||||
prompt_embeds = prompt_embeds.to(transformer_dtype)
|
||||
if negative_prompt_embeds is not None:
|
||||
negative_prompt_embeds = negative_prompt_embeds.to(transformer_dtype)
|
||||
|
||||
self.scheduler.set_timesteps(num_inference_steps, device=self.device)
|
||||
timesteps = self.scheduler.timesteps
|
||||
|
||||
num_channels_latents = self.models["transformer"].config.get("in_channels", 16)
|
||||
latents = self.prepare_latents(
|
||||
batch_size * num_videos_per_prompt,
|
||||
num_channels_latents,
|
||||
height,
|
||||
width,
|
||||
num_frames,
|
||||
torch.float32,
|
||||
device,
|
||||
generator,
|
||||
latents,
|
||||
)
|
||||
|
||||
num_warmup_steps = len(timesteps) - num_inference_steps * self.scheduler.order
|
||||
self._num_timesteps = len(timesteps)
|
||||
|
||||
latents = self.denoise_latents(
|
||||
latents=latents,
|
||||
prompt_embeds=prompt_embeds,
|
||||
negative_prompt_embeds=negative_prompt_embeds,
|
||||
timesteps=timesteps,
|
||||
guidance_scale=self.guidance_scale,
|
||||
guidance_scale_2=self.guidance_scale_2,
|
||||
transformer_dtype=transformer_dtype,
|
||||
num_warmup_steps=num_warmup_steps,
|
||||
attention_kwargs=attention_kwargs,
|
||||
callback_on_step_end=callback_on_step_end,
|
||||
callback_on_step_end_tensor_inputs=callback_on_step_end_tensor_inputs,
|
||||
warmup=warmup,
|
||||
)
|
||||
|
||||
with self.model_memory_manager(["vae_decoder"], low_vram=self.low_vram):
|
||||
video_raw = self.decode_latents(latents, num_frames)
|
||||
video = image_module.tensor2vid(video_raw, self, output_type="pil")
|
||||
|
||||
torch.cuda.synchronize()
|
||||
e2e_toc = time.perf_counter()
|
||||
|
||||
walltime_ms = (e2e_toc - e2e_tic) * 1000.0
|
||||
if not warmup:
|
||||
self.print_summary(num_inference_steps, walltime_ms, batch_size, num_frames)
|
||||
if save_video:
|
||||
self.save_video(video[0], self.pipeline_type.name.lower(), prompt, self.seed)
|
||||
|
||||
return video, walltime_ms
|
||||
|
||||
def run(self, prompt, height, width, num_frames, batch_size, batch_count, num_warmup_runs, 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 ..")
|
||||
for _ in range(num_warmup_runs):
|
||||
self.infer(prompt, height=height, width=width, num_frames=num_frames, warmup=True, **kwargs)
|
||||
|
||||
for _ in range(batch_count):
|
||||
print("[I] Running Wan T2V pipeline")
|
||||
if self.nvtx_profile:
|
||||
cudart.cudaProfilerStart()
|
||||
self.infer(prompt, height=height, width=width, num_frames=num_frames, warmup=False, **kwargs)
|
||||
if self.nvtx_profile:
|
||||
cudart.cudaProfilerStop()
|
||||
+853
@@ -0,0 +1,853 @@
|
||||
#
|
||||
# SPDX-FileCopyrightText: Copyright (c) 1993-2025 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 os
|
||||
import re
|
||||
from collections import defaultdict
|
||||
from random import choice, shuffle
|
||||
from typing import Set
|
||||
|
||||
import modelopt.torch.quantization as mtq
|
||||
import numpy as np
|
||||
import onnx_graphsurgeon as gs
|
||||
import torch
|
||||
import torch.nn.functional as F
|
||||
from diffusers.models.attention_processor import (
|
||||
Attention,
|
||||
AttnProcessor,
|
||||
FluxAttnProcessor2_0,
|
||||
)
|
||||
from diffusers.models.lora import LoRACompatibleConv, LoRACompatibleLinear
|
||||
from modelopt.torch.quantization import utils as quant_utils
|
||||
from modelopt.torch.quantization.calib.max import MaxCalibrator
|
||||
from PIL import Image
|
||||
from torch.utils.data import Dataset, Sampler
|
||||
|
||||
import onnx
|
||||
|
||||
USE_PEFT = True
|
||||
try:
|
||||
from peft.tuners.lora.layer import Conv2d as PEFTLoRAConv2d
|
||||
from peft.tuners.lora.layer import Linear as PEFTLoRALinear
|
||||
except ModuleNotFoundError:
|
||||
USE_PEFT = False
|
||||
|
||||
class PercentileCalibrator(MaxCalibrator):
|
||||
def __init__(self, num_bits=8, axis=None, unsigned=False, track_amax=False, **kwargs):
|
||||
super().__init__(num_bits, axis, unsigned, track_amax)
|
||||
self.percentile = kwargs["percentile"]
|
||||
self.total_step = kwargs["total_step"]
|
||||
self.collect_method = kwargs["collect_method"]
|
||||
self.data = {}
|
||||
self.i = 0
|
||||
|
||||
def collect(self, x):
|
||||
"""Tracks the absolute max of all tensors.
|
||||
|
||||
Args:
|
||||
x: A tensor
|
||||
|
||||
Raises:
|
||||
RuntimeError: If amax shape changes
|
||||
"""
|
||||
# Swap axis to reduce.
|
||||
axis = self._axis if isinstance(self._axis, (list, tuple)) else [self._axis]
|
||||
# Handle negative axis.
|
||||
axis = [x.dim() + i if isinstance(i, int) and i < 0 else i for i in axis]
|
||||
reduce_axis = []
|
||||
for i in range(x.dim()):
|
||||
if i not in axis:
|
||||
reduce_axis.append(i)
|
||||
local_amax = quant_utils.reduce_amax(x, axis=reduce_axis).detach()
|
||||
_cur_step = self.i % self.total_step
|
||||
if _cur_step not in self.data.keys():
|
||||
self.data[_cur_step] = local_amax
|
||||
else:
|
||||
if self.collect_method == "global_min":
|
||||
self.data[_cur_step] = torch.min(self.data[_cur_step], local_amax)
|
||||
elif self.collect_method == "min-max" or self.collect_method == "mean-max":
|
||||
self.data[_cur_step] = torch.max(self.data[_cur_step], local_amax)
|
||||
else:
|
||||
self.data[_cur_step] += local_amax
|
||||
if self._track_amax:
|
||||
raise NotImplementedError
|
||||
self.i += 1
|
||||
|
||||
def compute_amax(self):
|
||||
"""Return the absolute max of all tensors collected."""
|
||||
up_lim = int(self.total_step * self.percentile)
|
||||
if self.collect_method == "min-mean":
|
||||
amaxs_values = [self.data[i] / self.total_step for i in range(0, up_lim)]
|
||||
else:
|
||||
amaxs_values = [self.data[i] for i in range(0, up_lim)]
|
||||
if self.collect_method == "mean-max":
|
||||
act_amax = torch.vstack(amaxs_values).mean(axis=0)[0]
|
||||
else:
|
||||
act_amax = torch.vstack(amaxs_values).min(axis=0)[0]
|
||||
self._calib_amax = act_amax
|
||||
return self._calib_amax
|
||||
|
||||
def __str__(self):
|
||||
s = "PercentileCalibrator"
|
||||
return s.format(**self.__dict__)
|
||||
|
||||
def __repr__(self):
|
||||
s = "PercentileCalibrator("
|
||||
s += super(MaxCalibrator, self).__repr__()
|
||||
s += " calib_amax={_calib_amax}"
|
||||
if self._track_amax:
|
||||
s += " amaxs={_amaxs}"
|
||||
s += ")"
|
||||
return s.format(**self.__dict__)
|
||||
|
||||
def filter_func(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|proj_out).*"
|
||||
)
|
||||
return pattern.match(name) is not None
|
||||
|
||||
def filter_func_no_proj_out(name): # used for Flux
|
||||
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|x_embedder).*"
|
||||
)
|
||||
return pattern.match(name) is not None
|
||||
|
||||
def quantize_lvl(model_id, backbone, 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
|
||||
if we didn't add that unwanted layer into the config during the calibration
|
||||
"""
|
||||
for name, module in backbone.named_modules():
|
||||
if isinstance(module, torch.nn.Conv2d):
|
||||
if linear_only:
|
||||
module.input_quantizer.disable()
|
||||
module.weight_quantizer.disable()
|
||||
else:
|
||||
module.input_quantizer.enable()
|
||||
module.weight_quantizer.enable()
|
||||
elif isinstance(module, torch.nn.Linear):
|
||||
if (
|
||||
(quant_level >= 2 and "ff.net" in name)
|
||||
or (quant_level >= 2.5 and ("to_q" in name or "to_k" in name or "to_v" in name))
|
||||
or quant_level >= 3
|
||||
) and name != "proj_out": # Disable the final output layer from flux model
|
||||
module.input_quantizer.enable()
|
||||
module.weight_quantizer.enable()
|
||||
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)
|
||||
if quant_level >= 4 and head_size % 16 == 0:
|
||||
module.q_bmm_quantizer.enable()
|
||||
module.k_bmm_quantizer.enable()
|
||||
module.v_bmm_quantizer.enable()
|
||||
module.softmax_quantizer.enable()
|
||||
if model_id.startswith("flux.1"):
|
||||
if name.startswith("transformer_blocks"):
|
||||
module.bmm2_output_quantizer.enable()
|
||||
else:
|
||||
module.bmm2_output_quantizer.disable()
|
||||
setattr(module, "_disable_fp8_mha", False)
|
||||
else:
|
||||
module.q_bmm_quantizer.disable()
|
||||
module.k_bmm_quantizer.disable()
|
||||
module.v_bmm_quantizer.disable()
|
||||
module.softmax_quantizer.disable()
|
||||
module.bmm2_output_quantizer.disable()
|
||||
setattr(module, "_disable_fp8_mha", True)
|
||||
|
||||
def fp8_mha_disable(backbone, quantized_mha_output: bool = True):
|
||||
def mha_filter_func(name):
|
||||
pattern = re.compile(
|
||||
r".*(q_bmm_quantizer|k_bmm_quantizer|v_bmm_quantizer|softmax_quantizer).*"
|
||||
if quantized_mha_output
|
||||
else r".*(q_bmm_quantizer|k_bmm_quantizer|v_bmm_quantizer|softmax_quantizer|bmm2_output_quantizer).*"
|
||||
)
|
||||
return pattern.match(name) is not None
|
||||
|
||||
if hasattr(F, "scaled_dot_product_attention"):
|
||||
mtq.disable_quantizer(backbone, mha_filter_func)
|
||||
|
||||
def get_int8_config(
|
||||
model,
|
||||
quant_level=3,
|
||||
alpha=0.8,
|
||||
percentile=1.0,
|
||||
num_inference_steps=20,
|
||||
collect_method="min-mean",
|
||||
):
|
||||
quant_config = {
|
||||
"quant_cfg": {
|
||||
"*lm_head*": {"enable": False},
|
||||
"*output_layer*": {"enable": False},
|
||||
"*output_quantizer": {"enable": False},
|
||||
"default": {"num_bits": 8, "axis": None},
|
||||
},
|
||||
"algorithm": {"method": "smoothquant", "alpha": alpha},
|
||||
}
|
||||
for name, module in model.named_modules():
|
||||
w_name = f"{name}*weight_quantizer"
|
||||
i_name = f"{name}*input_quantizer"
|
||||
|
||||
if w_name in quant_config["quant_cfg"].keys() or i_name in quant_config["quant_cfg"].keys():
|
||||
continue
|
||||
if filter_func(name):
|
||||
continue
|
||||
if isinstance(module, (torch.nn.Linear, LoRACompatibleLinear)):
|
||||
if (
|
||||
(quant_level >= 2 and "ff.net" in name)
|
||||
or (quant_level >= 2.5 and ("to_q" in name or "to_k" in name or "to_v" in name))
|
||||
or quant_level == 3
|
||||
):
|
||||
quant_config["quant_cfg"][w_name] = {"num_bits": 8, "axis": 0}
|
||||
quant_config["quant_cfg"][i_name] = {"num_bits": 8, "axis": -1}
|
||||
elif isinstance(module, (torch.nn.Conv2d, LoRACompatibleConv)):
|
||||
quant_config["quant_cfg"][w_name] = {"num_bits": 8, "axis": 0}
|
||||
quant_config["quant_cfg"][i_name] = {
|
||||
"num_bits": 8,
|
||||
"axis": None,
|
||||
"calibrator": (
|
||||
PercentileCalibrator,
|
||||
(),
|
||||
{
|
||||
"num_bits": 8,
|
||||
"axis": None,
|
||||
"percentile": percentile,
|
||||
"total_step": num_inference_steps,
|
||||
"collect_method": collect_method,
|
||||
},
|
||||
),
|
||||
}
|
||||
return quant_config
|
||||
|
||||
SD_FP8_FP16_DEFAULT_CONFIG = {
|
||||
"quant_cfg": {
|
||||
"*weight_quantizer": {"num_bits": (4, 3), "axis": None, "trt_high_precision_dtype": "Half"},
|
||||
"*input_quantizer": {"num_bits": (4, 3), "axis": None, "trt_high_precision_dtype": "Half"},
|
||||
"*output_quantizer": {"enable": False},
|
||||
"*q_bmm_quantizer": {"num_bits": (4, 3), "axis": None, "trt_high_precision_dtype": "Half"},
|
||||
"*k_bmm_quantizer": {"num_bits": (4, 3), "axis": None, "trt_high_precision_dtype": "Half"},
|
||||
"*v_bmm_quantizer": {"num_bits": (4, 3), "axis": None, "trt_high_precision_dtype": "Half"},
|
||||
"*softmax_quantizer": {
|
||||
"num_bits": (4, 3),
|
||||
"axis": None,
|
||||
"trt_high_precision_dtype": "Half",
|
||||
},
|
||||
"default": {"enable": False},
|
||||
},
|
||||
"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_BF16_FLUX_MMDIT_BMM2_FP8_OUTPUT_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",
|
||||
},
|
||||
"transformer_blocks*bmm2_output_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"},
|
||||
"*input_quantizer": {"num_bits": (4, 3), "axis": None, "trt_high_precision_dtype": "Float"},
|
||||
"*output_quantizer": {"enable": False},
|
||||
"*q_bmm_quantizer": {"num_bits": (4, 3), "axis": None, "trt_high_precision_dtype": "Float"},
|
||||
"*k_bmm_quantizer": {"num_bits": (4, 3), "axis": None, "trt_high_precision_dtype": "Float"},
|
||||
"*v_bmm_quantizer": {"num_bits": (4, 3), "axis": None, "trt_high_precision_dtype": "Float"},
|
||||
"*softmax_quantizer": {
|
||||
"num_bits": (4, 3),
|
||||
"axis": None,
|
||||
"trt_high_precision_dtype": "Float",
|
||||
},
|
||||
"default": {"enable": False},
|
||||
},
|
||||
"algorithm": "max",
|
||||
}
|
||||
|
||||
def set_fmha(denoiser, is_flux=False):
|
||||
for name, module in denoiser.named_modules():
|
||||
if isinstance(module, Attention):
|
||||
if is_flux:
|
||||
module.set_processor(FluxAttnProcessor2_0())
|
||||
else:
|
||||
module.set_processor(AttnProcessor())
|
||||
|
||||
def check_lora(model):
|
||||
for name, module in model.named_modules():
|
||||
if isinstance(module, (LoRACompatibleConv, LoRACompatibleLinear)):
|
||||
assert (
|
||||
module.lora_layer is None
|
||||
), f"To quantize {name}, LoRA layer should be fused/merged. Please fuse the LoRA layer before quantization."
|
||||
elif USE_PEFT and isinstance(module, (PEFTLoRAConv2d, PEFTLoRALinear)):
|
||||
assert (
|
||||
module.merged
|
||||
), f"To quantize {name}, LoRA layer should be fused/merged. Please fuse the LoRA layer before quantization."
|
||||
|
||||
def generate_fp8_scales(unet):
|
||||
# temporary solution due to a known bug in torch.onnx._dynamo_export
|
||||
for _, module in unet.named_modules():
|
||||
if isinstance(module, (torch.nn.Linear, torch.nn.Conv2d)) and (
|
||||
hasattr(module.input_quantizer, "_amax") and module.input_quantizer is not None
|
||||
):
|
||||
module.input_quantizer._num_bits = 8
|
||||
module.weight_quantizer._num_bits = 8
|
||||
module.input_quantizer._amax = module.input_quantizer._amax * (127 / 448.0)
|
||||
module.weight_quantizer._amax = module.weight_quantizer._amax * (127 / 448.0)
|
||||
elif isinstance(module, Attention) and (
|
||||
hasattr(module.q_bmm_quantizer, "_amax")
|
||||
and module.q_bmm_quantizer is not None
|
||||
and hasattr(module.k_bmm_quantizer, "_amax")
|
||||
and module.k_bmm_quantizer is not None
|
||||
and hasattr(module.v_bmm_quantizer, "_amax")
|
||||
and module.v_bmm_quantizer is not None
|
||||
and hasattr(module.softmax_quantizer, "_amax")
|
||||
and module.softmax_quantizer is not None
|
||||
):
|
||||
module.q_bmm_quantizer._num_bits = 8
|
||||
module.q_bmm_quantizer._amax = module.q_bmm_quantizer._amax * (127 / 448.0)
|
||||
module.k_bmm_quantizer._num_bits = 8
|
||||
module.k_bmm_quantizer._amax = module.k_bmm_quantizer._amax * (127 / 448.0)
|
||||
module.v_bmm_quantizer._num_bits = 8
|
||||
module.v_bmm_quantizer._amax = module.v_bmm_quantizer._amax * (127 / 448.0)
|
||||
module.softmax_quantizer._num_bits = 8
|
||||
module.softmax_quantizer._amax = module.softmax_quantizer._amax * (127 / 448.0)
|
||||
|
||||
def get_parent_nodes(node):
|
||||
"""
|
||||
Returns list of input producer nodes for the given node.
|
||||
"""
|
||||
parents = []
|
||||
for tensor in node.inputs:
|
||||
# If the tensor is not a constant or graph input and has a producer,
|
||||
# the producer is a parent of node `node`
|
||||
if len(tensor.inputs) == 1:
|
||||
parents.append(tensor.inputs[0])
|
||||
return parents
|
||||
|
||||
def get_child_nodes(node):
|
||||
"""
|
||||
Returns list of output consumer nodes for the given node.
|
||||
"""
|
||||
children = []
|
||||
for tensor in node.outputs:
|
||||
for consumer in tensor.outputs: # Traverse all consumer of the tensor
|
||||
children.append(consumer)
|
||||
return children
|
||||
|
||||
def has_path_type(node, graph, path_type, is_forward, wild_card_types, path_nodes):
|
||||
"""
|
||||
Return pattern nodes for the given path_type.
|
||||
"""
|
||||
if not path_type:
|
||||
# All types matched
|
||||
return True
|
||||
|
||||
# Check if current non-wild node type does not match the expected path type
|
||||
node_type = node.op
|
||||
is_match = node_type == path_type[0]
|
||||
is_wild_match = node_type in wild_card_types
|
||||
if not is_match and not is_wild_match:
|
||||
return False
|
||||
|
||||
if is_match:
|
||||
path_nodes.append(node)
|
||||
next_path_type = path_type[1:]
|
||||
else:
|
||||
next_path_type = path_type[:]
|
||||
|
||||
if is_forward:
|
||||
next_level_nodes = get_child_nodes(node)
|
||||
else:
|
||||
next_level_nodes = get_parent_nodes(node)
|
||||
|
||||
# Check if any child (forward path) or parent (backward path) can match the remaining path types
|
||||
for next_node in next_level_nodes:
|
||||
sub_path = []
|
||||
if has_path_type(next_node, graph, next_path_type, is_forward, wild_card_types, sub_path):
|
||||
path_nodes.extend(sub_path)
|
||||
return True
|
||||
|
||||
# Path type matches if there is no remaining types to match
|
||||
return not next_path_type
|
||||
|
||||
def insert_cast(graph, input_tensor, attrs):
|
||||
"""
|
||||
Create a cast layer using tensor as input.
|
||||
"""
|
||||
output_tensor = gs.Variable(name=f"{input_tensor.name}/Cast_output", dtype=attrs["to"])
|
||||
next_node_list = input_tensor.outputs.copy()
|
||||
graph.layer(
|
||||
op="Cast",
|
||||
name=f"{input_tensor.name}/Cast",
|
||||
inputs=[input_tensor],
|
||||
outputs=[output_tensor],
|
||||
attrs=attrs,
|
||||
)
|
||||
|
||||
# use cast output as input to next node
|
||||
for next_node in next_node_list:
|
||||
for idx, next_input in enumerate(next_node.inputs):
|
||||
if next_input.name == input_tensor.name:
|
||||
next_node.inputs[idx] = output_tensor
|
||||
|
||||
def cast_layernorm_io(graph):
|
||||
"""
|
||||
Cast LayerNormalization scale and bias inputs from FP16 to FP32.
|
||||
In INT8 quantized graphs, DequantizeLinear outputs Float32 activations,
|
||||
but LayerNorm scale/bias remain FP16 from the original model, causing
|
||||
a type mismatch with --strongly-typed TensorRT builds.
|
||||
"""
|
||||
layernorm_nodes = [node for node in graph.nodes if node.op == "LayerNormalization"]
|
||||
|
||||
print(f"Found {len(layernorm_nodes)} LayerNormalization nodes to fix")
|
||||
for node in layernorm_nodes:
|
||||
# LayerNormalization inputs: 0=X (data), 1=Scale, 2=B (bias, optional)
|
||||
for i in range(1, len(node.inputs)):
|
||||
input_tensor = node.inputs[i]
|
||||
if input_tensor.name and hasattr(input_tensor, 'dtype') and input_tensor.dtype == np.float16:
|
||||
insert_cast(graph, input_tensor=input_tensor, attrs={"to": np.float32})
|
||||
|
||||
def cast_convtranspose_io(graph):
|
||||
"""
|
||||
Fix ConvTranspose input/output type mismatches for strongly-typed TRT builds.
|
||||
In mixed-precision graphs (e.g. BF16 Stable Cascade VQGAN), architectural FP16->FP32
|
||||
casts can leave a ConvTranspose with a FP32 activation input but FP16 kernel weights.
|
||||
We cast the activation to match the kernel dtype, then cast the output back to the
|
||||
original activation dtype so surrounding FP32 ops (e.g. residual Add) are unaffected.
|
||||
"""
|
||||
convtranspose_nodes = [node for node in graph.nodes if node.op == "ConvTranspose"]
|
||||
fixed = 0
|
||||
for node in convtranspose_nodes:
|
||||
if len(node.inputs) < 2:
|
||||
continue
|
||||
act_input = node.inputs[0]
|
||||
kernel = node.inputs[1]
|
||||
if act_input.dtype is None or kernel.dtype is None or act_input.dtype == kernel.dtype:
|
||||
continue
|
||||
orig_dtype = act_input.dtype # e.g. np.dtype('float32')
|
||||
target_dtype = kernel.dtype.type # e.g. np.float16
|
||||
insert_cast(graph, input_tensor=act_input, attrs={"to": target_dtype})
|
||||
# Update the output dtype to match and cast back, so downstream FP32 ops are unaffected.
|
||||
for out in node.outputs:
|
||||
if out.name and out.dtype == orig_dtype:
|
||||
out.dtype = target_dtype
|
||||
insert_cast(graph, input_tensor=out, attrs={"to": orig_dtype.type})
|
||||
fixed += 1
|
||||
print(f"Fixed {fixed} ConvTranspose input/output type mismatches")
|
||||
|
||||
|
||||
def convert_zp_fp8(onnx_graph):
|
||||
"""
|
||||
Convert Q/DQ zero datatype from INT8 to FP8.
|
||||
"""
|
||||
# Find all zero constant nodes
|
||||
qdq_zero_nodes = set()
|
||||
for node in onnx_graph.graph.node:
|
||||
if node.op_type == "QuantizeLinear":
|
||||
if len(node.input) > 2:
|
||||
qdq_zero_nodes.add(node.input[2])
|
||||
|
||||
print(f"Found {len(qdq_zero_nodes)} QDQ pairs")
|
||||
|
||||
# Convert zero point datatype from INT8 to FP8.
|
||||
for node in onnx_graph.graph.node:
|
||||
if node.output[0] in qdq_zero_nodes:
|
||||
node.attribute[0].t.data_type = onnx.TensorProto.FLOAT8E4M3FN
|
||||
|
||||
return onnx_graph
|
||||
|
||||
def cast_resize_io(graph, output_dtype=np.float16):
|
||||
"""
|
||||
Add cast nodes to Resize nodes I/O because Resize needs to be run in fp32.
|
||||
Inputs are cast to FP32, outputs are cast back to output_dtype (FP16 or BF16).
|
||||
"""
|
||||
resize_nodes = [node for node in graph.nodes if node.op == "Resize"]
|
||||
|
||||
print(f"Found {len(resize_nodes)} Resize nodes to fix")
|
||||
for resize_node in resize_nodes:
|
||||
# Skip Resize nodes whose data input is already FP32 — no casting needed.
|
||||
if resize_node.inputs[0].dtype == np.float32:
|
||||
continue
|
||||
for i, input_tensor in enumerate(resize_node.inputs):
|
||||
SIZES_INPUT_INDEX = 3 # Optional input "sizes" at index 3 must be in INT64. Skip cast for this input.
|
||||
if i != SIZES_INPUT_INDEX and input_tensor.name:
|
||||
insert_cast(graph, input_tensor=input_tensor, attrs={"to": np.float32})
|
||||
for output_tensor in resize_node.outputs:
|
||||
if output_tensor.name:
|
||||
insert_cast(graph, input_tensor=output_tensor, attrs={"to": output_dtype})
|
||||
|
||||
def cast_fp8_mha_io(graph):
|
||||
r"""
|
||||
Insert three cast ops.
|
||||
The first cast will be added before the input0 of MatMul to cast fp16 to fp32.
|
||||
The second cast will be added before the input1 of MatMul to cast fp16 to fp32.
|
||||
The third cast will be added after the output of MatMul to cast fp32 back to fp16.
|
||||
Q Q
|
||||
| |
|
||||
DQ DQ
|
||||
| |
|
||||
Cast Cast
|
||||
(fp16 to fp32) (fp16 to fp32)
|
||||
\ /
|
||||
\ /
|
||||
\ /
|
||||
MatMul
|
||||
|
|
||||
Cast (fp32 to fp16)
|
||||
|
|
||||
Q
|
||||
|
|
||||
DQ
|
||||
The insertion of Cast ops in the FP8 MHA part actually forbids the MHAs to run
|
||||
with FP16 accumulation because TensorRT only has FP32 accumulation kernels for FP8 MHAs.
|
||||
"""
|
||||
# Find FP8 MHA pattern.
|
||||
# Match FP8 MHA: Q -> DQ -> BMM1 -> (Mul/Div) -> (Add) -> Softmax -> (Cast) -> Q -> DQ -> BMM2 -> Q -> DQ
|
||||
softmax_bmm1_chain_type = ["Softmax", "MatMul", "DequantizeLinear", "QuantizeLinear"]
|
||||
softmax_bmm2_chain_type = [
|
||||
"Softmax",
|
||||
"QuantizeLinear",
|
||||
"DequantizeLinear",
|
||||
"MatMul",
|
||||
"QuantizeLinear",
|
||||
"DequantizeLinear",
|
||||
]
|
||||
wild_card_types = [
|
||||
"Div",
|
||||
"Mul",
|
||||
"ConstMul",
|
||||
"Add",
|
||||
"BiasAdd",
|
||||
"Reshape",
|
||||
"Transpose",
|
||||
"Flatten",
|
||||
"Cast",
|
||||
]
|
||||
|
||||
fp8_mha_partitions = []
|
||||
for node in graph.nodes:
|
||||
if node.op == "Softmax":
|
||||
fp8_mha_partition = []
|
||||
if has_path_type(
|
||||
node, graph, softmax_bmm1_chain_type, False, wild_card_types, fp8_mha_partition
|
||||
) and has_path_type(
|
||||
node, graph, softmax_bmm2_chain_type, True, wild_card_types, fp8_mha_partition
|
||||
):
|
||||
if (
|
||||
len(fp8_mha_partition) == 10
|
||||
and fp8_mha_partition[1].op == "MatMul"
|
||||
and fp8_mha_partition[7].op == "MatMul"
|
||||
):
|
||||
fp8_mha_partitions.append(fp8_mha_partition)
|
||||
|
||||
print(f"Found {len(fp8_mha_partitions)} FP8 attentions")
|
||||
|
||||
# Insert Cast nodes for BMM1 and BMM2.
|
||||
for fp8_mha_partition in fp8_mha_partitions:
|
||||
bmm1_node = fp8_mha_partition[1]
|
||||
insert_cast(graph, input_tensor=bmm1_node.inputs[0], attrs={"to": np.float32})
|
||||
insert_cast(graph, input_tensor=bmm1_node.inputs[1], attrs={"to": np.float32})
|
||||
insert_cast(graph, input_tensor=bmm1_node.outputs[0], attrs={"to": np.float16})
|
||||
|
||||
bmm2_node = fp8_mha_partition[7]
|
||||
insert_cast(graph, input_tensor=bmm2_node.inputs[0], attrs={"to": np.float32})
|
||||
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.
|
||||
"""
|
||||
for input_tensor in graph.inputs:
|
||||
input_tensor.dtype = onnx.TensorProto.FLOAT16
|
||||
for output_tensor in graph.outputs:
|
||||
output_tensor.dtype = onnx.TensorProto.FLOAT16
|
||||
|
||||
|
||||
def random_resize(cur_size: int):
|
||||
"""
|
||||
Randomly selects a new resolution for an image based on its current aspect ratio.
|
||||
|
||||
This function determines the current aspect ratio of an image, selects a new aspect ratio
|
||||
from predefined choices depending on whether the current aspect ratio is square,
|
||||
portrait, or landscape, and returns the corresponding resolution from a provided mapping.
|
||||
|
||||
Parameters:
|
||||
cur_size (int): A tuple (width, height) representing the current resolution of the image.
|
||||
resolution_to_aspects (dict[float, tuple[int, int]]): A mapping of aspect ratios (floats)
|
||||
to their corresponding resolutions as tuples of (width, height).
|
||||
|
||||
Returns:
|
||||
tuple[int, int]: A tuple (new_width, new_height) representing the newly selected resolution.
|
||||
|
||||
Raises:
|
||||
KeyError: If the chosen aspect ratio is not present in the `resolution_to_aspects` dictionary.
|
||||
|
||||
Notes:
|
||||
- For square images (aspect ratio = 1), the function selects from aspect ratios 1.25, 0.8, 1.5, and 0.667.
|
||||
- For landscape images (aspect ratio > 1), the function selects from aspect ratios 1.778, 1.25, and 1.5.
|
||||
- For portrait images (aspect ratio < 1), the function selects from aspect ratios 0.563, 0.8, and 0.667.
|
||||
"""
|
||||
resolution_to_aspects = {
|
||||
1.0: (1024, 1024),
|
||||
1.778: (768, 1344),
|
||||
0.563: (1344, 768),
|
||||
1.25: (896, 1152),
|
||||
0.8: (1152, 896),
|
||||
1.5: (832, 1216),
|
||||
0.667: (1216, 832),
|
||||
}
|
||||
|
||||
cur_aspect_ratio = round(cur_size[1] / cur_size[0], 3)
|
||||
|
||||
if cur_aspect_ratio == 1:
|
||||
new_aspect_ratio = choice((1.25, 0.8, 1.5, 0.667))
|
||||
new_res = resolution_to_aspects[new_aspect_ratio]
|
||||
elif cur_aspect_ratio > 1:
|
||||
new_aspect_ratio = choice((1.778, 1.25, 1.5))
|
||||
new_res = resolution_to_aspects[new_aspect_ratio]
|
||||
else:
|
||||
# cur_aspect_ratio < 1
|
||||
new_aspect_ratio = choice((0.563, 0.8, 0.667))
|
||||
new_res = resolution_to_aspects[new_aspect_ratio]
|
||||
|
||||
return new_res
|
||||
|
||||
|
||||
class PromptImageDataset(Dataset):
|
||||
def __init__(
|
||||
self,
|
||||
root_dir,
|
||||
):
|
||||
"""
|
||||
Args:
|
||||
root_dir (str): Directory with all the images and the prompt file.
|
||||
"""
|
||||
self.root_dir = root_dir
|
||||
self.possible_resolutions = {1024, 768, 1344, 896, 832, 1216}
|
||||
self.global_idx_template = "{} | {} | {}"
|
||||
|
||||
self.prompts_by_size = defaultdict(list)
|
||||
self.images_by_size = defaultdict(list)
|
||||
self.images = []
|
||||
self.prompts = []
|
||||
self.images_size = []
|
||||
# self.global_idx_2_group = dict()
|
||||
# self.global_idx_to_group_idx = dict()
|
||||
self.group_to_global_idx = {}
|
||||
|
||||
for idx, file in enumerate(os.listdir(os.path.join(self.root_dir, "prompts"))):
|
||||
if not file.endswith(".txt"):
|
||||
continue
|
||||
file_name = os.path.splitext(file)[0]
|
||||
image_path = os.path.join(
|
||||
self.root_dir,
|
||||
"inputs",
|
||||
f"{file_name}.png",
|
||||
)
|
||||
|
||||
with Image.open(image_path) as img, open(os.path.join(self.root_dir, "prompts", file), "r") as f:
|
||||
prompt = "\n".join(f.readlines())
|
||||
|
||||
std_img_size = (
|
||||
self.closest_value(img.size[0], self.possible_resolutions),
|
||||
self.closest_value(img.size[1], self.possible_resolutions),
|
||||
)
|
||||
|
||||
self.images_by_size[std_img_size].append(image_path)
|
||||
self.prompts_by_size[std_img_size].append(prompt)
|
||||
|
||||
self.images.append(image_path)
|
||||
self.prompts.append(prompt)
|
||||
self.images_size.append(std_img_size)
|
||||
|
||||
# create a unique key that map group and index inside the group to a global index
|
||||
in_group_idx = len(self.images_by_size[std_img_size]) - 1
|
||||
group_idx_key = self.global_idx_template.format(std_img_size[0], std_img_size[1], in_group_idx)
|
||||
self.group_to_global_idx[group_idx_key] = len(self.images) - 1
|
||||
|
||||
assert len(self.images) == len(self.prompts)
|
||||
assert len(self.images) == len(self.group_to_global_idx)
|
||||
|
||||
@staticmethod
|
||||
def closest_value(target: int, candidates: Set[int]):
|
||||
"""
|
||||
Find the closest value to the target from a set of candidate values.
|
||||
|
||||
Args:
|
||||
target (int): The integer to compare against.
|
||||
candidates (set): A set of integers as candidates.
|
||||
|
||||
Returns:
|
||||
int: The closest value from the candidates.
|
||||
"""
|
||||
if not candidates:
|
||||
raise ValueError("The candidates set cannot be empty.")
|
||||
|
||||
# Use the min function with a key that computes the absolute difference
|
||||
return min(candidates, key=lambda x: abs(x - target))
|
||||
|
||||
def __len__(self):
|
||||
return len(self.images)
|
||||
|
||||
def __getitem__(self, idx):
|
||||
"""
|
||||
Returns:
|
||||
image (Tensor): Transformed image.
|
||||
prompt (str): Corresponding text prompt.
|
||||
"""
|
||||
if torch.is_tensor(idx):
|
||||
idx = idx.tolist()
|
||||
|
||||
prompt = self.prompts[idx]
|
||||
image = self.images[idx]
|
||||
image_size = self.images_size[idx]
|
||||
return image, prompt, image_size
|
||||
|
||||
|
||||
class SameSizeSampler(Sampler):
|
||||
def __init__(self, dataset: PromptImageDataset, batch_size: int):
|
||||
"""
|
||||
Custom sampler that creates batches of images with the same size
|
||||
|
||||
Args:
|
||||
dataset (SameSizeImageDataset): Dataset to sample from
|
||||
batch_size (int): Number of images per batch
|
||||
"""
|
||||
super().__init__(dataset)
|
||||
self.dataset = dataset
|
||||
self.batch_size = batch_size
|
||||
|
||||
# Prepare size groups with indices
|
||||
self.size_groups = {}
|
||||
for size, image_paths in self.dataset.images_by_size.items():
|
||||
# Create a list of indices for this size group
|
||||
self.size_groups[size] = list(range(len(image_paths)))
|
||||
|
||||
def __iter__(self):
|
||||
"""
|
||||
Iteration method that yields indices for batches of same-size images
|
||||
"""
|
||||
# Create a copy of size groups to shuffle
|
||||
size_groups_copy = {std_img_size: indices.copy() for std_img_size, indices in self.size_groups.items()}
|
||||
|
||||
# Shuffle each size group
|
||||
for std_img_size, indices in size_groups_copy.items():
|
||||
shuffle(indices)
|
||||
|
||||
# Iterate through size groups
|
||||
for std_img_size, indices in size_groups_copy.items():
|
||||
# Batch indices of the same size
|
||||
for i in range(0, len(indices), self.batch_size):
|
||||
# Yield batch indices for this size
|
||||
batch_group_idxs = indices[i : min(i + self.batch_size, len(indices))]
|
||||
for in_group_idx in batch_group_idxs:
|
||||
group_idx_key = self.dataset.global_idx_template.format(
|
||||
std_img_size[0], std_img_size[1], in_group_idx
|
||||
)
|
||||
batch_global_idx = self.dataset.group_to_global_idx[group_idx_key]
|
||||
# batch_global_idxs.append(batch_global_idx)
|
||||
yield batch_global_idx
|
||||
|
||||
def __len__(self):
|
||||
"""
|
||||
Total number of batches
|
||||
"""
|
||||
return len(self.dataset.images) // self.batch_size
|
||||
|
||||
|
||||
def custom_collate(data):
|
||||
"""
|
||||
Custom collate function to handle batches of same-size images
|
||||
|
||||
Args:
|
||||
dataset (SameSizeImageDataset): Dataset instance
|
||||
batch (list): List of global indices
|
||||
|
||||
Returns:
|
||||
tuple: Batched images and their size
|
||||
"""
|
||||
# Group images by their size
|
||||
images, prompts, image_sizes = tuple(map(list, zip(*data)))
|
||||
assert len(images) > 0
|
||||
new_img_size = random_resize(image_sizes[0])
|
||||
batch_images = []
|
||||
for image in images:
|
||||
with Image.open(image) as image:
|
||||
image = image.convert("RGB").resize(size=new_img_size, resample=Image.LANCZOS)
|
||||
image = np.array(image)
|
||||
image = np.transpose(image, axes=(-1, 0, 1))
|
||||
image = torch.from_numpy(image).float() / 127.5 - 1.0
|
||||
batch_images.append(image)
|
||||
|
||||
batch_images = torch.stack(batch_images, dim=0)
|
||||
return batch_images, prompts
|
||||
|
||||
|
||||
def infinite_dataloader(dataloader):
|
||||
while True:
|
||||
for batch in dataloader:
|
||||
yield batch
|
||||
@@ -0,0 +1,641 @@
|
||||
# MIT License
|
||||
|
||||
# Copyright (c) 2024 Stability AI
|
||||
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
|
||||
# The above copyright notice and this permission notice shall be included in all
|
||||
# copies or substantial portions of the Software.
|
||||
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
|
||||
import math
|
||||
from typing import Dict, Optional
|
||||
|
||||
import numpy as np
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
from einops import rearrange, repeat
|
||||
|
||||
from demo_diffusion.utils_sd3.other_impls import Mlp, attention
|
||||
|
||||
|
||||
class PatchEmbed(nn.Module):
|
||||
""" 2D Image to Patch Embedding"""
|
||||
def __init__(
|
||||
self,
|
||||
img_size: Optional[int] = 224,
|
||||
patch_size: int = 16,
|
||||
in_chans: int = 3,
|
||||
embed_dim: int = 768,
|
||||
flatten: bool = True,
|
||||
bias: bool = True,
|
||||
strict_img_size: bool = True,
|
||||
dynamic_img_pad: bool = False,
|
||||
dtype=None,
|
||||
device=None,
|
||||
):
|
||||
super().__init__()
|
||||
self.patch_size = (patch_size, patch_size)
|
||||
if img_size is not None:
|
||||
self.img_size = (img_size, img_size)
|
||||
self.grid_size = tuple([s // p for s, p in zip(self.img_size, self.patch_size)])
|
||||
self.num_patches = self.grid_size[0] * self.grid_size[1]
|
||||
else:
|
||||
self.img_size = None
|
||||
self.grid_size = None
|
||||
self.num_patches = None
|
||||
|
||||
# flatten spatial dim and transpose to channels last, kept for bwd compat
|
||||
self.flatten = flatten
|
||||
self.strict_img_size = strict_img_size
|
||||
self.dynamic_img_pad = dynamic_img_pad
|
||||
|
||||
self.proj = nn.Conv2d(in_chans, embed_dim, kernel_size=patch_size, stride=patch_size, bias=bias, dtype=dtype, device=device)
|
||||
|
||||
def forward(self, x):
|
||||
B, C, H, W = x.shape
|
||||
x = self.proj(x)
|
||||
if self.flatten:
|
||||
x = x.flatten(2).transpose(1, 2) # NCHW -> NLC
|
||||
return x
|
||||
|
||||
|
||||
def modulate(x, shift, scale):
|
||||
if shift is None:
|
||||
shift = torch.zeros_like(scale)
|
||||
return x * (1 + scale.unsqueeze(1)) + shift.unsqueeze(1)
|
||||
|
||||
|
||||
#################################################################################
|
||||
# Sine/Cosine Positional Embedding Functions #
|
||||
#################################################################################
|
||||
|
||||
|
||||
def get_2d_sincos_pos_embed(embed_dim, grid_size, cls_token=False, extra_tokens=0, scaling_factor=None, offset=None):
|
||||
"""
|
||||
grid_size: int of the grid height and width
|
||||
return:
|
||||
pos_embed: [grid_size*grid_size, embed_dim] or [1+grid_size*grid_size, embed_dim] (w/ or w/o cls_token)
|
||||
"""
|
||||
grid_h = np.arange(grid_size, dtype=np.float32)
|
||||
grid_w = np.arange(grid_size, dtype=np.float32)
|
||||
grid = np.meshgrid(grid_w, grid_h) # here w goes first
|
||||
grid = np.stack(grid, axis=0)
|
||||
if scaling_factor is not None:
|
||||
grid = grid / scaling_factor
|
||||
if offset is not None:
|
||||
grid = grid - offset
|
||||
grid = grid.reshape([2, 1, grid_size, grid_size])
|
||||
pos_embed = get_2d_sincos_pos_embed_from_grid(embed_dim, grid)
|
||||
if cls_token and extra_tokens > 0:
|
||||
pos_embed = np.concatenate([np.zeros([extra_tokens, embed_dim]), pos_embed], axis=0)
|
||||
return pos_embed
|
||||
|
||||
|
||||
def get_2d_sincos_pos_embed_from_grid(embed_dim, grid):
|
||||
assert embed_dim % 2 == 0
|
||||
# use half of dimensions to encode grid_h
|
||||
emb_h = get_1d_sincos_pos_embed_from_grid(embed_dim // 2, grid[0]) # (H*W, D/2)
|
||||
emb_w = get_1d_sincos_pos_embed_from_grid(embed_dim // 2, grid[1]) # (H*W, D/2)
|
||||
emb = np.concatenate([emb_h, emb_w], axis=1) # (H*W, D)
|
||||
return emb
|
||||
|
||||
|
||||
def get_1d_sincos_pos_embed_from_grid(embed_dim, pos):
|
||||
"""
|
||||
embed_dim: output dimension for each position
|
||||
pos: a list of positions to be encoded: size (M,)
|
||||
out: (M, D)
|
||||
"""
|
||||
assert embed_dim % 2 == 0
|
||||
omega = np.arange(embed_dim // 2, dtype=np.float64)
|
||||
omega /= embed_dim / 2.0
|
||||
omega = 1.0 / 10000**omega # (D/2,)
|
||||
pos = pos.reshape(-1) # (M,)
|
||||
out = np.einsum("m,d->md", pos, omega) # (M, D/2), outer product
|
||||
emb_sin = np.sin(out) # (M, D/2)
|
||||
emb_cos = np.cos(out) # (M, D/2)
|
||||
return np.concatenate([emb_sin, emb_cos], axis=1) # (M, D)
|
||||
|
||||
|
||||
#################################################################################
|
||||
# Embedding Layers for Timesteps and Class Labels #
|
||||
#################################################################################
|
||||
|
||||
|
||||
class TimestepEmbedder(nn.Module):
|
||||
"""Embeds scalar timesteps into vector representations."""
|
||||
|
||||
def __init__(self, hidden_size, frequency_embedding_size=256, dtype=None, device=None):
|
||||
super().__init__()
|
||||
self.mlp = nn.Sequential(
|
||||
nn.Linear(frequency_embedding_size, hidden_size, bias=True, dtype=dtype, device=device),
|
||||
nn.SiLU(),
|
||||
nn.Linear(hidden_size, hidden_size, bias=True, dtype=dtype, device=device),
|
||||
)
|
||||
self.frequency_embedding_size = frequency_embedding_size
|
||||
|
||||
@staticmethod
|
||||
def timestep_embedding(t, dim, max_period=10000):
|
||||
"""
|
||||
Create sinusoidal timestep embeddings.
|
||||
:param t: a 1-D Tensor of N indices, one per batch element.
|
||||
These may be fractional.
|
||||
:param dim: the dimension of the output.
|
||||
:param max_period: controls the minimum frequency of the embeddings.
|
||||
:return: an (N, D) Tensor of positional embeddings.
|
||||
"""
|
||||
half = dim // 2
|
||||
freqs = torch.exp(
|
||||
-math.log(max_period)
|
||||
* torch.arange(start=0, end=half, dtype=torch.float32)
|
||||
/ half
|
||||
).to(device=t.device)
|
||||
args = t[:, None].float() * freqs[None]
|
||||
embedding = torch.cat([torch.cos(args), torch.sin(args)], dim=-1)
|
||||
if dim % 2:
|
||||
embedding = torch.cat([embedding, torch.zeros_like(embedding[:, :1])], dim=-1)
|
||||
if torch.is_floating_point(t):
|
||||
embedding = embedding.to(dtype=t.dtype)
|
||||
return embedding
|
||||
|
||||
def forward(self, t, dtype, **kwargs):
|
||||
t_freq = self.timestep_embedding(t, self.frequency_embedding_size).to(dtype)
|
||||
t_emb = self.mlp(t_freq)
|
||||
return t_emb
|
||||
|
||||
|
||||
class VectorEmbedder(nn.Module):
|
||||
"""Embeds a flat vector of dimension input_dim"""
|
||||
|
||||
def __init__(self, input_dim: int, hidden_size: int, dtype=None, device=None):
|
||||
super().__init__()
|
||||
self.mlp = nn.Sequential(
|
||||
nn.Linear(input_dim, hidden_size, bias=True, dtype=dtype, device=device),
|
||||
nn.SiLU(),
|
||||
nn.Linear(hidden_size, hidden_size, bias=True, dtype=dtype, device=device),
|
||||
)
|
||||
|
||||
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
||||
return self.mlp(x)
|
||||
|
||||
|
||||
#################################################################################
|
||||
# Core DiT Model #
|
||||
#################################################################################
|
||||
|
||||
|
||||
def split_qkv(qkv, head_dim):
|
||||
qkv = qkv.reshape(qkv.shape[0], qkv.shape[1], 3, -1, head_dim).movedim(2, 0)
|
||||
return qkv[0], qkv[1], qkv[2]
|
||||
|
||||
def optimized_attention(qkv, num_heads):
|
||||
return attention(qkv[0], qkv[1], qkv[2], num_heads)
|
||||
|
||||
class SelfAttention(nn.Module):
|
||||
ATTENTION_MODES = ("xformers", "torch", "torch-hb", "math", "debug")
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
dim: int,
|
||||
num_heads: int = 8,
|
||||
qkv_bias: bool = False,
|
||||
qk_scale: Optional[float] = None,
|
||||
attn_mode: str = "xformers",
|
||||
pre_only: bool = False,
|
||||
qk_norm: Optional[str] = None,
|
||||
rmsnorm: bool = False,
|
||||
dtype=None,
|
||||
device=None,
|
||||
):
|
||||
super().__init__()
|
||||
self.num_heads = num_heads
|
||||
self.head_dim = dim // num_heads
|
||||
|
||||
self.qkv = nn.Linear(dim, dim * 3, bias=qkv_bias, dtype=dtype, device=device)
|
||||
if not pre_only:
|
||||
self.proj = nn.Linear(dim, dim, dtype=dtype, device=device)
|
||||
assert attn_mode in self.ATTENTION_MODES
|
||||
self.attn_mode = attn_mode
|
||||
self.pre_only = pre_only
|
||||
|
||||
if qk_norm == "rms":
|
||||
self.ln_q = RMSNorm(self.head_dim, elementwise_affine=True, eps=1.0e-6, dtype=dtype, device=device)
|
||||
self.ln_k = RMSNorm(self.head_dim, elementwise_affine=True, eps=1.0e-6, dtype=dtype, device=device)
|
||||
elif qk_norm == "ln":
|
||||
self.ln_q = nn.LayerNorm(self.head_dim, elementwise_affine=True, eps=1.0e-6, dtype=dtype, device=device)
|
||||
self.ln_k = nn.LayerNorm(self.head_dim, elementwise_affine=True, eps=1.0e-6, dtype=dtype, device=device)
|
||||
elif qk_norm is None:
|
||||
self.ln_q = nn.Identity()
|
||||
self.ln_k = nn.Identity()
|
||||
else:
|
||||
raise ValueError(qk_norm)
|
||||
|
||||
def pre_attention(self, x: torch.Tensor):
|
||||
B, L, C = x.shape
|
||||
qkv = self.qkv(x)
|
||||
q, k, v = split_qkv(qkv, self.head_dim)
|
||||
q = self.ln_q(q).reshape(q.shape[0], q.shape[1], -1)
|
||||
k = self.ln_k(k).reshape(q.shape[0], q.shape[1], -1)
|
||||
return (q, k, v)
|
||||
|
||||
def post_attention(self, x: torch.Tensor) -> torch.Tensor:
|
||||
assert not self.pre_only
|
||||
x = self.proj(x)
|
||||
return x
|
||||
|
||||
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
||||
(q, k, v) = self.pre_attention(x)
|
||||
x = attention(q, k, v, self.num_heads)
|
||||
x = self.post_attention(x)
|
||||
return x
|
||||
|
||||
|
||||
class RMSNorm(torch.nn.Module):
|
||||
def __init__(
|
||||
self, dim: int, elementwise_affine: bool = False, eps: float = 1e-6, device=None, dtype=None
|
||||
):
|
||||
"""
|
||||
Initialize the RMSNorm normalization layer.
|
||||
Args:
|
||||
dim (int): The dimension of the input tensor.
|
||||
eps (float, optional): A small value added to the denominator for numerical stability. Default is 1e-6.
|
||||
Attributes:
|
||||
eps (float): A small value added to the denominator for numerical stability.
|
||||
weight (nn.Parameter): Learnable scaling parameter.
|
||||
"""
|
||||
super().__init__()
|
||||
self.eps = eps
|
||||
self.learnable_scale = elementwise_affine
|
||||
if self.learnable_scale:
|
||||
self.weight = nn.Parameter(torch.empty(dim, device=device, dtype=dtype))
|
||||
else:
|
||||
self.register_parameter("weight", None)
|
||||
|
||||
def _norm(self, x):
|
||||
"""
|
||||
Apply the RMSNorm normalization to the input tensor.
|
||||
Args:
|
||||
x (torch.Tensor): The input tensor.
|
||||
Returns:
|
||||
torch.Tensor: The normalized tensor.
|
||||
"""
|
||||
return x * torch.rsqrt(x.pow(2).mean(-1, keepdim=True) + self.eps)
|
||||
|
||||
def forward(self, x):
|
||||
"""
|
||||
Forward pass through the RMSNorm layer.
|
||||
Args:
|
||||
x (torch.Tensor): The input tensor.
|
||||
Returns:
|
||||
torch.Tensor: The output tensor after applying RMSNorm.
|
||||
"""
|
||||
x = self._norm(x)
|
||||
if self.learnable_scale:
|
||||
return x * self.weight.to(device=x.device, dtype=x.dtype)
|
||||
else:
|
||||
return x
|
||||
|
||||
|
||||
class SwiGLUFeedForward(nn.Module):
|
||||
def __init__(
|
||||
self,
|
||||
dim: int,
|
||||
hidden_dim: int,
|
||||
multiple_of: int,
|
||||
ffn_dim_multiplier: Optional[float] = None,
|
||||
):
|
||||
"""
|
||||
Initialize the FeedForward module.
|
||||
|
||||
Args:
|
||||
dim (int): Input dimension.
|
||||
hidden_dim (int): Hidden dimension of the feedforward layer.
|
||||
multiple_of (int): Value to ensure hidden dimension is a multiple of this value.
|
||||
ffn_dim_multiplier (float, optional): Custom multiplier for hidden dimension. Defaults to None.
|
||||
|
||||
Attributes:
|
||||
w1 (ColumnParallelLinear): Linear transformation for the first layer.
|
||||
w2 (RowParallelLinear): Linear transformation for the second layer.
|
||||
w3 (ColumnParallelLinear): Linear transformation for the third layer.
|
||||
|
||||
"""
|
||||
super().__init__()
|
||||
hidden_dim = int(2 * hidden_dim / 3)
|
||||
# custom dim factor multiplier
|
||||
if ffn_dim_multiplier is not None:
|
||||
hidden_dim = int(ffn_dim_multiplier * hidden_dim)
|
||||
hidden_dim = multiple_of * ((hidden_dim + multiple_of - 1) // multiple_of)
|
||||
|
||||
self.w1 = nn.Linear(dim, hidden_dim, bias=False)
|
||||
self.w2 = nn.Linear(hidden_dim, dim, bias=False)
|
||||
self.w3 = nn.Linear(dim, hidden_dim, bias=False)
|
||||
|
||||
def forward(self, x):
|
||||
return self.w2(nn.functional.silu(self.w1(x)) * self.w3(x))
|
||||
|
||||
|
||||
class DismantledBlock(nn.Module):
|
||||
"""A DiT block with gated adaptive layer norm (adaLN) conditioning."""
|
||||
|
||||
ATTENTION_MODES = ("xformers", "torch", "torch-hb", "math", "debug")
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hidden_size: int,
|
||||
num_heads: int,
|
||||
mlp_ratio: float = 4.0,
|
||||
attn_mode: str = "xformers",
|
||||
qkv_bias: bool = False,
|
||||
pre_only: bool = False,
|
||||
rmsnorm: bool = False,
|
||||
scale_mod_only: bool = False,
|
||||
swiglu: bool = False,
|
||||
qk_norm: Optional[str] = None,
|
||||
dtype=None,
|
||||
device=None,
|
||||
**block_kwargs,
|
||||
):
|
||||
super().__init__()
|
||||
assert attn_mode in self.ATTENTION_MODES
|
||||
if not rmsnorm:
|
||||
self.norm1 = nn.LayerNorm(hidden_size, elementwise_affine=False, eps=1e-6, dtype=dtype, device=device)
|
||||
else:
|
||||
self.norm1 = RMSNorm(hidden_size, elementwise_affine=False, eps=1e-6)
|
||||
self.attn = SelfAttention(dim=hidden_size, num_heads=num_heads, qkv_bias=qkv_bias, attn_mode=attn_mode, pre_only=pre_only, qk_norm=qk_norm, rmsnorm=rmsnorm, dtype=dtype, device=device)
|
||||
if not pre_only:
|
||||
if not rmsnorm:
|
||||
self.norm2 = nn.LayerNorm(hidden_size, elementwise_affine=False, eps=1e-6, dtype=dtype, device=device)
|
||||
else:
|
||||
self.norm2 = RMSNorm(hidden_size, elementwise_affine=False, eps=1e-6)
|
||||
mlp_hidden_dim = int(hidden_size * mlp_ratio)
|
||||
if not pre_only:
|
||||
if not swiglu:
|
||||
self.mlp = Mlp(in_features=hidden_size, hidden_features=mlp_hidden_dim, act_layer=nn.GELU(approximate="tanh"), dtype=dtype, device=device)
|
||||
else:
|
||||
self.mlp = SwiGLUFeedForward(dim=hidden_size, hidden_dim=mlp_hidden_dim, multiple_of=256)
|
||||
self.scale_mod_only = scale_mod_only
|
||||
if not scale_mod_only:
|
||||
n_mods = 6 if not pre_only else 2
|
||||
else:
|
||||
n_mods = 4 if not pre_only else 1
|
||||
self.adaLN_modulation = nn.Sequential(nn.SiLU(), nn.Linear(hidden_size, n_mods * hidden_size, bias=True, dtype=dtype, device=device))
|
||||
self.pre_only = pre_only
|
||||
|
||||
def pre_attention(self, x: torch.Tensor, c: torch.Tensor):
|
||||
assert x is not None, "pre_attention called with None input"
|
||||
if not self.pre_only:
|
||||
if not self.scale_mod_only:
|
||||
shift_msa, scale_msa, gate_msa, shift_mlp, scale_mlp, gate_mlp = self.adaLN_modulation(c).chunk(6, dim=1)
|
||||
else:
|
||||
shift_msa = None
|
||||
shift_mlp = None
|
||||
scale_msa, gate_msa, scale_mlp, gate_mlp = self.adaLN_modulation(c).chunk(4, dim=1)
|
||||
qkv = self.attn.pre_attention(modulate(self.norm1(x), shift_msa, scale_msa))
|
||||
return qkv, (x, gate_msa, shift_mlp, scale_mlp, gate_mlp)
|
||||
else:
|
||||
if not self.scale_mod_only:
|
||||
shift_msa, scale_msa = self.adaLN_modulation(c).chunk(2, dim=1)
|
||||
else:
|
||||
shift_msa = None
|
||||
scale_msa = self.adaLN_modulation(c)
|
||||
qkv = self.attn.pre_attention(modulate(self.norm1(x), shift_msa, scale_msa))
|
||||
return qkv, None
|
||||
|
||||
def post_attention(self, attn, x, gate_msa, shift_mlp, scale_mlp, gate_mlp):
|
||||
assert not self.pre_only
|
||||
x = x + gate_msa.unsqueeze(1) * self.attn.post_attention(attn)
|
||||
x = x + gate_mlp.unsqueeze(1) * self.mlp(modulate(self.norm2(x), shift_mlp, scale_mlp))
|
||||
return x
|
||||
|
||||
def forward(self, x: torch.Tensor, c: torch.Tensor) -> torch.Tensor:
|
||||
assert not self.pre_only
|
||||
(q, k, v), intermediates = self.pre_attention(x, c)
|
||||
attn = attention(q, k, v, self.attn.num_heads)
|
||||
return self.post_attention(attn, *intermediates)
|
||||
|
||||
|
||||
def block_mixing(context, x, context_block, x_block, c):
|
||||
assert context is not None, "block_mixing called with None context"
|
||||
context_qkv, context_intermediates = context_block.pre_attention(context, c)
|
||||
|
||||
x_qkv, x_intermediates = x_block.pre_attention(x, c)
|
||||
|
||||
o = []
|
||||
for t in range(3):
|
||||
o.append(torch.cat((context_qkv[t], x_qkv[t]), dim=1))
|
||||
q, k, v = tuple(o)
|
||||
|
||||
attn = attention(q, k, v, x_block.attn.num_heads)
|
||||
context_attn, x_attn = (attn[:, : context_qkv[0].shape[1]], attn[:, context_qkv[0].shape[1] :])
|
||||
|
||||
if not context_block.pre_only:
|
||||
context = context_block.post_attention(context_attn, *context_intermediates)
|
||||
else:
|
||||
context = None
|
||||
x = x_block.post_attention(x_attn, *x_intermediates)
|
||||
return context, x
|
||||
|
||||
|
||||
class JointBlock(nn.Module):
|
||||
"""just a small wrapper to serve as a fsdp unit"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__()
|
||||
pre_only = kwargs.pop("pre_only")
|
||||
qk_norm = kwargs.pop("qk_norm", None)
|
||||
self.context_block = DismantledBlock(*args, pre_only=pre_only, qk_norm=qk_norm, **kwargs)
|
||||
self.x_block = DismantledBlock(*args, pre_only=False, qk_norm=qk_norm, **kwargs)
|
||||
|
||||
def forward(self, *args, **kwargs):
|
||||
return block_mixing(*args, context_block=self.context_block, x_block=self.x_block, **kwargs)
|
||||
|
||||
|
||||
class FinalLayer(nn.Module):
|
||||
"""
|
||||
The final layer of DiT.
|
||||
"""
|
||||
|
||||
def __init__(self, hidden_size: int, patch_size: int, out_channels: int, total_out_channels: Optional[int] = None, dtype=None, device=None):
|
||||
super().__init__()
|
||||
self.norm_final = nn.LayerNorm(hidden_size, elementwise_affine=False, eps=1e-6, dtype=dtype, device=device)
|
||||
self.linear = (
|
||||
nn.Linear(hidden_size, patch_size * patch_size * out_channels, bias=True, dtype=dtype, device=device)
|
||||
if (total_out_channels is None)
|
||||
else nn.Linear(hidden_size, total_out_channels, bias=True, dtype=dtype, device=device)
|
||||
)
|
||||
self.adaLN_modulation = nn.Sequential(nn.SiLU(), nn.Linear(hidden_size, 2 * hidden_size, bias=True, dtype=dtype, device=device))
|
||||
|
||||
def forward(self, x: torch.Tensor, c: torch.Tensor) -> torch.Tensor:
|
||||
shift, scale = self.adaLN_modulation(c).chunk(2, dim=1)
|
||||
x = modulate(self.norm_final(x), shift, scale)
|
||||
x = self.linear(x)
|
||||
return x
|
||||
|
||||
|
||||
class MMDiT(nn.Module):
|
||||
"""Diffusion model with a Transformer backbone."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
input_size: int = 32,
|
||||
patch_size: int = 2,
|
||||
in_channels: int = 4,
|
||||
depth: int = 28,
|
||||
mlp_ratio: float = 4.0,
|
||||
learn_sigma: bool = False,
|
||||
adm_in_channels: Optional[int] = None,
|
||||
context_embedder_config: Optional[Dict] = None,
|
||||
register_length: int = 0,
|
||||
attn_mode: str = "torch",
|
||||
rmsnorm: bool = False,
|
||||
scale_mod_only: bool = False,
|
||||
swiglu: bool = False,
|
||||
out_channels: Optional[int] = None,
|
||||
pos_embed_scaling_factor: Optional[float] = None,
|
||||
pos_embed_offset: Optional[float] = None,
|
||||
pos_embed_max_size: Optional[int] = None,
|
||||
num_patches = None,
|
||||
qk_norm: Optional[str] = None,
|
||||
qkv_bias: bool = True,
|
||||
dtype = None,
|
||||
device = None,
|
||||
):
|
||||
super().__init__()
|
||||
self.dtype = dtype
|
||||
self.learn_sigma = learn_sigma
|
||||
self.in_channels = in_channels
|
||||
default_out_channels = in_channels * 2 if learn_sigma else in_channels
|
||||
self.out_channels = out_channels if out_channels is not None else default_out_channels
|
||||
self.patch_size = patch_size
|
||||
self.pos_embed_scaling_factor = pos_embed_scaling_factor
|
||||
self.pos_embed_offset = pos_embed_offset
|
||||
self.pos_embed_max_size = pos_embed_max_size
|
||||
|
||||
# apply magic --> this defines a head_size of 64
|
||||
hidden_size = 64 * depth
|
||||
num_heads = depth
|
||||
|
||||
self.num_heads = num_heads
|
||||
|
||||
self.x_embedder = PatchEmbed(input_size, patch_size, in_channels, hidden_size, bias=True, strict_img_size=self.pos_embed_max_size is None, dtype=dtype, device=device)
|
||||
self.t_embedder = TimestepEmbedder(hidden_size, dtype=dtype, device=device)
|
||||
|
||||
if adm_in_channels is not None:
|
||||
assert isinstance(adm_in_channels, int)
|
||||
self.y_embedder = VectorEmbedder(adm_in_channels, hidden_size, dtype=dtype, device=device)
|
||||
|
||||
self.context_embedder = nn.Identity()
|
||||
if context_embedder_config is not None:
|
||||
if context_embedder_config["target"] == "torch.nn.Linear":
|
||||
self.context_embedder = nn.Linear(**context_embedder_config["params"], dtype=dtype, device=device)
|
||||
|
||||
self.register_length = register_length
|
||||
if self.register_length > 0:
|
||||
self.register = nn.Parameter(torch.randn(1, register_length, hidden_size, dtype=dtype, device=device))
|
||||
|
||||
# num_patches = self.x_embedder.num_patches
|
||||
# Will use fixed sin-cos embedding:
|
||||
# just use a buffer already
|
||||
if num_patches is not None:
|
||||
self.register_buffer(
|
||||
"pos_embed",
|
||||
torch.zeros(1, num_patches, hidden_size, dtype=dtype, device=device),
|
||||
)
|
||||
else:
|
||||
self.pos_embed = None
|
||||
|
||||
self.joint_blocks = nn.ModuleList(
|
||||
[
|
||||
JointBlock(hidden_size, num_heads, mlp_ratio=mlp_ratio, qkv_bias=qkv_bias, attn_mode=attn_mode, pre_only=i == depth - 1, rmsnorm=rmsnorm, scale_mod_only=scale_mod_only, swiglu=swiglu, qk_norm=qk_norm, dtype=dtype, device=device)
|
||||
for i in range(depth)
|
||||
]
|
||||
)
|
||||
|
||||
self.final_layer = FinalLayer(hidden_size, patch_size, self.out_channels, dtype=dtype, device=device)
|
||||
|
||||
def cropped_pos_embed(self, hw):
|
||||
assert self.pos_embed_max_size is not None
|
||||
p = self.x_embedder.patch_size[0]
|
||||
h, w = hw
|
||||
# patched size
|
||||
h = h // p
|
||||
w = w // p
|
||||
assert h <= self.pos_embed_max_size, (h, self.pos_embed_max_size)
|
||||
assert w <= self.pos_embed_max_size, (w, self.pos_embed_max_size)
|
||||
top = (self.pos_embed_max_size - h) // 2
|
||||
left = (self.pos_embed_max_size - w) // 2
|
||||
spatial_pos_embed = rearrange(
|
||||
self.pos_embed,
|
||||
"1 (h w) c -> 1 h w c",
|
||||
h=self.pos_embed_max_size,
|
||||
w=self.pos_embed_max_size,
|
||||
)
|
||||
spatial_pos_embed = spatial_pos_embed[:, top : top + h, left : left + w, :]
|
||||
spatial_pos_embed = rearrange(spatial_pos_embed, "1 h w c -> 1 (h w) c")
|
||||
return spatial_pos_embed
|
||||
|
||||
def unpatchify(self, x, hw=None):
|
||||
"""
|
||||
x: (N, T, patch_size**2 * C)
|
||||
imgs: (N, H, W, C)
|
||||
"""
|
||||
c = self.out_channels
|
||||
p = self.x_embedder.patch_size[0]
|
||||
if hw is None:
|
||||
h = w = int(x.shape[1] ** 0.5)
|
||||
else:
|
||||
h, w = hw
|
||||
h = h // p
|
||||
w = w // p
|
||||
assert h * w == x.shape[1]
|
||||
|
||||
x = x.reshape(shape=(x.shape[0], h, w, p, p, c))
|
||||
x = torch.einsum("nhwpqc->nchpwq", x)
|
||||
imgs = x.reshape(shape=(x.shape[0], c, h * p, w * p))
|
||||
return imgs
|
||||
|
||||
def forward_core_with_concat(self, x: torch.Tensor, c_mod: torch.Tensor, context: Optional[torch.Tensor] = None) -> torch.Tensor:
|
||||
if self.register_length > 0:
|
||||
context = torch.cat((repeat(self.register, "1 ... -> b ...", b=x.shape[0]), context if context is not None else torch.Tensor([]).type_as(x)), 1)
|
||||
|
||||
# context is B, L', D
|
||||
# x is B, L, D
|
||||
for block in self.joint_blocks:
|
||||
context, x = block(context, x, c=c_mod)
|
||||
|
||||
x = self.final_layer(x, c_mod) # (N, T, patch_size ** 2 * out_channels)
|
||||
return x
|
||||
|
||||
def forward(self, x: torch.Tensor, t: torch.Tensor, y: Optional[torch.Tensor] = None, context: Optional[torch.Tensor] = None) -> torch.Tensor:
|
||||
"""
|
||||
Forward pass of DiT.
|
||||
x: (N, C, H, W) tensor of spatial inputs (images or latent representations of images)
|
||||
t: (N,) tensor of diffusion timesteps
|
||||
y: (N,) tensor of class labels
|
||||
"""
|
||||
hw = x.shape[-2:]
|
||||
x = self.x_embedder(x) + self.cropped_pos_embed(hw)
|
||||
c = self.t_embedder(t, dtype=x.dtype) # (N, D)
|
||||
if y is not None:
|
||||
y = self.y_embedder(y) # (N, D)
|
||||
c = c + y # (N, D)
|
||||
|
||||
context = self.context_embedder(context)
|
||||
|
||||
x = self.forward_core_with_concat(x, c, context)
|
||||
|
||||
x = self.unpatchify(x, hw=hw) # (N, out_channels, H, W)
|
||||
return x
|
||||
@@ -0,0 +1,555 @@
|
||||
# MIT License
|
||||
|
||||
# Copyright (c) 2024 Stability AI
|
||||
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
|
||||
# The above copyright notice and this permission notice shall be included in all
|
||||
# copies or substantial portions of the Software.
|
||||
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
|
||||
import torch, math
|
||||
import numpy as np
|
||||
from torch import nn
|
||||
from transformers import CLIPTokenizer, T5TokenizerFast
|
||||
|
||||
def load_into(f, model, prefix, device, dtype=None):
|
||||
"""Just a debugging-friendly hack to apply the weights in a safetensors file to the pytorch module."""
|
||||
for key in f.keys():
|
||||
if key.startswith(prefix) and not key.startswith("loss."):
|
||||
path = key[len(prefix):].split(".")
|
||||
obj = model
|
||||
for p in path:
|
||||
if obj is list:
|
||||
obj = obj[int(p)]
|
||||
else:
|
||||
obj = getattr(obj, p, None)
|
||||
if obj is None:
|
||||
print(f"Skipping key '{key}' in safetensors file as '{p}' does not exist in python model")
|
||||
break
|
||||
if obj is None:
|
||||
continue
|
||||
try:
|
||||
tensor = f.get_tensor(key).to(device=device)
|
||||
if dtype is not None:
|
||||
tensor = tensor.to(dtype=dtype)
|
||||
obj.requires_grad_(False)
|
||||
obj.set_(tensor)
|
||||
except Exception as e:
|
||||
print(f"Failed to load key '{key}' in safetensors file: {e}")
|
||||
raise e
|
||||
|
||||
def preprocess_image_sd3(image):
|
||||
image.convert("RGB")
|
||||
image_np = np.array(image).astype(np.float32) / 255.0
|
||||
image_np = np.moveaxis(image_np, 2, 0)
|
||||
batch_images = np.expand_dims(image_np, axis=0).repeat(1, axis=0)
|
||||
image_torch = torch.from_numpy(batch_images)
|
||||
image_torch = 2.0 * image_torch - 1.0
|
||||
|
||||
return image_torch
|
||||
|
||||
|
||||
#################################################################################################
|
||||
### Core/Utility
|
||||
#################################################################################################
|
||||
|
||||
|
||||
def attention(q, k, v, heads, mask=None):
|
||||
"""Convenience wrapper around a basic attention operation"""
|
||||
b, _, dim_head = q.shape
|
||||
dim_head //= heads
|
||||
q, k, v = map(lambda t: t.view(b, -1, heads, dim_head).transpose(1, 2), (q, k, v))
|
||||
out = torch.nn.functional.scaled_dot_product_attention(q, k, v, attn_mask=mask, dropout_p=0.0, is_causal=False)
|
||||
return out.transpose(1, 2).reshape(b, -1, heads * dim_head)
|
||||
|
||||
|
||||
class Mlp(nn.Module):
|
||||
""" MLP as used in Vision Transformer, MLP-Mixer and related networks"""
|
||||
def __init__(self, in_features, hidden_features=None, out_features=None, act_layer=nn.GELU, bias=True, dtype=None, device=None):
|
||||
super().__init__()
|
||||
out_features = out_features or in_features
|
||||
hidden_features = hidden_features or in_features
|
||||
|
||||
self.fc1 = nn.Linear(in_features, hidden_features, bias=bias, dtype=dtype, device=device)
|
||||
self.act = act_layer
|
||||
self.fc2 = nn.Linear(hidden_features, out_features, bias=bias, dtype=dtype, device=device)
|
||||
|
||||
def forward(self, x):
|
||||
x = self.fc1(x)
|
||||
x = self.act(x)
|
||||
x = self.fc2(x)
|
||||
return x
|
||||
|
||||
|
||||
#################################################################################################
|
||||
### CLIP
|
||||
#################################################################################################
|
||||
|
||||
|
||||
class CLIPAttention(torch.nn.Module):
|
||||
def __init__(self, embed_dim, heads, dtype, device):
|
||||
super().__init__()
|
||||
self.heads = heads
|
||||
self.q_proj = nn.Linear(embed_dim, embed_dim, bias=True, dtype=dtype, device=device)
|
||||
self.k_proj = nn.Linear(embed_dim, embed_dim, bias=True, dtype=dtype, device=device)
|
||||
self.v_proj = nn.Linear(embed_dim, embed_dim, bias=True, dtype=dtype, device=device)
|
||||
self.out_proj = nn.Linear(embed_dim, embed_dim, bias=True, dtype=dtype, device=device)
|
||||
|
||||
def forward(self, x, mask=None):
|
||||
q = self.q_proj(x)
|
||||
k = self.k_proj(x)
|
||||
v = self.v_proj(x)
|
||||
out = attention(q, k, v, self.heads, mask)
|
||||
return self.out_proj(out)
|
||||
|
||||
|
||||
ACTIVATIONS = {
|
||||
"quick_gelu": lambda a: a * torch.sigmoid(1.702 * a),
|
||||
"gelu": torch.nn.functional.gelu,
|
||||
}
|
||||
|
||||
class CLIPLayer(torch.nn.Module):
|
||||
def __init__(self, embed_dim, heads, intermediate_size, intermediate_activation, dtype, device):
|
||||
super().__init__()
|
||||
self.layer_norm1 = nn.LayerNorm(embed_dim, dtype=dtype, device=device)
|
||||
self.self_attn = CLIPAttention(embed_dim, heads, dtype, device)
|
||||
self.layer_norm2 = nn.LayerNorm(embed_dim, dtype=dtype, device=device)
|
||||
#self.mlp = CLIPMLP(embed_dim, intermediate_size, intermediate_activation, dtype, device)
|
||||
self.mlp = Mlp(embed_dim, intermediate_size, embed_dim, act_layer=ACTIVATIONS[intermediate_activation], dtype=dtype, device=device)
|
||||
|
||||
def forward(self, x, mask=None):
|
||||
x += self.self_attn(self.layer_norm1(x), mask)
|
||||
x += self.mlp(self.layer_norm2(x))
|
||||
return x
|
||||
|
||||
|
||||
class CLIPEncoder(torch.nn.Module):
|
||||
def __init__(self, num_layers, embed_dim, heads, intermediate_size, intermediate_activation, dtype, device):
|
||||
super().__init__()
|
||||
self.layers = torch.nn.ModuleList([CLIPLayer(embed_dim, heads, intermediate_size, intermediate_activation, dtype, device) for i in range(num_layers)])
|
||||
|
||||
def forward(self, x, mask=None, intermediate_output=None):
|
||||
if intermediate_output is not None:
|
||||
if intermediate_output < 0:
|
||||
intermediate_output = len(self.layers) + intermediate_output
|
||||
intermediate = None
|
||||
for i, l in enumerate(self.layers):
|
||||
x = l(x, mask)
|
||||
if i == intermediate_output:
|
||||
intermediate = x.clone()
|
||||
return x, intermediate
|
||||
|
||||
|
||||
class CLIPEmbeddings(torch.nn.Module):
|
||||
def __init__(self, embed_dim, vocab_size=49408, num_positions=77, dtype=None, device=None):
|
||||
super().__init__()
|
||||
self.token_embedding = torch.nn.Embedding(vocab_size, embed_dim, dtype=dtype, device=device)
|
||||
self.position_embedding = torch.nn.Embedding(num_positions, embed_dim, dtype=dtype, device=device)
|
||||
|
||||
def forward(self, input_tokens):
|
||||
return self.token_embedding(input_tokens) + self.position_embedding.weight
|
||||
|
||||
|
||||
class CLIPTextModel_(torch.nn.Module):
|
||||
def __init__(self, config_dict, dtype, device):
|
||||
num_layers = config_dict["num_hidden_layers"]
|
||||
embed_dim = config_dict["hidden_size"]
|
||||
heads = config_dict["num_attention_heads"]
|
||||
intermediate_size = config_dict["intermediate_size"]
|
||||
intermediate_activation = config_dict["hidden_act"]
|
||||
super().__init__()
|
||||
self.embeddings = CLIPEmbeddings(embed_dim, dtype=dtype, device=device)
|
||||
self.encoder = CLIPEncoder(num_layers, embed_dim, heads, intermediate_size, intermediate_activation, dtype, device)
|
||||
self.final_layer_norm = nn.LayerNorm(embed_dim, dtype=dtype, device=device)
|
||||
|
||||
def forward(self, input_tokens, intermediate_output=None, final_layer_norm_intermediate=True):
|
||||
x = self.embeddings(input_tokens)
|
||||
causal_mask = torch.empty(x.shape[1], x.shape[1], dtype=x.dtype, device=x.device).fill_(float("-inf")).triu_(1)
|
||||
x, i = self.encoder(x, mask=causal_mask, intermediate_output=intermediate_output)
|
||||
x = self.final_layer_norm(x)
|
||||
if i is not None and final_layer_norm_intermediate:
|
||||
i = self.final_layer_norm(i)
|
||||
pooled_output = x[torch.arange(x.shape[0], device=x.device), input_tokens.to(dtype=torch.int, device=x.device).argmax(dim=-1),]
|
||||
return x, i, pooled_output
|
||||
|
||||
|
||||
class CLIPTextModel(torch.nn.Module):
|
||||
def __init__(self, config_dict, dtype, device):
|
||||
super().__init__()
|
||||
self.num_layers = config_dict["num_hidden_layers"]
|
||||
self.text_model = CLIPTextModel_(config_dict, dtype, device)
|
||||
embed_dim = config_dict["hidden_size"]
|
||||
self.text_projection = nn.Linear(embed_dim, embed_dim, bias=False, dtype=dtype, device=device)
|
||||
|
||||
# WAR for RuntimeError: a leaf Variable that requires grad is being used in an in-place operation.
|
||||
with torch.no_grad():
|
||||
self.text_projection.weight.copy_(torch.eye(embed_dim))
|
||||
self.dtype = dtype
|
||||
|
||||
def get_input_embeddings(self):
|
||||
return self.text_model.embeddings.token_embedding
|
||||
|
||||
def set_input_embeddings(self, embeddings):
|
||||
self.text_model.embeddings.token_embedding = embeddings
|
||||
|
||||
def forward(self, *args, **kwargs):
|
||||
x = self.text_model(*args, **kwargs)
|
||||
out = self.text_projection(x[2])
|
||||
return (x[0], x[1], out, x[2])
|
||||
|
||||
|
||||
class SDTokenizer:
|
||||
def __init__(self, max_length=77, pad_with_end=True, tokenizer=None, has_start_token=True, pad_to_max_length=True, min_length=None):
|
||||
self.tokenizer = tokenizer
|
||||
self.max_length = max_length
|
||||
self.min_length = min_length
|
||||
empty = self.tokenizer('')["input_ids"]
|
||||
if has_start_token:
|
||||
self.tokens_start = 1
|
||||
self.start_token = empty[0]
|
||||
self.end_token = empty[1]
|
||||
else:
|
||||
self.tokens_start = 0
|
||||
self.start_token = None
|
||||
self.end_token = empty[0]
|
||||
self.pad_with_end = pad_with_end
|
||||
self.pad_to_max_length = pad_to_max_length
|
||||
vocab = self.tokenizer.get_vocab()
|
||||
self.inv_vocab = {v: k for k, v in vocab.items()}
|
||||
self.max_word_length = 8
|
||||
|
||||
|
||||
def tokenize_with_weights(self, text:str):
|
||||
"""Tokenize the text, with weight values - presume 1.0 for all and ignore other features here. The details aren't relevant for a reference impl, and weights themselves has weak effect on SD3."""
|
||||
if self.pad_with_end:
|
||||
pad_token = self.end_token
|
||||
else:
|
||||
pad_token = 0
|
||||
batch = []
|
||||
if self.start_token is not None:
|
||||
batch.append((self.start_token, 1.0))
|
||||
to_tokenize = text.replace("\n", " ").split(' ')
|
||||
to_tokenize = [x for x in to_tokenize if x != ""]
|
||||
for word in to_tokenize:
|
||||
batch.extend([(t, 1) for t in self.tokenizer(word)["input_ids"][self.tokens_start:-1]])
|
||||
batch.append((self.end_token, 1.0))
|
||||
if self.pad_to_max_length:
|
||||
batch.extend([(pad_token, 1.0)] * (self.max_length - len(batch)))
|
||||
if self.min_length is not None and len(batch) < self.min_length:
|
||||
batch.extend([(pad_token, 1.0)] * (self.min_length - len(batch)))
|
||||
if len(batch) > self.max_length:
|
||||
batch = batch[:self.max_length]
|
||||
return [batch]
|
||||
|
||||
|
||||
class SDXLClipGTokenizer(SDTokenizer):
|
||||
def __init__(self, tokenizer):
|
||||
super().__init__(pad_with_end=False, tokenizer=tokenizer)
|
||||
|
||||
|
||||
class SD3Tokenizer:
|
||||
def __init__(self):
|
||||
clip_tokenizer = CLIPTokenizer.from_pretrained("openai/clip-vit-large-patch14")
|
||||
self.clip_l = SDTokenizer(tokenizer=clip_tokenizer)
|
||||
self.clip_g = SDXLClipGTokenizer(clip_tokenizer)
|
||||
self.t5xxl = T5XXLTokenizer()
|
||||
|
||||
def tokenize_with_weights(self, text:str):
|
||||
out = {}
|
||||
out["g"] = self.clip_g.tokenize_with_weights(text)
|
||||
out["l"] = self.clip_l.tokenize_with_weights(text)
|
||||
out["t5xxl"] = self.t5xxl.tokenize_with_weights(text)
|
||||
return out
|
||||
|
||||
class ClipTokenWeightEncoder:
|
||||
def encode_token_weights(self, token_weight_pairs):
|
||||
tokens = list(map(lambda a: a[0], token_weight_pairs[0]))
|
||||
|
||||
# model inference
|
||||
tokens = torch.tensor([tokens], dtype=torch.int64, device="cuda")
|
||||
out, pooled = self(tokens)
|
||||
|
||||
if pooled is not None:
|
||||
first_pooled = pooled[0:1].cuda()
|
||||
else:
|
||||
first_pooled = pooled
|
||||
output = [out[0:1]]
|
||||
|
||||
return torch.cat(output, dim=-2).cuda(), first_pooled
|
||||
|
||||
class SDClipModel(torch.nn.Module, ClipTokenWeightEncoder):
|
||||
"""Uses the CLIP transformer encoder for text (from huggingface)"""
|
||||
LAYERS = ["last", "pooled", "hidden"]
|
||||
def __init__(self, device="cuda", max_length=77, layer="last", layer_idx=None, textmodel_json_config=None, dtype=None, model_class=CLIPTextModel,
|
||||
special_tokens={"start": 49406, "end": 49407, "pad": 49407}, layer_norm_hidden_state=True, return_projected_pooled=True):
|
||||
super().__init__()
|
||||
assert layer in self.LAYERS
|
||||
self.transformer = model_class(textmodel_json_config, dtype, device)
|
||||
self.num_layers = self.transformer.num_layers
|
||||
self.max_length = max_length
|
||||
self.transformer = self.transformer.eval()
|
||||
for param in self.parameters():
|
||||
param.requires_grad = False
|
||||
self.layer = layer
|
||||
self.layer_idx = None
|
||||
self.special_tokens = special_tokens
|
||||
self.logit_scale = torch.nn.Parameter(torch.tensor(4.6055))
|
||||
self.layer_norm_hidden_state = layer_norm_hidden_state
|
||||
self.return_projected_pooled = return_projected_pooled
|
||||
if layer == "hidden":
|
||||
assert layer_idx is not None
|
||||
assert abs(layer_idx) < self.num_layers
|
||||
self.set_clip_options({"layer": layer_idx})
|
||||
self.options_default = (self.layer, self.layer_idx, self.return_projected_pooled)
|
||||
|
||||
def set_clip_options(self, options):
|
||||
layer_idx = options.get("layer", self.layer_idx)
|
||||
self.return_projected_pooled = options.get("projected_pooled", self.return_projected_pooled)
|
||||
if layer_idx is None or abs(layer_idx) > self.num_layers:
|
||||
self.layer = "last"
|
||||
else:
|
||||
self.layer = "hidden"
|
||||
self.layer_idx = layer_idx
|
||||
|
||||
def forward(self, tokens):
|
||||
backup_embeds = self.transformer.get_input_embeddings()
|
||||
outputs = self.transformer(tokens, intermediate_output=self.layer_idx, final_layer_norm_intermediate=self.layer_norm_hidden_state)
|
||||
self.transformer.set_input_embeddings(backup_embeds)
|
||||
if self.layer == "last":
|
||||
z = outputs[0]
|
||||
else:
|
||||
z = outputs[1]
|
||||
pooled_output = None
|
||||
if len(outputs) >= 3:
|
||||
if not self.return_projected_pooled and len(outputs) >= 4 and outputs[3] is not None:
|
||||
pooled_output = outputs[3].float()
|
||||
elif outputs[2] is not None:
|
||||
pooled_output = outputs[2].float()
|
||||
return z.float(), pooled_output
|
||||
|
||||
|
||||
class SDXLClipG(SDClipModel):
|
||||
"""Wraps the CLIP-G model into the SD-CLIP-Model interface"""
|
||||
def __init__(self, config, device="cuda", layer="penultimate", layer_idx=None, dtype=None):
|
||||
if layer == "penultimate":
|
||||
layer="hidden"
|
||||
layer_idx=-2
|
||||
super().__init__(device=device, layer=layer, layer_idx=layer_idx, textmodel_json_config=config, dtype=dtype, special_tokens={"start": 49406, "end": 49407, "pad": 0}, layer_norm_hidden_state=False)
|
||||
|
||||
|
||||
class T5XXLModel(SDClipModel):
|
||||
"""Wraps the T5-XXL model into the SD-CLIP-Model interface for convenience"""
|
||||
def __init__(self, config, device="cuda", layer="last", layer_idx=None, dtype=None):
|
||||
super().__init__(device=device, layer=layer, layer_idx=layer_idx, textmodel_json_config=config, dtype=dtype, special_tokens={"end": 1, "pad": 0}, model_class=T5)
|
||||
|
||||
|
||||
#################################################################################################
|
||||
### T5 implementation, for the T5-XXL text encoder portion, largely pulled from upstream impl
|
||||
#################################################################################################
|
||||
|
||||
|
||||
class T5XXLTokenizer(SDTokenizer):
|
||||
"""Wraps the T5 Tokenizer from HF into the SDTokenizer interface"""
|
||||
def __init__(self):
|
||||
super().__init__(pad_with_end=False, tokenizer=T5TokenizerFast.from_pretrained("google/t5-v1_1-xxl"), has_start_token=False, pad_to_max_length=False, max_length=99999999, min_length=77)
|
||||
|
||||
|
||||
class T5LayerNorm(torch.nn.Module):
|
||||
def __init__(self, hidden_size, eps=1e-6, dtype=None, device=None):
|
||||
super().__init__()
|
||||
self.weight = torch.nn.Parameter(torch.ones(hidden_size, dtype=dtype, device=device))
|
||||
self.variance_epsilon = eps
|
||||
|
||||
def forward(self, x):
|
||||
variance = x.pow(2).mean(-1, keepdim=True)
|
||||
x = x * torch.rsqrt(variance + self.variance_epsilon)
|
||||
return self.weight.to(device=x.device, dtype=x.dtype) * x
|
||||
|
||||
|
||||
class T5DenseGatedActDense(torch.nn.Module):
|
||||
def __init__(self, model_dim, ff_dim, dtype, device):
|
||||
super().__init__()
|
||||
self.wi_0 = nn.Linear(model_dim, ff_dim, bias=False, dtype=dtype, device=device)
|
||||
self.wi_1 = nn.Linear(model_dim, ff_dim, bias=False, dtype=dtype, device=device)
|
||||
self.wo = nn.Linear(ff_dim, model_dim, bias=False, dtype=dtype, device=device)
|
||||
|
||||
def forward(self, x):
|
||||
hidden_gelu = torch.nn.functional.gelu(self.wi_0(x), approximate="tanh")
|
||||
hidden_linear = self.wi_1(x)
|
||||
x = hidden_gelu * hidden_linear
|
||||
x = self.wo(x)
|
||||
return x
|
||||
|
||||
|
||||
class T5LayerFF(torch.nn.Module):
|
||||
def __init__(self, model_dim, ff_dim, dtype, device):
|
||||
super().__init__()
|
||||
self.DenseReluDense = T5DenseGatedActDense(model_dim, ff_dim, dtype, device)
|
||||
self.layer_norm = T5LayerNorm(model_dim, dtype=dtype, device=device)
|
||||
|
||||
def forward(self, x):
|
||||
forwarded_states = self.layer_norm(x)
|
||||
forwarded_states = self.DenseReluDense(forwarded_states)
|
||||
x += forwarded_states
|
||||
return x
|
||||
|
||||
|
||||
class T5Attention(torch.nn.Module):
|
||||
def __init__(self, model_dim, inner_dim, num_heads, relative_attention_bias, dtype, device):
|
||||
super().__init__()
|
||||
# Mesh TensorFlow initialization to avoid scaling before softmax
|
||||
self.q = nn.Linear(model_dim, inner_dim, bias=False, dtype=dtype, device=device)
|
||||
self.k = nn.Linear(model_dim, inner_dim, bias=False, dtype=dtype, device=device)
|
||||
self.v = nn.Linear(model_dim, inner_dim, bias=False, dtype=dtype, device=device)
|
||||
self.o = nn.Linear(inner_dim, model_dim, bias=False, dtype=dtype, device=device)
|
||||
self.num_heads = num_heads
|
||||
self.relative_attention_bias = None
|
||||
if relative_attention_bias:
|
||||
self.relative_attention_num_buckets = 32
|
||||
self.relative_attention_max_distance = 128
|
||||
self.relative_attention_bias = torch.nn.Embedding(self.relative_attention_num_buckets, self.num_heads, device=device, dtype=dtype)
|
||||
|
||||
@staticmethod
|
||||
def _relative_position_bucket(relative_position, bidirectional=True, num_buckets=32, max_distance=128):
|
||||
"""
|
||||
Adapted from Mesh Tensorflow:
|
||||
https://github.com/tensorflow/mesh/blob/0cb87fe07da627bf0b7e60475d59f95ed6b5be3d/mesh_tensorflow/transformer/transformer_layers.py#L593
|
||||
|
||||
Translate relative position to a bucket number for relative attention. The relative position is defined as
|
||||
memory_position - query_position, i.e. the distance in tokens from the attending position to the attended-to
|
||||
position. If bidirectional=False, then positive relative positions are invalid. We use smaller buckets for
|
||||
small absolute relative_position and larger buckets for larger absolute relative_positions. All relative
|
||||
positions >=max_distance map to the same bucket. All relative positions <=-max_distance map to the same bucket.
|
||||
This should allow for more graceful generalization to longer sequences than the model has been trained on
|
||||
|
||||
Args:
|
||||
relative_position: an int32 Tensor
|
||||
bidirectional: a boolean - whether the attention is bidirectional
|
||||
num_buckets: an integer
|
||||
max_distance: an integer
|
||||
|
||||
Returns:
|
||||
a Tensor with the same shape as relative_position, containing int32 values in the range [0, num_buckets)
|
||||
"""
|
||||
relative_buckets = 0
|
||||
if bidirectional:
|
||||
num_buckets //= 2
|
||||
relative_buckets += (relative_position > 0).to(torch.long) * num_buckets
|
||||
relative_position = torch.abs(relative_position)
|
||||
else:
|
||||
relative_position = -torch.min(relative_position, torch.zeros_like(relative_position))
|
||||
# now relative_position is in the range [0, inf)
|
||||
# half of the buckets are for exact increments in positions
|
||||
max_exact = num_buckets // 2
|
||||
is_small = relative_position < max_exact
|
||||
# The other half of the buckets are for logarithmically bigger bins in positions up to max_distance
|
||||
relative_position_if_large = max_exact + (
|
||||
torch.log(relative_position.float() / max_exact)
|
||||
/ math.log(max_distance / max_exact)
|
||||
* (num_buckets - max_exact)
|
||||
).to(torch.long)
|
||||
relative_position_if_large = torch.min(relative_position_if_large, torch.full_like(relative_position_if_large, num_buckets - 1))
|
||||
relative_buckets += torch.where(is_small, relative_position, relative_position_if_large)
|
||||
return relative_buckets
|
||||
|
||||
def compute_bias(self, query_length, key_length, device):
|
||||
"""Compute binned relative position bias"""
|
||||
context_position = torch.arange(query_length, dtype=torch.long, device=device)[:, None]
|
||||
memory_position = torch.arange(key_length, dtype=torch.long, device=device)[None, :]
|
||||
relative_position = memory_position - context_position # shape (query_length, key_length)
|
||||
relative_position_bucket = self._relative_position_bucket(
|
||||
relative_position, # shape (query_length, key_length)
|
||||
bidirectional=True,
|
||||
num_buckets=self.relative_attention_num_buckets,
|
||||
max_distance=self.relative_attention_max_distance,
|
||||
)
|
||||
values = self.relative_attention_bias(relative_position_bucket) # shape (query_length, key_length, num_heads)
|
||||
values = values.permute([2, 0, 1]).unsqueeze(0) # shape (1, num_heads, query_length, key_length)
|
||||
return values
|
||||
|
||||
def forward(self, x, past_bias=None):
|
||||
q = self.q(x)
|
||||
k = self.k(x)
|
||||
v = self.v(x)
|
||||
if self.relative_attention_bias is not None:
|
||||
past_bias = self.compute_bias(x.shape[1], x.shape[1], x.device)
|
||||
if past_bias is not None:
|
||||
mask = past_bias
|
||||
out = attention(q, k * ((k.shape[-1] / self.num_heads) ** 0.5), v, self.num_heads, mask)
|
||||
return self.o(out), past_bias
|
||||
|
||||
|
||||
class T5LayerSelfAttention(torch.nn.Module):
|
||||
def __init__(self, model_dim, inner_dim, ff_dim, num_heads, relative_attention_bias, dtype, device):
|
||||
super().__init__()
|
||||
self.SelfAttention = T5Attention(model_dim, inner_dim, num_heads, relative_attention_bias, dtype, device)
|
||||
self.layer_norm = T5LayerNorm(model_dim, dtype=dtype, device=device)
|
||||
|
||||
def forward(self, x, past_bias=None):
|
||||
output, past_bias = self.SelfAttention(self.layer_norm(x), past_bias=past_bias)
|
||||
x += output
|
||||
return x, past_bias
|
||||
|
||||
|
||||
class T5Block(torch.nn.Module):
|
||||
def __init__(self, model_dim, inner_dim, ff_dim, num_heads, relative_attention_bias, dtype, device):
|
||||
super().__init__()
|
||||
self.layer = torch.nn.ModuleList()
|
||||
self.layer.append(T5LayerSelfAttention(model_dim, inner_dim, ff_dim, num_heads, relative_attention_bias, dtype, device))
|
||||
self.layer.append(T5LayerFF(model_dim, ff_dim, dtype, device))
|
||||
|
||||
def forward(self, x, past_bias=None):
|
||||
x, past_bias = self.layer[0](x, past_bias)
|
||||
x = self.layer[-1](x)
|
||||
return x, past_bias
|
||||
|
||||
|
||||
class T5Stack(torch.nn.Module):
|
||||
def __init__(self, num_layers, model_dim, inner_dim, ff_dim, num_heads, vocab_size, dtype, device):
|
||||
super().__init__()
|
||||
self.embed_tokens = torch.nn.Embedding(vocab_size, model_dim, device=device, dtype=dtype)
|
||||
self.block = torch.nn.ModuleList([T5Block(model_dim, inner_dim, ff_dim, num_heads, relative_attention_bias=(i == 0), dtype=dtype, device=device) for i in range(num_layers)])
|
||||
self.final_layer_norm = T5LayerNorm(model_dim, dtype=dtype, device=device)
|
||||
|
||||
def forward(self, input_ids, intermediate_output=None, final_layer_norm_intermediate=True):
|
||||
intermediate = None
|
||||
x = self.embed_tokens(input_ids)
|
||||
past_bias = None
|
||||
for i, l in enumerate(self.block):
|
||||
x, past_bias = l(x, past_bias)
|
||||
if i == intermediate_output:
|
||||
intermediate = x.clone()
|
||||
x = self.final_layer_norm(x)
|
||||
if intermediate is not None and final_layer_norm_intermediate:
|
||||
intermediate = self.final_layer_norm(intermediate)
|
||||
return x, intermediate
|
||||
|
||||
|
||||
class T5(torch.nn.Module):
|
||||
def __init__(self, config_dict, dtype, device):
|
||||
super().__init__()
|
||||
self.num_layers = config_dict["num_layers"]
|
||||
self.encoder = T5Stack(self.num_layers, config_dict["d_model"], config_dict["d_model"], config_dict["d_ff"], config_dict["num_heads"], config_dict["vocab_size"], dtype, device)
|
||||
self.dtype = dtype
|
||||
|
||||
def get_input_embeddings(self):
|
||||
return self.encoder.embed_tokens
|
||||
|
||||
def set_input_embeddings(self, embeddings):
|
||||
self.encoder.embed_tokens = embeddings
|
||||
|
||||
def forward(self, *args, **kwargs):
|
||||
return self.encoder(*args, **kwargs)
|
||||
@@ -0,0 +1,391 @@
|
||||
# MIT License
|
||||
|
||||
# Copyright (c) 2024 Stability AI
|
||||
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
|
||||
# The above copyright notice and this permission notice shall be included in all
|
||||
# copies or substantial portions of the Software.
|
||||
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
|
||||
import math
|
||||
|
||||
import einops
|
||||
import torch
|
||||
from PIL import Image
|
||||
|
||||
from demo_diffusion.utils_sd3.mmdit import MMDiT
|
||||
|
||||
#################################################################################################
|
||||
### MMDiT Model Wrapping
|
||||
#################################################################################################
|
||||
|
||||
|
||||
class ModelSamplingDiscreteFlow(torch.nn.Module):
|
||||
"""Helper for sampler scheduling (ie timestep/sigma calculations) for Discrete Flow models"""
|
||||
def __init__(self, shift=1.0):
|
||||
super().__init__()
|
||||
self.shift = shift
|
||||
timesteps = 1000
|
||||
ts = self.sigma(torch.arange(1, timesteps + 1, 1))
|
||||
self.register_buffer('sigmas', ts)
|
||||
|
||||
@property
|
||||
def sigma_min(self):
|
||||
return self.sigmas[0]
|
||||
|
||||
@property
|
||||
def sigma_max(self):
|
||||
return self.sigmas[-1]
|
||||
|
||||
def timestep(self, sigma):
|
||||
return sigma * 1000
|
||||
|
||||
def sigma(self, timestep: torch.Tensor):
|
||||
timestep = timestep / 1000.0
|
||||
if self.shift == 1.0:
|
||||
return timestep
|
||||
return self.shift * timestep / (1 + (self.shift - 1) * timestep)
|
||||
|
||||
def calculate_denoised(self, sigma, model_output, model_input):
|
||||
sigma = sigma.view(sigma.shape[:1] + (1,) * (model_output.ndim - 1))
|
||||
return model_input - model_output * sigma
|
||||
|
||||
def noise_scaling(self, sigma, noise, latent_image, max_denoise=False):
|
||||
return sigma * noise + (1.0 - sigma) * latent_image
|
||||
|
||||
|
||||
class BaseModel(torch.nn.Module):
|
||||
"""Wrapper around the core MM-DiT model"""
|
||||
def __init__(self, shift=1.0, device=None, dtype=torch.float32, file=None, prefix=""):
|
||||
super().__init__()
|
||||
# Important configuration values can be quickly determined by checking shapes in the source file
|
||||
# Some of these will vary between models (eg 2B vs 8B primarily differ in their depth, but also other details change)
|
||||
patch_size = file.get_tensor(f"{prefix}x_embedder.proj.weight").shape[2]
|
||||
depth = file.get_tensor(f"{prefix}x_embedder.proj.weight").shape[0] // 64
|
||||
num_patches = file.get_tensor(f"{prefix}pos_embed").shape[1]
|
||||
pos_embed_max_size = round(math.sqrt(num_patches))
|
||||
adm_in_channels = file.get_tensor(f"{prefix}y_embedder.mlp.0.weight").shape[1]
|
||||
context_shape = file.get_tensor(f"{prefix}context_embedder.weight").shape
|
||||
context_embedder_config = {
|
||||
"target": "torch.nn.Linear",
|
||||
"params": {
|
||||
"in_features": context_shape[1],
|
||||
"out_features": context_shape[0]
|
||||
}
|
||||
}
|
||||
self.diffusion_model = MMDiT(input_size=None, pos_embed_scaling_factor=None, pos_embed_offset=None, pos_embed_max_size=pos_embed_max_size, patch_size=patch_size, in_channels=16, depth=depth, num_patches=num_patches, adm_in_channels=adm_in_channels, context_embedder_config=context_embedder_config, device=device, dtype=dtype)
|
||||
self.model_sampling = ModelSamplingDiscreteFlow(shift=shift)
|
||||
|
||||
def forward(self, x, sigma, c_crossattn=None, y=None):
|
||||
dtype = self.get_dtype()
|
||||
timestep = self.model_sampling.timestep(sigma).float()
|
||||
model_output = self.diffusion_model(x.to(dtype), timestep, context=c_crossattn.to(dtype), y=y.to(dtype)).float()
|
||||
return self.model_sampling.calculate_denoised(sigma, model_output, x)
|
||||
|
||||
def get_dtype(self):
|
||||
return self.diffusion_model.dtype
|
||||
|
||||
|
||||
class CFGDenoiser(torch.nn.Module):
|
||||
"""Helper for applying CFG Scaling to diffusion outputs"""
|
||||
def __init__(self, model):
|
||||
super().__init__()
|
||||
self.model = model
|
||||
|
||||
def forward(self, x, timestep, cond, uncond, cond_scale):
|
||||
# Run cond and uncond in a batch together
|
||||
batched = self.model(torch.cat([x, x]), torch.cat([timestep, timestep]), c_crossattn=torch.cat([cond["c_crossattn"], uncond["c_crossattn"]]), y=torch.cat([cond["y"], uncond["y"]]))
|
||||
# Then split and apply CFG Scaling
|
||||
pos_out, neg_out = batched.chunk(2)
|
||||
scaled = neg_out + (pos_out - neg_out) * cond_scale
|
||||
return scaled
|
||||
|
||||
|
||||
class SD3LatentFormat:
|
||||
"""Latents are slightly shifted from center - this class must be called after VAE Decode to correct for the shift"""
|
||||
def __init__(self):
|
||||
self.scale_factor = 1.5305
|
||||
self.shift_factor = 0.0609
|
||||
|
||||
def process_in(self, latent):
|
||||
return (latent - self.shift_factor) * self.scale_factor
|
||||
|
||||
def process_out(self, latent):
|
||||
return (latent / self.scale_factor) + self.shift_factor
|
||||
|
||||
def decode_latent_to_preview(self, x0):
|
||||
"""Quick RGB approximate preview of sd3 latents"""
|
||||
factors = torch.tensor([
|
||||
[-0.0645, 0.0177, 0.1052], [ 0.0028, 0.0312, 0.0650],
|
||||
[ 0.1848, 0.0762, 0.0360], [ 0.0944, 0.0360, 0.0889],
|
||||
[ 0.0897, 0.0506, -0.0364], [-0.0020, 0.1203, 0.0284],
|
||||
[ 0.0855, 0.0118, 0.0283], [-0.0539, 0.0658, 0.1047],
|
||||
[-0.0057, 0.0116, 0.0700], [-0.0412, 0.0281, -0.0039],
|
||||
[ 0.1106, 0.1171, 0.1220], [-0.0248, 0.0682, -0.0481],
|
||||
[ 0.0815, 0.0846, 0.1207], [-0.0120, -0.0055, -0.0867],
|
||||
[-0.0749, -0.0634, -0.0456], [-0.1418, -0.1457, -0.1259]
|
||||
], device="cuda")
|
||||
latent_image = x0[0].permute(1, 2, 0).cuda() @ factors
|
||||
|
||||
latents_ubyte = (((latent_image + 1) / 2)
|
||||
.clamp(0, 1) # change scale from -1..1 to 0..1
|
||||
.mul(0xFF) # to 0..255
|
||||
.byte()).cuda()
|
||||
|
||||
return Image.fromarray(latents_ubyte.numpy())
|
||||
|
||||
|
||||
#################################################################################################
|
||||
### K-Diffusion Sampling
|
||||
#################################################################################################
|
||||
|
||||
|
||||
def append_dims(x, target_dims):
|
||||
"""Appends dimensions to the end of a tensor until it has target_dims dimensions."""
|
||||
dims_to_append = target_dims - x.ndim
|
||||
return x[(...,) + (None,) * dims_to_append]
|
||||
|
||||
|
||||
def to_d(x, sigma, denoised):
|
||||
"""Converts a denoiser output to a Karras ODE derivative."""
|
||||
return (x - denoised) / append_dims(sigma, x.ndim)
|
||||
|
||||
|
||||
@torch.no_grad()
|
||||
@torch.autocast("cuda", dtype=torch.float16)
|
||||
def sample_euler(func, x, sigmas, extra_args=None):
|
||||
"""Implements Algorithm 2 (Euler steps) from Karras et al. (2022)."""
|
||||
extra_args = {} if extra_args is None else extra_args
|
||||
s_in = x.new_ones([x.shape[0]])
|
||||
for i in range(len(sigmas) - 1):
|
||||
sigma_hat = sigmas[i]
|
||||
denoised = func(x, sigma_hat * s_in, **extra_args)
|
||||
d = to_d(x, sigma_hat, denoised)
|
||||
dt = sigmas[i + 1] - sigma_hat
|
||||
# Euler method
|
||||
x = x + d * dt
|
||||
return x
|
||||
|
||||
|
||||
#################################################################################################
|
||||
### VAE
|
||||
#################################################################################################
|
||||
|
||||
|
||||
def Normalize(in_channels, num_groups=32, dtype=torch.float32, device=None):
|
||||
return torch.nn.GroupNorm(num_groups=num_groups, num_channels=in_channels, eps=1e-6, affine=True, dtype=dtype, device=device)
|
||||
|
||||
|
||||
class ResnetBlock(torch.nn.Module):
|
||||
def __init__(self, *, in_channels, out_channels=None, dtype=torch.float32, device=None):
|
||||
super().__init__()
|
||||
self.in_channels = in_channels
|
||||
out_channels = in_channels if out_channels is None else out_channels
|
||||
self.out_channels = out_channels
|
||||
|
||||
self.norm1 = Normalize(in_channels, dtype=dtype, device=device)
|
||||
self.conv1 = torch.nn.Conv2d(in_channels, out_channels, kernel_size=3, stride=1, padding=1, dtype=dtype, device=device)
|
||||
self.norm2 = Normalize(out_channels, dtype=dtype, device=device)
|
||||
self.conv2 = torch.nn.Conv2d(out_channels, out_channels, kernel_size=3, stride=1, padding=1, dtype=dtype, device=device)
|
||||
if self.in_channels != self.out_channels:
|
||||
self.nin_shortcut = torch.nn.Conv2d(in_channels, out_channels, kernel_size=1, stride=1, padding=0, dtype=dtype, device=device)
|
||||
else:
|
||||
self.nin_shortcut = None
|
||||
self.swish = torch.nn.SiLU(inplace=True)
|
||||
|
||||
def forward(self, x):
|
||||
hidden = x
|
||||
hidden = self.norm1(hidden)
|
||||
hidden = self.swish(hidden)
|
||||
hidden = self.conv1(hidden)
|
||||
hidden = self.norm2(hidden)
|
||||
hidden = self.swish(hidden)
|
||||
hidden = self.conv2(hidden)
|
||||
if self.in_channels != self.out_channels:
|
||||
x = self.nin_shortcut(x)
|
||||
return x + hidden
|
||||
|
||||
|
||||
class AttnBlock(torch.nn.Module):
|
||||
def __init__(self, in_channels, dtype=torch.float32, device=None):
|
||||
super().__init__()
|
||||
self.norm = Normalize(in_channels, dtype=dtype, device=device)
|
||||
self.q = torch.nn.Conv2d(in_channels, in_channels, kernel_size=1, stride=1, padding=0, dtype=dtype, device=device)
|
||||
self.k = torch.nn.Conv2d(in_channels, in_channels, kernel_size=1, stride=1, padding=0, dtype=dtype, device=device)
|
||||
self.v = torch.nn.Conv2d(in_channels, in_channels, kernel_size=1, stride=1, padding=0, dtype=dtype, device=device)
|
||||
self.proj_out = torch.nn.Conv2d(in_channels, in_channels, kernel_size=1, stride=1, padding=0, dtype=dtype, device=device)
|
||||
|
||||
def forward(self, x):
|
||||
hidden = self.norm(x)
|
||||
q = self.q(hidden)
|
||||
k = self.k(hidden)
|
||||
v = self.v(hidden)
|
||||
b, c, h, w = q.shape
|
||||
q, k, v = map(lambda x: einops.rearrange(x, "b c h w -> b 1 (h w) c").contiguous(), (q, k, v))
|
||||
hidden = torch.nn.functional.scaled_dot_product_attention(q, k, v) # scale is dim ** -0.5 per default
|
||||
hidden = einops.rearrange(hidden, "b 1 (h w) c -> b c h w", h=h, w=w, c=c, b=b)
|
||||
hidden = self.proj_out(hidden)
|
||||
return x + hidden
|
||||
|
||||
|
||||
class Downsample(torch.nn.Module):
|
||||
def __init__(self, in_channels, dtype=torch.float32, device=None):
|
||||
super().__init__()
|
||||
self.conv = torch.nn.Conv2d(in_channels, in_channels, kernel_size=3, stride=2, padding=0, dtype=dtype, device=device)
|
||||
|
||||
def forward(self, x):
|
||||
pad = (0,1,0,1)
|
||||
x = torch.nn.functional.pad(x, pad, mode="constant", value=0)
|
||||
x = self.conv(x)
|
||||
return x
|
||||
|
||||
|
||||
class Upsample(torch.nn.Module):
|
||||
def __init__(self, in_channels, dtype=torch.float32, device=None):
|
||||
super().__init__()
|
||||
self.conv = torch.nn.Conv2d(in_channels, in_channels, kernel_size=3, stride=1, padding=1, dtype=dtype, device=device)
|
||||
|
||||
def forward(self, x):
|
||||
x = torch.nn.functional.interpolate(x, scale_factor=2.0, mode="nearest")
|
||||
x = self.conv(x)
|
||||
return x
|
||||
|
||||
|
||||
class VAEEncoder(torch.nn.Module):
|
||||
def __init__(self, ch=128, ch_mult=(1,2,4,4), num_res_blocks=2, in_channels=3, z_channels=16, dtype=torch.float32, device=None):
|
||||
super().__init__()
|
||||
self.num_resolutions = len(ch_mult)
|
||||
self.num_res_blocks = num_res_blocks
|
||||
# downsampling
|
||||
self.conv_in = torch.nn.Conv2d(in_channels, ch, kernel_size=3, stride=1, padding=1, dtype=dtype, device=device)
|
||||
in_ch_mult = (1,) + tuple(ch_mult)
|
||||
self.in_ch_mult = in_ch_mult
|
||||
self.down = torch.nn.ModuleList()
|
||||
for i_level in range(self.num_resolutions):
|
||||
block = torch.nn.ModuleList()
|
||||
attn = torch.nn.ModuleList()
|
||||
block_in = ch*in_ch_mult[i_level]
|
||||
block_out = ch*ch_mult[i_level]
|
||||
for i_block in range(num_res_blocks):
|
||||
block.append(ResnetBlock(in_channels=block_in, out_channels=block_out, dtype=dtype, device=device))
|
||||
block_in = block_out
|
||||
down = torch.nn.Module()
|
||||
down.block = block
|
||||
down.attn = attn
|
||||
if i_level != self.num_resolutions - 1:
|
||||
down.downsample = Downsample(block_in, dtype=dtype, device=device)
|
||||
self.down.append(down)
|
||||
# middle
|
||||
self.mid = torch.nn.Module()
|
||||
self.mid.block_1 = ResnetBlock(in_channels=block_in, out_channels=block_in, dtype=dtype, device=device)
|
||||
self.mid.attn_1 = AttnBlock(block_in, dtype=dtype, device=device)
|
||||
self.mid.block_2 = ResnetBlock(in_channels=block_in, out_channels=block_in, dtype=dtype, device=device)
|
||||
# end
|
||||
self.norm_out = Normalize(block_in, dtype=dtype, device=device)
|
||||
self.conv_out = torch.nn.Conv2d(block_in, 2 * z_channels, kernel_size=3, stride=1, padding=1, dtype=dtype, device=device)
|
||||
self.swish = torch.nn.SiLU(inplace=True)
|
||||
|
||||
def forward(self, x):
|
||||
# downsampling
|
||||
hs = [self.conv_in(x)]
|
||||
for i_level in range(self.num_resolutions):
|
||||
for i_block in range(self.num_res_blocks):
|
||||
h = self.down[i_level].block[i_block](hs[-1])
|
||||
hs.append(h)
|
||||
if i_level != self.num_resolutions-1:
|
||||
hs.append(self.down[i_level].downsample(hs[-1]))
|
||||
# middle
|
||||
h = hs[-1]
|
||||
h = self.mid.block_1(h)
|
||||
h = self.mid.attn_1(h)
|
||||
h = self.mid.block_2(h)
|
||||
# end
|
||||
h = self.norm_out(h)
|
||||
h = self.swish(h)
|
||||
h = self.conv_out(h)
|
||||
return h
|
||||
|
||||
|
||||
class VAEDecoder(torch.nn.Module):
|
||||
def __init__(self, ch=128, out_ch=3, ch_mult=(1, 2, 4, 4), num_res_blocks=2, resolution=256, z_channels=16, dtype=torch.float32, device=None):
|
||||
super().__init__()
|
||||
self.num_resolutions = len(ch_mult)
|
||||
self.num_res_blocks = num_res_blocks
|
||||
block_in = ch * ch_mult[self.num_resolutions - 1]
|
||||
curr_res = resolution // 2 ** (self.num_resolutions - 1)
|
||||
# z to block_in
|
||||
self.conv_in = torch.nn.Conv2d(z_channels, block_in, kernel_size=3, stride=1, padding=1, dtype=dtype, device=device)
|
||||
# middle
|
||||
self.mid = torch.nn.Module()
|
||||
self.mid.block_1 = ResnetBlock(in_channels=block_in, out_channels=block_in, dtype=dtype, device=device)
|
||||
self.mid.attn_1 = AttnBlock(block_in, dtype=dtype, device=device)
|
||||
self.mid.block_2 = ResnetBlock(in_channels=block_in, out_channels=block_in, dtype=dtype, device=device)
|
||||
# upsampling
|
||||
self.up = torch.nn.ModuleList()
|
||||
for i_level in reversed(range(self.num_resolutions)):
|
||||
block = torch.nn.ModuleList()
|
||||
block_out = ch * ch_mult[i_level]
|
||||
for i_block in range(self.num_res_blocks + 1):
|
||||
block.append(ResnetBlock(in_channels=block_in, out_channels=block_out, dtype=dtype, device=device))
|
||||
block_in = block_out
|
||||
up = torch.nn.Module()
|
||||
up.block = block
|
||||
if i_level != 0:
|
||||
up.upsample = Upsample(block_in, dtype=dtype, device=device)
|
||||
curr_res = curr_res * 2
|
||||
self.up.insert(0, up) # prepend to get consistent order
|
||||
# end
|
||||
self.norm_out = Normalize(block_in, dtype=dtype, device=device)
|
||||
self.conv_out = torch.nn.Conv2d(block_in, out_ch, kernel_size=3, stride=1, padding=1, dtype=dtype, device=device)
|
||||
self.swish = torch.nn.SiLU(inplace=True)
|
||||
|
||||
def forward(self, z):
|
||||
# z to block_in
|
||||
hidden = self.conv_in(z)
|
||||
# middle
|
||||
hidden = self.mid.block_1(hidden)
|
||||
hidden = self.mid.attn_1(hidden)
|
||||
hidden = self.mid.block_2(hidden)
|
||||
# upsampling
|
||||
for i_level in reversed(range(self.num_resolutions)):
|
||||
for i_block in range(self.num_res_blocks + 1):
|
||||
hidden = self.up[i_level].block[i_block](hidden)
|
||||
if i_level != 0:
|
||||
hidden = self.up[i_level].upsample(hidden)
|
||||
# end
|
||||
hidden = self.norm_out(hidden)
|
||||
hidden = self.swish(hidden)
|
||||
hidden = self.conv_out(hidden)
|
||||
return hidden
|
||||
|
||||
|
||||
class SDVAE(torch.nn.Module):
|
||||
def __init__(self, dtype=torch.float32, device=None):
|
||||
super().__init__()
|
||||
self.encoder = VAEEncoder(dtype=dtype, device=device)
|
||||
self.decoder = VAEDecoder(dtype=dtype, device=device)
|
||||
|
||||
@torch.autocast("cuda", dtype=torch.float16)
|
||||
def decode(self, latent):
|
||||
return self.decoder(latent)
|
||||
|
||||
@torch.autocast("cuda", dtype=torch.float16)
|
||||
def encode(self, image):
|
||||
hidden = self.encoder(image)
|
||||
mean, logvar = torch.chunk(hidden, 2, dim=1)
|
||||
logvar = torch.clamp(logvar, -30.0, 20.0)
|
||||
std = torch.exp(0.5 * logvar)
|
||||
return mean + std * torch.randn_like(mean)
|
||||
@@ -0,0 +1,81 @@
|
||||
#
|
||||
# SPDX-FileCopyrightText: Copyright (c) 1993-2025 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.
|
||||
#
|
||||
|
||||
# Configure dependencies before any external imports
|
||||
from demo_diffusion import deps
|
||||
deps.configure("sd")
|
||||
|
||||
import argparse
|
||||
|
||||
import PIL
|
||||
from cuda.bindings import runtime as cudart
|
||||
from PIL import Image
|
||||
|
||||
from demo_diffusion import dd_argparse
|
||||
from demo_diffusion import image as image_module
|
||||
from demo_diffusion import pipeline as pipeline_module
|
||||
|
||||
|
||||
def parseArgs():
|
||||
parser = argparse.ArgumentParser(description="Options for Stable Diffusion Img2Img Demo")
|
||||
parser = dd_argparse.add_arguments(parser)
|
||||
parser.add_argument('--input-image', type=str, default="", help="Path to the input image")
|
||||
return parser.parse_args()
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("[I] Initializing StableDiffusion img2img demo using TensorRT")
|
||||
args = parseArgs()
|
||||
|
||||
if args.input_image:
|
||||
input_image = Image.open(args.input_image)
|
||||
else:
|
||||
url = "https://raw.githubusercontent.com/CompVis/stable-diffusion/main/assets/stable-samples/img2img/sketch-mountains-input.jpg"
|
||||
input_image = image_module.download_image(url)
|
||||
|
||||
image_width, image_height = input_image.size
|
||||
if image_height != args.height or image_width != args.width:
|
||||
print(f"[I] Resizing input_image to {args.height}x{args.width}")
|
||||
input_image = input_image.resize((args.width, args.height))
|
||||
image_height, image_width = args.height, args.width
|
||||
|
||||
if isinstance(input_image, PIL.Image.Image):
|
||||
input_image = image_module.preprocess_image(input_image)
|
||||
|
||||
kwargs_init_pipeline, kwargs_load_engine, args_run_demo = dd_argparse.process_pipeline_args(args)
|
||||
|
||||
# Initialize demo
|
||||
demo = pipeline_module.StableDiffusionPipeline(
|
||||
pipeline_type=pipeline_module.PIPELINE_TYPE.IMG2IMG, **kwargs_init_pipeline
|
||||
)
|
||||
|
||||
# Load TensorRT engines and pytorch modules
|
||||
demo.loadEngines(
|
||||
args.engine_dir,
|
||||
args.framework_model_dir,
|
||||
args.onnx_dir,
|
||||
**kwargs_load_engine)
|
||||
|
||||
# Load resources
|
||||
_, shared_device_memory = cudart.cudaMalloc(demo.calculateMaxDeviceMemory())
|
||||
demo.activateEngines(shared_device_memory)
|
||||
demo.loadResources(args.height, args.width, args.batch_size, args.seed)
|
||||
|
||||
# Run inference
|
||||
demo_kwargs = {'input_image': input_image, 'image_strength': 0.75}
|
||||
demo.run(*args_run_demo, **demo_kwargs)
|
||||
|
||||
demo.teardown()
|
||||
Executable
+284
@@ -0,0 +1,284 @@
|
||||
#
|
||||
# SPDX-FileCopyrightText: Copyright (c) 1993-2025 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.
|
||||
#
|
||||
|
||||
# Configure dependencies before any external imports
|
||||
from demo_diffusion import deps
|
||||
deps.configure("flux")
|
||||
|
||||
import argparse
|
||||
import os
|
||||
|
||||
import controlnet_aux
|
||||
from cuda.bindings import runtime as cudart
|
||||
from PIL import Image
|
||||
|
||||
from demo_diffusion import dd_argparse
|
||||
from demo_diffusion import pipeline as pipeline_module
|
||||
|
||||
|
||||
def parse_args():
|
||||
parser = argparse.ArgumentParser(description="Options for Flux Img2Img Demo", conflict_handler="resolve")
|
||||
parser = dd_argparse.add_arguments(parser)
|
||||
parser.add_argument(
|
||||
"--version",
|
||||
type=str,
|
||||
default="flux.1-dev",
|
||||
choices=("flux.1-dev", "flux.1-schnell", "flux.1-dev-canny", "flux.1-dev-depth", "flux.1-kontext-dev"),
|
||||
help="Version of Flux",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--prompt2",
|
||||
default=None,
|
||||
nargs="*",
|
||||
help="Text prompt(s) to be sent to the T5 tokenizer and text encoder. If not defined, prompt will be used instead",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--height",
|
||||
type=int,
|
||||
default=1024,
|
||||
help="Height of image to generate (must be multiple of 8)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--width",
|
||||
type=int,
|
||||
default=1024,
|
||||
help="Width of image to generate (must be multiple of 8)",
|
||||
)
|
||||
parser.add_argument("--denoising-steps", type=int, default=50, help="Number of denoising steps")
|
||||
parser.add_argument(
|
||||
"--guidance-scale",
|
||||
type=float,
|
||||
default=3.5,
|
||||
help="Value of classifier-free guidance scale (must be greater than 1)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--max_sequence_length",
|
||||
type=int,
|
||||
help="Maximum sequence length to use with the prompt. Can be up to 512 for the dev and 256 for the schnell variant.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--t5-ws-percentage",
|
||||
type=int,
|
||||
default=None,
|
||||
help="Set runtime weight streaming budget as the percentage of the size of streamable weights for the T5 model. This argument only takes effect when --ws is set. 0 streams the most weights and 100 or None streams no weights. ",
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--transformer-ws-percentage",
|
||||
type=int,
|
||||
default=None,
|
||||
help="Set runtime weight streaming budget as the percentage of the size of streamable weights for the transformer model. This argument only takes effect when --ws is set. 0 streams the most weights and 100 or None streams no weights.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--control-image",
|
||||
type=str,
|
||||
default=None,
|
||||
help="Path to the control image for the flux.1-dev-canny and flux.1-dev-depth pipelines",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--input-image",
|
||||
type=str,
|
||||
default=None,
|
||||
help="Path to the input conditioning image for the flux.1-dev and flux.1-schnell img2img pipelines",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--kontext-image",
|
||||
type=str,
|
||||
default=None,
|
||||
help="Path to the input image for Kontext pipeline (flux.1-kontext-dev only, required)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--image-strength",
|
||||
type=float,
|
||||
default=1.0,
|
||||
help="Indicates extent to transform the reference `image`. Must be between 0 and 1. A value of 1 essentially ignores the input image.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--calibration-dataset",
|
||||
type=str,
|
||||
default=None,
|
||||
help="Path to the calibration dataset for quantization (only enabled for controlnet)",
|
||||
)
|
||||
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def process_demo_args(args):
|
||||
batch_size = args.batch_size
|
||||
prompt = args.prompt
|
||||
# If prompt2 is not defined, use prompt instead
|
||||
prompt2 = args.prompt2 or prompt
|
||||
|
||||
# Process input args
|
||||
if not isinstance(prompt, list):
|
||||
raise ValueError(f"`prompt` must be of type `list[str]`, but is {type(prompt)}")
|
||||
prompt = prompt * batch_size
|
||||
|
||||
if not isinstance(prompt2, list):
|
||||
raise ValueError(f"`prompt2` must be of type `str` list, but is {type(prompt2)}")
|
||||
if len(prompt2) == 1:
|
||||
prompt2 = prompt2 * batch_size
|
||||
|
||||
max_seq_supported_by_model = {
|
||||
"flux.1-schnell": 256,
|
||||
"flux.1-dev": 512,
|
||||
"flux.1-dev-canny": 512,
|
||||
"flux.1-dev-depth": 512,
|
||||
"flux.1-kontext-dev": 512,
|
||||
}[args.version]
|
||||
if args.max_sequence_length is not None:
|
||||
if args.max_sequence_length > max_seq_supported_by_model:
|
||||
raise ValueError(
|
||||
f"For {args.version}, `max_sequence_length` cannot be greater than {max_seq_supported_by_model} but is {args.max_sequence_length}"
|
||||
)
|
||||
else:
|
||||
args.max_sequence_length = max_seq_supported_by_model
|
||||
|
||||
controlnet_type = "depth" if "depth" in args.version else "canny" if "canny" in args.version else ""
|
||||
if controlnet_type:
|
||||
if args.input_image:
|
||||
raise ValueError(
|
||||
f"--input-image is a valid input for versions [flux.1-dev, flux.1-schnell]. Provided {args.version}"
|
||||
)
|
||||
if not args.control_image:
|
||||
raise ValueError(
|
||||
"--control-image input is required for versions [flux.1-dev-canny, flux.1-dev-depth]. Please provide it using --control-image flag."
|
||||
)
|
||||
args.control_image = Image.open(args.control_image).convert("RGB")
|
||||
|
||||
if controlnet_type == "canny":
|
||||
processor = controlnet_aux.CannyDetector()
|
||||
args.control_image = processor(
|
||||
args.control_image, low_threshold=50, high_threshold=200, detect_resolution=1024, image_resolution=1024
|
||||
)
|
||||
elif controlnet_type == "depth":
|
||||
args.control_image = controlnet_aux.LeresDetector.from_pretrained("lllyasviel/Annotators")(
|
||||
args.control_image
|
||||
)
|
||||
else:
|
||||
raise ValueError("Invalid controlnet type")
|
||||
else:
|
||||
if args.control_image:
|
||||
raise ValueError(
|
||||
f"--control-image is a valid input for versions [flux.1-dev-canny, flux.1-dev-depth]. Provided {args.version}"
|
||||
)
|
||||
|
||||
# Handle input image for img2img pipelines
|
||||
if args.version == "flux.1-kontext-dev":
|
||||
# For Kontext pipeline, only use kontext-image
|
||||
if not args.kontext_image:
|
||||
raise ValueError(
|
||||
"--kontext-image is required for the Kontext pipeline. Please provide it using the --kontext-image flag."
|
||||
)
|
||||
if args.input_image:
|
||||
raise ValueError(
|
||||
"--input-image is not supported for the Kontext pipeline. Please use --kontext-image instead."
|
||||
)
|
||||
# Kontext pipeline doesn't resize the input image
|
||||
args.kontext_image = Image.open(args.kontext_image).convert("RGB")
|
||||
else:
|
||||
if not args.input_image:
|
||||
raise ValueError(
|
||||
"--input-image is required for the img2img pipeline. Please provide it using the --input-image flag."
|
||||
)
|
||||
args.input_image = Image.open(args.input_image).convert("RGB").resize((args.width, args.height))
|
||||
|
||||
if args.fp8:
|
||||
if args.version == "flux.1-dev" or args.version == "flux.1-schnell":
|
||||
raise ValueError("--fp8 is currently not supported for Flux.1-dev and Flux.1-schnell img2img pipelines.")
|
||||
|
||||
if not args.calibration_dataset:
|
||||
args.calibration_dataset = os.path.join(f"{controlnet_type}-eval", "benchmark")
|
||||
print(f"[W] Calibration dataset path not provided, setting default path to {args.calibration_dataset}.")
|
||||
|
||||
if not os.path.exists(args.calibration_dataset):
|
||||
print(
|
||||
f"[W] Could not find the calibration dataset at {args.calibration_dataset}, and will fallback to using pre-exported ONNX models. Please follow the instructions in README to download calibration dataset and provide the path if pre-exported ONNX models are not provided either."
|
||||
)
|
||||
|
||||
if args.version == "flux.1-kontext-dev" and not args.download_onnx_models:
|
||||
raise ValueError(
|
||||
"--download-onnx-models is required when using --fp8 for Flux.1-kontext-dev img2img pipeline."
|
||||
)
|
||||
|
||||
if args.fp4:
|
||||
if args.version == "flux.1-dev" or args.version == "flux.1-schnell":
|
||||
raise ValueError("--fp4 is currently not supported for Flux.1-dev and Flux.1-schnell img2img pipelines.")
|
||||
if not args.download_onnx_models:
|
||||
raise ValueError("--download-onnx-models is required when using --fp4.")
|
||||
|
||||
kwargs_run_demo = {
|
||||
"prompt": prompt,
|
||||
"prompt2": prompt2,
|
||||
"height": args.height,
|
||||
"width": args.width,
|
||||
"batch_count": args.batch_count,
|
||||
"num_warmup_runs": args.num_warmup_runs,
|
||||
"use_cuda_graph": args.use_cuda_graph,
|
||||
"image_strength": args.image_strength,
|
||||
}
|
||||
|
||||
# Add the appropriate image parameter based on pipeline type
|
||||
if not args.version == "flux.1-kontext-dev":
|
||||
kwargs_run_demo["input_image"] = args.input_image
|
||||
kwargs_run_demo["control_image"] = args.control_image
|
||||
|
||||
return kwargs_run_demo
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("[I] Initializing Flux img2img demo using TensorRT")
|
||||
args = parse_args()
|
||||
|
||||
_, kwargs_load_engine, _ = dd_argparse.process_pipeline_args(args)
|
||||
kwargs_run_demo = process_demo_args(args)
|
||||
|
||||
# Initialize demo
|
||||
pipeline_type = pipeline_module.PIPELINE_TYPE.IMG2IMG
|
||||
if args.version == "flux.1-kontext-dev":
|
||||
demo = pipeline_module.FluxKontextPipeline.FromArgs(args, pipeline_type=pipeline_type)
|
||||
else:
|
||||
demo = pipeline_module.FluxPipeline.FromArgs(args, pipeline_type=pipeline_type)
|
||||
|
||||
# Load TensorRT engines and pytorch modules
|
||||
demo.load_engines(
|
||||
framework_model_dir=args.framework_model_dir,
|
||||
**kwargs_load_engine,
|
||||
)
|
||||
|
||||
if args.onnx_export_only:
|
||||
print("[I] ONNX export completed. Exiting...")
|
||||
demo.teardown()
|
||||
exit(0)
|
||||
|
||||
# Since VAE and VAE_encoder require by far the largest device memories, in low-vram mode
|
||||
# we allocate the required device memory individually before each model is run.
|
||||
if demo.low_vram:
|
||||
demo.device_memory_sizes = demo.get_device_memory_sizes()
|
||||
else:
|
||||
_, shared_device_memory = cudart.cudaMalloc(demo.calculate_max_device_memory())
|
||||
demo.activate_engines(shared_device_memory)
|
||||
|
||||
demo.load_resources(args.height, args.width, args.batch_size, args.seed)
|
||||
|
||||
# Run inference
|
||||
images = demo.run(**kwargs_run_demo)
|
||||
|
||||
demo.teardown()
|
||||
|
||||
# save images
|
||||
demo.save_images(kwargs_run_demo["prompt"], images, check_integrity=(args.version == "flux.1-kontext-dev"))
|
||||
@@ -0,0 +1,134 @@
|
||||
#
|
||||
# SPDX-FileCopyrightText: Copyright (c) 1993-2025 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.
|
||||
#
|
||||
|
||||
# Configure dependencies before any external imports
|
||||
from demo_diffusion import deps
|
||||
deps.configure("sd")
|
||||
|
||||
import argparse
|
||||
|
||||
from PIL import Image
|
||||
|
||||
from demo_diffusion import dd_argparse
|
||||
from demo_diffusion import image as image_module
|
||||
from demo_diffusion import pipeline as pipeline_module
|
||||
|
||||
|
||||
def parseArgs():
|
||||
parser = argparse.ArgumentParser(description="Options for Stable Diffusion Img2Vid Demo", conflict_handler='resolve')
|
||||
parser = dd_argparse.add_arguments(parser)
|
||||
parser.add_argument('--version', type=str, default="svd-xt-1.1", choices=["svd-xt-1.1"], help="Version of Stable Video Diffusion")
|
||||
parser.add_argument('--input-image', type=str, default="", help="Path to the input image")
|
||||
parser.add_argument('--height', type=int, default=576, help="Height of image to generate (must be multiple of 8)")
|
||||
parser.add_argument('--width', type=int, default=1024, help="Width of image to generate (must be multiple of 8)")
|
||||
parser.add_argument('--min-guidance-scale', type=float, default=1.0, help="The minimum guidance scale. Used for the classifier free guidance with first frame")
|
||||
parser.add_argument('--max-guidance-scale', type=float, default=3.0, help="The maximum guidance scale. Used for the classifier free guidance with last frame")
|
||||
parser.add_argument('--denoising-steps', type=int, default=25, help="Number of denoising steps")
|
||||
parser.add_argument('--num-warmup-runs', type=int, default=1, help="Number of warmup runs before benchmarking performance")
|
||||
return parser.parse_args()
|
||||
|
||||
def process_pipeline_args(args):
|
||||
|
||||
if not args.input_image:
|
||||
args.input_image = "https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/diffusers/svd/rocket.png?download=true"
|
||||
if isinstance(args.input_image, str):
|
||||
input_image = image_module.download_image(args.input_image).resize((args.width, args.height))
|
||||
elif isinstance(args.input_image, Image.Image):
|
||||
input_image = Image.open(args.input_image)
|
||||
else:
|
||||
raise ValueError(f"Input image(s) must be of type `PIL.Image.Image` or `str` (URL) but is {type(args.input_image)}")
|
||||
|
||||
if args.height % 8 != 0 or args.width % 8 != 0:
|
||||
raise ValueError(f"Image height and width have to be divisible by 8 but are: {args.image_height} and {args.width}.")
|
||||
|
||||
# TODO enable BS>1
|
||||
max_batch_size = 1
|
||||
args.build_static_batch = True
|
||||
|
||||
if args.batch_size > max_batch_size:
|
||||
raise ValueError(f"Batch size {args.batch_size} is larger than allowed {max_batch_size}.")
|
||||
|
||||
if not args.build_static_batch or args.build_dynamic_shape:
|
||||
raise ValueError("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,
|
||||
'denoising_steps': args.denoising_steps,
|
||||
'scheduler': args.scheduler,
|
||||
'min_guidance_scale': args.min_guidance_scale,
|
||||
'max_guidance_scale': args.max_guidance_scale,
|
||||
'output_dir': args.output_dir,
|
||||
'hf_token': args.hf_token,
|
||||
'verbose': args.verbose,
|
||||
'nvtx_profile': args.nvtx_profile,
|
||||
'use_cuda_graph': args.use_cuda_graph,
|
||||
'framework_model_dir': args.framework_model_dir,
|
||||
'torch_inference': args.torch_inference,
|
||||
}
|
||||
|
||||
kwargs_load_engine = {
|
||||
'onnx_opset': args.onnx_opset,
|
||||
'opt_batch_size': args.batch_size,
|
||||
'opt_image_height': args.height,
|
||||
'opt_image_width': args.width,
|
||||
'static_batch': args.build_static_batch,
|
||||
'static_shape': not args.build_dynamic_shape,
|
||||
'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)
|
||||
|
||||
return kwargs_init_pipeline, kwargs_load_engine, args_run_demo
|
||||
|
||||
if __name__ == "__main__":
|
||||
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 = pipeline_module.StableVideoDiffusionPipeline(
|
||||
pipeline_type=pipeline_module.PIPELINE_TYPE.IMG2VID, **kwargs_init_pipeline
|
||||
)
|
||||
demo.loadEngines(
|
||||
args.engine_dir,
|
||||
args.framework_model_dir,
|
||||
args.onnx_dir,
|
||||
**kwargs_load_engine)
|
||||
demo.loadResources(args.height, args.width, args.batch_size, args.seed)
|
||||
|
||||
# Run inference
|
||||
demo.run(*args_run_demo)
|
||||
|
||||
demo.teardown()
|
||||
@@ -0,0 +1,165 @@
|
||||
#
|
||||
# SPDX-FileCopyrightText: Copyright (c) 1993-2025 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.
|
||||
#
|
||||
|
||||
# Configure dependencies before any external imports
|
||||
from demo_diffusion import deps
|
||||
deps.configure("sd")
|
||||
|
||||
import argparse
|
||||
import os
|
||||
|
||||
import torch
|
||||
from cuda.bindings import runtime as cudart
|
||||
|
||||
from demo_diffusion import dd_argparse
|
||||
from demo_diffusion import pipeline as pipeline_module
|
||||
|
||||
|
||||
def parseArgs():
|
||||
parser = argparse.ArgumentParser(description="Options for Stable Cascade Txt2Img Demo", conflict_handler='resolve')
|
||||
parser = dd_argparse.add_arguments(parser)
|
||||
parser.add_argument('--version', type=str, default="cascade", choices=["cascade"], help="Version of Stable Cascade")
|
||||
parser.add_argument('--height', type=int, default=1024, help="Height of image to generate (must be multiple of 8)")
|
||||
parser.add_argument('--width', type=int, default=1024, help="Width of image to generate (must be multiple of 8)")
|
||||
parser.add_argument('--lite', action='store_true', help="Use the Lite Version of the Stage B and Stage C models")
|
||||
parser.add_argument('--prior-guidance-scale', type=float, default=4.0, help="Value of classifier-free guidance scale for the prior")
|
||||
parser.add_argument('--decoder-guidance-scale', type=float, default=0.0, help="Value of classifier-free guidance scale for the decoder")
|
||||
parser.add_argument('--prior-denoising-steps', type=int, default=20, help="Number of denoising steps for the prior")
|
||||
parser.add_argument('--decoder-denoising-steps', type=int, default=10, help="Number of denoising steps for the decoder")
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
class StableCascadeDemoPipeline(pipeline_module.StableCascadePipeline):
|
||||
def __init__(self, prior_denoising_steps, decoder_denoising_steps, prior_guidance_scale, decoder_guidance_scale, lite, **kwargs):
|
||||
self.nvtx_profile = kwargs['nvtx_profile']
|
||||
self.prior = pipeline_module.StableCascadePipeline(
|
||||
pipeline_type=pipeline_module.PIPELINE_TYPE.CASCADE_PRIOR,
|
||||
denoising_steps=prior_denoising_steps,
|
||||
guidance_scale=prior_guidance_scale,
|
||||
return_latents=True,
|
||||
lite=lite,
|
||||
**kwargs,
|
||||
)
|
||||
self.decoder = pipeline_module.StableCascadePipeline(
|
||||
pipeline_type=pipeline_module.PIPELINE_TYPE.CASCADE_DECODER,
|
||||
denoising_steps=decoder_denoising_steps,
|
||||
guidance_scale=decoder_guidance_scale,
|
||||
lite=lite,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
def loadEngines(self, framework_model_dir, onnx_dir, engine_dir, **kwargs):
|
||||
prior_suffix = "prior_lite" if self.prior.lite else "prior"
|
||||
decoder_suffix = "decoder_lite" if self.decoder.lite else "decoder"
|
||||
self.prior.loadEngines(
|
||||
os.path.join(engine_dir, prior_suffix),
|
||||
framework_model_dir,
|
||||
os.path.join(onnx_dir, prior_suffix),
|
||||
**kwargs)
|
||||
self.decoder.loadEngines(
|
||||
os.path.join(engine_dir, decoder_suffix),
|
||||
framework_model_dir,
|
||||
os.path.join(onnx_dir, decoder_suffix),
|
||||
**kwargs)
|
||||
|
||||
def activateEngines(self, shared_device_memory=None):
|
||||
self.prior.activateEngines(shared_device_memory)
|
||||
self.decoder.activateEngines(shared_device_memory)
|
||||
|
||||
def loadResources(self, image_height, image_width, batch_size, seed):
|
||||
self.prior.loadResources(image_height, image_width, batch_size, seed)
|
||||
# Use a different seed for decoder
|
||||
self.decoder.loadResources(image_height, image_width, batch_size, ((seed+1) if seed is not None else None))
|
||||
|
||||
def get_max_device_memory(self):
|
||||
max_device_memory = self.prior.calculateMaxDeviceMemory()
|
||||
max_device_memory = max(max_device_memory, self.decoder.calculateMaxDeviceMemory())
|
||||
return max_device_memory
|
||||
|
||||
def run(self, prompt, negative_prompt, height, width, batch_size, batch_count, num_warmup_runs, use_cuda_graph):
|
||||
# Process prompt
|
||||
if not isinstance(prompt, list):
|
||||
raise ValueError(f"`prompt` must be of type `str` list, but is {type(prompt)}")
|
||||
prompt = prompt * batch_size
|
||||
|
||||
if not isinstance(negative_prompt, list):
|
||||
raise ValueError(f"`--negative-prompt` must be of type `str` list, but is {type(negative_prompt)}")
|
||||
if len(negative_prompt) == 1:
|
||||
negative_prompt = negative_prompt * batch_size
|
||||
|
||||
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 ..")
|
||||
for _ in range(num_warmup_runs):
|
||||
latents, _ = self.prior.infer(prompt, negative_prompt, height, width, warmup=True)
|
||||
latents = latents.to(torch.float16) if self.decoder.fp16 else latents
|
||||
images, _ = self.decoder.infer(prompt, negative_prompt, height, width, image_embeddings=latents, warmup=True)
|
||||
|
||||
for _ in range(batch_count):
|
||||
print("[I] Running Stable Cascade pipeline")
|
||||
if self.nvtx_profile:
|
||||
cudart.cudaProfilerStart()
|
||||
latents, time_prior = self.prior.infer(prompt, negative_prompt, height, width, warmup=False)
|
||||
latents = latents.to(torch.float16) if self.decoder.fp16 else latents
|
||||
images, time_decoder = self.decoder.infer(prompt, negative_prompt, height, width, image_embeddings=latents, warmup=False)
|
||||
|
||||
if self.nvtx_profile:
|
||||
cudart.cudaProfilerStop()
|
||||
print('|-----------------|--------------|')
|
||||
print('| {:^15} | {:>9.2f} ms |'.format('e2e', time_prior + time_decoder))
|
||||
print('|-----------------|--------------|')
|
||||
|
||||
def teardown(self):
|
||||
self.prior.teardown()
|
||||
self.decoder.teardown()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("[I] Initializing StableCascade txt2img demo using TensorRT")
|
||||
args = parseArgs()
|
||||
|
||||
kwargs_init_pipeline, kwargs_load_engine, args_run_demo = dd_argparse.process_pipeline_args(args)
|
||||
|
||||
# Initialize demo
|
||||
_ = kwargs_init_pipeline.pop('guidance_scale')
|
||||
_ = kwargs_init_pipeline.pop('denoising_steps')
|
||||
demo = StableCascadeDemoPipeline(
|
||||
args.prior_denoising_steps,
|
||||
args.decoder_denoising_steps,
|
||||
args.prior_guidance_scale,
|
||||
args.decoder_guidance_scale,
|
||||
args.lite,
|
||||
**kwargs_init_pipeline
|
||||
)
|
||||
|
||||
# Load TensorRT engines and pytorch modules
|
||||
demo.loadEngines(
|
||||
args.framework_model_dir,
|
||||
args.onnx_dir,
|
||||
args.engine_dir,
|
||||
**kwargs_load_engine,
|
||||
)
|
||||
|
||||
# Load resources
|
||||
_, shared_device_memory = cudart.cudaMalloc(demo.get_max_device_memory())
|
||||
demo.activateEngines(shared_device_memory)
|
||||
demo.loadResources(args.height, args.width, args.batch_size, args.seed)
|
||||
|
||||
# Run inference
|
||||
demo.run(*args_run_demo)
|
||||
|
||||
demo.teardown()
|
||||
@@ -0,0 +1,151 @@
|
||||
#
|
||||
# SPDX-FileCopyrightText: Copyright (c) 1993-2025 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.
|
||||
#
|
||||
|
||||
# Configure dependencies before any external imports
|
||||
from demo_diffusion import deps
|
||||
deps.configure("cosmos")
|
||||
|
||||
import argparse
|
||||
|
||||
from cuda.bindings import runtime as cudart
|
||||
|
||||
from demo_diffusion import dd_argparse
|
||||
from demo_diffusion import pipeline as pipeline_module
|
||||
|
||||
|
||||
def parse_args():
|
||||
parser = argparse.ArgumentParser(description="Options for Cosmos text2image Demo", conflict_handler="resolve")
|
||||
parser = dd_argparse.add_arguments(parser)
|
||||
parser.add_argument(
|
||||
"--version",
|
||||
type=str,
|
||||
default="cosmos-predict2-2b-text2image",
|
||||
choices=("cosmos-predict2-2b-text2image", "cosmos-predict2-14b-text2image"),
|
||||
help="Version of Cosmos",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--height",
|
||||
type=int,
|
||||
default=768,
|
||||
help="Height of image to generate (must be multiple of 8)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--width",
|
||||
type=int,
|
||||
default=1360,
|
||||
help="Width of image to generate (must be multiple of 8)",
|
||||
)
|
||||
parser.add_argument("--denoising-steps", type=int, default=35, help="Number of denoising steps")
|
||||
parser.add_argument(
|
||||
"--guidance-scale",
|
||||
type=float,
|
||||
default=7.0,
|
||||
help="Value of classifier-free guidance scale (must be greater than 1)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--num-images-per-prompt", type=int, default=1, help="The number of images to generate per prompt."
|
||||
)
|
||||
parser.add_argument(
|
||||
"--max_sequence_length",
|
||||
type=int,
|
||||
default=512,
|
||||
help="Maximum sequence length to use with the prompt.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--t5-ws-percentage",
|
||||
type=int,
|
||||
default=None,
|
||||
help="Set runtime weight streaming budget as the percentage of the size of streamable weights for the T5 model. This argument only takes effect when --ws is set. 0 streams the most weights and 100 or None streams no weights. ",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--transformer-ws-percentage",
|
||||
type=int,
|
||||
default=None,
|
||||
help="Set runtime weight streaming budget as the percentage of the size of streamable weights for the transformer model. This argument only takes effect when --ws is set. 0 streams the most weights and 100 or None streams no weights.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--bf16",
|
||||
action="store_true",
|
||||
default=True,
|
||||
help="Use bfloat16 precision by default.",
|
||||
)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def process_demo_args(args):
|
||||
batch_size = args.batch_size
|
||||
prompt = args.prompt
|
||||
negative_prompt = args.negative_prompt
|
||||
# Process input args
|
||||
if not isinstance(prompt, list):
|
||||
raise ValueError(f"`prompt` must be of type `str` list, but is {type(prompt)}")
|
||||
prompt = prompt * batch_size
|
||||
if not isinstance(negative_prompt, list):
|
||||
raise ValueError(f"`negative_prompt` must be of type `str` list, but is {type(negative_prompt)}")
|
||||
negative_prompt = negative_prompt * batch_size
|
||||
|
||||
kwargs_run_demo = {
|
||||
"prompt": prompt,
|
||||
"negative_prompt": negative_prompt,
|
||||
"height": args.height,
|
||||
"width": args.width,
|
||||
"batch_count": args.batch_count,
|
||||
"num_warmup_runs": args.num_warmup_runs,
|
||||
"use_cuda_graph": args.use_cuda_graph,
|
||||
"num_images_per_prompt": args.num_images_per_prompt,
|
||||
}
|
||||
|
||||
return kwargs_run_demo
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("[I] Initializing Cosmos text2image demo using TensorRT")
|
||||
args = parse_args()
|
||||
|
||||
_, kwargs_load_engine, _ = dd_argparse.process_pipeline_args(args)
|
||||
kwargs_run_demo = process_demo_args(args)
|
||||
|
||||
# Initialize demo
|
||||
demo = pipeline_module.CosmosPipeline.FromArgs(args, pipeline_type=pipeline_module.PIPELINE_TYPE.TXT2IMG)
|
||||
|
||||
# Load TensorRT engines and pytorch modules
|
||||
demo.load_engines(
|
||||
framework_model_dir=args.framework_model_dir,
|
||||
**kwargs_load_engine,
|
||||
)
|
||||
|
||||
if args.onnx_export_only:
|
||||
print("[I] ONNX export completed. Exiting...")
|
||||
demo.teardown()
|
||||
exit(0)
|
||||
|
||||
# In low-vram mode we allocate the required device memory individually before each model is run.
|
||||
if demo.low_vram:
|
||||
demo.device_memory_sizes = demo.get_device_memory_sizes()
|
||||
else:
|
||||
_, shared_device_memory = cudart.cudaMalloc(demo.calculate_max_device_memory())
|
||||
demo.activate_engines(shared_device_memory)
|
||||
|
||||
demo.load_resources(args.height, args.width, args.batch_size, args.seed)
|
||||
|
||||
# Run inference
|
||||
images = demo.run(**kwargs_run_demo)
|
||||
|
||||
demo.teardown()
|
||||
|
||||
# save images
|
||||
demo.save_images(kwargs_run_demo["prompt"], images, check_integrity=True)
|
||||
@@ -0,0 +1,61 @@
|
||||
#
|
||||
# SPDX-FileCopyrightText: Copyright (c) 1993-2025 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.
|
||||
#
|
||||
|
||||
# Configure dependencies before any external imports
|
||||
from demo_diffusion import deps
|
||||
deps.configure("sd")
|
||||
|
||||
import argparse
|
||||
|
||||
from cuda.bindings import runtime as cudart
|
||||
|
||||
from demo_diffusion import dd_argparse
|
||||
from demo_diffusion import pipeline as pipeline_module
|
||||
|
||||
|
||||
def parseArgs():
|
||||
parser = argparse.ArgumentParser(description="Options for Stable Diffusion Txt2Img Demo")
|
||||
parser = dd_argparse.add_arguments(parser)
|
||||
return parser.parse_args()
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("[I] Initializing StableDiffusion txt2img demo using TensorRT")
|
||||
args = parseArgs()
|
||||
|
||||
kwargs_init_pipeline, kwargs_load_engine, args_run_demo = dd_argparse.process_pipeline_args(args)
|
||||
|
||||
# Initialize demo
|
||||
demo = pipeline_module.StableDiffusionPipeline(
|
||||
pipeline_type=pipeline_module.PIPELINE_TYPE.TXT2IMG, **kwargs_init_pipeline
|
||||
)
|
||||
|
||||
# Load TensorRT engines and pytorch modules
|
||||
demo.loadEngines(
|
||||
args.engine_dir,
|
||||
args.framework_model_dir,
|
||||
args.onnx_dir,
|
||||
**kwargs_load_engine)
|
||||
|
||||
# Load resources
|
||||
_, shared_device_memory = cudart.cudaMalloc(demo.calculateMaxDeviceMemory())
|
||||
demo.activateEngines(shared_device_memory)
|
||||
demo.loadResources(args.height, args.width, args.batch_size, args.seed)
|
||||
|
||||
# Run inference
|
||||
demo.run(*args_run_demo)
|
||||
|
||||
demo.teardown()
|
||||
@@ -0,0 +1,171 @@
|
||||
#
|
||||
# SPDX-FileCopyrightText: Copyright (c) 1993-2025 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.
|
||||
#
|
||||
|
||||
# Configure dependencies before any external imports
|
||||
from demo_diffusion import deps
|
||||
deps.configure("flux")
|
||||
|
||||
import argparse
|
||||
|
||||
from cuda.bindings import runtime as cudart
|
||||
|
||||
from demo_diffusion import dd_argparse
|
||||
from demo_diffusion import pipeline as pipeline_module
|
||||
|
||||
|
||||
def parse_args():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Options for Flux Txt2Img Demo", conflict_handler="resolve"
|
||||
)
|
||||
parser = dd_argparse.add_arguments(parser)
|
||||
parser.add_argument(
|
||||
"--version",
|
||||
type=str,
|
||||
default="flux.1-dev",
|
||||
choices=("flux.1-dev", "flux.1-schnell"),
|
||||
help="Version of Flux",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--prompt2",
|
||||
default=None,
|
||||
nargs="*",
|
||||
help="Text prompt(s) to be sent to the T5 tokenizer and text encoder. If not defined, prompt will be used instead",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--height",
|
||||
type=int,
|
||||
default=1024,
|
||||
help="Height of image to generate (must be multiple of 8)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--width",
|
||||
type=int,
|
||||
default=1024,
|
||||
help="Width of image to generate (must be multiple of 8)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--denoising-steps", type=int, default=50, help="Number of denoising steps"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--guidance-scale",
|
||||
type=float,
|
||||
default=3.5,
|
||||
help="Value of classifier-free guidance scale (must be greater than 1)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--max_sequence_length",
|
||||
type=int,
|
||||
help="Maximum sequence length to use with the prompt. Can be up to 512 for the dev and 256 for the schnell variant.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--t5-ws-percentage",
|
||||
type=int,
|
||||
default=None,
|
||||
help="Set runtime weight streaming budget as the percentage of the size of streamable weights for the T5 model. This argument only takes effect when --ws is set. 0 streams the most weights and 100 or None streams no weights. ",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--transformer-ws-percentage",
|
||||
type=int,
|
||||
default=None,
|
||||
help="Set runtime weight streaming budget as the percentage of the size of streamable weights for the transformer model. This argument only takes effect when --ws is set. 0 streams the most weights and 100 or None streams no weights."
|
||||
)
|
||||
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def process_demo_args(args):
|
||||
batch_size = args.batch_size
|
||||
prompt = args.prompt
|
||||
# If prompt2 is not defined, use prompt instead
|
||||
prompt2 = args.prompt2 or prompt
|
||||
|
||||
# Process input args
|
||||
if not isinstance(prompt, list):
|
||||
raise ValueError(f"`prompt` must be of type `str` list, but is {type(prompt)}")
|
||||
prompt = prompt * batch_size
|
||||
|
||||
if not isinstance(prompt2, list):
|
||||
raise ValueError(
|
||||
f"`prompt2` must be of type `str` list, but is {type(prompt2)}"
|
||||
)
|
||||
if len(prompt2) == 1:
|
||||
prompt2 = prompt2 * batch_size
|
||||
|
||||
max_seq_supported_by_model = {
|
||||
"flux.1-schnell": 256,
|
||||
"flux.1-dev": 512,
|
||||
}[args.version]
|
||||
if args.max_sequence_length is not None:
|
||||
if args.max_sequence_length > max_seq_supported_by_model:
|
||||
raise ValueError(
|
||||
f"For {args.version}, `max_sequence_length` cannot be greater than {max_seq_supported_by_model} but is {args.max_sequence_length}"
|
||||
)
|
||||
else:
|
||||
args.max_sequence_length = max_seq_supported_by_model
|
||||
|
||||
kwargs_run_demo = {
|
||||
"prompt": prompt,
|
||||
"prompt2": prompt2,
|
||||
"height": args.height,
|
||||
"width": args.width,
|
||||
"batch_count": args.batch_count,
|
||||
"num_warmup_runs": args.num_warmup_runs,
|
||||
"use_cuda_graph": args.use_cuda_graph,
|
||||
}
|
||||
|
||||
return kwargs_run_demo
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("[I] Initializing Flux txt2img demo using TensorRT")
|
||||
args = parse_args()
|
||||
|
||||
_, kwargs_load_engine, _ = dd_argparse.process_pipeline_args(args)
|
||||
kwargs_run_demo = process_demo_args(args)
|
||||
|
||||
# Initialize demo
|
||||
pipeline_type = pipeline_module.PIPELINE_TYPE.TXT2IMG
|
||||
demo = pipeline_module.FluxPipeline.FromArgs(args, pipeline_type=pipeline_type)
|
||||
|
||||
# Load TensorRT engines and pytorch modules
|
||||
demo.load_engines(
|
||||
framework_model_dir=args.framework_model_dir,
|
||||
**kwargs_load_engine,
|
||||
)
|
||||
|
||||
if args.onnx_export_only:
|
||||
print("[I] ONNX export completed. Exiting...")
|
||||
demo.teardown()
|
||||
exit(0)
|
||||
|
||||
# Since VAE and VAE_encoder require by far the largest device memories, in low-vram mode
|
||||
# we allocate the required device memory individually before each model is run.
|
||||
if demo.low_vram:
|
||||
demo.device_memory_sizes = demo.get_device_memory_sizes()
|
||||
else:
|
||||
_, shared_device_memory = cudart.cudaMalloc(demo.calculate_max_device_memory())
|
||||
demo.activate_engines(shared_device_memory)
|
||||
|
||||
demo.load_resources(args.height, args.width, args.batch_size, args.seed)
|
||||
|
||||
# Run inference
|
||||
images = demo.run(**kwargs_run_demo)
|
||||
|
||||
demo.teardown()
|
||||
|
||||
# save images
|
||||
demo.save_images(kwargs_run_demo["prompt"], images)
|
||||
@@ -0,0 +1,130 @@
|
||||
#
|
||||
# SPDX-FileCopyrightText: Copyright (c) 1993-2025 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.
|
||||
#
|
||||
|
||||
# Configure dependencies before any external imports
|
||||
from demo_diffusion import deps
|
||||
deps.configure("sd")
|
||||
|
||||
import argparse
|
||||
|
||||
from cuda.bindings import runtime as cudart
|
||||
from PIL import Image
|
||||
|
||||
from demo_diffusion import dd_argparse
|
||||
from demo_diffusion import pipeline as pipeline_module
|
||||
from demo_diffusion.utils_sd3.other_impls import preprocess_image_sd3
|
||||
|
||||
|
||||
def parseArgs():
|
||||
# Stable Diffusion 3 configuration
|
||||
parser = argparse.ArgumentParser(description="Options for Stable Diffusion 3 Txt2Img Demo", conflict_handler='resolve')
|
||||
parser = dd_argparse.add_arguments(parser)
|
||||
parser.add_argument('--version', type=str, default="sd3", choices=["sd3"], help="Version of Stable Diffusion")
|
||||
parser.add_argument('--height', type=int, default=1024, help="Height of image to generate (must be multiple of 8)")
|
||||
parser.add_argument('--width', type=int, default=1024, help="Height of image to generate (must be multiple of 8)")
|
||||
parser.add_argument('--shift', type=int, default=1.0, help="Shift parameter for SD3")
|
||||
parser.add_argument('--cfg-scale', type=int, default=5, help="CFG Scale for SD3")
|
||||
parser.add_argument('--denoising-steps', type=int, default=50, help="Number of denoising steps")
|
||||
parser.add_argument('--denoising-percentage', type=float, default=0.6, help="Percentage of denoising steps to run. This parameter is only used if input-image is provided")
|
||||
parser.add_argument('--input-image', type=str, default="", help="Path to the input image")
|
||||
|
||||
return parser.parse_args()
|
||||
|
||||
def process_pipeline_args(args):
|
||||
if args.height % 8 != 0 or args.width % 8 != 0:
|
||||
raise ValueError(f"Image height and width have to be divisible by 8 but specified as: {args.image_height} and {args.width}.")
|
||||
|
||||
max_batch_size = 4
|
||||
if args.batch_size > max_batch_size:
|
||||
raise ValueError(f"Batch size {args.batch_size} is larger than allowed {max_batch_size}.")
|
||||
|
||||
if args.use_cuda_graph and (not args.build_static_batch or args.build_dynamic_shape):
|
||||
raise ValueError(
|
||||
"Using CUDA graph requires static dimensions. Enable `--build-static-batch` and do not specify `--build-dynamic-shape`"
|
||||
)
|
||||
|
||||
input_image = None
|
||||
if args.input_image:
|
||||
input_image = Image.open(args.input_image)
|
||||
|
||||
image_width, image_height = input_image.size
|
||||
if image_height != args.height or image_width != args.width:
|
||||
print(f"[I] Resizing input_image to {args.height}x{args.width}")
|
||||
input_image = input_image.resize((args.width, args.height), Image.LANCZOS)
|
||||
image_height, image_width = args.height, args.width
|
||||
|
||||
input_image = preprocess_image_sd3(input_image)
|
||||
|
||||
kwargs_init_pipeline = {
|
||||
'version': args.version,
|
||||
'max_batch_size': max_batch_size,
|
||||
'output_dir': args.output_dir,
|
||||
'hf_token': args.hf_token,
|
||||
'verbose': args.verbose,
|
||||
'nvtx_profile': args.nvtx_profile,
|
||||
'use_cuda_graph': args.use_cuda_graph,
|
||||
'framework_model_dir': args.framework_model_dir,
|
||||
'torch_inference': args.torch_inference,
|
||||
'shift': args.shift,
|
||||
'cfg_scale': args.cfg_scale,
|
||||
'denoising_steps': args.denoising_steps,
|
||||
'denoising_percentage': args.denoising_percentage,
|
||||
'input_image': input_image
|
||||
}
|
||||
|
||||
kwargs_load_engine = {
|
||||
'onnx_opset': args.onnx_opset,
|
||||
'opt_batch_size': args.batch_size,
|
||||
'opt_image_height': args.height,
|
||||
'opt_image_width': args.width,
|
||||
'static_batch': args.build_static_batch,
|
||||
'static_shape': not args.build_dynamic_shape,
|
||||
'enable_all_tactics': args.build_all_tactics,
|
||||
'timing_cache': args.timing_cache,
|
||||
}
|
||||
|
||||
args_run_demo = (args.prompt, args.negative_prompt, args.height, args.width, args.batch_size, args.batch_count, args.num_warmup_runs, args.use_cuda_graph)
|
||||
|
||||
return kwargs_init_pipeline, kwargs_load_engine, args_run_demo
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("[I] Initializing Stable Diffusion 3 demo using TensorRT")
|
||||
args = parseArgs()
|
||||
|
||||
kwargs_init_pipeline, kwargs_load_engine, args_run_demo = process_pipeline_args(args)
|
||||
|
||||
# Initialize demo
|
||||
demo = pipeline_module.StableDiffusion3Pipeline(
|
||||
pipeline_type=pipeline_module.PIPELINE_TYPE.TXT2IMG, **kwargs_init_pipeline
|
||||
)
|
||||
|
||||
# Load TensorRT engines and pytorch modules
|
||||
demo.loadEngines(
|
||||
args.engine_dir,
|
||||
args.framework_model_dir,
|
||||
args.onnx_dir,
|
||||
**kwargs_load_engine)
|
||||
|
||||
# Load resources
|
||||
_, shared_device_memory = cudart.cudaMalloc(demo.calculateMaxDeviceMemory())
|
||||
demo.activateEngines(shared_device_memory)
|
||||
demo.loadResources(args.height, args.width, args.batch_size, args.seed)
|
||||
|
||||
# Run inference
|
||||
demo.run(*args_run_demo)
|
||||
|
||||
demo.teardown()
|
||||
@@ -0,0 +1,135 @@
|
||||
#
|
||||
# SPDX-FileCopyrightText: Copyright (c) 1993-2025 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.
|
||||
#
|
||||
|
||||
# Configure dependencies before any external imports
|
||||
from demo_diffusion import deps
|
||||
deps.configure("sd")
|
||||
|
||||
import argparse
|
||||
|
||||
from cuda.bindings import runtime as cudart
|
||||
|
||||
from demo_diffusion import dd_argparse
|
||||
from demo_diffusion import pipeline as pipeline_module
|
||||
|
||||
|
||||
def parseArgs():
|
||||
# Stable Diffusion 3.5 configuration
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Options for Stable Diffusion 3.5 Txt2Img Demo", conflict_handler="resolve"
|
||||
)
|
||||
parser = dd_argparse.add_arguments(parser)
|
||||
parser.add_argument(
|
||||
"--version",
|
||||
type=str,
|
||||
default="3.5-medium",
|
||||
choices={"3.5-medium", "3.5-large"},
|
||||
help="Version of Stable Diffusion 3.5",
|
||||
)
|
||||
parser.add_argument("--height", type=int, default=1024, help="Height of image to generate (must be multiple of 8)")
|
||||
parser.add_argument("--width", type=int, default=1024, help="Height of image to generate (must be multiple of 8)")
|
||||
parser.add_argument(
|
||||
"--guidance-scale",
|
||||
type=float,
|
||||
default=7.0,
|
||||
help="Value of classifier-free guidance scale (must be greater than 1)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--max-sequence-length",
|
||||
type=int,
|
||||
default=256,
|
||||
help="Maximum sequence length to use with the prompt.",
|
||||
)
|
||||
parser.add_argument("--denoising-steps", type=int, default=50, help="Number of denoising steps")
|
||||
|
||||
return parser.parse_args()
|
||||
|
||||
def process_demo_args(args):
|
||||
batch_size = args.batch_size
|
||||
prompt = args.prompt
|
||||
negative_prompt = args.negative_prompt
|
||||
# Process prompt
|
||||
if not isinstance(prompt, list):
|
||||
raise ValueError(f"`prompt` must be of type `str` list, but is {type(prompt)}")
|
||||
prompt = prompt * batch_size
|
||||
|
||||
if not isinstance(negative_prompt, list):
|
||||
raise ValueError(f"`--negative-prompt` must be of type `str` list, but is {type(negative_prompt)}")
|
||||
if len(negative_prompt) == 1:
|
||||
negative_prompt = negative_prompt * batch_size
|
||||
|
||||
if args.height % 8 != 0 or args.width % 8 != 0:
|
||||
raise ValueError(
|
||||
f"Image height and width have to be divisible by 8 but specified as: {args.image_height} and {args.width}."
|
||||
)
|
||||
|
||||
max_batch_size = 4
|
||||
if args.batch_size > max_batch_size:
|
||||
raise ValueError(f"Batch size {args.batch_size} is larger than allowed {max_batch_size}.")
|
||||
|
||||
if args.use_cuda_graph and (not args.build_static_batch or args.build_dynamic_shape):
|
||||
raise ValueError(
|
||||
"Using CUDA graph requires static dimensions. Enable `--build-static-batch` and do not specify `--build-dynamic-shape`"
|
||||
)
|
||||
|
||||
kwargs_run_demo = {
|
||||
"prompt": prompt,
|
||||
"negative_prompt": negative_prompt,
|
||||
"height": args.height,
|
||||
"width": args.width,
|
||||
"batch_count": args.batch_count,
|
||||
"num_warmup_runs": args.num_warmup_runs,
|
||||
"use_cuda_graph": args.use_cuda_graph,
|
||||
}
|
||||
|
||||
return kwargs_run_demo
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("[I] Initializing Stable Diffusion 3.5 demo using TensorRT")
|
||||
args = parseArgs()
|
||||
|
||||
_, kwargs_load_engine, _ = dd_argparse.process_pipeline_args(args)
|
||||
kwargs_run_demo = process_demo_args(args)
|
||||
|
||||
# Initialize demo
|
||||
demo = pipeline_module.StableDiffusion35Pipeline.FromArgs(args, pipeline_type=pipeline_module.PIPELINE_TYPE.TXT2IMG)
|
||||
|
||||
# Load TensorRT engines and pytorch modules
|
||||
demo.load_engines(
|
||||
framework_model_dir=args.framework_model_dir,
|
||||
**kwargs_load_engine,
|
||||
)
|
||||
|
||||
if demo.low_vram:
|
||||
demo.device_memory_sizes = demo.get_device_memory_sizes()
|
||||
else:
|
||||
_, shared_device_memory = cudart.cudaMalloc(demo.calculate_max_device_memory())
|
||||
demo.activate_engines(shared_device_memory)
|
||||
|
||||
# Load resources
|
||||
demo.load_resources(
|
||||
image_height=args.height,
|
||||
image_width=args.width,
|
||||
batch_size=args.batch_size,
|
||||
seed=args.seed,
|
||||
)
|
||||
|
||||
# Run inference
|
||||
demo.run(**kwargs_run_demo)
|
||||
|
||||
demo.teardown()
|
||||
@@ -0,0 +1,160 @@
|
||||
#
|
||||
# SPDX-FileCopyrightText: Copyright (c) 1993-2025 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.
|
||||
#
|
||||
|
||||
# Configure dependencies before any external imports
|
||||
from demo_diffusion import deps
|
||||
deps.configure("sd")
|
||||
|
||||
import argparse
|
||||
|
||||
from cuda.bindings import runtime as cudart
|
||||
|
||||
from demo_diffusion import dd_argparse
|
||||
from demo_diffusion import pipeline as pipeline_module
|
||||
|
||||
|
||||
def parseArgs():
|
||||
parser = argparse.ArgumentParser(description="Options for Stable Diffusion XL Txt2Img Demo", conflict_handler='resolve')
|
||||
parser = dd_argparse.add_arguments(parser)
|
||||
parser.add_argument('--version', type=str, default="xl-1.0", choices=["xl-1.0", "xl-turbo"], help="Version of Stable Diffusion XL")
|
||||
parser.add_argument('--height', type=int, default=1024, help="Height of image to generate (must be multiple of 8)")
|
||||
parser.add_argument('--width', type=int, default=1024, help="Height of image to generate (must be multiple of 8)")
|
||||
parser.add_argument('--num-warmup-runs', type=int, default=1, help="Number of warmup runs before benchmarking performance")
|
||||
|
||||
parser.add_argument('--guidance-scale', type=float, default=5.0, help="Value of classifier-free guidance scale (must be greater than 1)")
|
||||
|
||||
parser.add_argument('--enable-refiner', action='store_true', help="Enable SDXL-Refiner model")
|
||||
parser.add_argument('--image-strength', type=float, default=0.3, help="Strength of transformation applied to input_image (must be between 0 and 1)")
|
||||
parser.add_argument('--onnx-refiner-dir', default='onnx_xl_refiner', help="Directory for SDXL-Refiner ONNX models")
|
||||
parser.add_argument('--engine-refiner-dir', default='engine_xl_refiner', help="Directory for SDXL-Refiner TensorRT engines")
|
||||
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
class StableDiffusionXLPipeline(pipeline_module.StableDiffusionPipeline):
|
||||
def __init__(self, vae_scaling_factor=0.13025, enable_refiner=False, **kwargs):
|
||||
self.enable_refiner = enable_refiner
|
||||
self.nvtx_profile = kwargs['nvtx_profile']
|
||||
self.base = pipeline_module.StableDiffusionPipeline(
|
||||
pipeline_type=pipeline_module.PIPELINE_TYPE.XL_BASE,
|
||||
vae_scaling_factor=vae_scaling_factor,
|
||||
return_latents=self.enable_refiner,
|
||||
**kwargs,
|
||||
)
|
||||
if self.enable_refiner:
|
||||
self.refiner = pipeline_module.StableDiffusionPipeline(
|
||||
pipeline_type=pipeline_module.PIPELINE_TYPE.XL_REFINER,
|
||||
vae_scaling_factor=vae_scaling_factor,
|
||||
return_latents=False,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
def loadEngines(self, framework_model_dir, onnx_dir, engine_dir, onnx_refiner_dir='onnx_xl_refiner', engine_refiner_dir='engine_xl_refiner', **kwargs):
|
||||
self.base.loadEngines(engine_dir, framework_model_dir, onnx_dir, **kwargs)
|
||||
if self.enable_refiner:
|
||||
self.refiner.loadEngines(engine_refiner_dir, framework_model_dir, onnx_refiner_dir, **kwargs)
|
||||
|
||||
def activateEngines(self, shared_device_memory=None):
|
||||
self.base.activateEngines(shared_device_memory)
|
||||
if self.enable_refiner:
|
||||
self.refiner.activateEngines(shared_device_memory)
|
||||
|
||||
def loadResources(self, image_height, image_width, batch_size, seed):
|
||||
self.base.loadResources(image_height, image_width, batch_size, seed)
|
||||
if self.enable_refiner:
|
||||
# Use a different seed for refiner - we arbitrarily use base seed+1, if specified.
|
||||
self.refiner.loadResources(image_height, image_width, batch_size, ((seed+1) if seed is not None else None))
|
||||
|
||||
def get_max_device_memory(self):
|
||||
max_device_memory = self.base.calculateMaxDeviceMemory()
|
||||
if self.enable_refiner:
|
||||
max_device_memory = max(max_device_memory, self.refiner.calculateMaxDeviceMemory())
|
||||
return max_device_memory
|
||||
|
||||
def run(self, prompt, negative_prompt, height, width, batch_size, batch_count, num_warmup_runs, use_cuda_graph, **kwargs_infer_refiner):
|
||||
# Process prompt
|
||||
if not isinstance(prompt, list):
|
||||
raise ValueError(f"`prompt` must be of type `str` list, but is {type(prompt)}")
|
||||
prompt = prompt * batch_size
|
||||
|
||||
if not isinstance(negative_prompt, list):
|
||||
raise ValueError(f"`--negative-prompt` must be of type `str` list, but is {type(negative_prompt)}")
|
||||
if len(negative_prompt) == 1:
|
||||
negative_prompt = negative_prompt * batch_size
|
||||
|
||||
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 ..")
|
||||
for _ in range(num_warmup_runs):
|
||||
images, _ = self.base.infer(prompt, negative_prompt, height, width, warmup=True)
|
||||
if args.enable_refiner:
|
||||
images, _ = self.refiner.infer(prompt, negative_prompt, height, width, input_image=images, warmup=True, **kwargs_infer_refiner)
|
||||
|
||||
ret = []
|
||||
for _ in range(batch_count):
|
||||
print("[I] Running StableDiffusionXL pipeline")
|
||||
if self.nvtx_profile:
|
||||
cudart.cudaProfilerStart()
|
||||
latents, time_base = self.base.infer(prompt, negative_prompt, height, width, warmup=False)
|
||||
if self.enable_refiner:
|
||||
images, time_refiner = self.refiner.infer(prompt, negative_prompt, height, width, input_image=latents, warmup=False, **kwargs_infer_refiner)
|
||||
ret.append(images)
|
||||
else:
|
||||
ret.append(latents)
|
||||
|
||||
if self.nvtx_profile:
|
||||
cudart.cudaProfilerStop()
|
||||
if self.enable_refiner:
|
||||
print('|-----------------|--------------|')
|
||||
print('| {:^15} | {:>9.2f} ms |'.format('e2e', time_base + time_refiner))
|
||||
print('|-----------------|--------------|')
|
||||
return ret
|
||||
|
||||
def teardown(self):
|
||||
self.base.teardown()
|
||||
if self.enable_refiner:
|
||||
self.refiner.teardown()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("[I] Initializing TensorRT accelerated StableDiffusionXL txt2img pipeline")
|
||||
args = parseArgs()
|
||||
|
||||
kwargs_init_pipeline, kwargs_load_engine, args_run_demo = dd_argparse.process_pipeline_args(args)
|
||||
|
||||
# Initialize demo
|
||||
demo = StableDiffusionXLPipeline(vae_scaling_factor=0.13025, enable_refiner=args.enable_refiner, **kwargs_init_pipeline)
|
||||
|
||||
# Load TensorRT engines and pytorch modules
|
||||
kwargs_load_refiner = {'onnx_refiner_dir': args.onnx_refiner_dir, 'engine_refiner_dir': args.engine_refiner_dir} if args.enable_refiner else {}
|
||||
demo.loadEngines(
|
||||
args.framework_model_dir,
|
||||
args.onnx_dir,
|
||||
args.engine_dir,
|
||||
**kwargs_load_refiner,
|
||||
**kwargs_load_engine)
|
||||
|
||||
# Load resources
|
||||
_, shared_device_memory = cudart.cudaMalloc(demo.get_max_device_memory())
|
||||
demo.activateEngines(shared_device_memory)
|
||||
demo.loadResources(args.height, args.width, args.batch_size, args.seed)
|
||||
|
||||
# Run inference
|
||||
kwargs_infer_refiner = {'image_strength': args.image_strength} if args.enable_refiner else {}
|
||||
demo.run(*args_run_demo, **kwargs_infer_refiner)
|
||||
|
||||
demo.teardown()
|
||||
@@ -0,0 +1,128 @@
|
||||
#
|
||||
# SPDX-FileCopyrightText: Copyright (c) 1993-2025 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.
|
||||
#
|
||||
|
||||
# Configure dependencies before any external imports
|
||||
from demo_diffusion import deps
|
||||
deps.configure("cosmos")
|
||||
|
||||
import argparse
|
||||
|
||||
from cuda.bindings import runtime as cudart
|
||||
|
||||
from demo_diffusion import dd_argparse
|
||||
from demo_diffusion import pipeline as pipeline_module
|
||||
|
||||
|
||||
def parseArgs():
|
||||
parser = argparse.ArgumentParser(description="Options for Wan 2.2 Txt2Vid Demo", conflict_handler='resolve')
|
||||
parser = dd_argparse.add_arguments(parser)
|
||||
parser.add_argument('--version', type=str, default="wan2.2-t2v-a14b", help="Version of Wan")
|
||||
parser.add_argument('--guidance-scale', type=float, default=4.0, help="Guidance scale for high-noise stage (Wan default: 4.0)")
|
||||
parser.add_argument('--guidance-scale-2', type=float, default=3.0, help="Guidance scale for low-noise stage (Wan default: 3.0)")
|
||||
parser.add_argument('--boundary-ratio', type=float, default=0.875, help="Boundary ratio for two-stage denoising (default: 0.875)")
|
||||
parser.add_argument('--denoising-steps', type=int, default=40, help="Number of denoising steps (Wan default: 40)")
|
||||
parser.add_argument('--num-warmup-runs', type=int, default=1, help="Number of warmup runs before benchmarking")
|
||||
parser.add_argument(
|
||||
'--negative-prompt',
|
||||
nargs='*',
|
||||
default= (
|
||||
"vivid colors, overexposed, static, blurry details, subtitles, style, "
|
||||
"work of art, painting, picture, still, overall grayish, worst quality, "
|
||||
"low quality, JPEG artifacts, ugly, deformed, extra fingers, poorly drawn hands, "
|
||||
"poorly drawn face, deformed, disfigured, deformed limbs, fused fingers, "
|
||||
"static image, cluttered background, three legs, many people in the background, "
|
||||
"walking backwards"
|
||||
),
|
||||
help="Negative prompt (Wan team default, English translation)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--onnx-opset",
|
||||
type=int,
|
||||
default=23,
|
||||
choices=range(7, 24),
|
||||
help="Select ONNX opset version to target for exported models",
|
||||
)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def process_demo_args(args):
|
||||
args.height = 720
|
||||
args.width = 1280
|
||||
args.num_frames = 81
|
||||
args.max_sequence_length = 512
|
||||
|
||||
# require static batch = 1
|
||||
if args.batch_size > 1:
|
||||
raise ValueError(f"Batch size {args.batch_size} is larger than allowed (max=1 for Wan).")
|
||||
args.batch_size = 1
|
||||
args.build_static_batch = True
|
||||
args.build_dynamic_shape = False
|
||||
|
||||
print(f"[I] Building Wan 2.2 T2V with fixed resolution: {args.height}×{args.width}, {args.num_frames} frames")
|
||||
|
||||
negative_prompt = args.negative_prompt
|
||||
if isinstance(negative_prompt, list):
|
||||
negative_prompt = ' '.join(negative_prompt) if negative_prompt else ""
|
||||
|
||||
kwargs_run_demo = {
|
||||
'prompt': args.prompt,
|
||||
'height': args.height,
|
||||
'width': args.width,
|
||||
'num_frames': args.num_frames,
|
||||
'batch_size': args.batch_size,
|
||||
'batch_count': args.batch_count,
|
||||
'num_warmup_runs': args.num_warmup_runs,
|
||||
'use_cuda_graph': args.use_cuda_graph,
|
||||
'negative_prompt': negative_prompt,
|
||||
'num_inference_steps': args.denoising_steps,
|
||||
}
|
||||
|
||||
return kwargs_run_demo
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("[I] Initializing Wan 2.2 txt2vid demo using TensorRT")
|
||||
args = parseArgs()
|
||||
|
||||
kwargs_run_demo = process_demo_args(args)
|
||||
|
||||
_, kwargs_load_engine, _ = dd_argparse.process_pipeline_args(args)
|
||||
|
||||
# Initialize demo
|
||||
demo = pipeline_module.WanPipeline.FromArgs(args, pipeline_type=pipeline_module.PIPELINE_TYPE.TXT2VID)
|
||||
|
||||
# Load TensorRT engines and pytorch modules
|
||||
demo.load_engines(
|
||||
framework_model_dir=args.framework_model_dir,
|
||||
**kwargs_load_engine,
|
||||
)
|
||||
|
||||
# In low-vram mode we allocate the required device memory individually before each model is run
|
||||
if args.low_vram:
|
||||
demo.device_memory_sizes = demo.get_device_memory_sizes()
|
||||
else:
|
||||
_, shared_device_memory = cudart.cudaMalloc(demo.calculate_max_device_memory())
|
||||
demo.activate_engines(shared_device_memory)
|
||||
|
||||
# Load resources
|
||||
demo.load_resources(args.height, args.width, args.batch_size, args.seed)
|
||||
|
||||
# Run inference
|
||||
demo.run(**kwargs_run_demo)
|
||||
|
||||
demo.teardown()
|
||||
|
||||
@@ -0,0 +1,174 @@
|
||||
#
|
||||
# SPDX-FileCopyrightText: Copyright (c) 1993-2025 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.
|
||||
#
|
||||
|
||||
# Configure dependencies before any external imports
|
||||
from demo_diffusion import deps
|
||||
deps.configure("cosmos")
|
||||
|
||||
import argparse
|
||||
|
||||
from cuda.bindings import runtime as cudart
|
||||
from diffusers.utils import load_image, load_video
|
||||
|
||||
from demo_diffusion import dd_argparse
|
||||
from demo_diffusion import pipeline as pipeline_module
|
||||
|
||||
|
||||
def parse_args():
|
||||
parser = argparse.ArgumentParser(description="Options for Cosmos video2world Demo", conflict_handler="resolve")
|
||||
parser = dd_argparse.add_arguments(parser)
|
||||
parser.add_argument(
|
||||
"--version",
|
||||
type=str,
|
||||
default="cosmos-predict2-2b-video2world",
|
||||
choices=("cosmos-predict2-2b-video2world", "cosmos-predict2-14b-video2world"),
|
||||
help="Version of Cosmos",
|
||||
)
|
||||
parser.add_argument('--input-image', type=str, default=None, help="Path to the input image")
|
||||
parser.add_argument('--input-video', type=str, default=None, help="Path to the input video")
|
||||
parser.add_argument(
|
||||
"--height",
|
||||
type=int,
|
||||
default=704,
|
||||
help="Height of image to generate (must be multiple of 8)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--width",
|
||||
type=int,
|
||||
default=1280,
|
||||
help="Width of image to generate (must be multiple of 8)",
|
||||
)
|
||||
parser.add_argument("--denoising-steps", type=int, default=35, help="Number of denoising steps")
|
||||
parser.add_argument(
|
||||
"--guidance-scale",
|
||||
type=float,
|
||||
default=7.0,
|
||||
help="Value of classifier-free guidance scale (must be greater than 1)",
|
||||
)
|
||||
parser.add_argument("--num-frames", type=int, default=93, help="The number of frames in the generated video.")
|
||||
parser.add_argument("--fps", type=int, default=16, help="The frames per second of the generated video.")
|
||||
parser.add_argument("--num-videos-per-prompt", type=int, default=1, help="The number of videos to generate per prompt.")
|
||||
parser.add_argument(
|
||||
"--max_sequence_length",
|
||||
type=int,
|
||||
default=512,
|
||||
help="Maximum sequence length to use with the prompt.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--t5-ws-percentage",
|
||||
type=int,
|
||||
default=None,
|
||||
help="Set runtime weight streaming budget as the percentage of the size of streamable weights for the T5 model. This argument only takes effect when --ws is set. 0 streams the most weights and 100 or None streams no weights. ",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--transformer-ws-percentage",
|
||||
type=int,
|
||||
default=None,
|
||||
help="Set runtime weight streaming budget as the percentage of the size of streamable weights for the transformer model. This argument only takes effect when --ws is set. 0 streams the most weights and 100 or None streams no weights.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--bf16",
|
||||
action="store_true",
|
||||
default=True,
|
||||
help="Use bfloat16 precision by default.",
|
||||
)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def process_demo_args(args):
|
||||
batch_size = args.batch_size
|
||||
prompt = args.prompt
|
||||
negative_prompt = args.negative_prompt
|
||||
# Process input args
|
||||
if not isinstance(prompt, list):
|
||||
raise ValueError(f"`prompt` must be of type `str` list, but is {type(prompt)}")
|
||||
prompt = prompt * batch_size
|
||||
if not isinstance(negative_prompt, list):
|
||||
raise ValueError(f"`negative_prompt` must be of type `str` list, but is {type(negative_prompt)}")
|
||||
negative_prompt = negative_prompt * batch_size
|
||||
|
||||
# process input image and input video
|
||||
if args.input_image and args.input_video:
|
||||
raise ValueError("Only one of --input-image or --input-video can be provided")
|
||||
if args.input_image:
|
||||
args.input_image = load_image(args.input_image)
|
||||
elif args.input_video:
|
||||
args.input_video = load_video(args.input_video)
|
||||
else:
|
||||
# load default image
|
||||
args.input_image = load_image("https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/diffusers/yellow-scrubber.png")
|
||||
|
||||
kwargs_run_demo = {
|
||||
"prompt": prompt,
|
||||
"negative_prompt": negative_prompt,
|
||||
"height": args.height,
|
||||
"width": args.width,
|
||||
"batch_count": args.batch_count,
|
||||
"num_warmup_runs": args.num_warmup_runs,
|
||||
"use_cuda_graph": args.use_cuda_graph,
|
||||
"num_frames": args.num_frames,
|
||||
"fps": args.fps,
|
||||
"input_image": args.input_image,
|
||||
"input_video": args.input_video,
|
||||
"num_videos_per_prompt": args.num_videos_per_prompt,
|
||||
}
|
||||
|
||||
return kwargs_run_demo
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("[I] Initializing Cosmos video2world demo using TensorRT")
|
||||
args = parse_args()
|
||||
|
||||
# Enforce torch-inference is enabled
|
||||
if not args.torch_inference:
|
||||
print("[W] The video2world demo only supports the PyTorch backend. Enabling torch-inference with 'eager' mode.")
|
||||
args.torch_inference = "eager"
|
||||
|
||||
_, kwargs_load_engine, _ = dd_argparse.process_pipeline_args(args)
|
||||
kwargs_run_demo = process_demo_args(args)
|
||||
|
||||
# Initialize demo
|
||||
demo = pipeline_module.CosmosPipeline.FromArgs(args, pipeline_type=pipeline_module.PIPELINE_TYPE.VIDEO2WORLD)
|
||||
|
||||
# Load TensorRT engines and pytorch modules
|
||||
demo.load_engines(
|
||||
framework_model_dir=args.framework_model_dir,
|
||||
**kwargs_load_engine,
|
||||
)
|
||||
|
||||
if args.onnx_export_only:
|
||||
print("[I] ONNX export completed. Exiting...")
|
||||
demo.teardown()
|
||||
exit(0)
|
||||
|
||||
# In low-vram mode we allocate the required device memory individually before each model is run.
|
||||
if demo.low_vram:
|
||||
demo.device_memory_sizes = demo.get_device_memory_sizes()
|
||||
else:
|
||||
_, shared_device_memory = cudart.cudaMalloc(demo.calculate_max_device_memory())
|
||||
demo.activate_engines(shared_device_memory)
|
||||
|
||||
demo.load_resources(args.height, args.width, args.batch_size, args.seed)
|
||||
|
||||
# Run inference
|
||||
videos = demo.run(**kwargs_run_demo)
|
||||
|
||||
demo.teardown()
|
||||
|
||||
# save video
|
||||
demo.save_video(kwargs_run_demo["prompt"], videos, check_integrity=True)
|
||||
@@ -0,0 +1,33 @@
|
||||
<!--- SPDX-License-Identifier: Apache-2.0 -->
|
||||
|
||||
# Supported Diffusion Models
|
||||
|
||||
This demo supports Diffusion models that are popular in the Generative AI community. The table below lists the various configurations we support for each pipeline.
|
||||
|
||||
## Pipeline Support Matrix
|
||||
|
||||
| Pipeline | Version | Task | Supported Precisions | Additional features | Hub | Restrictions |
|
||||
|------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------|-----------------------------------------------|--------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| Stable Diffusion | 1.4 | <ul><li>Text-to-image</li><li>Image-to-image</li></ul> | FP16, FP8, INT8 * | N/A | [CompVis/stable-diffusion-v1-4](https://huggingface.co/CompVis/stable-diffusion-v1-4) | |
|
||||
| Stable Diffusion | dreamshaper-7 | <ul><li>Text-to-image</li><li>Image-to-image</li></ul> | FP16 | N/A | [Lykon/dreamshaper-7](https://huggingface.co/Lykon/dreamshaper-7) |
|
||||
| Stable Diffusion | [XL 1.0-base](../README.md#generate-an-image-with-stable-diffusion-xl-guided-by-a-single-text-prompt) | <ul><li>Text-to-image</li><li>Image-to-image</li></ul> | FP16, FP8, INT8 * | LoRA (FP16, BF16, FP8) | [stabilityai/stable-diffusion-xl-base-1.0](https://huggingface.co/stabilityai/stable-diffusion-xl-base-1.0) |
|
||||
| Stable Diffusion | [XL 1.0-refiner](../README.md#generate-an-image-with-stable-diffusion-xl-guided-by-a-single-text-prompt) | <ul><li>Text-to-image</li><li>Image-to-image</li></ul> | FP16 | N/A | [stabilityai/stable-diffusion-xl-refiner-1.0](https://huggingface.co/stabilityai/stable-diffusion-xl-refiner-1.0) |
|
||||
| Stable Diffusion | [XL-Turbo](../README.md#faster-text-to-image-using-sdxl-turbo) | <ul><li>Text-to-image</li><li>Image-to-image</li></ul> | FP16 | N/A | [stabilityai/sdxl-turbo](https://huggingface.co/stabilityai/sdxl-turbo) |
|
||||
| Stable Diffusion | [3](../README.md#generate-an-image-guided-by-a-text-prompt-using-stable-diffusion-3) | <ul><li>Text-to-image</li></ul> | FP16 | N/A | [stabilityai/stable-diffusion-3-medium](https://huggingface.co/stabilityai/stable-diffusion-3-medium) |
|
||||
| Stable Diffusion | [3.5-medium](../README.md#generate-an-image-guided-by-a-text-prompt-using-stable-diffusion-3) | <ul><li>Text-to-image</li></ul> | FP16, BF16 | N/A | [stabilityai/stable-diffusion-3-medium](https://huggingface.co/stabilityai/stable-diffusion-3-medium) |
|
||||
| Stable Diffusion | [3.5-large](../README.md#generate-an-image-guided-by-a-text-prompt-using-stable-diffusion-3) | <ul><li>Text-to-image</li></ul> | FP16, BF16, FP8 | N/A | [stabilityai/stable-diffusion-3-large](https://huggingface.co/stabilityai/stable-diffusion-3-large) |
|
||||
| ControlNet | [1.4](../README.md#generate-an-image-with-controlnet-guided-by-images-and-text-prompts) | <ul><li>Image-to-image</li></ul> | FP16 | N/A | <ul><li>[lllyasviel/sd-controlnet-canny](https://huggingface.co/lllyasviel/sd-controlnet-canny)</li><li>[lllyasviel/sd-controlnet-depth](https://huggingface.co/lllyasviel/sd-controlnet-depth)</li><li>[lllyasviel/sd-controlnet-hed](https://huggingface.co/lllyasviel/sd-controlnet-hed)</li><li>[lllyasviel/sd-controlnet-mlsd](https://huggingface.co/lllyasviel/sd-controlnet-mlsd)</li><li>[lllyasviel/sd-controlnet-normal](https://huggingface.co/lllyasviel/sd-controlnet-normal)</li><li>[lllyasviel/sd-controlnet_openpose](https://huggingface.co/lllyasviel/sd-controlnet-openpose)</li><li>[lllyasviel/sd-controlnet_scribble](https://huggingface.co/lllyasviel/sd-controlnet-scribble)</li><li>[lllyasviel/sd-controlnet_seg](https://huggingface.co/lllyasviel/sd-controlnet-seg)</li></ul> |
|
||||
| ControlNet | [XL 1.0-base](../README.md#generate-an-image-with-stable-diffusion-xl-guided-by-a-single-text-prompt) | <ul><li>Image-to-image</li></ul> | FP16, FP8 | N/A | [stabilityai/controlnet-canny-sdxl-1.0](https://huggingface.co/diffusers/controlnet-canny-sdxl-1.0) |
|
||||
| ControlNet | [3.5-large](../README.md#generate-an-image-with-stable-diffusion-v35-large-with-controlnet-guided-by-an-image-and-a-text-prompt) | <ul><li>Image-to-image</li></ul> | FP16, BF16, FP8 (canny and depth only) | N/A | <ul><li>[stabilityai/stable-diffusion-3.5-large-controlnet-canny](https://huggingface.co/stabilityai/stable-diffusion-3.5-large-controlnet-canny)</li><li>[stabilityai/stable-diffusion-3.5-large-controlnet-depth](https://huggingface.co/stabilityai/stable-diffusion-3.5-large-controlnet-depth)</li><li>[stabilityai/stable-diffusion-3.5-large-controlnet-blur](https://huggingface.co/stabilityai/stable-diffusion-3.5-large-controlnet-blur)</li></ul> |
|
||||
| Stable Video Diffusion | [XT-1.1](../README.md#generate-a-video-guided-by-an-initial-image-using-stable-video-diffusion) | <ul><li>Text-to-video</li></ul> | FP16, FP8 | N/A | [stabilityai/stable-video-diffusion-img2vid-xt-1-1](https://huggingface.co/stabilityai/stable-video-diffusion-img2vid-xt-1-1) |
|
||||
| Stable Cascade | [N/A](../README.md#generate-an-image-guided-by-a-text-prompt-using-stable-cascade) | <ul><li>Text-to-image</li></ul> | BF16 | N/A | <ul><li>[stabilityai/stable-cascade-prior](https://huggingface.co/stabilityai/stable-cascade-prior)</li><li>[stabilityai/stable-cascade](https://huggingface.co/stabilityai/stable-cascade)</li></ul> |
|
||||
| Flux | [1-Dev](../README.md#generate-an-image-guided-by-a-text-prompt-using-flux) | <ul><li>Text-to-image</li><li>Image-to-image</li></ul> | FP16, BF16, FP8, FP4 * | LoRA (FP16, BF16, FP8) | [black-forest-labs/FLUX.1-dev](https://huggingface.co/black-forest-labs/FLUX.1-dev) |
|
||||
| Flux | [1-Schnell](../README.md#generate-an-image-guided-by-a-text-prompt-using-flux) | <ul><li>Text-to-image</li><li>Image-to-image</li></ul> | FP16, BF16, FP8, FP4 * | LoRA (FP16, BF16, FP8) | [black-forest-labs/FLUX.1-schnell](https://huggingface.co/black-forest-labs/FLUX.1-schnell) |
|
||||
| Flux | [1-Canny-Dev](../README.md#generate-an-image-guided-by-a-text-prompt-and-a-control-image-using-flux-controlnet) | <ul><li>Image-to-image</li></ul> | FP16, BF16, FP8, FP4 | N/A | [black-forest-labs/FLUX.1-Canny-dev](https://huggingface.co/black-forest-labs/FLUX.1-Canny-dev) |
|
||||
| Flux | [1-Depth-Dev](../README.md#generate-an-image-guided-by-a-text-prompt-and-a-control-image-using-flux-controlnet) | <ul><li>Image-to-image</li></ul> | FP16, BF16, FP8, FP4 | N/A | [black-forest-labs/FLUX.1-Depth-dev](https://huggingface.co/black-forest-labs/FLUX.1-Depth-dev) |
|
||||
| Flux | [1-Kontext-Dev](../README.md#5-edit-an-image-using-flux-kontext) | <ul><li>Image-to-image</li></ul> | BF16, FP8, FP4 | N/A | [black-forest-labs/FLUX.1-Kontext-dev](https://huggingface.co/black-forest-labs/FLUX.1-Kontext-dev) |
|
||||
| Cosmos | [cosmos-predict2-2b-text2image](../README.md#1-generate-an-image-from-a-text-prompt-1), cosmos-predict2-14b-text2image | <ul><li>Text-to-image</li></ul> | BF16 | N/A | <ul><li>[nvidia/Cosmos-Predict2-2B-Text2Image](https://huggingface.co/nvidia/Cosmos-Predict2-2B-Text2Image)</li><li>[nvidia/Cosmos-Predict2-14B-Text2Image](https://huggingface.co/nvidia/Cosmos-Predict2-14B-Text2Image) |
|
||||
| Cosmos | [cosmos-predict2-2b-video2world](../README.md#2-generate-a-video-guided-by-an-initial-video-conditioning-and-a-text-prompt), cosmos-predict2-14b-video2world | <ul><li>Video-to-World</li></ul> | BF16 | N/A | <ul><li>[nvidia/Cosmos-Predict2-2B-Video2World](https://huggingface.co/nvidia/Cosmos-Predict2-2B-Video2World)</li><li>[nvidia/Cosmos-Predict2-14B-Video2World](https://huggingface.co/nvidia/Cosmos-Predict2-14B-Video2World) |
|
||||
| Wan | [2.2-T2V-A14B](../README.md#generate-a-video-from-a-text-prompt-using-wan) | <ul><li>Text-to-video</li></ul> | BF16 | N/A | [wan-ai/wan2.2-t2v-a14b](https://huggingface.co/Wan-AI/Wan2.2-T2V-A14B-Diffusers) |
|
||||
|
||||
*Note: Only the text2image pipelines support FP4/FP8/INT8 quantization. The image2image pipelines don't support quantization.
|
||||
Executable
+24
@@ -0,0 +1,24 @@
|
||||
apex==0.9.10dev
|
||||
accelerate==1.2.1
|
||||
colored
|
||||
controlnet_aux==0.0.6
|
||||
cuda-python
|
||||
diffusers==0.35.2
|
||||
git+https://github.com/black-forest-labs/flux.git
|
||||
ftfy
|
||||
matplotlib
|
||||
nvtx
|
||||
onnx==1.18.0
|
||||
onnxscript==0.5.4
|
||||
opencv-python-headless==4.8.0.74
|
||||
scipy
|
||||
transformers==4.52.4
|
||||
--extra-index-url https://pypi.nvidia.com
|
||||
nvidia-modelopt[torch,onnx]==0.31.0
|
||||
onnx-graphsurgeon==0.5.2
|
||||
peft==0.17.0
|
||||
polygraphy==0.49.22
|
||||
sentencepiece
|
||||
numpy==1.26.4
|
||||
imageio-ffmpeg
|
||||
|
||||
@@ -0,0 +1,578 @@
|
||||
#!/usr/bin/env python3
|
||||
#
|
||||
# SPDX-FileCopyrightText: Copyright (c) 1993-2025 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.
|
||||
#
|
||||
|
||||
"""
|
||||
Setup script for TensorRT Diffusion dependencies.
|
||||
|
||||
Usage:
|
||||
python setup.py [GROUP] [OPTIONS]
|
||||
|
||||
Groups:
|
||||
sd = SD 1.4, SDXL, SD3, SD3.5, SVD, Stable Cascade (Stability AI)
|
||||
flux = Flux models (Black Forest Labs)
|
||||
cosmos = Cosmos models (NVIDIA), Wan2.2 T2V
|
||||
all = Install all groups (default)
|
||||
|
||||
Options:
|
||||
--skip-tensorrt Skip TensorRT upgrade/installation
|
||||
--force Force reinstallation even if already installed
|
||||
--deps-root DIR Root directory for dependencies
|
||||
-q, --quiet Suppress informational output (errors are always shown)
|
||||
|
||||
Examples:
|
||||
python setup.py # Install all
|
||||
python setup.py sd # Install SD family only
|
||||
python setup.py flux # Install Flux only
|
||||
python setup.py cosmos # Install Cosmos only
|
||||
python setup.py --skip-tensorrt # Install all, skip TensorRT upgrade
|
||||
python setup.py flux --skip-tensorrt # Install Flux only, skip TensorRT upgrade
|
||||
python setup.py all --quiet # Install all, minimal output
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
# Global quiet flag (set by parse_args)
|
||||
_quiet = False
|
||||
|
||||
|
||||
def log(msg: str = ""):
|
||||
"""Print a message unless in quiet mode."""
|
||||
if not _quiet:
|
||||
print(msg)
|
||||
|
||||
# Group descriptions
|
||||
GROUP_DESCRIPTIONS = {
|
||||
"sd": "SD family (SD 1.4, SDXL, SD3, SD3.5, SVD, Stable Cascade)",
|
||||
"flux": "Flux family (Black Forest Labs)",
|
||||
"cosmos": "Cosmos family (NVIDIA), Wan2.2 T2V",
|
||||
}
|
||||
|
||||
# Valid dependency groups
|
||||
VALID_GROUPS = set(GROUP_DESCRIPTIONS.keys())
|
||||
|
||||
# Default installation root
|
||||
# Can be overridden by:
|
||||
# 1. Environment variable: TENSORRT_DIFFUSION_DEPS_ROOT
|
||||
# 2. Command-line argument: --deps-root
|
||||
DEFAULT_DEPS_ROOT = os.environ.get("TENSORRT_DIFFUSION_DEPS_ROOT", "/workspace/deps")
|
||||
|
||||
# Marker file to indicate successful installation
|
||||
# This file is created after all packages are successfully installed
|
||||
# Prevents treating incomplete installations as complete
|
||||
INSTALL_COMPLETE_MARKER = ".install_complete"
|
||||
|
||||
|
||||
def _normalize_package_name(name: str) -> str:
|
||||
"""Normalize a package name per PEP 503 (e.g. 'Nvidia_ModelOpt' -> 'nvidia-modelopt')."""
|
||||
return re.sub(r"[-_.]+", "-", name).lower()
|
||||
|
||||
|
||||
# Regex to extract the package name from a PEP 508 requirement string.
|
||||
# Matches the leading identifier before any extras, version specifier, or URL marker.
|
||||
# "nvidia-modelopt[torch,onnx]==0.40.0" -> "nvidia-modelopt"
|
||||
# "flux @ git+https://..." -> "flux"
|
||||
_REQ_NAME_RE = re.compile(r"^([A-Za-z0-9]([A-Za-z0-9._-]*[A-Za-z0-9])?)")
|
||||
|
||||
|
||||
def get_pyproject_package_names(pyproject_path: str, group: str) -> set[str]:
|
||||
"""Return normalized names of packages explicitly listed in pyproject.toml."""
|
||||
try:
|
||||
import tomllib
|
||||
except ModuleNotFoundError:
|
||||
try:
|
||||
import tomli as tomllib
|
||||
except ModuleNotFoundError:
|
||||
return set()
|
||||
|
||||
with open(pyproject_path, "rb") as f:
|
||||
data = tomllib.load(f)
|
||||
|
||||
project = data.get("project", {})
|
||||
reqs = list(project.get("dependencies", []))
|
||||
reqs.extend(project.get("optional-dependencies", {}).get(group, []))
|
||||
|
||||
names = set()
|
||||
for req in reqs:
|
||||
match = _REQ_NAME_RE.match(req.strip())
|
||||
if match:
|
||||
names.add(_normalize_package_name(match.group(1)))
|
||||
return names
|
||||
|
||||
|
||||
def get_container_provided_packages() -> list[str]:
|
||||
"""Discover torch/NVIDIA packages already installed in the container."""
|
||||
from importlib.metadata import distributions
|
||||
prefixes = ("torch", "torchvision", "triton", "nvidia-")
|
||||
return sorted({
|
||||
dist.metadata["Name"].lower()
|
||||
for dist in distributions()
|
||||
if dist.metadata["Name"].lower().startswith(prefixes)
|
||||
})
|
||||
|
||||
|
||||
def print_header():
|
||||
"""Print setup header."""
|
||||
log("=" * 60)
|
||||
log(" TensorRT Diffusion - Dependency Setup")
|
||||
log("=" * 60)
|
||||
|
||||
|
||||
def check_uv_installed() -> tuple[bool, bool]:
|
||||
"""
|
||||
Check if uv is installed and accessible.
|
||||
|
||||
Returns:
|
||||
tuple: (is_installed, needs_path_setup)
|
||||
- is_installed: True if uv is available
|
||||
- needs_path_setup: True if user needs to add ~/.local/bin to PATH permanently
|
||||
"""
|
||||
# First check if uv is in PATH
|
||||
try:
|
||||
subprocess.run(
|
||||
["uv", "--version"],
|
||||
capture_output=True,
|
||||
check=True,
|
||||
text=True
|
||||
)
|
||||
return True, False
|
||||
except (subprocess.CalledProcessError, FileNotFoundError):
|
||||
# If not in PATH, check the default installation location
|
||||
local_bin = os.path.expanduser("~/.local/bin")
|
||||
uv_path = os.path.join(local_bin, "uv")
|
||||
|
||||
if os.path.exists(uv_path) and os.access(uv_path, os.X_OK):
|
||||
# uv exists but not in PATH - add it temporarily for this script
|
||||
if local_bin not in os.environ["PATH"]:
|
||||
os.environ["PATH"] = f"{local_bin}:{os.environ['PATH']}"
|
||||
return True, True # Needs permanent PATH setup
|
||||
|
||||
return False, False
|
||||
|
||||
|
||||
def upgrade_tensorrt(pip_spec: str) -> bool:
|
||||
"""Upgrade/install TensorRT using pip.
|
||||
|
||||
Args:
|
||||
pip_spec: PIP specifier for TensorRT, e.g. "tensorrt-cu12" or "tensorrt-cu12==10.16.*"
|
||||
|
||||
Returns:
|
||||
True on success, False otherwise
|
||||
"""
|
||||
print("Installing TensorRT: {}".format(pip_spec))
|
||||
try:
|
||||
subprocess.run(
|
||||
[sys.executable, "-m", "pip", "install", "--upgrade", "--pre", pip_spec],
|
||||
check=True,
|
||||
capture_output=_quiet,
|
||||
)
|
||||
# Print installed version for confirmation
|
||||
try:
|
||||
import importlib
|
||||
trt = importlib.import_module("tensorrt")
|
||||
log(f" Installed TensorRT version: {getattr(trt, '__version__', 'unknown')}")
|
||||
except Exception:
|
||||
pass
|
||||
return True
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(f"Failed to upgrade/install TensorRT: {e}")
|
||||
return False
|
||||
|
||||
|
||||
def install_libgl1() -> bool:
|
||||
"""Install libgl1 system package when possible (Debian/Ubuntu).
|
||||
|
||||
Returns:
|
||||
True if successfully installed or already present, False otherwise.
|
||||
"""
|
||||
print("Checking for libgl1 (system dependency)...")
|
||||
apt_get = shutil.which("apt-get")
|
||||
if not apt_get:
|
||||
log(" Skipping: apt-get not found. Please install 'libgl1' via your OS package manager.")
|
||||
return False
|
||||
|
||||
sudo = shutil.which("sudo")
|
||||
use_sudo = False
|
||||
try:
|
||||
use_sudo = (hasattr(os, "geteuid") and os.geteuid() != 0 and sudo is not None)
|
||||
except Exception:
|
||||
use_sudo = sudo is not None
|
||||
|
||||
try:
|
||||
# Update package list first
|
||||
cmd_update = ([sudo] if use_sudo else []) + [apt_get, "update"]
|
||||
subprocess.run(cmd_update, check=True, capture_output=_quiet)
|
||||
|
||||
cmd_install = ([sudo] if use_sudo else []) + [apt_get, "install", "-y", "libgl1"]
|
||||
subprocess.run(cmd_install, check=True, capture_output=_quiet)
|
||||
log(" libgl1 installed (or already up to date)")
|
||||
return True
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(f" Warning: Failed to install libgl1: {e}")
|
||||
print(" Please install 'libgl1' manually via your OS package manager.")
|
||||
return False
|
||||
|
||||
|
||||
def install_uv():
|
||||
"""Install uv package manager."""
|
||||
log("Installing uv...")
|
||||
try:
|
||||
# Download and run uv installer
|
||||
curl_cmd = [
|
||||
"curl", "-LsSf",
|
||||
"https://astral.sh/uv/install.sh"
|
||||
]
|
||||
|
||||
curl_process = subprocess.Popen(
|
||||
curl_cmd,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE
|
||||
)
|
||||
|
||||
sh_process = subprocess.Popen(
|
||||
["sh"],
|
||||
stdin=curl_process.stdout,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE
|
||||
)
|
||||
|
||||
curl_process.stdout.close()
|
||||
stdout, stderr = sh_process.communicate()
|
||||
|
||||
if sh_process.returncode != 0:
|
||||
print(f"Failed to install uv: {stderr.decode()}")
|
||||
sys.exit(1)
|
||||
|
||||
# Add to PATH for this session
|
||||
# uv installer uses ~/.local/bin by default
|
||||
local_bin = os.path.expanduser("~/.local/bin")
|
||||
if local_bin not in os.environ["PATH"]:
|
||||
os.environ["PATH"] = f"{local_bin}:{os.environ['PATH']}"
|
||||
log("uv installed successfully")
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error installing uv: {e}")
|
||||
print("Please install manually: curl -LsSf https://astral.sh/uv/install.sh | sh")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def get_installed_groups(deps_root: str) -> set[str]:
|
||||
"""
|
||||
Get set of already installed dependency groups.
|
||||
|
||||
A group is considered installed only if both:
|
||||
1. The group directory exists
|
||||
2. The .install_complete marker file exists in that directory
|
||||
|
||||
Args:
|
||||
deps_root: Root directory where dependencies are installed
|
||||
|
||||
Returns:
|
||||
Set of installed group names
|
||||
"""
|
||||
installed = set()
|
||||
deps_path = Path(deps_root)
|
||||
|
||||
if not deps_path.exists():
|
||||
return installed
|
||||
|
||||
for group in VALID_GROUPS:
|
||||
group_dir = deps_path / group
|
||||
marker_file = group_dir / INSTALL_COMPLETE_MARKER
|
||||
|
||||
# Only consider it installed if marker file exists
|
||||
if group_dir.exists() and marker_file.exists():
|
||||
installed.add(group)
|
||||
|
||||
return installed
|
||||
|
||||
|
||||
def install_group(group: str, deps_root: str, project_root: str) -> bool:
|
||||
"""
|
||||
Install dependencies for a specific group.
|
||||
|
||||
Args:
|
||||
group: Dependency group name
|
||||
deps_root: Root directory where dependencies will be installed
|
||||
project_root: Project root directory (where pyproject.toml is)
|
||||
|
||||
Returns:
|
||||
True if installation succeeded, False otherwise
|
||||
"""
|
||||
description = GROUP_DESCRIPTIONS.get(group, group)
|
||||
install_path = os.path.join(deps_root, group)
|
||||
|
||||
print(f"Installing {description}...")
|
||||
log(f" Location: {install_path}")
|
||||
|
||||
try:
|
||||
# Determine Python version and site-packages path
|
||||
python_version = f"{sys.version_info.major}.{sys.version_info.minor}"
|
||||
site_packages = Path(install_path) / "lib" / f"python{python_version}" / "site-packages"
|
||||
|
||||
# Create site-packages directory if it doesn't exist
|
||||
site_packages.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Build an overrides file so uv never installs packages that are
|
||||
# already installed in the container. Exclude packages explicitly
|
||||
# listed in pyproject.toml so they get installed at the pinned version.
|
||||
container_pkgs = get_container_provided_packages()
|
||||
declared_pkgs = get_pyproject_package_names(
|
||||
str(Path(project_root) / "pyproject.toml"), group
|
||||
)
|
||||
overrides_content = "\n".join(
|
||||
f'{pkg} ; python_version < "0"'
|
||||
for pkg in container_pkgs
|
||||
if _normalize_package_name(pkg) not in declared_pkgs
|
||||
)
|
||||
|
||||
with tempfile.NamedTemporaryFile(
|
||||
mode="w", suffix=".txt", prefix="uv_overrides_", delete=False
|
||||
) as f:
|
||||
f.write(overrides_content)
|
||||
overrides_path = f.name
|
||||
|
||||
try:
|
||||
cmd = [
|
||||
"uv", "pip", "install",
|
||||
"--python-preference", "only-system",
|
||||
"--prefix", install_path,
|
||||
"--overrides", overrides_path,
|
||||
f".[{group}]"
|
||||
]
|
||||
|
||||
subprocess.run(
|
||||
cmd,
|
||||
cwd=project_root,
|
||||
text=True,
|
||||
capture_output=_quiet,
|
||||
check=True
|
||||
)
|
||||
finally:
|
||||
os.unlink(overrides_path)
|
||||
|
||||
# Create marker file to indicate successful installation
|
||||
marker_file = Path(install_path) / INSTALL_COMPLETE_MARKER
|
||||
marker_file.write_text("Installation completed successfully\n")
|
||||
|
||||
print(f" {description} installed successfully")
|
||||
return True
|
||||
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(f"Failed to install {description}")
|
||||
print(f" Error: {e}")
|
||||
|
||||
# Clean up incomplete installation
|
||||
if os.path.exists(install_path):
|
||||
log(f" Cleaning up incomplete installation at {install_path}")
|
||||
try:
|
||||
shutil.rmtree(install_path)
|
||||
except Exception as cleanup_error:
|
||||
print(f" Warning: Failed to clean up: {cleanup_error}")
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def determine_groups_to_install(requested: str, already_installed: set[str]) -> list[str]:
|
||||
"""
|
||||
Determine which groups need to be installed.
|
||||
|
||||
Args:
|
||||
requested: Requested group or "all"
|
||||
already_installed: Set of already installed groups
|
||||
|
||||
Returns:
|
||||
List of groups to install
|
||||
"""
|
||||
if requested == "all":
|
||||
return sorted(VALID_GROUPS - already_installed)
|
||||
elif requested in VALID_GROUPS:
|
||||
return [] if requested in already_installed else [requested]
|
||||
return []
|
||||
|
||||
|
||||
def print_summary(installed_groups: set[str]):
|
||||
"""
|
||||
Print summary of installed groups.
|
||||
|
||||
Args:
|
||||
installed_groups: Set of all installed groups
|
||||
"""
|
||||
log("=" * 60)
|
||||
print("Setup complete!")
|
||||
log("=" * 60)
|
||||
|
||||
if installed_groups:
|
||||
log("Installed groups:")
|
||||
for group in sorted(installed_groups):
|
||||
description = GROUP_DESCRIPTIONS.get(group, group)
|
||||
log(f" {description}")
|
||||
|
||||
log("Each demo script automatically uses the correct dependencies.")
|
||||
else:
|
||||
log("No groups installed.")
|
||||
|
||||
log()
|
||||
|
||||
|
||||
def parse_args():
|
||||
"""Parse command line arguments."""
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Setup TensorRT Diffusion dependencies",
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
epilog="""
|
||||
Examples:
|
||||
python setup.py # Install all groups
|
||||
python setup.py sd # Install SD family only
|
||||
python setup.py flux # Install Flux only
|
||||
python setup.py cosmos # Install Cosmos only
|
||||
|
||||
Groups:
|
||||
sd - SD 1.4, SDXL, SD3, SD3.5, SVD, Stable Cascade (Stability AI)
|
||||
flux - Flux models (Black Forest Labs)
|
||||
cosmos - Cosmos models (NVIDIA)
|
||||
all - All groups (default)
|
||||
"""
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"group",
|
||||
nargs="?",
|
||||
default="all",
|
||||
choices=list(VALID_GROUPS) + ["all"],
|
||||
help="Dependency group to install (default: all)"
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--deps-root",
|
||||
default=DEFAULT_DEPS_ROOT,
|
||||
help=f"Root directory for dependencies (default: {DEFAULT_DEPS_ROOT})"
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--force",
|
||||
action="store_true",
|
||||
help="Force reinstallation even if already installed"
|
||||
)
|
||||
|
||||
parser.add_argument("--skip-tensorrt", action="store_true", help="Skip TensorRT upgrade/installation")
|
||||
|
||||
parser.add_argument("-q", "--quiet", action="store_true", help="Suppress informational output (errors are always shown)")
|
||||
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def main():
|
||||
"""Main setup function."""
|
||||
args = parse_args()
|
||||
|
||||
global _quiet
|
||||
_quiet = args.quiet
|
||||
|
||||
print_header()
|
||||
|
||||
# Check/install uv
|
||||
uv_installed, needs_path_setup = check_uv_installed()
|
||||
if not uv_installed:
|
||||
install_uv()
|
||||
needs_path_setup = True # Fresh install always needs PATH setup
|
||||
else:
|
||||
log("uv is already installed")
|
||||
if needs_path_setup:
|
||||
log(" (temporarily added ~/.local/bin to PATH for this session)")
|
||||
|
||||
# Get project root (where pyproject.toml is)
|
||||
project_root = str(Path(__file__).parent.absolute())
|
||||
pyproject_path = Path(project_root) / "pyproject.toml"
|
||||
|
||||
if not pyproject_path.exists():
|
||||
print(f"Error: pyproject.toml not found in {project_root}")
|
||||
print(" Make sure you're running this script from the project root.")
|
||||
sys.exit(1)
|
||||
|
||||
# Check what's already installed
|
||||
installed_groups = get_installed_groups(args.deps_root)
|
||||
|
||||
# Install system dependency (best-effort) and ensure TensorRT is present
|
||||
install_libgl1()
|
||||
if not args.skip_tensorrt:
|
||||
upgrade_tensorrt("tensorrt-cu12")
|
||||
else:
|
||||
log("Skipping TensorRT upgrade (--skip-tensorrt specified)")
|
||||
|
||||
if installed_groups and not args.force:
|
||||
log(f"Already installed: {', '.join(sorted(installed_groups))}")
|
||||
|
||||
# Determine what to install
|
||||
if args.force and args.group == "all":
|
||||
groups_to_install = sorted(VALID_GROUPS)
|
||||
elif args.force and args.group in VALID_GROUPS:
|
||||
groups_to_install = [args.group]
|
||||
else:
|
||||
groups_to_install = determine_groups_to_install(args.group, installed_groups)
|
||||
|
||||
# Early exit if nothing to do
|
||||
if not groups_to_install:
|
||||
if args.group == "all":
|
||||
print("All requested dependencies are already installed!")
|
||||
else:
|
||||
description = GROUP_DESCRIPTIONS.get(args.group, args.group)
|
||||
print(f"{description} is already installed!")
|
||||
log(f" To reinstall, use --force flag or remove {args.deps_root}/{args.group}")
|
||||
print_summary(installed_groups)
|
||||
sys.exit(0)
|
||||
|
||||
# Install groups
|
||||
print(f"Installing {len(groups_to_install)} group(s): {', '.join(groups_to_install)}")
|
||||
|
||||
success_count = 0
|
||||
for group in groups_to_install:
|
||||
if install_group(group, args.deps_root, project_root):
|
||||
installed_groups.add(group)
|
||||
success_count += 1
|
||||
|
||||
# Print summary
|
||||
print_summary(installed_groups)
|
||||
|
||||
# Print PATH setup instructions if needed
|
||||
if needs_path_setup:
|
||||
print()
|
||||
print("=" * 60)
|
||||
print(" Action Required: Update PATH")
|
||||
print("=" * 60)
|
||||
print("uv was installed to ~/.local/bin but is not in your PATH.")
|
||||
print()
|
||||
print("To make uv available in future sessions, run:")
|
||||
print(" source ~/.local/bin/env")
|
||||
print()
|
||||
print("Or restart your shell.")
|
||||
print("=" * 60)
|
||||
|
||||
# Exit with error if any installations failed
|
||||
if success_count < len(groups_to_install):
|
||||
print(f"Warning: {len(groups_to_install) - success_count} group(s) failed to install")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,21 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Minimal and backward-compatible: default to modern requirements, allow override via env.
|
||||
REQ_FILE="${REQUIREMENTS_FILE:-requirements.txt}"
|
||||
echo "[I] Using requirements file: ${REQ_FILE} (override with REQUIREMENTS_FILE=<file>)"
|
||||
|
||||
# Upgrade pip and install TensorRT
|
||||
python3 -m pip install --upgrade pip
|
||||
pip3 install --pre tensorrt-cu12
|
||||
|
||||
# Install the required packages
|
||||
PIP_CONSTRAINT= pip3 install -r "${REQ_FILE}"
|
||||
|
||||
# Install libgl1
|
||||
# Check if apt-get is available
|
||||
if command -v apt-get >/dev/null 2>&1; then
|
||||
sudo apt-get install -y libgl1
|
||||
else
|
||||
echo "Warning: apt-get not found. Please install libgl1 manually for your system."
|
||||
fi
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
pytest
|
||||
@@ -0,0 +1,62 @@
|
||||
#
|
||||
# SPDX-FileCopyrightText: Copyright (c) 2025 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 numpy as np
|
||||
import onnx_graphsurgeon as gs
|
||||
|
||||
from demo_diffusion import utils_modelopt
|
||||
|
||||
|
||||
def create_graph_with_fp16_resize() -> gs.Graph:
|
||||
"""Return a gs.Graph with a single Resize node with FP16 input."""
|
||||
return gs.Graph(
|
||||
nodes=[
|
||||
gs.Node(
|
||||
op="Resize",
|
||||
name="my_resize_node",
|
||||
inputs=[
|
||||
gs.Variable(name="X", dtype=np.float16),
|
||||
gs.Variable(name="roi", dtype=np.float16),
|
||||
gs.Variable(name="scales", dtype=np.float16),
|
||||
gs.Variable(name="sizes", dtype=np.int64),
|
||||
],
|
||||
outputs=[gs.Variable(name="Y", dtype=np.float16)],
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def test_should_cast_resize_to_fp32() -> None:
|
||||
"""Test that `cast_resize_to_fp32` correctly casts all Resize nodes to FP32."""
|
||||
# Precondition.
|
||||
graph = create_graph_with_fp16_resize()
|
||||
|
||||
# Under test.
|
||||
utils_modelopt.cast_resize_io(graph)
|
||||
|
||||
# Postcondition.
|
||||
has_resize = False
|
||||
for node in graph.nodes:
|
||||
if node.op == "Resize":
|
||||
has_resize = True
|
||||
x, roi, scales, sizes = node.inputs
|
||||
assert x.dtype == np.float32
|
||||
assert roi.dtype == np.float32
|
||||
assert scales.dtype == np.float32
|
||||
assert sizes.dtype == np.int64 # "sizes" is the exception input that cannot be cast to FP32.
|
||||
|
||||
assert has_resize
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,8 @@
|
||||
# TensorRT demo with EfficientDet
|
||||
|
||||
To run the demo Jupyter notebook in this folder, follow the instructions in the TRT setup guide to generate the TensorRT-OSS docker container, and build TensorRT-OSS.
|
||||
|
||||
As specified in the guide for setting up the build environment, specify port number using `--jupyter <port>` to launch Jupyter lab interface with the container.
|
||||
|
||||
EfficientDet-TensorRT8.ipynb: Step by step walkthrough for building EfficientDet TensorRT engine from pre-trained Tensorflow checkpoint.
|
||||
|
||||
Reference in New Issue
Block a user