Vai al contenuto

Pre-elaborazione GPU con NVIDIA

Introduzione

Durante l'implementazione Ultralytics YOLO in produzione, la pre-elaborazione diventa spesso il collo di bottiglia. Mentre TensorRT è in grado di eseguire l'inferenza del modello in pochi millisecondi, la pre-elaborazione CPU(ridimensionamento, riempimento, normalizzazione) può richiedere dai 2 ai 10 ms per immagine, specialmente ad alte risoluzioni. NVIDIA (Data Loading Library) risolve questo problema spostando l'intera pipeline di pre-elaborazione sulla GPU.

Questa guida illustra come creare pipeline DALI che riproducano fedelmenteYOLO Ultralytics YOLO , integrandole con model.predict(), elaborazione di flussi video e implementazione end-to-end con Triton Inference Server.

A chi è rivolta questa guida?

La presente guida è destinata agli ingegneri che implementano YOLO in ambienti di produzione in cui CPU rappresenta un collo di bottiglia misurabile — in genere TensorRT implementazioni su NVIDIA , pipeline video ad alta produttività o Triton Inference Server configurazioni. Se stai eseguendo un'inferenza standard con model.predict() e se non si verificano colli di bottiglia nella fase di pre-elaborazione, la CPU predefinita CPU funziona bene.

Sintesi

  • 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 in formato letterbox YOLO sulla GPU.
  • Integrazione con Ultralytics? Trasmetti l'uscita DALI come un torch.Tensor a model.predict() — Ultralytics automaticamente la fase di pre-elaborazione delle immagini.
  • Stai effettuando il deployment con Triton? Utilizza il backend DALI con un TensorRT perCPU .

Perché utilizzare DALI per YOLO

In una tipica pipeline YOLO , le fasi di pre-elaborazione vengono eseguite sulla CPU:

  1. Decodifica l'immagine (JPEG/PNG)
  2. Ridimensiona mantenendo le proporzioni
  3. Adatta all'area di destinazione (letterbox)
  4. Normalizza 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 CPU . Ciò risulta particolarmente utile quando:

ScenarioPerché DALI è utile
GPU veloce GPUTensorRT I motori con inferenza inferiore al millisecondo rendono CPU il costo principale
Ingressi ad alta risoluzioneI flussi video a 1080p e 4K richiedono costose operazioni di ridimensionamento
Lotti di grandi dimensioniElaborazione inferenziale lato server di molte immagini in parallelo
Numero limitato di CPUDispositivi periferici come NVIDIA o GPU con GPU ad alta densità e pochi CPU per GPU

Prerequisiti

Solo per Linux

NVIDIA è compatibile solo con Linux. Non è disponibile su Windows o macOS.

Installa i pacchetti necessari:

pip install ultralytics
pip install --extra-index-url https://pypi.nvidia.com nvidia-dali-cuda120
pip install ultralytics
pip install --extra-index-url https://pypi.nvidia.com nvidia-dali-cuda110

Requisiti:

  • GPU NVIDIA GPU capacità di calcolo 5.0+ / Maxwell o versioni successive)
  • CUDA .0 o versioni successive oppure 12.0 o versioni successive
  • Python .10-3.14
  • Sistema operativo Linux

Comprendere YOLO

Prima di creare una pipeline DALI, è utile capire esattamente cosa 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)
)

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

PassaggioFunzionamentoCPUEquivalente DALI
1Modifica delle dimensioni della casella di postacv2.resizefn.resize(mode="not_larger")
2Allineamento al centrocv2.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 letterboxing mantiene le proporzioni tramite:

  1. Scala di calcolo: r = min(target_h / h, target_w / w)
  2. Ridimensionamento a (round(w * r), round(h * r))
  3. Riempire lo spazio rimanente con il colore grigio (114) per raggiungere la dimensione desiderata
  4. Centrare l'immagine in modo che il margine interno sia distribuito equamente su entrambi i lati

Pipeline DALI per YOLO

Utilizza la pipeline centrata riportata di seguito come riferimento predefinito. È compatibile con Ultralytics LetterBox(center=True) comportamento, che è quello utilizzato YOLO standard.

Questa versione riproduce esattamente la Ultralytics predefinita Ultralytics con riempimento centrato, in modo che corrisponda LetterBox(center=True):

Pipeline DALI con riempimento centrato (consigliato)

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 basta?

Se non ti serve l'esatto LetterBox(center=True) parità, è possibile semplificare la fase di riempimento utilizzando fn.pad(...) invece di fn.crop(..., out_of_bounds_policy="pad"). Questa variante inserisce solo il a destra e in basso bordi, il che può essere accettabile per le pipeline di distribuzione personalizzate, ma non corrisponderà esattamente al comportamento predefinito Ultralytics con il formato letterbox centrato.

Perché fn.crop per il riempimento centrato?

di DALI fn.pad L'operatore aggiunge solo spazi di riempimento al a destra e in basso bordi. Per ottenere un riempimento centrato (in linea con Ultralytics LetterBox(center=True)), utilizzare fn.crop con out_of_bounds_policy="pad". Con l'impostazione predefinita crop_pos_x=0.5 e crop_pos_y=0.5, l'immagine viene automaticamente centrata con un margine simmetrico.

Discrepanza nell'antialiasing

di DALI fn.resize attiva l'antialiasing per impostazione predefinita (antialias=True), mentre OpenCV cv2.resize con INTER_LINEAR fa non applica l'antialiasing. Impostare sempre antialias=False in DALI per adattarsi alla CPU . Se si omette questo passaggio, si verificano sottili differenze numeriche che possono influire su precisione 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}]")

Utilizzo di DALI con Ultralytics

È possibile passare un file pre-elaborato PyTorch tensor a model.predict(). Quando un torch.Tensor è stato approvato, Ultralytics salta la pre-elaborazione delle immagini (letterbox, conversione da BGR a RGB, da HWC a CHW e normalizzazione a /255) ed esegue solo la conversione per il dispositivo e il casting del tipo di dati prima di inviarli al modello.

Poiché in questo caso Ultralytics ha accesso alle dimensioni originali dell'immagine, le coordinate del riquadro di rilevamento vengono restituite nello spazio letterbox da 640×640. Per ricollegarle alle coordinate dell'immagine originale, utilizzare 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))

Ciò vale per tutti i percorsi di pre-elaborazione esterni: tensor diretto, flussi video e Triton .

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

Nessun sovraccarico di pre-elaborazione

Quando si supera un torch.Tensor a model.predict(), la fase di pre-elaborazione dell'immagine richiede circa 0,004 ms (praticamente zero) rispetto ai circa 1-10 ms necessari con CPU . Il tensor essere in formato BCHW, di tipo float32 (o float16) e normalizzato a [0, 1]. Ultralytics a gestire automaticamente il trasferimento dei dispositivi e la conversione dei tipi di dati.

DALI con flussi video

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

Pipeline DALI per la pre-elaborazione dei flussi 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
import cv2
import numpy as np
import torch

from ultralytics import YOLO

model = YOLO("yolo26n.engine")  # TensorRT model

pipe = yolo_video_pipeline(target_size=640)
pipe.build()

cap = cv2.VideoCapture("video.mp4")
while cap.isOpened():
    ret, frame = cap.read()
    if not ret:
        break

    # Feed BGR frame (convert to RGB for DALI)
    frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
    pipe.feed_input("input", [np.array(frame_rgb)])
    (output,) = pipe.run()

    # Convert DALI output to torch tensor for inference.
    # This is a simple fallback path: using feed_input() with pipe.run() keeps a GPU->CPU->GPU copy.
    # For high-throughput deployments, prefer a reader-based pipeline plus DALIGenericIterator to keep data on GPU.
    tensor = torch.tensor(output.as_cpu().as_array()).to("cuda")
    results = model.predict(tensor, verbose=False)

Triton Server con DALI

Per l'implementazione in produzione, combinare la pre-elaborazione DALI con TensorRT nel Triton Server utilizzando un modello ensemble. Ciò elimina completamente CPU : entrano i byte JPEG grezzi, escono i rilevamenti, con tutto elaborato sulla GPU.

Struttura del repository dei modelli

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: Creare la pipeline DALI

Serializza la pipeline DALI per il backend Triton :

Serializzare 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: esportare YOLO TensorRT

Esporta YOLO nel 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: Configurare 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.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 l'Ensemble Mapping

L'insieme collega i modelli tramite tensor virtuali. output_map valore "preprocessed_image" nella fase DALI corrisponde al input_map valore "preprocessed_image" nella TensorRT . Si tratta di nomi arbitrari che collegano l'output di una fase all'input della fase successiva; non è necessario che corrispondano tensor interni del modello.

Fase 4: Inviare le richieste di inferenza

Perché tritonclient invece di YOLO(\"http://...\")?

Ultralytics Triton integrato Triton che gestisce automaticamente la pre-elaborazione e la post-elaborazione. Tuttavia, non funziona con l'ensemble DALI perché YOLO() invia un tensor float32 preelaborato, tensor l'ensemble si aspetta byte JPEG non elaborati. Utilizza tritonclient specificamente per gli ensemble DALI, e il integrazione nativa per installazioni standard senza DALI.

Invia immagini all 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 si invia un lotto di immagini JPEG a Triton, è necessario allineare tutti gli array di byte codificati alla stessa lunghezza (il numero massimo di byte nel lotto). Triton i lotti abbiano una struttura omogenea per il tensor di input.

Attività supportate

La pre-elaborazione DALI funziona con tutte YOLO che utilizzano lo standard LetterBox pipeline:

TaskSupportatoNote
RilevamentoPre-elaborazione standard per la casella postale
SegmentazioneStessa pre-elaborazione utilizzata per il rilevamento
Stima della posaStessa pre-elaborazione utilizzata per il rilevamento
Rilevamento orientato (OBB)Stessa pre-elaborazione utilizzata per il rilevamento
ClassificazioneUtilizza le trasformazioni Torchvision (ritaglio al centro), non il formato letterbox

Limitazioni

  • Solo Linux: DALI non supporta Windows né macOS
  • GPU NVIDIA : non è prevista una soluzione alternativa CPU
  • Pipeline statica: la struttura della pipeline viene definita in fase di compilazione e non può essere modificata dinamicamente
  • fn.pad solo a destra/in basso: Usa fn.crop con out_of_bounds_policy="pad" per il riempimento centrato
  • Modalità non rettangolare: Le pipeline DALI producono output di dimensioni fisse (ad es. 640×640). Il auto=True La modalità che produce immagini di dimensioni variabili (ad es. 384×640) non è supportata. Si noti che, sebbene TensorRT supporta forme di input dinamiche; una pipeline DALI a dimensione fissa si abbina naturalmente a un motore a dimensione fissa per garantire la massima produttività
  • Memoria con più istanze: Utilizzo di instance_group con count > 1 in Triton causare un elevato consumo di memoria. Utilizza il gruppo di istanze predefinito per il modello DALI

FAQ

Come si colloca la velocità di CPU DALI rispetto a CPU ?

Il vantaggio dipende dalla tua pipeline. Quando GPU è già veloce con TensorRT, CPU (2-10 ms) può diventare il costo dominante. DALI elimina questo collo di bottiglia eseguendo la pre-elaborazione sulla GPU. I vantaggi maggiori si riscontrano con input ad alta risoluzione (1080p, 4K), batch di grandi dimensioni e sistemi con un numero limitato CPU per GPU.

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

Sì. Usa DALIGenericIterator per essere sottoposto a pre-elaborazione torch.Tensor risultati, quindi passarli a model.predict(). Tuttavia, il miglioramento delle prestazioni è maggiore con TensorRT modelli in cui l'inferenza è già molto veloce e CPU diventa il collo di bottiglia.

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

fn.pad aggiunge un margine solo al a destra e in basso bordi. fn.crop con out_of_bounds_policy="pad" centra l'immagine e aggiunge un margine simmetrico su tutti i lati, in linea con Ultralytics LetterBox(center=True) comportamento.

DALI produce risultati identici a quelli ottenuti con CPU ?

Quasi identici. Set antialias=False in fn.resize in modo da corrispondere a OpenCV cv2.INTER_LINEAR. Minor floating-point differences (< 0.001) may occur due to GPU vs CPU arithmetic, but these have no measurable impact on detection accuratezza.

Che ne pensate diCUDA alternativa a DALI?

CUDA è un'altra NVIDIA per l'elaborazione visiva GPU. Offre un controllo operatore per operatore (come OpenCV (ma sulla GPU) anziché l'approccio basato su pipeline di DALI.CUDA cvcuda.copymakeborder() supporta il riempimento esplicito per ciascun lato, semplificando la creazione di immagini in formato letterbox centrate. Scegli DALI per i flussi di lavoro basati su pipeline (in particolare con Triton), eCUDA un controllo dettagliato a livello di operatore nel codice di inferenza personalizzato.



📅 Creato 0 giorni fa ✏️ Aggiornato 0 giorni fa
raimbekovm

Commenti