YOLO Vision 2026:

Preprocesamiento acelerado por GPU con NVIDIA DALI#

Al desplegar modelos de Ultralytics YOLO en producción, el preprocesamiento suele convertirse en el cuello de botella. Aunque TensorRT puede ejecutar la inferencia del modelo en tan solo unos milisegundos, el preprocesamiento basado en CPU (redimensionado, relleno y normalización) puede tardar entre 2 y 10 ms por imagen, especialmente con resoluciones altas. NVIDIA DALI (Biblioteca de carga de datos) resuelve este problema trasladando todo el flujo de preprocesamiento a la GPU.

Esta guía te muestra cómo crear pipelines de DALI que replican exactamente el preprocesamiento de Ultralytics YOLO, integrarlos con model.predict(), procesar flujos de vídeo y desplegar todo el proceso con Triton Inference Server.

¿Para quién es esta guía?

Esta guía está dirigida a ingenieros que despliegan modelos YOLO en entornos de producción donde el preprocesamiento en CPU supone un cuello de botella medido —normalmente despliegues de TensorRT en GPU de NVIDIA, pipelines de vídeo de alto rendimiento o configuraciones de Triton Inference Server. Si ejecutas inferencias estándar con model.predict() y no tienes un cuello de botella en el preprocesamiento, el pipeline de CPU predeterminado funciona bien.

Resumen rápido
  • ¿Estás creando un pipeline de DALI? Usa fn.resize(mode="not_larger") + fn.crop(out_of_bounds_policy="pad") + fn.crop_mirror_normalize para replicar el preprocesamiento letterbox de YOLO en la GPU.
  • ¿Lo estás integrando con Ultralytics? Pasa la salida de DALI como torch.Tensor a model.predict(); Ultralytics omitirá automáticamente el preprocesamiento de la imagen.
  • ¿Lo estás desplegando con Triton? Usa el backend de DALI con un ensemble de TensorRT para realizar el preprocesamiento sin usar la CPU.

Por qué usar DALI para el preprocesamiento de YOLO#

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

  1. Decodificar la imagen (JPEG/PNG)
  2. Redimensionar conservando la relación de aspecto
  3. Añadir relleno hasta alcanzar el tamaño objetivo (letterbox)
  4. Normalizar los valores de píxel de [0, 255] a [0, 1]
  5. Convertir el formato de HWC a CHW

Con DALI, todas estas operaciones se ejecutan en la GPU, eliminando el cuello de botella de la CPU. Esto resulta especialmente útil cuando:

EscenarioPor qué DALI ayuda
Inferencia rápida en GPULos motores de TensorRT, con inferencias inferiores a un milisegundo, hacen que el preprocesamiento en CPU sea el coste predominante
Entradas de alta resoluciónLos flujos de vídeo 1080p y 4K requieren operaciones de redimensionado costosas
Tamaños de lote grandesInferencia en el servidor procesando muchas imágenes en paralelo
Número limitado de núcleos de CPUDispositivos periféricos como NVIDIA Jetson o servidores con GPU densas y pocos núcleos de CPU por GPU

Requisitos previos#

Solo Linux

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

Instala los paquetes necesarios:

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

Requisitos:

  • GPU de NVIDIA (capacidad de cálculo 5.0 o superior / Maxwell o posterior)
  • CUDA 11.0 o superior, 12.0 o superior o 13.0 o superior
  • Python 3.10-3.14
  • Sistema operativo Linux

Comprender el preprocesamiento de YOLO#

Antes de crear un pipeline de DALI, conviene entender exactamente qué hace 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 pipeline de preprocesamiento completo de ultralytics/engine/predictor.py realiza estos pasos:

PasoOperaciónFunción de CPUEquivalente en DALI
1Redimensionado letterboxcv2.resizefn.resize(mode="not_larger")
2Relleno centradocv2.copyMakeBorderfn.crop(out_of_bounds_policy="pad")
3BGR → RGBim[..., ::-1]fn.decoders.image(output_type=types.RGB)
4HWC → CHW + normalización /255np.transpose + tensor / 255fn.crop_mirror_normalize(std=[255,255,255])

La operación letterbox conserva la relación de aspecto mediante:

  1. Cálculo de la escala: r = min(target_h / h, target_w / w)
  2. Redimensionado a (round(w * r), round(h * r))
  3. Relleno del espacio restante con gris (114) hasta alcanzar el tamaño objetivo
  4. Centrado de la imagen para distribuir el relleno por igual a ambos lados

Pipeline de DALI para YOLO#

El pipeline de DALI recomendado replica el comportamiento predeterminado LetterBox(center=True) de Ultralytics, que es el que utiliza la inferencia estándar de YOLO.

Pipeline centrado (recomendado, coincide con LetterBox de Ultralytics)#

Esta versión replica exactamente el preprocesamiento predeterminado de Ultralytics con relleno centrado, coincidiendo con LetterBox(center=True):

Pipeline de DALI con relleno centrado (recomendado)
from nvidia import dali
from nvidia.dali import fn, 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 basta con `fn.pad`?

Si no necesitas una paridad exacta con LetterBox(center=True), puedes simplificar el paso de relleno usando fn.pad(...) en lugar de fn.crop(..., out_of_bounds_policy="pad"). Esta variante solo añade relleno en los bordes derecho e inferior, lo que puede ser aceptable para pipelines de despliegue personalizados, pero no coincidirá exactamente con el comportamiento letterbox centrado predeterminado de Ultralytics.

¿Por qué usar `fn.crop` para el relleno centrado?

El operador fn.pad de DALI solo añade relleno en los bordes derecho e inferior. Para obtener un relleno centrado (que coincida con LetterBox(center=True) de Ultralytics), usa fn.crop con out_of_bounds_policy="pad". Con los valores predeterminados crop_pos_x=0.5 y crop_pos_y=0.5, la imagen se centra automáticamente con un relleno simétrico.

Diferencia en el antialiasing

fn.resize de DALI activa el antialiasing de forma predeterminada (antialias=True), mientras que cv2.resize de OpenCV con INTER_LINEAR no aplica antialiasing. Establece siempre antialias=False en DALI para que coincida con el pipeline de CPU. Omitirlo provoca pequeñas diferencias numéricas que pueden afectar a la precisión del modelo.

Ejecución del pipeline#

Crear y ejecutar un pipeline de 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}]")

Usar DALI con Ultralytics Predict#

Puedes pasar directamente un tensor de PyTorch preprocesado a model.predict(). Cuando se pasa un torch.Tensor, Ultralytics omite el preprocesamiento de la imagen (letterbox, BGR→RGB, HWC→CHW y normalización /255) y solo realiza la transferencia al dispositivo y la conversión del tipo de datos antes de enviarlo al modelo.

Como Ultralytics no tiene acceso a las dimensiones de la imagen original en este caso, las coordenadas de las cajas de detección se devuelven en el espacio letterbox de 640×640. Para volver a asignarlas a las coordenadas de la imagen original, usa scale_boxes, que gestiona la lógica exacta de redondeo 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 externo: entrada directa de tensores, flujos de vídeo y despliegue con Triton.

Predicción con 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")
Coste cero de preprocesamiento

Cuando pasas un torch.Tensor a model.predict(), el paso de preprocesamiento de la imagen tarda ~0,004 ms (prácticamente cero), frente a ~1-10 ms con el preprocesamiento en CPU. El tensor debe estar en formato BCHW, ser float32 (o float16) y estar normalizado a [0, 1]. Ultralytics seguirá gestionando automáticamente la transferencia al dispositivo y la conversión del tipo de datos.

DALI con flujos de vídeo#

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

Pipeline de DALI para el preprocesamiento de flujos de vídeo
from nvidia import dali
from nvidia.dali import fn, 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

Triton Inference Server con DALI#

Para el despliegue en producción, combina el preprocesamiento de DALI con la inferencia de TensorRT en Triton Inference Server mediante un modelo ensemble. Esto elimina por completo el preprocesamiento en CPU: entran bytes JPEG sin procesar y salen detecciones, con todo el procesamiento realizado en la GPU.

Estructura del repositorio del modelo#

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 pipeline de DALI#

Serializa el pipeline de DALI para el backend DALI de Triton:

Serializar el pipeline de DALI para Triton
from nvidia import dali
from nvidia.dali import fn, 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 a TensorRT#

Exportar el modelo YOLO a un motor TensorRT
from ultralytics import YOLO

model = YOLO("yolo26n.pt")
model.export(
    format="engine", imgsz=640, quantize=16, batch=8, dynamic=True, nms=False
)  # NMS-free (N, 300, 6); TensorRT >= 8.5
# 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_trt/config.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 la asignación del ensemble

El ensemble conecta los modelos mediante nombres de tensores virtuales. El valor "preprocessed_image" de output_map en el paso de DALI coincide con el valor "preprocessed_image" de input_map en el paso de TensorRT. Son nombres arbitrarios que vinculan la salida de un paso con la entrada del siguiente; no tienen que coincidir con los nombres de los tensores internos de ningún modelo.

Paso 4: enviar solicitudes de inferencia#

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

Ultralytics tiene compatibilidad integrada con Triton que gestiona automáticamente el preprocesamiento y el posprocesamiento. Sin embargo, no funcionará con el ensemble de DALI porque YOLO() envía un tensor float32 preprocesado, mientras que el ensemble espera bytes JPEG sin procesar. Usa tritonclient directamente para los ensembles de DALI y la integración integrada para despliegues estándar sin DALI.

Enviar imágenes al ensemble de 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 for the nms=False export)
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, añade relleno a todas las matrices de bytes codificados hasta que tengan la misma longitud (el número máximo de bytes del lote). Triton requiere formas de lote homogéneas para el tensor de entrada.

Tareas compatibles#

El preprocesamiento de DALI funciona con todas las tareas de YOLO que utilizan el pipeline estándar LetterBox:

TareaCompatibleNotas
DetecciónPreprocesamiento letterbox estándar
Segmentación de instanciasMismo preprocesamiento que en detección
Segmentación semánticaMismo preprocesamiento de imágenes que en detección
ClasificaciónUsa transformaciones de torchvision (recorte central), no letterbox
Estimación de poseMismo preprocesamiento que en detección
Detección orientada (OBB)Mismo preprocesamiento que en detección

Limitaciones#

  • Solo Linux: DALI no es compatible con Windows ni macOS
  • Se requiere una GPU NVIDIA: No hay alternativa que use únicamente la CPU
  • Pipeline estático: La estructura del pipeline se define durante la compilación y no puede cambiar dinámicamente
  • fn.pad solo admite derecha/abajo: Usa fn.crop con out_of_bounds_policy="pad" para un relleno centrado
  • Sin modo rect: Los pipelines de DALI generan salidas de tamaño fijo (por ejemplo, 640×640). El modo rect de auto=True, que genera salidas de tamaño variable (por ejemplo, 384×640), no es compatible. Ten en cuenta que, aunque TensorRT admite formas de entrada dinámicas, un pipeline de DALI de tamaño fijo se combina de forma natural con un motor de tamaño fijo para maximizar el rendimiento
  • Memoria con varias instancias: Usar instance_group con count > 1 en Triton puede provocar un uso elevado de memoria. Usa el grupo de instancias predeterminado para el modelo DALI

Preguntas frecuentes#

  • El beneficio depende de tu pipeline. Cuando la inferencia en la GPU ya es rápida con TensorRT, el preprocesamiento en la CPU, de 2-10 ms, puede convertirse en el coste principal. DALI elimina este cuello de botella al ejecutar el preprocesamiento en la GPU. Las mayores mejoras se observan con entradas de alta resolución (1080p, 4K), tamaños de lote grandes y sistemas con un número limitado de núcleos de CPU por GPU.

  • Sí. Usa DALIGenericIterator para obtener salidas torch.Tensor preprocesadas y, a continuación, pásalas a model.predict(). Sin embargo, el beneficio en rendimiento es mayor con modelos de TensorRT, cuya inferencia ya es muy rápida y en los que el preprocesamiento en la CPU se convierte en el cuello de botella.

  • fn.pad añade relleno únicamente en los bordes derecho e inferior. fn.crop con out_of_bounds_policy="pad" centra la imagen y añade relleno simétricamente en todos los lados, igualando el comportamiento de LetterBox(center=True) de Ultralytics.

  • Prácticamente idénticos. Configura antialias=False en fn.resize para que coincida con cv2.INTER_LINEAR de OpenCV. Pueden producirse pequeñas diferencias de coma flotante (< 0.001) debido a las operaciones aritméticas en la GPU frente a la CPU, pero no tienen un impacto medible en la precisión de la detección.

  • CV-CUDA es otra biblioteca de NVIDIA para el procesamiento de visión acelerado por GPU. Proporciona control por operador (como OpenCV, pero en la GPU) en lugar del enfoque basado en pipelines de DALI. cvcuda.copymakeborder() de CV-CUDA admite un relleno explícito por lado, lo que facilita el letterbox centrado. Elige DALI para flujos de trabajo basados en pipelines (especialmente con Triton) y CV-CUDA para un control detallado a nivel de operador en código de inferencia personalizado.

Comentarios