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.
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.
- Eine DALI-Pipeline erstellen? Nutze
fn.resize(mode="not_larger")+fn.crop(out_of_bounds_policy="pad")+fn.crop_mirror_normalizeum die Letterbox-Vorverarbeitung von YOLO auf der GPU zu replizieren. - Integration mit Ultralytics? Übergib die DALI-Ausgabe als
torch.Tensorbismodel.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:
- Dekodierung des Bildes (JPEG/PNG)
- Skalierung unter Beibehaltung des Seitenverhältnisses
- Auffüllung (Padding) auf die Zielgröße (Letterbox)
- Normalisierung der Pixelwerte von
[0, 255]bis[0, 1] - 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:
| Szenario | Warum DALI hilft |
|---|---|
| Schnelle GPU-Inferenz | TensorRT Engines mit Inferenz im Sub-Millisekundenbereich machen die CPU-Vorverarbeitung zum dominierenden Kostenfaktor |
| Hochauflösende Eingaben | 1080p und 4K-Videostreams erfordern aufwendige Skalierungsvorgänge |
| Große Batch-Größen | Serverseitige Inferenz, die viele Bilder parallel verarbeitet |
| Begrenzte CPU-Kerne | Edge-Geräte wie NVIDIA Jetson, oder dichte GPU-Server mit wenigen CPU-Kernen pro GPU |
Voraussetzungen
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-cuda120Anforderungen:
- 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:
| Schritt | Operation | CPU-Funktion | DALI-Äquivalent |
|---|---|---|---|
| 1 | Letterbox-Skalierung | cv2.resize | fn.resize(mode="not_larger") |
| 2 | Zentriertes Padding | 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]) |
Die Letterbox-Operation bewahrt das Seitenverhältnis durch:
- Berechnung der Skalierung:
r = min(target_h / h, target_w / w) - Skalierung auf
(round(w * r), round(h * r)) - Auffüllen des verbleibenden Raums mit Grau (
114) bis die Zielgröße erreicht ist - 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):
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 outputWenn 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.
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.
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
# 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.
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 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:
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 outputTriton 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.pbtxtSchritt 1: Erstelle die DALI-Pipeline
Serialisiere die DALI-Pipeline für das Triton DALI-Backend:
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
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.planSchritt 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"
}
}
]
}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.
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")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:
| Aufgabe | Unterstützt | Hinweise |
|---|---|---|
| Detektion | ✅ | Standard-Letterbox-Vorverarbeitung |
| Segmentation | ✅ | Gleiche Vorverarbeitung wie bei der Erkennung |
| Pose-Schätzung | ✅ | Gleiche Vorverarbeitung wie bei der Erkennung |
| Oriented Detection (OBB) | ✅ | Gleiche Vorverarbeitung wie bei der Erkennung |
| Klassifizierung | ❌ | Verwendet 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.padist nur rechts/unten: 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=TrueRect-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_groupmitcount> 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.