GPU-beschleunigte Vorverarbeitung mit NVIDIA DALI#
Beim produktiven Einsatz von Ultralytics YOLO-Modellen wird die Vorverarbeitung häufig zum Engpass. TensorRT kann die Modell-Inferenz zwar in nur wenigen Millisekunden ausführen, doch die CPU-basierte Vorverarbeitung (Skalierung, Auffüllen, Normalisierung) kann pro Bild 2–10 ms dauern, insbesondere bei hohen Auflösungen. NVIDIA DALI (Bibliothek zum Laden von Daten) löst dieses Problem, indem die gesamte Vorverarbeitungspipeline auf die GPU verlagert wird.
Diese Anleitung führt dich durch den Aufbau von DALI-Pipelines, die die Vorverarbeitung von Ultralytics YOLO exakt nachbilden, deren Integration mit model.predict(), die Verarbeitung von Videostreams und die durchgängige Bereitstellung mit dem Triton Inference Server.
Diese Anleitung richtet sich an Entwickler, die YOLO-Modelle in Produktionsumgebungen einsetzen, in denen die CPU-Vorverarbeitung nachweislich einen Engpass darstellt – typischerweise bei TensorRT-Bereitstellungen auf NVIDIA-GPUs, Videopipelines mit hohem Durchsatz oder Triton Inference Server-Konfigurationen. Wenn du mit model.predict() eine gewöhnliche Inferenz ausführst und keinen Engpass bei der Vorverarbeitung hast, funktioniert die standardmäßige CPU-Pipeline gut.
- Du baust eine DALI-Pipeline? Verwende
fn.resize(mode="not_larger")+fn.crop(out_of_bounds_policy="pad")+fn.crop_mirror_normalize, um YOLOs Letterbox-Vorverarbeitung auf der GPU nachzubilden. - Du integrierst DALI mit Ultralytics? Übergebe die DALI-Ausgabe als
torch.Tensoranmodel.predict()– Ultralytics überspringt die Bildvorverarbeitung automatisch. - Du stellst mit Triton bereit? Verwende das DALI-Backend mit einem TensorRT-Ensemble, um die Vorverarbeitung vollständig ohne CPU auszuführen.
Warum DALI für die YOLO-Vorverarbeitung verwenden?#
In einer typischen YOLO-Inferenzpipeline werden die Vorverarbeitungsschritte auf der CPU ausgeführt:
- Dekodieren des Bildes (JPEG/PNG)
- Skalieren unter Beibehaltung des Seitenverhältnisses
- Auffüllen auf die Zielgröße (Letterbox)
- Normalisieren der Pixelwerte von
[0, 255]auf[0, 1] - Umwandeln des Layouts von HWC in CHW
Mit DALI laufen all diese Vorgänge auf der GPU, wodurch der CPU-Engpass entfällt. Das ist besonders vorteilhaft, wenn:
| Szenario | Warum DALI hilft |
|---|---|
| Schnelle GPU-Inferenz | TensorRT-Engines mit Inferenzzeiten unter einer Millisekunde machen die CPU-Vorverarbeitung zum größten Kostenfaktor |
| Eingaben mit hoher Auflösung | 1080p- und 4K-Videostreams erfordern aufwendige Skalierungsvorgänge |
| Große Batchgrößen | Inferenz auf dem Server, bei der viele Bilder parallel verarbeitet werden |
| Begrenzte CPU-Kerne | Edge-Geräte wie NVIDIA Jetson oder GPU-Server mit hoher Dichte und wenigen CPU-Kernen pro GPU |
Voraussetzungen#
NVIDIA DALI unterstützt nur Linux. Unter Windows oder macOS ist es nicht verfügbar.
Installiere die erforderlichen Pakete:
pip install ultralytics
pip install --extra-index-url https://pypi.nvidia.com nvidia-dali-cuda130Anforderungen:
- 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, solltest du genau verstehen, was Ultralytics bei der Vorverarbeitung macht. Die zentrale Klasse ist LetterBox in ultralytics/data/augment.py:
from ultralytics.data.augment import LetterBox
letterbox = LetterBox(
new_shape=(640, 640), # Zielgröße
center=True, # Bild zentrieren (auf beiden Seiten gleichmäßig auffüllen)
stride=32, # Ausrichtung am Stride
padding_value=114, # Graues Padding (114, 114, 114)
)Die vollständige Vorverarbeitungspipeline in ultralytics/engine/predictor.py führt diese Schritte aus:
| Schritt | Vorgang | CPU-Funktion | DALI-Entsprechung |
|---|---|---|---|
| 1 | Letterbox-Skalierung | cv2.resize | fn.resize(mode="not_larger") |
| 2 | Zentriertes Auffüllen | cv2.copyMakeBorder | fn.crop(out_of_bounds_policy="pad") |
| 3 | BGR → RGB | im[..., ::-1] | fn.decoders.image(output_type=types.RGB) |
| 4 | HWC → CHW + Normalisierung /255 | np.transpose + tensor / 255 | fn.crop_mirror_normalize(std=[255,255,255]) |
Der Letterbox-Vorgang erhält das Seitenverhältnis durch folgende Schritte:
- Skalierung berechnen:
r = min(target_h / h, target_w / w) - Auf
(round(w * r), round(h * r))skalieren - Den verbleibenden Platz mit Grau auffüllen (
114), um die Zielgröße zu erreichen - Das Bild zentrieren, sodass das Padding gleichmäßig auf beide Seiten verteilt wird
DALI-Pipeline für YOLO#
Die empfohlene DALI-Pipeline bildet das standardmäßige Verhalten von Ultralytics LetterBox(center=True) nach, das bei der gewöhnlichen YOLO-Inferenz verwendet wird.
Zentrierte Pipeline (empfohlen, entspricht Ultralytics LetterBox)#
Diese Variante bildet die standardmäßige Vorverarbeitung von Ultralytics mit zentriertem Padding exakt nach und entspricht LetterBox(center=True):
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.
"""
# Bilder auf der GPU lesen und dekodieren
jpegs, _ = fn.readers.file(file_root=image_dir, random_shuffle=False, name="Reader")
images = fn.decoders.image(jpegs, device="mixed", output_type=types.RGB)
# Skalierung unter Beibehaltung des Seitenverhältnisses
resized = fn.resize(
images,
resize_x=target_size,
resize_y=target_size,
mode="not_larger",
interp_type=types.INTERP_LINEAR,
antialias=False, # Entspricht cv2.INTER_LINEAR (kein Antialiasing)
)
# Zentriertes Auffüllen mit fn.crop und out_of_bounds_policy
# Wenn die Zuschnittgröße größer als die Bildgröße ist, zentriert fn.crop das Bild und füllt es symmetrisch auf
padded = fn.crop(
resized,
crop=(target_size, target_size),
out_of_bounds_policy="pad",
fill_values=114, # YOLO-Paddingwert
)
# Normalisieren und Layout umwandeln
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 outputWenn du keine exakte Übereinstimmung mit LetterBox(center=True) benötigst, kannst du den Auffüllschritt vereinfachen und statt fn.crop(..., out_of_bounds_policy="pad") einfach fn.pad(...) verwenden. Diese Variante füllt nur den rechten und unteren Rand auf. Das kann für benutzerdefinierte Bereitstellungspipelines ausreichen, entspricht aber nicht exakt dem standardmäßigen zentrierten Letterbox-Verhalten von Ultralytics.
Der Operator fn.pad von DALI fügt nur am rechten und unteren Rand Padding hinzu. Um zentriertes Padding zu erhalten, das dem LetterBox(center=True) von Ultralytics entspricht, 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 zentriert und symmetrisch aufgefüllt.
fn.resize von DALI aktiviert standardmäßig Antialiasing (antialias=True), während cv2.resize von OpenCV mit INTER_LINEAR kein Antialiasing anwendet. Setze in DALI immer antialias=False, damit das Ergebnis der CPU-Pipeline entspricht. Wenn du diese Einstellung weglässt, entstehen geringfügige numerische Abweichungen, die sich auf die Modellgenauigkeit auswirken können.
Pipeline 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. Wird ein torch.Tensor übergeben, überspringt Ultralytics die Bildvorverarbeitung (Letterbox, BGR→RGB, HWC→CHW und Normalisierung /255) und führt vor der Übergabe an das Modell nur die Geräteübertragung und die Umwandlung des Datentyps aus.
Da Ultralytics in diesem Fall keinen Zugriff auf die Abmessungen des Originalbildes hat, werden die Koordinaten der Erkennungsrahmen im Letterbox-Bereich von 640×640 zurückgegeben. Um sie auf die Koordinaten des Originalbildes abzubilden, verwende scale_boxes. Dabei kommt dieselbe Rundungslogik wie in LetterBox zum Einsatz:
from ultralytics.utils.ops import scale_boxes
# boxes: Tensor der Form (N, 4) im xyxy-Format, Koordinaten im Letterbox-Bereich 640x640
# Rahmen vom Letterbox-Bereich (640, 640) auf das Originalformat (orig_h, orig_w) zurückskalieren
boxes = scale_boxes((640, 640), boxes, (orig_h, orig_w))Dies gilt für alle externen Vorverarbeitungspfade – direkte Tensor-Eingabe, Videostreams und Triton-Bereitstellung.
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")Wenn du ein torch.Tensor an model.predict() übergibst, dauert die Bildvorverarbeitung etwa ~0.004 ms (praktisch gar nicht), verglichen mit ~1–10 ms bei CPU-basierter Vorverarbeitung. Der Tensor muss das Format BCHW haben, vom Typ float32 (oder float16) und auf [0, 1] normalisiert sein. Ultralytics übernimmt weiterhin automatisch die Geräteübertragung und die Umwandlung des Datentyps.
DALI mit Videostreams#
Für die Echtzeitverarbeitung von Videos kannst du mit fn.external_source Einzelbilder aus beliebigen Quellen einspeisen – etwa von OpenCV, GStreamer oder benutzerdefinierten Erfassungsbibliotheken:
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."""
# Externe Quelle zum Einspeisen von Einzelbildern aus OpenCV, GStreamer usw.
frames = fn.external_source(device="cpu", name="input")
frames = fn.reshape(frames, layout="HWC")
# Auf die GPU übertragen und vorverarbeiten
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 outputTriton Inference Server mit DALI#
Kombiniere für den produktiven Einsatz 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 ausgegeben und alles 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.pbtxtSchritt 1: DALI-Pipeline erstellen#
Serialisiere die DALI-Pipeline für das Triton-DALI-Backend:
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."""
# Eingabe: rohe, codierte Bild-Bytes von 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
# Pipeline ins Modell-Repository serialisieren
pipe = triton_dali_pipeline()
pipe.serialize(filename="model_repository/dali_preprocessing/1/model.dali")Schritt 2: YOLO nach TensorRT exportieren#
from pathlib import Path
from ultralytics import YOLO
model = YOLO("yolo26n.pt")
engine_path = model.export(
format="engine", imgsz=640, quantize=16, batch=8, dynamic=True, nms=False
) # Ohne NMS (N, 300, 6); TensorRT >= 8.5
# Ultralytics fügt .engine-Dateien einen Metadaten-Header hinzu. Entferne ihn, damit Triton den unverarbeiteten TensorRT-Plan laden kann
with open(engine_path, "rb") as f:
meta_len = int.from_bytes(f.read(4), byteorder="little") # Länge des JSON-Metadaten-Headers
f.seek(4 + meta_len)
plan = f.read()
Path("model_repository/yolo_trt/1").mkdir(parents=True, exist_ok=True)
Path("model_repository/yolo_trt/1/model.plan").write_bytes(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"
}
}
]
}Das Ensemble verbindet Modelle mithilfe von virtuellen Tensor-Namen. Der Wert "preprocessed_image" von output_map im DALI-Schritt entspricht dem Wert "preprocessed_image" von input_map im TensorRT-Schritt. Das sind beliebige Namen, die die Ausgabe eines Schritts mit der Eingabe des nächsten Schritts verknüpfen – sie müssen nicht mit den internen Tensor-Namen eines Modells übereinstimmen.
Schritt 4: Inferenzanfragen senden#
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 Standardbereitstellungen ohne DALI die integrierte Integration.
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")Wenn du einen Stapel von JPEG-Bildern an Triton sendest, fülle alle codierten Byte-Arrays auf dieselbe Länge auf (die maximale Byte-Anzahl im Stapel). Triton benötigt für den Eingabetensor einheitliche Stapelformen.
Unterstützte Aufgaben#
Die DALI-Vorverarbeitung funktioniert mit allen YOLO-Aufgaben, die die standardmäßige LetterBox-Pipeline verwenden:
| Aufgabe | Unterstützt | Hinweise |
|---|---|---|
| Detektion | ✅ | Standardmäßige Letterbox-Vorverarbeitung |
| Instanzsegmentierung | ✅ | Dieselbe Vorverarbeitung wie bei der Erkennung |
| Semantische Segmentierung | ✅ | Dieselbe Bildvorverarbeitung wie bei der Erkennung |
| Tiefenschätzung | ✅ | Dieselbe Bildvorverarbeitung wie bei der Erkennung |
| Klassifizierung | ❌ | Verwendet torchvision-Transformationen (zentriertes Zuschneiden) statt Letterbox |
| Pose-Schätzung | ✅ | Dieselbe Vorverarbeitung wie bei der Erkennung |
| Erkennung orientierter Objekte (OBB) | ✅ | Dieselbe Vorverarbeitung wie bei der Erkennung |
Einschränkungen#
- Nur Linux: DALI unterstützt weder Windows noch macOS
- NVIDIA-GPU erforderlich: Keine Ausweichlösung nur mit CPU
- Statische Pipeline: Die Pipeline-Struktur wird beim Erstellen festgelegt und kann nicht dynamisch geändert werden
fn.padfügt Padding nur rechts und unten hinzu: Verwendefn.cropmitout_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: Zwar unterstützt TensorRT dynamische Eingabeformen, aber eine DALI-Pipeline mit fester Größe lässt sich für maximalen Durchsatz am besten mit einer Engine mit fester Größe kombinieren. - Speicherverbrauch bei mehreren Instanzen: Die Verwendung von
instance_groupmitcount> 1 in Triton kann zu einem hohen Speicherverbrauch führen. Verwende für das DALI-Modell die standardmäßige Instanzgruppe.
Häufig gestellte Fragen#
Der Vorteil hängt von deiner Pipeline ab. Wenn die GPU-Inferenz mit TensorRT bereits schnell ist, können 2–10 ms CPU-Vorverarbeitung zum größten Kostenfaktor werden. DALI beseitigt diesen Engpass, indem die Vorverarbeitung auf der GPU ausgeführt wird. Die größten Verbesserungen zeigen sich bei Eingaben mit hoher Auflösung (1080p, 4K), großen Batch-Größen und Systemen mit wenigen CPU-Kernen pro GPU.
Ja. Verwende
DALIGenericIterator, um vorverarbeitete Ausgaben vom Typtorch.Tensorzu erhalten, und übergib sie anschließend anmodel.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.fn.padfügt nur an den rechten und unteren Rändern Padding hinzu.fn.cropmitout_of_bounds_policy="pad"zentriert das Bild und fügt auf allen Seiten symmetrisch Padding hinzu, entsprechend dem Verhalten von UltralyticsLetterBox(center=True).Nahezu identisch. Setze
antialias=Falseinfn.resize, damit es mitcv2.INTER_LINEARvon OpenCV übereinstimmt. Aufgrund der Berechnungen auf der GPU und CPU kann es zu geringfügigen Gleitkommaabweichungen (< 0.001) kommen, die sich jedoch nicht messbar auf die Genauigkeit der Erkennung auswirken.CV-CUDA ist eine weitere NVIDIA-Bibliothek für GPU-beschleunigte Bildverarbeitung. Sie bietet eine Steuerung auf Ebene einzelner Operatoren (wie OpenCV, aber auf der GPU) statt des Pipeline-Ansatzes von DALI.
cvcuda.copymakeborder()von CV-CUDA unterstützt explizites Padding pro Seite und erleichtert so zentriertes Letterbox. Wähle DALI für pipelinebasierte Workflows (insbesondere mit Triton) und CV-CUDA für eine fein abgestufte Steuerung einzelner Operatoren in benutzerdefiniertem Inferenzcode.