GPU-beschleunigte Vorverarbeitung mit NVIDIA DALI#
Beim Bereitstellen von Ultralytics YOLO-Modellen in der Produktion wird die Vorverarbeitung häufig zum Engpass. Während TensorRT die Inferenz eines Modells in wenigen Millisekunden ausführen kann, dauert die CPU-basierte Vorverarbeitung (Größenänderung, Auffüllen, Normalisieren) pro Bild 2–10 ms, 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 zeigt dir, wie du DALI-Pipelines erstellst, die die Vorverarbeitung von Ultralytics YOLO exakt nachbilden, sie mit model.predict() integrierst, Videostreams verarbeitest und die gesamte Lösung mit Triton Inference Server bereitstellst.
Diese Anleitung richtet sich an Entwicklerinnen und 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-Setups. Wenn du standardmäßige Inferenzen mit model.predict() ausführst und die Vorverarbeitung keinen Engpass darstellt, funktioniert die standardmäßige CPU-Pipeline gut.
- Du erstellst 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? Übergib die DALI-Ausgabe als
torch.Tensoranmodel.predict()– Ultralytics überspringt die Bildvorverarbeitung automatisch. - Du stellst die Lösung mit Triton bereit? Verwende das DALI-Backend zusammen 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 laufen die Vorverarbeitungsschritte auf der CPU:
- Dekodieren des Bildes (JPEG/PNG)
- Größe ändern und dabei das Seitenverhältnis beibehalten
- Auf die Zielgröße auffüllen (Letterbox)
- Pixelwerte von
[0, 255]bis[0, 1]normalisieren - Das Datenlayout von HWC in CHW umwandeln
Mit DALI laufen all diese Vorgänge auf der GPU, wodurch der CPU-Engpass entfällt. Das ist besonders nützlich, wenn:
| Szenario | Warum DALI hilfreich ist |
|---|---|
| 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 Größenänderungen |
| Große Batchgrößen | Serverseitige Inferenz, bei der viele Bilder parallel verarbeitet werden |
| Begrenzte Anzahl an CPU-Kernen | Edge-Geräte wie NVIDIA Jetson oder GPU-Server mit hoher Rechendichte 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, # Das Bild zentrieren (auf beiden Seiten gleichmäßig auffüllen)
stride=32, # Ausrichtung am Stride
padding_value=114, # Graue Auffüllung (114, 114, 114)
)Die vollständige Vorverarbeitungspipeline in ultralytics/engine/predictor.py führt die folgenden Schritte aus:
| Schritt | Vorgang | CPU-Funktion | DALI-Entsprechung |
|---|---|---|---|
| 1 | Größe mit Letterbox ändern | cv2.resize | fn.resize(mode="not_larger") |
| 2 | Zentrierte Auffüllung | cv2.copyMakeBorder | fn.crop(out_of_bounds_policy="pad") |
| 3 | BGR → RGB | im[..., ::-1] | fn.decoders.image(output_type=types.RGB) |
| 4 | HWC → CHW + durch 255 normalisieren | 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) - Größe ändern auf
(round(w * r), round(h * r)) - Den verbleibenden Platz mit Grau auffüllen (
114), bis die Zielgröße erreicht ist - Das Bild zentrieren, sodass die Auffüllung auf beiden Seiten gleichmäßig verteilt ist
DALI-Pipeline für YOLO#
Die empfohlene DALI-Pipeline bildet das standardmäßige Verhalten von Ultralytics mit LetterBox(center=True) nach, das bei der standardmäßigen YOLO-Inferenz verwendet wird.
Zentrierte Pipeline (empfohlen, entspricht Ultralytics LetterBox)#
Diese Variante bildet die standardmäßige Vorverarbeitung von Ultralytics mit zentrierter Auffüllung 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)
# Größenänderung bei beibehaltenem Seitenverhältnis
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)
)
# Zentrierte Auffüllung mit fn.crop und out_of_bounds_policy
# Wenn die Zuschnittgröße größer als das Bild 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-Auffüllungswert
)
# Normalisieren und Datenlayout 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 keine exakte Übereinstimmung mit LetterBox(center=True) erforderlich ist, kannst du den Auffüllungsschritt vereinfachen, indem du statt fn.crop(..., out_of_bounds_policy="pad") fn.pad(...) verwendest. Diese Variante füllt nur den rechten und unteren Rand auf. Für benutzerdefinierte Bereitstellungspipelines kann das akzeptabel sein, 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 Auffüllung hinzu. Für eine zentrierte Auffüllung (entsprechend LetterBox(center=True) von Ultralytics) verwendest du fn.crop mit out_of_bounds_policy="pad". Mit den Standardeinstellungen crop_pos_x=0.5 und crop_pos_y=0.5 wird das Bild automatisch zentriert und symmetrisch aufgefüllt.
Der Operator fn.resize von DALI aktiviert standardmäßig Antialiasing (antialias=True), während OpenCVs cv2.resize mit INTER_LINEAR kein Antialiasing anwendet. Setze in DALI immer antialias=False, damit das Ergebnis der CPU-Pipeline entspricht. Ohne diese Einstellung 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. Wenn ein torch.Tensor übergeben wird, überspringt Ultralytics die Bildvorverarbeitung (Letterbox, BGR→RGB, HWC→CHW und Normalisierung durch 255) und führt vor der Übergabe an das Modell nur den Gerätetransfer und die Umwandlung des Datentyps aus.
Da Ultralytics in diesem Fall keinen Zugriff auf die ursprünglichen Bildabmessungen hat, werden die Koordinaten der Erkennungsrahmen im Letterbox-Bereich von 640×640 zurückgegeben. Um sie den Koordinaten des Originalbilds zuzuordnen, verwende scale_boxes. Dabei wird die exakte Rundungslogik von LetterBox angewendet:
from ultralytics.utils.ops import scale_boxes
# boxes: Tensor der Form (N, 4) im xyxy-Format, Koordinaten im Letterbox-Bereich von 640x640
# boxes vom Letterbox-Bereich (640, 640) zurück auf die Originalgröße (orig_h, orig_w) skalieren
boxes = scale_boxes((640, 640), boxes, (orig_h, orig_w))Dies gilt für alle externen Vorverarbeitungswege – 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 ~0.004ms (praktisch null) statt ~1-10ms bei der CPU-Vorverarbeitung. Der Tensor muss das BCHW-Format haben, vom Typ float32 (oder float16) sein und auf [0, 1] normalisiert sein. Ultralytics übernimmt weiterhin automatisch den Gerätetransfer und die Umwandlung des Datentyps.
DALI mit Videostreams#
Für die Echtzeitverarbeitung von Videos verwendest du fn.external_source, um Einzelbilder aus beliebigen Quellen einzuspeisen – etwa aus 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 die Bereitstellung in der Produktion 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 Modellarchivs#
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 DALI-Backend von Triton:
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, kodierte Bildbytes 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 im Modellarchiv 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 Metadatenkopf hinzu. Entferne ihn, damit Triton den rohen 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-Metadatenkopfs
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 über virtuelle Namen für Tensoren. Im DALI-Schritt entspricht der Wert output_map "preprocessed_image" dem Wert input_map "preprocessed_image" 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 Tensorbezeichnungen eines Modells übereinstimmen.
Schritt 4: Inferenzanfragen senden#
Ultralytics bietet 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 tritonclient direkt für DALI-Ensembles und die integrierte Integration für Standardbereitstellungen ohne DALI.
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 einheitliche Stapelformen für den Eingabetensor.
Unterstützte Aufgaben#
Die DALI-Vorverarbeitung funktioniert mit allen YOLO-Aufgaben, die die standardmäßige LetterBox-Pipeline verwenden:
| Aufgabe | Unterstützt | Hinweise |
|---|---|---|
| Erkennung | ✅ | 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 (zentrierter Zuschnitt) statt Letterbox |
| Posenschä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 reine CPU-Ausweichlösung
- Statische Pipeline: Die Pipeline-Struktur wird zur Erstellungszeit 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 Rect-Modus
auto=True, 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 für maximalen Durchsatz gut mit einer Engine fester Größe zusammenpasst. - Speicherbedarf bei mehreren Instanzen: Die Verwendung von
instance_groupmitcount> 1 in Triton kann zu hohem Speicherverbrauch führen. Verwende die Standard-Instanzgruppe für das DALI-Modell.
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 Zugewinne gibt es bei hochauflösenden Eingaben (1080p, 4K), großen Stapelgröß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.cropzentriert das Bild mitout_of_bounds_policy="pad"und fügt symmetrisch an allen Seiten Padding hinzu. Das entspricht dem Verhalten von UltralyticsLetterBox(center=True).Nahezu identisch. Setze
antialias=Falseinfn.resize, damit es dem Wertcv2.INTER_LINEARvon OpenCV entspricht. Aufgrund der GPU- gegenüber der CPU-Arithmetik können geringfügige Gleitkommaabweichungen (< 0.001) auftreten, 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, wodurch sich zentriertes Letterbox einfach umsetzen lässt. Wähle DALI für Pipeline-basierte Arbeitsabläufe (insbesondere mit Triton) und CV-CUDA für die präzise Steuerung einzelner Operatoren in benutzerdefiniertem Inferenzcode.