Skip to main content

GPU-beschleunigte Vorverarbeitung mit NVIDIA DALI

Einführung

Beim Deployment von Ultralytics YOLO Modelle in der Produktion, Vorverarbeitung wird oft zum Engpass. Während TensorRT Modell-Inferenz in nur wenigen Millisekunden ausführen kann, benötigt die CPU-basierte Vorverarbeitung (Skalierung, Auffüllung, Normalisierung) oft 2–10 ms pro Bild, besonders bei hohen Auflösungen. NVIDIA DALI (Data Loading Library) löst dieses Problem, indem die gesamte Vorverarbeitungs-Pipeline auf die GPU verlagert wird.

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

Für wen ist dieser Leitfaden?

Dieser Leitfaden richtet sich an Entwickler, die YOLO Modelle in Produktionsumgebungen einsetzen, in denen die CPU-Vorverarbeitung ein gemessener Engpass ist — typischerweise TensorRT Deployments auf NVIDIA GPUs, Video-Pipelines mit hohem Durchsatz oder Triton Inference Server Setups. Wenn du Standard-Inferenz mit model.predict() ausführst und keinen Engpass bei der Vorverarbeitung hast, ist die standardmäßige CPU-Pipeline ausreichend.

Kurzzusammenfassung
  • Eine DALI-Pipeline erstellen? Nutze 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? Übergib die DALI-Ausgabe als torch.Tensor bis model.predict() — Ultralytics überspringt die Bildvorverarbeitung dann automatisch.
  • Bereitstellung mit Triton? Nutze das DALI-Backend mit einem TensorRT-Ensemble für eine Vorverarbeitung ohne CPU-Last.

Warum DALI für die YOLO Vorverarbeitung nutzen?

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

  1. Dekodierung des Bildes (JPEG/PNG)
  2. Skalierung unter Beibehaltung des Seitenverhältnisses
  3. Auffüllung (Padding) auf die Zielgröße (Letterbox)
  4. Normalisierung der Pixelwerte von [0, 255] bis [0, 1]
  5. Konvertierung des Layouts von HWC zu 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 Inferenz im Sub-Millisekundenbereich machen die CPU-Vorverarbeitung zum dominierenden Kostenfaktor
Hochauflösende Eingaben1080p und 4K-Videostreams erfordern aufwendige Skalierungsvorgänge
Große Batch-GrößenServerseitige Inferenz, die viele Bilder parallel verarbeitet
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 unter Windows oder macOS verfügbar.

Installiere die erforderlichen Pakete:

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

Anforderungen:

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

Verständnis der YOLO Vorverarbeitung

Bevor du eine DALI-Pipeline erstellst, hilft es 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 Vorverarbeitungs-Pipeline in ultralytics/engine/predictor.py führt diese Schritte durch:

SchrittOperationCPU-FunktionDALI-Äquivalent
1Letterbox-Skalierungcv2.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. Skalierung auf (round(w * r), round(h * r))
  3. Auffüllen des verbleibenden Raums mit Grau (114) bis die Zielgröße erreicht ist
  4. Zentrieren des Bildes, sodass das Padding auf beiden Seiten gleichmäßig verteilt ist

DALI-Pipeline für YOLO

Verwende die unten stehende zentrierte Pipeline als Standardreferenz. Sie entspricht dem Verhalten von Ultralytics LetterBox(center=True), was auch die standardmäßige YOLO Inferenz nutzt.

Zentrierte Pipeline (Empfohlen, entspricht Ultralytics LetterBox)

Diese Version repliziert exakt die standardmäßige Ultralytics-Vorverarbeitung 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 ist `fn.pad` ausreichend?

Wenn du keine exakte LetterBox(center=True) Parität benötigst, kannst du den Padding-Schritt durch die Verwendung von fn.pad(...) statt von fn.crop(..., out_of_bounds_policy="pad") vereinfachen. Diese Variante füllt nur die rechten und unteren Ränder auf, was für benutzerdefinierte Deployment-Pipelines akzeptabel sein kann, aber sie entspricht nicht exakt dem standardmäßigen zentrierten Letterbox-Verhalten von Ultralytics.

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

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

Antialias-Diskrepanz

Der DALI-Operator fn.resize aktiviert standardmäßig Antialiasing (antialias=True), während OpenCVs cv2.resize mit INTER_LINEAR dies nicht anwendet. Setze in DALI immer antialias=False, um es an die CPU-Pipeline anzupassen. Das Weglassen verursacht subtile numerische Unterschiede, die Modellgenauigkeit.

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 nur die Geräteübertragung sowie das Dtype-Casting durch, bevor es das Modell erreicht.

Da Ultralytics in diesem Fall keinen Zugriff auf die ursprünglichen Bilddimensionen hat, werden die Koordinaten der Erkennungsboxen im 640×640 Letterbox-Raum zurückgegeben. Um sie auf die ursprünglichen Bildkoordinaten zurückzuführen, verwende scale_boxes, das die exakte Rundungslogik von 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))

verwendet. 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")
Kein Vorverarbeitungs-Overhead

Wenn du einen torch.Tensor bis model.predict() übergibst, benötigt der Bildvorverarbeitungsschritt ~0,004 ms (im Grunde 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 werden. Ultralytics übernimmt weiterhin automatisch die Geräteübertragung und das Dtype-Casting.

DALI mit Videostreams

Für Echtzeit-Videoverarbeitung nutze fn.external_source, um Frames aus jeder Quelle einzuspeisen — OpenCV, GStreamer oder benutzerdefinierte Capture-Bibliotheken:

DALI-Pipeline für 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

Triton Inference Server mit DALI

Kombiniere für die Produktion das DALI-Vorverarbeitung mit TensorRT Inferenz in 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.

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

Schritt 2: Exportiere YOLO nach TensorRT

Exportiere das YOLO-Modell zur 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

Schritt 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"
      }
    }
  ]
}
Wie die Ensemble-Zuordnung funktioniert

Das Ensemble verbindet Modelle über virtuelle Tensor-Namen. Das output_map Der Wert "preprocessed_image" im DALI-Schritt entspricht dem input_map Der 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 internen Tensor-Namen eines Modells übereinstimmen.

Schritt 4: Sende Inferenzanfragen

!!! info "Warum tritonclient statt 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.
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")
Batching von JPEG-Bildern

Wenn du einen 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 Eingabe-Tensor.

Unterstützte Aufgaben

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

AufgabeUnterstütztHinweise
DetektionStandard-Letterbox-Vorverarbeitung
SegmentationGleiche Vorverarbeitung wie bei der Erkennung
Pose-SchätzungGleiche Vorverarbeitung wie bei der Erkennung
Oriented Detection (OBB)Gleiche Vorverarbeitung wie bei der Erkennung
KlassifizierungVerwendet torchvision-Transformationen (Center Crop), nicht Letterbox

Einschränkungen

  • nur Linux: DALI unterstützt kein Windows oder macOS
  • NVIDIA GPU erforderlich: Kein CPU-Only-Fallback
  • Statische Pipeline: Die Pipeline-Struktur ist zur Build-Zeit definiert und kann nicht dynamisch geändert werden
  • fn.pad ist 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 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 für maximalen Durchsatz gut mit einer Engine mit fester Größe harmoniert
  • Speicher mit 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

Wie schneidet die DALI-Vorverarbeitung im Vergleich zur CPU-Vorverarbeitungsgeschwindigkeit ab?

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

Kann ich DALI mit PyTorch-Modellen verwenden (nicht nur TensorRT)?

Ja. Verwende DALIGenericIterator, um vorverarbeitete torch.Tensor Ausgaben zu erhalten, und leite 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 Flaschenhals wird.

Was ist der Unterschied zwischen fn.pad und fn.crop für Padding?

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

Erzeugt DALI pixelgenaue Ergebnisse im Vergleich zur CPU-Vorverarbeitung?

Nahezu identisch. Setze antialias=False in fn.resize, um es an OpenCVs cv2.INTER_LINEAR anzupassen. Geringfügige Unterschiede bei der Gleitkomma-Arithmetik (< 0,001) können aufgrund der GPU- vs. CPU-Berechnung auftreten, haben aber keine messbaren Auswirkungen auf die Erkennungs-accuracy.

Was ist mit CV-CUDA als Alternative zu DALI?

CV-CUDA ist eine weitere NVIDIA-Bibliothek für GPU-beschleunigte Bildverarbeitung. Sie bietet pro-Operator-Kontrolle (wie OpenCV aber auf der GPU), anstatt den Pipeline-Ansatz von DALI zu verwenden. CV-KUDAs cvcuda.copymakeborder() unterstützt explizites Padding pro Seite, was zentriertes Letterbox direkt macht. Wähle DALI für Pipeline-basierte Workflows (insbesondere mit Triton), und CV-CUDA für eine fein abgestimmte Steuerung auf Operatorebene in benutzerdefiniertem Inference-Code.

Kommentare