YOLO Vision 2026:

使用 NVIDIA DALI 加速 GPU 预处理#

在生产环境中部署 Ultralytics YOLO 模型时,预处理 往往会成为瓶颈。虽然 TensorRT 可以在短短几毫秒内完成模型推理,但基于 CPU 的预处理(调整大小、填充、归一化)每张图像可能需要 2–10 毫秒,尤其是在高分辨率下。NVIDIA DALI(数据加载库)通过将整个预处理流程转移到 GPU 来解决这一问题。

本指南将带你构建能够完全复现 Ultralytics YOLO 预处理的 DALI 流程,将其与 model.predict() 集成,处理视频流,并通过 Triton Inference Server 实现端到端部署。

本指南适合哪些人?

本指南面向在生产环境中部署 YOLO 模型、且 CPU 预处理已成为可测量瓶颈的工程师——通常包括在 NVIDIA GPU 上部署 TensorRT、构建高吞吐量视频流程,或配置 Triton Inference Server。如果你使用 model.predict() 运行标准推理,且不存在预处理瓶颈,默认的 CPU 流程就能很好地工作。

快速摘要
  • 正在构建 DALI 流程? 使用 fn.resize(mode="not_larger") + fn.crop(out_of_bounds_policy="pad") + fn.crop_mirror_normalize,在 GPU 上复现 YOLO 的 letterbox 预处理。
  • 正在与 Ultralytics 集成? 将 DALI 输出作为 torch.Tensor 传递给 model.predict(),Ultralytics 会自动跳过图像预处理。
  • 正在使用 Triton 部署? 将 DALI 后端与 TensorRT 集成模型结合使用,实现零 CPU 预处理。

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

在典型的 YOLO 推理流程中,预处理步骤在 CPU 上运行:

  1. 解码图像(JPEG/PNG)
  2. 调整大小,同时保持宽高比
  3. 填充到目标大小(letterbox)
  4. 归一化像素值,将其从 [0, 255] 转换为 [0, 1]
  5. 转换布局,从 HWC 转换为 CHW

使用 DALI 后,所有这些操作都会在 GPU 上运行,从而消除 CPU 瓶颈。这在以下情况下尤其有价值:

场景DALI 的优势
快速 GPU 推理具有亚毫秒级推理速度的 TensorRT 引擎会使 CPU 预处理成为主要开销
高分辨率输入1080p 和 4K 视频流需要执行开销较高的调整大小操作
较大的批量大小服务器端推理并行处理大量图像
有限的 CPU 核心例如 NVIDIA Jetson 等边缘设备,或每个 GPU 分配的 CPU 核心较少的高密度 GPU 服务器

前置条件#

仅支持 Linux

NVIDIA DALI 仅支持 Linux,Windows 和 macOS 上不可用。

安装所需的软件包:

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

要求:

  • NVIDIA GPU(计算能力 5.0+ / Maxwell 或更新版本)
  • CUDA 11.0+、12.0+ 或 13.0+
  • Python 3.10–3.14
  • Linux 操作系统

了解 YOLO 预处理#

在构建 DALI 流程之前,值得先准确了解 Ultralytics 在预处理期间执行的操作。关键类是 ultralytics/data/augment.py 中的 LetterBox

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 中的完整预处理流程会执行以下步骤:

步骤操作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 + /255 归一化np.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 流程#

推荐的 DALI 流程复现了 Ultralytics 默认的 LetterBox(center=True) 行为,这是标准 YOLO 推理所使用的行为。

居中流程(推荐,与 Ultralytics LetterBox 一致)#

此版本完全复现了 Ultralytics 默认的居中填充预处理,与 LetterBox(center=True) 一致:

带居中填充的 DALI 流程(推荐)
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 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.cropout_of_bounds_policy="pad" 结合使用。在默认的 crop_pos_x=0.5crop_pos_y=0.5 设置下,图像会自动居中,并采用对称填充。

抗锯齿不匹配

DALI 的 fn.resize 默认启用抗锯齿(antialias=True),而 OpenCV 的 cv2.resize 配合 INTER_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}]")

在 Ultralytics Predict 中使用 DALI#

你可以将预处理后的 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.Tensor 传递给 model.predict() 时,图像预处理耗时约为 ~0.004ms(基本为零),而 CPU 预处理约为 ~1–10ms。张量必须采用 BCHW 格式、float32(或 float16),并归一化为 [0, 1]。Ultralytics 仍会自动处理设备转移和数据类型转换。

使用 DALI 处理视频流#

要进行实时视频处理,请使用 fn.external_source 从任意来源输入帧,例如 OpenCV、GStreamer 或自定义采集库:

用于视频流预处理的 DALI 流程
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 output

使用 DALI 的 Triton Inference Server#

在生产环境中部署时,使用集成模型将 DALI 预处理与 Triton Inference Server 中的 TensorRT 推理结合起来。这会彻底消除 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 后端序列化 DALI 流程:

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

第 2 步:将 YOLO 导出为 TensorRT#

将 YOLO 模型导出为 TensorRT 引擎
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.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_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"
      }
    }
  ]
}
集成模型映射的工作原理

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

第 4 步:发送推理请求#

为什么使用 `tritonclient`,而不是 `YOLO('http://...')`?

Ultralytics 内置了 Triton 支持,可以自动处理前处理和后处理。不过,它无法与 DALI 集成模型配合使用,因为 YOLO() 发送的是预处理后的 float32 张量,而集成模型需要原始 JPEG 字节。对于 DALI 集成模型,请直接使用 tritonclient;对于不使用 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 for the nms=False export)
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 预处理
实例分割与检测相同的预处理
语义分割与检测相同的图像预处理
分类使用 torchvision 变换(中心裁剪),而不是 letterbox
姿态估计与检测相同的预处理
定向检测(OBB)与检测相同的预处理

限制#

  • 仅支持 Linux:DALI 不支持 Windows 或 macOS
  • 需要 NVIDIA GPU:不提供仅使用 CPU 的回退方案
  • 静态流水线:流水线结构在构建时定义,无法动态更改
  • fn.pad 仅支持右侧/底部填充:将 fn.cropout_of_bounds_policy="pad" 搭配使用以进行居中填充
  • 不支持 rect 模式:DALI 流水线会生成固定尺寸的输出(例如 640×640)。生成可变尺寸输出(例如 384×640)的 auto=True rect 模式不受支持。请注意,尽管 TensorRT 支持动态输入形状,但固定尺寸的 DALI 流水线与固定尺寸的引擎搭配,可以自然地实现最高吞吐量
  • 多实例时的内存使用:在 Triton 中使用 instance_groupcount > 1 时,可能导致较高的内存使用量。请为 DALI 模型使用默认实例组

常见问题#

  • 具体收益取决于你的流水线。当使用 TensorRT 时 GPU 推理已经很快,CPU 预处理的 2–10 毫秒就可能成为主要开销。DALI 通过在 GPU 上运行预处理来消除这一瓶颈。在高分辨率输入(1080p、4K)、较大的批量大小,以及每个 GPU 可用 CPU 核心数有限的系统中,收益最为显著。

  • 可以。使用 DALIGenericIterator 获取经过预处理的 torch.Tensor 输出,然后将其传递给 model.predict()。不过,对于推理已经非常快、CPU 预处理成为瓶颈的 TensorRT 模型,性能收益最大。

  • fn.pad 仅在右侧和底部边缘添加填充。fn.cropout_of_bounds_policy="pad" 搭配使用时,会将图像居中,并在四周对称添加填充,与 Ultralytics 的 LetterBox(center=True) 行为一致。

  • 几乎一致。在 fn.resize 中设置 antialias=False,即可匹配 OpenCV 的 cv2.INTER_LINEAR。由于 GPU 与 CPU 使用的算术运算不同,可能会出现细微的浮点差异(< 0.001),但这对检测准确率没有可测量的影响。

  • CV-CUDA 是另一个用于 GPU 加速视觉处理的 NVIDIA 库。它提供按算子控制的能力(类似于 OpenCV,但运行在 GPU 上),而不是采用 DALI 的流水线方式。CV-CUDA 的 cvcuda.copymakeborder() 支持显式设置各侧填充,因此可以轻松实现居中 letterbox。对于基于流水线的工作流(尤其是搭配 Triton 时),请选择 DALI;对于自定义推理代码中的细粒度算子级控制,请选择 CV-CUDA。

评论