Ultralytics YOLO27:

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 ai?

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.

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 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.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 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:

  1. Giải mã ảnh (JPEG/PNG)
  2. Resize đồng thời giữ nguyên tỷ lệ khung hình
  3. Pad đế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 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ốngDALI hữu ích như thế nào
Suy luận GPU nhanhCá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 caoLuồng video 1080p và 4K đòi hỏi các thao tác resize tốn kém
Batch size lớnSuy 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#

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á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ướcThao tácHàm CPUTương đương trong DALI
1Resize letterboxcv2.resizefn.resize(mode="not_larger")
2Padding 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. Resize thành (round(w * r), round(h * r))
  3. Pad 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 để 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):

Pipeline DALI với padding 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)

    # 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 output
Khi nào chỉ cần `fn.pad` là đủ?

Nế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.

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

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.

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}]")

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.

Dự đoán với DALI + Ultralytics
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 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:

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

Triton 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.pbtxt

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

Serialize DALI pipeline cho Triton DALI backend:

Serialize DALI pipeline 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

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

Export YOLO model thành TensorRT engine
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"
      }
    }
  ]
}
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 ý 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#

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

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ả 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.pad chỉ đệm bên phải/dưới: Dùng fn.crop với out_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=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, 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_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 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 ra torch.Tensor đã qua tiền xử lý, sau đó truyền đầu ra đó vào model.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.pad chỉ thêm phần đệm ở các cạnh phải và 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 cách hoạt động 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 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.

Bình luận