Meet YOLO26: next-gen vision AI.

Link to this sectionPreprocessing accelerato via GPU con NVIDIA DALI#

Link to this sectionIntroduzione#

Quando distribuisci i modelli Ultralytics YOLO in produzione, la preelaborazione diventa spesso il collo di bottiglia. Mentre TensorRT può eseguire l'inferenza del modello in pochi millisecondi, la preelaborazione 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 preelaborazione sulla GPU.

Questa guida ti accompagna nella creazione di pipeline DALI che replicano esattamente la preelaborazione di Ultralytics YOLO, integrandole con model.predict(), elaborando flussi video e distribuendo il tutto end-to-end con Triton Inference Server.

A chi è rivolta questa guida?

Questa guida è pensata per gli ingegneri che distribuiscono modelli YOLO in ambienti di produzione in cui la preelaborazione tramite CPU rappresenta un collo di bottiglia misurato: tipicamente implementazioni TensorRT su GPU NVIDIA, pipeline video ad alto throughput o configurazioni Triton Inference Server. Se esegui un'inferenza standard con model.predict() e non riscontri colli di bottiglia nella preelaborazione, la pipeline CPU predefinita funziona bene.

Sommario rapido
  • Stai costruendo una pipeline DALI? Usa fn.resize(mode="not_larger") + fn.crop(out_of_bounds_policy="pad") + fn.crop_mirror_normalize per replicare la preelaborazione letterbox di YOLO sulla GPU.
  • Stai integrando con Ultralytics? Passa l'output di DALI come torch.Tensor a model.predict(): Ultralytics salterà automaticamente la preelaborazione dell'immagine.
  • Stai distribuendo con Triton? Usa il backend DALI con un insieme TensorRT per una preelaborazione a zero carico sulla CPU.

Link to this sectionPerché usare DALI per la preelaborazione YOLO#

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

  1. Decodifica l'immagine (JPEG/PNG)
  2. Ridimensiona mantenendo le proporzioni
  3. Riempie alla dimensione 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 GPU rapidaI motori TensorRT con inferenza sub-millisecondo rendono la preelaborazione CPU il costo dominante
Input ad alta risoluzioneI flussi video 1080p e 4K richiedono costose operazioni di ridimensionamento
Grandi dimensioni del batchElaborazione dell'inferenza lato server su molte immagini in parallelo
Core CPU limitatiDispositivi edge come NVIDIA Jetson o server GPU densi con pochi core CPU per GPU

Link to this sectionPrerequisiti#

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 (capacità di calcolo 5.0+ / Maxwell o successiva)
  • CUDA 11.0+, 12.0+ o 13.0+
  • Python 3.10-3.14
  • Sistema operativo Linux

Link to this sectionComprendere la preelaborazione YOLO#

Prima di costruire una pipeline DALI, è utile comprendere esattamente cosa fa Ultralytics durante la preelaborazione. 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 preelaborazione 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

Link to this sectionPipeline DALI per YOLO#

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

Link to this sectionPipeline centrata (consigliata, corrisponde a Ultralytics LetterBox)#

Questa versione replica esattamente la preelaborazione 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 `fn.pad` è sufficiente?

Se non hai bisogno della parità esatta con LetterBox(center=True), 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 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 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 automaticamente centrata con un riempimento simmetrico.

Discrepanza Antialias

Il 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. Omettere questo passaggio causa sottili differenze numeriche che possono influire sulla accuratezza del modello.

Link to this sectionEsecuzione 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}]")

Link to this sectionUso di DALI con Ultralytics Predict#

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

Poiché in questo caso Ultralytics non ha accesso alle dimensioni originali dell'immagine, le coordinate del box di rilevamento vengono restituite nello spazio letterboxed 640×640. Per riportarle alle coordinate originali dell'immagine, 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 preelaborazione esterni: input diretto del tensore, 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")
Zero sovraccarico di preelaborazione

Quando passi un torch.Tensor a model.predict(), il passaggio di preelaborazione dell'immagine richiede ~0.004ms (praticamente zero) rispetto a ~1-10ms con la preelaborazione 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 casting del dtype.

Link to this sectionDALI con flussi video#

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

Pipeline DALI per la preelaborazione 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

Link to this sectionTriton Inference Server con DALI#

Per la distribuzione in produzione, combina la preelaborazione DALI con l'inferenza TensorRT in Triton Inference Server utilizzando un modello d'insieme. Questo elimina completamente la preelaborazione CPU: vengono inseriti byte JPEG grezzi e si ottengono i rilevamenti, con tutto il processo eseguito sulla GPU.

Link to this sectionStruttura 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

Link to this sectionPassaggio 1: Crea la pipeline DALI#

Serializza la pipeline DALI per il backend Triton DALI:

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

Link to this sectionPassaggio 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

Link to this sectionPassaggio 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 il mapping 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. 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.

Link to this sectionPassaggio 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")
Invio in batch di immagini JPEG

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

Link to this sectionAttività supportate#

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

AttivitàSupportatoNote
RilevamentoPre-elaborazione letterbox standard
Segmentazione di istanzeStessa pre-elaborazione del rilevamento
Segmentazione semanticaStessa pre-elaborazione dell'immagine del rilevamento
Stima della posaStessa pre-elaborazione del rilevamento
Rilevamento orientato (OBB)Stessa pre-elaborazione del rilevamento
ClassificazioneUtilizza trasformazioni torchvision (ritaglio centrale), non letterbox

Link to this sectionLimitazioni#

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

Link to this sectionFAQ#

Link to this sectionCome 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 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 vedono con input ad alta risoluzione (1080p, 4K), grandi batch sizes e sistemi con core CPU limitati per GPU.

Link to this sectionPosso usare DALI con modelli PyTorch (non solo TensorRT)?#

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 dove l'inferenza è già molto veloce e la pre-elaborazione CPU diventa il collo di bottiglia.

Link to this sectionQual è la differenza tra fn.pad e fn.crop per il riempimento?#

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

Link to this sectionDALI produce risultati identici a livello di pixel rispetto alla pre-elaborazione 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 rispetto a quella CPU, ma queste non hanno alcun impatto misurabile sull'accuratezza del rilevamento.

Link to this sectionE che dire di CV-CUDA come alternativa a DALI?#

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 a pipeline di DALI. cvcuda.copymakeborder() di CV-CUDA supporta il riempimento 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