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. TensorRT có thể chạy suy luận model chỉ trong vài mili giây, nhưng tiền xử lý dựa trên CPU (resize, pad, chuẩn hóa) 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 sẽ chỉ cho bạn cách xây dựng pipeline DALI tái tạo chính xác quy trình tiền xử lý của Ultralytics YOLO, tích hợp chúng với model.predict(), xử lý luồng video và triển khai toàn diện bằng 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ý bằng CPU được xác định là nút thắt cổ chai — thường là các hệ thống triển khai TensorRT trên GPU NVIDIA, pipeline video thông lượng cao hoặc các thiết lập Triton Inference Server. Nếu bạn đang chạy suy luận thông thường bằng model.predict() và không gặp nút thắt cổ chai ở khâu tiền xử lý, pipeline CPU mặc định sẽ hoạt động tốt.
- Đang xây dựng pipeline DALI? Dùng
fn.resize(mode="not_larger")+fn.crop(out_of_bounds_policy="pad")+fn.crop_mirror_normalizeđể tái tạo quy trình 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.Tensorchomodel.predict()— Ultralytics sẽ tự động bỏ qua bước tiền xử lý ảnh. - Đang triển khai với Triton? Dùng backend DALI cùng với TensorRT ensemble để tiền xử lý mà không cần CPU.
Vì sao nên dùng DALI để tiền xử lý YOLO#
Trong pipeline suy luận YOLO thông thường, các bước tiền xử lý chạy trên CPU:
- Giải mã ảnh (JPEG/PNG)
- Resize đồng thời giữ nguyên tỷ lệ khung hình
- Pad đến kích thước đích (letterbox)
- Chuẩn hóa giá trị pixel từ
[0, 255]đến[0, 1] - Chuyển đổi bố cục 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 ở CPU. Điều này đặc biệt hữu ích khi:
| Tình huống | DALI hữu ích như thế nào |
|---|---|
| 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ý bằng CPU trở thành chi phí chính |
| Đầu vào độ phân giải cao | Luồng video 1080p và 4K đòi hỏi các thao tác resize tốn kém |
| Batch size lớn | Suy luận phía máy chủ xử lý nhiều ảnh song song |
| Số lõi CPU hạn chế | Các thiết bị biên như NVIDIA Jetson, hoặc máy chủ 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 có trên Windows hoặc macOS.
Cài đặt các gói cần thiết:
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 trở lê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 tìm hiểu chính xác các thao tác Ultralytics thực hiện 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), # Kích thước đích
center=True, # Căn giữa ảnh (pad đều ở cả hai bên)
stride=32, # Căn chỉnh theo stride
padding_value=114, # Padding màu xám (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 trong 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 + chuẩn hóa /255 | np.transpose + tensor / 255 | fn.crop_mirror_normalize(std=[255,255,255]) |
Thao tác letterbox giữ nguyên tỷ lệ khung hình bằng cách:
- Tính tỷ lệ:
r = min(target_h / h, target_w / w) - Resize thành
(round(w * r), round(h * r)) - Pad phần còn lại bằng màu xám (
114) để đạt kích thước đích - Căn giữa ảnh để padding được phân bổ đều ở cả hai bên
Pipeline DALI cho YOLO#
Pipeline DALI được khuyến nghị tái tạo hành vi mặc định LetterBox(center=True) của Ultralytics, vốn được dùng trong suy luận YOLO thông thường.
Pipeline căn giữa (được khuyến nghị, khớp với LetterBox của Ultralytics)#
Phiên bản này tái tạo chính xác quy trình tiền xử lý mặc định của Ultralytics với padding căn giữa, khớp 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.
"""
# Đọc và giải mã ảnh trên 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)
# Resize đồng thời giữ nguyên tỷ lệ khung hình
resized = fn.resize(
images,
resize_x=target_size,
resize_y=target_size,
mode="not_larger",
interp_type=types.INTERP_LINEAR,
antialias=False, # Khớp với cv2.INTER_LINEAR (không khử răng cưa)
)
# Padding căn giữa bằng fn.crop với out_of_bounds_policy
# Khi kích thước crop > kích thước ảnh, fn.crop căn giữa ảnh và pad đối xứng
padded = fn.crop(
resized,
crop=(target_size, target_size),
out_of_bounds_policy="pad",
fill_values=114, # Giá trị padding YOLO
)
# Chuẩn hóa và chuyển đổi bố cục
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 khớp 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 dùng fn.pad(...) thay vì fn.crop(..., out_of_bounds_policy="pad"). Biến thể này chỉ pad các cạnh phải và 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 hành vi letterbox căn giữa mặc định của Ultralytics.
Toán tử fn.pad của DALI chỉ thêm padding vào các cạnh phải và dưới. Để padding căn giữa (khớp với LetterBox(center=True) của Ultralytics), hãy 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 khử răng cưa theo mặc định (antialias=True), trong khi cv2.resize của OpenCV với INTER_LINEAR không áp dụng khử răng cưa. 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 sai khác số học nhỏ, 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}]")Dùng DALI với Ultralytics Predict#
Bạn có thể truyền trực tiếp tensor PyTorch đã tiền xử lý cho model.predict(). Khi truyền torch.Tensor, Ultralytics bỏ qua bước tiền xử lý ảnh (letterbox, BGR→RGB, HWC→CHW và chuẩn hóa /255), chỉ thực hiện chuyển thiết bị và ép kiểu dtype trước khi gửi ảnh đến model.
Trong trường hợp này, Ultralytics không có thông tin về kích thước ảnh gốc, nên tọa độ 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 dùng scale_boxes, công cụ 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 có shape (N, 4) ở định dạng xyxy, tọa độ trong không gian letterbox 640x640
# Scale box từ không gian letterbox (640, 640) về kích thước gốc (orig_h, orig_w)
boxes = scale_boxes((640, 640), boxes, (orig_h, orig_w))Điều này áp dụng cho tất cả các 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 cho model.predict(), bước tiền xử lý ảnh mất ~0.004ms (gần như bằng không), so với ~1-10ms khi tiền xử lý bằng CPU. Tensor phải ở định dạng BCHW, kiểu float32 (hoặc float16) và được chuẩn hóa về [0, 1]. Ultralytics vẫn tự động xử lý việc chuyển thiết bị và ép kiểu dtype.
DALI với luồng video#
Để xử lý video theo thời gian thực, hãy dùng fn.external_source để đưa các frame từ bất kỳ nguồn nào — OpenCV, GStreamer hoặc thư viện thu nhận 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."""
# Nguồn ngoài để nạp frame từ OpenCV, GStreamer, v.v.
frames = fn.external_source(device="cpu", name="input")
frames = fn.reshape(frames, layout="HWC")
# Chuyển sang GPU và tiền xử lý
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 bước tiền xử lý DALI với bước suy luận TensorRT trong Triton Inference Server bằng một model ensemble. Cách này loại bỏ hoàn toàn bước tiền xử lý trên CPU — đầu vào là byte JPEG thô, đầu ra là các detection; 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 DALI Pipeline#
Serialize DALI pipeline cho Triton DALI backend:
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."""
# Đầu vào: byte ảnh thô đã mã hóa từ 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 vào model repository
pipe = triton_dali_pipeline()
pipe.serialize(filename="model_repository/dali_preprocessing/1/model.dali")Bước 2: Export YOLO sang TensorRT#
from pathlib import Path
from ultralytics import YOLO
model = YOLO("yolo26n.pt")
engine_path = model.export(
format="engine", imgsz=640, quantize=16, batch=8, dynamic=True, nms=False
) # Không dùng NMS (N, 300, 6); TensorRT >= 8.5
# Ultralytics thêm header metadata vào các file .engine; hãy loại bỏ header để Triton có thể tải raw TensorRT plan
with open(engine_path, "rb") as f:
meta_len = int.from_bytes(f.read(4), byteorder="little") # độ dài của header metadata JSON
f.seek(4 + meta_len)
plan = f.read()
Path("model_repository/yolo_trt/1").mkdir(parents=True, exist_ok=True)
Path("model_repository/yolo_trt/1/model.plan").write_bytes(plan)Bướ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 bước này 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 yêu cầu 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, cách này không hoạt động với DALI ensemble vì YOLO() gửi tensor float32 đã qua tiền xử lý, trong khi ensemble cần byte JPEG thô. Hãy dùng trực tiếp tritonclient cho DALI ensemble và dùng tích hợp sẵn cho các triển khai thông thường 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 đệm tất cả mảng byte đã mã hóa để chúng có cùng độ dài (bằng số byte lớn nhất trong batch). Triton yêu cầu các shape batch của tensor đầu vào phải đồng nhất.
Các tác vụ được hỗ trợ#
Tiền xử lý DALI hoạt động với mọi tác vụ YOLO sử dụng pipeline LetterBox tiêu chuẩn:
| Tác vụ | Được hỗ trợ | Ghi chú |
|---|---|---|
| Phát hiện | ✅ | Tiền xử lý letterbox tiêu chuẩn |
| Phân đoạn instance | ✅ | Sử dụng cùng bước tiền xử lý như detection |
| Phân đoạn ngữ nghĩa | ✅ | Sử dụng cùng bước tiền xử lý ảnh như detection |
| Ước lượng độ sâu | ✅ | Sử dụng cùng bước tiền xử lý ảnh như detection |
| Phân loại | ❌ | Sử dụng các phép biến đổi của torchvision (center crop), không dùng letterbox |
| Ước tính pose | ✅ | Sử dụng cùng bước tiền xử lý như detection |
| Phát hiện định hướng (OBB) | ✅ | Sử dụng cùng bước tiền xử lý như detection |
Hạn chế#
- Chỉ Linux: DALI không hỗ trợ Windows hoặc macOS
- Yêu cầu NVIDIA GPU: 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 linh hoạt
fn.padchỉ đệm bên phải/dưới: Dùngfn.cropvớiout_of_bounds_policy="pad"để đệm căn giữa- Không hỗ trợ chế độ rect: DALI pipeline tạo đầu ra có kích thước cố định (ví dụ: 640×640). Chế độ rect
auto=Truetạo đầu ra có kích thước thay đổi (ví dụ: 384×640) không được hỗ trợ. Lưu ý rằng dù TensorRT hỗ trợ shape đầu vào động, DALI pipeline có kích thước cố định sẽ kết hợp tự nhiên với engine có kích thước cố định để đạt thông lượng tối đa - Bộ nhớ khi dùng nhiều instance: Dùng
instance_groupvớicount> 1 trong Triton có thể gây tiêu thụ nhiều bộ nhớ. Hãy dùng nhóm instance mặc định cho DALI model
Câu hỏi thường gặp#
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, bước tiền xử lý trên CPU mất 2–10ms có thể trở thành chi phí lớn nhất. DALI loại bỏ nút thắt này bằng cách chạy bước tiền xử lý trên GPU. Lợi ích rõ nhất khi dùng đầu vào độ phân giải cao (1080p, 4K), batch size lớn và hệ thống có ít CPU core trên mỗi GPU.
Có. Dùng
DALIGenericIteratorđể lấy đầu ratorch.Tensorđã qua tiền xử lý, sau đó truyền đầu ra đó vàomodel.predict(). Tuy nhiên, lợi ích về hiệu năng lớn nhất khi dùng model TensorRT, trong đó suy luận vốn đã rất nhanh và bước tiền xử lý trên CPU trở thành nút thắt.fn.padchỉ thêm phần đệm ở các cạnh phải và dưới.fn.cropcùng vớiout_of_bounds_policy="pad"căn giữa ảnh và thêm phần đệm đối xứng ở tất cả các cạnh, khớp với cách hoạt độngLetterBox(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ể xảy ra sai khác nhỏ về số thực dấu phẩy động (< 0.001) do 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 đáng kể đến độ chính xác detection.CV-CUDA là một thư viện khác của NVIDIA để tăng tốc xử lý thị giác trên GPU. Thư viện này cung cấp khả năng kiểm soát từng operator (tương tự OpenCV nhưng chạy trên GPU) thay vì cách tiếp cận dựa trên pipeline của DALI.
cvcuda.copymakeborder()của CV-CUDA hỗ trợ đệm rõ ràng cho từng cạnh, giúp thực hiện letterbox căn giữa dễ dàng. Chọn DALI cho quy trình làm việc dựa trên pipeline (đặc biệt khi dùng với Triton) và CV-CUDA để kiểm soát chi tiết từng operator trong mã suy luận tùy chỉnh.