YOLO Vision 2026:

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

Tóm tắt nhanh
  • Đ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.Tensor tới model.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:

  1. Giải mã ảnh (JPEG/PNG)
  2. Resize đồng thời duy trì tỷ lệ khung hình
  3. Pad đến kích thước đích (letterbox)
  4. Normalize các 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 trên CPU. Điều này đặc biệt có giá trị khi:

Tình huốngTại sao DALI hữu ích
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ý trên CPU trở thành chi phí chính
Đầu vào độ phân giải caoCá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ớnSuy 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#

Chỉ Linux

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

Yê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ướcThao tácHàm CPUTương đương 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 + normalize /255np.transpose + tensor / 255fn.crop_mirror_normalize(std=[255,255,255])

Thao tác letterbox duy trì tỷ lệ khung hình bằng cách:

  1. Tính scale: r = min(target_h / h, target_w / w)
  2. Resize thành (round(w * r), round(h * r))
  3. Pad phần không gian 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 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):

Pipeline DALI với padding căn giữa (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.
    """
    # 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
Khi nào `fn.pad` là đủ?

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

Tại sao dùng `fn.crop` cho padding căn giữa?

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.5crop_pos_y=0.5 mặc định, ảnh sẽ tự động được căn giữa với padding đối xứng.

Không tương thích về antialias

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#

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

Dự đoán 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 có overhead tiền xử lý

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:

Pipeline DALI cho 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."""
    # 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 output

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

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

Serialize pipeline DALI cho DALI backend của Triton:

Serialize 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."""
    # 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#

Export model YOLO thành TensorRT engine
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.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 ánh xạ ensemble hoạt động

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#

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

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 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ú
DetectionTiền xử lý letterbox tiêu chuẩn
Phân đoạn instanceTiền xử lý giống như đối với detection
Semantic SegmentationTiền xử lý hình ảnh giống như đối với detection
ClassificationSử 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.pad chỉ dành cho bên phải/dưới: Sử dụng fn.crop với out_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=True tạ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_group với count > 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 output torch.Tensor đã được tiền xử lý, sau đó truyền chúng vào model.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.pad chỉ thêm padding vào 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 hình ảnh và thêm padding đối xứng ở tất cả các phía, 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ể 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.

Những người đóng góp

Bình luận