GPU-beschleunigte Vorverarbeitung mit NVIDIA DALI#
Beim Bereitstellen von Ultralytics YOLO-Modellen in Produktionsumgebungen wird die Vorverarbeitung häufig zum Engpass. Während TensorRT die Inferenz eines Modells in nur wenigen Millisekunden ausführen kann, kann die CPU-basierte Vorverarbeitung (Skalieren, Auffüllen, Normalisieren) insbesondere bei hohen Auflösungen 2–10 ms pro Bild beanspruchen. NVIDIA DALI (Bibliothek zum Laden von Daten) 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 in model.predict() integrierst, Videostreams verarbeitest und eine durchgängige Bereitstellung mit dem Triton Inference Server umsetzt.
Diese Anleitung richtet sich an Ingenieure, die YOLO-Modelle in Produktionsumgebungen bereitstellen, in denen die CPU-Vorverarbeitung einen messbaren Engpass darstellt – typischerweise bei TensorRT-Bereitstellungen auf NVIDIA-GPUs, Videopipelines mit hohem Durchsatz oder Triton Inference Server-Setups. Wenn du die standardmäßige Inferenz mit model.predict() ausführst und keinen Engpass bei der Vorverarbeitung hast, funktioniert die standardmäßige 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 nachzubilden. - Integrierst du DALI in Ultralytics? Übergib die DALI-Ausgabe als
torch.Tensoranmodel.predict()– Ultralytics überspringt die Bildvorverarbeitung automatisch. - Stellst du mit Triton bereit? Verwende das DALI-Backend mit einem TensorRT-Ensemble, um die CPU-Vorverarbeitung vollständig zu vermeiden.
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] - Konvertieren des Layouts von HWC zu CHW
Mit DALI werden all diese Vorgänge auf der GPU ausgeführt, wodurch der CPU-Engpass entfällt. Das ist besonders wertvoll, wenn:
| Szenario | Warum DALI hilft |
|---|---|
| Schnelle GPU-Inferenz | TensorRT-Engines mit einer Inferenzzeit von unter einer Millisekunde machen die CPU-Vorverarbeitung zum dominierenden 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-Kernanzahl | Edge-Geräte wie NVIDIA Jetson oder dicht bestückte GPU-Server mit wenigen CPU-Kernen pro GPU. |
Voraussetzungen#
NVIDIA DALI unterstützt nur Linux. Es ist unter Windows oder macOS nicht verfügbar.
Installiere die erforderlichen Pakete:
pip install ultralytics
pip install --extra-index-url https://pypi.nvidia.com nvidia-dali-cuda130Voraussetzungen:
- NVIDIA-GPU (Compute Capability 5.0+ / Maxwell oder neuer)
- CUDA 11.0+, 12.0+ oder 13.0+
- Python 3.10–3.14
- Linux-Betriebssystem
Die YOLO-Vorverarbeitung verstehen#
Bevor du eine DALI-Pipeline erstellst, lohnt es sich, genau zu verstehen, was Ultralytics während der Vorverarbeitung ausführt. Die zentrale Klasse 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 die folgenden 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 Bereich mit Grau (
114) auffüllen, um die Zielgröße zu erreichen - Das Bild zentrieren, sodass das Auffüllen gleichmäßig auf beide Seiten verteilt wird
DALI-Pipeline für YOLO#
Die empfohlene DALI-Pipeline bildet das standardmäßige Verhalten von LetterBox(center=True) in Ultralytics nach, das von der standardmäßigen YOLO-Inferenz verwendet wird.
Zentrierte Pipeline (empfohlen, entspricht Ultralytics LetterBox)#
Diese Variante bildet die standardmäßige Ultralytics-Vorverarbeitung mit zentriertem Auffüllen 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.
"""
# 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 Übereinstimmung mit LetterBox(center=True) benötigst, kannst du den Auffüllschritt vereinfachen, indem du fn.pad(...) statt fn.crop(..., out_of_bounds_policy="pad") verwendest. Diese Variante füllt nur am rechten und unteren Rand auf. Das kann für benutzerdefinierte Bereitstellungspipelines akzeptabel sein, entspricht jedoch 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 zentriertes Auffüllen (entsprechend Ultralytics LetterBox(center=True)) verwendest du 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 symmetrischer Auffüllung zentriert.
DALI aktiviert im Operator fn.resize standardmäßig die Kantenglättung (antialias=True), während OpenCVs cv2.resize mit INTER_LINEAR keine Kantenglättung anwendet. Setze in DALI immer antialias=False, damit die CPU-Pipeline übereinstimmt. Wenn du dies weglässt, entstehen subtile numerische Unterschiede, 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 /255-Normalisierung) und führt vor der Übergabe an das Modell nur die Geräteübertragung und die Konvertierung des Datentyps aus.
Da Ultralytics in diesem Fall keinen Zugriff auf die ursprünglichen Bildabmessungen hat, werden die Koordinaten der Erkennungsrahmen im Letterbox-Raum von 640×640 zurückgegeben. Um sie auf die Koordinaten des ursprünglichen Bildes abzubilden, verwende scale_boxes, das die exakte Rundungslogik von LetterBox verwendet:
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-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 null), verglichen mit etwa ~1–10 ms bei CPU-Vorverarbeitung. Der Tensor muss das BCHW-Format haben, als float32 (oder float16) vorliegen und auf [0, 1] normalisiert sein. Ultralytics übernimmt die Geräteübertragung und die Konvertierung des Datentyps weiterhin automatisch.
DALI mit Videostreams#
Für die Echtzeitverarbeitung von Videos verwendest du 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 Bereitstellung in Produktionsumgebungen 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 werden ausgegeben und alles wird 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."""
# 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: YOLO nach TensorRT exportieren#
from ultralytics import YOLO
model = YOLO("yolo26n.pt")
model.export(
format="engine", imgsz=640, quantize=16, batch=8, dynamic=True, nms=False
) # NMS-free (N, 300, 6); TensorRT >= 8.5
# Copy the .engine file to model_repository/yolo_trt/1/model.planSchritt 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 TENSOR-Namen. Der Wert output_map "preprocessed_image" im DALI-Schritt entspricht dem Wert input_map "preprocessed_image" im TensorRT-Schritt. Dies sind beliebige Namen, die die Ausgabe eines Schritts mit der Eingabe des nächsten Schritts verknüpfen – sie müssen mit keinen 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 standardmäßige Bereitstellungen 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 Byteanzahl im Stapel). Triton erfordert 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 |
|---|---|---|
| Erkennung | ✅ | Standardmäßige Letterbox-Vorverarbeitung |
| Instanzsegmentierung | ✅ | Dieselbe Vorverarbeitung wie bei der Objekterkennung |
| Semantische Segmentierung | ✅ | Dieselbe Bildvorverarbeitung wie bei der Objekterkennung |
| Klassifizierung | ❌ | Verwendet torchvision-Transformationen (zentriertes Zuschneiden), nicht Letterbox |
| Posenschätzung | ✅ | Dieselbe Vorverarbeitung wie bei der Objekterkennung |
| Orientierte Objekterkennung (OBB) | ✅ | Dieselbe Vorverarbeitung wie bei der Objekterkennung |
Einschränkungen#
- Nur Linux: DALI unterstützt weder Windows noch macOS
- NVIDIA-GPU erforderlich: Kein Fallback nur für CPU
- Statische Pipeline: Die Pipeline-Struktur wird zum Erstellungszeitpunkt definiert und kann nicht dynamisch geändert werden
fn.padgilt nur für rechts und 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=True-Rect-Modus, der Ausgaben variabler Größe (z. B. 384×640) erzeugt, wird nicht unterstützt. Beachte, dass TensorRT zwar dynamische Eingabeformen unterstützt, eine DALI-Pipeline mit fester Größe jedoch für maximalen Durchsatz natürlich mit einer Engine fester Größe kombiniert wird - 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 Vorteil hängt von deiner Pipeline ab. Wenn die GPU-Inferenz mit TensorRT bereits schnell ist, kann die CPU-Vorverarbeitung mit 2–10 ms zum dominierenden Kostenfaktor werden. DALI beseitigt diesen Engpass, indem die Vorverarbeitung auf der GPU ausgeführt wird. Die größten Leistungsgewinne zeigen sich bei Eingaben mit hoher Auflösung (1080p, 4K), großen Batchgrößen und Systemen mit wenigen CPU-Kernen pro GPU.
Ja. Verwende
DALIGenericIterator, um vorverarbeitete Ausgaben vontorch.Tensorzu erhalten, und übergib sie anschließend anmodel.predict(). Der Leistungsvorteil ist jedoch bei TensorRT-Modellen am größten, da deren 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 zusammen mitout_of_bounds_policy="pad"und fügt an allen Seiten symmetrisch Padding hinzu, entsprechend dem Verhalten von UltralyticsLetterBox(center=True).Nahezu identische. Setze
antialias=Falseinfn.resize, um das Verhalten von OpenCVscv2.INTER_LINEARabzugleichen. Aufgrund der Berechnung auf der GPU im Vergleich zur CPU können geringfügige Gleitkommaabweichungen (< 0.001) auftreten, haben jedoch keine messbaren Auswirkungen auf die Genauigkeit der Erkennung.CV-CUDA ist eine weitere NVIDIA-Bibliothek für die GPU-beschleunigte Bildverarbeitung. Sie bietet eine Steuerung auf Operator-Ebene (wie OpenCV, aber auf der GPU) statt des Pipeline-Ansatzes von DALI. Das
cvcuda.copymakeborder()von CV-CUDA unterstützt explizites Padding pro Seite, wodurch zentriertes Letterbox unkompliziert wird. Wähle DALI für pipelinebasierte Arbeitsabläufe (insbesondere mit Triton) und CV-CUDA für eine detaillierte Steuerung auf Operator-Ebene in benutzerdefiniertem Inferenzcode.