Link to this section使用 NVIDIA DALI 进行 GPU 加速预处理#
Link to this section简介#
在生产环境中部署 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 部署? 使用带有 TensorRT 集成模型的 DALI 后端,实现零 CPU 预处理。
Link to this section为什么要为 YOLO 预处理使用 DALI#
在典型的 YOLO 推理流水线中,预处理步骤在 CPU 上运行:
- 解码 图像 (JPEG/PNG)
- 缩放 同时保持纵横比
- 填充 至目标尺寸 (letterbox)
- 归一化 像素值,从
[0, 255]到[0, 1] - 转换 布局,从 HWC 到 CHW
使用 DALI,所有这些操作都在 GPU 上运行,消除了 CPU 瓶颈。这在以下情况下尤其有效:
| 场景 | 为什么 DALI 有帮助 |
|---|---|
| 快速 GPU 推理 | 具有亚毫秒级推理能力的 TensorRT 引擎使得 CPU 预处理成为主要成本 |
| 高分辨率输入 | 1080p 和 4K 视频流需要耗费大量的缩放操作 |
| 大 批量大小 | 服务器端推理并行处理多张图像 |
| 有限的 CPU 核心 | 如 NVIDIA Jetson 等边缘设备,或每个 GPU 配备少量 CPU 核心的高密度 GPU 服务器 |
Link to this section前提条件#
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 操作系统
Link to this section理解 YOLO 预处理#
Before building a DALI pipeline, it helps to understand exactly what Ultralytics does during preprocessing. The key class is 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)
)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) 填充剩余空间以达到目标尺寸 - 将图像居中,以便填充在两侧均匀分布
Link to this sectionYOLO 的 DALI 流水线#
使用下方的居中流水线作为默认参考。它匹配 Ultralytics 的 LetterBox(center=True) 行为,这也是标准 YOLO 推理所使用的。
Link to this section居中流水线(推荐,匹配 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's fn.pad operator only adds padding to the right and bottom edges. To get centered padding (matching Ultralytics LetterBox(center=True)), use fn.crop with out_of_bounds_policy="pad". With the default crop_pos_x=0.5 and crop_pos_y=0.5, the image is automatically centered with symmetric padding.
DALI 的 fn.resize 默认启用抗锯齿 (antialias=True),而 OpenCV 的 cv2.resize 配合 INTER_LINEAR 不应用抗锯齿。请始终在 DALI 中设置 antialias=False 以匹配 CPU 流水线。省略此设置会导致细微的数值差异,从而可能影响 模型准确率。
Link to this section运行流水线#
# 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}]")Link to this section在 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 部署。
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.004 毫秒(本质上为零),而 CPU 预处理则需约为 1-10 毫秒。张量必须为 BCHW 格式,float32(或 float16),并归一化至 [0, 1]。Ultralytics 仍会自动处理设备传输和数据类型转换。
Link to this sectionDALI 与视频流#
对于实时视频处理,请使用 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 outputLink to this section使用 DALI 的 Triton Inference Server#
For production deployment, combine DALI preprocessing with TensorRT inference in Triton Inference Server using an ensemble model. This eliminates CPU preprocessing entirely — raw JPEG bytes go in, detections come out, with everything processed on the GPU.
Link to this section模型仓库结构#
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.pbtxtLink to this section第 1 步:创建 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")Link to this section第 2 步:将 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.planLink to this section第 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" 相匹配。这些是用于将一个步骤的输出链接到下一个步骤输入的任意名称——它们不需要与任何模型内部的张量名称相匹配。
Link to this section第 4 步:发送推理请求#
!!! 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 要求输入张量具有同质的批次形状。
Link to this section支持的任务#
DALI 预处理适用于所有使用标准 LetterBox 流水线的 YOLO 任务:
| 任务 | 支持 | 备注 |
|---|---|---|
| 检测 | ✅ | 标准 letterbox 预处理 |
| 实例分割 | ✅ | 与检测相同的预处理 |
| 语义分割 | ✅ | 与检测相同的图像预处理 |
| 姿态估计 | ✅ | 与检测相同的预处理 |
| 旋转检测 (OBB) | ✅ | 与检测相同的预处理 |
| 分类 | ❌ | 使用 torchvision 变换(中心裁剪),而非 letterbox |
Link to this section局限性#
- 仅限 Linux:DALI 不支持 Windows 或 macOS
- 需要 NVIDIA GPU:没有仅 CPU 的回退方案
- 静态流水线:流水线结构在构建时定义,无法动态更改
fn.pad仅支持右侧/底部:使用fn.crop并设置out_of_bounds_policy="pad"来实现居中填充- 无 rect 模式:DALI 流水线生成固定大小的输出(例如 640×640)。不支持生成可变大小输出(例如 384×640)的
auto=Truerect 模式。请注意,虽然 TensorRT 支持动态输入形状,但固定大小的 DALI 流水线与固定大小的引擎配合使用可以实现最大吞吐量 - 多实例内存占用:在 Triton 中使用
instance_group且count> 1 可能会导致高内存使用。请对 DALI 模型使用默认实例组
Link to this section常见问题解答#
Link to this sectionDALI 预处理与 CPU 预处理速度相比如何?#
收益取决于你的流水线。当 TensorRT 的 GPU 推理已经很快时,耗时 2-10 毫秒的 CPU 预处理可能会成为主要瓶颈。DALI 通过在 GPU 上运行预处理消除了此瓶颈。对于高分辨率输入(1080p, 4K)、大 batch sizes 以及每 GPU CPU 核心数有限的系统,收益最为显著。
Link to this section我可以在 PyTorch 模型中使用 DALI 吗(不仅是 TensorRT)?#
可以。使用 DALIGenericIterator 获取预处理后的 torch.Tensor 输出,然后将其传递给 model.predict()。然而,对于推理速度极快且 CPU 预处理成为瓶颈的 TensorRT 模型,性能提升最为明显。
Link to this sectionfn.pad 和 fn.crop 在填充方面有什么区别?#
fn.pad adds padding only to the right and bottom edges. fn.crop with out_of_bounds_policy="pad" centers the image and adds padding symmetrically on all sides, matching Ultralytics LetterBox(center=True) behavior.
Link to this sectionDALI 产生的结果与 CPU 预处理的结果像素级完全一致吗?#
Nearly identical. Set antialias=False in fn.resize to match OpenCV's 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 accuracy.
Link to this sectionCV-CUDA 作为 DALI 的替代品怎么样?#
CV-CUDA 是另一种用于 GPU 加速视觉处理的 NVIDIA 库。它提供算子级控制(类似 OpenCV 但在 GPU 上),而不是 DALI 的流水线方法。CV-CUDA 的 cvcuda.copymakeborder() 支持显式的每侧填充,使得居中 letterbox 非常直观。对于基于流水线的工作流(特别是在 Triton 中),请选择 DALI;而对于自定义推理代码中精细的算子级控制,请选择 CV-CUDA。