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 prétraitement devient souvent le goulot d'étranglement. Alors que TensorRT peut effectuer l'inférence du modèle en quelques millisecondes seulement, le prétraitement basé sur le CPU (redimensionnement, ajout de bordures, normalisation) peut prendre 2 à 10 ms par image, en particulier aux hautes résolutions. NVIDIA DALI (bibliothèque de chargement des données) résout ce problème en déplaçant l'ensemble du pipeline de prétraitement vers le GPU.

Ce guide t'explique comment créer des pipelines DALI qui reproduisent exactement le prétraitement d'Ultralytics YOLO, les intégrer à model.predict(), traiter des flux vidéo et effectuer un déploiement de bout en bout avec Triton Inference Server.

À qui s'adresse ce guide ?

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

Résumé rapide
  • Tu construis un pipeline DALI ? Utilise fn.resize(mode="not_larger") + fn.crop(out_of_bounds_policy="pad") + fn.crop_mirror_normalize pour reproduire le prétraitement letterbox de YOLO sur GPU.
  • Tu intègres Ultralytics ? Transmets la sortie DALI comme 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 éliminer totalement le prétraitement sur CPU.

Pourquoi utiliser DALI pour le prétraitement de YOLO#

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

  1. Décoder l'image (JPEG/PNG)
  2. Redimensionner en conservant les proportions
  3. Ajouter des bordures pour atteindre la taille cible (letterbox)
  4. Normaliser les valeurs des pixels de [0, 255] à [0, 1]
  5. Convertir la disposition de HWC vers CHW

Avec DALI, toutes ces opérations s'exécutent sur le GPU, ce qui élimine le goulot d'étranglement du CPU. C'est particulièrement utile lorsque :

ScénarioPourquoi DALI est utile
Inférence GPU rapideLes moteurs TensorRT dont l'inférence s'effectue en moins d'une 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
Tailles de lot importantesTraitement côté serveur de nombreuses images en parallèle
Nombre limité de cœurs CPUAppareils edge comme NVIDIA Jetson, ou 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 ni 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 version ultérieure)
  • CUDA 11.0+, 12.0+ ou 13.0+
  • Python 3.10-3.14
  • Système d'exploitation Linux

Comprendre le prétraitement de YOLO#

Avant de créer un pipeline DALI, il est utile de comprendre précisément ce qu'effectue Ultralytics pendant le 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 les étapes suivantes :

ÉtapeOpérationFonction CPUÉquivalent DALI
1Redimensionnement letterboxcv2.resizefn.resize(mode="not_larger")
2Ajout de bordures centréescv2.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 conserve les proportions en :

  1. Calculant l'échelle : r = min(target_h / h, target_w / w)
  2. Redimensionnant vers (round(w * r), round(h * r))
  3. Ajoutant des bordures grises (114) à l'espace restant pour atteindre la taille cible
  4. Centrant l'image afin que les bordures soient réparties uniformément des deux côtés

Pipeline DALI pour YOLO#

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

Pipeline centré (recommandé, conforme à Ultralytics LetterBox)#

Cette version reproduit exactement le prétraitement par défaut d'Ultralytics avec des bordures centrées, conformément à LetterBox(center=True) :

Pipeline DALI avec bordures centrées (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 tu n'as pas besoin d'une parité exacte avec LetterBox(center=True), tu peux simplifier l'étape d'ajout de bordures en utilisant fn.pad(...) à la place de fn.crop(..., out_of_bounds_policy="pad"). Cette variante ajoute des bordures uniquement sur les côtés droit et inférieur, ce qui peut convenir aux pipelines de déploiement personnalisés, mais elle ne reproduira pas exactement le comportement letterbox centré par défaut d'Ultralytics.

Pourquoi utiliser `fn.crop` pour les bordures centrées ?

L'opérateur DALI fn.pad ajoute uniquement des bordures sur les côtés droit et inférieur. Pour obtenir des bordures centrées (conformes à LetterBox(center=True) d'Ultralytics), utilise fn.crop avec out_of_bounds_policy="pad". Avec crop_pos_x=0.5 et crop_pos_y=0.5 par défaut, l'image est automatiquement centrée avec des bordures symétriques.

Différence d'anticrénelage

L'opérateur DALI fn.resize 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éfinis toujours antialias=False dans DALI pour correspondre au pipeline CPU. L'omettre entraîne de subtiles différences numériques susceptibles d'affecter la précision du modèle.

Exécuter le 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}]")

Utiliser DALI avec Ultralytics Predict#

Tu peux transmettre directement un tenseur PyTorch prétraité à model.predict(). Lorsqu'un torch.Tensor est transmis, Ultralytics ignore le prétraitement de l'image (letterbox, BGR→RGB, HWC→CHW et normalisation /255) et effectue uniquement le transfert vers le périphérique et la conversion du type avant de l'envoyer au modèle.

Comme 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 640×640. Pour les remapper vers les coordonnées de l'image d'origine, utilise scale_boxes, qui applique exactement la logique d'arrondi 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 d'un tenseur, flux vidéo et déploiement Triton.

Prédiction 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")
Surcharge de prétraitement nulle

Lorsque tu transmets un torch.Tensor à model.predict(), l'étape de prétraitement de l'image prend ~0.004ms (pratiquement zéro), contre ~1-10ms avec un prétraitement CPU. Le tenseur doit être au format BCHW, en float32 (ou float16) et normalisé à [0, 1]. Ultralytics gère toujours automatiquement le transfert vers le périphérique et la conversion du type.

DALI avec des flux vidéo#

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

Pipeline DALI pour le prétraitement d'un 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 un déploiement en production, combine le prétraitement DALI avec l'inférence TensorRT dans Triton Inference Server à l'aide d'un modèle d'ensemble. Cela élimine entièrement le prétraitement CPU — des octets JPEG bruts entrent, les détections ressortent, 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érialise le pipeline DALI pour le backend DALI de Triton :

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 un moteur TensorRT
from ultralytics import YOLO

model = YOLO("yolo26n.pt")
model.export(
    format="engine", imgsz=640, quantize=16, batch=8, dynamic=True, nms=False
)  # NMS-free (N, 300, 6); TensorRT >= 8.5
# 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"
      }
    }
  ]
}
Fonctionnement du mappage de l'ensemble

L'ensemble relie les modèles par l'intermédiaire de noms de tenseurs virtuels. La valeur "preprocessed_image" de output_map à l'étape DALI correspond à la valeur "preprocessed_image" de input_map à l'étape TensorRT. Ce sont des noms arbitraires qui relient la sortie d'une étape à l'entrée de l'étape suivante — ils ne doivent correspondre aux noms de tenseurs internes d'aucun modèle.

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

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

Ultralytics intègre une prise en charge native de Triton qui gère automatiquement le prétraitement et le post-traitement. Cependant, elle ne fonctionnera pas avec l'ensemble DALI, car YOLO() envoie un tenseur float32 prétraité alors que l'ensemble attend des octets JPEG bruts. Utilise directement tritonclient pour les ensembles DALI, et l'intégration native pour les déploiements standard sans DALI.

Envoyer des images à 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 for the nms=False export)
detections = detections[0]  # First image
detections = detections[detections[:, 4] > 0.25]  # Confidence threshold
print(f"Detected {len(detections)} objects")
Regrouper des images JPEG

Lorsque tu envoies un lot d'images JPEG à Triton, complète tous les tableaux d'octets encodés jusqu'à obtenir la même longueur (le nombre maximal d'octets du lot). Triton exige des formes homogènes pour les lots du 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 chargeRemarques
DétectionPrétraitement letterbox standard
Segmentation d'instancesMême prétraitement que pour la détection
Segmentation sémantiqueMême prétraitement des images que pour la détection
ClassificationUtilise les transformations de torchvision (recadrage centré), et non le letterbox
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

Limites#

  • Linux uniquement : DALI ne prend pas en charge Windows ni macOS
  • GPU NVIDIA requis : aucun mode de secours uniquement sur CPU
  • Pipeline statique : la structure du pipeline est définie lors de sa construction et ne peut pas changer dynamiquement
  • fn.pad est limité à droite/en bas : utilise 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 mode rect auto=True, qui produit des sorties de taille variable (par exemple, 384×640), n'est pas pris en charge. Note que, même si TensorRT prend 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 consommation mémoire élevée. Utilise le groupe d'instances par défaut pour le modèle DALI

FAQ#

  • Le bénéfice dépend de ton pipeline. Lorsque l'inférence sur GPU est déjà rapide avec TensorRT, le prétraitement sur CPU, qui prend 2 à 10 ms, peut devenir le coût dominant. 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), de grandes tailles de batch et des systèmes disposant de peu de cœurs CPU par GPU.

  • Oui. Utilise DALIGenericIterator pour obtenir des sorties torch.Tensor prétraitées, puis transmets-les à model.predict(). Toutefois, le gain de performances est maximal avec les modèles TensorRT, dont l'inférence est déjà très rapide et pour lesquels le prétraitement sur CPU devient le goulot d'étranglement.

  • fn.pad ajoute du 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, conformément au comportement LetterBox(center=True) d'Ultralytics.

  • Presque identiques. Définis antialias=False dans fn.resize pour correspondre à cv2.INTER_LINEAR d'OpenCV. De légères différences en virgule flottante (< 0.001) peuvent apparaître en raison des calculs effectués sur GPU plutôt que sur CPU, mais elles n'ont aucun impact mesurable sur la précision de la détection.

  • CV-CUDA est une autre bibliothèque NVIDIA destinée au traitement de la vision accéléré par GPU. Elle fournit 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 simplifie le letterbox centré. Choisis DALI pour les workflows fondés sur des pipelines (en particulier avec Triton) et CV-CUDA pour un contrôle précis au niveau des opérateurs dans du code d'inférence personnalisé.

Commentaires