Meet YOLO26: next-gen vision AI.

Link to this sectionGPU-beschleunigte Vorverarbeitung mit NVIDIA DALI#

Link to this sectionEinführung#

Beim Bereitstellen von Ultralytics YOLO-Modellen in der Produktion wird die Vorverarbeitung oft zum Flaschenhals. Während TensorRT die Modell-Inferenz in nur wenigen Millisekunden ausführen kann, kann die CPU-basierte Vorverarbeitung (Größenänderung, Auffüllen, Normalisierung) insbesondere bei hohen Auflösungen 2-10 ms pro Bild in Anspruch nehmen. NVIDIA DALI (Data Loading Library) löst dies, indem die gesamte Vorverarbeitungspipeline auf die GPU verlagert wird.

Dieser Leitfaden führt dich durch den Aufbau von DALI-Pipelines, die die Ultralytics YOLO-Vorverarbeitung exakt nachbilden, sie mit model.predict() integrieren, Videostreams verarbeiten und End-to-End mit dem Triton Inference Server bereitstellen.

Für wen ist dieser Leitfaden gedacht?

Dieser Leitfaden richtet sich an Ingenieure, die YOLO-Modelle in Produktionsumgebungen bereitstellen, in denen die CPU-Vorverarbeitung ein messbarer Flaschenhals ist – typischerweise bei TensorRT-Bereitstellungen auf NVIDIA GPUs, hochdurchsatzfähigen Video-Pipelines oder Triton Inference Server-Setups. Wenn du Standard-Inferenz mit model.predict() ausführst und keinen Flaschenhals bei der Vorverarbeitung hast, funktioniert die Standard-CPU-Pipeline gut.

Kurzzusammenfassung
  • DALI-Pipeline aufbauen? Verwende fn.resize(mode="not_larger") + fn.crop(out_of_bounds_policy="pad") + fn.crop_mirror_normalize, um die Letterbox-Vorverarbeitung von YOLO auf der GPU zu replizieren.
  • Integration mit Ultralytics? Übergebe die DALI-Ausgabe als torch.Tensor an model.predict() – Ultralytics überspringt die Bildvorverarbeitung dann automatisch.
  • Bereitstellung mit Triton? Verwende das DALI-Backend mit einem TensorRT-Ensemble für eine CPU-freie Vorverarbeitung.

Link to this sectionWarum DALI für die YOLO-Vorverarbeitung nutzen?#

In einer typischen YOLO-Inferenz-Pipeline laufen die Vorverarbeitungsschritte auf der CPU:

  1. Dekodieren des Bildes (JPEG/PNG)
  2. Größenänderung unter Beibehaltung des Seitenverhältnisses
  3. Auffüllen (Pad) auf die Zielgröße (Letterbox)
  4. Normalisieren der Pixelwerte von [0, 255] auf [0, 1]
  5. Konvertieren des Layouts von HWC zu CHW

Mit DALI laufen all diese Operationen auf der GPU, wodurch der CPU-Flaschenhals eliminiert wird. Dies ist besonders wertvoll bei:

SzenarioWarum DALI hilft
Schnelle GPU-InferenzTensorRT-Engines mit Sub-Millisekunden-Inferenz machen die CPU-Vorverarbeitung zum dominierenden Kostenfaktor
Hochauflösende Eingaben1080p- und 4K-Videostreams erfordern aufwendige Größenänderungsoperationen
Große Batch sizesServerseitige Inferenzverarbeitung vieler Bilder parallel
Begrenzte CPU-KerneEdge-Geräte wie NVIDIA Jetson oder dichte GPU-Server mit wenigen CPU-Kernen pro GPU

Link to this sectionVoraussetzungen#

Nur Linux

NVIDIA DALI unterstützt nur Linux. Es ist nicht für Windows oder macOS verfügbar.

Installiere die benötigten Pakete:

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

Anforderungen:

  • NVIDIA GPU (Rechenkapazität 5.0+ / Maxwell oder neuer)
  • CUDA 11.0+, 12.0+ oder 13.0+
  • Python 3.10-3.14
  • Linux-Betriebssystem

Link to this sectionYOLO-Vorverarbeitung verstehen#

Bevor du eine DALI-Pipeline baust, ist es hilfreich zu verstehen, was Ultralytics während der Vorverarbeitung genau macht. Die Schlüsselklasse ist 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)
)

Die vollständige Vorverarbeitungspipeline in ultralytics/engine/predictor.py führt diese Schritte aus:

SchrittOperationCPU-FunktionDALI-Äquivalent
1Letterbox-Größenänderungcv2.resizefn.resize(mode="not_larger")
2Zentriertes Paddingcv2.copyMakeBorderfn.crop(out_of_bounds_policy="pad")
3BGR → RGBim[..., ::-1]fn.decoders.image(output_type=types.RGB)
4HWC → CHW + Normalisierung /255np.transpose + tensor / 255fn.crop_mirror_normalize(std=[255,255,255])

Die Letterbox-Operation bewahrt das Seitenverhältnis durch:

  1. Berechnung des Skalierungsfaktors: r = min(target_h / h, target_w / w)
  2. Größenänderung auf (round(w * r), round(h * r))
  3. Auffüllen des verbleibenden Raums mit Grau (114), um die Zielgröße zu erreichen
  4. Zentrieren des Bildes, sodass das Padding auf beiden Seiten gleich verteilt ist

Link to this sectionDALI-Pipeline für YOLO#

Verwende die untenstehende zentrierte Pipeline als Standardreferenz. Sie entspricht dem Verhalten von Ultralytics LetterBox(center=True), das bei der Standard-YOLO-Inferenz verwendet wird.

Link to this sectionZentrierte Pipeline (Empfohlen, entspricht Ultralytics LetterBox)#

Diese Version repliziert exakt die Standard-Vorverarbeitung von Ultralytics mit zentriertem Padding und entspricht LetterBox(center=True):

DALI-Pipeline mit zentriertem Padding (empfohlen)
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
Wann reicht `fn.pad` aus?

Wenn du keine exakte Parität zu LetterBox(center=True) benötigst, kannst du den Padding-Schritt vereinfachen, indem du fn.pad(...) anstelle von fn.crop(..., out_of_bounds_policy="pad") verwendest. Diese Variante fügt nur an den rechten und unteren Rändern Padding hinzu, was für benutzerdefinierte Bereitstellungspipelines akzeptabel sein kann, aber nicht exakt dem zentrierten Letterbox-Verhalten von Ultralytics entspricht.

Warum `fn.crop` für zentriertes Padding?

Der fn.pad-Operator von DALI fügt nur an den rechten und unteren Rändern Padding hinzu. Um zentriertes Padding zu erhalten (passend zu Ultralytics LetterBox(center=True)), verwende fn.crop mit out_of_bounds_policy="pad". Mit den Standardwerten crop_pos_x=0.5 und crop_pos_y=0.5 wird das Bild automatisch mit symmetrischem Padding zentriert.

Antialiasing-Diskrepanz

DALI's fn.resize aktiviert standardmäßig Antialiasing (antialias=True), während OpenCV's cv2.resize mit INTER_LINEAR kein Antialiasing anwendet. Setze in DALI immer antialias=False, um der CPU-Pipeline zu entsprechen. Das Weglassen führt zu subtilen numerischen Unterschieden, die die Modellgenauigkeit beeinflussen können.

Link to this sectionAusführen der Pipeline#

DALI-Pipeline erstellen und ausführen
# 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 sectionVerwendung von DALI mit Ultralytics Predict#

Du kannst einen vorverarbeiteten PyTorch-Tensor direkt an model.predict() übergeben. Wenn ein torch.Tensor übergeben wird, überspringt Ultralytics die Bildvorverarbeitung (Letterbox, BGR→RGB, HWC→CHW und /255-Normalisierung) und führt vor dem Senden an das Modell nur die Gerätetransferierung und Dtype-Konvertierung durch.

Da Ultralytics in diesem Fall keinen Zugriff auf die ursprünglichen Bildabmessungen hat, werden die Koordinaten der Erkennungsboxen im 640×640 Letterboxed-Raum zurückgegeben. Um diese auf die ursprünglichen Bildkoordinaten zurückzurechnen, verwende scale_boxes, welches die exakte Rundungslogik von LetterBox handhabt:

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

Dies gilt für alle externen Vorverarbeitungspfade – direkte Tensor-Eingabe, Videostreams und Triton-Bereitstellung.

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")
Null Vorverarbeitungs-Overhead

Wenn du einen torch.Tensor an model.predict() übergibst, dauert der Bildvorverarbeitungsschritt ~0,004 ms (im Grunde null) im Vergleich zu ~1-10 ms bei CPU-Vorverarbeitung. Der Tensor muss im BCHW-Format vorliegen, float32 (oder float16) sein und auf [0, 1] normalisiert sein. Ultralytics übernimmt weiterhin automatisch den Gerätetransfer und die Dtype-Konvertierung.

Link to this sectionDALI mit Videostreams#

Für Echtzeit-Videoverarbeitung verwende fn.external_source, um Frames aus einer beliebigen Quelle einzuspeisen – OpenCV, GStreamer oder benutzerdefinierte Capture-Bibliotheken:

DALI-Pipeline für die Videostream-Vorverarbeitung
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 mit DALI#

Für die Produktion kombiniere die DALI-Vorverarbeitung mit der TensorRT-Inferenz im Triton Inference Server unter Verwendung eines Ensemble-Modells. Dies eliminiert die CPU-Vorverarbeitung vollständig – rohe JPEG-Bytes gehen hinein, Erkennungen kommen heraus, wobei alles auf der GPU verarbeitet wird.

Link to this sectionStruktur des Modell-Repositorys#

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 sectionSchritt 1: Erstelle die DALI-Pipeline#

Serialisiere die DALI-Pipeline für das Triton DALI-Backend:

Serialisiere DALI-Pipeline für 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 sectionSchritt 2: Exportiere YOLO nach TensorRT#

Exportiere das YOLO-Modell in eine TensorRT-Engine
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 sectionSchritt 3: Konfiguriere 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"
      }
    }
  ]
}
So funktioniert das Ensemble-Mapping

Das Ensemble verbindet Modelle über virtuelle Tensor-Namen. Der output_map-Wert "preprocessed_image" im DALI-Schritt entspricht dem input_map-Wert "preprocessed_image" im TensorRT-Schritt. Dies sind beliebige Namen, die die Ausgabe eines Schritts mit der Eingabe des nächsten verknüpfen – sie müssen nicht mit den internen Tensor-Namen eines Modells übereinstimmen.

Link to this sectionSchritt 4: Inferenzanfragen senden#

!!! info "Warum tritonclient anstelle von 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.
Bilder an das Triton-Ensemble senden
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")
Batch-Verarbeitung von JPEG-Bildern

Wenn du einen Stapel (Batch) von JPEG-Bildern an Triton sendest, fülle alle kodierten Byte-Arrays auf die gleiche Länge auf (die maximale Byte-Anzahl im Batch). Triton erfordert homogene Batch-Formen für den Eingabetensor.

Link to this sectionUnterstützte Aufgaben#

Die DALI-Vorverarbeitung funktioniert mit allen YOLO-Aufgaben, die die standardmäßige LetterBox-Pipeline verwenden:

AufgabeUnterstütztHinweise
DetektionStandardmäßige Letterbox-Vorverarbeitung
InstanzsegmentierungGleiche Vorverarbeitung wie bei der Objekterkennung
Semantische SegmentierungGleiche Bildvorverarbeitung wie bei der Objekterkennung
Pose-SchätzungGleiche Vorverarbeitung wie bei der Objekterkennung
Orientierte Objekterkennung (OBB)Gleiche Vorverarbeitung wie bei der Objekterkennung
KlassifizierungVerwendet torchvision-Transformationen (center crop), kein Letterbox

Link to this sectionEinschränkungen#

  • Nur Linux: DALI unterstützt kein Windows oder macOS
  • NVIDIA GPU erforderlich: Kein reiner CPU-Fallback verfügbar
  • Statische Pipeline: Die Pipelinestruktur wird zur Build-Zeit definiert und kann nicht dynamisch geändert werden
  • fn.pad nur rechts/unten: Verwende fn.crop mit out_of_bounds_policy="pad" für zentriertes Padding
  • Kein Rect-Modus: DALI-Pipelines erzeugen Ausgaben mit fester Größe (z. B. 640×640). Der auto=True-Rect-Modus, der Ausgaben mit variabler Größe (z. B. 384×640) erzeugt, wird nicht unterstützt. Beachte, dass TensorRT zwar dynamische Eingabeformen unterstützt, eine DALI-Pipeline mit fester Größe jedoch optimal mit einer Engine mit fester Größe für maximalen Durchsatz harmoniert.
  • Speicher bei mehreren Instanzen: Die Verwendung von instance_group mit count > 1 in Triton kann zu einem hohen Speicherverbrauch führen. Verwende die Standard-Instanzgruppe für das DALI-Modell

Link to this sectionFAQ#

Link to this sectionWie schneidet die DALI-Vorverarbeitung im Vergleich zur CPU-Vorverarbeitung ab?#

Der Vorteil hängt von deiner Pipeline ab. Wenn die GPU-Inferenz mit TensorRT bereits schnell ist, kann die CPU-Vorverarbeitung mit 2-10 ms zum dominierenden Engpass werden. DALI eliminiert diesen Engpass, indem die Vorverarbeitung auf der GPU ausgeführt wird. Die größten Zuwächse zeigen sich bei hochauflösenden Eingaben (1080p, 4K), großen Batch-Größen und Systemen mit begrenzten CPU-Kernen pro GPU.

Link to this sectionKann ich DALI mit PyTorch-Modellen verwenden (nicht nur mit TensorRT)?#

Ja. Verwende DALIGenericIterator, um vorverarbeitete torch.Tensor-Ausgaben zu erhalten, und übergib diese dann an model.predict(). Der Leistungsvorteil ist jedoch bei TensorRT-Modellen am größten, bei denen die Inferenz bereits sehr schnell ist und die CPU-Vorverarbeitung zum Engpass wird.

Link to this sectionWas ist der Unterschied zwischen fn.pad und fn.crop beim Padding?#

fn.pad fügt Padding nur an den rechten und unteren Kanten hinzu. fn.crop mit out_of_bounds_policy="pad" zentriert das Bild und fügt Padding symmetrisch an allen Seiten hinzu, was dem Verhalten von Ultralytics LetterBox(center=True) entspricht.

Link to this sectionErzeugt DALI pixelidentische Ergebnisse wie die CPU-Vorverarbeitung?#

Fast identisch. Setze antialias=False in fn.resize, um cv2.INTER_LINEAR von OpenCV zu entsprechen. Aufgrund der unterschiedlichen Arithmetik von GPU und CPU können geringfügige Unterschiede bei Gleitkommazahlen (< 0.001) auftreten, diese haben jedoch keinen messbaren Einfluss auf die Genauigkeit der Erkennung.

Link to this sectionWas ist mit CV-CUDA als Alternative zu DALI?#

CV-CUDA ist eine weitere NVIDIA-Bibliothek für GPU-beschleunigte Bildverarbeitung. Sie bietet Kontrolle pro Operator (wie OpenCV, aber auf der GPU) anstatt des Pipeline-Ansatzes von DALI. cvcuda.copymakeborder() von CV-CUDA unterstützt explizites Padding pro Seite, was ein zentriertes Letterboxing unkompliziert macht. Wähle DALI für pipelinebasierte Arbeitsabläufe (insbesondere mit Triton) und CV-CUDA für eine fein abgestimmte Kontrolle auf Operatorebene in benutzerdefiniertem Inferenzcode.

Kommentare