YOLO Vision 2026:

GPU-beschleunigte Vorverarbeitung mit NVIDIA DALI#

Bei der Bereitstellung von Ultralytics YOLO-Modellen in der Produktion wird die Vorverarbeitung häufig zum Engpass. Während TensorRT die Modell-Inferenz in nur wenigen Millisekunden ausführen kann, benötigt die CPU-basierte Vorverarbeitung (Größenanpassung, Auffüllung, Normalisierung) 2–10 ms pro Bild, insbesondere bei hohen Auflösungen. NVIDIA DALI (Data Loading Library) löst dieses Problem, indem die gesamte Vorverarbeitungspipeline auf die GPU verlagert wird.

Diese Anleitung führt dich durch die Erstellung von DALI-Pipelines, die die Ultralytics YOLO-Vorverarbeitung exakt replizieren, sie in model.predict() integrieren, Videostreams verarbeiten und mit dem Triton Inference Server von Ende zu Ende bereitstellen.

Für wen ist dieser Leitfaden gedacht?

Diese Anleitung richtet sich an Ingenieure, die YOLO-Modelle in Produktionsumgebungen bereitstellen, in denen die CPU-Vorverarbeitung ein gemessener Engpass ist – typischerweise TensorRT-Bereitstellungen auf NVIDIA-GPUs, Video-Pipelines mit hohem Durchsatz oder Triton Inference Server-Setups. Wenn du Standardinferenz mit model.predict() ausführst und keinen Vorverarbeitungsengpass hast, funktioniert die Standard-CPU-Pipeline gut.

Kurze Zusammenfassung
  • 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 zu replizieren.
  • Integration mit Ultralytics? Gib die DALI-Ausgabe als torch.Tensor an model.predict() weiter – Ultralytics überspringt die Bildvorverarbeitung automatisch.
  • Deployment mit Triton? Verwende das DALI-Backend mit einem TensorRT-Ensemble für eine CPU-freie Vorverarbeitung.

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

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

  1. Dekodieren des Bildes (JPEG/PNG)
  2. Größenänderung unter Beibehaltung des Seitenverhältnisses
  3. Padding auf die Zielgröße (Letterbox)
  4. Normalisiere Pixelwerte von [0, 255] auf [0, 1]
  5. Konvertierung des Layouts von HWC auf CHW

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

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

Voraussetzungen#

Nur Linux

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

Installiere die erforderlichen Pakete:

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

Anforderungen:

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

YOLO-Vorverarbeitung verstehen#

Bevor du eine DALI-Pipeline erstellst, lohnt es sich zu verstehen, was Ultralytics während der Vorverarbeitung genau tut. 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 der Skalierung: r = min(target_h / h, target_w / w)
  2. Größenanpassung 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 gleichmäßig verteilt ist

DALI-Pipeline für YOLO#

Die empfohlene DALI-Pipeline repliziert das Standardverhalten von LetterBox(center=True) von Ultralytics, das von der Standard-YOLO-Inferenz verwendet wird.

Zentrierte Pipeline (Empfohlen, entspricht Ultralytics LetterBox)#

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

DALI-Pipeline mit zentriertem Padding (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 LetterBox(center=True)-Parität 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üllt nur den rechten und unteren Rand auf, was für benutzerdefinierte Bereitstellungspipelines akzeptabel sein kann, aber sie entspricht nicht genau dem Standard-Letterbox-Verhalten von Ultralytics mit Zentrierung.

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

Der DALI-Operator fn.pad fügt Padding nur am rechten und unteren Rand hinzu. Um ein 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

DALIs fn.resize aktiviert Antialiasing standardmäßig (antialias=True), während OpenCs 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 beeinträchtigen können.

Ausführen der Pipeline#

Erstelle und starte eine DALI-Pipeline
# 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 lediglich Gerätetransfers und Datentyp-Umwandlungen durch, bevor es an das Modell gesendet wird.

Da Ultralytics in diesem Fall keinen Zugriff auf die ursprünglichen Bildabmessungen hat, werden die Koordinaten der Bounding-Box im 640×640 Letterboxed-Raum zurückgegeben. Um sie auf die ursprünglichen Bildkoordinaten abzubilden, verwende scale_boxes, das die exakte Rundungslogik von LetterBox verarbeitet:

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-Deployment.

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 Overhead bei der Vorverarbeitung

Wenn du einen torch.Tensor an model.predict() übergibst, dauert der Schritt der Bildvorverarbeitung ~0,004 ms (im Wesentlichen null) im Vergleich zu ~1–10 ms bei der 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 Datentyp-Umwandlung.

DALI mit Videostreams#

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

DALI-Pipeline für die Videostream-Vorverarbeitung
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 Produktionsbereitstellung die DALI-Vorverarbeitung mit der TensorRT-Inferenz im Triton Inference Server unter Verwendung eines Ensemble-Modells. Dadurch entfällt die CPU-Vorverarbeitung vollständig – rohe JPEG-Bytes gehen hinein, Detektionen kommen heraus, wobei alles auf der GPU verarbeitet wird.

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: Erstellen der DALI-Pipeline#

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: Exportieren von YOLO nach TensorRT#

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)
# Copy the .engine file to model_repository/yolo_trt/1/model.plan

Schritt 3: Konfigurieren von 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"
      }
    }
  ]
}
Wie das Ensemble-Mapping funktioniert

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

Schritt 4: Inferenzanfragen senden#

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

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

Sende Bilder an das Triton-Ensemble
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 eine Charge von JPEG-Bildern an Triton sendest, fülle alle kodierten Byte-Arrays auf die gleiche Länge auf (die maximale Byte-Anzahl in der Charge). Triton erfordert homogene Batch-Formen für den Eingabetensor.

Unterstützte Aufgaben#

Die DALI-Vorverarbeitung funktioniert mit allen YOLO-Aufgaben, die die Standard-LetterBox-Pipeline verwenden:

AufgabeUnterstütztHinweise
DetektionStandard-Letterbox-Vorverarbeitung
InstanzsegmentierungGleiche Vorverarbeitung wie bei der Detektion
Semantic SegmentationGleiche Bildvorverarbeitung wie bei der Detektion
ClassificationVerwendet torchvision-Transformationen (Center Crop), nicht Letterbox
Pose EstimationGleiche Vorverarbeitung wie bei der Detektion
Orientierte Detektion (OBB)Gleiche Vorverarbeitung wie bei der Detektion

Einschränkungen#

  • Nur Linux: DALI unterstützt kein Windows oder macOS
  • NVIDIA GPU erforderlich: Kein CPU-Fallback vorhanden
  • Statische Pipeline: Die Pipelinenstruktur wird zur Erstellungszeit definiert und kann nicht dynamisch geändert werden
  • fn.pad ist nur für rechts/unten: Verwende fn.crop mit out_of_bounds_policy="pad" für zentriertes Padding
  • Kein Rect-Modus: DALI-Pipelines erzeugen Ausgaben fester Größe (z. B. 640×640). Der auto=True-Rect-Modus, der Ausgaben variabler Größe erzeugt (z. B. 384×640), wird nicht unterstützt. Beachte, dass TensorRT zwar dynamische Eingabeformen unterstützt, eine DALI-Pipeline mit fester Größe jedoch natürlich zu einer Engine mit fester Größe für maximalen Durchsatz passt
  • 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 Nutzen hängt von deiner Pipeline ab. Wenn die GPU-Inferenz mit TensorRT bereits schnell ist, kann die CPU-Vorverarbeitung bei 2–10 ms zum dominierenden Kostenfaktor werden. DALI beseitigt 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.

  • Ja. Verwende DALIGenericIterator, um vorverarbeitete torch.Tensor-Ausgaben zu erhalten, und gib sie dann an model.predict() weiter. 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.

  • fn.pad fügt Padding nur am rechten und unteren Rand hinzu. fn.crop mit out_of_bounds_policy="pad" zentriert das Bild und fügt Padding symmetrisch an allen Seiten hinzu, entsprechend dem Ultralytics LetterBox(center=True)-Verhalten.

  • Nahezu identisch. Setze antialias=False in fn.resize, um dem OpenCV-Wert cv2.INTER_LINEAR zu entsprechen. Geringfügige Gleitkommadifferenzen (< 0,001) können aufgrund von GPU- im Vergleich zu CPU-Arithmetik auftreten, haben jedoch keine messbaren Auswirkungen auf die Detektions-Genauigkeit.

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

Kommentare