Link to this sectionPreprocesamiento acelerado por GPU con NVIDIA DALI#
Link to this sectionIntroducción#
Al implementar modelos Ultralytics YOLO en producción, el preprocesamiento suele convertirse en el cuello de botella. Aunque TensorRT puede ejecutar la inferencia del modelo en solo unos milisegundos, el preprocesamiento basado en CPU (cambio de tamaño, relleno, normalización) puede tomar de 2 a 10 ms por imagen, especialmente a altas resoluciones. NVIDIA DALI (Data Loading Library) soluciona esto moviendo toda la canalización de preprocesamiento a la GPU.
Esta guía te ayuda a construir canalizaciones de DALI que replican exactamente el preprocesamiento de Ultralytics YOLO, integrándolas con model.predict(), procesando secuencias de vídeo e implementando todo de extremo a extremo con Triton Inference Server.
Esta guía es para ingenieros que implementan modelos YOLO en entornos de producción donde el preprocesamiento por CPU es un cuello de botella medido, típicamente implementaciones de TensorRT en GPU de NVIDIA, canalizaciones de vídeo de alto rendimiento o configuraciones de Triton Inference Server. Si ejecutas una inferencia estándar con model.predict() y no tienes un cuello de botella en el preprocesamiento, la canalización predeterminada de la CPU funciona bien.
- ¿Construyendo una canalización DALI? Usa
fn.resize(mode="not_larger")+fn.crop(out_of_bounds_policy="pad")+fn.crop_mirror_normalizepara replicar el preprocesamiento de tipo letterbox de YOLO en la GPU. - ¿Integrando con Ultralytics? Pasa la salida de DALI como un
torch.Tensoramodel.predict()— Ultralytics omitirá automáticamente el preprocesamiento de imagen. - ¿Implementando con Triton? Usa el backend de DALI con un conjunto de TensorRT para un preprocesamiento con cero CPU.
Link to this section¿Por qué usar DALI para el preprocesamiento de YOLO?#
En una canalización de inferencia típica de YOLO, los pasos de preprocesamiento se ejecutan en la CPU:
- Decodificar la imagen (JPEG/PNG)
- Cambiar el tamaño preservando la relación de aspecto
- Rellenar al tamaño objetivo (letterbox)
- Normalizar los valores de píxel de
[0, 255]a[0, 1] - Convertir el diseño de HWC a CHW
Con DALI, todas estas operaciones se ejecutan en la GPU, eliminando el cuello de botella de la CPU. Esto es especialmente valioso cuando:
| Escenario | Por qué ayuda DALI |
|---|---|
| Inferencia rápida por GPU | Los motores TensorRT con inferencia de sub-milisegundos hacen que el preprocesamiento por CPU sea el coste dominante |
| Entradas de alta resolución | Las secuencias de vídeo 1080p y 4K requieren operaciones de cambio de tamaño costosas |
| Grandes tamaños de lote | Procesamiento de inferencia en el servidor con muchas imágenes en paralelo |
| Núcleos de CPU limitados | Dispositivos de borde como NVIDIA Jetson, o servidores con GPU densas con pocos núcleos de CPU por GPU |
Link to this sectionRequisitos previos#
NVIDIA DALI es compatible solo con Linux. No está disponible en Windows ni en macOS.
Instala los paquetes necesarios:
pip install ultralytics
pip install --extra-index-url https://pypi.nvidia.com nvidia-dali-cuda130Requisitos:
- GPU NVIDIA (capacidad de cómputo 5.0+ / Maxwell o más reciente)
- CUDA 11.0+, 12.0+ o 13.0+
- Python 3.10-3.14
- Sistema operativo Linux
Link to this sectionEntender el preprocesamiento de YOLO#
Antes de construir una canalización DALI, ayuda 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)
)La canalización de preprocesamiento completa en ultralytics/engine/predictor.py realiza estos pasos:
| Paso | Operación | Función CPU | Equivalente DALI |
|---|---|---|---|
| 1 | Cambio de tamaño Letterbox | cv2.resize | fn.resize(mode="not_larger") |
| 2 | Relleno centrado | 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 operación de letterbox preserva la relación de aspecto mediante:
- Cálculo de escala:
r = min(target_h / h, target_w / w) - Cambio de tamaño a
(round(w * r), round(h * r)) - Relleno del espacio restante con gris (
114) para alcanzar el tamaño objetivo - Centrado de la imagen para que el relleno se distribuya equitativamente en ambos lados
Link to this sectionCanalización DALI para YOLO#
Usa la canalización centrada a continuación como referencia predeterminada. Coincide con el comportamiento de LetterBox(center=True) de Ultralytics, que es lo que utiliza la inferencia estándar de YOLO.
Link to this sectionCanalización centrada (Recomendado, coincide con Ultralytics LetterBox)#
Esta versión replica exactamente el preprocesamiento predeterminado de Ultralytics con relleno centrado, coincidiendo con LetterBox(center=True):
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 outputSi no necesitas 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"). Esa variante rellena solo los bordes derecho e inferior, lo cual puede ser aceptable para canalizaciones de implementación personalizadas, pero no coincidirá exactamente con el comportamiento de letterbox centrado predeterminado de Ultralytics.
El operador fn.pad de DALI solo añade relleno a los bordes derecho e inferior. Para obtener un relleno centrado (coincidiendo con LetterBox(center=True) de Ultralytics), utiliza 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.
El fn.resize de DALI activa el suavizado de forma predeterminada (antialias=True), mientras que el cv2.resize de OpenCV con INTER_LINEAR no aplica suavizado. Configura siempre antialias=False en DALI para coincidir con la canalización de la CPU. Omitir esto causa diferencias numéricas sutiles que pueden afectar a la precisión del modelo.
Link to this sectionEjecución de la canalización#
# 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}]")Link to this sectionUsar DALI con Ultralytics Predict#
Puedes pasar un tensor de PyTorch preprocesado directamente a model.predict(). Cuando se pasa un torch.Tensor, Ultralytics omite el preprocesamiento de imagen (letterbox, BGR→RGB, HWC→CHW y normalización /255) y solo realiza la transferencia al dispositivo y la conversión de tipo antes de enviarlo al modelo.
Dado que Ultralytics no tiene acceso a las dimensiones originales de la imagen en este caso, las coordenadas de las cajas de detección se devuelven en el espacio letterboxed de 640×640. Para volver a mapearlas a las coordenadas originales de la imagen, usa scale_boxes, que maneja 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: entrada directa de tensor, secuencias de vídeo e implementación en Triton.
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")Cuando pasas un torch.Tensor a model.predict(), el paso de preprocesamiento de imagen toma ~0.004 ms (esencialmente cero) en comparación con los ~1-10 ms del preprocesamiento por CPU. El tensor debe estar en formato BCHW, float32 (o float16) y normalizado a [0, 1]. Ultralytics seguirá gestionando la transferencia al dispositivo y la conversión de tipo automáticamente.
Link to this sectionDALI con secuencias de vídeo#
Para el procesamiento de vídeo en tiempo real, usa fn.external_source para alimentar fotogramas desde cualquier fuente: OpenCV, GStreamer o bibliotecas de captura personalizadas:
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 outputLink to this sectionTriton Inference Server con DALI#
Para la implementación en producción, combina el preprocesamiento de DALI con la inferencia de TensorRT en Triton Inference Server usando un modelo de conjunto. Esto elimina por completo el preprocesamiento de la CPU: entran bytes JPEG sin procesar y salen detecciones, todo procesado en la GPU.
Link to this sectionEstructura 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.pbtxtLink to this sectionPaso 1: Crea la canalización DALI#
Serializa la canalización DALI para el backend de DALI en 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")Link to this sectionPaso 2: Exporta YOLO a 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.planLink to this sectionPaso 3: Configura 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"
}
}
]
}El conjunto conecta modelos a través de nombres de tensores virtuales. El valor de output_map "preprocessed_image" en el paso de DALI coincide con el valor de input_map "preprocessed_image" en el paso de TensorRT. Estos son nombres arbitrarios que vinculan la salida de un paso con la entrada del siguiente; no necesitan coincidir con los nombres de tensores internos de ningún modelo.
Link to this sectionPaso 4: Enviar solicitudes de inferencia#
!!! info "¿Por qué tritonclient en lugar de YOLO(\"http://...\")?"
Ultralytics has [built-in Triton support](triton-inference-server.md#running-inference) that handles pre/postprocessing automatically. However, it won't work with the DALI ensemble because `YOLO()` sends a preprocessed float32 tensor while the ensemble expects raw JPEG bytes. Use `tritonclient` directly for DALI ensembles, and the [built-in integration](triton-inference-server.md) for standard deployments without DALI.
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")Al enviar un lote de imágenes JPEG a Triton, rellena (pad) todos los arreglos de bytes codificados hasta la misma longitud (el recuento máximo de bytes en el lote). Triton requiere formas de lote homogéneas para el tensor de entrada.
Link to this sectionTareas admitidas#
El preprocesamiento con DALI funciona con todas las tareas de YOLO que utilizan la canalización estándar LetterBox:
| Tarea | Admitido | Notas |
|---|---|---|
| Detección | ✅ | Preprocesamiento estándar letterbox |
| Segmentación de instancias | ✅ | El mismo preprocesamiento que en la detección |
| Segmentación semántica | ✅ | El mismo preprocesamiento de imagen 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 central), no letterbox |
Link to this sectionLimitaciones#
- Solo Linux: DALI no es compatible con Windows o macOS
- Se requiere GPU NVIDIA: No hay alternativa solo para CPU
- Canalización estática: La estructura de la canalización se define en el momento de la compilación y no puede cambiar dinámicamente
fn.pades solo para derecha/abajo: Usafn.cropconout_of_bounds_policy="pad"para un relleno centrado- Sin modo rect: Las canalizaciones de DALI producen salidas de tamaño fijo (ej. 640×640). El modo rect
auto=Trueque produce salidas de tamaño variable (ej. 384×640) no es compatible. Ten en cuenta que, aunque TensorRT admite formas de entrada dinámicas, una canalización de DALI de tamaño fijo se combina naturalmente con un motor de tamaño fijo para obtener el máximo rendimiento - Memoria con múltiples instancias: El uso de
instance_groupconcount> 1 en Triton puede provocar un alto uso de memoria. Usa el grupo de instancias predeterminado para el modelo DALI
Link to this sectionFAQ#
Link to this section¿Cómo se compara el preprocesamiento de DALI con la velocidad de preprocesamiento de la CPU?#
El beneficio depende de tu canalización. Cuando la inferencia en GPU ya es rápida con TensorRT, el preprocesamiento en CPU a 2-10 ms puede convertirse en el costo dominante. DALI elimina este cuello de botella ejecutando el preprocesamiento en la GPU. Las mayores ganancias se observan con entradas de alta resolución (1080p, 4K), tamaños de lote grandes y sistemas con núcleos de CPU limitados por GPU.
Link to this section¿Puedo usar DALI con modelos de PyTorch (no solo TensorRT)?#
Sí. Usa DALIGenericIterator para obtener salidas torch.Tensor preprocesadas y luego pásalas a model.predict(). Sin embargo, el beneficio de rendimiento es mayor con modelos de TensorRT, donde la inferencia ya es muy rápida y el preprocesamiento de la CPU se convierte en el cuello de botella.
Link to this section¿Cuál es la diferencia entre fn.pad y fn.crop para el relleno?#
fn.pad añade relleno solo 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.
Link to this section¿DALI produce resultados idénticos a nivel de píxel al preprocesamiento de la CPU?#
Casi idénticos. Configura antialias=False en fn.resize para que coincida con cv2.INTER_LINEAR de OpenCV. Pueden ocurrir pequeñas diferencias de punto flotante (< 0.001) debido a la aritmética de GPU frente a CPU, pero estas no tienen un impacto medible en la precisión de la detección.
Link to this section¿Qué pasa con CV-CUDA como alternativa a DALI?#
CV-CUDA es otra biblioteca de NVIDIA para el procesamiento de visión acelerado por GPU. Proporciona control por operador (como OpenCV pero en GPU) en lugar del enfoque de canalización de DALI. cvcuda.copymakeborder() de CV-CUDA admite un relleno explícito por lado, lo que hace que el letterbox centrado sea sencillo. Elige DALI para flujos de trabajo basados en canalizaciones (especialmente con Triton) y CV-CUDA para un control detallado a nivel de operador en código de inferencia personalizado.