Passer au contenu

Prétraitement GPU avec NVIDIA

Introduction

Lors du déploiement Ultralytics YOLO en production, le prétraitement devient souvent le goulot d'étranglement. Alors que TensorRT peut exécuter l'inférence du modèle en quelques millisecondes seulement, le prétraitement CPU(redimensionnement, remplissage, normalisation) peut prendre entre 2 et 10 ms par image, en particulier à haute résolution. NVIDIA (Data Loading Library) résout ce problème en transférant l'ensemble du pipeline de prétraitement vers le GPU.

Ce guide vous explique comment créer des pipelines DALI qui reproduisent fidèlementYOLO Ultralytics , et comment les intégrer à model.predict(), le traitement des flux vidéo et le déploiement de bout en bout avec Serveur d'inférence Triton.

À qui s'adresse ce guide ?

Ce guide s'adresse aux ingénieurs qui déploient YOLO dans des environnements de production où CPU constitue un goulot d'étranglement avéré — généralement TensorRT déploiements sur NVIDIA , des pipelines vidéo à haut débit, ou Serveur d'inférence Triton configurations. Si vous effectuez une inférence standard avec model.predict() et s'il n'y a pas de goulot d'étranglement au niveau du prétraitement, le CPU par défaut fonctionne bien.

Résumé rapide

  • Vous mettez en place un pipeline DALI ? Utilisez fn.resize(mode="not_larger") + fn.crop(out_of_bounds_policy="pad") + fn.crop_mirror_normalize pour reproduire le prétraitement « letterbox » YOLO sur GPU.
  • Intégration avec Ultralytics? Transmettez la sortie DALI sous forme de torch.Tensor à model.predict() — Ultralytics automatiquement le prétraitement des images.
  • Vous effectuez un déploiement avec Triton? Utilisez le backend DALI avec un TensorRT pourCPU .

Pourquoi utiliser DALI pour YOLO ?

Dans un pipeline YOLO classique, les étapes de prétraitement s'exécutent sur le CPU:

  1. Décoder l'image (JPEG/PNG)
  2. Redimensionner tout en conservant les proportions
  3. Ajuster à la taille souhaitée (format letterbox)
  4. Normaliser valeurs de pixels provenant de [0, 255] à [0, 1]
  5. Convertir la mise en page de HWC en CHW

Avec DALI, toutes ces opérations s'exécutent sur le GPU, ce qui élimine le CPU . Cela s'avère particulièrement utile lorsque :

ScénarioPourquoi DALI est utile
GPU rapide GPUTensorRT Les moteurs TensorRT, avec un temps d'inférence inférieur à la milliseconde, font CPU le principal facteur de coût
Entrées haute résolutionLes flux vidéo en 1080p et 4K nécessitent des opérations de redimensionnement coûteuses
Grands volumes de productionTraitement par inférence côté serveur de nombreuses images en parallèle
Nombre limité de CPUAppareils en périphérie tels que NVIDIA , ou GPU à forte densité GPU avec peu de CPU par GPU

Prérequis

Uniquement sous Linux

NVIDIA est uniquement compatible avec Linux. Il n'est pas disponible sous Windows ni sous macOS.

Installez les packages requis:

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

Conditions requises :

  • GPU NVIDIA GPU capacité de calcul 5.0+ / architecture Maxwell ou plus récente)
  • CUDA .CUDA ou version ultérieure, ou CUDA 12.0 ou version ultérieure
  • Python .10 à 3.14
  • système d'exploitation Linux

Comprendre YOLO

Avant de créer un pipeline DALI, il est utile de bien comprendre ce que Ultralytics exactement Ultralytics lors du prétraitement. La classe principale est LetterBox dans 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'ensemble du pipeline de prétraitement dans ultralytics/engine/predictor.py effectue les étapes suivantes :

ÉtapeFonctionnementCPUÉquivalent DALI
1Redimensionnement de la boîte aux lettrescv2.resizefn.resize(mode="not_larger")
2Justification centréecv2.copyMakeBorderfn.crop(out_of_bounds_policy="pad")
3BGR → RVBim[..., ::-1]fn.decoders.image(output_type=types.RGB)
4HWC → CHW + normalisation / 255np.transpose + tensor / 255fn.crop_mirror_normalize(std=[255,255,255])

La technique du « letterboxing » préserve le format d'image en :

  1. Échelle de calcul : r = min(target_h / h, target_w / w)
  2. Redimensionner à (round(w * r), round(h * r))
  3. Remplir l'espace restant avec du gris (114) pour atteindre la taille souhaitée
  4. Centrer l'image de manière à ce que la marge soit répartie de manière égale des deux côtés

Pipeline DALI pour YOLO

Utilisez le pipeline centré ci-dessous comme référence par défaut. Il correspond à Ultralytics LetterBox(center=True) comportement, qui est celui utilisé par YOLO standard.

Cette version reproduit exactement le Ultralytics par défaut Ultralytics avec un remplissage centré, en faisant correspondre LetterBox(center=True):

Pipeline DALI avec remplissage centré (recommandé)

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

Quand est-ce que fn.pad C'est suffisant ?

Si vous n'avez pas besoin d'une précision absolue LetterBox(center=True) parité, vous pouvez simplifier l'étape de remplissage en utilisant fn.pad(...) au lieu de fn.crop(..., out_of_bounds_policy="pad"). Cette variante ne rembourre que le à droite et en bas les bords, ce qui peut convenir pour des pipelines de déploiement personnalisés, mais cela ne correspondra pas exactement au comportement par défaut Ultralytics, qui consiste à centrer l'image dans un cadre.

Pourquoi fn.crop pour un remplissage centré ?

DALI fn.pad L'opérateur ajoute uniquement un remplissage à la à droite et en bas bords. Pour obtenir un remplissage centré (conforme à Ultralytics LetterBox(center=True)), utilisez fn.crop avec out_of_bounds_policy="pad". Avec la valeur par défaut crop_pos_x=0.5 et crop_pos_y=0.5, l'image est automatiquement centrée avec un espacement symétrique.

Incohérence de l'anticrénelage

DALI fn.resize active l'anticrénelage par défaut (antialias=True), tandis que la bibliothèque OpenCV cv2.resize avec INTER_LINEAR fait pas appliquer l'anticrénelage. Toujours activer antialias=False en DALI pour s'adapter au CPU . Si l'on omet cette étape, cela entraîne de légères différences numériques qui peuvent avoir une incidence sur précision du modèle.

Exécution du pipeline

Créer et exécuter un 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}]")

Utilisation de DALI avec Ultralytics

Vous pouvez transmettre un fichier prétraité PyTorch tensor vers model.predict(). Lorsqu'un torch.Tensor est adopté, Ultralytics ignore le prétraitement des images (lettrage, conversion BGR→RGB, HWC→CHW et normalisation /255) et n'effectue que la conversion de périphérique et le typage avant de l'envoyer au modèle.

Étant donné Ultralytics a Ultralytics accès aux dimensions d'origine de l'image dans ce cas, les coordonnées du cadre de détection sont renvoyées dans un espace de 640 × 640 avec bordures. Pour les convertir en coordonnées de l'image d'origine, utilisez scale_boxes qui gère la logique d'arrondi exacte utilisée par 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))

Cela s'applique à tous les chemins de prétraitement externes : tensor directe, flux vidéo et 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")

Aucune charge liée au prétraitement

Lorsque vous passez devant un torch.Tensor à model.predict(), l'étape de prétraitement de l'image prend environ 0,004 ms (pratiquement rien) contre environ 1 à 10 ms avec CPU . Le tensor être au format BCHW, de type float32 (ou float16), et normalisé à [0, 1]. Ultralytics gérer automatiquement le transfert de données et la conversion de types de données.

DALI avec flux vidéo

Pour le traitement vidéo en temps réel, utilisez fn.external_source pour lire des images provenant de n'importe quelle source — OpenCV, GStreamer ou des bibliothèques de capture personnalisées :

Pipeline DALI pour le prétraitement des flux vidéo

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)

Serveur Triton avec DALI

Pour un déploiement en production, associez le prétraitement DALI à TensorRT dans Triton Server à l'aide d'un modèle d'ensemble. Cela élimine complètement CPU : les octets JPEG bruts sont entrés, les détections sont produites, et tout est traité sur le GPU.

Structure du référentiel de modèles

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

Étape 1 : Créer le pipeline DALI

Sérialiser le pipeline DALI pour le backend Triton :

Sérialiser le pipeline DALI pour 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")

Étape 2 : Exporter YOLO TensorRT

Exporter YOLO vers 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

Étape 3 : Configurer 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"
      }
    }
  ]
}

Comment fonctionne la cartographie d'ensemble

L'ensemble relie les modèles via tensor virtuels. Le output_map valeur "preprocessed_image" dans l'étape DALI correspond à input_map valeur "preprocessed_image" dans TensorRT . Il s'agit de noms arbitraires qui relient la sortie d'une étape à l'entrée de l'étape suivante ; ils ne doivent pas nécessairement correspondre tensor internes du modèle.

Étape 4 : Envoyer des demandes d'inférence

Pourquoi tritonclient au lieu de YOLO(\"http://...\")?

Ultralytics Triton intégrée Triton qui gère automatiquement le pré- et le post-traitement. Cependant, cela ne fonctionnera pas avec l'ensemble DALI car YOLO() envoie un tensor float32 prétraité tensor l'ensemble attend des octets JPEG bruts. Utilisez tritonclient spécialement pour les ensembles DALI, et le intégration native pour les installations standard sans DALI.

Envoyer des images à 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")

Traitement par lots d'images JPEG

Lorsque vous envoyez un lot d'images JPEG à Triton, alignez tous les tableaux d'octets encodés à la même longueur (le nombre maximal d'octets dans le lot). Triton les formes des lots soient homogènes pour le tensor d'entrée.

Tâches prises en charge

Le prétraitement DALI fonctionne avec toutes YOLO qui utilisent le modèle standard LetterBox pipeline :

TâchePrise en chargeRemarques
DétectionPrétraitement standard des boîtes aux lettres
SegmentationMême prétraitement que pour la détection
Estimation de poseMême prétraitement que pour la détection
Détection orientée (OBB)Même prétraitement que pour la détection
ClassificationUtilise les transformations Torchvision (recadrage centré), et non le format letterbox

Limites

  • Uniquement sous Linux: DALI ne prend pas en charge Windows ni macOS
  • GPU NVIDIA GPU : pas de solution de secours CPU
  • Pipeline statique: la structure du pipeline est définie au moment de la compilation et ne peut pas être modifiée de manière dynamique
  • fn.pad à droite/en bas uniquement: Utilisez fn.crop avec out_of_bounds_policy="pad" pour un remplissage centré
  • Pas de mode rect: Les pipelines DALI produisent des sorties de taille fixe (par exemple, 640×640). Le auto=True le mode rect qui génère des sorties de taille variable (par exemple, 384×640) n'est pas pris en charge. Notez que bien que TensorRT bien qu'il prenne en charge les formats d'entrée dynamiques, un pipeline DALI à taille fixe s'associe naturellement à un moteur à taille fixe pour un débit maximal
  • Mémoire à instances multiples: Utilisation de instance_group avec count > La version 1 de Triton entraîner une utilisation élevée de la mémoire. Utilisez le groupe d'instances par défaut pour le modèle DALI

FAQ

Comment la vitesse de prétraitement DALI se compare-t-elle à celle CPU ?

L'avantage dépend de votre pipeline. Lorsque GPU est déjà rapide avec TensorRT, CPU , qui prend entre 2 et 10 ms, peut devenir le principal facteur de coût. DALI élimine ce goulot d'étranglement en exécutant le prétraitement sur le GPU. Les gains les plus importants sont observés avec des entrées haute résolution (1080p, 4K), des lots volumineux et des systèmes disposant d'un nombre limité CPU par GPU.

Puis-je utiliser DALI avec PyTorch (et pas seulement avec TensorRT) ?

Oui. Utilisez DALIGenericIterator pour être prétraité torch.Tensor sorties, puis transmettez-les à model.predict(). Cependant, c'est avec TensorRT des modèles pour lesquels l'inférence est déjà très rapide et où CPU devient le goulot d'étranglement.

Quelle est la différence entre fn.pad et fn.crop pour le rembourrage ?

fn.pad ajoute un espacement uniquement à la à droite et en bas bords. fn.crop avec out_of_bounds_policy="pad" centre l'image et ajoute un espacement symétrique de chaque côté, conformément aux spécifications Ultralytics LetterBox(center=True) comportement.

Le traitement DALI produit-il des résultats identiques au niveau des pixels à ceux CPU ?

Presque identiques. Ensemble antialias=False dans fn.resize pour s'adapter à 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 précision.

Et pourquoi pasCUDA alternative à DALI ?

CUDA est NVIDIA autre NVIDIA dédiée au traitement de l'image GPU. Elle offre un contrôle au niveau de chaque opérateur (comme OpenCV mais sur GPU) plutôt que l'approche par pipeline de DALI.CUDA cvcuda.copymakeborder() prend en charge le remplissage explicite par côté, ce qui facilite la création d'un format letterbox centré. Optez pour DALI pour les flux de travail basés sur des pipelines (en particulier avec Triton), ainsi queCUDA un contrôle précis au niveau des opérateurs dans le code d'inférence personnalisé.



📅 Créé il y a 0 jours ✏️ Mis à jour il y a 0 jours
raimbekovm

Commentaires