YOLO Vision 2026:

Preprocessing accelerato via GPU con NVIDIA DALI#

Quando distribuisci modelli Ultralytics YOLO in produzione, la preprocessing diventa spesso il collo di bottiglia. Mentre TensorRT può eseguire l'inference del modello in pochi millisecondi, la pre-elaborazione basata su CPU (ridimensionamento, padding, normalizzazione) può richiedere 2-10ms per immagine, specialmente ad alte risoluzioni. NVIDIA DALI (Data Loading Library) 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, integrandole con model.predict(), elaborando flussi video e distribuendo 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 CPU è un collo di bottiglia misurato — tipicamente distribuzioni TensorRT su GPU NVIDIA, pipeline video ad alto throughput o configurazioni di 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 veloce
  • 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 su GPU.
  • Ti stai integrando con Ultralytics? Passa l'output DALI come torch.Tensor a model.predict() — Ultralytics salta automaticamente la pre-elaborazione dell'immagine.
  • Distribuzione con Triton? Usa il backend DALI con un ensemble TensorRT per un preprocessing a zero carico su CPU.

Perché usare DALI per il preprocessing di YOLO#

In una tipica pipeline di inferenza YOLO, i passaggi di preprocessing vengono eseguiti sulla CPU:

  1. Decodifica dell'immagine (JPEG/PNG)
  2. Ridimensionamento mantenendo le proporzioni
  3. Padding alla dimensione target (letterbox)
  4. Normalizza i 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 prezioso quando:

ScenarioPerché DALI aiuta
Inferenza GPU veloceI motori TensorRT con inferenza sub-millisecondo rendono la pre-elaborazione CPU il costo dominante
Input ad alta risoluzioneGli stream video 1080p e 4K richiedono operazioni di ridimensionamento costose
Batch sizes elevatiInferenza lato server che elabora molte immagini in parallelo
Core CPU limitatiDispositivi edge come NVIDIA Jetson, o server GPU densi 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 successiva)
  • CUDA 11.0+, 12.0+ o 13.0+
  • Python 3.10-3.14
  • Sistema operativo Linux

Comprendere il preprocessing di YOLO#

Prima di creare una pipeline DALI, vale la pena capire esattamente cosa fa Ultralytics durante la pre-elaborazione. 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)
)

La pipeline di pre-elaborazione completa 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 di letterbox preserva le proporzioni:

  1. Calcolo della scala: r = min(target_h / h, target_w / w)
  2. Ridimensionamento a (round(w * r), round(h * r))
  3. Riempimento dello spazio rimanente con grigio (114) per raggiungere la dimensione target
  4. Centrando l'immagine in modo che il padding sia distribuito equamente su entrambi i lati

Pipeline DALI per YOLO#

La pipeline DALI consigliata replica il comportamento predefinito LetterBox(center=True) di Ultralytics, che è quello 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 (consigliato)
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 `fn.pad` è sufficiente?

Se non hai bisogno della parità esatta di LetterBox(center=True), puoi semplificare il passaggio del padding usando fn.pad(...) invece di fn.crop(..., out_of_bounds_policy="pad"). Questa 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 letterbox centrato predefinito di Ultralytics.

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

L'operatore fn.pad di DALI aggiunge il padding solo ai bordi destro e inferiore. Per ottenere un padding centrato (che corrisponde a LetterBox(center=True) di Ultralytics), usa fn.crop con out_of_bounds_policy="pad". Con crop_pos_x=0.5 e crop_pos_y=0.5 predefiniti, l'immagine viene automaticamente centrata con un padding simmetrico.

Discrepanza Antialias

L'operatore 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 farlo corrispondere alla pipeline CPU. Omettere questo passaggio causa lievi differenze numeriche che possono influenzare l'accuratezza del modello.

Esecuzione della pipeline#

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

Uso di DALI con Ultralytics Predict#

Puoi passare un tensore PyTorch pre-elaborato direttamente 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 il cast del dtype prima di inviarlo al modello.

Poiché in questo caso Ultralytics non ha accesso alle dimensioni originali dell'immagine, le coordinate del riquadro di rilevamento vengono restituite nello spazio letterboxed 640×640. Per mapparle nuovamente sulle 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.

DALI + Ultralytics predict
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")
Zero overhead di preprocessing

Quando passi un torch.Tensor a model.predict(), il passaggio di pre-elaborazione dell'immagine richiede ~0,004ms (essenzialmente zero) rispetto a ~1-10ms con la pre-elaborazione 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.

DALI con stream video#

Per l'elaborazione video in tempo reale, usa fn.external_source per alimentare i fotogrammi da qualsiasi sorgente — OpenCV, GStreamer o librerie di cattura personalizzate:

Pipeline DALI per il preprocessing di stream 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 la distribuzione in produzione, combina la pre-elaborazione DALI con l'inferenza TensorRT in Triton Inference Server utilizzando un modello di ensemble. Questo elimina completamente la pre-elaborazione CPU: i byte JPEG grezzi entrano, i rilevamenti escono, con tutto elaborato 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 Triton DALI:

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 nel motore TensorRT
from ultralytics import YOLO

model = YOLO("yolo26n.pt")
model.export(format="engine", imgsz=640, quantize=16, batch=8)
# 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. 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.

Passaggio 4: Invia richieste di inferenza#

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

Ultralytics ha un supporto Triton integrato che gestisce automaticamente la pre/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 le distribuzioni standard senza DALI.

Invia immagini all'insieme 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 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.

Attività supportate#

La pre-elaborazione DALI funziona con tutti i task YOLO che utilizzano la pipeline standard LetterBox:

CompitoSupportatoNote
RilevamentoPre-elaborazione standard letterbox
Segmentazione di istanzeStessa pre-elaborazione del rilevamento
Segmentazione semanticaStessa pre-elaborazione delle immagini del rilevamento
ClassificazioneUtilizza trasformazioni torchvision (ritaglio centrale), non letterbox
Stima della posaStessa pre-elaborazione del rilevamento
Oriented Detection (OBB)Stessa pre-elaborazione del rilevamento

Limitazioni#

  • 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 destro/inferiore: Usa fn.crop con out_of_bounds_policy="pad" per il padding centrato
  • Nessuna modalità rect: Le pipeline DALI producono output a dimensione fissa (ad es. 640×640). La modalità rect auto=True che produce output a dimensione variabile (ad es. 384×640) non è supportata. Nota che sebbene TensorRT supporti forme di input dinamiche, una pipeline DALI a dimensione fissa si accoppia naturalmente con un motore a dimensione fissa per il massimo throughput
  • Memoria con istanze multiple: L'uso 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 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 guadagni maggiori si ottengono con input ad alta risoluzione (1080p, 4K), batch sizes grandi e sistemi con core CPU limitati per GPU.

  • Sì. Usa DALIGenericIterator per ottenere output torch.Tensor pre-elaborati, quindi passa questi ultimi a model.predict(). Tuttavia, il vantaggio in termini di prestazioni è massimo con i modelli TensorRT in cui l'inferenza è già molto veloce e la pre-elaborazione CPU diventa il collo di bottiglia.

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

  • Quasi identici. Imposta antialias=False in fn.resize per farlo corrispondere a cv2.INTER_LINEAR di OpenCV. Potrebbero verificarsi lievi differenze in virgola mobile (< 0,001) dovute all'aritmetica GPU rispetto a quella CPU, ma queste non hanno alcun impatto misurabile sull'accuratezza del rilevamento.

  • CV-CUDA è un'altra libreria NVIDIA per l'elaborazione visiva accelerata da GPU. Fornisce un controllo per singolo operatore (come OpenCV ma su GPU) anziché l'approccio basato su pipeline di DALI. Il cvcuda.copymakeborder() di CV-CUDA supporta il padding esplicito per singolo 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.

Commenti