Implementación del modelo Pytorch -------- Introducción a TensorRT

TensorRT


Reimprimir enlace

Introducción

TensorRT (TRT) es una herramienta que puede acelerar significativamente la inferencia de modelos de aprendizaje profundo. Si se puede usar bien, puede mejorar significativamente la eficiencia de uso de nuestra GPU y la velocidad de ejecución del modelo.

TensorRT (TRT) es un marco de inferencia de GPU rápido, y su proceso habitual es utilizar archivos de modelo existentesCompilar un motorEn el proceso de compilación del motor, ** encontrará el método de operador óptimo para cada nivel de operación de cálculo, de modo que el motor compilado se pueda ejecutar de manera muy eficiente. ** Es muy similar al proceso de compilación de C ++.

Con respecto a la información relevante de la TRT, creo que prevalecerá el funcionario de NV. Para TRT, su ventaja de velocidad en el razonamiento es definitivamente evidente, lo bueno que es, lo rápido que es y si es adecuado para su escenario comercial, esto requiere que todos juzguen por sí mismos. El proceso general y algunas optimizaciones básicas también se explican en la información oficial de NV. Puede consultar este artículo de ticket deploying-deep-learning-nvidia-tensorrt . Para una comprensión rápida de TRT y el proceso de instalación, también puede consultar TensorRT-Introduction-Use-Install

Construcción del modelo

TRT compila la estructura y los parámetros del modelo, así como el método de cálculo del kernel correspondiente en un motor binario, lo que acelera enormemente la velocidad de inferencia después de la implementación. Para poder usar TRT para razonar, es necesario crear un eneninge. Hay dos formas de crear un motor en TRT:

  • Compilado por la estructura del modelo de red y los archivos de parámetros, es muy lento.
  • Leer un motor existente (archivo gie) es más rápido porque omite el proceso de análisis del modelo y así sucesivamente.

El primer método es muy lento, pero cuando implementa un modelo por primera vez, o modifica la precisión del modelo, el tipo de datos de entrada, la estructura de la red, etc., siempre que se modifique el modelo, se debe volver a compilar (en De hecho, hay otro TRT que se puede recargar (el método de parámetros no se trata en este artículo).

Ahora suponga que estamos usando TRT por primera vez, por lo que solo podemos elegir la primera forma de crear un motor. Para crear un motor, necesitamos dos archivos, la estructura del modelo y los parámetros del modelo, y un método para analizar estos dos archivos. En TRT, el motor se compila mediante la IBuilderconducción de un objeto, por lo que necesitamos nuevos IBuilderobjetos clave :

nvinfer1::IBuilder *builder = createInferBuilder(gLogger);

gLoggerEs la interfaz de registro en TRT ILogger, herede esta interfaz y cree su propio objeto de registro para pasar.

Para compilar un motor, builderprimero debe crear un INetworkDefinitioncontenedor como modelo:

nvinfer1::INetworkDefinition *network = builder->createNetwork();

Tenga ennetwork cuenta que en este momento está vacío , necesitamos completar la estructura y los parámetros del modelo, es decir, para resolver nuestra propia estructura de modelo y archivos de parámetros, obtener datos en ellos.

TRT dio oficialmente tres analizadores de formatos de modelo de marco convencionales, a saber:

  • ONNX:IOnnxParser parser = nvonnxparser::createParser(*network, gLogger);
  • Caffe:ICaffeParser parser = nvcaffeparser1::createCaffeParser();
  • UFF:IUffParser parser = nvuffparser::createUffParser();

Entre ellos, UFF es el formato utilizado para TensorFlow. Los archivos correspondientes se pueden analizar llamando a estos tres analizadores. Con ICaffeParser, por ejemplo, llama a su parsemétodo para llenar network.

virtual const IBlobNameToTensor* nvcaffeparser1::ICaffeParser::parse(
    const char* deploy, 
    const char * model, 
	nvinfer1::INetworkDefinition &network, 
	nvinfer1::DataType weightType)

//Parameters
//deploy	    The plain text, prototxt file used to define the network configuration.
//model	        The binaryproto Caffe model that contains the weights associated with the network.
//network	    Network in which the CaffeParser will fill the layers.
//weightType    The type to which the weights will transformed.

De esta forma, puedes conseguir un relleno network, puedes compilar el motor, parece que todo es maravilloso ...

Sin embargo, el TRT real no es perfecto. Por ejemplo, muchas operaciones de TensorFlow no son compatibles, por lo que los archivos que pasas a menudo no se analizan en absoluto (uno de los dilemas más comunes de los marcos de aprendizaje profundo). Por lo tanto, debemos hacer lo que usted complete network, que debe llamarse interfaz de bajo nivel TRT para crear la estructura del modelo, similar a usted o TensorFlow Caffe al hacerlo.

TRT proporciona una interfaz más rica para que pueda crear su propia red directamente a través de estas interfaces, como agregar una capa convolucional:

virtual IConvolutionLayer* nvinfer1::INetworkDefinition::addConvolution(ITensor &input, 
                                                                        int nbOutputMaps,
                                                                        DimsHW kernelSize,
                                                                        Weights kernelWeights,
                                                                        Weights biasWeights)		

// Parameters
// input	The input tensor to the convolution.
// nbOutputMaps	The number of output feature maps for the convolution.
// kernelSize	The HW-dimensions of the convolution kernel.
// kernelWeights	The kernel weights for the convolution.
// biasWeights	The optional bias weights for the convolution.

Los parámetros aquí básicamente tienen significados similares a otros marcos de aprendizaje profundo, y no hay nada de qué hablar. Simplemente encapsule los datos en la estructura de datos en el TRT. Quizás la diferencia entre construir la red de entrenamiento en tiempos de paz es la necesidad de completar los parámetros del modelo, porque TRT es un marco de inferencia y los parámetros son conocidos y determinados. Este proceso generalmente consiste en leer el modelo entrenado, construir el tipo de estructura de datos de TRT y colocarlo en él, lo que significa que debe analizar el archivo de parámetros del modelo usted mismo.

La razón por la que las interfaces de configuración de red TRT son más abundantes , porque incluso con estas interfaces de bajo nivel, muchos todavía no pueden completar la operación, ese no es un add*método correspondiente , sin mencionar que la realidad del negocio también puede involucrar una gran cantidad de funciones personalizadas. capas, por lo tanto, ha habido una interfaz de complemento TRT que le permite definir una add*operación. Su flujo de nvinfer1::IPluginV2interfaces hereda , usa cuda escribe una capa de función autodefinida, luego hereda nvinfer1::IPluginCreatorla preparación de sus clases de creación para anular sus métodos virtuales createPlugin. Se REGISTER_TENSORRT_PLUGINpuede utilizar la última macro de llamada para registrar el complemento. Introducción a las funciones miembro de la interfaz del complemento.

// 获得该自定义层的输出个数,比如 leaky relu 层的输出个数为1
virtual int getNbOutputs() const = 0;

// 得到输出 Tensor 的维数
virtual Dims getOutputDimensions(int index, const Dims* inputs, int nbInputDims) = 0;

// 配置该层的参数。该函数在 initialize() 函数之前被构造器调用。它为该层提供了一个机会,可以根据其权重、尺寸和最大批量大小来做出算法选择。
virtual void configure(const Dims* inputDims, int nbInputs, const Dims* outputDims, int nbOutputs, int maxBatchSize) = 0;

// 对该层进行初始化,在 engine 创建时被调用。
virtual int initialize() = 0;

// 该函数在 engine 被摧毁时被调用
virtual void terminate() = 0;

// 获得该层所需的临时显存大小。
virtual size_t getWorkspaceSize(int maxBatchSize) const = 0;

// 执行该层
virtual int enqueue(int batchSize, const void*const * inputs, void** outputs, void* workspace, cudaStream_t stream) = 0;

// 获得该层进行 serialization 操作所需要的内存大小
virtual size_t getSerializationSize() = 0;

// 序列化该层,根据序列化大小 getSerializationSize(),将该类的参数和额外内存空间全都写入到系列化buffer中。
virtual void serialize(void* buffer) = 0;

Necesitamos reescribir la implementación de todas o parte de las funciones aquí de acuerdo con las funciones de nuestra propia capa. Aquí hay muchos detalles, y no hay forma de expandirlos uno por uno. Cuando necesite personalizarlo, todavía Necesito mirar la API oficial.

Una vez que se crea el modelo de red, se puede compilar el motor y se requieren algunas configuraciones para el motor. Por ejemplo, precisión de cálculo, tamaño de lote admitido, etc., debido a que estas configuraciones son diferentes, el motor compilado también es diferente.

TRT admite el cálculo FP16, que también es la precisión de cálculo recomendada oficialmente, y su configuración es más simple. Llámelo directamente:

builder->setFp16Mode(true);

Además, al configurar la precisión, hay una interfaz para configurar la política estricta:

builder->setStrictTypeConstraints(true);

Esta interfaz es si se debe realizar la conversión de tipo estrictamente de acuerdo con la precisión establecida. Si no se establece la política estricta, el TRT puede elegir un tipo de cálculo de mayor precisión (sin afectar el rendimiento) en algunos cálculos.

Además de la precisión, el tamaño del lote y el tamaño del espacio de trabajo deben configurarse para que se ejecuten:

builder->setMaxBatchSize(batch_size);
builder->setMaxWorkspaceSize(workspace_size);

El tamaño de lote aquí es el tamaño de lote más grande que se puede admitir en tiempo de ejecución, y se puede seleccionar un tamaño de lote más pequeño que este valor en tiempo de ejecución, y el espacio de trabajo también se establece en relación con este tamaño de lote máximo.

Después de configurar los parámetros anteriores, puede compilar el motor.

nvinfer1::ICudaEngine *engine = builder->buildCudaEngine(*network);

La compilación lleva mucho tiempo, espera con paciencia.

Serialización y deserialización del motor

La compilación del motor lleva mucho tiempo. Cuando el modelo, la precisión del cálculo, el tamaño del lote, etc. permanecen sin cambios, podemos optar por guardar el motor localmente para la siguiente ejecución, es decir, la serialización del motor. TRT proporciona un método de serialización conveniente:

nvinfer1::IHostMemory *modelStream = engine->serialize();

A través de esta llamada, lo que se obtiene es un flujo binario, que se puede guardar escribiendo este flujo en un archivo.

Si necesita implementar nuevamente, puede deserializar directamente el archivo guardado y omitir el paso de compilación.

IRuntime* runtime = createInferRuntime(gLogger);
ICudaEngine* engine = runtime->deserializeCudaEngine(modelData, modelSize, nullptr);

Utilice el motor para hacer predicciones

Una vez que tenga el motor, puede usarlo para hacer inferencias.

Primero crea un contexto de inferencia. Este contexto es similar a un espacio de nombres y se usa para almacenar variables de una tarea de inferencia.

IExecutionContext *context = engine->createExecutionContext();

Un motor puede tener varios contextos , lo que significa que un motor puede realizar varias tareas de predicción al mismo tiempo.

Luego está el índice de entrada y salida vinculantes. La razón de este paso es que en el proceso de construcción del motor, TRT mapea la entrada y la salida a la secuencia del número de índice, por lo que solo podemos obtener la información de la capa de entrada y salida a través del número de índice. Aunque TRT proporciona una interfaz para obtener números de índice por nombre, el almacenamiento local puede facilitar las operaciones posteriores.

Primero podemos obtener el número de números de índice:

int index_number = engine->getNbBindings();

Podemos juzgar si el número de números es igual a la suma de la entrada y la salida de nuestra red. Por ejemplo, si tiene una entrada y una salida, entonces el número de números es 2. Si no es así, significa que hay un problema con el motor; si no hay problema, podemos obtener el número de serie correspondiente a la entrada y salida por nombre:

int input_index = engine->getBindingIndex(input_layer_name);
int output_index = engine->getBindingIndex(output_layer_name);

Para una red de entrada y salida común, el número de índice de entrada es 0 y el número de índice de salida es 1, por lo que este paso no es necesario.

A continuación, debe asignar espacio de memoria para las capas de entrada y salida. Para asignar espacio de memoria de video, necesitamos conocer la información dimensional de entrada y salida y el tipo de datos almacenados. La representación de la información dimensional y el tipo de datos en TRT es la siguiente:

class Dims
{
public:
    static const int MAX_DIMS = 8; //!< The maximum number of dimensions supported for a tensor.
    int nbDims;                    //!< The number of dimensions.
    int d[MAX_DIMS];               //!< The extent of each dimension.
    DimensionType type[MAX_DIMS];  //!< The type of each dimension.
};

enum class DataType : int
{
    kFLOAT = 0, //!< FP32 format.
    kHALF = 1,  //!< FP16 format.
    kINT8 = 2,  //!< quantized INT8 format.
    kINT32 = 3  //!< INT32 format.
};

Obtenemos la dimensión de datos (atenuaciones) y el tipo de datos (dtype) de la entrada y salida a través del número de índice, y luego abrimos espacio de memoria para cada capa de salida para almacenar el resultado de salida:

for (int i = 0; i < index_number; ++i)
{
	nvinfer1::Dims dims = engine->getBindingDimensions(i);
	nvinfer1::DataType dtype = engine->getBindingDataType(i);
    // 获取数据长度
    auto buff_len = std::accumulate(dims.d, dims.d + dims.nbDims, 1, std::multiplies<int64_t>());
    // ...
    // 获取数据类型大小
    dtype_size = getTypeSize(dtype);	// 自定义函数
}

// 为 output 分配显存空间
for (auto &output_i : outputs)
{
    cudaMalloc(buffer_len_i * dtype_size_i * batch_size);
}

Lo que ofrece este artículo es un pseudocódigo, que solo representa la lógica, por lo que se incluirán algunas funciones personalizadas simples.

En este punto, hemos hecho los preparativos y ahora podemos poner datos en el modelo para razonar.

Predicción hacia adelante

La ejecución de predicción directa de TRT es asincrónica y el contexto envía tareas a través de una llamada en cola:

cudaStream_t stream;
cudaStreamCreate(&stream);
context->enqueue(batch_size, buffers, stream, nullptr);
cudaStreamSynchronize(stream);

Enqueue es una función de TRT que realmente ejecuta tareas.También necesitamos implementar esta interfaz de función al escribir el complemento. entre ellos:

  • batch_size: Motor pasado durante el proceso de compilación max_batch_size.

  • buffers: Es una matriz de punteros y su subíndice corresponde al número de índice de la capa de entrada y salida Almacena el puntero de datos de entrada y la dirección de almacenamiento de datos de salida (es decir, la dirección de memoria de video abierta).

  • stream: Stream es el concepto de una serie de operaciones secuenciales en cuda. Para nuestro modelo, todas las operaciones del modelo se ejecutan en el equipo especificado en el orden especificado por la (estructura de red).

    La secuencia CUDA se refiere a un grupo de operaciones CUDA asincrónicas, que se ejecutan en el dispositivo en el orden en que se llama al código de host. Stream mantiene la secuencia de estas operaciones y permite que estas operaciones entren en la cola de trabajo después de que se complete todo el preprocesamiento, y también puede realizar algunas operaciones de consulta en estas operaciones. Estas operaciones incluyen la transferencia de datos del host al dispositivo, el lanzamiento del kernel y otras acciones de inicio del host ejecutadas por el dispositivo. La ejecución de estas operaciones siempre es asincrónica y el tiempo de ejecución de cuda determinará el momento adecuado de estas operaciones. Podemos utilizar la cuda api correspondiente para asegurarnos de que los resultados obtenidos se obtienen una vez finalizadas todas las operaciones. Las operaciones en la misma secuencia tienen un orden de ejecución estricto , pero las diferentes secuencias no tienen tal restricción.

Cabe señalar que los búferes de datos de entrada y salida en la matriz están en la GPU, por cudaMemcpy(es necesario abrir una memoria para almacenar por adelantado) en los datos de entrada a la GPU copia la CPU. De la misma manera, los datos de salida también deben copiarse de la GPU a la CPU.

Las dos primeras oraciones crean un flujo cuda, y la última oración es esperar a que se complete el flujo asíncrono y luego copiar los datos de la memoria de video.

En este punto, hemos completado un proceso básico de pronóstico de TRT.

para resumir

Este artículo solo describe el proceso de predicción TRT y algunas llamadas comunes, y no involucra redes específicas e implementaciones específicas, y no hay demasiados detalles de codificación. Las diferentes operaciones en diferentes redes requieren la escritura de algunos complementos de extensión, y la codificación, incluido el desarrollo y la administración de la memoria y la memoria de video, y la deconstrucción y limpieza de TRT, etc., están más allá del alcance de este artículo.

Supongo que te gusta

Origin blog.csdn.net/ahelloyou/article/details/114870232
Recomendado
Clasificación