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_normalizepara reproducir el preprocesamiento «letterbox» YOLO en GPU. - ¿Integración con Ultralytics? Pasa la salida DALI como un
torch.Tensora datosmodel.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:
- Descifra la imagen (JPEG/PNG)
- Cambiar el tamaño conservando la relación de aspecto
- Ajustar al tamaño deseado (formato letterbox)
- Normalizar valores de píxeles de
[0, 255]a datos[0, 1] - 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:
| Escenario | Por qué DALI es útil |
|---|---|
| GPU rápida en GPU | TensorRT Los motores con inferencia en menos de un milisegundo hacen que CPU sea el principal factor de coste |
| Entradas de alta resolución | Las transmisiones de vídeo en 1080p y 4K requieren costosas operaciones de redimensionamiento |
| Lotes de gran tamaño | Procesamiento de inferencia del lado del servidor de múltiples imágenes en paralelo |
| Número limitado de CPU | Dispositivos 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:
| Paso | Funcionamiento | CPU | Equivalente a DALI |
|---|---|---|---|
| 1 | Ajustar el tamaño del buzón | cv2.resize | fn.resize(mode="not_larger") |
| 2 | Justificación centrada | cv2.copyMakeBorder | fn.crop(out_of_bounds_policy="pad") |
| 3 | BGR → RGB | im[..., ::-1] | fn.decoders.image(output_type=types.RGB) |
| 4 | HWC → CHW + normalizar /255 | np.transpose + tensor / 255 | fn.crop_mirror_normalize(std=[255,255,255]) |
La técnica del «letterboxing» conserva la relación de aspecto mediante:
- Escala de cálculo:
r = min(target_h / h, target_w / w) - Cambiar el tamaño a
(round(w * r), round(h * r)) - Rellenar el espacio restante con gris (
114) para alcanzar el tamaño deseado - 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.
Tubo centrado (recomendado, compatible con Ultralytics )
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:
| Tarea | Compatible | Notas |
|---|---|---|
| Detección | ✅ | Preprocesamiento estándar de buzones |
| Segmentación | ✅ | El mismo preprocesamiento que en la detección |
| Estimación de pose | ✅ | El mismo preprocesamiento que en la detección |
| Detección orientada (OBB) | ✅ | El mismo preprocesamiento que en la detección |
| Clasificación | ❌ | Utiliza 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.padsolo a la derecha/abajo: Usefn.cropconout_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=TrueNo 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_groupconcount> 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.