YOLO Vision 2026:

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.

A chi è destinata questa guida?

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.

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 replicare la pre-elaborazione letterbox di YOLO sulla GPU.
  • Stai integrando con Ultralytics? Passa l'output DALI come torch.Tensor a model.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:

  1. Decodifica dell'immagine (JPEG/PNG)
  2. Ridimensionamento mantenendo le proporzioni
  3. Padding fino alla dimensione di destinazione (letterbox)
  4. Normalizzazione dei valori dei pixel da [0, 255] a [0, 1]
  5. 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:

ScenarioPerché DALI è utile
Inferenza rapida sulla GPUI motori TensorRT con inferenza inferiore al millisecondo rendono la pre-elaborazione sulla CPU il costo principale
Input ad alta risoluzioneI flussi video a 1080p e 4K richiedono operazioni di ridimensionamento costose
Dimensioni dei batch elevateElaborazione dell'inferenza lato server di 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 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:

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 mantiene le proporzioni mediante:

  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 allo spazio rimanente con il grigio (114) per raggiungere la dimensione di destinazione
  4. 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):

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.
    """
    # 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 è sufficiente `fn.pad`?

Se 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.

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

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.

Differenze nell'antialiasing

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#

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

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.

Predizione 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")
Sovraccarico di pre-elaborazione nullo

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:

Pipeline DALI per la pre-elaborazione 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."""
    # 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

Triton 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.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: 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#

Esporta il modello YOLO in un motore 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.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 "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#

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

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.

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")
Creazione di batch di immagini JPEG

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àSupportateNote
RilevamentoPreprocessing standard letterbox
Segmentazione delle istanzeStesso preprocessing del rilevamento
Segmentazione semanticaStesso preprocessing delle immagini del rilevamento
ClassificazioneUtilizza le trasformazioni di torchvision (ritaglio centrale), non il letterbox
Stima della posaStesso 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.pad solo a destra/in basso: usa fn.crop con out_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_group con count > 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 DALIGenericIterator per ottenere output torch.Tensor sottoposti a preprocessing, quindi passali a model.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.pad aggiunge spaziatura solo ai bordi destro e inferiore. fn.crop con out_of_bounds_policy="pad" centra l'immagine e aggiunge spaziatura simmetricamente su tutti i lati, riproducendo il comportamento di Ultralytics LetterBox(center=True).

  • Quasi identici. Imposta antialias=False in fn.resize in modo che corrisponda a cv2.INTER_LINEAR di 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.

Commenti