YOLO Vision 2026:

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

Khi triển khai các model Ultralytics YOLO trong môi trường production, quá trình preprocessing thường trở thành điểm nghẽn (bottleneck). Trong khi TensorRT có thể chạy model inference chỉ trong vài mili-giây, thì quá trình preprocessing dựa trên CPU (resize, pad, normalize) có thể tốn 2-10ms cho mỗi ảnh, đặc biệt ở độ phân giải cao. NVIDIA DALI (Data Loading Library) giải quyết vấn đề này bằng cách chuyển toàn bộ pipeline preprocessing sang GPU.

Hướng dẫn này sẽ đưa bạn qua các bước xây dựng các pipeline DALI mô phỏng chính xác quá trình preprocessing 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 các model YOLO trong môi trường production nơi việc preprocessing trên CPU là điểm nghẽn đã đo đạc được — thường là các bản triển khai TensorRT trên GPU NVIDIA, các 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 inference chuẩn với model.predict() và không gặp phải điểm nghẽn preprocessing, pipeline CPU mặc định sẽ hoạt động tốt.

Tóm tắt nhanh
  • Đang xây dựng một pipeline DALI? Sử dụng fn.resize(mode="not_larger") + fn.crop(out_of_bounds_policy="pad") + fn.crop_mirror_normalize để mô phỏng quá trình letterbox preprocessing của YOLO trên GPU.
  • Tích hợp với Ultralytics? Truyền đầu ra của DALI dưới dạng torch.Tensor vào model.predict() — Ultralytics sẽ tự động bỏ qua bước image preprocessing.
  • Đang triển khai với Triton? Sử dụng DALI backend cùng với một ensemble TensorRT để tiền xử lý zero-CPU.

Tại sao sử dụng DALI cho tiền xử lý YOLO#

Trong một pipeline inference 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 trong khi vẫn giữ nguyên tỷ lệ khung hình
  3. Pad đến kích thước mục tiêu (letterbox)
  4. Normalize giá trị pixel từ [0, 255] sang [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, giúp loại bỏ điểm nghẽn CPU. Điều này đặc biệt có giá trị khi:

Kịch bảnTại sao DALI giúp ích
Inference GPU nhanhCác engine TensorRT với tốc độ inference dưới 1 mili-giây khiến cho CPU preprocessing trở thành chi phí chiếm ưu thế
Đầ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 kém
Batch size lớnInference phía máy chủ xử lý nhiều ảnh song song
Giới hạn CPU coreCác thiết bị edge như NVIDIA Jetson, hoặc các server GPU mật độ cao với số nhân CPU trên mỗi GPU ít

Điều kiện tiên quyết#

Chỉ dành cho Linux

NVIDIA DALI chỉ hỗ trợ Linux. Nó không khả dụng 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:

  • NVIDIA GPU (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

Hiểu về tiền xử lý YOLO#

Trước khi xây dựng một pipeline DALI, bạn nên hiểu rõ những gì Ultralytics thực hiện trong quá trình preprocessing. Class cốt lõi 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)
)

Toàn bộ pipeline preprocessing trong ultralytics/engine/predictor.py thực hiện các bước sau:

BướcThao tácHàm CPUTương đương DALI
1Letterbox resizecv2.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 duy trì tỷ lệ khung hình bằng cách:

  1. Tính toán scale: r = min(target_h / h, target_w / w)
  2. Resize về (round(w * r), round(h * r))
  3. Thêm padding vào không gian còn lại bằng màu xám (114) để đạt được kích thước mục tiêu
  4. Căn giữa ảnh để phần đệm được phân bổ đều ở cả hai bên

DALI Pipeline cho YOLO#

Pipeline DALI được khuyến nghị mô phỏng hành vi LetterBox(center=True) mặc định của Ultralytics, đây là điều mà YOLO inference tiêu chuẩn sử dụng.

Pipeline căn giữa (Được khuyến nghị, khớp với Ultralytics LetterBox)#

Phiên bản này mô phỏng chính xác quá trình preprocessing mặc định của Ultralytics với padding căn giữa, khớp với LetterBox(center=True):

Pipeline DALI với phần đệm 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 bạn không cần sự đồng nhất 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 vì fn.crop(..., out_of_bounds_policy="pad"). Biến thể đó chỉ thêm padding cho các cạnh phải và dưới, điều này có thể chấp nhận được đối với các pipeline triển khai tùy chỉnh, nhưng nó 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.

Tại sao dùng `fn.crop` cho phần đệm căn giữa?

Operator fn.pad của DALI chỉ thêm padding vào các cạnh phải và dưới. Để có padding căn giữa (khớp với LetterBox(center=True) của Ultralytics), hãy sử dụng fn.crop với out_of_bounds_policy="pad". Với crop_pos_x=0.5crop_pos_y=0.5 mặc định, bức ảnh sẽ được tự động căn giữa với padding đối xứng.

Lệch khử răng cưa (Antialias)

Operator fn.resize của DALI bật tính năng khử răng cưa (antialiasing) 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. Việc bỏ qua điều này sẽ gây ra các khác biệt nhỏ về số học 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 một tensor PyTorch đã qua xử lý sơ bộ trực tiếp tới model.predict(). Khi một torch.Tensor được truyền vào, Ultralytics sẽ bỏ qua quá trình image preprocessing (letterbox, BGR→RGB, HWC→CHW, và chuẩn hóa /255) và chỉ thực hiện chuyển đổi thiết bị (device transfer) cùng ép kiểu dữ liệu (dtype casting) trước khi gửi nó đến model.

Vì 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, các tọa độ của hộp bounding box nhận diện sẽ được trả về trong không gian letterbox 640×640. Để ánh xạ chúng trở lại tọa độ ảnh gốc, hãy sử dụng scale_boxes để xử lý logic làm tròn chính xác được dùng bởi LetterBox:

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 tất cả các đường dẫn 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")
Zero Preprocessing Overhead

Khi bạn truyền torch.Tensor vào model.predict(), bước image preprocessing chỉ mất khoảng ~0.004ms (gần như bằng không) so với khoảng ~1-10ms khi dùng CPU preprocessing. Tensor phải ở định dạng BCHW, float32 (hoặc float16), và được chuẩn hóa về [0, 1]. Ultralytics sẽ vẫn xử lý việc chuyển đổi thiết bị và ép kiểu dữ liệu một cách tự động.

DALI với luồng video#

Đối với xử lý video thời gian thực, hãy sử dụng fn.external_source để nạp các khung hình từ bất kỳ nguồn nào — OpenCV, GStreamer, hoặc các thư viện bắt hình 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#

Đối với việc triển khai production, hãy kết hợp DALI preprocessing với TensorRT inference trong Triton Inference Server bằng cách sử dụng một ensemble model. Điều này loại bỏ hoàn toàn quá trình CPU preprocessing — các byte JPEG thô đi vào, kết quả phát hiện đi ra, với mọi thứ đượ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 (serialize) pipeline DALI cho Triton DALI backend:

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."""
    # 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: Xuất YOLO sang TensorRT#

Xuất model YOLO sang engine TensorRT
from ultralytics import YOLO

model = YOLO("yolo26n.pt")
model.export(format="engine", imgsz=640, quantize=16, batch=8)
# 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 thức 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à những tên tùy ý liên kết đầu ra của một bước này với đầu ra của bước tiếp theo — chúng không cần phải khớp với bất kỳ tên tensor nội bộ nào của model.

Bước 4: Gửi các yêu cầu Inference#

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

Ultralytics có hỗ trợ Triton tích hợp sẵn xử lý các bước tiền/hậu xử lý một cách tự động. Tuy nhiên, nó sẽ không hoạt động với DALI ensemble vì YOLO() gửi một tensor float32 đã được xử lý sơ bộ trong khi ensemble lại mong đợi các byte JPEG thô. Hãy sử dụng trực tiếp tritonclient cho các DALI ensemble, và tích hợp sẵn cho các bản triển khai tiêu chuẩn không dùng DALI.

Gửi hình ảnh đến ensemble Triton
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 — YOLO26 is end-to-end)
detections = detections[0]  # First image
detections = detections[detections[:, 4] > 0.25]  # Confidence threshold
print(f"Detected {len(detections)} objects")
Xử lý theo lô (Batching) hình ảnh JPEG

Khi gửi một lô hình ảnh JPEG đến Triton, hãy đệm (pad) tất cả các mảng byte đã mã hóa về cùng một độ dài (số byte tối đa trong lô). Triton yêu cầu các hình dạng lô đồng nhất cho tensor đầu vào.

Các tác vụ được hỗ trợ#

DALI preprocessing hoạt động với tất cả các tác vụ YOLO sử dụng pipeline LetterBox chuẩn:

Tác vụĐược hỗ trợLưu ý
Phát hiệnTiền xử lý letterbox tiêu chuẩn
Phân đoạn Cá thểTiền xử lý tương tự như nhận diện
Phân đoạn Ngữ nghĩaTiền xử lý hình ảnh tương tự như nhận diện
Phân loạiSử dụng các transform từ torchvision (center crop), không sử dụng letterbox
Ước lượng Tư thếTiền xử lý tương tự như nhận diện
Oriented Detection (OBB)Tiền xử lý tương tự như nhận diện

Hạn chế#

  • Chỉ dành cho 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 định nghĩa tại thời điểm build và không thể thay đổi linh hoạt
  • fn.pad chỉ áp dụng cho cạnh phải/dưới: Sử dụng fn.crop với out_of_bounds_policy="pad" để căn giữa padding
  • Không có chế độ rect: Các pipeline DALI tạo ra đầu ra có kích thước cố định (ví dụ: 640×640). Chế độ rect của auto=True tạo ra đầu ra có kích thước biến đổi (ví dụ: 384×640) không được hỗ trợ. Lưu ý rằng mặc dù TensorRT có hỗ trợ hình dạng đầu vào động (dynamic input shapes), một pipeline DALI kích thước cố định sẽ kết hợp tự nhiên với một engine kích thước cố định để đạt thông lượng tối đa.
  • Bộ nhớ với 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. Hãy sử dụng instance group 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 GPU inference đã nhanh với TensorRT, CPU preprocessing ở mức 2-10ms có thể trở thành chi phí chiếm ưu thế. DALI loại bỏ điểm nghẽn này bằng cách chạy preprocessing trên GPU. Những cải thiện lớn nhất được thấy với các đầu vào độ phân giải cao (1080p, 4K), batch size lớn, và các hệ thống có số nhân CPU giới hạn trên mỗi GPU.

  • Có. Sử dụng DALIGenericIterator để lấy đầu ra torch.Tensor đã qua xử lý sơ bộ, sau đó truyền chúng đến model.predict(). Tuy nhiên, lợi ích về hiệu năng lớn nhất đối với các model TensorRT nơi inference đã rất nhanh và CPU preprocessing trở thành điểm nghẽn.

  • fn.pad thêm padding chỉ vào các cạnh phải và dưới. fn.crop với out_of_bounds_policy="pad" căn giữa bức ả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 nhau. Đặt antialias=False trong fn.resize để khớp với cv2.INTER_LINEAR của OpenCV. Các khác biệt nhỏ về dấu phẩy động (< 0.001) có thể xảy ra do phép toán số học trên GPU so với CPU, nhưng chúng không có tác động đáng kể nào đến độ chính xác phát hiện.

  • CV-CUDA là một thư viện khác của NVIDIA dành cho xử lý thị giác máy tính được tăng tốc bằng GPU. Nó cung cấp khả năng điều khiển từng operator (giống như OpenCV nhưng trên GPU) thay vì cách tiếp cận theo pipeline của DALI. Operator cvcuda.copymakeborder() của CV-CUDA hỗ trợ padding rõ ràng trên từng cạnh, làm cho letterbox căn giữa trở nên đơn giản. Hãy chọn DALI cho các workflow dựa trên pipeline (đặc biệt với Triton), và CV-CUDA để điều khiển mức độ operator chi tiết trong code inference tùy chỉnh.

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

Bình luận