YOLO Vision 2026 :

Prétraitement accéléré par GPU avec NVIDIA DALI#

Lors du déploiement de modèles Ultralytics YOLO en production, le preprocessing devient souvent le goulet d'étranglement. Alors que TensorRT peut exécuter l'inference d'un modèle en quelques millisecondes seulement, le prétraitement basé sur le CPU (redimensionnement, remplissage, normalisation) peut prendre 2 à 10 ms par image, en particulier à haute résolution. NVIDIA DALI (Data Loading Library) résout ce problème en déplaçant l'ensemble du pipeline de prétraitement sur le GPU.

Ce guide vous accompagne dans la création de pipelines DALI qui reproduisent exactement le prétraitement d'Ultralytics YOLO, en les intégrant avec model.predict(), en traitant des flux vidéo et en effectuant un déploiement de bout en bout avec Triton Inference Server.

À qui s'adresse ce guide ?

Ce guide s'adresse aux ingénieurs déployant des modèles YOLO dans des environnements de production où le prétraitement CPU constitue un goulet d'étranglement mesuré — généralement des déploiements TensorRT sur des GPU NVIDIA, des pipelines vidéo à haut débit ou des configurations Triton Inference Server. Si vous exécutez une inférence standard avec model.predict() et que vous ne rencontrez pas de goulet d'étranglement lié au prétraitement, le pipeline CPU par défaut fonctionne très bien.

Résumé rapide
  • Vous créez 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 de type letterbox de YOLO sur le GPU.
  • Vous l'intégrez avec Ultralytics ? Transmettez la sortie DALI en tant que torch.Tensor à model.predict() — Ultralytics ignore automatiquement le prétraitement des images.
  • Tu déploies avec Triton ? Utilise le backend DALI avec un ensemble TensorRT pour un prétraitement zéro-CPU.

Pourquoi utiliser DALI pour le prétraitement YOLO ?#

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

  1. Décodage de l'image (JPEG/PNG)
  2. Redimensionnement en préservant le rapport hauteur/largeur
  3. Remplissage (pad) pour atteindre la taille cible (letterbox)
  4. Normaliser les valeurs des pixels de [0, 255] à [0, 1]
  5. Conversion de la disposition HWC vers CHW

Avec DALI, toutes ces opérations s'exécutent sur le GPU, éliminant ainsi le goulot d'étranglement du CPU. C'est particulièrement utile dans les cas suivants :

ScénarioPourquoi DALI est utile
Inférence GPU rapideLes moteurs TensorRT dotés d'une inférence sub-milliseconde font du prétraitement CPU le coût dominant.
Entrées haute résolutionLes flux vidéo 1080p et 4K nécessitent des opérations de redimensionnement coûteuses.
Grandes batch sizesInférence côté serveur traitant de nombreuses images en parallèle.
Cœurs CPU limitésLes appareils de périphérie (Edge) comme NVIDIA Jetson, ou les serveurs GPU denses disposant de peu de cœurs CPU par GPU.

Prérequis#

Linux uniquement

NVIDIA DALI prend en charge Linux uniquement. Il n'est pas disponible sous Windows ou macOS.

Installe les packages requis :

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

Prérequis :

  • GPU NVIDIA (capacité de calcul 5.0+ / Maxwell ou plus récent)
  • CUDA 11.0+, 12.0+ ou 13.0+
  • Python 3.10-3.14
  • Système d'exploitation Linux

Comprendre le prétraitement YOLO#

Avant de créer un pipeline DALI, il est utile de comprendre exactement ce qu'Ultralytics fait lors du prétraitement. La classe clé 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)
)

Le pipeline de prétraitement complet dans ultralytics/engine/predictor.py effectue ces étapes :

ÉtapeOpérationFonction CPUÉquivalent DALI
1Redimensionnement Letterboxcv2.resizefn.resize(mode="not_larger")
2Remplissage centrécv2.copyMakeBorderfn.crop(out_of_bounds_policy="pad")
3BGR → RGBim[..., ::-1]fn.decoders.image(output_type=types.RGB)
4HWC → CHW + normalisation /255np.transpose + tensor / 255fn.crop_mirror_normalize(std=[255,255,255])

L'opération letterbox préserve le rapport hauteur/largeur en :

  1. Calcul de l'échelle : r = min(target_h / h, target_w / w)
  2. Redimensionnement vers (round(w * r), round(h * r))
  3. Remplissage de l'espace restant avec du gris (114) pour atteindre la taille cible.
  4. Centrant l'image afin que le remplissage soit distribué équitablement des deux côtés

Pipeline DALI pour YOLO#

Le pipeline DALI recommandé reproduit le comportement par défaut LetterBox(center=True) d'Ultralytics, qui est celui utilisé par l'inférence YOLO standard.

Pipeline centré (Recommandé, correspond à Ultralytics LetterBox)#

Cette version reproduit exactement le prétraitement par défaut d'Ultralytics avec un rembourrage (padding) centré, correspondant à LetterBox(center=True) :

Pipeline DALI avec remplissage centré (recommandé)
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
Quand `fn.pad` suffit-il ?

Si vous n'avez pas besoin d'une parité exacte avec LetterBox(center=True), vous pouvez simplifier l'étape de remplissage en utilisant fn.pad(...) au lieu de fn.crop(..., out_of_bounds_policy="pad"). Cette variante ne remplit que les bords droit et inférieur, ce qui peut convenir pour des pipelines de déploiement personnalisés, mais cela ne correspondra pas exactement au comportement de letterbox centré par défaut d'Ultralytics.

Pourquoi `fn.crop` pour le remplissage centré ?

L'opérateur fn.pad de DALI ajoute uniquement un remplissage aux bords droit et inférieur. Pour obtenir un remplissage centré (correspondant à LetterBox(center=True) d'Ultralytics), utilisez fn.crop avec out_of_bounds_policy="pad". Avec les valeurs par défaut crop_pos_x=0.5 et crop_pos_y=0.5, l'image est automatiquement centrée avec un remplissage symétrique.

Désaccord sur l'anti-aliasing

L'opérateur fn.resize de DALI active l'anticrénelage par défaut (antialias=True), tandis que cv2.resize d'OpenCV avec INTER_LINEAR n'applique pas d'anticrénelage. Définissez toujours antialias=False dans DALI pour correspondre au pipeline CPU. L'omission de cette étape entraîne de subtiles différences numériques qui peuvent affecter la model accuracy.

Exécution du pipeline#

Construis et exécute 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 Predict#

Vous pouvez passer un tenseur PyTorch prétraité directement à model.predict(). Lorsqu'un torch.Tensor est fourni, Ultralytics ignore le prétraitement des images (letterbox, BGR→RGB, HWC→CHW et normalisation /255) et effectue uniquement le transfert sur l'appareil et la conversion de type (dtype casting) avant de l'envoyer au modèle.

Puisqu'Ultralytics n'a pas accès aux dimensions d'origine de l'image dans ce cas, les coordonnées des boîtes de détection sont renvoyées dans l'espace letterbox de 640×640. Pour les ramener aux 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 — entrée directe par tenseur, flux vidéo et déploiement 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")
Zéro surcoût de prétraitement

Lorsque vous passez un torch.Tensor à model.predict(), l'étape de prétraitement de l'image prend ~0,004 ms (essentiellement zéro) par rapport à ~1-10 ms avec le prétraitement CPU. Le tenseur doit être au format BCHW, en float32 (ou float16), et normalisé à [0, 1]. Ultralytics gérera toujours automatiquement le transfert vers l'appareil et la conversion de type.

DALI avec flux vidéo#

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

Pipeline DALI pour le prétraitement de flux vidéo
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 avec DALI#

Pour le déploiement en production, combinez le prétraitement DALI avec l'inférence TensorRT dans Triton Inference Server en utilisant un modèle d'ensemble. Cela élimine entièrement le prétraitement CPU : les octets JPEG bruts entrent, les détections ressortent, tout étant traité sur le GPU.

Structure du dépôt 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érialise le pipeline DALI pour le backend Triton DALI :

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

Étape 2 : Exporter YOLO vers TensorRT#

Exporter le modèle YOLO vers le moteur 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

É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_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"
      }
    }
  ]
}
Comment fonctionne le mappage d'ensemble

L'ensemble connecte les modèles via des noms de tenseurs virtuels. La valeur output_map "preprocessed_image" dans l'étape DALI correspond à la valeur input_map "preprocessed_image" dans l'étape TensorRT. Ce sont des noms arbitraires qui lient la sortie d'une étape à l'entrée de l'étape suivante — ils n'ont pas besoin de correspondre aux noms de tenseurs internes d'un modèle.

Étape 4 : Envoyer des requêtes d'inférence#

Pourquoi utiliser `tritonclient` plutôt que `YOLO('http://...')` ?

Ultralytics dispose d'un built-in Triton support qui gère automatiquement le pré/post-traitement. Cependant, cela ne fonctionnera pas avec l'ensemble DALI car YOLO() envoie un tenseur float32 prétraité tandis que l'ensemble attend des octets JPEG bruts. Utilisez tritonclient directement pour les ensembles DALI, et l'built-in integration pour les déploiements standard sans DALI.

Envoyer des images vers l'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")
Traitement par lots d'images JPEG

Lors de l'envoi d'un lot d'images JPEG vers Triton, complète tous les tableaux d'octets encodés à la même longueur (le nombre d'octets maximum dans le lot). Triton exige des formes de lots homogènes pour le tenseur d'entrée.

Tâches prises en charge#

Le prétraitement DALI fonctionne avec toutes les tâches YOLO qui utilisent le pipeline standard LetterBox :

TâchePris en chargeNotes
DétectionPrétraitement standard de letterbox
Segmentation d'instancesMême prétraitement que pour la détection
Segmentation sémantiqueMême prétraitement d'image que pour la détection
ClassificationUtilise les transformations torchvision (recadrage central), pas le letterbox
Estimation de poseMême prétraitement que pour la détection
Oriented Detection (OBB)Même prétraitement que pour la détection

Limites#

  • Linux uniquement : DALI ne prend pas en charge Windows ou macOS
  • GPU NVIDIA requis : Pas de solution de secours utilisant uniquement le CPU
  • Pipeline statique : La structure du pipeline est définie au moment de la compilation et ne peut pas changer dynamiquement
  • fn.pad est uniquement à droite/en bas : 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 ex. 640×640). Le mode rect auto=True qui produit des sorties de taille variable (par ex. 384×640) n'est pas pris en charge. Notez que bien que TensorRT prenne en charge les formes d'entrée dynamiques, un pipeline DALI de taille fixe s'associe naturellement à un moteur de taille fixe pour un débit maximal.
  • Mémoire avec plusieurs instances : L'utilisation de instance_group avec count > 1 dans Triton peut entraîner une utilisation élevée de la mémoire. Utilisez le groupe d'instances par défaut pour le modèle DALI.

FAQ#

  • L'avantage dépend de votre pipeline. Lorsque l'inférence GPU est déjà rapide avec TensorRT, le prétraitement CPU à 2-10 ms peut devenir le coût dominant. DALI élimine ce goulet 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), de grandes batch sizes et des systèmes dotés d'un nombre limité de cœurs CPU par GPU.

  • Oui. Utilisez DALIGenericIterator pour obtenir des sorties torch.Tensor prétraitées, puis transmettez-les à model.predict(). Cependant, le gain de performance est maximal avec les modèles TensorRT où l'inférence est déjà très rapide et où le prétraitement CPU devient le goulet d'étranglement.

  • fn.pad ajoute un remplissage uniquement sur les bords droit et inférieur. fn.crop avec out_of_bounds_policy="pad" centre l'image et ajoute un remplissage symétrique sur tous les côtés, ce qui correspond au comportement de LetterBox(center=True) d'Ultralytics.

  • Presque identique. Définissez antialias=False dans fn.resize pour correspondre à cv2.INTER_LINEAR d'OpenCV. De mineures différences de virgule flottante (< 0,001) peuvent se produire en raison de l'arithmétique GPU par rapport au CPU, mais celles-ci n'ont aucun impact mesurable sur l'accuracy de détection.

  • CV-CUDA est une autre bibliothèque NVIDIA pour le traitement visuel accéléré par GPU. Elle offre un contrôle par opérateur (comme OpenCV mais sur GPU) plutôt que l'approche par pipeline de DALI. Le cvcuda.copymakeborder() de CV-CUDA prend en charge un remplissage explicite par côté, ce qui rend le letterbox centré simple. Choisissez DALI pour les flux de travail basés sur des pipelines (en particulier avec Triton), et CV-CUDA pour un contrôle précis au niveau des opérateurs dans le code d'inférence personnalisé.

Commentaires