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.
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.
- 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.Tensoranmodel.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:
- Dekodieren des Bildes (JPEG/PNG)
- Größenänderung unter Beibehaltung des Seitenverhältnisses
- Padding auf die Zielgröße (Letterbox)
- Normalisiere Pixelwerte von
[0, 255]auf[0, 1] - 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:
| Szenario | Warum DALI hilft |
|---|---|
| Schnelle GPU-Inferenz | TensorRT-Engines mit Submillisekunden-Inferenz machen die CPU-Vorverarbeitung zum dominierenden Kostenfaktor |
| Hochauflösende Inputs | 1080p- und 4K-Videostreams erfordern aufwendige Größenänderungsoperationen |
| Große Batch-Größen | Server-seitige Inferenzverarbeitung vieler Bilder parallel |
| 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 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-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, 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:
| Schritt | Operation | CPU-Funktion | DALI-Äquivalent |
|---|---|---|---|
| 1 | Letterbox-Größenänderung | 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) - Größenanpassung auf
(round(w * r), round(h * r)) - Auffüllen des verbleibenden Raums mit Grau (
114), um die Zielgröße zu erreichen - 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):
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 outputWenn 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.
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.
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#
# 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.
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 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:
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 outputTriton 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.pbtxtSchritt 1: Erstellen der DALI-Pipeline#
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."""
# 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#
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.planSchritt 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"
}
}
]
}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#
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.
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 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:
| Aufgabe | Unterstützt | Hinweise |
|---|---|---|
| Detektion | ✅ | Standard-Letterbox-Vorverarbeitung |
| Instanzsegmentierung | ✅ | Gleiche Vorverarbeitung wie bei der Detektion |
| Semantic Segmentation | ✅ | Gleiche Bildvorverarbeitung wie bei der Detektion |
| Classification | ❌ | Verwendet torchvision-Transformationen (Center Crop), nicht Letterbox |
| Pose Estimation | ✅ | Gleiche 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.padist nur für rechts/unten: Verwendefn.cropmitout_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_groupmitcount> 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 vorverarbeitetetorch.Tensor-Ausgaben zu erhalten, und gib sie dann anmodel.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.padfügt Padding nur am rechten und unteren Rand hinzu.fn.cropmitout_of_bounds_policy="pad"zentriert das Bild und fügt Padding symmetrisch an allen Seiten hinzu, entsprechend dem UltralyticsLetterBox(center=True)-Verhalten.Nahezu identisch. Setze
antialias=Falseinfn.resize, um dem OpenCV-Wertcv2.INTER_LINEARzu 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.