Tiền xử lý tăng tốc GPU với NVIDIA DALI#
Khi triển khai các model Ultralytics YOLO trong môi trường production, tiền xử lý thường trở thành nút thắt cổ chai. Trong khi TensorRT có thể chạy suy luận model chỉ trong vài mili giây, tiền xử lý dựa trên CPU (resize, pad, normalize) có thể mất 2–10 ms cho mỗi ảnh, đặc biệt ở độ phân giải cao. NVIDIA DALI (Thư viện tải dữ liệu) giải quyết vấn đề này bằng cách chuyển toàn bộ pipeline tiền xử lý sang GPU.
Hướng dẫn này trình bày cách xây dựng các pipeline DALI tái hiện chính xác quá trình tiền xử lý của Ultralytics YOLO, tích hợp chúng với model.predict(), xử lý các luồng video và triển khai end-to-end với Triton Inference Server.
Hướng dẫn này dành cho các kỹ sư triển khai model YOLO trong môi trường production, nơi tiền xử lý trên CPU là một nút thắt cổ chai đã được đo lường — thường là các triển khai TensorRT trên GPU NVIDIA, pipeline video throughput cao hoặc các thiết lập Triton Inference Server. Nếu bạn đang chạy suy luận tiêu chuẩn với model.predict() và không gặp nút thắt ở bước tiền xử lý, pipeline CPU mặc định hoạt động tốt.
- Đang xây dựng pipeline DALI? Sử dụng
fn.resize(mode="not_larger")+fn.crop(out_of_bounds_policy="pad")+fn.crop_mirror_normalizeđể tái hiện tiền xử lý letterbox của YOLO trên GPU. - Đang tích hợp với Ultralytics? Truyền đầu ra DALI dưới dạng
torch.Tensortớimodel.predict()— Ultralytics sẽ tự động bỏ qua bước tiền xử lý ảnh. - Đang triển khai với Triton? Sử dụng DALI backend cùng với TensorRT ensemble để loại bỏ hoàn toàn tiền xử lý trên CPU.
Tại sao nên sử dụng DALI cho tiền xử lý YOLO#
Trong một pipeline suy luận YOLO điển hình, các bước tiền xử lý chạy trên CPU:
- Giải mã ảnh (JPEG/PNG)
- Resize đồng thời duy trì tỷ lệ khung hình
- Pad đến kích thước đích (letterbox)
- Normalize các giá trị pixel từ
[0, 255]đến[0, 1] - Chuyển đổi layout từ HWC sang CHW
Với DALI, tất cả các thao tác này chạy trên GPU, loại bỏ nút thắt cổ chai trên CPU. Điều này đặc biệt có giá trị khi:
| Tình huống | Tại sao DALI hữu ích |
|---|---|
| Suy luận GPU nhanh | Các engine TensorRT có thời gian suy luận dưới một mili giây khiến tiền xử lý trên CPU trở thành chi phí chính |
| Đầu vào độ phân giải cao | Các luồng video 1080p và 4K yêu cầu các thao tác resize tốn nhiều tài nguyên |
| Batch size lớn | Suy luận phía server xử lý nhiều ảnh song song |
| Số lõi CPU hạn chế | Các thiết bị edge như NVIDIA Jetson hoặc các server GPU mật độ cao có ít lõi CPU trên mỗi GPU |
Điều kiện tiên quyết#
NVIDIA DALI chỉ hỗ trợ Linux. DALI không khả dụng trên Windows hoặc macOS.
Cài đặt các package bắt buộc:
pip install ultralytics
pip install --extra-index-url https://pypi.nvidia.com nvidia-dali-cuda130Yêu cầu:
- GPU NVIDIA (compute capability 5.0+ / Maxwell hoặc mới hơn)
- CUDA 11.0+, 12.0+ hoặc 13.0+
- Python 3.10–3.14
- Hệ điều hành Linux
Tìm hiểu về tiền xử lý YOLO#
Trước khi xây dựng pipeline DALI, bạn nên hiểu chính xác Ultralytics thực hiện những gì trong quá trình tiền xử lý. Class chính là LetterBox trong 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)
)Pipeline tiền xử lý đầy đủ trong ultralytics/engine/predictor.py thực hiện các bước sau:
| Bước | Thao tác | Hàm CPU | Tương đương DALI |
|---|---|---|---|
| 1 | Resize letterbox | cv2.resize | fn.resize(mode="not_larger") |
| 2 | Padding căn giữa | cv2.copyMakeBorder | fn.crop(out_of_bounds_policy="pad") |
| 3 | BGR → RGB | im[..., ::-1] | fn.decoders.image(output_type=types.RGB) |
| 4 | HWC → CHW + normalize /255 | np.transpose + tensor / 255 | fn.crop_mirror_normalize(std=[255,255,255]) |
Thao tác letterbox duy trì tỷ lệ khung hình bằng cách:
- Tính scale:
r = min(target_h / h, target_w / w) - Resize thành
(round(w * r), round(h * r)) - Pad phần không gian còn lại bằng màu xám (
114) để đạt kích thước đích - Căn giữa ảnh để phần padding được phân bổ đều ở hai bên
Pipeline DALI cho YOLO#
Pipeline DALI được khuyến nghị tái hiện behavior LetterBox(center=True) mặc định của Ultralytics, vốn được sử dụng trong suy luận YOLO tiêu chuẩn.
Pipeline căn giữa (Khuyến nghị, tương thích với Ultralytics LetterBox)#
Phiên bản này tái hiện chính xác quá trình tiền xử lý mặc định của Ultralytics với padding căn giữa, tương thích với LetterBox(center=True):
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 outputNếu không cần tính tương đương chính xác với LetterBox(center=True), bạn có thể đơn giản hóa bước padding bằng cách sử dụng fn.pad(...) thay cho fn.crop(..., out_of_bounds_policy="pad"). Biến thể này chỉ pad các cạnh bên phải và bên dưới, có thể phù hợp với các pipeline triển khai tùy chỉnh, nhưng sẽ không khớp chính xác với behavior letterbox căn giữa mặc định của Ultralytics.
Operator fn.pad của DALI chỉ thêm padding vào các cạnh bên phải và bên dưới. Để có padding căn giữa (tương thích với LetterBox(center=True) của Ultralytics), hãy sử dụng fn.crop cùng với out_of_bounds_policy="pad". Với crop_pos_x=0.5 và crop_pos_y=0.5 mặc định, ảnh sẽ tự động được căn giữa với padding đối xứng.
fn.resize của DALI bật antialias theo mặc định (antialias=True), trong khi cv2.resize của OpenCV cùng với INTER_LINEAR không áp dụng antialias. Luôn đặt antialias=False trong DALI để khớp với pipeline CPU. Bỏ qua thiết lập này sẽ gây ra các khác biệt số học tinh vi, có thể ảnh hưởng đến độ chính xác của model.
Chạy pipeline#
# 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}]")Sử dụng DALI với Ultralytics Predict#
Bạn có thể truyền trực tiếp tensor PyTorch đã tiền xử lý vào model.predict(). Khi truyền torch.Tensor, Ultralytics sẽ bỏ qua bước tiền xử lý ảnh (letterbox, BGR→RGB, HWC→CHW và normalize /255), đồng thời chỉ thực hiện chuyển thiết bị và ép kiểu dữ liệu trước khi gửi tensor đến model.
Vì Ultralytics không có quyền truy cập vào kích thước ảnh gốc trong trường hợp này, tọa độ của các box phát hiện được trả về trong không gian letterbox 640×640. Để ánh xạ chúng về tọa độ ảnh gốc, hãy sử dụng scale_boxes, API này xử lý logic làm tròn chính xác được LetterBox sử dụng:
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))Điều này áp dụng cho mọi luồng tiền xử lý bên ngoài — đầu vào tensor trực tiếp, luồng video và triển khai 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")Khi truyền torch.Tensor vào model.predict(), bước tiền xử lý ảnh mất ~0.004ms (về cơ bản là bằng 0), so với ~1–10ms khi tiền xử lý trên CPU. Tensor phải ở định dạng BCHW, float32 (hoặc float16) và được normalize về [0, 1]. Ultralytics vẫn tự động xử lý việc chuyển thiết bị và ép kiểu dữ liệu.
DALI với luồng video#
Để xử lý video theo thời gian thực, hãy sử dụng fn.external_source để cung cấp các frame từ bất kỳ nguồn nào — OpenCV, GStreamer hoặc các thư viện capture tùy chỉnh:
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 outputTriton Inference Server với DALI#
Để triển khai production, hãy kết hợp tiền xử lý DALI với suy luận TensorRT trong Triton Inference Server bằng model ensemble. Cách này loại bỏ hoàn toàn tiền xử lý trên CPU — byte JPEG thô được đưa vào và kết quả phát hiện được trả ra, với mọi thao tác đều được xử lý trên GPU.
Cấu trúc model repository#
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.pbtxtBước 1: Tạo pipeline DALI#
Serialize pipeline DALI cho DALI backend của Triton:
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")Bước 2: Export YOLO sang 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.planBước 3: Cấu hình 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"
}
}
]
}Ensemble kết nối các model thông qua tên tensor ảo. Giá trị "preprocessed_image" của output_map trong bước DALI khớp với giá trị "preprocessed_image" của input_map trong bước TensorRT. Đây là các tên tùy ý liên kết đầu ra của một bước với đầu vào của bước tiếp theo — chúng không cần khớp với tên tensor nội bộ của bất kỳ model nào.
Bước 4: Gửi request suy luận#
Ultralytics có hỗ trợ Triton tích hợp sẵn, tự động xử lý tiền xử lý và hậu xử lý. Tuy nhiên, tính năng này sẽ không hoạt động với DALI ensemble vì YOLO() gửi tensor float32 đã tiền xử lý trong khi ensemble yêu cầu byte JPEG thô. Hãy sử dụng trực tiếp tritonclient cho DALI ensemble và tích hợp tích hợp sẵn cho các triển khai tiêu chuẩn không dùng 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 for the nms=False export)
detections = detections[0] # First image
detections = detections[detections[:, 4] > 0.25] # Confidence threshold
print(f"Detected {len(detections)} objects")Khi gửi một batch ảnh JPEG đến Triton, hãy pad tất cả các mảng byte đã encode đến cùng một độ dài (số byte lớn nhất trong batch). Triton yêu cầu các shape batch đồng nhất cho tensor đầu vào.
Các Task được hỗ trợ#
Tiền xử lý DALI hoạt động với tất cả các task YOLO sử dụng pipeline LetterBox tiêu chuẩn:
| Task | Được hỗ trợ | Ghi chú |
|---|---|---|
| Detection | ✅ | Tiền xử lý letterbox tiêu chuẩn |
| Phân đoạn instance | ✅ | Tiền xử lý giống như đối với detection |
| Semantic Segmentation | ✅ | Tiền xử lý hình ảnh giống như đối với detection |
| Classification | ❌ | Sử dụng các phép biến đổi của torchvision (center crop), không phải letterbox |
| Ước tính tư thế | ✅ | Tiền xử lý giống như đối với detection |
| Phát hiện đối tượng có hướng (OBB) | ✅ | Tiền xử lý giống như đối với detection |
Hạn chế#
- Chỉ Linux: DALI không hỗ trợ Windows hoặc macOS
- Yêu cầu GPU NVIDIA: Không có phương án dự phòng chỉ dùng CPU
- Pipeline tĩnh: Cấu trúc pipeline được xác định tại thời điểm build và không thể thay đổi động
fn.padchỉ dành cho bên phải/dưới: Sử dụngfn.cropvớiout_of_bounds_policy="pad"để padding căn giữa- Không có chế độ rect: Các pipeline DALI tạo ra output có kích thước cố định (ví dụ: 640×640). Chế độ rect
auto=Truetạo output có kích thước thay đổi (ví dụ: 384×640) không được hỗ trợ. Lưu ý rằng mặc dù TensorRT hỗ trợ shape input động, pipeline DALI có kích thước cố định kết hợp tự nhiên với engine có kích thước cố định để đạt throughput tối đa - Bộ nhớ khi có nhiều instance: Sử dụng
instance_groupvớicount> 1 trong Triton có thể gây mức sử dụng bộ nhớ cao. Sử dụng instance group mặc định cho model DALI
FAQ#
Lợi ích phụ thuộc vào pipeline của bạn. Khi suy luận trên GPU vốn đã nhanh với TensorRT, tiền xử lý bằng CPU mất 2–10 ms có thể trở thành chi phí chi phối. DALI loại bỏ nút thắt này bằng cách chạy tiền xử lý trên GPU. Mức cải thiện lớn nhất đạt được với input độ phân giải cao (1080p, 4K), batch size lớn và các hệ thống có số core CPU giới hạn trên mỗi GPU.
Có. Sử dụng
DALIGenericIteratorđể nhận outputtorch.Tensorđã được tiền xử lý, sau đó truyền chúng vàomodel.predict(). Tuy nhiên, lợi ích về hiệu năng lớn nhất đạt được với các model TensorRT, trong đó suy luận vốn đã rất nhanh và tiền xử lý bằng CPU trở thành nút thắt.fn.padchỉ thêm padding vào các cạnh bên phải và bên dưới.fn.cropcùng vớiout_of_bounds_policy="pad"căn giữa hình ảnh và thêm padding đối xứng ở tất cả các phía, khớp với hành viLetterBox(center=True)của Ultralytics.Gần như giống hệt. Đặt
antialias=Falsetrongfn.resizeđể khớp vớicv2.INTER_LINEARcủa OpenCV. Có thể xuất hiện sai khác nhỏ do số thực dấu phẩy động (< 0.001) vì phép tính trên GPU và CPU khác nhau, nhưng những sai khác này không ảnh hưởng đo được đến độ chính xác detection.CV-CUDA là một thư viện khác của NVIDIA dành cho xử lý thị giác được tăng tốc bằng GPU. Thư viện này cung cấp khả năng kiểm soát theo từng operator (tương tự OpenCV nhưng chạy trên GPU), thay vì phương pháp pipeline của DALI.
cvcuda.copymakeborder()của CV-CUDA hỗ trợ padding tường minh theo từng phía, giúp dễ dàng thực hiện letterbox căn giữa. Chọn DALI cho các workflow dựa trên pipeline (đặc biệt khi dùng với Triton), và CV-CUDA để kiểm soát chi tiết ở cấp operator trong code suy luận tùy chỉnh.