Preelaborazione accelerata dalla GPU con NVIDIA DALI#
Quando distribuisci i modelli Ultralytics YOLO in produzione, la preelaborazione diventa spesso il collo di bottiglia. Anche se TensorRT può eseguire l'inferenza del modello in pochi millisecondi, la preelaborazione basata su CPU (ridimensionamento, padding, normalizzazione) può richiedere 2-10 ms per immagine, soprattutto ad alte risoluzioni. NVIDIA DALI (libreria di caricamento dati) risolve il problema spostando l'intera pipeline di preelaborazione sulla GPU.
Questa guida ti accompagna nella creazione di pipeline DALI che replicano esattamente la preelaborazione di Ultralytics YOLO, nell'integrazione con model.predict(), nell'elaborazione di flussi video e nella distribuzione end-to-end con Triton Inference Server.
Questa guida è rivolta agli ingegneri che distribuiscono modelli YOLO in ambienti di produzione in cui la preelaborazione sulla CPU è 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 preelaborazione, 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 sulla GPU la preelaborazione letterbox di YOLO. - Vuoi integrare DALI con Ultralytics? Passa l'output di DALI come
torch.Tensoramodel.predict(): Ultralytics salta automaticamente la preelaborazione delle immagini. - Vuoi distribuire con Triton? Usa il backend DALI con un ensemble TensorRT per eliminare la preelaborazione sulla CPU.
Perché usare DALI per la preelaborazione di YOLO#
In una tipica pipeline di inferenza YOLO, i passaggi di preelaborazione vengono eseguiti sulla CPU:
- Decodifica dell'immagine (JPEG/PNG)
- Ridimensionamento mantenendo le proporzioni
- Padding fino alla dimensione di destinazione (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 utile quando:
| Scenario | Perché DALI è utile |
|---|---|
| Inferenza GPU veloce | I motori TensorRT con inferenza inferiore al millisecondo rendono la preelaborazione sulla CPU il costo principale |
| 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 necessari:
pip install ultralytics
pip install --extra-index-url https://pypi.nvidia.com nvidia-dali-cuda130Requisiti:
- GPU NVIDIA (compute capability 5.0+ / Maxwell o più recente)
- CUDA 11.0+, 12.0+ o 13.0+
- Python 3.10-3.14
- Sistema operativo Linux
Capire la preelaborazione di YOLO#
Prima di creare una pipeline DALI, è utile capire esattamente cosa fa Ultralytics durante la preelaborazione. La classe principale è LetterBox in ultralytics/data/augment.py:
from ultralytics.data.augment import LetterBox
letterbox = LetterBox(
new_shape=(640, 640), # Dimensione di destinazione
center=True, # Centra l'immagine (aggiungi la stessa quantità di padding su entrambi i lati)
stride=32, # Allineamento allo stride
padding_value=114, # Padding grigio (114, 114, 114)
)L'intera pipeline di preelaborazione 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 mantiene le proporzioni tramite:
- Calcolo del fattore di 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 la dimensione di destinazione - Centratura dell'immagine, in modo che il padding sia distribuito equamente su entrambi i lati
Pipeline DALI per YOLO#
La pipeline DALI consigliata replica il comportamento predefinito di Ultralytics LetterBox(center=True), utilizzato dall'inferenza YOLO standard.
Pipeline centrata (consigliata, conforme a Ultralytics LetterBox)#
Questa versione replica esattamente la preelaborazione predefinita 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 che mantiene le 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 usando fn.crop con out_of_bounds_policy
# Quando la dimensione del ritaglio è maggiore di quella 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
)
# Normalizzazione e conversione del 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 una corrispondenza esatta con LetterBox(center=True), puoi semplificare il passaggio di padding usando fn.pad(...) invece di fn.crop(..., out_of_bounds_policy="pad"). Questa variante aggiunge padding solo ai bordi destro e inferiore: 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 abilita l'antialiasing per impostazione predefinita (antialias=True), mentre cv2.resize di OpenCV 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 producono 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](https://ultralytics-translation-0.invalid preelaborato a model.predict(). Quando passi un torch.Tensor, Ultralytics salta la preelaborazione delle immagini (letterbox, BGR→RGB, HWC→CHW e normalizzazione /255) ed esegue solo il trasferimento del dispositivo e la conversione del tipo di dato prima di inviarlo al modello.
Poiché in questo caso Ultralytics non ha accesso alle dimensioni dell'immagine originale, le coordinate dei riquadri di rilevamento vengono restituite nello spazio letterbox 640×640. Per convertirle di nuovo nelle coordinate dell'immagine originale, usa scale_boxes, che applica la stessa logica di arrotondamento esatta usata da LetterBox:
from ultralytics.utils.ops import scale_boxes
# boxes: tensore di forma (N, 4) in formato xyxy, coordinate letterbox 640x640
# Riporta i riquadri dallo spazio letterbox (640, 640) all'immagine originale (orig_h, orig_w)
boxes = scale_boxes((640, 640), boxes, (orig_h, orig_w))Questo vale per tutti i percorsi di preelaborazione 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 preelaborazione delle immagini richiede ~0.004 ms (praticamente zero), rispetto a ~1-10 ms con la preelaborazione sulla CPU. Il tensore deve essere nel formato BCHW, float32 (o float16) e normalizzato a [0, 1]. Ultralytics gestirà comunque automaticamente il trasferimento del 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 fotogrammi 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 fotogrammi da OpenCV, GStreamer e simili.
frames = fn.external_source(device="cpu", name="input")
frames = fn.reshape(frames, layout="HWC")
# Sposta sulla GPU e preelabora
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 preprocessing DALI con l'inferenza TensorRT in Triton Inference Server usando un modello ensemble. In questo modo elimini completamente il preprocessing sulla CPU: entrano byte JPEG grezzi ed escono rilevamenti, con tutto il processamento 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 immagine codificati grezzi 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 dei modelli
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 aggiunge un'intestazione di metadati ai file .engine; rimuovila per consentire a Triton di 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 JSON dei metadati
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 successivo: non devono corrispondere ai nomi dei tensori interni di alcun modello.
Passaggio 4: invia richieste di inferenza#
Ultralytics offre il supporto Triton integrato, che gestisce automaticamente il pre/postprocessing. Tuttavia, non funziona con l'ensemble DALI perché YOLO() invia un tensore float32 preprocessato, mentre l'ensemble si aspetta byte JPEG grezzi. Per gli ensemble DALI, usa direttamente tritonclient; per le distribuzioni standard senza DALI, usa l'integrazione integrata.
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, aggiungi padding a tutti gli array di byte codificati fino a ottenere la stessa lunghezza (il numero massimo di byte nel batch). Triton richiede che le forme del tensore di input siano omogenee nel batch.
Attività supportate#
Il preprocessing DALI funziona con tutti i task YOLO che usano la pipeline standard LetterBox:
| Attività | Supportato | Note |
|---|---|---|
| Rilevamento | ✅ | Preprocessing letterbox standard |
| Segmentazione delle istanze | ✅ | Stesso preprocessing del rilevamento |
| Segmentazione semantica | ✅ | Stesso preprocessing delle immagini del rilevamento |
| Stima della profondità | ✅ | Stesso preprocessing delle immagini del rilevamento |
| Classificazione | ❌ | Usa le trasformazioni di torchvision (ritaglio centrale), non il letterbox |
| Stima della posa | ✅ | Stesso preprocessing del rilevamento |
| Rilevamento orientato (OBB) | ✅ | Stesso preprocessing del rilevamento |
Limitazioni#
- Solo Linux: DALI non supporta Windows o macOS
- È necessaria una GPU NVIDIA: non è disponibile una modalità alternativa basata solo sulla CPU
- Pipeline statica: la struttura della pipeline è definita in fase di compilazione e non può cambiare dinamicamente
fn.padagisce solo a destra/in basso: per centrare il padding, usafn.cropconout_of_bounds_policy="pad"- 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, sebbene TensorRT supporti forme di input dinamiche, una pipeline DALI a dimensioni fisse si abbina naturalmente a un motore a dimensioni fisse per garantire la massima velocità di elaborazione - Memoria con più istanze: usare
instance_groupconcount> 1 in Triton può causare un utilizzo elevato della memoria. Usa il gruppo di istanze predefinito per il modello DALI
Domande frequenti#
Il vantaggio dipende dalla pipeline. Quando l'inferenza sulla GPU è già veloce con TensorRT, il preprocessing sulla CPU, che richiede 2-10 ms, può diventare il costo dominante. DALI elimina questo collo di bottiglia eseguendo il preprocessing sulla GPU. I vantaggi maggiori si ottengono con input ad alta risoluzione (1080p, 4K), batch di grandi dimensioni e sistemi con un numero limitato di core CPU per GPU.
Sì. Usa
DALIGenericIteratorper ottenere outputtorch.Tensorpreprocessati, quindi passali amodel.predict(). Tuttavia, il vantaggio in termini di prestazioni è maggiore con i modelli TensorRT, nei quali l'inferenza è già molto veloce e il preprocessing sulla 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 simmetricamente su tutti i lati, riproducendo il comportamentoLetterBox(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 e CPU, ma non hanno alcun 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) anziché l'approccio basato su pipeline di DALI.
cvcuda.copymakeborder()di CV-CUDA supporta il padding esplicito per lato, semplificando la creazione di un 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.