Ultralytics YOLO27: Tham gia danh sách chờ.
Get Started

Tiền xử lý tăng tốc bằng GPU với NVIDIA DALI#

Khi triển khai 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 inference model chỉ trong vài mili giây, tiền xử lý dựa trên CPU (thay đổi kích thước, đệm, chuẩn hóa) có thể mất 2-10ms 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 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ý Ultralytics YOLO, tích hợp pipeline với model.predict(), xử lý luồng video và triển khai toàn diện với Triton Inference Server.

Hướng dẫn này dành cho ai?

Hướng dẫn này dành cho kỹ sư triển khai model YOLO trong môi trường production, nơi tiền xử lý trên CPU được xác định là nút thắt cổ chai — thường là các triển khai TensorRT trên GPU NVIDIA, pipeline video thông lượng cao hoặc cấu hình Triton Inference Server. Nếu bạn chạy inference tiêu chuẩn bằng model.predict() và không gặp nút thắt ở khâu tiền xử lý, pipeline CPU mặc định sẽ hoạt động tốt.

Tóm tắt nhanh
  • Đ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 bước 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.Tensor cho model.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 kết hợp với TensorRT ensemble để tiền xử lý mà không cần CPU.

Tại sao nên dùng DALI để tiền xử lý YOLO#

Trong pipeline inference YOLO thông thường, các bước tiền xử lý chạy trên CPU:

  1. Giải mã ảnh (JPEG/PNG)
  2. Thay đổi kích thước đồng thời giữ nguyên tỷ lệ khung hình
  3. Đệm đến kích thước đích (letterbox)
  4. Chuẩn hóa giá trị pixel từ [0, 255] đến [0, 1]
  5. 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 ở CPU. Điều này đặc biệt hữu ích khi:

Kịch bảnVì sao DALI hữu ích
Inference GPU tốc độ caoCác engine TensorRT có thời gian inference dưới một mili giây khiến tiền xử lý trên CPU trở thành chi phí chiếm ưu thế
Đầu vào độ phân giải caoCác luồng video 1080p và 4K đòi hỏi thao tác thay đổi kích thước tốn kém
Batch size lớnInference 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#

Chỉ hỗ trợ Linux

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-cuda130

Yê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ách Ultralytics tiền xử lý dữ liệu. Class quan trọng 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 (đệm đều ở cả hai bên)
    stride=32,  # Căn chỉnh theo stride
    padding_value=114,  # Đệm 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ướcThao tácHàm CPUTương đương trong DALI
1Thay đổi kích thước letterboxcv2.resizefn.resize(mode="not_larger")
2Đệm căn giữacv2.copyMakeBorderfn.crop(out_of_bounds_policy="pad")
3BGR → RGBim[..., ::-1]fn.decoders.image(output_type=types.RGB)
4HWC → CHW + chuẩn hóa /255np.transpose + tensor / 255fn.crop_mirror_normalize(std=[255,255,255])

Thao tác letterbox giữ nguyên tỷ lệ khung hình bằng cách:

  1. Tính tỷ lệ: r = min(target_h / h, target_w / w)
  2. Thay đổi kích thước thành (round(w * r), round(h * r))
  3. Đệm phần còn lại bằng màu xám (114) để đạt kích thước đích
  4. Căn giữa ảnh để phần đệm được phân bố đều ở cả hai bên

Pipeline DALI cho YOLO#

Pipeline DALI được khuyến nghị sẽ tái tạo hành vi mặc định của LetterBox(center=True) trong Ultralytics, vốn được sử dụng cho inference YOLO tiêu chuẩn.

Phiên bản này tái tạo chính xác tiền xử lý mặc định của Ultralytics với phần đệm căn giữa, tương thích với LetterBox(center=True):

Pipeline DALI với phần đệm căn giữa (được khuyến nghị)
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)

    # Thay đổi kích thước mà vẫn 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)
    )

    # Đệm căn giữa bằng fn.crop với out_of_bounds_policy
    # Khi kích thước crop lớn hơn kích thước ảnh, fn.crop căn giữa ảnh và đệm đối xứng
    padded = fn.crop(
        resized,
        crop=(target_size, target_size),
        out_of_bounds_policy="pad",
        fill_values=114,  # Giá trị đệm YOLO
    )

    # Chuẩn hóa và chuyển đổi 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
Khi nào `fn.pad` là đủ?

Nếu không cần độ tương thích chính xác với LetterBox(center=True), bạn có thể đơn giản hóa bước đệm bằng cách dùng fn.pad(...) thay cho fn.crop(..., out_of_bounds_policy="pad"). Biến thể này chỉ đệm các cạnh bên phải và bên dưới, điều này 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.

Vì sao dùng `fn.crop` để đệm căn giữa?

Toán tử fn.pad của DALI chỉ thêm phần đệm vào các cạnh bên phải và bên dưới. Để đệm căn giữa (tương thích với LetterBox(center=True) của Ultralytics), hãy dùng fn.crop 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 phần đệm đối xứng.

Sai khác về khử răng cưa

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#

Xây dựng và chạy pipeline 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}]")

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 bỏ qua bước tiền xử lý ảnh (letterbox, BGR→RGB, HWC→CHW và chuẩn hóa /255), đồng thời chỉ thực hiện chuyển thiết bị và ép kiểu dữ liệu trước khi đưa ảnh vào model.

Trong trường hợp này, Ultralytics không có quyền truy cập vào kích thước ảnh gốc, nên tọa độ bounding 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, hàm xử lý chính xác logic làm tròn được dùng bởi LetterBox:

from ultralytics.utils.ops import scale_boxes

# boxes: tensor có shape (N, 4) theo định dạng xyxy, tọa độ letterbox 640x640
# Scale boxes từ 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.

DALI + Ultralytics predict
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")
Không tốn chi phí tiền xử lý

Khi bạn truyền torch.Tensor vào model.predict(), bước tiền xử lý ảnh chỉ 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, có 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 dữ liệu.

DALI với luồng video#

Để xử lý video theo thời gian thực, hãy dùng fn.external_source để nạp khung hình từ bất kỳ nguồn nào — OpenCV, GStreamer hoặc thư viện thu nhận tùy chỉnh:

Pipeline DALI để tiền xử lý luồng video
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 bên ngoài để nạp khung hình 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 output

Triton Inference Server với DALI#

Để triển khai trong môi trường production, hãy kết hợp tiền xử lý DALI với inference TensorRT trong Triton Inference Server bằng model ensemble. Cách này loại bỏ hoàn toàn bước tiền xử lý trên CPU — dữ liệu JPEG thô được đưa vào, kết quả phát hiện được trả về và mọi thao tác đều được xử lý trên GPU.

Cấu trúc kho lưu trữ model#

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

Bước 1: Tạo pipeline DALI#

Tuần tự hóa pipeline DALI cho backend DALI của Triton:

Tuần tự hóa pipeline DALI cho 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."""
    # Đầ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

# Tuần tự hóa pipeline vào kho lưu trữ model
pipe = triton_dali_pipeline()
pipe.serialize(filename="model_repository/dali_preprocessing/1/model.dali")

Bước 2: Xuất YOLO sang TensorRT#

Xuất model YOLO thành engine 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 phần header metadata vào các tệp .engine; hãy loại bỏ phần này để Triton có thể tải TensorRT plan thô
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"
      }
    }
  ]
}
Cách hoạt động của ánh xạ ensemble

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 ý dùng để 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 inference#

Tại sao dùng `tritonclient` thay vì `YOLO('http://...')`?

Ultralytics có hỗ trợ Triton tích hợp sẵn để tự động xử lý tiền/hậu xử lý. Tuy nhiên, tính năng này không hoạt động với DALI ensemble vì YOLO() gửi tensor float32 đã 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 tích hợp sẵn cho các triển khai tiêu chuẩn không dùng DALI.

Gửi ảnh đến Triton ensemble
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")
Batch ảnh JPEG

Khi gửi một batch ảnh JPEG đến Triton, hãy đệm tất cả các mảng byte đã mã hóa để chúng có cùng độ dài (bằng số byte tối đa trong batch). Triton yêu cầu các shape của batch đầ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 đối tượng✅Tiền xử lý giống như cho phát hiện đối tượng
Phân đoạn ngữ nghĩa✅Tiền xử lý ảnh giống như cho phát hiện đối tượng
Ước lượng độ sâu✅Tiền xử lý ảnh giống như cho phát hiện đối tượng
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 lượng tư thế✅Tiền xử lý giống như cho phát hiện đối tượng
Phát hiện đối tượng định hướng (OBB)✅Tiền xử lý giống như cho phát hiện đối tượng

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 linh hoạt
  • fn.pad chỉ đệm bên phải/bên dưới: Dùng fn.crop với out_of_bounds_policy="pad" để đệm căn giữa
  • Không có chế độ rect: Pipeline DALI tạo đầu ra có kích thước cố định (ví dụ: 640×640). Chế độ rect auto=True tạ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, pipeline DALI có kích thước cố định kết hợp tự nhiên với engine 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_group với count > 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 model DALI

Câu hỏi thường gặp#

  • Lợi ích phụ thuộc vào pipeline của bạn. Khi inference bằng GPU đã nhanh với TensorRT, tiền xử lý bằng CPU mất 2-10ms có thể trở thành chi phí chiếm ưu thế. 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 thường thấy ở đầu vào độ phân giải cao (1080p, 4K), batch size lớn và hệ thống có số lõi CPU hạn chế trên mỗi GPU.

  • Có. Dùng DALIGenericIterator để lấy đầu ra torch.Tensor đã tiền xử lý, rồi truyền chúng vào model.predict(). Tuy nhiên, lợi ích về hiệu năng lớn nhất khi dùng model TensorRT, vì inference vốn đã rất nhanh và tiền xử lý bằng CPU trở thành nút thắt.

  • fn.pad chỉ thêm phần đệm ở các cạnh bên phải và bên dưới. fn.crop cùng với out_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 hành vi LetterBox(center=True) của Ultralytics.

  • Gần như giống hệt. Đặt antialias=False trong fn.resize để khớp với cv2.INTER_LINEAR của OpenCV. Có thể xảy ra sai khác nhỏ về số thực (< 0.001) do phép tính trên GPU và CPU, nhưng không ảnh hưởng đáng kể đến độ chính xác phát hiện.

  • CV-CUDA là một thư viện khác của NVIDIA dùng để xử lý thị giá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 toán tử (giống 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 tường minh theo từng cạnh, giúp việc căn giữa letterbox trở nên đơn giản. 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 ở cấp toán tử trong mã inference tùy chỉnh.

Bình luận