Ultralytics YOLO27: Iscriviti alla lista d'attesa.
Get Started

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. Mentre TensorRT può eseguire l'inferenza del modello in pochi millisecondi, la pre-elaborazione basata su CPU (ridimensionamento, padding, normalizzazione) può richiedere da 2 a 10 ms per immagine, soprattutto alle alte risoluzioni. NVIDIA DALI (Data Loading Library) risolve il problema trasferendo l'intera pipeline di pre-elaborazione sulla GPU.

Questa guida illustra come creare pipeline DALI che riproducono esattamente la pre-elaborazione di Ultralytics YOLO, integrarle con model.predict(), elaborare flussi video e distribuire la soluzione end-to-end con Triton Inference Server.

A chi è rivolta questa guida?

Questa guida è rivolta 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 di Triton Inference Server. Se esegui inferenze standard con model.predict() e non hai un collo di bottiglia nella pre-elaborazione, la pipeline CPU predefinita funziona bene.

Riepilogo rapido
  • Stai creando una pipeline DALI? Usa fn.resize(mode="not_larger") + fn.crop(out_of_bounds_policy="pad") + fn.crop_mirror_normalize per riprodurre sulla GPU la pre-elaborazione letterbox di YOLO.
  • Stai integrando DALI con Ultralytics? Passa l'output di DALI come torch.Tensor a model.predict(): Ultralytics salta automaticamente la pre-elaborazione delle immagini.
  • Stai distribuendo con Triton? Usa il backend DALI con un ensemble TensorRT per eliminare 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:

  1. Decodifica dell'immagine (JPEG/PNG)
  2. Ridimensionamento preservando le proporzioni
  3. Adatta alle dimensioni target (letterbox)
  4. Normalizza i valori dei pixel da [0, 255] a [0, 1]
  5. 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:

ScenarioPerché DALI è utile
Inferenza rapida su GPUMotori TensorRT con inferenza inferiore al millisecondo rendono il pre-processing sulla CPU il costo predominante
Input ad alta risoluzioneI flussi video a 1080p e 4K richiedono operazioni di ridimensionamento costose
Dimensioni dei batch elevateInferenza lato server che elabora molte immagini in parallelo
Numero limitato di core CPUDispositivi edge come NVIDIA Jetson o server GPU ad alta densità con pochi core CPU per GPU

Prerequisiti#

Solo Linux

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-cuda130

Requisiti:

  • GPU NVIDIA (compute capability 5.0+ / Maxwell o versioni successive)
  • CUDA 11.0+, 12.0+ o 13.0+
  • Python 3.10-3.14
  • Sistema operativo Linux

Comprendere il pre-processing di YOLO#

Prima di creare una pipeline DALI, è utile capire esattamente cosa fa Ultralytics durante il pre-processing. La classe principale è LetterBox in ultralytics/data/augment.py:

from ultralytics.data.augment import LetterBox

letterbox = LetterBox(
    new_shape=(640, 640),  # Dimensione target
    center=True,  # Centra l'immagine (aggiungi padding in modo uniforme su entrambi i lati)
    stride=32,  # Allineamento allo stride
    padding_value=114,  # Padding grigio (114, 114, 114)
)

La pipeline completa di pre-processing in ultralytics/engine/predictor.py esegue questi passaggi:

PassaggioOperazioneFunzione CPUEquivalente DALI
1Ridimensionamento letterboxcv2.resizefn.resize(mode="not_larger")
2Padding centratocv2.copyMakeBorderfn.crop(out_of_bounds_policy="pad")
3BGR → RGBim[..., ::-1]fn.decoders.image(output_type=types.RGB)
4HWC → CHW + normalizzazione /255np.transpose + tensor / 255fn.crop_mirror_normalize(std=[255,255,255])

L'operazione letterbox preserva le proporzioni tramite:

  1. Calcolo della scala: r = min(target_h / h, target_w / w)
  2. Ridimensionamento a (round(w * r), round(h * r))
  3. Aggiunta di padding grigio (114) allo spazio rimanente per raggiungere le dimensioni target
  4. Centrare l'immagine in modo che il padding sia distribuito uniformemente su entrambi i lati

Pipeline DALI per YOLO#

La pipeline DALI consigliata replica il comportamento predefinito di Ultralytics LetterBox(center=True), utilizzato dall'inferenza standard di YOLO.

Questa versione replica esattamente il pre-processing predefinito di Ultralytics con padding centrato, corrispondente a LetterBox(center=True):

Pipeline DALI con padding centrato (consigliata)
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 con mantenimento delle 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 con fn.crop e out_of_bounds_policy
    # Quando le dimensioni del ritaglio superano quelle 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
    )

    # Normalizza e converti il 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 è sufficiente `fn.pad`?

Se non ti serve una corrispondenza esatta con LetterBox(center=True), puoi semplificare il passaggio del padding usando fn.pad(...) anziché fn.crop(..., out_of_bounds_policy="pad"). Questa variante aggiunge padding solo ai bordi destro e inferiore, cosa che può essere accettabile per pipeline di distribuzione personalizzate, ma non corrisponde esattamente al comportamento letterbox centrato predefinito di Ultralytics.

Perché usare `fn.crop` per il padding centrato?

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.

Discrepanza nell'antialiasing

L'operatore DALI fn.resize attiva l'antialiasing per impostazione predefinita (antialias=True), mentre OpenCV cv2.resize 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 verificano sottili differenze numeriche che possono influire sulla precisione del modello.

Esecuzione della pipeline#

Crea ed esegui 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}]")

Usare DALI con Ultralytics Predict#

Puoi passare direttamente un tensore PyTorch pre-elaborato a model.predict(). Quando viene passato un torch.Tensor, Ultralytics salta il pre-processing dell'immagine (letterbox, BGR→RGB, HWC→CHW e normalizzazione /255) ed esegue solo il trasferimento sul dispositivo e la conversione del tipo di dato prima di inviarlo al modello.

Poiché in questo caso Ultralytics non ha accesso alle dimensioni originali dell'immagine, le coordinate dei riquadri di rilevamento vengono restituite nello spazio letterbox 640×640. Per ricondurle alle coordinate dell'immagine originale, usa scale_boxes, che applica la logica di arrotondamento esatta usata da LetterBox:

from ultralytics.utils.ops import scale_boxes

# riquadri: tensore di forma (N, 4) in formato xyxy, coordinate letterbox 640x640
# Riporta i riquadri dalle coordinate letterbox (640, 640) a quelle originali (orig_h, orig_w)
boxes = scale_boxes((640, 640), boxes, (orig_h, orig_w))

Questo vale per tutti i flussi di pre-processing esterni: input diretto di tensori, flussi video e distribuzione con Triton.

DALI + predict di 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 overhead di pre-processing

Quando passi un torch.Tensor a model.predict(), il pre-processing dell'immagine richiede ~0.004ms (praticamente zero), rispetto a ~1-10ms con il pre-processing 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 dato.

DALI con flussi video#

Per l'elaborazione video in tempo reale, usa fn.external_source per fornire frame da qualsiasi sorgente: OpenCV, GStreamer o librerie di acquisizione personalizzate:

Pipeline DALI per il pre-processing dei flussi video
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 frame da OpenCV, GStreamer ecc.
    frames = fn.external_source(device="cpu", name="input")
    frames = fn.reshape(frames, layout="HWC")

    # Sposta sulla GPU ed esegui il pre-processing
    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

Triton Inference Server con DALI#

Per la distribuzione in produzione, combina il pre-processing DALI con l'inferenza TensorRT in Triton Inference Server usando un modello ensemble. Questo elimina completamente il pre-processing 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.pbtxt

Passaggio 1: crea la pipeline DALI#

Serializza la pipeline DALI per il backend DALI di Triton:

Serializza la pipeline DALI per 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 grezzi dell'immagine codificata 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 del modello
pipe = triton_dali_pipeline()
pipe.serialize(filename="model_repository/dali_preprocessing/1/model.dali")

Passaggio 2: esporta YOLO in TensorRT#

Esporta il modello YOLO in un motore 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 antepone un'intestazione di metadati ai file .engine; rimuovila affinché Triton possa 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 dei metadati JSON
    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"
      }
    }
  ]
}
Come funziona la mappatura dell'ensemble

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 passaggio successivo: non devono corrispondere ai nomi dei tensori interni dei modelli.

Passaggio 4: invia richieste di inferenza#

Perché `tritonclient` invece di `YOLO('http://...')`?

Ultralytics offre il supporto Triton integrato, che gestisce automaticamente la pre-elaborazione e la post-elaborazione. Tuttavia, non funziona 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 le distribuzioni standard senza DALI.

Invia immagini all'ensemble 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 for the nms=False export)
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 invii a Triton un batch di immagini JPEG, porta tutti gli array di byte codificati alla stessa lunghezza (il numero massimo di byte nel batch). Triton richiede forme omogenee per i tensori di input del batch.

Attività supportate#

La pre-elaborazione DALI funziona con tutte le attività YOLO che usano la pipeline standard LetterBox:

AttivitàSupportatoNote
Rilevamento✅Pre-elaborazione letterbox standard
Segmentazione di istanze✅Stessa pre-elaborazione del rilevamento
Segmentazione semantica✅Stessa pre-elaborazione delle immagini del rilevamento
Stima della profondità✅Stessa pre-elaborazione delle immagini del rilevamento
Classificazione❌Usa le trasformazioni torchvision (ritaglio centrale), non il letterbox
Stima della posa✅Stessa pre-elaborazione del rilevamento
Rilevamento orientato (OBB)✅Stessa pre-elaborazione del rilevamento

Limitazioni#

  • Solo Linux: DALI non supporta Windows o macOS
  • È necessaria una GPU NVIDIA: non è disponibile un'alternativa che usi solo la CPU
  • Pipeline statica: la struttura della pipeline viene definita in fase di compilazione e non può cambiare dinamicamente
  • fn.pad agisce solo a destra/in basso: usa fn.crop con out_of_bounds_policy="pad" per un padding centrato
  • 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, anche se TensorRT supporta forme di input dinamiche, una pipeline DALI a dimensione fissa si abbina in modo naturale a un motore a dimensione fissa per ottenere il massimo throughput
  • Memoria con più istanze: usare instance_group con count > 1 in Triton può causare un elevato consumo di memoria. Usa il gruppo di istanze predefinito per il modello DALI

Domande frequenti#

  • Il vantaggio dipende dalla pipeline. Quando l'inferenza su GPU è già veloce con TensorRT, la pre-elaborazione su CPU, che richiede 2-10 ms, può diventare il costo principale. DALI elimina questo collo di bottiglia eseguendo la pre-elaborazione sulla GPU. I vantaggi maggiori si ottengono con input ad alta risoluzione (1080p, 4K), dimensioni di batch elevate e sistemi con pochi core CPU per GPU.

  • 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, la cui inferenza è già molto veloce e per i quali la pre-elaborazione su CPU diventa il collo di bottiglia.

  • fn.pad aggiunge padding solo ai bordi destro e inferiore. fn.crop con out_of_bounds_policy="pad" centra l'immagine e aggiunge padding simmetrico su tutti i lati, in linea con il comportamento di LetterBox(center=True) di Ultralytics.

  • Quasi identici. Imposta antialias=False in fn.resize per ottenere risultati corrispondenti a cv2.INTER_LINEAR di OpenCV. Possono verificarsi lievi differenze in virgola mobile (< 0.001) dovute ai calcoli su GPU rispetto a quelli su CPU, ma non hanno un 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), invece dell'approccio basato su pipeline di DALI. cvcuda.copymakeborder() di CV-CUDA supporta il padding esplicito per ciascun lato, semplificando il 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.

Commenti