使用 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.Tensor到model.predict()将 DALI 的输出作为 - 传递 —— Ultralytics 会自动跳过图像预处理。使用 Triton 部署?
使用带有 TensorRT 集合的 DALI 后端实现零 CPU 预处理。
为什么使用 DALI 进行 YOLO 预处理
- 在典型的 YOLO 推理流水线中,预处理步骤在 CPU 上运行:解码
- 图像 (JPEG/PNG)调整大小
- 同时保持纵横比填充
- 至目标尺寸 (letterbox)归一化
[0, 255]到[0, 1] - 像素值,从 转换
布局,从 HWC 到 CHW。
| 场景 | 使用 DALI,所有这些操作都在 GPU 上运行,消除了 CPU 瓶颈。这在以下情况下尤其有效: |
|---|---|
| 为什么 DALI 有帮助 | 设置 快速 GPU 推理 |
| 具有亚毫秒级推理能力的引擎使得 CPU 预处理成为主要成本 | 高分辨率输入 |
| 1080p 和 4K 视频流需要昂贵的调整大小操作将模型投入生产时,内存需求和训练效率与推理速度同样重要。Ultralytics 模型,特别是 YOLO26,经过高度优化,可减少训练期间的 CUDA 内存占用。这使得开发者能够在消费级 GPU 上使用更大的 | 大型 |
| 服务器端推理,并行处理大量图像 | 有限的 CPU 核心NVIDIA Jetson 诸如 |
先决条件
仅限 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 等效操作 |
|---|---|---|---|
| 1 | Letterbox 调整大小 | cv2.resize | fn.resize(mode="not_larger") |
| 2 | 居中填充 | cv2.copyMakeBorder | fn.crop(out_of_bounds_policy="pad") |
| 3 | BGR → RGB | im[..., ::-1] | fn.decoders.image(output_type=types.RGB) |
| 4 | HWC → CHW + 归一化 /255 | np.transpose + tensor / 255 | fn.crop_mirror_normalize(std=[255,255,255]) |
Letterbox 操作通过以下方式保持纵横比:
- 计算缩放比例:
r = min(target_h / h, target_w / w) - 调整大小为
(round(w * r), round(h * r)) - 用灰色填充剩余空间 (
114) 以达到目标尺寸 - 将图像居中,使填充在两侧均匀分布
用于 YOLO 的 DALI 流水线
使用下方的居中流水线作为默认参考。它匹配 Ultralytics LetterBox(center=True) 行为,这正是标准 YOLO 推理所使用的。
居中流水线(推荐,匹配 Ultralytics LetterBox)
此版本完全复制了带有居中填充的默认 Ultralytics 预处理,匹配 LetterBox(center=True):
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如果你不需要精确的 LetterBox(center=True) 同步,可以通过使用 fn.pad(...) 而不是 fn.crop(..., out_of_bounds_policy="pad") 来简化填充步骤。该变体仅填充 右侧和底部 边缘,这对于自定义部署流水线可能是可以接受的,但它不会完全匹配 Ultralytics 的默认居中 letterbox 行为。
DALI 的 fn.pad 算子仅在 右侧和底部 边缘添加填充。要获得居中填充(匹配 Ultralytics LetterBox(center=True)),请使用 fn.cropSGDout_of_bounds_policy="pad"。使用默认的 crop_pos_x=0.5 和 crop_pos_y=0.5,图像会自动居中并进行对称填充。
DALI 的 fn.resize 默认启用抗锯齿 (antialias=True), 而 OpenCV 的 cv2.resizeSGDINTER_LINEAR 并 不 应用抗锯齿。请务必在 DALI 中设置 antialias=False 以匹配 CPU 流水线。忽略此项会导致细微的数值差异,从而影响 模型精度.
运行流水线
# 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 部署。
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 或自定义捕获库:
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 流水线:
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
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.
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")当向 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.pad 和 fn.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。