Pre-elaborazione accelerata dalla GPU con NVIDIA DALI#
Quando distribuisci in produzione i modelli Ultralytics YOLO, la pre-elaborazione diventa spesso il collo di bottiglia. Mentre TensorRT può eseguire l'inferenza del modello in pochi millisecondi, la pre-elaborazione basata su CPU (ridimensionamento, padding, normalizzazione) può richiedere da 2 a 10 ms per immagine, soprattutto alle alte risoluzioni. NVIDIA DALI (Data Loading Library) risolve il problema trasferendo l'intera pipeline di pre-elaborazione sulla GPU.
Questa guida illustra come creare pipeline DALI che riproducono esattamente la pre-elaborazione di Ultralytics YOLO, integrarle con model.predict(), elaborare flussi video e distribuire la soluzione end-to-end con Triton Inference Server.
Questa guida è rivolta agli ingegneri che distribuiscono modelli YOLO in ambienti di produzione in cui la pre-elaborazione sulla CPU rappresenta un collo di bottiglia misurato: in genere distribuzioni TensorRT su GPU NVIDIA, pipeline video ad alto throughput o configurazioni di Triton Inference Server. Se esegui inferenze 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 riprodurre sulla GPU la pre-elaborazione letterbox di YOLO. - Stai integrando DALI con Ultralytics? Passa l'output di DALI come
torch.Tensoramodel.predict(): Ultralytics salta automaticamente la pre-elaborazione delle immagini. - Stai distribuendo con Triton? Usa il backend DALI con un ensemble TensorRT per eliminare la pre-elaborazione sulla CPU.
Perché usare DALI per la pre-elaborazione di YOLO#
In una tipica pipeline di inferenza YOLO, le fasi di pre-elaborazione vengono eseguite sulla CPU:
- Decodifica dell'immagine (JPEG/PNG)
- Ridimensionamento preservando le proporzioni
- Adatta alle dimensioni target (letterbox)
- Normalizza i valori dei pixel da
[0, 255]a[0, 1] - Converti il layout da HWC a CHW
Con DALI, tutte queste operazioni vengono eseguite sulla GPU, eliminando il collo di bottiglia della CPU. Questo è particolarmente utile quando:
| Scenario | Perché DALI è utile |
|---|---|
| Inferenza rapida su GPU | Motori TensorRT con inferenza inferiore al millisecondo rendono il pre-processing sulla CPU il costo predominante |
| Input ad alta risoluzione | I flussi video a 1080p e 4K richiedono operazioni di ridimensionamento costose |
| Dimensioni dei batch elevate | Inferenza lato server che elabora molte immagini in parallelo |
| Numero limitato di core CPU | Dispositivi edge come NVIDIA Jetson o server GPU ad alta densità con pochi core CPU per GPU |
Prerequisiti#
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-cuda130Requisiti:
- GPU NVIDIA (compute capability 5.0+ / Maxwell o versioni successive)
- CUDA 11.0+, 12.0+ o 13.0+
- Python 3.10-3.14
- Sistema operativo Linux
Comprendere il pre-processing di YOLO#
Prima di creare una pipeline DALI, è utile capire esattamente cosa fa Ultralytics durante il pre-processing. La classe principale è LetterBox in ultralytics/data/augment.py:
from ultralytics.data.augment import LetterBox
letterbox = LetterBox(
new_shape=(640, 640), # Dimensione target
center=True, # Centra l'immagine (aggiungi padding in modo uniforme su entrambi i lati)
stride=32, # Allineamento allo stride
padding_value=114, # Padding grigio (114, 114, 114)
)La pipeline completa di pre-processing 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 | Padding 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)) - Aggiunta di padding grigio (
114) allo spazio rimanente per raggiungere le dimensioni target - Centrare l'immagine in modo che il padding sia distribuito uniformemente su entrambi i lati
Pipeline DALI per YOLO#
La pipeline DALI consigliata replica il comportamento predefinito di Ultralytics LetterBox(center=True), utilizzato dall'inferenza standard di YOLO.
Pipeline centrata (consigliata, corrisponde a Ultralytics LetterBox)#
Questa versione replica esattamente il pre-processing predefinito di Ultralytics con padding centrato, corrispondente a LetterBox(center=True):
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.
"""
# Leggi e decodifica le immagini sulla 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)
# Ridimensionamento con mantenimento delle proporzioni
resized = fn.resize(
images,
resize_x=target_size,
resize_y=target_size,
mode="not_larger",
interp_type=types.INTERP_LINEAR,
antialias=False, # Corrisponde a cv2.INTER_LINEAR (senza antialiasing)
)
# Padding centrato con fn.crop e out_of_bounds_policy
# Quando le dimensioni del ritaglio superano quelle dell'immagine, fn.crop centra l'immagine e aggiunge padding simmetrico
padded = fn.crop(
resized,
crop=(target_size, target_size),
out_of_bounds_policy="pad",
fill_values=114, # Valore di padding YOLO
)
# Normalizza e converti il 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 ti serve una corrispondenza esatta con LetterBox(center=True), puoi semplificare il passaggio del padding usando fn.pad(...) anziché fn.crop(..., out_of_bounds_policy="pad"). Questa variante aggiunge padding solo ai bordi destro e inferiore, cosa che può essere accettabile per pipeline di distribuzione personalizzate, ma non corrisponde esattamente al comportamento letterbox centrato predefinito di Ultralytics.
L'operatore DALI fn.pad aggiunge padding solo ai bordi destro e inferiore. Per ottenere un padding centrato (corrispondente a Ultralytics LetterBox(center=True)), usa fn.crop con out_of_bounds_policy="pad". Con i valori predefiniti crop_pos_x=0.5 e crop_pos_y=0.5, l'immagine viene centrata automaticamente con padding simmetrico.
L'operatore DALI fn.resize attiva l'antialiasing per impostazione predefinita (antialias=True), mentre OpenCV cv2.resize con INTER_LINEAR non applica l'antialiasing. Imposta sempre antialias=False in DALI per ottenere una corrispondenza con la pipeline CPU. Se lo ometti, si verificano sottili differenze numeriche che possono influire sulla precisione del modello.
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}]")Usare DALI con Ultralytics Predict#
Puoi passare direttamente un tensore PyTorch pre-elaborato a model.predict(). Quando viene passato un torch.Tensor, Ultralytics salta il pre-processing dell'immagine (letterbox, BGR→RGB, HWC→CHW e normalizzazione /255) ed esegue solo il trasferimento sul dispositivo e la conversione del tipo di dato prima di inviarlo al modello.
Poiché in questo caso Ultralytics non ha accesso alle dimensioni originali dell'immagine, le coordinate dei riquadri di rilevamento vengono restituite nello spazio letterbox 640×640. Per ricondurle alle coordinate dell'immagine originale, usa scale_boxes, che applica la logica di arrotondamento esatta usata da LetterBox:
from ultralytics.utils.ops import scale_boxes
# riquadri: tensore di forma (N, 4) in formato xyxy, coordinate letterbox 640x640
# Riporta i riquadri dalle coordinate letterbox (640, 640) a quelle originali (orig_h, orig_w)
boxes = scale_boxes((640, 640), boxes, (orig_h, orig_w))Questo vale per tutti i flussi di pre-processing esterni: input diretto di tensori, flussi video e distribuzione con 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 pre-processing dell'immagine richiede ~0.004ms (praticamente zero), rispetto a ~1-10ms con il pre-processing sulla 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 la conversione del tipo di dato.
DALI con flussi video#
Per l'elaborazione video in tempo reale, usa fn.external_source per fornire frame da qualsiasi sorgente: OpenCV, GStreamer o librerie di acquisizione personalizzate:
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."""
# Sorgente esterna per fornire frame da OpenCV, GStreamer ecc.
frames = fn.external_source(device="cpu", name="input")
frames = fn.reshape(frames, layout="HWC")
# Sposta sulla GPU ed esegui il pre-processing
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 il pre-processing DALI con l'inferenza TensorRT in Triton Inference Server usando un modello ensemble. Questo elimina completamente il pre-processing sulla CPU: entrano byte JPEG grezzi ed escono le rilevazioni, con tutto il processo eseguito 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:
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: byte grezzi dell'immagine codificata da 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
# Serializza la pipeline nel repository del modello
pipe = triton_dali_pipeline()
pipe.serialize(filename="model_repository/dali_preprocessing/1/model.dali")Passaggio 2: esporta YOLO in TensorRT#
from pathlib import Path
from ultralytics import YOLO
model = YOLO("yolo26n.pt")
engine_path = model.export(
format="engine", imgsz=640, quantize=16, batch=8, dynamic=True, nms=False
) # Senza NMS (N, 300, 6); TensorRT >= 8.5
# Ultralytics antepone un'intestazione di metadati ai file .engine; rimuovila affinché Triton possa caricare il piano TensorRT grezzo
with open(engine_path, "rb") as f:
meta_len = int.from_bytes(f.read(4), byteorder="little") # lunghezza dell'intestazione dei metadati JSON
f.seek(4 + meta_len)
plan = f.read()
Path("model_repository/yolo_trt/1").mkdir(parents=True, exist_ok=True)
Path("model_repository/yolo_trt/1/model.plan").write_bytes(plan)Passaggio 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. Il valore output_map "preprocessed_image" nel passaggio DALI corrisponde al valore input_map "preprocessed_image" nel passaggio TensorRT. Sono nomi arbitrari che collegano l'output di un passaggio all'input del passaggio successivo: non devono corrispondere ai nomi dei tensori interni dei modelli.
Passaggio 4: invia richieste di inferenza#
Ultralytics offre il supporto Triton integrato, che gestisce automaticamente la pre-elaborazione e la post-elaborazione. Tuttavia, non funziona con l'ensemble DALI perché YOLO() invia un tensore float32 pre-elaborato, mentre l'ensemble si aspetta byte JPEG grezzi. Usa direttamente tritonclient per gli ensemble DALI e l'integrazione integrata per le distribuzioni standard senza 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 for the nms=False export)
detections = detections[0] # First image
detections = detections[detections[:, 4] > 0.25] # Confidence threshold
print(f"Detected {len(detections)} objects")Quando invii a Triton un batch di immagini JPEG, porta tutti gli array di byte codificati alla stessa lunghezza (il numero massimo di byte nel batch). Triton richiede forme omogenee per i tensori di input del batch.
Attività supportate#
La pre-elaborazione DALI funziona con tutte le attività YOLO che usano la pipeline standard LetterBox:
| Attività | Supportato | Note |
|---|---|---|
| Rilevamento | ✅ | Pre-elaborazione letterbox standard |
| Segmentazione di istanze | ✅ | Stessa pre-elaborazione del rilevamento |
| Segmentazione semantica | ✅ | Stessa pre-elaborazione delle immagini del rilevamento |
| Stima della profondità | ✅ | Stessa pre-elaborazione delle immagini del rilevamento |
| Classificazione | ❌ | Usa le trasformazioni torchvision (ritaglio centrale), non il letterbox |
| Stima della posa | ✅ | Stessa pre-elaborazione del rilevamento |
| Rilevamento orientato (OBB) | ✅ | Stessa pre-elaborazione del rilevamento |
Limitazioni#
- Solo Linux: DALI non supporta Windows o macOS
- È necessaria una GPU NVIDIA: non è disponibile un'alternativa che usi solo la CPU
- Pipeline statica: la struttura della pipeline viene definita in fase di compilazione e non può cambiare dinamicamente
fn.padagisce solo a destra/in basso: usafn.cropconout_of_bounds_policy="pad"per un padding centrato- Nessuna modalità rect: le pipeline DALI producono output di dimensioni fisse (ad es. 640×640). La modalità rect di
auto=True, che produce output di dimensioni variabili (ad es. 384×640), non è supportata. Nota che, anche se TensorRT supporta forme di input dinamiche, una pipeline DALI a dimensione fissa si abbina in modo naturale a un motore a dimensione fissa per ottenere il massimo throughput - Memoria con più istanze: usare
instance_groupconcount> 1 in Triton può causare un elevato consumo di memoria. Usa il gruppo di istanze predefinito per il modello DALI
Domande frequenti#
Il vantaggio dipende dalla pipeline. Quando l'inferenza su GPU è già veloce con TensorRT, la pre-elaborazione su CPU, che richiede 2-10 ms, può diventare il costo principale. DALI elimina questo collo di bottiglia eseguendo la pre-elaborazione sulla GPU. I vantaggi maggiori si ottengono con input ad alta risoluzione (1080p, 4K), dimensioni di batch elevate e sistemi con pochi core CPU per GPU.
Sì. Usa
DALIGenericIteratorper ottenere outputtorch.Tensorpre-elaborati, quindi passali amodel.predict(). Tuttavia, il vantaggio in termini di prestazioni è maggiore con i modelli TensorRT, la cui inferenza è già molto veloce e per i quali la pre-elaborazione su CPU diventa il collo di bottiglia.fn.padaggiunge padding solo ai bordi destro e inferiore.fn.cropconout_of_bounds_policy="pad"centra l'immagine e aggiunge padding simmetrico su tutti i lati, in linea con il comportamento diLetterBox(center=True)di Ultralytics.Quasi identici. Imposta
antialias=Falseinfn.resizeper ottenere risultati corrispondenti acv2.INTER_LINEARdi OpenCV. Possono verificarsi lievi differenze in virgola mobile (< 0.001) dovute ai calcoli su GPU rispetto a quelli su CPU, ma non hanno un impatto misurabile sulla precisione del rilevamento.CV-CUDA è un'altra libreria NVIDIA per l'elaborazione della visione accelerata su GPU. Offre il controllo per singolo operatore (come OpenCV, ma su GPU), invece dell'approccio basato su pipeline di DALI.
cvcuda.copymakeborder()di CV-CUDA supporta il padding esplicito per ciascun lato, semplificando il letterbox centrato. Scegli DALI per i flussi di lavoro basati su pipeline (specialmente con Triton) e CV-CUDA per un controllo dettagliato a livello di operatore nel codice di inferenza personalizzato.