Pre-elaborazione GPU con NVIDIA
Introduzione
Durante l'implementazione Ultralytics YOLO in produzione, la pre-elaborazione diventa spesso il collo di bottiglia. Mentre TensorRT è in grado di eseguire l'inferenza del modello in pochi millisecondi, la pre-elaborazione CPU(ridimensionamento, riempimento, normalizzazione) può richiedere dai 2 ai 10 ms per immagine, specialmente ad alte risoluzioni. NVIDIA (Data Loading Library) risolve questo problema spostando l'intera pipeline di pre-elaborazione sulla GPU.
Questa guida illustra come creare pipeline DALI che riproducano fedelmenteYOLO Ultralytics YOLO , integrandole con model.predict(), elaborazione di flussi video e implementazione end-to-end con Triton Inference Server.
A chi è rivolta questa guida?
La presente guida è destinata agli ingegneri che implementano YOLO in ambienti di produzione in cui CPU rappresenta un collo di bottiglia misurabile — in genere TensorRT implementazioni su NVIDIA , pipeline video ad alta produttività o Triton Inference Server configurazioni. Se stai eseguendo un'inferenza standard con model.predict() e se non si verificano colli di bottiglia nella fase di pre-elaborazione, la CPU predefinita CPU funziona bene.
Sintesi
- 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 in formato letterbox YOLO sulla GPU. - Integrazione con Ultralytics? Trasmetti l'uscita DALI come un
torch.Tensoramodel.predict()— Ultralytics automaticamente la fase di pre-elaborazione delle immagini. - Stai effettuando il deployment con Triton? Utilizza il backend DALI con un TensorRT perCPU .
Perché utilizzare DALI per YOLO
In una tipica pipeline YOLO , le fasi di pre-elaborazione vengono eseguite sulla CPU:
- Decodifica l'immagine (JPEG/PNG)
- Ridimensiona mantenendo le proporzioni
- Adatta all'area di destinazione (letterbox)
- Normalizza 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 CPU . Ciò risulta particolarmente utile quando:
| Scenario | Perché DALI è utile |
|---|---|
| GPU veloce GPU | TensorRT I motori con inferenza inferiore al millisecondo rendono CPU il costo principale |
| Ingressi ad alta risoluzione | I flussi video a 1080p e 4K richiedono costose operazioni di ridimensionamento |
| Lotti di grandi dimensioni | Elaborazione inferenziale lato server di molte immagini in parallelo |
| Numero limitato di CPU | Dispositivi periferici come NVIDIA o GPU con GPU ad alta densità e pochi CPU per GPU |
Prerequisiti
Solo per Linux
NVIDIA è compatibile solo con 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-cuda120
pip install ultralytics
pip install --extra-index-url https://pypi.nvidia.com nvidia-dali-cuda110
Requisiti:
- GPU NVIDIA GPU capacità di calcolo 5.0+ / Maxwell o versioni successive)
- CUDA .0 o versioni successive oppure 12.0 o versioni successive
- Python .10-3.14
- Sistema operativo Linux
Comprendere YOLO
Prima di creare una pipeline DALI, è utile capire esattamente cosa 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)
)
L'intera pipeline di pre-elaborazione in ultralytics/engine/predictor.py esegue questi passaggi:
| Passaggio | Funzionamento | CPU | Equivalente DALI |
|---|---|---|---|
| 1 | Modifica delle dimensioni della casella di posta | cv2.resize | fn.resize(mode="not_larger") |
| 2 | Allineamento al centro | 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 letterboxing mantiene le proporzioni tramite:
- Scala di calcolo:
r = min(target_h / h, target_w / w) - Ridimensionamento a
(round(w * r), round(h * r)) - Riempire lo spazio rimanente con il colore grigio (
114) per raggiungere la dimensione desiderata - Centrare l'immagine in modo che il margine interno sia distribuito equamente su entrambi i lati
Pipeline DALI per YOLO
Utilizza la pipeline centrata riportata di seguito come riferimento predefinito. È compatibile con Ultralytics LetterBox(center=True) comportamento, che è quello utilizzato YOLO standard.
Pipeline centrata (consigliata, compatibile con Ultralytics )
Questa versione riproduce esattamente la Ultralytics predefinita Ultralytics con riempimento centrato, in modo che corrisponda LetterBox(center=True):
Pipeline DALI con riempimento centrato (consigliato)
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 output
Quando è fn.pad basta?
Se non ti serve l'esatto LetterBox(center=True) parità, è possibile semplificare la fase di riempimento utilizzando fn.pad(...) invece di fn.crop(..., out_of_bounds_policy="pad"). Questa variante inserisce solo il a destra e in basso bordi, il che può essere accettabile per le pipeline di distribuzione personalizzate, ma non corrisponderà esattamente al comportamento predefinito Ultralytics con il formato letterbox centrato.
Perché fn.crop per il riempimento centrato?
di DALI fn.pad L'operatore aggiunge solo spazi di riempimento al a destra e in basso bordi. Per ottenere un riempimento centrato (in linea con Ultralytics LetterBox(center=True)), utilizzare fn.crop con out_of_bounds_policy="pad". Con l'impostazione predefinita crop_pos_x=0.5 e crop_pos_y=0.5, l'immagine viene automaticamente centrata con un margine simmetrico.
Discrepanza nell'antialiasing
di DALI fn.resize attiva l'antialiasing per impostazione predefinita (antialias=True), mentre OpenCV cv2.resize con INTER_LINEAR fa non applica l'antialiasing. Impostare sempre antialias=False in DALI per adattarsi alla CPU . Se si omette questo passaggio, si verificano sottili differenze numeriche che possono influire su precisione del modello.
Esecuzione della pipeline
Creare ed eseguire una pipeline DALI
# 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}]")
Utilizzo di DALI con Ultralytics
È possibile passare un file pre-elaborato PyTorch tensor a model.predict(). Quando un torch.Tensor è stato approvato, Ultralytics salta la pre-elaborazione delle immagini (letterbox, conversione da BGR a RGB, da HWC a CHW e normalizzazione a /255) ed esegue solo la conversione per il dispositivo e il casting del tipo di dati prima di inviarli al modello.
Poiché in questo caso Ultralytics ha accesso alle dimensioni originali dell'immagine, le coordinate del riquadro di rilevamento vengono restituite nello spazio letterbox da 640×640. Per ricollegarle alle coordinate dell'immagine originale, utilizzare 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))
Ciò vale per tutti i percorsi di pre-elaborazione esterni: tensor diretto, flussi video e Triton .
DALI + Ultralytics
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")
Nessun sovraccarico di pre-elaborazione
Quando si supera un torch.Tensor a model.predict(), la fase di pre-elaborazione dell'immagine richiede circa 0,004 ms (praticamente zero) rispetto ai circa 1-10 ms necessari con CPU . Il tensor essere in formato BCHW, di tipo float32 (o float16) e normalizzato a [0, 1]. Ultralytics a gestire automaticamente il trasferimento dei dispositivi e la conversione dei tipi di dati.
DALI con flussi video
Per l'elaborazione video in tempo reale, utilizzare fn.external_source per importare fotogrammi da qualsiasi fonte — OpenCV, GStreamer o librerie di acquisizione personalizzate:
Pipeline DALI per la pre-elaborazione dei flussi video
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 output
import cv2
import numpy as np
import torch
from ultralytics import YOLO
model = YOLO("yolo26n.engine") # TensorRT model
pipe = yolo_video_pipeline(target_size=640)
pipe.build()
cap = cv2.VideoCapture("video.mp4")
while cap.isOpened():
ret, frame = cap.read()
if not ret:
break
# Feed BGR frame (convert to RGB for DALI)
frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
pipe.feed_input("input", [np.array(frame_rgb)])
(output,) = pipe.run()
# Convert DALI output to torch tensor for inference.
# This is a simple fallback path: using feed_input() with pipe.run() keeps a GPU->CPU->GPU copy.
# For high-throughput deployments, prefer a reader-based pipeline plus DALIGenericIterator to keep data on GPU.
tensor = torch.tensor(output.as_cpu().as_array()).to("cuda")
results = model.predict(tensor, verbose=False)
Triton Server con DALI
Per l'implementazione in produzione, combinare la pre-elaborazione DALI con TensorRT nel Triton Server utilizzando un modello ensemble. Ciò elimina completamente CPU : entrano i byte JPEG grezzi, escono i rilevamenti, con tutto elaborato sulla GPU.
Struttura del repository dei modelli
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.pbtxt
Passaggio 1: Creare la pipeline DALI
Serializza la pipeline DALI per il backend Triton :
Serializzare la pipeline DALI per Triton
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")
Passaggio 2: esportare YOLO TensorRT
Esporta YOLO nel 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.plan
Passaggio 3: Configurare 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.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"
}
}
]
}
Come funziona l'Ensemble Mapping
L'insieme collega i modelli tramite tensor virtuali. output_map valore "preprocessed_image" nella fase DALI corrisponde al input_map valore "preprocessed_image" nella TensorRT . Si tratta di nomi arbitrari che collegano l'output di una fase all'input della fase successiva; non è necessario che corrispondano tensor interni del modello.
Fase 4: Inviare le richieste di inferenza
Perché tritonclient invece di YOLO(\"http://...\")?
Ultralytics Triton integrato Triton che gestisce automaticamente la pre-elaborazione e la post-elaborazione. Tuttavia, non funziona con l'ensemble DALI perché YOLO() invia un tensor float32 preelaborato, tensor l'ensemble si aspetta byte JPEG non elaborati. Utilizza tritonclient specificamente per gli ensemble DALI, e il integrazione nativa per installazioni standard senza DALI.
Invia immagini all Triton
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")
Elaborazione in batch di immagini JPEG
Quando si invia un lotto di immagini JPEG a Triton, è necessario allineare tutti gli array di byte codificati alla stessa lunghezza (il numero massimo di byte nel lotto). Triton i lotti abbiano una struttura omogenea per il tensor di input.
Attività supportate
La pre-elaborazione DALI funziona con tutte YOLO che utilizzano lo standard LetterBox pipeline:
| Task | Supportato | Note |
|---|---|---|
| Rilevamento | ✅ | Pre-elaborazione standard per la casella postale |
| Segmentazione | ✅ | Stessa pre-elaborazione utilizzata per il rilevamento |
| Stima della posa | ✅ | Stessa pre-elaborazione utilizzata per il rilevamento |
| Rilevamento orientato (OBB) | ✅ | Stessa pre-elaborazione utilizzata per il rilevamento |
| Classificazione | ❌ | Utilizza le trasformazioni Torchvision (ritaglio al centro), non il formato letterbox |
Limitazioni
- Solo Linux: DALI non supporta Windows né macOS
- GPU NVIDIA : non è prevista una soluzione alternativa CPU
- Pipeline statica: la struttura della pipeline viene definita in fase di compilazione e non può essere modificata dinamicamente
fn.padsolo a destra/in basso: Usafn.cropconout_of_bounds_policy="pad"per il riempimento centrato- Modalità non rettangolare: Le pipeline DALI producono output di dimensioni fisse (ad es. 640×640). Il
auto=TrueLa modalità che produce immagini di dimensioni variabili (ad es. 384×640) non è supportata. Si noti che, sebbene TensorRT supporta forme di input dinamiche; una pipeline DALI a dimensione fissa si abbina naturalmente a un motore a dimensione fissa per garantire la massima produttività - Memoria con più istanze: Utilizzo di
instance_groupconcount> 1 in Triton causare un elevato consumo di memoria. Utilizza il gruppo di istanze predefinito per il modello DALI
FAQ
Come si colloca la velocità di CPU DALI rispetto a CPU ?
Il vantaggio dipende dalla tua pipeline. Quando GPU è già veloce con TensorRT, CPU (2-10 ms) può diventare il costo dominante. DALI elimina questo collo di bottiglia eseguendo la pre-elaborazione sulla GPU. I vantaggi maggiori si riscontrano con input ad alta risoluzione (1080p, 4K), batch di grandi dimensioni e sistemi con un numero limitato CPU per GPU.
Posso usare DALI con PyTorch (non solo con TensorRT)?
Sì. Usa DALIGenericIterator per essere sottoposto a pre-elaborazione torch.Tensor risultati, quindi passarli a model.predict(). Tuttavia, il miglioramento delle prestazioni è maggiore con TensorRT modelli in cui l'inferenza è già molto veloce e CPU diventa il collo di bottiglia.
Qual è la differenza tra fn.pad e fn.crop per riempire?
fn.pad aggiunge un margine solo al a destra e in basso bordi. fn.crop con out_of_bounds_policy="pad" centra l'immagine e aggiunge un margine simmetrico su tutti i lati, in linea con Ultralytics LetterBox(center=True) comportamento.
DALI produce risultati identici a quelli ottenuti con CPU ?
Quasi identici. Set antialias=False in fn.resize in modo da corrispondere a OpenCV cv2.INTER_LINEAR. Minor floating-point differences (< 0.001) may occur due to GPU vs CPU arithmetic, but these have no measurable impact on detection accuratezza.
Che ne pensate diCUDA alternativa a DALI?
CUDA è un'altra NVIDIA per l'elaborazione visiva GPU. Offre un controllo operatore per operatore (come OpenCV (ma sulla GPU) anziché l'approccio basato su pipeline di DALI.CUDA cvcuda.copymakeborder() supporta il riempimento esplicito per ciascun lato, semplificando la creazione di immagini in formato letterbox centrate. Scegli DALI per i flussi di lavoro basati su pipeline (in particolare con Triton), eCUDA un controllo dettagliato a livello di operatore nel codice di inferenza personalizzato.