GPU Vorverarbeitung mit NVIDIA
Einführung
Bei der Bereitstellung Ultralytics YOLO -Modelle in der Produktion eingesetzt werden, wird die Vorverarbeitung oft zum Engpass. Während TensorRTdie Modellinferenz in nur wenigen Millisekunden ausführen kann, kann die CPU Vorverarbeitung (Größenanpassung, Auffüllen, Normalisierung) 2–10 ms pro Bild in Anspruch nehmen, insbesondere bei hohen Auflösungen. NVIDIA (Data Loading Library) löst dieses Problem, indem die gesamte Vorverarbeitungs-Pipeline auf die GPU verlagert wird.
Dieser Leitfaden führt Sie durch die Erstellung von DALI-Pipelines, dieYOLO Ultralytics YOLO exakt nachbilden, und deren Integration in model.predict(), die Verarbeitung von Videostreams und die End-to-End-Bereitstellung mit Triton Inference Server.
Für wen ist dieser Leitfaden gedacht?
Dieser Leitfaden richtet sich an Entwickler, die YOLO in Produktionsumgebungen einsetzen, in denen CPU einen messbaren Engpass darstellt – typischerweise TensorRT Bereitstellungen auf NVIDIA , Videopipelines mit hohem Durchsatz oder Triton Inference Server Konfigurationen. Wenn Sie eine Standard-Inferenz mit model.predict() und es gibt keinen Engpass bei der Vorverarbeitung, funktioniert die Standard CPU gut.
Kurzzusammenfassung
- Eine DALI-Pipeline aufbauen? Verwenden Sie
fn.resize(mode="not_larger")+fn.crop(out_of_bounds_policy="pad")+fn.crop_mirror_normalizeum die Letterbox-Vorverarbeitung YOLO auf GPU nachzubilden. - Integration mit Ultralytics? Gib den DALI-Ausgang als
torch.Tensorzumodel.predict()— Ultralytics die Bildvorverarbeitung automatisch. - Sie setzen Triton ein? Nutzen Sie das DALI-Backend mit einem TensorRT fürCPU .
Warum DALI für YOLO verwenden?
In einer typischen YOLO werden die Vorverarbeitungsschritte auf der CPU ausgeführt:
- Bild entschlüsseln (JPEG/PNG)
- Größe ändern unter Beibehaltung des Seitenverhältnisses
- Auf die Zielgröße zuschneiden (Letterbox)
- Normalisieren Pixelwerte aus
[0, 255]zu[0, 1] - Layout von HWC in CHW konvertieren
Bei DALI werden all diese Vorgänge auf der GPU ausgeführt, wodurch der CPU beseitigt wird. Dies ist besonders dann von Vorteil, wenn:
| Szenario | Warum DALI hilft |
|---|---|
| Schnelle GPU | TensorRT Engines mit einer Inferenzzeit von weniger als einer Millisekunde machen CPU zum größten Kostenfaktor |
| Hochauflösende Eingänge | 1080p- und 4K-Videostreams erfordern aufwendige Skalierungsvorgänge |
| Große Losgrößen | Serverseitige Inferenzverarbeitung vieler Bilder parallel |
| Begrenzte Anzahl an CPU | Edge-Geräte wie NVIDIA oder GPU mit hoher GPU und wenigen CPU pro GPU |
Voraussetzungen
Nur Linux
NVIDIA wird nur unter Linux unterstützt. Es ist unter Windows oder macOS nicht verfügbar.
Installieren Sie die erforderlichen Pakete:
pip install ultralytics
pip install --extra-index-url https://pypi.nvidia.com nvidia-dali-cuda120
pip install ultralytics
pip install --extra-index-url https://pypi.nvidia.com nvidia-dali-cuda110
Voraussetzungen:
- GPU Compute Capability 5.0+ / Maxwell oder neuer)
- CUDA .0 oder höher bzw. 12.0 oder höher
- Python .10–3.14
- Linux-Betriebssystem
Einführung in YOLO
Bevor man eine DALI-Pipeline erstellt, ist es hilfreich, genau zu verstehen, was Ultralytics während der Vorverarbeitung Ultralytics . Die wichtigste 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 gesamte Vorverarbeitungs-Pipeline in ultralytics/engine/predictor.py führt folgende Schritte aus:
| Schritt | Betrieb | CPU | DALI-Äquivalent |
|---|---|---|---|
| 1 | Größe des Briefkastens anpassen | cv2.resize | fn.resize(mode="not_larger") |
| 2 | Zentrierte Ausrichtung | 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]) |
Bei der Letterbox-Bearbeitung wird das Seitenverhältnis wie folgt beibehalten:
- Rechenmaßstab:
r = min(target_h / h, target_w / w) - Größe anpassen auf
(round(w * r), round(h * r)) - Den verbleibenden Platz mit Grau auffüllen (
114), um die Zielgröße zu erreichen - Das Bild zentrieren, sodass der Abstand zu beiden Seiten gleichmäßig verteilt ist
DALI-Pipeline für YOLO
Verwenden Sie die unten zentrierte Pipeline als Standardreferenz. Sie entspricht Ultralytics LetterBox(center=True) Verhalten, das bei YOLO Standard YOLO verwendet wird.
Zentrierte Pipeline (empfohlen, entspricht Ultralytics )
Diese Version bildet die Ultralytics mit zentriertem Auffüllung und Anpassung exakt nach LetterBox(center=True):
DALI-Pipeline mit zentriertem Füllmaterial (empfohlen)
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 output
Wann ist fn.pad Reicht das?
Wenn Sie keine genauen Angaben benötigen LetterBox(center=True) Parität: Sie können den Auffüllschritt vereinfachen, indem Sie fn.pad(...) anstelle von fn.crop(..., out_of_bounds_policy="pad"). Diese Variante füllt nur die rechts und unten Ränder, was für benutzerdefinierte Bereitstellungs-Pipelines zwar akzeptabel sein mag, jedoch nicht genau dem standardmäßigen, zentrierten Letterbox-Verhalten Ultralytics entspricht.
Warum fn.crop für zentrierten Abstand?
DALI's fn.pad Der Operator fügt nur Auffüllzeichen zum rechts und unten Ränder. Um einen zentrierten Abstand zu erhalten (entsprechend Ultralytics LetterBox(center=True)), verwende fn.crop mit out_of_bounds_policy="pad". Mit der Standardeinstellung crop_pos_x=0.5 und crop_pos_y=0.5wird das Bild automatisch zentriert und symmetrisch ausgerichtet.
Antialias-Diskrepanz
DALI's fn.resize aktiviert standardmäßig das Antialiasing (antialias=True), während OpenCV cv2.resize mit INTER_LINEAR tut nicht Antialiasing anwenden. Immer aktivieren antialias=False in DALI, um die CPU anzupassen. Wird dies weggelassen, führt dies zu geringfügigen numerischen Abweichungen, die sich auswirken können Modellgenauigkeit.
Die Pipeline ausführen
Eine DALI-Pipeline erstellen und 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}]")
Einsatz von DALI mit Ultralytics
Sie können eine vorverarbeitete PyTorch tensor an model.predict(). Wenn ein torch.Tensor wird übergeben, Ultralytics überspringt die Bildvorverarbeitung (Letterbox, BGR→RGB, HWC→CHW und Normalisierung auf /255) und führt vor der Übermittlung an das Modell lediglich eine Gerätetransformation und einen Datentyp-Cast durch.
Da Ultralytics in diesem Fall Ultralytics Zugriff auf die ursprünglichen Bildabmessungen hat, werden die Koordinaten des Erkennungsrahmens im Letterbox-Format 640×640 zurückgegeben. Um sie wieder auf die ursprünglichen Bildkoordinaten abzubilden, verwenden Sie scale_boxes das die genaue Rundungslogik übernimmt, die 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))
Dies gilt für alle externen Vorverarbeitungspfade – direkte tensor , Videostreams und Triton .
DALI + Ultralytics
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")
Kein Overhead bei der Vorverarbeitung
Wenn du an einem torch.Tensor zu model.predict()… dauert die Bildvorverarbeitung etwa 0,004 ms (praktisch null), verglichen mit etwa 1–10 ms bei CPU . Der tensor im BCHW-Format vorliegen, den Datentyp float32 (oder float16) haben und auf [0, 1]. Ultralytics weiterhin automatisch die Typkonvertierung und die Umwandlung von Datentypen.
DALI mit Videostreams
Verwenden Sie für die Echtzeit-Videoverarbeitung fn.external_source um Bilder aus beliebigen Quellen einzuspielen — OpenCV, GStreamer oder benutzerdefinierte Aufzeichnungsbibliotheken:
DALI-Pipeline zur Vorverarbeitung von Videostreams
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 output
import cv2
import numpy as np
import torch
from ultralytics import YOLO
model = YOLO("yolo26n.engine") # TensorRT model
pipe = yolo_video_pipeline(target_size=640)
pipe.build()
cap = cv2.VideoCapture("video.mp4")
while cap.isOpened():
ret, frame = cap.read()
if not ret:
break
# Feed BGR frame (convert to RGB for DALI)
frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
pipe.feed_input("input", [np.array(frame_rgb)])
(output,) = pipe.run()
# Convert DALI output to torch tensor for inference.
# This is a simple fallback path: using feed_input() with pipe.run() keeps a GPU->CPU->GPU copy.
# For high-throughput deployments, prefer a reader-based pipeline plus DALIGenericIterator to keep data on GPU.
tensor = torch.tensor(output.as_cpu().as_array()).to("cuda")
results = model.predict(tensor, verbose=False)
Triton Server mit DALI
Für den Einsatz in der Produktion kombinieren Sie die DALI-Vorverarbeitung mit TensorRT Inferenz in Triton Server unter Verwendung eines Ensemble-Modells. Dadurch entfällt CPU vollständig – es werden rohe JPEG-Bytes eingegeben, Erkennungsergebnisse ausgegeben, wobei die gesamte Verarbeitung auf der GPU erfolgt.
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.pbtxt
Schritt 1: Erstellen der DALI-Pipeline
Serialisiere die DALI-Pipeline für das Triton -Backend:
DALI-Pipeline für Triton serialisieren
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: YOLO TensorRT exportieren
YOLO in TensorRT exportieren
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.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.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"
}
}
]
}
So funktioniert Ensemble-Mapping
Das Ensemble verbindet Modelle durch tensor virtueller tensor. Der output_map Wert "preprocessed_image" im DALI-Schritt entspricht dem input_map Wert "preprocessed_image" im TensorRT . Dabei handelt es sich um beliebige Namen, die die Ausgabe eines Schritts mit der Eingabe des nächsten Schritts verknüpfen – sie müssen nicht mit tensor internen tensor des Modells übereinstimmen.
Schritt 4: Inferenzanfragen senden
Warum tritonclient anstelle von YOLO(\"http://...\")?
Ultralytics integrierte Triton das die Vor- und Nachbearbeitung automatisch übernimmt. Allerdings funktioniert es nicht mit dem DALI-Ensemble, da YOLO() sendet einen vorverarbeiteten Float32 tensor das Ensemble rohe JPEG-Bytes erwartet. Verwenden Sie tritonclient direkt für DALI-Ensembles und die integrierte Lösung für Standardinstallationen ohne DALI.
Bilder an Triton senden
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")
JPEG-Bilder stapelweise verarbeiten
Wenn Sie einen Stapel von JPEG-Bildern an Triton senden, füllen Sie alle kodierten Byte-Arrays auf die gleiche Länge auf (die maximale Byte-Anzahl im Stapel). Triton für den tensor homogene Stapelformen.
Unterstützte Aufgaben
Die DALI-Vorverarbeitung funktioniert mit allen YOLO , die den Standard verwenden LetterBox Pipeline:
| Aufgabe | Unterstützt | Hinweise |
|---|---|---|
| Erkennung | ✅ | Standardmäßige Vorverarbeitung für Briefkästen |
| Segmentation | ✅ | Gleiche Vorverarbeitung wie bei der Erkennung |
| Pose-Schätzung | ✅ | Gleiche Vorverarbeitung wie bei der Erkennung |
| Orientierte Erkennung (OBB) | ✅ | Gleiche Vorverarbeitung wie bei der Erkennung |
| Klassifizierung | ❌ | Verwendet Torchvision-Transformationen (zentrierter Ausschnitt), kein Letterbox-Format |
Einschränkungen
- Nur Linux: DALI unterstützt weder Windows noch macOS
- GPU : Kein Fallback auf CPU
- Statische Pipeline: Die Struktur der Pipeline wird zum Zeitpunkt der Erstellung festgelegt und kann nicht dynamisch geändert werden
fn.padnur rechts/unten: Verwenden Siefn.cropmitout_of_bounds_policy="pad"für zentrierten Abstand- Kein Rect-Modus: DALI-Pipelines erzeugen Ausgaben mit fester Größe (z. B. 640×640). Die
auto=TrueDer Rect-Modus, der Ausgabebilder mit variabler Größe (z. B. 384×640) erzeugt, wird nicht unterstützt. Beachten Sie, dass zwar TensorRT unterstützt zwar dynamische Eingabeformate, doch eine DALI-Pipeline mit fester Größe lässt sich am besten mit einer Engine mit fester Größe kombinieren, um einen maximalen Durchsatz zu erzielen - Speicher mit mehreren Instanzen: Verwendung von
instance_groupmitcount> 1 in Triton zu einer hohen Speicherauslastung führen. Verwenden Sie die Standard-Instanzgruppe für das DALI-Modell
FAQ
Wie schneidet die DALI-Vorverarbeitung im Vergleich zur Geschwindigkeit CPU ab?
Der Nutzen hängt von Ihrer Pipeline ab. Wenn GPU bereits schnell ist mit TensorRTschnell ist, kann CPU mit 2–10 ms zum größten Kostenfaktor werden. DALI beseitigt diesen Engpass, indem die Vorverarbeitung auf der GPU ausgeführt wird. Die größten Vorteile zeigen sich bei hochauflösenden Eingaben (1080p, 4K), großen Batch-Größen und Systemen mit einer begrenzten Anzahl von CPU pro GPU.
Kann ich DALI mit PyTorch verwenden (nicht nur mit TensorRT)?
Ja. Verwenden Sie DALIGenericIterator zur Vorverarbeitung torch.Tensor Ausgaben, dann übergebe sie an model.predict(). Der Leistungsvorteil ist jedoch am größten bei TensorRT Modelle, bei denen die Inferenz bereits sehr schnell ist und CPU zum Engpass wird.
Was ist der Unterschied zwischen fn.pad und fn.crop als Füllmaterial?
fn.pad fügt nur an den rechts und unten Kanten. fn.crop mit out_of_bounds_policy="pad" zentriert das Bild und fügt symmetrisch auf allen Seiten einen Abstand hinzu, entsprechend den Vorgaben von Ultralytics LetterBox(center=True) Verhalten.
Liefert DALI Ergebnisse, die pixelgenau mit CPU übereinstimmen?
Fast identisch. Set antialias=False in fn.resize um OpenCV anzupassen cv2.INTER_LINEAR. Minor floating-point differences (< 0.001) may occur due to GPU vs CPU arithmetic, but these have no measurable impact on detection Genauigkeit.
Wie wäre es mitCUDA Alternative zu DALI?
CUDA ist NVIDIA weitere NVIDIA für GPU Bildverarbeitung. Sie ermöglicht die Steuerung auf Operatorenebene (wie OpenCV (sondern auf GPU) statt des Pipeline-Ansatzes von DALI.CUDA cvcuda.copymakeborder() unterstützt explizites Seitenrand-Padding, wodurch die zentrierte Letterbox-Darstellung ganz einfach wird. Wählen Sie DALI für pipelinebasierte Workflows (insbesondere bei Triton) sowieCUDA eine feinkörnige Steuerung auf Operatorenebene in benutzerdefiniertem Inferenzcode.