Skip to main content

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

Giới thiệu

Khi triển khai các mô hình Ultralytics YOLO các model trong môi trường production, tiền xử lý thường trở thành điểm nghẽn. Trong khi TensorRT có thể chạy inference cho model suy luận chỉ trong vài mili giây, tiền xử lý trên CPU (resize, pad, normalize) có thể mất 2-10ms 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 tiền xử lý sang GPU.

Hướng dẫn này sẽ dẫn dắt bạn xây dựng các pipeline DALI tái lập chính xác tiền xử lý YOLO của Ultralytics, tích hợp chúng với model.predict(), xử lý 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 điểm nghẽn đo lường được — thường là trong TensorRT các đợt triển khai trên GPU NVIDIA, các pipeline video thông lượng cao, hoặc Triton Inference Server các thiết lập. Nếu bạn đang chạy inference tiêu chuẩn với model.predict() và không gặp điểm nghẽn tiền xử lý, pipeline mặc định trên CPU vẫn hoạt động tốt.

Tóm tắt nhanh
  • 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 lập tiền xử lý letterbox của YOLO trên GPU.
  • Tích hợp với Ultralytics? Truyền đầu ra DALI như một torch.Tensor đến model.predict() — Ultralytics sẽ tự động bỏ qua bước tiền xử lý ảnh.
  • Triển khai với Triton? Sử dụng DALI backend cùng với một ensemble TensorRT để đạt tiền xử lý zero-CPU.

Tại sao nên 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ã (Decode) ảnh (JPEG/PNG)
  2. Thay đổi kích thước (Resize) trong khi vẫn bảo toàn tỷ lệ khung hình
  3. Đệm (Pad) tới kích thước mục tiêu (letterbox)
  4. Chuẩn hóa (Normalize) giá trị pixel từ [0, 255] đến [0, 1]
  5. Chuyển đổi (Convert) 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ỏ điểm nghẽn CPU. Điều này đặc biệt có giá trị khi:

Kịch bảnTại sao DALI hỗ trợ
Inference GPU tốc độ caoTensorRT các engine với inference dưới một mili giây khiến tiền xử lý CPU trở thành chi phí chiếm ưu thế
Đầu vào độ phân giải caoluồng video 1080p và 4K yêu cầu các thao tác resize tốn kém
Các batch sizesInference phía server 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 các server GPU dày đặc với ít lõi CPU trên mỗi GPU

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

Chỉ dành cho Linux

NVIDIA DALI hỗ trợ chỉ Linux. Nó không khả dụng trên Windows hoặc macOS.

Cài đặt các gói yêu cầu:

pip install ultralytics
pip install --extra-index-url https://pypi.nvidia.com nvidia-dali-cuda120

Yêu cầu:

  • NVIDIA GPU (compute capability 5.0+ / Maxwell trở lên)
  • CUDA 11.0+ hoặc 12.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, việc hiểu chính xác những gì Ultralytics thực hiện trong quá trình tiền xử lý sẽ rất hữu ích. Lớp quan trọng 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 tiền xử lý 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ữa (Centered padding)cv2.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 bảo toàn tỷ lệ khung hình bằng cách:

  1. Tính tỷ lệ scale: 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 khoảng trống 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 để đệm được phân bổ đều ở cả hai bên

Pipeline DALI cho YOLO

Sử dụng pipeline căn giữa dưới đây làm tài liệu tham khảo mặc định. Nó khớp với hành vi LetterBox(center=True) của Ultralytics, đây là thứ mà inference YOLO tiêu chuẩn sử dụng.

Pipeline căn giữa (Khuyên dùng, khớp với Ultralytics LetterBox)

Phiên bản này tái lập chính xác tiền xử lý mặc định của Ultralytics với đệm căn giữa, khớp với LetterBox(center=True):

Pipeline DALI với đệm căn giữa (khuyên dùng)
import nvidia.dali as dali
import nvidia.dali.fn as fn
import nvidia.dali.types as 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ự tương đương 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 sử dụng fn.pad(...)thay vìfn.crop(..., out_of_bounds_policy="pad"). Biến thể đó chỉ đệm 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 lại dùng `fn.crop` cho đệm căn giữa?

Toán tử fn.pad của DALI chỉ thêm đệm vào các cạnh phải và dưới. Để có đệm 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, ảnh được tự động căn giữa với đệm đối xứng.

Antialias Mismatch

Toán tử fn.resize kích hoạt chống răng cưa theo mặc định (antialias=True), trong khi cv2.resize với INTER_LINEAR của OpenCV lại không áp dụng chống răng cưa. Luôn đặt antialias=False trong DALI để khớp với pipeline trên CPU. Việc bỏ qua bước này sẽ gây ra các sai biệt số học nhỏ có thể ảnh hưởng đến độ chính xác của mô hình.

Chạy Pipeline

Xây dựng và chạy một 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 dự đoán của Ultralytics

Bạn có thể chuyển một PyTorch tensor đã tiền xử lý trực tiếp tới model.predict(). Khi một torch.Tensor được truyền vào, Ultralytics sẽ bỏ qua bước tiền xử lý ảnh (letterbox, BGR→RGB, HWC→CHW, và chuẩn hóa /255) và chỉ thực hiện chuyển đổi thiết bị cũng như ép kiểu dữ liệu (dtype casting) trước khi gửi tới 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 độ khung hình dự đoán đượ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, vốn xử lý logic làm tròn chính xác được sử 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 trên Triton.

DALI + Dự đoán của 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 tiêu tốn tài nguyên tiền xử lý

Khi bạn truyền một torch.Tensor đến 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, float32 (hoặc float16), và được chuẩn hóa về [0, 1]. Ultralytics vẫn sẽ tự động xử lý việc chuyển đổi thiết bị và ép kiểu dữ liệu.

DALI với các luồng video

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

Pipeline DALI để tiền xử lý luồng video
import nvidia.dali as dali
import nvidia.dali.fn as fn
import nvidia.dali.types as 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 triển khai sản xuất, hãy kết hợp tiền xử lý DALI với TensorRT suy luận trong Triton Inference Server sử dụng một ensemble model. Cách này loại bỏ hoàn toàn tiền xử lý trên CPU — các byte JPEG thô đi vào, kết quả dự đoá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 backend DALI của Triton:

Tuần tự hóa pipeline DALI cho Triton
import nvidia.dali as dali
import nvidia.dali.fn as fn
import nvidia.dali.types as 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, half=True, 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 ánh xạ Ensemble hoạt động

Ensemble kết nối các model thông qua tên tensor ảo. Chữ output_map giá trị "preprocessed_image" trong bước DALI khớp với input_map giá trị "preprocessed_image" trong bước TensorRT. Đây là những cái 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 phải 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

!!! info "Tại sao tritonclientthay vìYOLO(\"http://...\")?"

Ultralytics has [built-in Triton support](triton-inference-server.md#running-inference) that handles pre/postprocessing automatically. However, it won't work with the DALI ensemble because `YOLO()` sends a preprocessed float32 tensor while the ensemble expects raw JPEG bytes. Use `tritonclient` directly for DALI ensembles, and the [built-in integration](triton-inference-server.md) for standard deployments without DALI.
Gửi hình ảnh tới ensemble của 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")
Batching hình ảnh JPEG

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

, giúp model tổng quát hóa tốt hơn với dữ liệu chưa từng thấy. Bảng dưới đây phác thảo mục đích và hiệu quả của từng đối số tăng cường:

Tiền xử lý DALI hoạt động với tất cả các tác vụ YOLO sử dụng pipeline LetterBox tiêu chuẩn:

Tác vụĐược hỗ trợGhi chú
DetectionTiền xử lý letterbox tiêu chuẩn
SegmentationTiền xử lý tương tự như detection
Ước tính tư thế (Pose Estimation)Tiền xử lý tương tự như detection
Dự đoán định hướng (OBB)Tiền xử lý tương tự như detection
ClassificationSử dụng các transform của torchvision (cắt tâm), không phải letterbox

Hạn chế

  • chỉ Linux: DALI không hỗ trợ Windows hoặc macOS
  • Yêu cầu NVIDIA GPU: Không có chế độ 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ỉ nằm ở bên phải/dưới: Sử dụng fn.crop với out_of_bounds_policy="pad" để đệm vào trung tâm
  • 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 auto=True tạo ra đầu ra có kích thước thay đổ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 linh hoạt, một pipeline DALI kích thước cố định kết hợp tốt nhất với 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 ra 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 (FAQ)

Tiền xử lý DALI so với tốc độ tiền xử lý CPU như thế nào?

Lợi ích phụ thuộc vào pipeline của bạn. Khi suy luận trên GPU đã nhanh với TensorRT, tiền xử lý trên CPU ở mức 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 thực hiện tiền xử lý trên GPU. Những lợi ích lớn nhất được thấy rõ với các đầu vào độ phân giải cao (1080p, 4K), các batch sizes lớn, và các hệ thống có số lõi CPU hạn chế trên mỗi GPU.

Tôi có thể sử dụng DALI với các model PyTorch (không chỉ TensorRT) không?

Có. Sử dụng DALIGenericIterator để nhận các đầu ra torch.Tensor đã được tiền xử lý, sau đó chuyển chúng đến model.predict(). Tuy nhiên, lợi ích hiệu năng lớn nhất là với các model TensorRT nơi mà suy luận đã rất nhanh và tiền xử lý trên CPU trở thành nút thắt cổ chai.

Sự khác biệt giữa fn.padfn.crop cho phần đệm?

fn.pad thêm đệm chỉ vào các cạnh phải và dưới. fn.crop với out_of_bounds_policy="pad" đặt ảnh vào giữa và thêm đệm đối xứng ở tất cả các bên, khớp với hành vi LetterBox(center=True) của Ultralytics.

DALI có tạo ra kết quả giống hệt pixel với tiền xử lý trên CPU không?

Gần như giống hệt. Đặt antialias=False trong fn.resize để khớp với cv2.INTER_LINEAR của OpenCV. Những sai biệt nhỏ về dấu phẩy động (< 0,001) có thể xảy ra do số học giữa GPU và CPU, nhưng những khác biệt này không có tác động đáng kể nào đến việc dự đoán accuracy.

Còn CV-CUDA như một lựa chọn thay thế cho DALI thì sao?

CV-CUDA là một thư viện khác của NVIDIA dành cho xử lý thị giác tăng tốc bằng GPU. Nó cung cấp quyền kiểm soát theo từng toán tử (như OpenCV nhưng trên GPU) thay vì cách tiếp cận theo pipeline của DALI. cvcuda.copymakeborder() của CV-CUDA hỗ trợ đệm rõ ràng theo 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 quy trình công việc dựa trên pipeline (đặc biệt là 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