Pre-elaborazione accelerata da GPU con NVIDIA DALI
Introduzione
Quando distribuisci modelli Ultralytics YOLO modelli in produzione, pre-elaborazione spesso diventa il collo di bottiglia. Mentre TensorRT può eseguire l'inferenza del modello inferenza in pochi millisecondi, la pre-elaborazione basata su CPU (ridimensionamento, riempimento, normalizzazione) può richiedere 2-10ms per immagine, specialmente ad alte risoluzioni. NVIDIA DALI (Data Loading Library) risolve questo problema spostando l'intera pipeline di pre-elaborazione sulla GPU.
Questa guida ti accompagna nella creazione di pipeline DALI che replicano esattamente la pre-elaborazione di Ultralytics YOLO, integrandole con model.predict(), elaborando flussi video e distribuendo end-to-end con Triton Inference Server.
Questa guida è per gli ingegneri che distribuiscono modelli YOLO in ambienti di produzione dove la pre-elaborazione della CPU rappresenta un collo di bottiglia misurato — tipicamente TensorRT distribuzioni su GPU NVIDIA, pipeline video ad alta velocità, o Triton Inference Server configurazioni. Se stai eseguendo un'inferenza standard con model.predict() e non hai un collo di bottiglia nella pre-elaborazione, la pipeline CPU predefinita funziona bene.
- Stai creando una pipeline DALI? Usa
fn.resize(mode="not_larger")+fn.crop(out_of_bounds_policy="pad")+fn.crop_mirror_normalizeper replicare la pre-elaborazione letterbox di YOLO su GPU. - Ti stai integrando con Ultralytics? Passa l'output DALI come
torch.Tensoramodel.predict()— Ultralytics salta automaticamente la pre-elaborazione dell'immagine. - Stai distribuendo con Triton? Usa il backend DALI con un ensemble TensorRT per una pre-elaborazione a zero carico su CPU.
Perché usare DALI per la pre-elaborazione di YOLO
In una tipica pipeline di inferenza YOLO, i passaggi di pre-elaborazione vengono eseguiti sulla CPU:
- Decodifica l'immagine (JPEG/PNG)
- Ridimensionamento mantenendo le proporzioni
- Riempimento alla dimensione target (letterbox)
- Normalizzazione dei valori dei pixel da
[0, 255]a[0, 1] - Conversione del layout da HWC a CHW
Con DALI, tutte queste operazioni vengono eseguite sulla GPU, eliminando il collo di bottiglia della CPU. Questo è particolarmente prezioso quando:
| Scenario | Perché DALI è utile |
|---|---|
| Inferenza GPU rapida | TensorRT motori con inferenza sub-millisecondo rendono la pre-elaborazione CPU il costo principale |
| Input ad alta risoluzione | flussi video 1080p e 4K richiedono costose operazioni di ridimensionamento |
| Grandi batch size | Inferenza lato server che elabora molte immagini in parallelo |
| Core CPU limitati | Dispositivi edge come NVIDIA Jetson, o server GPU densi con pochi core CPU per GPU |
Prima di iniziare, assicurati di avere accesso a un workspace AzureML. Se non ne hai uno, puoi crearne uno nuovo
NVIDIA DALI supporta solo Linux. Non è disponibile su Windows o macOS.
Installa i pacchetti richiesti:
pip install ultralytics
pip install --extra-index-url https://pypi.nvidia.com nvidia-dali-cuda120Requisiti:
- GPU NVIDIA (capacità di calcolo 5.0+ / Maxwell o più recente)
- CUDA 11.0+ o 12.0+
- Python 3.10-3.14
- Sistema operativo Linux
Comprendere la pre-elaborazione di YOLO
Prima di costruire una pipeline DALI, è utile capire esattamente cosa fa Ultralytics durante la pre-elaborazione. La classe chiave è LetterBox in 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)
)L'intera pipeline di pre-elaborazione in ultralytics/engine/predictor.py esegue questi passaggi:
| Passaggio | Operazione | Funzione CPU | Equivalente DALI |
|---|---|---|---|
| 1 | Ridimensionamento letterbox | cv2.resize | fn.resize(mode="not_larger") |
| 2 | Riempimento centrato | cv2.copyMakeBorder | fn.crop(out_of_bounds_policy="pad") |
| 3 | BGR → RGB | im[..., ::-1] | fn.decoders.image(output_type=types.RGB) |
| 4 | HWC → CHW + normalizzazione /255 | np.transpose + tensor / 255 | fn.crop_mirror_normalize(std=[255,255,255]) |
L'operazione letterbox preserva le proporzioni tramite:
- Calcolo della scala:
r = min(target_h / h, target_w / w) - Ridimensionamento a
(round(w * r), round(h * r)) - Riempimento dello spazio rimanente con grigio (
114) per raggiungere la dimensione target - Centratura dell'immagine in modo che il riempimento sia distribuito equamente su entrambi i lati
Pipeline DALI per YOLO
Usa la pipeline centrata qui sotto come riferimento predefinito. Corrisponde al comportamento di Ultralytics LetterBox(center=True), che è quello utilizzato dall'inferenza standard YOLO.
Pipeline centrata (consigliata, corrisponde a LetterBox di Ultralytics)
Questa versione replica esattamente la pre-elaborazione predefinita di Ultralytics con riempimento centrato, corrispondente a 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 outputSe non hai bisogno di un'esatta LetterBox(center=True) parità, puoi semplificare il passaggio di riempimento usando fn.pad(...) invece di fn.crop(..., out_of_bounds_policy="pad"). Quella variante riempie solo i bordi destro e inferiore, il che può essere accettabile per pipeline di distribuzione personalizzate, ma non corrisponderà esattamente al comportamento di letterbox centrato predefinito di Ultralytics.
L'operatore fn.pad di DALI aggiunge riempimento solo ai bordi destro e inferiore. Per ottenere un riempimento centrato (corrispondente a Ultralytics LetterBox(center=True)), usa fn.crop con out_of_bounds_policy="pad". Con il predefinito crop_pos_x=0.5 e crop_pos_y=0.5, l'immagine viene automaticamente centrata con un riempimento simmetrico.
L'operatore fn.resize abilita l'antialiasing per impostazione predefinita (antialias=True), mentre cv2.resize con INTER_LINEAR di OpenCV nonnon applica l'antialiasing. Imposta sempre antialias=False in DALI per corrispondere alla pipeline CPU. Ometterlo causa sottili differenze numeriche che possono influenzare model accuracy.
Esecuzione della Pipeline
# 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}]")Utilizzo di DALI con Ultralytics Predict
Puoi passare un PyTorch tensore pre-elaborato direttamente a model.predict(). Quando viene passato un torch.Tensor, Ultralytics salta la pre-elaborazione delle immagini (letterbox, BGR→RGB, HWC→CHW e normalizzazione /255) ed esegue solo il trasferimento sul dispositivo e il cast del dtype prima di inviarlo al modello.
Poiché Ultralytics non ha accesso alle dimensioni originali dell'immagine in questo caso, le coordinate del riquadro di rilevamento vengono restituite nello spazio letterbox 640×640. Per mapparle nuovamente alle coordinate dell'immagine originale, usa scale_boxes che gestisce l'esatta logica di arrotondamento utilizzata da 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))Questo si applica a tutti i percorsi di pre-elaborazione esterni: input diretto di tensori, flussi video e distribuzione 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")Quando passi un torch.Tensor a model.predict(), il passaggio di pre-elaborazione dell'immagine richiede ~0,004ms (essenzialmente zero) rispetto ai ~1-10ms con la pre-elaborazione CPU. Il tensore deve essere in formato BCHW, float32 (o float16) e normalizzato a [0, 1]. Ultralytics gestirà comunque automaticamente il trasferimento sul dispositivo e il cast del dtype.
DALI con flussi video
Per l'elaborazione video in tempo reale, usa fn.external_source per alimentare i frame da qualsiasi fonte: OpenCV, GStreamer o librerie di acquisizione personalizzate:
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 outputTriton Inference Server con DALI
Per la distribuzione in produzione, combina la pre-elaborazione DALI con l'inferenza TensorRT in Triton Inference Server utilizzando un modello ensemble. Questo elimina completamente la pre-elaborazione CPU: entrano byte JPEG grezzi, escono rilevamenti, con tutto elaborato sulla GPU.
Struttura del Repository del Modello
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.pbtxtPassaggio 1: Crea la Pipeline DALI
Serializza la pipeline DALI per il backend DALI di 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")Passaggio 2: Esporta YOLO in 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.planPassaggio 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"
}
}
]
}L'ensemble collega i modelli tramite nomi di tensori virtuali. La output_map il valore "preprocessed_image" nel passaggio DALI corrisponde al input_map il valore "preprocessed_image" nel passaggio TensorRT. Questi sono nomi arbitrari che collegano l'output di un passaggio all'input del passaggio successivo: non devono corrispondere ai nomi dei tensori interni di alcun modello.
Passaggio 4: Invia richieste di inferenza
!!! info "Perché tritonclient invece di 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")Quando invii un batch di immagini JPEG a Triton, applica il padding a tutti gli array di byte codificati alla stessa lunghezza (il conteggio massimo di byte nel batch). Triton richiede forme di batch omogenee per il tensore di input.
Attività supportate
La pre-elaborazione DALI funziona con tutte le attività YOLO che utilizzano la pipeline standard LetterBox :
| Attività | Supportato | Note |
|---|---|---|
| Rilevamento | ✅ | Pre-elaborazione letterbox standard |
| segmentazione | ✅ | Stessa pre-elaborazione del rilevamento |
| Pose Estimation | ✅ | Stessa pre-elaborazione del rilevamento |
| Rilevamento Orientato (OBB) | ✅ | Stessa pre-elaborazione del rilevamento |
| Classificazione | ❌ | Utilizza trasformazioni torchvision (center crop), non letterbox |
Limitazioni
- solo Linux: DALI non supporta Windows o macOS
- Richiesta GPU NVIDIA: Nessun fallback solo CPU
- Pipeline statica: La struttura della pipeline è definita al momento della compilazione e non può cambiare dinamicamente
fn.padè solo destra/basso: Usafn.cropconout_of_bounds_policy="pad"per il padding centrato- Nessuna modalità rettangolare: Le pipeline DALI producono output di dimensioni fisse (es. 640×640). La
auto=Truemodalità rettangolare che produce output di dimensioni variabili (es. 384×640) non è supportata. Nota che sebbene TensorRT supporti forme di input dinamiche, una pipeline DALI di dimensioni fisse si abbina naturalmente a un motore di dimensioni fisse per il massimo throughput - Memoria con istanze multiple: Utilizzare
instance_groupconcount> 1 in Triton può causare un elevato utilizzo della memoria. Usa il gruppo di istanze predefinito per il modello DALI
FAQ
Come si confronta la pre-elaborazione DALI con la velocità di pre-elaborazione della CPU?
Il vantaggio dipende dalla tua pipeline. Quando l'inferenza GPU è già veloce con TensorRT, la pre-elaborazione della CPU a 2-10ms può diventare il costo dominante. DALI elimina questo collo di bottiglia eseguendo la pre-elaborazione sulla GPU. I maggiori guadagni si riscontrano con input ad alta risoluzione (1080p, 4K), grandi batch size e sistemi con un numero limitato di core CPU per GPU.
Posso usare DALI con modelli PyTorch (non solo TensorRT)?
Sì. Usa DALIGenericIterator per ottenere output pre-elaborati torch.Tensor, quindi passali a model.predict(). Tuttavia, il vantaggio in termini di prestazioni è maggiore con modelli TensorRT dove l'inferenza è già molto veloce e la pre-elaborazione della CPU diventa il collo di bottiglia.
Qual è la differenza tra fn.pad e fn.crop per il padding?
fn.pad aggiunge padding solo ai bordi destro e inferiore. fn.crop con out_of_bounds_policy="pad" centra l'immagine e aggiunge padding simmetricamente su tutti i lati, corrispondendo al comportamento Ultralytics LetterBox(center=True) .
DALI produce risultati identici ai pixel rispetto alla pre-elaborazione della CPU?
Quasi identici. Imposta antialias=False in fn.resize per corrispondere a cv2.INTER_LINEAR di OpenCV. Possono verificarsi lievi differenze in virgola mobile (< 0,001) a causa dell'aritmetica GPU vs CPU, ma queste non hanno alcun impatto misurabile sul rilevamento precisione.
Che dire di CV-CUDA come alternativa a DALI?
CV-CUDA è un'altra libreria NVIDIA per l'elaborazione visiva accelerata dalla GPU. Fornisce un controllo per operatore (come OpenCV ma su GPU) anziché l'approccio a pipeline di DALI. Il cvcuda.copymakeborder() di CV-CUDA supporta il padding esplicito per lato, rendendo semplice il letterbox centrato. Scegli DALI per flussi di lavoro basati su pipeline (specialmente con Triton) e CV-CUDA per un controllo granulare a livello di operatore nel codice di inferenza personalizzato.