Zum Inhalt springen

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_normalize um die Letterbox-Vorverarbeitung YOLO auf GPU nachzubilden.
  • Integration mit Ultralytics? Gib den DALI-Ausgang als torch.Tensor zu model.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:

  1. Bild entschlüsseln (JPEG/PNG)
  2. Größe ändern unter Beibehaltung des Seitenverhältnisses
  3. Auf die Zielgröße zuschneiden (Letterbox)
  4. Normalisieren Pixelwerte aus [0, 255] zu [0, 1]
  5. 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:

SzenarioWarum DALI hilft
Schnelle GPUTensorRT Engines mit einer Inferenzzeit von weniger als einer Millisekunde machen CPU zum größten Kostenfaktor
Hochauflösende Eingänge1080p- und 4K-Videostreams erfordern aufwendige Skalierungsvorgänge
Große LosgrößenServerseitige Inferenzverarbeitung vieler Bilder parallel
Begrenzte Anzahl an CPUEdge-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:

SchrittBetriebCPUDALI-Äquivalent
1Größe des Briefkastens anpassencv2.resizefn.resize(mode="not_larger")
2Zentrierte Ausrichtungcv2.copyMakeBorderfn.crop(out_of_bounds_policy="pad")
3BGR → RGBim[..., ::-1]fn.decoders.image(output_type=types.RGB)
4HWC → CHW + Normalisierung / 255np.transpose + tensor / 255fn.crop_mirror_normalize(std=[255,255,255])

Bei der Letterbox-Bearbeitung wird das Seitenverhältnis wie folgt beibehalten:

  1. Rechenmaßstab: r = min(target_h / h, target_w / w)
  2. Größe anpassen auf (round(w * r), round(h * r))
  3. Den verbleibenden Platz mit Grau auffüllen (114), um die Zielgröße zu erreichen
  4. 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.

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:

AufgabeUnterstütztHinweise
ErkennungStandardmäßige Vorverarbeitung für Briefkästen
SegmentationGleiche Vorverarbeitung wie bei der Erkennung
Pose-SchätzungGleiche Vorverarbeitung wie bei der Erkennung
Orientierte Erkennung (OBB)Gleiche Vorverarbeitung wie bei der Erkennung
KlassifizierungVerwendet 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.pad nur rechts/unten: Verwenden Sie fn.crop mit out_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=True Der 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_group mit count > 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.



📅 Erstellt vor 0 Tagen ✏️ Aktualisiert vor 0 Tagen
raimbekovm

Kommentare