Link to this sectionPreprocessing accelerato via GPU con NVIDIA DALI#
Link to this sectionIntroduzione#
Quando distribuisci modelli Ultralytics YOLO in produzione, il preprocessing diventa spesso il collo di bottiglia. Sebbene TensorRT possa eseguire l'inference del modello in pochi millisecondi, il preprocessing basato su CPU (ridimensionamento, padding, normalizzazione) può richiedere 2-10ms per immagine, specialmente ad alte risoluzioni. NVIDIA DALI (Data Loading Library) risolve questo problema spostando l'intero workflow di preprocessing sulla GPU.
Questa guida ti accompagna nella creazione di pipeline DALI che replicano esattamente il preprocessing di Ultralytics YOLO, integrandole con model.predict(), elaborando stream video e distribuendo end-to-end con Triton Inference Server.
Questa guida è destinata agli ingegneri che distribuiscono modelli YOLO in ambienti di produzione dove il preprocessing su CPU è un collo di bottiglia misurato — tipicamente distribuzioni TensorRT su GPU NVIDIA, pipeline video ad alto throughput o configurazioni Triton Inference Server. Se esegui un'inferenza standard con model.predict() e non hai problemi di colli di bottiglia nel preprocessing, la pipeline CPU predefinita funziona bene.
- Stai costruendo una pipeline DALI? Usa
fn.resize(mode="not_larger")+fn.crop(out_of_bounds_policy="pad")+fn.crop_mirror_normalizeper replicare il preprocessing letterbox di YOLO su GPU. - Integrazione con Ultralytics? Passa l'output di DALI come
torch.Tensoramodel.predict()— Ultralytics salterà automaticamente il preprocessing dell'immagine. - Distribuzione con Triton? Usa il backend DALI con un ensemble TensorRT per un preprocessing a zero carico su CPU.
Link to this sectionPerché usare DALI per il preprocessing di YOLO#
In una tipica pipeline di inferenza YOLO, i passaggi di preprocessing vengono eseguiti sulla CPU:
- Decodifica dell'immagine (JPEG/PNG)
- Ridimensionamento mantenendo le proporzioni
- Padding 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 aiuta |
|---|---|
| Inferenza GPU veloce | I motori TensorRT con inferenza sub-millisecondo rendono il preprocessing CPU il costo predominante |
| Input ad alta risoluzione | Gli stream video 1080p e 4K richiedono operazioni di ridimensionamento costose |
| Batch sizes elevati | 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 |
Link to this sectionPrerequisiti#
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 successiva)
- CUDA 11.0+, 12.0+ o 13.0+
- Python 3.10-3.14
- Sistema operativo Linux
Link to this sectionComprendere il preprocessing di YOLO#
Prima di costruire una pipeline DALI, è utile capire esattamente cosa fa Ultralytics durante il preprocessing. 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 preprocessing 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 di letterbox preserva le proporzioni:
- Calcolando la scala:
r = min(target_h / h, target_w / w) - Ridimensionando a
(round(w * r), round(h * r)) - Riempendo lo spazio rimanente con grigio (
114) per raggiungere la dimensione target - Centrando l'immagine in modo che il padding sia distribuito equamente su entrambi i lati
Link to this sectionPipeline DALI per YOLO#
Usa la pipeline centrata qui sotto come riferimento predefinito. Corrisponde al comportamento di LetterBox(center=True) di Ultralytics, che è quello utilizzato dall'inferenza YOLO standard.
Link to this sectionPipeline centrata (Consigliata, corrisponde a Ultralytics LetterBox)#
Questa versione replica esattamente il preprocessing predefinito di Ultralytics con padding 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 una parità esatta con LetterBox(center=True), puoi semplificare il passaggio di padding usando fn.pad(...) invece di fn.crop(..., out_of_bounds_policy="pad"). Quella variante aggiunge il padding solo ai bordi destro e inferiore, il che può essere accettabile per pipeline di distribuzione personalizzate, ma non corrisponderà esattamente al comportamento predefinito del letterbox centrato 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 automaticamente centrata con un 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. Omettere ciò causa sottili differenze numeriche che possono influenzare l'accuratezza del modello.
Link to this sectionEsecuzione 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}]")Link to this sectionUso di DALI con Ultralytics Predict#
Puoi passare un tensore PyTorch pre-elaborato direttamente a model.predict(). Quando viene passato un torch.Tensor, Ultralytics salta il preprocessing dell'immagine (letterbox, BGR→RGB, HWC→CHW e normalizzazione /255) ed esegue solo il trasferimento sul dispositivo e il cast del tipo di dati prima di inviarlo al modello.
Dato che in questo caso Ultralytics non ha accesso alle dimensioni originali dell'immagine, le coordinate del box di rilevamento vengono restituite nello spazio letterbox 640×640. Per rimapparle 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 vale per tutti i percorsi di preprocessing esterni — input tensore diretto, stream 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 preprocessing dell'immagine richiede ~0.004ms (praticamente zero) rispetto a ~1-10ms con preprocessing 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.
Link to this sectionDALI con stream video#
Per l'elaborazione video in tempo reale, usa fn.external_source per alimentare i frame da qualsiasi sorgente — OpenCV, GStreamer o librerie di cattura 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 outputLink to this sectionTriton Inference Server con DALI#
Per la distribuzione in produzione, combina il preprocessing DALI con l'inferenza TensorRT in Triton Inference Server utilizzando un modello ensemble. Ciò elimina completamente il preprocessing CPU — entrano byte JPEG grezzi, escono i rilevamenti, con tutto elaborato sulla GPU.
Link to this sectionStruttura 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.pbtxtLink to this sectionPassaggio 1: Crea la pipeline DALI#
Serializza la pipeline DALI per il backend Triton DALI:
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 sectionPassaggio 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.planLink to this sectionPassaggio 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. 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 nessun modello.
Link to this sectionPassaggio 4: Invia richieste di inferenza#
Ultralytics dispone di un supporto Triton integrato che gestisce automaticamente la pre/post-elaborazione. Tuttavia, non funzionerà con l'insieme DALI perché YOLO() invia un tensore float32 pre-elaborato mentre l'insieme si aspetta byte JPEG grezzi. Usa tritonclient direttamente per gli insiemi 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 — 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, esegui il padding di tutti gli array di byte codificati alla stessa lunghezza (il numero massimo di byte nel batch). Triton richiede forme di batch omogenee per il tensore di input.
Link to this sectionAttività supportate#
La pre-elaborazione DALI funziona con tutti i task YOLO che utilizzano la pipeline standard LetterBox:
| Compito | Supportato | Note |
|---|---|---|
| Rilevamento | ✅ | Pre-elaborazione standard letterbox |
| Segmentazione di istanze | ✅ | Stessa pre-elaborazione del rilevamento |
| Segmentazione semantica | ✅ | Stessa pre-elaborazione delle immagini del rilevamento |
| Pose Estimation | ✅ | Stessa pre-elaborazione del rilevamento |
| Rilevamento Orientato (OBB) | ✅ | Stessa pre-elaborazione del rilevamento |
| Classificazione | ❌ | Utilizza trasformazioni torchvision (ritaglio centrale), non letterbox |
Link to this sectionLimitazioni#
- Solo Linux: DALI non supporta Windows o macOS
- NVIDIA GPU richiesta: Nessun fallback solo CPU
- Pipeline statica: La struttura della pipeline è definita in fase di compilazione e non può cambiare dinamicamente
fn.padè solo a destra/in basso: Usafn.cropconout_of_bounds_policy="pad"per un padding centrato- Nessuna modalità rettangolare: Le pipeline DALI producono output a dimensione fissa (es. 640×640). La modalità rettangolare
auto=Trueche produce output a dimensione variabile (es. 384×640) non è supportata. Nota che mentre TensorRT supporta forme di input dinamiche, una pipeline DALI a dimensione fissa si abbina naturalmente a un motore a dimensione fissa per il massimo throughput - Memoria con istanze multiple: 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
Link to this sectionFAQ#
Link to this sectionCome si confronta la velocità di pre-elaborazione di DALI con quella 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 vedono con input ad alta risoluzione (1080p, 4K), dimensioni di batch elevate e sistemi con core CPU limitati per GPU.
Link to this sectionPosso usare DALI con modelli PyTorch (non solo TensorRT)?#
Sì. Usa DALIGenericIterator per ottenere output torch.Tensor pre-elaborati, quindi passali a model.predict(). Tuttavia, il vantaggio in termini di prestazioni è maggiore con i modelli TensorRT dove l'inferenza è già molto veloce e la pre-elaborazione della CPU diventa il collo di bottiglia.
Link to this sectionQual è 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 di Ultralytics LetterBox(center=True).
Link to this sectionDALI 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. Differenze minori in virgola mobile (< 0.001) possono verificarsi a causa dell'aritmetica GPU vs CPU, ma queste non hanno alcun impatto misurabile sull'accuratezza del rilevamento.
Link to this sectionChe dire di CV-CUDA come alternativa a DALI?#
CV-CUDA è un'altra libreria NVIDIA per l'elaborazione visiva accelerata da 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 un padding esplicito per lato, rendendo il letterbox centrato semplice. 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.