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. Sebbene TensorRT possa eseguire l'inferenza del modello in pochi millisecondi, la pre-elaborazione basata sulla CPU (ridimensionamento, padding, normalizzazione) può richiedere 2-10 ms per immagine, soprattutto alle alte risoluzioni. NVIDIA DALI (libreria di caricamento dei dati) risolve il 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, nella loro integrazione con model.predict(), nell'elaborazione di flussi video e nella distribuzione end-to-end con Triton Inference Server.
Questa guida è destinata 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 con Triton Inference Server. Se esegui l'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 sulla GPU. - Stai integrando con Ultralytics? Passa l'output DALI come
torch.Tensoramodel.predict(): Ultralytics salta automaticamente la pre-elaborazione dell'immagine. - Stai effettuando il deployment con Triton? Usa il backend DALI con un ensemble TensorRT per eliminare completamente 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 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 rapida sulla GPU | I motori TensorRT con inferenza inferiore al millisecondo rendono la pre-elaborazione 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 | Elaborazione dell'inferenza lato server di 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 versione successiva)
- CUDA 11.0+, 12.0+ o 13.0+
- Python 3.10-3.14
- Sistema operativo Linux
Comprendere la pre-elaborazione di YOLO#
Prima di creare una pipeline DALI, è utile comprendere esattamente cosa fa Ultralytics durante la pre-elaborazione. La classe principale è 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)
)La pipeline completa 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 | 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 mediante:
- Calcolo della scala:
r = min(target_h / h, target_w / w) - Ridimensionamento a
(round(w * r), round(h * r)) - Aggiunta di padding allo spazio rimanente con il grigio (
114) per raggiungere la dimensione di destinazione - Centratura dell'immagine, in modo che il padding venga distribuito equamente su entrambi i lati
Pipeline DALI per YOLO#
La pipeline DALI consigliata replica il comportamento predefinito LetterBox(center=True) di Ultralytics, utilizzato dall'inferenza YOLO standard.
Pipeline centrata (consigliata, corrisponde a Ultralytics LetterBox)#
Questa versione replica esattamente la pre-elaborazione 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.
"""
# 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 ti serve 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, il che può essere accettabile per pipeline di deployment personalizzate, ma non corrisponderà esattamente al comportamento letterbox centrato predefinito di Ultralytics.
L'operatore fn.pad di DALI aggiunge padding solo ai bordi destro e inferiore. Per ottenere un padding centrato (corrispondente a LetterBox(center=True) di Ultralytics), 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.
fn.resize di DALI 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 corrispondere alla pipeline CPU. L'omissione di questa impostazione causa sottili differenze numeriche che possono influire sull'accuratezza 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 la pre-elaborazione dell'immagine (letterbox, BGR→RGB, HWC→CHW e normalizzazione /255) ed esegue solo il trasferimento sul dispositivo e la conversione del tipo di dati 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 riportarle alle coordinate dell'immagine originale, usa scale_boxes, che gestisce la logica di arrotondamento esatta 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 vale per tutti i percorsi di pre-elaborazione esterni: input tensoriale diretto, flussi video e deployment 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.004 ms (essenzialmente zero), rispetto a ~1-10 ms con la pre-elaborazione 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 dati.
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."""
# 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 il deployment in produzione, combina la pre-elaborazione DALI con l'inferenza TensorRT in Triton Inference Server usando un modello ensemble. In questo modo elimini completamente la pre-elaborazione 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: 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, quantize=16, batch=8, dynamic=True, nms=False
) # NMS-free (N, 300, 6); TensorRT >= 8.5
# 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. Il valore "preprocessed_image" di output_map nel passaggio DALI corrisponde al valore "preprocessed_image" di input_map nel passaggio TensorRT. Si tratta di 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 le richieste di inferenza#
Ultralytics offre supporto Triton integrato che gestisce automaticamente la pre- e post-elaborazione. Tuttavia, non funzionerà 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 i deployment 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 un batch di immagini JPEG a Triton, aggiungi padding a tutti gli array di byte codificati fino alla stessa lunghezza (il numero massimo di byte nel batch). Triton richiede forme omogenee dei batch per il tensore di input.
Attività supportate#
La pre-elaborazione DALI funziona con tutte le attività YOLO che usano la pipeline standard LetterBox:
| Attività | Supportate | Note |
|---|---|---|
| Rilevamento | ✅ | Preprocessing standard letterbox |
| Segmentazione delle istanze | ✅ | Stesso preprocessing del rilevamento |
| Segmentazione semantica | ✅ | Stesso preprocessing delle immagini del rilevamento |
| Classificazione | ❌ | Utilizza 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 alcun fallback solo CPU
- Pipeline statica: la struttura della pipeline viene definita al momento della compilazione e non può cambiare dinamicamente
fn.padsolo a destra/in basso: usafn.cropconout_of_bounds_policy="pad"per una spaziatura centrata- 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. Tieni presente che, sebbene TensorRT supporti forme di input dinamiche, una pipeline DALI di dimensioni fisse si abbina naturalmente a un motore di dimensioni fisse per ottenere il throughput massimo - Memoria con più istanze: l'utilizzo di
instance_groupconcount> 1 in Triton può causare un utilizzo elevato della memoria. Usa il gruppo di istanze predefinito per il modello DALI
FAQ#
Il vantaggio dipende dalla tua pipeline. Quando l'inferenza sulla GPU è già veloce con TensorRT, il preprocessing sulla CPU, con un tempo di 2-10 ms, può diventare il costo principale. DALI elimina questo collo di bottiglia eseguendo il preprocessing sulla GPU. I vantaggi maggiori si ottengono con input ad alta risoluzione (1080p, 4K), dimensioni dei batch elevate e sistemi con un numero limitato di core CPU per GPU.
Sì. Usa
DALIGenericIteratorper ottenere outputtorch.Tensorsottoposti a preprocessing, quindi passali amodel.predict(). Tuttavia, il vantaggio in termini di prestazioni è massimo con i modelli TensorRT, nei quali l'inferenza è già molto veloce e il preprocessing sulla CPU diventa il collo di bottiglia.fn.padaggiunge spaziatura solo ai bordi destro e inferiore.fn.cropconout_of_bounds_policy="pad"centra l'immagine e aggiunge spaziatura simmetricamente su tutti i lati, riproducendo il comportamento di UltralyticsLetterBox(center=True).Quasi identici. Imposta
antialias=Falseinfn.resizein modo che corrisponda acv2.INTER_LINEARdi OpenCV. Possono verificarsi lievi differenze in virgola mobile (< 0.001) dovute all'aritmetica su GPU rispetto a quella su CPU, ma non hanno alcun impatto misurabile sull'accuratezza del rilevamento.CV-CUDA è un'altra libreria NVIDIA per l'elaborazione della visione accelerata su GPU. Fornisce un controllo per operatore (come OpenCV, ma su GPU) invece dell'approccio basato su pipeline di DALI.
cvcuda.copymakeborder()di CV-CUDA supporta la spaziatura esplicita per lato, rendendo semplice il letterbox centrato. Scegli DALI per i workflow basati su pipeline (soprattutto con Triton) e CV-CUDA per un controllo dettagliato a livello di operatore nel codice di inferenza personalizzato.