YOLO Vision 2026:

GPU-beschleunigte Vorverarbeitung mit NVIDIA DALI#

Beim Bereitstellen von Ultralytics YOLO-Modellen in Produktionsumgebungen wird die Vorverarbeitung häufig zum Engpass. Während TensorRT die Inferenz eines Modells in nur wenigen Millisekunden ausführen kann, kann die CPU-basierte Vorverarbeitung (Skalieren, Auffüllen, Normalisieren) insbesondere bei hohen Auflösungen 2–10 ms pro Bild beanspruchen. NVIDIA DALI (Bibliothek zum Laden von Daten) löst dieses Problem, indem die gesamte Vorverarbeitungspipeline auf die GPU verlagert wird.

Diese Anleitung zeigt dir, wie du DALI-Pipelines erstellst, die die Vorverarbeitung von Ultralytics YOLO exakt nachbilden, sie in model.predict() integrierst, Videostreams verarbeitest und eine durchgängige Bereitstellung mit dem Triton Inference Server umsetzt.

Für wen ist diese Anleitung gedacht?

Diese Anleitung richtet sich an Ingenieure, die YOLO-Modelle in Produktionsumgebungen bereitstellen, in denen die CPU-Vorverarbeitung einen messbaren Engpass darstellt – typischerweise bei TensorRT-Bereitstellungen auf NVIDIA-GPUs, Videopipelines mit hohem Durchsatz oder Triton Inference Server-Setups. Wenn du die standardmäßige Inferenz mit model.predict() ausführst und keinen Engpass bei der Vorverarbeitung hast, funktioniert die standardmäßige CPU-Pipeline gut.

Kurzübersicht
  • Erstellst du eine DALI-Pipeline? 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 nachzubilden.
  • Integrierst du DALI in Ultralytics? Übergib die DALI-Ausgabe als torch.Tensor an model.predict() – Ultralytics überspringt die Bildvorverarbeitung automatisch.
  • Stellst du mit Triton bereit? Verwende das DALI-Backend mit einem TensorRT-Ensemble, um die CPU-Vorverarbeitung vollständig zu vermeiden.

Warum DALI für die YOLO-Vorverarbeitung verwenden?#

In einer typischen YOLO-Inferenzpipeline werden die Vorverarbeitungsschritte auf der CPU ausgeführt:

  1. Dekodieren des Bildes (JPEG/PNG)
  2. Skalieren unter Beibehaltung des Seitenverhältnisses
  3. Auffüllen 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 werden all diese Vorgänge auf der GPU ausgeführt, wodurch der CPU-Engpass entfällt. Das ist besonders wertvoll, wenn:

SzenarioWarum DALI hilft
Schnelle GPU-InferenzTensorRT-Engines mit einer Inferenzzeit von unter einer Millisekunde machen die CPU-Vorverarbeitung zum dominierenden Kostenfaktor.
Eingaben mit hoher Auflösung1080p- und 4K-Videostreams erfordern aufwendige Skalierungsvorgänge.
Große BatchgrößenInferenz auf dem Server, bei der viele Bilder parallel verarbeitet werden.
Begrenzte CPU-KernanzahlEdge-Geräte wie NVIDIA Jetson oder dicht bestückte GPU-Server mit wenigen CPU-Kernen pro GPU.

Voraussetzungen#

Nur Linux

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

Installiere die erforderlichen Pakete:

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

Voraussetzungen:

  • NVIDIA-GPU (Compute Capability 5.0+ / Maxwell oder neuer)
  • CUDA 11.0+, 12.0+ oder 13.0+
  • Python 3.10–3.14
  • Linux-Betriebssystem

Die YOLO-Vorverarbeitung verstehen#

Bevor du eine DALI-Pipeline erstellst, lohnt es sich, genau zu verstehen, was Ultralytics während der Vorverarbeitung ausführt. Die zentrale Klasse 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 die folgenden Schritte aus:

SchrittVorgangCPU-FunktionDALI-Entsprechung
1Letterbox-Skalierungcv2.resizefn.resize(mode="not_larger")
2Zentriertes Auffüllencv2.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])

Der Letterbox-Vorgang erhält das Seitenverhältnis durch folgende Schritte:

  1. Skalierung berechnen: r = min(target_h / h, target_w / w)
  2. Auf (round(w * r), round(h * r)) skalieren
  3. Den verbleibenden Bereich mit Grau (114) auffüllen, um die Zielgröße zu erreichen
  4. Das Bild zentrieren, sodass das Auffüllen gleichmäßig auf beide Seiten verteilt wird

DALI-Pipeline für YOLO#

Die empfohlene DALI-Pipeline bildet das standardmäßige Verhalten von LetterBox(center=True) in Ultralytics nach, das von der standardmäßigen YOLO-Inferenz verwendet wird.

Zentrierte Pipeline (empfohlen, entspricht Ultralytics LetterBox)#

Diese Variante bildet die standardmäßige Ultralytics-Vorverarbeitung mit zentriertem Auffüllen exakt nach und entspricht LetterBox(center=True):

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

Wenn du keine exakte Übereinstimmung mit LetterBox(center=True) benötigst, kannst du den Auffüllschritt vereinfachen, indem du fn.pad(...) statt fn.crop(..., out_of_bounds_policy="pad") verwendest. Diese Variante füllt nur am rechten und unteren Rand auf. Das kann für benutzerdefinierte Bereitstellungspipelines akzeptabel sein, entspricht jedoch nicht exakt dem standardmäßigen zentrierten Letterbox-Verhalten von Ultralytics.

Warum `fn.crop` für zentriertes Auffüllen?

Der Operator fn.pad von DALI fügt nur am rechten und unteren Rand Auffüllung hinzu. Für zentriertes Auffüllen (entsprechend Ultralytics LetterBox(center=True)) verwendest du 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 symmetrischer Auffüllung zentriert.

Abweichung bei der Kantenglättung

DALI aktiviert im Operator fn.resize standardmäßig die Kantenglättung (antialias=True), während OpenCVs cv2.resize mit INTER_LINEAR keine Kantenglättung anwendet. Setze in DALI immer antialias=False, damit die CPU-Pipeline übereinstimmt. Wenn du dies weglässt, entstehen subtile numerische Unterschiede, die sich auf die Modellgenauigkeit auswirken können.

Pipeline ausführen#

Eine 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}]")

DALI mit Ultralytics Predict verwenden#

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 der Übergabe an das Modell nur die Geräteübertragung und die Konvertierung des Datentyps aus.

Da Ultralytics in diesem Fall keinen Zugriff auf die ursprünglichen Bildabmessungen hat, werden die Koordinaten der Erkennungsrahmen im Letterbox-Raum von 640×640 zurückgegeben. Um sie auf die Koordinaten des ursprünglichen Bildes abzubilden, verwende scale_boxes, das die exakte Rundungslogik von LetterBox verwendet:

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

Wenn du ein torch.Tensor an model.predict() übergibst, dauert die Bildvorverarbeitung etwa ~0,004 ms (praktisch null), verglichen mit etwa ~1–10 ms bei CPU-Vorverarbeitung. Der Tensor muss das BCHW-Format haben, als float32 (oder float16) vorliegen und auf [0, 1] normalisiert sein. Ultralytics übernimmt die Geräteübertragung und die Konvertierung des Datentyps weiterhin automatisch.

DALI mit Videostreams#

Für die Echtzeitverarbeitung von Videos verwendest du fn.external_source, um Frames aus einer beliebigen Quelle einzuspeisen – OpenCV, GStreamer oder benutzerdefinierte Erfassungsbibliotheken:

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

Kombiniere für die Bereitstellung in Produktionsumgebungen die DALI-Vorverarbeitung mit der TensorRT-Inferenz im Triton Inference Server mithilfe eines Ensemble-Modells. Dadurch entfällt die CPU-Vorverarbeitung vollständig: Rohe JPEG-Bytes werden eingespeist, Erkennungen werden ausgegeben und alles wird auf der GPU verarbeitet.

Struktur 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

Schritt 1: DALI-Pipeline erstellen#

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

DALI-Pipeline für Triton serialisieren
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")

Schritt 2: YOLO nach TensorRT exportieren#

YOLO-Modell in eine TensorRT-Engine exportieren
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

Schritt 3: Triton konfigurieren#

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"
      }
    }
  ]
}
Funktionsweise der Ensemble-Zuordnung

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

Schritt 4: Inferenzanfragen senden#

Warum `tritonclient` statt `YOLO('http://...')`?

Ultralytics bietet eine integrierte Triton-Unterstützung, die die Vor- und Nachverarbeitung automatisch übernimmt. Sie funktioniert jedoch nicht mit dem DALI-Ensemble, da YOLO() einen vorverarbeiteten float32-Tensor sendet, während das Ensemble rohe JPEG-Bytes erwartet. Verwende für DALI-Ensembles direkt tritonclient und für standardmäßige Bereitstellungen ohne DALI die integrierte Integration.

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

Wenn du einen Stapel von JPEG-Bildern an Triton sendest, fülle alle codierten Byte-Arrays auf dieselbe Länge auf (die maximale Byteanzahl im Stapel). Triton erfordert für den Eingabetensor einheitliche Stapelformen.

Unterstützte Aufgaben#

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

AufgabeUnterstütztHinweise
ErkennungStandardmäßige Letterbox-Vorverarbeitung
InstanzsegmentierungDieselbe Vorverarbeitung wie bei der Objekterkennung
Semantische SegmentierungDieselbe Bildvorverarbeitung wie bei der Objekterkennung
KlassifizierungVerwendet torchvision-Transformationen (zentriertes Zuschneiden), nicht Letterbox
PosenschätzungDieselbe Vorverarbeitung wie bei der Objekterkennung
Orientierte Objekterkennung (OBB)Dieselbe Vorverarbeitung wie bei der Objekterkennung

Einschränkungen#

  • Nur Linux: DALI unterstützt weder Windows noch macOS
  • NVIDIA-GPU erforderlich: Kein Fallback nur für CPU
  • Statische Pipeline: Die Pipeline-Struktur wird zum Erstellungszeitpunkt definiert und kann nicht dynamisch geändert werden
  • fn.pad gilt nur für rechts und 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 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 für maximalen Durchsatz natürlich mit einer Engine fester Größe kombiniert wird
  • Speicher bei mehreren Instanzen: Die Verwendung von instance_group mit count > 1 in Triton kann zu hohem Speicherverbrauch führen. Verwende die Standard-Instanzgruppe für das DALI-Modell

FAQ#

  • 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 Kostenfaktor werden. DALI beseitigt diesen Engpass, indem die Vorverarbeitung auf der GPU ausgeführt wird. Die größten Leistungsgewinne zeigen sich bei Eingaben mit hoher Auflösung (1080p, 4K), großen Batchgrößen und Systemen mit wenigen CPU-Kernen pro GPU.

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

  • fn.pad fügt nur an den rechten und unteren Rändern Padding hinzu. fn.crop zentriert das Bild zusammen mit out_of_bounds_policy="pad" und fügt an allen Seiten symmetrisch Padding hinzu, entsprechend dem Verhalten von Ultralytics LetterBox(center=True).

  • Nahezu identische. Setze antialias=False in fn.resize, um das Verhalten von OpenCVs cv2.INTER_LINEAR abzugleichen. Aufgrund der Berechnung auf der GPU im Vergleich zur CPU können geringfügige Gleitkommaabweichungen (< 0.001) auftreten, haben jedoch keine messbaren Auswirkungen auf die Genauigkeit der Erkennung.

  • CV-CUDA ist eine weitere NVIDIA-Bibliothek für die GPU-beschleunigte Bildverarbeitung. Sie bietet eine Steuerung auf Operator-Ebene (wie OpenCV, aber auf der GPU) statt des Pipeline-Ansatzes von DALI. Das cvcuda.copymakeborder() von CV-CUDA unterstützt explizites Padding pro Seite, wodurch zentriertes Letterbox unkompliziert wird. Wähle DALI für pipelinebasierte Arbeitsabläufe (insbesondere mit Triton) und CV-CUDA für eine detaillierte Steuerung auf Operator-Ebene in benutzerdefiniertem Inferenzcode.

Kommentare