Ir al contenido

Preprocesamiento GPU con NVIDIA

Introducción

Al implementar Ultralytics YOLO en producción, el preprocesamiento suele convertirse en el cuello de botella. Mientras que TensorRT puede ejecutar la inferencia del modelo en tan solo unos milisegundos, el preprocesamiento CPU(redimensionar, rellenar, normalizar) puede tardar entre 2 y 10 ms por imagen, especialmente a altas resoluciones. NVIDIA (Data Loading Library) resuelve esto trasladando todo el proceso de preprocesamiento a la GPU.

Esta guía te explica paso a paso cómo crear flujos de trabajo DALI que reproduzcan exactamenteYOLO Ultralytics YOLO , integrándolos con model.predict(), el procesamiento de flujos de vídeo y la implementación de extremo a extremo con Servidor de Inferencia Triton.

¿A quién va dirigida esta guía?

Esta guía está dirigida a ingenieros que implementan YOLO en entornos de producción en los que CPU supone un cuello de botella quantificado —por lo general TensorRT implementaciones en NVIDIA , flujos de trabajo de vídeo de alto rendimiento o Servidor de Inferencia Triton configuraciones. Si estás ejecutando una inferencia estándar con model.predict() y, si no hay un cuello de botella en el preprocesamiento, la CPU predeterminada CPU funciona bien.

Resumen rápido

  • ¿Estás creando un proceso de trabajo con DALI? Utilice fn.resize(mode="not_larger") + fn.crop(out_of_bounds_policy="pad") + fn.crop_mirror_normalize para reproducir el preprocesamiento «letterbox» YOLO en GPU.
  • ¿Integración con Ultralytics? Pasa la salida DALI como un torch.Tensor a datos model.predict() — Ultralytics automáticamente el preprocesamiento de imágenes.
  • ¿Estás realizando una implementación con Triton? Utiliza el backend DALI junto con un TensorRT paraCPU .

¿Por qué utilizar DALI para YOLO ?

En un proceso típico YOLO , los pasos de preprocesamiento se ejecutan en la CPU:

  1. Descifra la imagen (JPEG/PNG)
  2. Cambiar el tamaño conservando la relación de aspecto
  3. Ajustar al tamaño deseado (formato letterbox)
  4. Normalizar valores de píxeles de [0, 255] a datos [0, 1]
  5. Convertir el diseño de HWC a CHW

Con DALI, todas estas operaciones se ejecutan en la GPU, lo que elimina el CPU . Esto resulta especialmente útil cuando:

EscenarioPor qué DALI es útil
GPU rápida en GPUTensorRT Los motores con inferencia en menos de un milisegundo hacen que CPU sea el principal factor de coste
Entradas de alta resoluciónLas transmisiones de vídeo en 1080p y 4K requieren costosas operaciones de redimensionamiento
Lotes de gran tamañoProcesamiento de inferencia del lado del servidor de múltiples imágenes en paralelo
Número limitado de CPUDispositivos periféricos como NVIDIA , o GPU con gran densidad GPU y pocos CPU por GPU

Prerrequisitos

Solo para Linux

NVIDIA solo es compatible con Linux. No está disponible en Windows ni en macOS.

Instale los paquetes requeridos:

pip install ultralytics
pip install --extra-index-url https://pypi.nvidia.com nvidia-dali-cuda120
pip install ultralytics
pip install --extra-index-url https://pypi.nvidia.com nvidia-dali-cuda110

Requisitos:

  • GPU NVIDIA GPU capacidad de cálculo 5.0+ / Maxwell o posterior)
  • CUDA .0 o superior, o 12.0 o superior
  • Python .10-3.14
  • Sistema operativo Linux

Comprender YOLO

Antes de crear un pipeline DALI, conviene comprender exactamente qué Ultralytics durante el preprocesamiento. La clase clave es LetterBox en ultralytics/data/augment.py:

from ultralytics.data.augment import LetterBox

letterbox = LetterBox(
    new_shape=(640, 640),  # Target size
    center=True,  # Center the image (pad equally on both sides)
    stride=32,  # Stride alignment
    padding_value=114,  # Gray padding (114, 114, 114)
)

El proceso completo de preprocesamiento en ultralytics/engine/predictor.py sigue estos pasos:

PasoFuncionamientoCPUEquivalente a DALI
1Ajustar el tamaño del buzóncv2.resizefn.resize(mode="not_larger")
2Justificación centradacv2.copyMakeBorderfn.crop(out_of_bounds_policy="pad")
3BGR → RGBim[..., ::-1]fn.decoders.image(output_type=types.RGB)
4HWC → CHW + normalizar /255np.transpose + tensor / 255fn.crop_mirror_normalize(std=[255,255,255])

La técnica del «letterboxing» conserva la relación de aspecto mediante:

  1. Escala de cálculo: r = min(target_h / h, target_w / w)
  2. Cambiar el tamaño a (round(w * r), round(h * r))
  3. Rellenar el espacio restante con gris (114) para alcanzar el tamaño deseado
  4. Centrar la imagen de modo que el relleno se distribuya por igual a ambos lados

Pipeline DALI para YOLO

Utiliza el diagrama central que aparece a continuación como referencia predeterminada. Coincide con Ultralytics LetterBox(center=True) comportamiento, que es lo que utiliza YOLO estándar.

Esta versión reproduce exactamente el Ultralytics predeterminado Ultralytics con relleno centrado, haciendo coincidir LetterBox(center=True):

Canal DALI con relleno centrado (recomendado)

import nvidia.dali as dali
import nvidia.dali.fn as fn
import nvidia.dali.types as types


@dali.pipeline_def(batch_size=8, num_threads=4, device_id=0)
def yolo_dali_pipeline_centered(image_dir, target_size=640):
    """DALI pipeline replicating YOLO preprocessing with centered padding.

    Matches Ultralytics LetterBox(center=True) behavior exactly.
    """
    # Read and decode images on GPU
    jpegs, _ = fn.readers.file(file_root=image_dir, random_shuffle=False, name="Reader")
    images = fn.decoders.image(jpegs, device="mixed", output_type=types.RGB)

    # Aspect-ratio-preserving resize
    resized = fn.resize(
        images,
        resize_x=target_size,
        resize_y=target_size,
        mode="not_larger",
        interp_type=types.INTERP_LINEAR,
        antialias=False,  # Match cv2.INTER_LINEAR (no antialiasing)
    )

    # Centered padding using fn.crop with out_of_bounds_policy
    # When crop size > image size, fn.crop centers the image and pads symmetrically
    padded = fn.crop(
        resized,
        crop=(target_size, target_size),
        out_of_bounds_policy="pad",
        fill_values=114,  # YOLO padding value
    )

    # Normalize and convert layout
    output = fn.crop_mirror_normalize(
        padded,
        dtype=types.FLOAT,
        output_layout="CHW",
        mean=[0.0, 0.0, 0.0],
        std=[255.0, 255.0, 255.0],
    )
    return output

¿Cuándo es fn.pad ¿Es suficiente?

Si no necesitas la exactitud LetterBox(center=True) paridad, puedes simplificar el paso de relleno utilizando fn.pad(...) en lugar de fn.crop(..., out_of_bounds_policy="pad"). Esa variante solo rellena el a la derecha y abajo los bordes, lo cual puede ser aceptable para los procesos de implementación personalizados, pero no se corresponderá exactamente con el comportamiento predeterminado Ultralytics, que centra la imagen en un recuadro.

¿Por qué? fn.crop ¿Para el relleno centrado?

de DALI fn.pad El operador solo añade relleno al a la derecha y abajo bordes. Para obtener un relleno centrado (igual al de Ultralytics LetterBox(center=True)), utiliza fn.crop con out_of_bounds_policy="pad". Con la configuración predeterminada crop_pos_x=0.5 y crop_pos_y=0.5, la imagen se centra automáticamente con un relleno simétrico.

Desajuste en el suavizado

de DALI fn.resize activa el suavizado de forma predeterminada (antialias=True), mientras que OpenCV cv2.resize con INTER_LINEAR hace no están Aplicar suavizado. Configurar siempre antialias=False en DALI para adaptarse al CPU . Si se omite esto, se producen sutiles diferencias numéricas que pueden afectar precisión del modelo.

Puesta en marcha del proceso

Crear y ejecutar un proceso DALI

# Build and run the pipeline
pipe = yolo_dali_pipeline_centered(image_dir="/path/to/images", target_size=640)
pipe.build()

# Get a batch of preprocessed images
(output,) = pipe.run()

# Convert to numpy or PyTorch tensors
batch_np = output.as_cpu().as_array()  # Shape: (batch_size, 3, 640, 640)
print(f"Output shape: {batch_np.shape}, dtype: {batch_np.dtype}")
print(f"Value range: [{batch_np.min():.4f}, {batch_np.max():.4f}]")

Uso de DALI con Ultralytics

Puedes pasar un archivo preprocesado PyTorch tensor a model.predict(). Cuando un torch.Tensor se aprueba, Ultralytics omite el preprocesamiento de imágenes (ajuste de formato, conversión de BGR a RGB, de HWC a CHW y normalización a /255) y solo realiza la conversión de dispositivos y el cambio de tipo de datos antes de enviarlo al modelo.

Dado que Ultralytics tiene acceso a las dimensiones originales de la imagen en este caso, las coordenadas del cuadro de detección se devuelven en el espacio de 640×640 con formato letterbox. Para volver a asignarlas a las coordenadas originales de la imagen, utiliza scale_boxes que gestiona la lógica de redondeo exacta utilizada por LetterBox:

from ultralytics.utils.ops import scale_boxes

# boxes: tensor of shape (N, 4) in xyxy format, in 640x640 letterboxed coords
# Scale boxes from letterboxed (640, 640) back to original (orig_h, orig_w)
boxes = scale_boxes((640, 640), boxes, (orig_h, orig_w))

Esto se aplica a todas las rutas de preprocesamiento externas: tensor directa tensor , flujos de vídeo e Triton .

DALI + Ultralytics

from nvidia.dali.plugin.pytorch import DALIGenericIterator

from ultralytics import YOLO

# Load model
model = YOLO("yolo26n.pt")

# Create DALI iterator
pipe = yolo_dali_pipeline_centered(image_dir="/path/to/images", target_size=640)
pipe.build()
dali_iter = DALIGenericIterator(pipe, ["images"], reader_name="Reader")

# Run inference with DALI-preprocessed tensors
for batch in dali_iter:
    images = batch[0]["images"]  # Already on GPU, shape (B, 3, 640, 640)
    results = model.predict(images, verbose=False)
    for result in results:
        print(f"Detected {len(result.boxes)} objects")

Sin sobrecarga de preprocesamiento

Cuando pasas por un torch.Tensor a datos model.predict(), el paso de preprocesamiento de la imagen tarda unos 0,004 ms (prácticamente nada), frente a los aproximadamente 1-10 ms que requiere CPU con CPU . El tensor estar en formato BCHW, ser de tipo float32 (o float16) y estar normalizado a [0, 1]. Ultralytics gestionando automáticamente la conversión de tipos de datos y la conversión de tipos de datos.

DALI con transmisiones de vídeo

Para el procesamiento de vídeo en tiempo real, utiliza fn.external_source para importar fotogramas desde cualquier fuente — OpenCV, GStreamer o bibliotecas de captura personalizadas:

Canal DALI para el preprocesamiento de flujos de vídeo

import nvidia.dali as dali
import nvidia.dali.fn as fn
import nvidia.dali.types as types


@dali.pipeline_def(batch_size=1, num_threads=4, device_id=0)
def yolo_video_pipeline(target_size=640):
    """DALI pipeline for processing video frames from external source."""
    # External source for feeding frames from OpenCV, GStreamer, etc.
    frames = fn.external_source(device="cpu", name="input")
    frames = fn.reshape(frames, layout="HWC")

    # Move to GPU and preprocess
    frames_gpu = frames.gpu()
    resized = fn.resize(
        frames_gpu,
        resize_x=target_size,
        resize_y=target_size,
        mode="not_larger",
        interp_type=types.INTERP_LINEAR,
        antialias=False,
    )
    padded = fn.crop(
        resized,
        crop=(target_size, target_size),
        out_of_bounds_policy="pad",
        fill_values=114,
    )
    output = fn.crop_mirror_normalize(
        padded,
        dtype=types.FLOAT,
        output_layout="CHW",
        mean=[0.0, 0.0, 0.0],
        std=[255.0, 255.0, 255.0],
    )
    return output
import cv2
import numpy as np
import torch

from ultralytics import YOLO

model = YOLO("yolo26n.engine")  # TensorRT model

pipe = yolo_video_pipeline(target_size=640)
pipe.build()

cap = cv2.VideoCapture("video.mp4")
while cap.isOpened():
    ret, frame = cap.read()
    if not ret:
        break

    # Feed BGR frame (convert to RGB for DALI)
    frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
    pipe.feed_input("input", [np.array(frame_rgb)])
    (output,) = pipe.run()

    # Convert DALI output to torch tensor for inference.
    # This is a simple fallback path: using feed_input() with pipe.run() keeps a GPU->CPU->GPU copy.
    # For high-throughput deployments, prefer a reader-based pipeline plus DALIGenericIterator to keep data on GPU.
    tensor = torch.tensor(output.as_cpu().as_array()).to("cuda")
    results = model.predict(tensor, verbose=False)

Servidor de Triton con DALI

Para la implementación en producción, combina el preprocesamiento de DALI con TensorRT en Triton Server utilizando un modelo de conjunto. Esto elimina por completo CPU : se introducen los bytes JPEG sin procesar y se obtienen las detecciones, con todo procesado en la GPU.

Estructura del repositorio de modelos

model_repository/
├── dali_preprocessing/
│   ├── 1/
│   │   └── model.dali
│   └── config.pbtxt
├── yolo_trt/
│   ├── 1/
│   │   └── model.plan
│   └── config.pbtxt
└── ensemble_dali_yolo/
    ├── 1/                  # Empty directory (required by Triton)
    └── config.pbtxt

Paso 1: Crear el proceso DALI

Serializar el canal DALI para el backend Triton :

Serializar el canal DALI para Triton

import nvidia.dali as dali
import nvidia.dali.fn as fn
import nvidia.dali.types as types


@dali.pipeline_def(batch_size=8, num_threads=4, device_id=0)
def triton_dali_pipeline():
    """DALI preprocessing pipeline for Triton deployment."""
    # Input: raw encoded image bytes from Triton
    images = fn.external_source(device="cpu", name="DALI_INPUT_0")
    images = fn.decoders.image(images, device="mixed", output_type=types.RGB)

    resized = fn.resize(
        images,
        resize_x=640,
        resize_y=640,
        mode="not_larger",
        interp_type=types.INTERP_LINEAR,
        antialias=False,
    )
    padded = fn.crop(
        resized,
        crop=(640, 640),
        out_of_bounds_policy="pad",
        fill_values=114,
    )
    output = fn.crop_mirror_normalize(
        padded,
        dtype=types.FLOAT,
        output_layout="CHW",
        mean=[0.0, 0.0, 0.0],
        std=[255.0, 255.0, 255.0],
    )
    return output


# Serialize pipeline to model repository
pipe = triton_dali_pipeline()
pipe.serialize(filename="model_repository/dali_preprocessing/1/model.dali")

Paso 2: Exportar YOLO TensorRT

Exportar YOLO al TensorRT

from ultralytics import YOLO

model = YOLO("yolo26n.pt")
model.export(format="engine", imgsz=640, half=True, batch=8)
# Copy the .engine file to model_repository/yolo_trt/1/model.plan

Paso 3: Configurar Triton

dali_preprocessing/config.pbtxt:

name: "dali_preprocessing"
backend: "dali"
max_batch_size: 8
input [
  {
    name: "DALI_INPUT_0"
    data_type: TYPE_UINT8
    dims: [ -1 ]
  }
]
output [
  {
    name: "DALI_OUTPUT_0"
    data_type: TYPE_FP32
    dims: [ 3, 640, 640 ]
  }
]

yolo.pbtxt:

name: "yolo_trt"
platform: "tensorrt_plan"
max_batch_size: 8
input [
  {
    name: "images"
    data_type: TYPE_FP32
    dims: [ 3, 640, 640 ]
  }
]
output [
  {
    name: "output0"
    data_type: TYPE_FP32
    dims: [ 300, 6 ]
  }
]

ensemble_dali_yolo/config.pbtxt:

name: "ensemble_dali_yolo"
platform: "ensemble"
max_batch_size: 8
input [
  {
    name: "INPUT"
    data_type: TYPE_UINT8
    dims: [ -1 ]
  }
]
output [
  {
    name: "OUTPUT"
    data_type: TYPE_FP32
    dims: [ 300, 6 ]
  }
]
ensemble_scheduling {
  step [
    {
      model_name: "dali_preprocessing"
      model_version: -1
      input_map {
        key: "DALI_INPUT_0"
        value: "INPUT"
      }
      output_map {
        key: "DALI_OUTPUT_0"
        value: "preprocessed_image"
      }
    },
    {
      model_name: "yolo_trt"
      model_version: -1
      input_map {
        key: "images"
        value: "preprocessed_image"
      }
      output_map {
        key: "output0"
        value: "OUTPUT"
      }
    }
  ]
}

Cómo funciona el mapeo de conjuntos

El conjunto conecta los modelos a través de tensor virtuales. El output_map valor "preprocessed_image" en el paso DALI coincide con el input_map valor "preprocessed_image" en el TensorRT . Se trata de nombres arbitrarios que vinculan la salida de un paso con la entrada del siguiente; no es necesario que coincidan con tensor internos del modelo.

Paso 4: Enviar solicitudes de inferencia

¿Por qué? tritonclient en lugar de YOLO(\"http://...\")?

Ultralytics Triton integrada con Triton que gestiona el preprocesamiento y el posprocesamiento de forma automática. Sin embargo, no funcionará con el conjunto DALI porque YOLO() envía un tensor float32 preprocesado, tensor el conjunto espera bytes JPEG sin procesar. Utiliza tritonclient directamente para conjuntos DALI, y el integración nativa para instalaciones estándar sin DALI.

Enviar imágenes al Triton

import numpy as np
import tritonclient.http as httpclient

client = httpclient.InferenceServerClient(url="localhost:8000")

# Load image as raw bytes (JPEG/PNG encoded)
image_data = np.fromfile("image.jpg", dtype="uint8")
image_data = np.expand_dims(image_data, axis=0)  # Add batch dimension

# Create input
input_tensor = httpclient.InferInput("INPUT", image_data.shape, "UINT8")
input_tensor.set_data_from_numpy(image_data)

# Run inference through the ensemble
result = client.infer(model_name="ensemble_dali_yolo", inputs=[input_tensor])
detections = result.as_numpy("OUTPUT")  # Shape: (1, 300, 6) -> [x1, y1, x2, y2, conf, class_id]

# Filter by confidence (no NMS needed — YOLO26 is end-to-end)
detections = detections[0]  # First image
detections = detections[detections[:, 4] > 0.25]  # Confidence threshold
print(f"Detected {len(detections)} objects")

Procesamiento por lotes de imágenes JPEG

Al enviar un lote de imágenes JPEG a Triton, completa todas las matrices de bytes codificados hasta alcanzar la misma longitud (el número máximo de bytes del lote). Triton los lotes tengan una estructura homogénea para el tensor de entrada.

Tareas admitidas

El preprocesamiento DALI funciona con todas YOLO que utilizan el estándar LetterBox cartera de proyectos:

TareaCompatibleNotas
DetecciónPreprocesamiento estándar de buzones
SegmentaciónEl mismo preprocesamiento que en la detección
Estimación de poseEl mismo preprocesamiento que en la detección
Detección orientada (OBB)El mismo preprocesamiento que en la detección
ClasificaciónUtiliza transformaciones de Torchvision (recorte centrado), no formato letterbox

Limitaciones

  • Solo para Linux: DALI no es compatible con Windows ni con macOS
  • GPU NVIDIA : no hay alternativa CPU
  • Pipeline estático: la estructura del pipeline se define en el momento de la compilación y no puede modificarse dinámicamente
  • fn.pad solo a la derecha/abajo: Use fn.crop con out_of_bounds_policy="pad" para el relleno centrado
  • Sin modo recto: Los canales DALI generan resultados de tamaño fijo (por ejemplo, 640×640). El auto=True No se admite el modo rect que genera salidas de tamaño variable (por ejemplo, 384×640). Tenga en cuenta que, aunque TensorRT Aunque admite formas de entrada dinámicas, un canal DALI de tamaño fijo se combina de forma natural con un motor de tamaño fijo para obtener el máximo rendimiento
  • Memoria con múltiples instancias: Uso de instance_group con count > 1 en Triton provocar un elevado consumo de memoria. Utiliza el grupo de instancias predeterminado para el modelo DALI

Preguntas frecuentes

¿En qué se diferencia la velocidad del preprocesamiento DALI de la CPU ?

La ventaja depende de tu proceso de trabajo. Cuando GPU ya es rápida con TensorRT, CPU , que tarda entre 2 y 10 ms, puede convertirse en el principal factor de coste. DALI elimina este cuello de botella al ejecutar el preprocesamiento en la GPU. Las mayores ventajas se observan con entradas de alta resolución (1080p, 4K), lotes de gran tamaño y sistemas con un número limitado CPU por GPU.

¿Puedo utilizar DALI con PyTorch (y no solo con TensorRT)?

Sí. Utiliza DALIGenericIterator para ser preprocesado torch.Tensor resultados, y luego pasarlos a model.predict(). Sin embargo, la mejora en el rendimiento es mayor con TensorRT modelos en los que la inferencia ya es muy rápida y CPU se convierte en el cuello de botella.

¿Cuál es la diferencia entre fn.pad y fn.crop ¿para rellenar?

fn.pad solo añade relleno al a la derecha y abajo bordes. fn.crop con out_of_bounds_policy="pad" centra la imagen y añade un margen simétrico por todos los lados, al estilo de Ultralytics LetterBox(center=True) comportamiento.

¿Ofrece DALI resultados idénticos a los del CPU ?

Casi idénticos. Conjunto antialias=False en fn.resize para que coincida con el OpenCV cv2.INTER_LINEAR. Minor floating-point differences (< 0.001) may occur due to GPU vs CPU arithmetic, but these have no measurable impact on detection precisión.

¿Qué hay deCUDA alternativa a DALI?

CUDA es otra NVIDIA para el procesamiento de imágenes GPU. Ofrece control por operador (como OpenCV (pero en GPU) en lugar del enfoque por canalizaciones de DALI.CUDA cvcuda.copymakeborder() admite un margen explícito por cada lado, lo que facilita la creación de una relación de aspecto «letterbox» centrada. Elige DALI para flujos de trabajo basados en canalizaciones (especialmente con Triton), yCUDA un control detallado a nivel de operador en código de inferencia personalizado.



📅 Creado hace 0 días ✏️ Actualizado hace 0 días
raimbekovm

Comentarios