Tutorial de introducción a Nvidia Triton

1 preliminares relevantes

  • Modelo : una red (parámetros + estructura) que contiene una gran cantidad de parámetros, cuyo tamaño varía entre 10 MB y 10 GB.
  • Formato del modelo : El mismo modelo puede tener diferentes formatos de almacenamiento (similares a archivos de audio y video), actualmente los principales son torch, tf, onnx y trt, y tf incluye tres formatos.
  • Inferencia del modelo : se realizan varias operaciones en la entrada y los parámetros de la red para obtener una salida, una tarea computacionalmente intensiva que requiere aceleración de GPU.
  • Motor de razonamiento de modelos : una herramienta de razonamiento de modelos que puede hacer que el razonamiento de modelos sea más rápido. El uso de esta herramienta a menudo requiere un formato de modelo específico. Actualmente, los motores de razonamiento convencionales incluyen trt y ort.
  • Marco de razonamiento del modelo : El proceso de razonamiento del modelo está encapsulado para que sea más conveniente agregar, eliminar y reemplazar modelos; los marcos más avanzados también tienen funciones como equilibrio de carga, monitoreo de modelos y generación automática de interfaces grpc y http, que son para el despliegue nacido. 

El tritón que se presentará a continuación es un excelente marco de razonamiento modelo en la actualidad .

tritón de 2 entradas

A continuación, te enseñaré cómo ejecutar el tritón paso a paso, para que puedas entender lo que hace el tritón.

2.1 Registrar plataforma NGC

NGC puede entenderse como un almacén de software oficial de NV, que contiene una gran cantidad de software compilado, imágenes de Docker, etc. Necesitamos registrar NGC y generar la clave API correspondiente, que se utiliza para iniciar sesión en NGC en la ventana acoplable y descargar la imagen que contiene.

La dirección del sitio web oficial de NGC es:  https://ngc.nvidia.com

Después de registrarse e iniciar sesión, puede generar claves API en la interfaz de usuario (perfil UI). Esta clave le permite iniciar sesión en NGC en Docker u otros lugares apropiados y descargar recursos como imágenes de Docker desde este repositorio.

2.2 Iniciar sesión

Entrada CLI  docker login nvcr.io

Luego ingrese el nombre de usuario y la clave que generó en el paso anterior, el nombre de usuario es $oauthtoken , no olvide el símbolo $, no use su propio nombre de usuario.
Finalmente, aparecerán las palabras Inicio de sesión exitoso, lo que significa que el inicio de sesión fue exitoso.

2.3 Tirar la imagen

docker pull nvcr.io/nvidia/tritonserver:22.04-py3

También puedes optar por tirar de otras versiones de tritón. El tamaño de la imagen es de aproximadamente varios gigabytes, por lo que debe esperar pacientemente. Esta imagen no distingue entre gpu y cpu y es universal.

2.4 Construir el directorio del modelo

Ejecute el comando mkdir -p /home/triton/model_repository/fc_model_pt/1.
Entre ellos /home/triton/model_repositoryse encuentra su repositorio de modelos, todos los modelos están en este directorio de modelos. Cuando se inicia el contenedor, se asignará a la carpeta en el contenedor.fc_model_pt /modelpuede entenderse como el directorio de almacenamiento de un determinado modelo, como un modelo para la clasificación de sentimientos, el nombre no es necesario, es mejor ver el nombre, 1 significa La versión es 1.
La estructura de directorios del almacén de modelos es la siguiente:

  <model-repository-path>/# 模型仓库目录<model-name>/ # 模型名字[config.pbtxt] # 模型配置文件[<output-labels-file> ...] # 标签文件,可以没有<version>/ # 该版本下的模型<model-definition-file><version>/<model-definition-file>...<model-name>/[config.pbtxt][<output-labels-file> ...]<version>/<model-definition-file><version>/<model-definition-file>......

2.5 Generar un modelo de antorcha para razonar

Cree un modelo de antorcha y guárdelo con torchscript:

import torch
import torch.nn as nn


class SimpleModel(nn.Module):
    def __init__(self):
        super(SimpleModel, self).__init__()
        self.embedding = nn.Embedding(100, 8)
        self.fc = nn.Linear(8, 4)
        self.fc_list = nn.Sequential(*[nn.Linear(8, 8) for _ in range(4)])

    def forward(self, input_ids):
        word_emb = self.embedding(input_ids)
        output1 = self.fc(word_emb)
        output2 = self.fc_list(word_emb)

        return output1, output2


if __name__ == "__main__":
    model = SimpleModel()
    ipt = torch.tensor([[1, 2, 3], [4, 5, 6]], dtype=torch.long)
    
    script_model = torch.jit.trace(model, ipt, strict=True)
    torch.jit.save(script_model, "model.pt")

// 这段代码定义了一个简单的 PyTorch 模型,然后使用一个输入 Tensor 来在严格模式下 Trace 该模型,并将 Trace 后的模型保存为 "model.pt" 文件。

Después de generar el modelo, cópielo en el directorio que acaba de crear, tenga en cuenta que debe colocarse en el directorio correspondiente al número de versión y el nombre del archivo del modelo debe ser model.pt.

Triton admite muchos formatos de modelos; este es solo un ejemplo de antorcha.

2.6 Escribir archivos de configuración

Para que el marco reconozca el modelo que acaba de colocar, necesitamos escribir un archivo de configuración config.pbtxt, el contenido específico es el siguiente:

name: "fc_model_pt" # 模型名,也是目录名
platform: "pytorch_libtorch" # 模型对应的平台,本次使用的是torch,不同格式的对应的平台可以在官方文档找到
max_batch_size : 64 # 一次送入模型的最大bsz,防止oom
input [{name: "input__0" # 输入名字,对于torch来说名字于代码的名字不需要对应,但必须是<name>__<index>的形式,注意是2个下划线,写错就报错data_type: TYPE_INT64 # 类型,torch.long对应的就是int64,不同语言的tensor类型与triton类型的对应关系可以在官方文档找到dims: [ -1 ]  # -1 代表是可变维度,虽然输入是二维的,但是默认第一个是bsz,所以只需要写后面的维度就行(无法理解的操作,如果是[-1,-1]调用模型就报错)}
]
output [{name: "output__0" # 命名规范同输入data_type: TYPE_FP32dims: [ -1, -1, 4 ]},{name: "output__1"data_type: TYPE_FP32dims: [ -1, -1, 8 ]}
]

Se estima que este archivo de configuración del modelo es la parte más complicada de todo Tritón. Se estima que la mayor parte del trabajo del modelo en línea consiste en escribir el archivo de configuración. Es difícil para mí explicarlo claramente en unas pocas palabras. Puedo Solo le doy una breve introducción, para más detalles, consulte el documento oficial. Tenga en cuenta que el archivo de configuración no debe colocarse en el directorio del número de versión, sino en el directorio del modelo, es decir, config.pbtxt y el directorio del número de versión están en el mismo nivel.

La descripción oficial de la forma de entrada predeterminada es bsz:

Como se analizó anteriormente, Triton supone que el procesamiento por lotes se produce a lo largo de la primera dimensión que no figura en las atenuaciones del tensor de entrada o salida. Sin embargo, para los tensores de forma, el procesamiento por lotes se produce en el primer valor de forma. Para el ejemplo anterior, una solicitud de inferencia debe proporcionar entradas con las siguientes formas.

2.7 Crear un contenedor e iniciarlo

Ejecutando una orden:

docker run --rm -p8000:8000 -p8001:8001 -p8002:8002 \-v /home/triton/model_repository/:/models \nvcr.io/nvidia/tritonserver:22.04-py3 \tritonserver \--model-repository=/models 

Si su sistema tiene una GPU disponible, puede agregar los siguientes parámetros  --gpus=1para permitir que el marco de inferencia utilice la aceleración de GPU. Este parámetro debe colocarse después de la ejecución.

2.8 Interfaz de prueba

Si sigues mi tutorial paso a paso, definitivamente podrás iniciar el contenedor con éxito. A continuación, podemos escribir un fragmento de código para probar si la interfaz está conectada. La dirección de llamada es: el código de prueba es el siguiente
http:\\localhost:8000/v2/models/{model_name}/versions/{version}/infer
:

import requests

if __name__ == "__main__":
    request_data = {
        "inputs": [
            {
                "name": "input__0",
                "shape": [2, 3],
                "datatype": "INT64",
                "data": [[1, 2, 3], [4,5,6]]
            }
        ],
        "outputs": [
            {"name": "output__0"}, 
            {"name": "output__1"}
        ]
    }
    
    res = requests.post(
        url="http://localhost:8000/v2/models/fc_model_pt/versions/1/infer",
        json=request_data
    ).json()
    
    print(res)


// 这段代码使用 Python 的 requests 库向本地运行的服务器发送 POST 请求。服务器托管了一个模型,即 fc_model_pt 。POST 请求包含输入数据和所期望的输出数据格式,并打印服务器的响应结果。

Después de ejecutar el código, obtendrá el resultado correspondiente:

{'model_name': 'fc_model_pt','model_version': '1','outputs': [{'name': 'output__0','datatype': 'FP32','shape': [2, 3, 4],'data': [1.152763843536377, 1.1349767446517944, -0.6294105648994446, 0.8846281170845032, 0.059508904814720154, -0.06066855788230896, -1.497096061706543, -1.192716121673584, 0.7339693307876587, 0.28189709782600403, 0.3425392210483551, 0.08894850313663483, 0.48277992010116577, 0.9581012725830078, 0.49371692538261414, -1.0144696235656738, -0.03292369842529297, 0.3465275764465332, -0.5444514751434326, -0.6578375697135925, 1.1234807968139648, 1.1258794069290161, -0.24797165393829346, 0.4530307352542877]}, {'name': 'output__1','datatype': 'FP32','shape': [2, 3, 8],'data': [-0.28994596004486084, 0.0626179575920105, -0.018645435571670532, -0.3376324474811554, -0.35003775358200073, 0.2884367108345032, -0.2418503761291504, -0.5449661016464233, -0.48939061164855957, -0.482677698135376, -0.27752232551574707, -0.26671940088272095, -0.2171783447265625, 0.025355860590934753, -0.3266356587409973, -0.06301657110452652, -0.1746724545955658, -0.23552510142326355, 0.10773542523384094, -0.4255935847759247, -0.47757795453071594, 0.4816707670688629, -0.16988427937030792, -0.35756853222846985, -0.06549499928951263, -0.04733048379421234, -0.035484105348587036, -0.4210450053215027, -0.07763291895389557, 0.2223128080368042, -0.23027443885803223, -0.4195460081100464, -0.21789231896400452, -0.19235755503177643, -0.16810789704322815, -0.34017443656921387, -0.05121977627277374, 0.08258339017629623, -0.2775516211986542, -0.27720844745635986, -0.25765007734298706, -0.014576494693756104, 0.0661710798740387, -0.38623639941215515, -0.45036202669143677, 0.3960753381252289, -0.20757021009922028, -0.511818528175354]}]
}

No sé si hay algún problema con mi uso. Desde la perspectiva de la experiencia del usuario, esta interfaz de razonamiento me resulta algo incómoda:

  1. Obviamente, el tipo de datos se especifica en config.pbtxt, pero debe especificarse al ingresar, si no se especifica, se informará un error.
  2. También es necesario especificar la forma de entrada; de lo contrario, se informará un error.
  3. El valor del tipo de datos es inconsistente con el de config.pbtxt. Si el tipo de datos se establece en TYPE_INT64, se informará un error y debe ser INT64.
  4. Los datos de salida son una matriz unidimensional, que debe remodelarse automáticamente en una matriz correspondiente de acuerdo con la forma devuelta.

Además de escribir directamente llamadas de código como yo, también puedes usar la biblioteca oficial que proporcionan pip install tritonclient[http], la dirección es la siguiente: https://github.com/triton-inference-server/client.

3 Uso de las funciones avanzadas de triton

El tutorial de la sección anterior solo utilizó las funciones básicas de Triton, por lo que solo se puede decir que el rango es Oro. A continuación se presentan algunas características avanzadas de Triton.

3.1 Paralelismo del modelo

El paralelismo de modelos puede referirse al lanzamiento de múltiples modelos o múltiples instancias de un solo modelo al mismo tiempo. No es complicado de implementar, basta con modificar los parámetros de configuración. De forma predeterminada, Triton implementará una instancia del modelo en cada GPU disponible para lograr el paralelismo.
A continuación, probaré una variedad de situaciones para informarle el efecto del paralelismo del modelo. Mi configuración es 2 piezas de 3060 (alquiladas) para probar múltiples modelos.
Usando el comando de prueba de presión ab -k -c 5 -n 500 -p ipt.json http://localhost:8000/v2/models/fc_model_pt/versions/1/infer
Este comando significa que 5 procesos llaman repetidamente a la interfaz 500 veces.
La configuración de prueba y el QPS correspondiente son los siguientes:

  • 1 tarjeta en total; cada tarjeta ejecuta 1 instancia: QPS es 603
  • 2 tarjetas en total; cada tarjeta ejecuta 1 instancia: QPS es 1115
  • 2 tarjetas en total; cada tarjeta ejecuta 2 instancias: QPS es 1453
  • 2 tarjetas en total; cada tarjeta ejecuta 2 instancias; coloque 2 instancias en la CPU al mismo tiempo: QPS es 972

Las conclusiones son las siguientes: se mejora el rendimiento de varias tarjetas; varias instancias pueden mejorar aún más las capacidades de concurrencia; agregar CPU reducirá la velocidad, principalmente porque la velocidad de la CPU es demasiado lenta.

El siguiente es el elemento de configuración correspondiente a la prueba anterior, simplemente cópielo y colóquelo en config.pbtxt.

#共2个卡;每个卡运行2个实例
instance_group [
{count: 2kind: KIND_GPUgpus: [ 0 ]
},
{count: 2kind: KIND_GPUgpus: [ 1 ]
}
]
# 共2个卡;每个卡运行2个实例;同时在CPU上放2个实例
instance_group [
{count: 2kind: KIND_GPUgpus: [ 0 ]
},
{count: 2kind: KIND_GPUgpus: [ 1 ]
},
{count: 2kind: KIND_CPU
}
]

--gpusEn cuanto a cuántas tarjetas utilizar, lo especifica el

3.2 Lote dinámico

Lote dinámico significa que para una solicitud, no inferir primero, esperar unos milisegundos y unir todas las solicitudes en estos milisegundos en un lote para inferir, de modo que pueda aprovechar al máximo el hardware y mejorar el paralelismo. la desventaja es que los usuarios individuales El tiempo de espera se vuelve más largo, lo que no es adecuado para escenarios de solicitudes de baja frecuencia. Es muy simple usar lotes dinámicos. Solo necesita agregarlo en config.pbtxt  dynamic_batching { }. Puede consultar el documento para obtener detalles de parámetros específicos. El límite superior de bsz es el límite superior de mi método de escritura simple. max_batch_sizeEl resultado de mi La prueba de presión mejora aproximadamente el 50% del QPS. De todos modos, es solo que funciona.

PD: este método de optimización es simplemente hacer trampa para las pruebas de estrés. . .

3.3 Servidor personalizado

El llamado backend personalizado consiste en escribir el proceso de razonamiento usted mismo. Normalmente, todo el proceso de razonamiento se resuelve directamente a través del modelo, pero algunos procesos de razonamiento también incluyen algo de lógica empresarial. Por ejemplo, todo el proceso de razonamiento requiere 2 modelos, de donde el primero La salida del primer modelo se puede utilizar como entrada del segundo modelo después de hacer algunos juicios lógicos y luego modificar la salida. La forma más sencilla es llamar al servicio Triton dos veces, primero llamar al primer modelo para obtener la salida. Luego haga juicios y modificaciones de la lógica de negocios y luego llame al segundo modelo. Sin embargo, en Triton, podemos personalizar un backend para escribir todo el proceso de llamada en él, lo que simplifica el proceso de llamada y evita parte del retraso en la transmisión HTTP.
El ejemplo que di es en realidad una canalización que incluye lógica de negocios. Este enfoque se llama BLS (Business Logic Scripting).

También es muy simple implementar un backend personalizado. Es básicamente el mismo que el proceso de poner el modelo de antorcha mencionado anteriormente. Primero, cree una carpeta de modelo, luego cree una nueva en la carpeta, luego cree una nueva carpeta de versión. y luego póngalo. Este config.pbtxtarchivo py model.pyescribió el proceso de razonamiento. Para ilustrar la estructura del directorio, imprimiré el árbol de directorios del almacén modelo construido y lo mostraré:

model_repository/  # 模型仓库|-- custom_model # 我们的自定义backend模型目录|   |-- 1 # 版本|   |   |-- model.py # 模型Py文件,里面主要是推理的逻辑|   `-- config.pbtxt # 配置文件`-- fc_model_pt # 上一小节介绍的模型|-- 1|   `-- model.pt`-- config.pbtxt

Si comprende la sección anterior, encontrará que la configuración del directorio del modelo del backend personalizado es la misma que la configuración del directorio normal, la única diferencia es que el archivo del modelo cambia del peso de la red al código escrito por usted mismo. Hablemos del contenido de los archivos config.pbtxt y model.py, puede copiar y pegar directamente:

import json
import numpy as np
import triton_python_backend_utils as pb_utils


class TritonPythonModel:
    """
    Your Python model must use the same class name. Every Python model
    that is created must have "TritonPythonModel" as the class name.
    """

    def initialize(self, args):
        """
        `initialize` is called only once when the model is being loaded.
        Implementing `initialize` function is optional. This function allows
        the model to intialize any state associated with this model.

        Parameters
        ----------
        args : dict
            Both keys and values are strings. The dictionary keys and values are:
            * model_config: A JSON string containing the model configuration
            * model_instance_kind: A string containing model instance kind
            * model_instance_device_id: A string containing model instance device ID
            * model_repository: Model repository path
            * model_version: Model version
            * model_name: Model name
        """
        # You must parse model_config. JSON string is not parsed here
        self.model_config = model_config = json.loads(args['model_config'])

        # Get output__0 configuration
        output0_config = pb_utils.get_output_config_by_name(model_config, "output__0")

        # Get output__1 configuration
        output1_config = pb_utils.get_output_config_by_name(model_config, "output__1")

        # Convert Triton types to numpy types
        self.output0_dtype = pb_utils.triton_string_to_numpy(output0_config['data_type'])
        self.output1_dtype = pb_utils.triton_string_to_numpy(output1_config['data_type'])

    def execute(self, requests):
        """
        requests : list
            A list of pb_utils.InferenceRequest

        Returns
        -------
        list
            A list of pb_utils.InferenceResponse. The length of this list must
            be the same as `requests`
        """
        output0_dtype = self.output0_dtype
        output1_dtype = self.output1_dtype

        responses = []

        # Every Python backend must iterate over everyone of the requests
        # and create a pb_utils.InferenceResponse for each of them.
        for request in requests:
            # 获取请求数据
            in_0 = pb_utils.get_input_tensor_by_name(request, "input__0")

            # 第一个输出结果自己随便造一个假的,就假装是有逻辑了
            out_0 = np.array([1, 2, 3, 4, 5, 6, 7, 8])  # 为演示方便先写死
            out_tensor_0 = pb_utils.Tensor("output__0", out_0.astype(output0_dtype))

            # 第二个输出结果调用fc_model_pt获取结果
            inference_request = pb_utils.InferenceRequest(
                model_name='fc_model_pt',
                requested_output_names=['output__0', 'output__1'],
                inputs=[in_0]
            )
            inference_response = inference_request.exec()
            out_tensor_1 = pb_utils.get_output_tensor_by_name(inference_response, 'output__1')

            inference_response = pb_utils.InferenceResponse(output_tensors=[out_tensor_0, out_tensor_1])
            responses.append(inference_response)

        return responses

    def finalize(self):
        """
        `finalize` is called only once when the model is being unloaded.
        Implementing `finalize` function is OPTIONAL. This function allows
        the model to perform any necessary clean ups before exit.
        """
        print('Cleaning up...')

// 这段代码是一个 Python 版本的 Triton 后端模型的基本框架。模型的 initialize 方法在加载模型时被调用一次,execute 方法在收到推理请求时被调用,并返回服务器的响应,finalize 在模型被卸载时调用,可选的实现清理工作。

El contenido del archivo de config.pbtxt:

name: "custom_model"
backend: "python"
input [{name: "input__0"data_type: TYPE_INT64dims: [ -1, -1 ]}
]
output [{name: "output__0" data_type: TYPE_FP32dims: [ -1, -1, 4 ]},{name: "output__1"data_type: TYPE_FP32dims: [ -1, -1, 8 ]}
]

Una vez completado el trabajo anterior, puede iniciar el programa para ver los resultados de la ejecución y puede copiar directamente mi código para probarlo:

import requests

if __name__ == "__main__":
    request_data = {
        "inputs": [
            {
                "name": "input__0",
                "shape": [1, 2],
                "datatype": "INT64",
                "data": [[1, 2]]
            }
        ],
        "outputs": [
            {"name": "output__0"}, 
            {"name": "output__1"}
        ]
    }
    
    res = requests.post(
        url="http://localhost:8000/v2/models/fc_model_pt/versions/1/infer",
        json=request_data
    ).json()
    print(res)
    
    res = requests.post(
        url="http://localhost:8000/v2/models/custom_model/versions/1/infer",
        json=request_data
    ).json()
    print(res)


// 这段代码使用 Python 的requests库向本地运行的服务器发送 POST 请求。服务器托管了两个模型,一个是fc_model_pt,另一个是custom_model。POST 请求包含输入数据和期望的输出数据格式。然后,将服务器的响应结果打印出来。

Como resultado de la operación, se puede encontrar que en la salida de los dos tiempos, la salida __0 es diferente, pero la salida __1 es la misma, esto está relacionado con la lógica del model.py que escribimos, por lo que no lo explicaré. aquí.

PD: el backend personalizado evita el retraso en la transmisión causado por llamar repetidamente al modelo NLG para su generación

Supongo que te gusta

Origin blog.csdn.net/JineD/article/details/132092086
Recomendado
Clasificación