跳转至内容

使用NVIDIA 进行GPU预处理

简介

部署时 Ultralytics YOLO 模型时,预处理往往成为瓶颈。虽然 TensorRT 虽能在几毫秒内完成模型推理,CPU处理(调整尺寸、填充、归一化)每张图像可能耗时2-10毫秒,尤其在高分辨率下更为显著。NVIDIA (数据加载库)通过将整个前处理管道迁移GPU,从而解决了这一问题。

本指南将引导您构建能够完全复现Ultralytics YOLO 的 DALI 管道,并将其与 model.predict()、处理视频流,并使用 Triton 推理服务器.

本指南适用于哪些人群?

本指南适用于在生产环境中部署YOLO 工程师,在这些环境中,CPU 通常是已知的瓶颈—— TensorRT 在NVIDIA 上的部署、高吞吐量视频处理管道,或 Triton 推理服务器 配置。如果您正在运行标准推理,则 model.predict() 而且如果不存在预处理瓶颈,默认的CPU 运行良好。

快速摘要

  • 正在构建 DALI 管道吗? 使用 fn.resize(mode="not_larger") + fn.crop(out_of_bounds_policy="pad") + fn.crop_mirror_normalize 在GPU 上复现YOLO信箱预处理。
  • 与Ultralytics 集成? 将 DALI 输出作为 torch.Tensormodel.predict() —Ultralytics 会自动Ultralytics 图像预处理步骤。
  • 正在使用Triton 进行部署?请结合 DALI 后端与TensorRT ,实现CPU 。

为什么在YOLO 使用 DALI

在典型的YOLO 管道中,预处理步骤在CPU 上运行:

  1. 解码图片(JPEG/PNG)
  2. 调整大小并保持宽高比
  3. 调整至目标尺寸(宽屏)
  4. 归一化 像素值来自 [0, 255][0, 1]
  5. 将布局从 HWC转换为 CHW

借助 DALI,所有这些操作都在GPU 上运行,从而消除了CPU 。这在以下情况下尤为重要:

场景DALI 为何能提供帮助
快速GPUTensorRT 具有亚毫秒级推理能力的引擎使得CPU 成为主要开销
高分辨率输入1080p 和 4K 视频流需要进行耗费资源的尺寸调整操作
大批量服务器端并行处理大量图像
CPU 有限诸如NVIDIA 之类的边缘设备,或是每块GPU仅配备少量CPU 的高密度GPU

准备工作

仅限 Linux

NVIDIA 支持Linux 系统。Windows 或 macOS 系统上无法使用该功能。

安装所需的软件包:

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

要求:

  • NVIDIA GPU 计算能力 5.0+ / Maxwell 或更新版本)
  • CUDA .0 及以上版本或 12.0 及以上版本
  • Python .10–3.14
  • Linux 操作系统

了解YOLO

在构建 DALI 管道之前,了解Ultralytics 在预处理阶段的具体Ultralytics 会有所帮助。关键类是 LetterBoxultralytics/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)
)

完整的预处理流程在 ultralytics/engine/predictor.py 执行以下步骤:

步骤操作CPUDALI等效
1调整信箱大小cv2.resizefn.resize(mode="not_larger")
2居中填充cv2.copyMakeBorderfn.crop(out_of_bounds_policy="pad")
3BGR → RGBim[..., ::-1]fn.decoders.image(output_type=types.RGB)
4HWC → CHW + 归一化 / 255np.transpose + tensor / 255fn.crop_mirror_normalize(std=[255,255,255])

“信箱”操作通过以下方式保持宽高比:

  1. 计算范围: r = min(target_h / h, target_w / w)
  2. 调整大小为 (round(w * r), round(h * r))
  3. 用灰色填充剩余空间(114) 以达到目标大小
  4. 将图片居中,使两侧的填充空间均匀分布

适用于YOLO的 DALI 管道

请将下方的居中管道作为默认参考。它与Ultralytics匹配 LetterBox(center=True) 行为,这也是标准YOLO 所采用的方法。

此版本完全复现了Ultralytics 的默认Ultralytics ,包括居中填充,并匹配 LetterBox(center=True):

带居中填充的 DALI 管道(推荐)

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

什么时候 fn.pad 够了吗?

如果你不需要精确的 LetterBox(center=True) 奇偶校验,你可以通过使用 fn.pad(...) 代替 fn.crop(..., out_of_bounds_policy="pad"). 该变体仅对 右侧和底部 边缘,这对于自定义部署管道来说是可以接受的,但它无法完全匹配Ultralytics默认居中黑边框行为。

为什么 fn.crop 用于居中填充?

DALI的 fn.pad 该运算符仅向 右侧和底部 边缘。要获得居中填充(与Ultralytics一致) LetterBox(center=True)),使用 fn.crop 使用 out_of_bounds_policy="pad". 使用默认设置 crop_pos_x=0.5crop_pos_y=0.5, 图片会自动居中显示,并带有对称的内边距。

抗锯齿不匹配

DALI的 fn.resize 默认启用抗锯齿(antialias=True),而OpenCV cv2.resize 使用 INTER_LINEAR 是否 应用抗锯齿。请始终设置 antialias=False 在 DALI 中以匹配CPU 。省略此步骤会导致细微的数值差异,从而可能影响 模型准确度.

运行管道

构建并运行 DALI 管道

# 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}]")

在Ultralytics 中使用 DALI

您可以传递一个预处理过的 PyTorch tensor 映射到 model.predict(). 当一个 torch.Tensor 通过后,Ultralytics 跳过图像预处理 (信箱模式、BGR→RGB、HWC→CHW 以及 /255 归一化),并在将其发送至模型之前仅执行设备转换和数据类型转换。

由于Ultralytics 在此情况下Ultralytics 获取原始图像的尺寸,因此检测框的坐标是在 640×640 的黑边区域内返回的。若要将其映射回原始图像的坐标,请使用 scale_boxes 该函数负责处理 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))

这适用于所有外部预处理路径——直接tensor 、视频流以及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")

零预处理开销

当你经过一个 torch.Tensormodel.predict(),图像预处理步骤耗时约0.004毫秒(几乎为零),而使用CPU 则需约1至10毫秒。tensor 采用BCHW格式,为float32(或float16)类型,并归一化为 [0, 1].Ultralytics 仍Ultralytics 自动处理设备转换和数据类型转换。

支持视频流的DALI

对于实时视频处理,请使用 fn.external_source 从任何来源输入帧 — OpenCV、GStreamer 或自定义捕获库:

用于视频流预处理的DALI管道

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)

支持 DALI 的Triton 服务器

在生产环境中部署时,请将 DALI 预处理与 TensorRT 推理,在Triton Server中采用集成模型。这完全消除了CPU ——输入原始 JPEG 字节,输出检测结果,所有处理均在GPU 上完成。

模型存储库结构

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

步骤 1:创建 DALI 管道

将Triton 后端的 DALI 管道序列化:

为Triton序列化 DALI 管道

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")

步骤 2:将YOLO 导出YOLO TensorRT

将YOLO 导出到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.plan

步骤 3:配置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.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"
      }
    }
  ]
}

集合映射的工作原理

该集合通过……将模型连接起来 虚拟tensoroutput_map"preprocessed_image" 在 DALI 步骤中与 input_map"preprocessed_image" 在TensorRT 。这些是用于将一个步骤的输出与下一个步骤的输入关联起来的任意名称——它们无需与任何模型的内部tensor 相匹配。

第 4 步:发送推理请求

为什么 tritonclient 代替 YOLO(\"http://...\")?

Ultralytics 内置Triton 该功能可自动处理预处理和后处理。然而,它无法与 DALI 套件配合使用,因为 YOLO() 发送了一个经过预处理的 float32tensor 集合模型期望接收原始 JPEG 字节。请使用 tritonclient 直接用于 DALI 组合,以及 内置集成 适用于不带DALI的标准部署。

将图片发送至Triton

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 图片

将一批 JPEG 图像发送至Triton 时,请将所有编码字节数组补齐至相同长度(即该批次中的最大字节数)。Triton 输入tensor批次形状保持一致。

支持的任务

YOLO ALI预处理适用于所有使用标准 LetterBox 管道:

任务支持备注
检测标准信箱预处理
分割与检测相同的预处理
姿势估计与检测相同的预处理
定向检测(旋转框检测)与检测相同的预处理
分类使用火炬视觉变换(居中裁剪),而非黑边格式

局限性

  • 仅限 Linux:DALI 不支持 Windows 或 macOS
  • GPU NVIDIA GPU :不支持CPU 的备用方案
  • 静态管道:管道结构在构建时定义,无法动态更改
  • fn.pad 仅限右侧/底部:使用 fn.crop 使用 out_of_bounds_policy="pad" 用于居中填充
  • 无矩形模式: DALI 管道生成固定尺寸的输出(例如 640×640)。该 auto=True 不支持生成可变尺寸输出(例如 384×640)的 rect 模式。请注意,虽然 TensorRT 虽然支持动态输入形状,但固定大小的 DALI 管道与固定大小的引擎搭配使用,可实现最大吞吐量
  • 具有多个实例的内存: 使用 instance_group 使用 count >Triton 中的 1Triton 导致内存占用过高。请使用 DALI 模型的默认实例组

常见问题

DALI预处理与CPU 的速度相比如何?

具体收益取决于您的处理流程。当GPU TensorRT已能实现快速运行时,2-10毫秒CPU 可能成为主要开销。DALI通过在GPU上执行预处理来消除这一瓶颈。在高分辨率输入(1080p、4K)、大批量处理以及GPU CPU 有限的系统中,性能提升最为显著。

我可以在PyTorch 使用 DALI 吗(而不仅仅是TensorRT)?

是的。使用 DALIGenericIterator 进行预处理 torch.Tensor 输出结果,然后将其传递给 model.predict(). 然而,性能提升最大的是 TensorRT 在某些模型中,推理速度已经非常快,而CPU 则成为了瓶颈。

两者有什么区别? fn.padfn.crop 是为了填充吗?

fn.pad 仅对 右侧和底部 边缘。 fn.crop 使用 out_of_bounds_policy="pad" 将图像居中,并在四周对称地添加内边距,与Ultralytics保持一致 LetterBox(center=True) 行为。

DALI 生成的结果与CPU 像素完全一致吗?

几乎一模一样。套装 antialias=Falsefn.resize 以匹配OpenCV 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 准确性.

将CUDA DALI 的替代方案如何?

CUDA 是另一款用于GPU视觉处理的NVIDIA 。它提供了针对每个运算符的控制(例如 OpenCV (而是在GPU 上)而非 DALI 的流水线方法。CUDA cvcuda.copymakeborder() 支持明确的每边填充,使居中宽银幕效果的实现变得简单。若采用基于管道的工作流程(尤其是与 Triton),以及CUDA 在自定义推理代码中实现细粒度运算符级控制CUDA 。



📅 创建于 0 天前 ✏️ 更新于 0 天前
raimbekovm

评论