Skip to main content

Pre-elaborazione accelerata da GPU con NVIDIA DALI

Introduzione

Quando distribuisci modelli Ultralytics YOLO modelli in produzione, pre-elaborazione spesso diventa il collo di bottiglia. Mentre TensorRT può eseguire l'inferenza del modello inferenza in pochi millisecondi, la pre-elaborazione 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 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 è per gli ingegneri che distribuiscono modelli YOLO in ambienti di produzione dove la pre-elaborazione della CPU rappresenta un collo di bottiglia misurato — tipicamente TensorRT distribuzioni su GPU NVIDIA, pipeline video ad alta velocità, o Triton Inference Server configurazioni. Se stai eseguendo un'inferenza standard con model.predict() e non hai un collo di bottiglia nella pre-elaborazione, la pipeline CPU predefinita funziona bene.

Riassunto 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 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.
  • Stai distribuendo con Triton? Usa il backend DALI con un ensemble TensorRT per una pre-elaborazione a zero carico su CPU.

Perché usare DALI per la pre-elaborazione di YOLO

In una tipica pipeline di inferenza YOLO, i passaggi di pre-elaborazione vengono eseguiti sulla CPU:

  1. Decodifica l'immagine (JPEG/PNG)
  2. Ridimensionamento mantenendo le proporzioni
  3. Riempimento alla dimensione target (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 prezioso quando:

ScenarioPerché DALI è utile
Inferenza GPU rapidaTensorRT motori con inferenza sub-millisecondo rendono la pre-elaborazione CPU il costo principale
Input ad alta risoluzioneflussi video 1080p e 4K richiedono costose operazioni di ridimensionamento
Grandi batch sizeInferenza 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

Prima di iniziare, assicurati di avere accesso a un workspace AzureML. Se non ne hai uno, puoi crearne uno nuovo

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

Requisiti:

  • GPU NVIDIA (capacità di calcolo 5.0+ / Maxwell o più recente)
  • CUDA 11.0+ o 12.0+
  • Python 3.10-3.14
  • Sistema operativo Linux

Comprendere la pre-elaborazione di YOLO

Prima di costruire una pipeline DALI, è utile 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)
)

L'intera pipeline di pre-elaborazione in ultralytics/engine/predictor.py esegue questi passaggi:

PassaggioOperazioneFunzione CPUEquivalente DALI
1Ridimensionamento letterboxcv2.resizefn.resize(mode="not_larger")
2Riempimento 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. Riempimento dello spazio rimanente con grigio (114) per raggiungere la dimensione target
  4. Centratura dell'immagine in modo che il riempimento sia distribuito equamente su entrambi i lati

Pipeline DALI per YOLO

Usa la pipeline centrata qui sotto come riferimento predefinito. Corrisponde al comportamento di Ultralytics LetterBox(center=True), che è quello utilizzato dall'inferenza standard YOLO.

Pipeline centrata (consigliata, corrisponde a LetterBox di Ultralytics)

Questa versione replica esattamente la pre-elaborazione predefinita di Ultralytics con riempimento centrato, corrispondente a LetterBox(center=True):

Pipeline DALI con riempimento centrato (consigliata)
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 è sufficiente `fn.pad`?

Se non hai bisogno di un'esatta LetterBox(center=True) parità, 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 di letterbox centrato predefinito di Ultralytics.

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

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

Mancata corrispondenza antialias

L'operatore fn.resize abilita l'antialiasing per impostazione predefinita (antialias=True), mentre cv2.resize con INTER_LINEAR di OpenCV nonnon applica l'antialiasing. Imposta sempre antialias=False in DALI per corrispondere alla pipeline CPU. Ometterlo causa sottili differenze numeriche che possono influenzare model accuracy.

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

Utilizzo di DALI con Ultralytics Predict

Puoi passare un PyTorch tensore pre-elaborato direttamente a model.predict(). Quando viene passato un torch.Tensor, Ultralytics salta la pre-elaborazione delle immagini (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é Ultralytics non ha accesso alle dimensioni originali dell'immagine in questo caso, le coordinate del riquadro di rilevamento vengono restituite nello spazio letterbox 640×640. Per mapparle nuovamente alle 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 si applica a tutti i percorsi di pre-elaborazione esterni: input diretto di tensori, flussi 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")
Nessun sovraccarico di pre-elaborazione

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

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

Pipeline DALI per la pre-elaborazione del flusso 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

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 ensemble. Questo elimina completamente la pre-elaborazione CPU: entrano byte JPEG grezzi, escono rilevamenti, 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 DALI di Triton:

Serializza 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: 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, half=True, 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. La output_map il valore "preprocessed_image" nel passaggio DALI corrisponde al input_map il valore "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.

Passaggio 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.
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 — YOLO26 is end-to-end)
detections = detections[0]  # First image
detections = detections[detections[:, 4] > 0.25]  # Confidence threshold
print(f"Detected {len(detections)} objects")
Batching di immagini JPEG

Quando invii un batch di immagini JPEG a Triton, applica il padding a 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.

Attività supportate

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

AttivitàSupportatoNote
RilevamentoPre-elaborazione letterbox standard
segmentazioneStessa pre-elaborazione del rilevamento
Pose EstimationStessa pre-elaborazione del rilevamento
Rilevamento Orientato (OBB)Stessa pre-elaborazione del rilevamento
ClassificazioneUtilizza trasformazioni torchvision (center crop), non letterbox

Limitazioni

  • solo Linux: DALI non supporta Windows o macOS
  • Richiesta GPU NVIDIA: Nessun fallback solo CPU
  • Pipeline statica: La struttura della pipeline è definita al momento della compilazione e non può cambiare dinamicamente
  • fn.pad è solo destra/basso: Usa fn.crop con out_of_bounds_policy="pad" per il padding centrato
  • Nessuna modalità rettangolare: Le pipeline DALI producono output di dimensioni fisse (es. 640×640). La auto=True modalità rettangolare che produce output di dimensioni variabili (es. 384×640) non è supportata. Nota che sebbene TensorRT supporti forme di input dinamiche, una pipeline DALI di dimensioni fisse si abbina naturalmente a un motore di dimensioni fisse per il massimo throughput
  • Memoria con istanze multiple: Utilizzare instance_group con count > 1 in Triton può causare un elevato utilizzo della memoria. Usa il gruppo di istanze predefinito per il modello DALI

FAQ

Come 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 della 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 riscontrano con input ad alta risoluzione (1080p, 4K), grandi batch size e sistemi con un numero limitato di core CPU per GPU.

Posso usare DALI con modelli PyTorch (non solo TensorRT)?

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

Qual è la differenza tra fn.pad e fn.crop per il padding?

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

DALI produce risultati identici ai pixel rispetto alla pre-elaborazione della 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 vs CPU, ma queste non hanno alcun impatto misurabile sul rilevamento precisione.

Che dire di CV-CUDA come alternativa a DALI?

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

Commenti