Tabla de contenido
Inferencia usando la API de Python
Razonamiento usando la API de C++
¡El tutorial de introducción a la implementación de modelos continúa actualizándose! Creo que después de los estudios anteriores, todos tienen una comprensión más completa de la representación intermedia de ONNX, pero en un entorno de producción específico, el modelo ONNX a menudo debe convertirse en un formato de modelo que pueda usar el back-end de razonamiento específico. En este tutorial, conoceremos contigo el famoso backend de razonamiento TensorRT.
Introducción a TensorRT
TensorRT es un marco de aprendizaje profundo lanzado por NVIDIA para ejecutar inferencias de aprendizaje profundo en su hardware. TensorRT proporciona capacitación consciente de la cuantificación y funciones de cuantificación fuera de línea, y los usuarios pueden elegir dos modos de optimización, INT8 y FP16, para aplicar modelos de aprendizaje profundo a implementaciones de producción de diferentes tareas, como transmisión de video, reconocimiento de voz, recomendación, detección de fraude, generación de texto , y manejo del lenguaje natural. TensorRT está altamente optimizado para ejecutarse en GPU NVIDIA y es probablemente el motor de inferencia más rápido que actualmente ejecuta modelos en GPU NVIDIA. Puede encontrar información más específica sobre TensorRT en el sitio web oficial de TensorRT .
Instalar TensorRT
ventanas
De manera predeterminada, en una máquina con una tarjeta gráfica NVIDIA, instale CUDA y CUDNN con anticipación e inicie sesión en el sitio web oficial de NVIDIA para descargar el paquete comprimido de TensorRT que es compatible con la versión host de CUDA.
Tomando CUDA versión 10.2 como ejemplo, seleccione el paquete zip que se adapta a CUDA 10.2 Una vez completada la descarga, los usuarios con el entorno virtual conda pueden cambiar primero al entorno virtual y luego ejecutar comandos similares a los siguientes en powershell para instalar y prueba:
cd \the\path\of\tensorrt\zip\file
Expand-Archive TensorRT-8.2.5.1.Windows10.x86_64.cuda-10.2.cudnn8.2.zip .
$env:TENSORRT_DIR = "$pwd\TensorRT-8.2.5.1"
$env:path = "$env:TENSORRT_DIR\lib;" + $env:path
pip install $env:TENSORRT_DIR\python\tensorrt-8.2.5.1-cp36-none-win_amd64.whl
python -c "import tensorrt;print(tensorrt.__version__)"
El comando anterior verificará la versión de TensorRT después de la instalación.Si el resultado impreso es 8.2.5.1, significa que el paquete de Python se instaló correctamente.
linux
Similar a la instalación en el entorno de Windows, CUDA y CUDNN se instalan por adelantado en una máquina con una tarjeta gráfica NVIDIA de forma predeterminada, y puede iniciar sesión en el sitio web oficial de NVIDIA para descargar el paquete comprimido TensorRT que es compatible con el host CUDA versión.
Tome CUDA versión 10.2 como ejemplo, seleccione el paquete tar que se adapta a CUDA 10.2 y luego ejecute comandos similares a los siguientes para instalar y probar:
cd /the/path/of/tensorrt/tar/gz/file
tar -zxvf TensorRT-8.2.5.1.linux.x86_64-gnu.cuda-10.2.cudnn8.2.tar.gz
export TENSORRT_DIR=$(pwd)/TensorRT-8.2.5.1
export LD_LIBRARY_PATH=$TENSORRT_DIR/lib:$LD_LIBRARY_PATH
pip install TensorRT-8.2.5.1/python/tensorrt-8.2.5.1-cp37-none-linux_x86_64.whl
python -c "import tensorrt;print(tensorrt.__version__)"
Si el resultado impreso es 8.2.5.1, significa que el paquete de Python se instaló correctamente.
construcción del modelo
Usamos TensorRT para generar modelos de dos maneras principales:
- Construya la red capa por capa directamente a través de la API de TensorRT;
- Convierta el modelo de representación intermedia en un modelo TensorRT, como convertir un modelo ONNX en un modelo TensorRT.
A continuación, usaremos estos dos métodos para construir el modelo TensorRT en Python y C++, y usaremos el modelo generado para la inferencia.
construir directamente
Usando la API de TensorRT para construir una red capa por capa, este proceso es similar a usar un marco de entrenamiento general, como usar Pytorch o TensorFlow para construir una red. Cabe señalar que para la parte de peso, como la convolución o la capa de normalización, el contenido de peso debe asignarse a la red TensorRT. Este artículo no lo mostrará en detalle, sino que solo construirá una red simple que agrupe la entrada.
Compilación con la API de Python
El primero es usar la API de Python para construir directamente la red TensorRT. Este método usa principalmente funciones tensorrt.Builder
y create_builder_config
funciones create_network
para construir la configuración y la red respectivamente. El primero se usa para establecer parámetros como el espacio de trabajo máximo de la red y el Este último es el cuerpo principal de la red, que debe agregarse capa por capa.
Además, es necesario definir los nombres de entrada y salida, serializar la red construida y guardarla como un archivo local. Vale la pena señalar que si desea que la red acepte entrada y salida con diferentes resoluciones, debe usar tensorrt.Builder
la create_optimization_profile
función y establecer los tamaños mínimo y máximo.
El código de implementación es el siguiente:
import tensorrt as trt
verbose = True
IN_NAME = 'input'
OUT_NAME = 'output'
IN_H = 224
IN_W = 224
BATCH_SIZE = 1
EXPLICIT_BATCH = 1 << (int)(
trt.NetworkDefinitionCreationFlag.EXPLICIT_BATCH)
TRT_LOGGER = trt.Logger(trt.Logger.VERBOSE) if verbose else trt.Logger()
with trt.Builder(TRT_LOGGER) as builder, builder.create_builder_config(
) as config, builder.create_network(EXPLICIT_BATCH) as network:
# define network
input_tensor = network.add_input(
name=IN_NAME, dtype=trt.float32, shape=(BATCH_SIZE, 3, IN_H, IN_W))
pool = network.add_pooling(
input=input_tensor, type=trt.PoolingType.MAX, window_size=(2, 2))
pool.stride = (2, 2)
pool.get_output(0).name = OUT_NAME
network.mark_output(pool.get_output(0))
# serialize the model to engine file
profile = builder.create_optimization_profile()
profile.set_shape_input('input', *[[BATCH_SIZE, 3, IN_H, IN_W]]*3)
builder.max_batch_size = 1
config.max_workspace_size = 1 << 30
engine = builder.build_engine(network, config)
with open('model_python_trt.engine', mode='wb') as f:
f.write(bytearray(engine.serialize()))
print("generating file done!")
Compilación con la API de C++
Para los socios pequeños que desean usar directamente el lenguaje C++ para construir una red, todo el proceso es muy similar al proceso de ejecución de Python mencionado anteriormente. Los puntos principales a tener en cuenta son:
nvinfer1:: createInferBuilder
En correspondencia con eso en Python , la instancia de la clasetensorrt.Builder
debe pasarse , pero es una clase abstracta, y el usuario debe heredar la clase e implementar la función virtual interna. Pero aquí usamos directamente la subclase de implementación en el archivo de la carpeta de muestras después de descomprimir el paquete TensorRT.ILogger
ILogger
../samples/common/logger.h
Logger
- Establecer el tamaño de entrada del modelo TensorRT requiere varias llamadas
IOptimizationProfile
,setDimensions
lo que es un poco más engorroso que Python.IOptimizationProfile
Se requieren funcionescreateOptimizationProfile
, correspondientes acreate_builder_config
las funciones de Python.
El código de implementación es el siguiente:
#include <fstream>
#include <iostream>
#include <NvInfer.h>
#include <../samples/common/logger.h>
using namespace nvinfer1;
using namespace sample;
const char* IN_NAME = "input";
const char* OUT_NAME = "output";
static const int IN_H = 224;
static const int IN_W = 224;
static const int BATCH_SIZE = 1;
static const int EXPLICIT_BATCH = 1 << (int)(NetworkDefinitionCreationFlag::kEXPLICIT_BATCH);
int main(int argc, char** argv)
{
// Create builder
Logger m_logger;
IBuilder* builder = createInferBuilder(m_logger);
IBuilderConfig* config = builder->createBuilderConfig();
// Create model to populate the network
INetworkDefinition* network = builder->createNetworkV2(EXPLICIT_BATCH);
ITensor* input_tensor = network->addInput(IN_NAME, DataType::kFLOAT, Dims4{ BATCH_SIZE, 3, IN_H, IN_W });
IPoolingLayer* pool = network->addPoolingNd(*input_tensor, PoolingType::kMAX, DimsHW{ 2, 2 });
pool->setStrideNd(DimsHW{ 2, 2 });
pool->getOutput(0)->setName(OUT_NAME);
network->markOutput(*pool->getOutput(0));
// Build engine
IOptimizationProfile* profile = builder->createOptimizationProfile();
profile->setDimensions(IN_NAME, OptProfileSelector::kMIN, Dims4(BATCH_SIZE, 3, IN_H, IN_W));
profile->setDimensions(IN_NAME, OptProfileSelector::kOPT, Dims4(BATCH_SIZE, 3, IN_H, IN_W));
profile->setDimensions(IN_NAME, OptProfileSelector::kMAX, Dims4(BATCH_SIZE, 3, IN_H, IN_W));
config->setMaxWorkspaceSize(1 << 20);
ICudaEngine* engine = builder->buildEngineWithConfig(*network, *config);
// Serialize the model to engine file
IHostMemory* modelStream{ nullptr };
assert(engine != nullptr);
modelStream = engine->serialize();
std::ofstream p("model.engine", std::ios::binary);
if (!p) {
std::cerr << "could not open output file to save model" << std::endl;
return -1;
}
p.write(reinterpret_cast<const char*>(modelStream->data()), modelStream->size());
std::cout << "generating file done!" << std::endl;
// Release resources
modelStream->destroy();
network->destroy();
engine->destroy();
builder->destroy();
config->destroy();
return 0;
}
modelo de conversión IR
Además de construir la red capa por capa y serializar el modelo directamente a través de la API de TensorRT, TensorRT también admite la conversión de modelos de representación intermedios (como ONNX) en modelos de TensorRT.
Convertir utilizando la API de Python
Primero usamos Pytorch para implementar un modelo consistente con lo anterior, es decir, solo agrupamos la entrada y la generamos una vez; luego convertimos el modelo Pytorch al modelo ONNX; finalmente convertimos el modelo ONNX al modelo TensorRT.
La función TensorRT se usa principalmente aquí OnnxParser
, que puede analizar el modelo ONNX en la red TensorRT. Finalmente, también podemos obtener un modelo TensorRT cuya función es consistente con la del modelo implementado en el método anterior.
El código de implementación es el siguiente:
import torch
import onnx
import tensorrt as trt
onnx_model = 'model.onnx'
class NaiveModel(torch.nn.Module):
def __init__(self):
super().__init__()
self.pool = torch.nn.MaxPool2d(2, 2)
def forward(self, x):
return self.pool(x)
device = torch.device('cuda:0')
# generate ONNX model
torch.onnx.export(NaiveModel(), torch.randn(1, 3, 224, 224), onnx_model, input_names=['input'], output_names=['output'], opset_version=11)
onnx_model = onnx.load(onnx_model)
# create builder and network
logger = trt.Logger(trt.Logger.ERROR)
builder = trt.Builder(logger)
EXPLICIT_BATCH = 1 << (int)(
trt.NetworkDefinitionCreationFlag.EXPLICIT_BATCH)
network = builder.create_network(EXPLICIT_BATCH)
# parse onnx
parser = trt.OnnxParser(network, logger)
if not parser.parse(onnx_model.SerializeToString()):
error_msgs = ''
for error in range(parser.num_errors):
error_msgs += f'{parser.get_error(error)}\n'
raise RuntimeError(f'Failed to parse onnx, {error_msgs}')
config = builder.create_builder_config()
config.max_workspace_size = 1<<20
profile = builder.create_optimization_profile()
profile.set_shape('input', [1,3 ,224 ,224], [1,3,224, 224], [1,3 ,224 ,224])
config.add_optimization_profile(profile)
# create engine
with torch.cuda.device(device):
engine = builder.build_engine(network, config)
with open('model.engine', mode='wb') as f:
f.write(bytearray(engine.serialize()))
print("generating file done!")
Durante la conversión IR, si se requieren múltiples lotes, múltiples entradas y formas dinámicas, se pueden configurar set_shape
llamando a la función varias veces. set_shape
Los parámetros aceptados por la función son: nombre del nodo de entrada, tamaño de entrada mínimo aceptable, tamaño de entrada óptimo y tamaño de entrada máximo aceptable. Generalmente se requiere que la relación de tamaño de estas tres dimensiones sea monótonamente creciente.
Convertir utilizando la API de C++
Después de presentar cómo convertir el modelo ONNX al modelo TensorRT con lenguaje Python, luego presente cómo convertir el modelo ONNX al modelo TensorRT con C++. A través de esto NvOnnxParser
, podemos analizar directamente en la red el archivo ONNX obtenido en la sección anterior.
El código de implementación es el siguiente:
#include <fstream>
#include <iostream>
#include <NvInfer.h>
#include <NvOnnxParser.h>
#include <../samples/common/logger.h>
using namespace nvinfer1;
using namespace nvonnxparser;
using namespace sample;
int main(int argc, char** argv)
{
// Create builder
Logger m_logger;
IBuilder* builder = createInferBuilder(m_logger);
const auto explicitBatch = 1U << static_cast<uint32_t>(NetworkDefinitionCreationFlag::kEXPLICIT_BATCH);
IBuilderConfig* config = builder->createBuilderConfig();
// Create model to populate the network
INetworkDefinition* network = builder->createNetworkV2(explicitBatch);
// Parse ONNX file
IParser* parser = nvonnxparser::createParser(*network, m_logger);
bool parser_status = parser->parseFromFile("model.onnx", static_cast<int>(ILogger::Severity::kWARNING));
// Get the name of network input
Dims dim = network->getInput(0)->getDimensions();
if (dim.d[0] == -1) // -1 means it is a dynamic model
{
const char* name = network->getInput(0)->getName();
IOptimizationProfile* profile = builder->createOptimizationProfile();
profile->setDimensions(name, OptProfileSelector::kMIN, Dims4(1, dim.d[1], dim.d[2], dim.d[3]));
profile->setDimensions(name, OptProfileSelector::kOPT, Dims4(1, dim.d[1], dim.d[2], dim.d[3]));
profile->setDimensions(name, OptProfileSelector::kMAX, Dims4(1, dim.d[1], dim.d[2], dim.d[3]));
config->addOptimizationProfile(profile);
}
// Build engine
config->setMaxWorkspaceSize(1 << 20);
ICudaEngine* engine = builder->buildEngineWithConfig(*network, *config);
// Serialize the model to engine file
IHostMemory* modelStream{ nullptr };
assert(engine != nullptr);
modelStream = engine->serialize();
std::ofstream p("model.engine", std::ios::binary);
if (!p) {
std::cerr << "could not open output file to save model" << std::endl;
return -1;
}
p.write(reinterpret_cast<const char*>(modelStream->data()), modelStream->size());
std::cout << "generate file success!" << std::endl;
// Release resources
modelStream->destroy();
network->destroy();
engine->destroy();
builder->destroy();
config->destroy();
return 0;
}
modelo de razonamiento
Anteriormente, usamos dos formas de construir modelos TensorRT y generamos cuatro modelos TensorRT en Python y C++ respectivamente.Las funciones de estos cuatro modelos son teóricamente idénticas.
A continuación, usaremos Python y C++ para realizar inferencias en el modelo TensorRT generado.
Inferencia usando la API de Python
El primero es usar la API de Python para inferir el modelo TensorRT Parte del código aquí se refiere a MMDeploy . Ejecute el siguiente código, puede encontrar que 1x3x224x224
se ingresa un tensor y 1x3x112x112
se genera un tensor, lo que está exactamente en línea con nuestra expectativa del resultado después de la agrupación de entrada.
from typing import Union, Optional, Sequence,Dict,Any
import torch
import tensorrt as trt
class TRTWrapper(torch.nn.Module):
def __init__(self,engine: Union[str, trt.ICudaEngine],
output_names: Optional[Sequence[str]] = None) -> None:
super().__init__()
self.engine = engine
if isinstance(self.engine, str):
with trt.Logger() as logger, trt.Runtime(logger) as runtime:
with open(self.engine, mode='rb') as f:
engine_bytes = f.read()
self.engine = runtime.deserialize_cuda_engine(engine_bytes)
self.context = self.engine.create_execution_context()
names = [_ for _ in self.engine]
input_names = list(filter(self.engine.binding_is_input, names))
self._input_names = input_names
self._output_names = output_names
if self._output_names is None:
output_names = list(set(names) - set(input_names))
self._output_names = output_names
def forward(self, inputs: Dict[str, torch.Tensor]):
assert self._input_names is not None
assert self._output_names is not None
bindings = [None] * (len(self._input_names) + len(self._output_names))
profile_id = 0
for input_name, input_tensor in inputs.items():
# check if input shape is valid
profile = self.engine.get_profile_shape(profile_id, input_name)
assert input_tensor.dim() == len(
profile[0]), 'Input dim is different from engine profile.'
for s_min, s_input, s_max in zip(profile[0], input_tensor.shape,
profile[2]):
assert s_min <= s_input <= s_max, \
'Input shape should be between ' \
+ f'{profile[0]} and {profile[2]}' \
+ f' but get {tuple(input_tensor.shape)}.'
idx = self.engine.get_binding_index(input_name)
# All input tensors must be gpu variables
assert 'cuda' in input_tensor.device.type
input_tensor = input_tensor.contiguous()
if input_tensor.dtype == torch.long:
input_tensor = input_tensor.int()
self.context.set_binding_shape(idx, tuple(input_tensor.shape))
bindings[idx] = input_tensor.contiguous().data_ptr()
# create output tensors
outputs = {}
for output_name in self._output_names:
idx = self.engine.get_binding_index(output_name)
dtype = torch.float32
shape = tuple(self.context.get_binding_shape(idx))
device = torch.device('cuda')
output = torch.empty(size=shape, dtype=dtype, device=device)
outputs[output_name] = output
bindings[idx] = output.data_ptr()
self.context.execute_async_v2(bindings,
torch.cuda.current_stream().cuda_stream)
return outputs
model = TRTWrapper('model.engine', ['output'])
output = model(dict(input = torch.randn(1, 3, 224, 224).cuda()))
print(output)
Razonamiento usando la API de C++
Finalmente, en muchos entornos de producción reales, usaremos el lenguaje C++ para completar tareas específicas para lograr efectos de ejecución de código más eficientes. Además, los usuarios de TensoRT generalmente valoran su uso en C++, por lo que también usamos el lenguaje C++ Implementar el razonamiento del modelo nuevamente, lo que puede también puede compararse con el uso de la API de Python para razonar sobre el modelo.
El código de implementación es el siguiente:
#include <fstream>
#include <iostream>
#include <NvInfer.h>
#include <../samples/common/logger.h>
#define CHECK(status) \
do\
{\
auto ret = (status);\
if (ret != 0)\
{\
std::cerr << "Cuda failure: " << ret << std::endl;\
abort();\
}\
} while (0)
using namespace nvinfer1;
using namespace sample;
const char* IN_NAME = "input";
const char* OUT_NAME = "output";
static const int IN_H = 224;
static const int IN_W = 224;
static const int BATCH_SIZE = 1;
static const int EXPLICIT_BATCH = 1 << (int)(NetworkDefinitionCreationFlag::kEXPLICIT_BATCH);
void doInference(IExecutionContext& context, float* input, float* output, int batchSize)
{
const ICudaEngine& engine = context.getEngine();
// Pointers to input and output device buffers to pass to engine.
// Engine requires exactly IEngine::getNbBindings() number of buffers.
assert(engine.getNbBindings() == 2);
void* buffers[2];
// In order to bind the buffers, we need to know the names of the input and output tensors.
// Note that indices are guaranteed to be less than IEngine::getNbBindings()
const int inputIndex = engine.getBindingIndex(IN_NAME);
const int outputIndex = engine.getBindingIndex(OUT_NAME);
// Create GPU buffers on device
CHECK(cudaMalloc(&buffers[inputIndex], batchSize * 3 * IN_H * IN_W * sizeof(float)));
CHECK(cudaMalloc(&buffers[outputIndex], batchSize * 3 * IN_H * IN_W /4 * sizeof(float)));
// Create stream
cudaStream_t stream;
CHECK(cudaStreamCreate(&stream));
// DMA input batch data to device, infer on the batch asynchronously, and DMA output back to host
CHECK(cudaMemcpyAsync(buffers[inputIndex], input, batchSize * 3 * IN_H * IN_W * sizeof(float), cudaMemcpyHostToDevice, stream));
context.enqueue(batchSize, buffers, stream, nullptr);
CHECK(cudaMemcpyAsync(output, buffers[outputIndex], batchSize * 3 * IN_H * IN_W / 4 * sizeof(float), cudaMemcpyDeviceToHost, stream));
cudaStreamSynchronize(stream);
// Release stream and buffers
cudaStreamDestroy(stream);
CHECK(cudaFree(buffers[inputIndex]));
CHECK(cudaFree(buffers[outputIndex]));
}
int main(int argc, char** argv)
{
// create a model using the API directly and serialize it to a stream
char *trtModelStream{ nullptr };
size_t size{ 0 };
std::ifstream file("model.engine", std::ios::binary);
if (file.good()) {
file.seekg(0, file.end);
size = file.tellg();
file.seekg(0, file.beg);
trtModelStream = new char[size];
assert(trtModelStream);
file.read(trtModelStream, size);
file.close();
}
Logger m_logger;
IRuntime* runtime = createInferRuntime(m_logger);
assert(runtime != nullptr);
ICudaEngine* engine = runtime->deserializeCudaEngine(trtModelStream, size, nullptr);
assert(engine != nullptr);
IExecutionContext* context = engine->createExecutionContext();
assert(context != nullptr);
// generate input data
float data[BATCH_SIZE * 3 * IN_H * IN_W];
for (int i = 0; i < BATCH_SIZE * 3 * IN_H * IN_W; i++)
data[i] = 1;
// Run inference
float prob[BATCH_SIZE * 3 * IN_H * IN_W /4];
doInference(*context, data, prob, BATCH_SIZE);
// Destroy the engine
context->destroy();
engine->destroy();
runtime->destroy();
return 0;
}
Resumir
A través del estudio de este artículo, hemos dominado dos formas de construir el modelo TensorRT: construir directamente la red capa por capa a través de la API de TensorRT; convertir el modelo de representación intermedia en el modelo TensorRT. No solo eso, también completamos la construcción y razonamiento del modelo TensorRT en C++ y Python respectivamente ¡Creo que todos han ganado algo! En el próximo artículo, aprenderemos cómo agregar operadores personalizados de TensorRT contigo, así que permanece atento~
Preguntas más frecuentes
- P : Se informa un error al ejecutar el código: No se pudo encontrar: cudnn64_8.dll ¿Está en su RUTA?
- R: Primero verifique si su variable de entorno contiene la ruta de cudnn64_8.dll.Si encuentra que la ruta de cudnn está en C:\\Program Files\\NVIDIA GPU Computing Toolkit\\CUDA\\v10.2\\bin, Pero solo contiene cudnn64_7.dll.La solución es descargar el paquete zip cuDNN del sitio web oficial de NVIDIA, descomprimirlo y copiar cudnn64_8.dll en el directorio bin de CUDA Toolkit. En este momento, también puede copiar una copia de cudnn64_7.dll y luego cambiar el nombre de la copia copiada a cudnn64_8.dll, lo que también puede resolver este problema.
referencia
Portal de la serie
OpenMMLab: Interpretación de TorchScript (2): análisis de implementación de Torch jit tracer
OpenMMLab: Interpretación de TorchScript (3): reescritura de subgrafos en jit
OpenMMLab: Interpretación de TorchScript (4): Análisis de alias en Torch jit