Cuantización de modelos: Motores de inferencia de alto rendimiento con NVIDIA TensorRT

Imagen decorativa de la serie de cuantización
Imagen decorativa de la serie de cuantización
  • Exportar un checkpoint de CLIP cuantizado en FP8 con NVIDIA ModelOpt al formato ONNX, y luego compilarlo en un motor de TensorRT, reduce significativamente el tamaño del modelo y su huella en disco comparado con FP16: los archivos ONNX del codificador de texto se reducen en un 34% y el codificador de imágenes en un 50%, con reducciones similares en el tamaño del motor y el uso de VRAM.
  • El proceso de creación de motores de NVIDIA TensorRT fusiona los nodos QuantizeLinear/DequantizeLinear (Q/DQ) en capas adyacentes, permitiendo la ejecución directa en núcleos FP8 Tensor Cores y kernels especializados; esto genera un mayor throughput computacional y un menor uso de ancho de banda de memoria, acelerando particularmente las capas GEMM y MHA en GPUs con arquitectura Ada.
  • Las pruebas de rendimiento en una GPU NVIDIA RTX 6000 Ada y el perfilado con Nsight Deep Learning Designer demuestran que la cuantización FP8 proporciona una aceleración de 1.39x a 1.45x en la latencia de inferencia para codificadores CLIP frente a FP16, atribuyendo la mayoría de las ganancias a la ejecución optimizada de kernels de matmul y atención en FP8.

El contenido generado por IA puede resumir información de manera incompleta. Verifique la información importante. Más información

Convertir un checkpoint cuantizado en un motor de NVIDIA TensorRT cierra la brecha entre la optimización del modelo y el despliegue en producción, permitiendo una inferencia más rápida, mayor throughput y una utilización más eficiente de la GPU a gran escala.

En una publicación anterior, generamos un checkpoint de Contrastive Language-Image Pretraining (CLIP) cuantizado en FP8 de alta calidad con el optimizador de modelos de NVIDIA TensorRT.

Esta publicación continúa donde lo dejamos, recorriendo cómo exportar el checkpoint a ONNX y compilarlo en un motor de NVIDIA TensorRT listo para la inferencia en producción. También perfilamos el motor FP8 de TensorRT resultante frente a la línea base FP16 para medir la aceleración real que entrega el modelo cuantizado.

La Figura 1 muestra las cinco etapas de un flujo de trabajo de cuantización típico de extremo a extremo. Este es el pipeline estándar para desplegar un modelo CLIP cuantizado. Los LLM cuantizados siguen un camino diferente a través de TensorRT-LLM, lo cual se detalla en este tutorial.

Figura 1. Flujo de trabajo de cuantización y despliegue con ModelOpt y TensorRT
Figura 1. Flujo de trabajo de cuantización y despliegue con ModelOpt y TensorRT

Exportar modelo al formato ONNX

El primer paso es exportar el checkpoint de ModelOpt a ONNX. El siguiente pseudocódigo realiza esto para el checkpoint CLIP cuantizado en FP8 utilizando un asistente integrado de ModelOpt (la exportación apunta a ONNX opset 20+, donde QuantizeLinear/DequantizeLinear de FP8 es totalmente soportado). Este proceso pliega cada par de quantize-then-dequantize (Q-DQ) del lado de los pesos en una cadena solo de DQ almacenada en FP8, reduciendo notablemente el archivo ONNX.

En principio, el uso de torch.onnx.export nativo también funciona, pero requiere escribir un script de conversión personalizado.

Código
import torch
from transformers import CLIPModel, CLIPTokenizer
from transformers.models.clip.modeling_clip import CLIPAttention
import modelopt.torch.opt as mto
import modelopt.torch.quantization as mtq
from modelopt.torch._deploy.utils import OnnxBytes, get_onnx_bytes_and_metadata
from modelopt.torch.quantization.plugins.diffusion.diffusers import _QuantAttention

class TextEncoder(torch.nn.Module):
    def __init__(self, m):
        super().__init__(); self.m = m
    def forward(self, x):
        return self.m.get_text_features(x)

class ImageEncoder(torch.nn.Module):
    def __init__(self, m):
        super().__init__(); self.m = m
    def forward(self, x): 
        return self.m.get_image_features(x)

def prepare_for_fp8_onnx_export(model):
    for _, mod in model.named_modules():
        if isinstance(mod, _QuantAttention):
            mod._disable_fp8_mha = False
        if isinstance(mod, CLIPAttention) and getattr(mod, "scale", None) is not None:
            mod.scale = None

def export(wrapper, dummy, axis_name, out_name):
    onnx_bytes, _ = get_onnx_bytes_and_metadata(
        model=wrapper, dummy_input=(dummy,), model_name=out_name,
        dynamic_axes={axis_name: {0: "batch"}}, onnx_opset=20, weights_dtype="fp16",
    )
    OnnxBytes.from_bytes(onnx_bytes).write_to_disk("./onnx_output", clean_dir=False)

mto.enable_huggingface_checkpointing()
mtq.QuantModuleRegistry.register({CLIPAttention: "CLIPAttention"})(_QuantAttention)
model = (
    CLIPModel.from_pretrained(modelopt_ckpt, attn_implementation="sdpa", torch_dtype=torch.float16)
    .eval().cuda()
)
prepare_for_fp8_onnx_export(model)

tok = CLIPTokenizer.from_pretrained(model_ckpt)
dummy_text = tok(["a photo of a cat"], return_tensors="pt", padding="max_length", max_length=77)["input_ids"].cuda()
export(TextEncoder(model), dummy_text, "text_input", "text_clip_fp8")

dummy_image = torch.randn(16, 3, 224, 224, dtype=torch.float16).cuda()
export(ImageEncoder(model), dummy_image, "image_input", "image_clip_fp8")

La Tabla 1 compara los tamaños de archivo ONNX de la exportación del checkpoint FP8 de ModelOpt frente a la exportación original del checkpoint FP16 de HuggingFace. La exportación del checkpoint FP8 produce archivos ONNX notablemente más pequeños, aproximadamente un 34% menores para el codificador de texto y un 50% menores para el codificador de imágenes.

Es importante notar que reducir el archivo ONNX es una conveniencia, no un requisito. TensorRT pliega el nodo Q del lado de los pesos en el peso FP8 durante el tiempo de construcción del motor. El exportador ONNX de ModelOpt lo hace antes en el lado de ONNX para mantener el archivo en disco más pequeño.

Podemos inspeccionar el archivo ONNX exportado con NVIDIA Nsight Deep Learning Designer, una herramienta eficiente para la edición de modelos ONNX, perfilado de rendimiento y construcción de motores TensorRT.

La Figura 2 muestra una parte del grafo ONNX exportado visualizado en Nsight Deep Learning Designer. Podemos ver que el grafo ahora contiene nodos QuantizeLinear/DequantizeLinear (Q/DQ), marcando los límites de FP8.

Figura 2. Nodos Q/DQ en el grafo ONNX FP8 alrededor de un MatMul de atención
Figura 2. Nodos Q/DQ en el grafo ONNX FP8 alrededor de un MatMul de atención

Durante la construcción del motor, TensorRT fusiona estos nodos con las capas adyacentes para optimizar el rendimiento de la inferencia. Esta fusión elimina las transiciones innecesarias de quantize-then-dequantize, permitiendo el uso de kernels FP8 optimizados para el cómputo.

Perfilado del modelo ONNX con TensorRT

Con el modelo ONNX FP8 exportado, el siguiente paso es pasarlo a TensorRT y medir qué tan rápido se ejecuta. Antes de comenzar, asegúrese de que TensorRT esté correctamente descargado e instalado siguiendo este tutorial. Una vez listo, utilizaremos trtexec (wrapper de línea de comandos de TensorRT) para realizar el benchmark del modelo ONNX con el siguiente comando:

Código
# Configurar el entorno de TensorRT
export PATH=<TensorRT-${version}/bin>:$PATH
export LD_LIBRARY_PATH=<TensorRT-${version}/lib>:$LD_LIBRARY_PATH

# Benchmark del modelo ONNX con trtexec
trtexec --onnx=text_clip_fp8.onnx \
        --shapes=text_input:128x77 \
        --stronglyTyped \
        --saveEngine=text_clip_fp8.plan

Vía NVIDIA Developer Blog.