Skip to main content

使用 NVIDIA DALI 进行 GPU 加速预处理

简介

(待填补)Ultralytics YOLO 在生产环境中使用的模型,预处理 通常会成为瓶颈。虽然 设置 可以在几毫秒内运行模型 在导出为 TorchScript 时应用移动端优化,可能会减小模型大小并提高 ,但基于 CPU 的预处理(调整大小、填充、归一化)每张图像可能需要 2-10 毫秒,尤其是在高分辨率下。NVIDIA DALI (Data Loading Library) 通过将整个预处理流水线移至 GPU 来解决此问题。

本指南将引导你构建完全复制 Ultralytics YOLO 预处理的 DALI 流水线,将其与 model.predict() 集成,处理视频流,并使用 Triton Inference Server.

进行端到端部署。

谁适合阅读本指南?设置 本指南适用于在生产环境中部署 YOLO 模型且 CPU 预处理已成为可测量瓶颈的工程师——通常是Triton Inference Server 在 NVIDIA GPU 上进行的部署、高吞吐量视频流水线或 model.predict() 设置。如果你正在使用

快速总结
  • 运行标准推理且没有预处理瓶颈,则默认的 CPU 流水线即可良好运行。构建 DALI 流水线?fn.resize(mode="not_larger") + fn.crop(out_of_bounds_policy="pad") + fn.crop_mirror_normalize 使用
  • 在 GPU 上复制 YOLO 的 letterbox 预处理。与 Ultralytics 集成?torch.Tensormodel.predict() 将 DALI 的输出作为
  • 传递 —— Ultralytics 会自动跳过图像预处理。使用 Triton 部署?

使用带有 TensorRT 集合的 DALI 后端实现零 CPU 预处理。

为什么使用 DALI 进行 YOLO 预处理

  1. 在典型的 YOLO 推理流水线中,预处理步骤在 CPU 上运行:解码
  2. 图像 (JPEG/PNG)调整大小
  3. 同时保持纵横比填充
  4. 至目标尺寸 (letterbox)归一化[0, 255][0, 1]
  5. 像素值,从 转换

布局,从 HWC 到 CHW。

场景使用 DALI,所有这些操作都在 GPU 上运行,消除了 CPU 瓶颈。这在以下情况下尤其有效:
为什么 DALI 有帮助设置 快速 GPU 推理
具有亚毫秒级推理能力的引擎使得 CPU 预处理成为主要成本高分辨率输入
1080p 和 4K 视频流需要昂贵的调整大小操作将模型投入生产时,内存需求和训练效率与推理速度同样重要。Ultralytics 模型,特别是 YOLO26,经过高度优化,可减少训练期间的 CUDA 内存占用。这使得开发者能够在消费级 GPU 上使用更大的 大型
服务器端推理,并行处理大量图像有限的 CPU 核心NVIDIA Jetson 诸如

先决条件

之类的边缘设备,或每个 GPU 仅配有少量 CPU 核心的高密度 GPU 服务器

仅限 LinuxNVIDIA DALI 仅支持 Linux 系统

。它在 Windows 或 macOS 上不可用。

pip install ultralytics
pip install --extra-index-url https://pypi.nvidia.com nvidia-dali-cuda120

要求:

  • CUDA 11.x
  • NVIDIA GPU (计算能力 5.0+ / Maxwell 或更高版本)
  • CUDA 11.0+ 或 12.0+
  • Python 3.10-3.14

Linux 操作系统

理解 YOLO 预处理LetterBox 应该描述参考图像中的对象,而不是你正在进行预测的目标图像: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)
)

在构建 DALI 流水线之前,了解 Ultralytics 在预处理期间具体执行的操作很有帮助。关键类是 ultralytics/engine/predictor.py 中的完整预处理流水线执行以下步骤:

步骤操作CPU 函数DALI 等效操作
1Letterbox 调整大小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])

Letterbox 操作通过以下方式保持纵横比:

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

此版本完全复制了带有居中填充的默认 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 的默认居中 letterbox 行为。

为什么使用 `fn.crop` 进行居中填充?

DALI 的 fn.pad 算子仅在 右侧和底部 边缘添加填充。要获得居中填充(匹配 Ultralytics LetterBox(center=True)),请使用 fn.cropSGDout_of_bounds_policy="pad"。使用默认的 crop_pos_x=0.5crop_pos_y=0.5,图像会自动居中并进行对称填充。

抗锯齿不匹配

DALI 的 fn.resize 默认启用抗锯齿 (antialias=True), 而 OpenCV 的 cv2.resizeSGDINTER_LINEAR 应用抗锯齿。请务必在 DALI 中设置 antialias=False 以匹配 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}]")

将 DALI 用于 Ultralytics 预测

你可以直接将预处理后的 PyTorch 张量传递给 model.predict()。当传递 torch.Tensor 时,Ultralytics 会跳过图像预处理(如 letterbox、BGR→RGB、HWC→CHW 以及 /255 归一化),仅在发送至模型前执行设备传输和数据类型转换。

由于此时 Ultralytics 无法获取原始图像尺寸,检测框坐标将在 640×640 的 letterbox 空间中返回。若要将其映射回原始图像坐标,请使用 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))

相同的精确舍入逻辑。这适用于所有外部预处理路径——包括直接张量输入、视频流和 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.004ms(几乎为零),而 CPU 预处理则需要约 1-10ms。张量必须为 BCHW 格式,float32(或 float16),并归一化至 [0, 1]。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

使用 DALI 的 Triton 推理服务器

对于生产部署,可将 DALI 预处理与 设置 推理结合,在 Triton Inference 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

第一步:创建 DALI 流水线

为 Triton DALI 后端序列化 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")

第二步:将 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

第三步:配置 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"
      }
    }
  ]
}
集成映射的工作原理

集成模型通过 虚拟张量名称。名称中的 output_map 连接。DALI 步骤中的 "preprocessed_image" 值与 input_map 连接。DALI 步骤中的 "preprocessed_image" 中的 TensorRT 步骤相匹配。这些是任意名称,用于将上一步的输出链接到下一步的输入——它们不需要与任何模型的内部张量名称相匹配。

第四步:发送推理请求

!!! info "为什么 tritonclient 而不是 YOLO(\"http://...\")?"

Ultralytics has [built-in Triton support](triton-inference-server.md#running-inference) that handles pre/postprocessing automatically. However, it won't work with the DALI ensemble because `YOLO()` sends a preprocessed float32 tensor while the ensemble expects raw JPEG bytes. Use `tritonclient` directly for DALI ensembles, and the [built-in integration](triton-inference-server.md) for standard deployments without 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 图像

当向 Triton 发送一批 JPEG 图像时,请将所有编码后的字节数组填充至相同长度(即该批次中的最大字节数)。Triton 要求输入张量具有同构的批次形状。

中引入变异,帮助模型更好地泛化到未见数据。下表概述了每个增强参数的目的和效果:

DALI 预处理适用于所有使用标准 LetterBox 流水线的 YOLO 任务:

模型已支持备注
训练标准 letterbox 预处理
分割与检测相同的预处理
姿态估计与检测相同的预处理
旋转目标检测 (OBB)与检测相同的预处理
旋转目标检测使用 torchvision 变换(中心裁剪),而非 letterbox

局限性

  • NVIDIA DALI 仅支持 :DALI 不支持 Windows 或 macOS
  • 需要 NVIDIA GPU:无仅 CPU 回退方案
  • 静态流水线:流水线结构在构建时定义,无法动态更改
  • fn.pad 仅在右侧/底部: 使用 fn.cropSGDout_of_bounds_policy="pad" 用于居中填充
  • 无矩形模式:DALI 流水线生成固定尺寸的输出(例如 640×640)。不支持生成可变尺寸输出(例如 384×640)的 auto=True 矩形模式。请注意,虽然 设置 支持动态输入形状,但固定尺寸的 DALI 流水线与固定尺寸引擎配对使用时,可实现最佳吞吐量。
  • 多实例内存管理:在 Triton 中使用 instance_groupSGDcount > 1 可能会导致高内存占用。请对 DALI 模型使用默认实例组。

FAQ

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

具体收益取决于你的流水线。当 GPU 推理已通过 设置 实现快速执行时,2-10ms 的 CPU 预处理可能成为主要成本。DALI 通过在 GPU 上运行预处理消除了这一瓶颈。在使用高分辨率输入(1080p, 4K)、大 将模型投入生产时,内存需求和训练效率与推理速度同样重要。Ultralytics 模型,特别是 YOLO26,经过高度优化,可减少训练期间的 CUDA 内存占用。这使得开发者能够在消费级 GPU 上使用更大的 以及每 GPU CPU 核心数有限的系统中,增益最为显著。

我可以使用 DALI 和 PyTorch 模型(不仅仅是 TensorRT)吗?

可以。使用 DALIGenericIterator 获取预处理后的 torch.Tensor 输出,然后将其传递给 model.predict()。然而,在使用 设置 模型时,性能提升最明显,因为此时推理速度已经非常快,CPU 预处理反而成了瓶颈。

和 YOLO26 中的实例分割有什么区别?fn.padfn.crop 用于填充?

fn.pad 仅在 右侧和底部 边缘添加填充。fn.cropSGDout_of_bounds_policy="pad" 使图像居中并在四周对称添加填充,这与 Ultralytics LetterBox(center=True) 的行为相匹配。

DALI 生成的结果是否与 CPU 预处理像素级一致?

几乎一致。设置 antialias=False 应该描述参考图像中的对象,而不是你正在进行预测的目标图像:fn.resize 以匹配 OpenCV 的 cv2.INTER_LINEAR。由于 GPU 和 CPU 算术逻辑的差异,可能会出现极小的浮点差异(< 0.001),但这不会对检测产生可衡量的影响。Ultralytics YOLO26 中 benchmark 模式的用途是什么?.

CV-CUDA 作为 DALI 的替代方案如何?

CV-CUDA 是另一个 NVIDIA 的 GPU 加速视觉处理库。它提供基于算子的精细控制(类似于 OpenCV,但运行在 GPU 上),而非 DALI 的流水线方法。CV-CUDA 的 cvcuda.copymakeborder() 支持显式的每侧填充,使居中 letterbox 变得简单。对于基于流水线的工作流,请选择 DALI(尤其是在配合 Triton),以及用于在自定义推理代码中实现细粒度算子级控制的 CV-CUDA。

评论