Link to this sectionPreprocessing accelerato via GPU con NVIDIA DALI#
Link to this sectionIntroduzione#
Quando distribuisci i modelli Ultralytics YOLO in produzione, la preelaborazione diventa spesso il collo di bottiglia. Mentre TensorRT può eseguire l'inferenza del modello in pochi millisecondi, la preelaborazione 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 preelaborazione sulla GPU.
Questa guida ti accompagna nella creazione di pipeline DALI che replicano esattamente la preelaborazione di Ultralytics YOLO, integrandole con model.predict(), elaborando flussi video e distribuendo il tutto end-to-end con Triton Inference Server.
Questa guida è pensata per gli ingegneri che distribuiscono modelli YOLO in ambienti di produzione in cui la preelaborazione tramite CPU rappresenta un collo di bottiglia misurato: tipicamente implementazioni TensorRT su GPU NVIDIA, pipeline video ad alto throughput o configurazioni Triton Inference Server. Se esegui un'inferenza standard con model.predict() e non riscontri colli di bottiglia nella preelaborazione, 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 la preelaborazione letterbox di YOLO sulla GPU. - Stai integrando con Ultralytics? Passa l'output di DALI come
torch.Tensoramodel.predict(): Ultralytics salterà automaticamente la preelaborazione dell'immagine. - Stai distribuendo con Triton? Usa il backend DALI con un insieme TensorRT per una preelaborazione a zero carico sulla CPU.
Link to this sectionPerché usare DALI per la preelaborazione YOLO#
In una tipica pipeline di inferenza YOLO, i passaggi di preelaborazione vengono eseguiti sulla CPU:
- Decodifica l'immagine (JPEG/PNG)
- Ridimensiona mantenendo le proporzioni
- Riempie alla dimensione 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 GPU rapida | I motori TensorRT con inferenza sub-millisecondo rendono la preelaborazione CPU il costo dominante |
| Input ad alta risoluzione | I flussi video 1080p e 4K richiedono costose operazioni di ridimensionamento |
| Grandi dimensioni del batch | Elaborazione dell'inferenza lato server su 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 (capacità di calcolo 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 la preelaborazione YOLO#
Prima di costruire una pipeline DALI, è utile comprendere esattamente cosa fa Ultralytics durante la preelaborazione. 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 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 | 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
Link to this sectionPipeline DALI per YOLO#
Usa la pipeline centrata di seguito come riferimento predefinito. Corrisponde al comportamento 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 la preelaborazione 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 della parità esatta con LetterBox(center=True), 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 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 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 riempimento simmetrico.
Il 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 questo passaggio causa sottili differenze numeriche che possono influire sulla 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 preelaborato direttamente a model.predict(). Quando viene passato un torch.Tensor, Ultralytics salta la preelaborazione dell'immagine (letterbox, BGR→RGB, HWC→CHW e normalizzazione /255) ed esegue solo il trasferimento sul dispositivo e il casting del dtype prima di inviarlo al modello.
Poiché in questo caso Ultralytics non ha accesso alle dimensioni originali dell'immagine, le coordinate del box di rilevamento vengono restituite nello spazio letterboxed 640×640. Per riportarle alle coordinate originali dell'immagine, 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 preelaborazione esterni: input diretto del tensore, 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 dell'immagine richiede ~0.004ms (praticamente zero) rispetto a ~1-10ms con la preelaborazione 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 casting del dtype.
Link to this sectionDALI con flussi video#
Per l'elaborazione video in tempo reale, usa fn.external_source per alimentare i fotogrammi 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 outputLink to this sectionTriton Inference Server con DALI#
Per la distribuzione in produzione, combina la preelaborazione DALI con l'inferenza TensorRT in Triton Inference Server utilizzando un modello d'insieme. Questo elimina completamente la preelaborazione CPU: vengono inseriti byte JPEG grezzi e si ottengono i rilevamenti, con tutto il processo eseguito 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 alcun modello.
Link to this sectionPassaggio 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, riempi 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.
Link to this sectionAttività 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 di istanze | ✅ | Stessa pre-elaborazione del rilevamento |
| Segmentazione semantica | ✅ | Stessa pre-elaborazione dell'immagine del rilevamento |
| Stima della posa | ✅ | 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 al momento della compilazione e non può cambiare dinamicamente
fn.padè solo a destra/basso: Usafn.cropconout_of_bounds_policy="pad"per il riempimento centrato- Nessuna modalità rettangolare: Le pipeline DALI producono output di dimensioni fisse (es. 640×640). La modalità rettangolare
auto=Trueche produce output di dimensioni variabili (es. 384×640) non è supportata. Nota che, sebbene TensorRT supporti 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 elevato utilizzo della memoria. Usa il gruppo di istanze predefinito per il modello DALI
Link to this sectionFAQ#
Link to this sectionCome 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 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), grandi batch sizes 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 CPU diventa il collo di bottiglia.
Link to this sectionQual è la differenza tra fn.pad e fn.crop per il riempimento?#
fn.pad aggiunge il riempimento solo ai bordi destro e inferiore. fn.crop con out_of_bounds_policy="pad" centra l'immagine e aggiunge il riempimento simmetricamente su tutti i lati, corrispondendo al comportamento LetterBox(center=True) di Ultralytics.
Link to this sectionDALI produce risultati identici a livello di pixel rispetto alla pre-elaborazione 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 rispetto a quella CPU, ma queste non hanno alcun impatto misurabile sull'accuratezza del rilevamento.
Link to this sectionE che dire di CV-CUDA come alternativa a DALI?#
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 a pipeline di DALI. cvcuda.copymakeborder() di CV-CUDA supporta il riempimento 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.