- vừa được xem lúc

Ensemble NVIDIA DALI Backend để xử lý dữ liệu trong Triton Inference Server ?

0 0 1

Người đăng: Duong Quang Minh B

Theo Viblo Asia

Overview

Trong các bài viết trước đây, mình đã lần lượt giới thiệu đến các bạn hai công cụ cho quá trình tăng tốc xử lý dữ liệu và triển khai mô hình Deep Learning là NVIDIA DALI và Triton Inference Server. Mình xin phép nhắc lại thông tin cơ bản một chút.

NVIDIA DALI

Với NVIDIA DALI (Data Loading Library), mình đã trình bày các khái niệm nền tảng về thư viện này với mục đích tối ưu hóa khâu tiền xử lý dữ liệu – vốn là điểm nghẽn phổ biến khi huấn luyện mô hình deep learning. DALI cho phép chuyển phần lớn công việc xử lý dữ liệu từ CPU sang GPU, giúp tăng tốc độ tải và biến đổi dữ liệu đầu vào. Ngoài ra, mình cũng đã thử xây dựng một pipeline đơn giản với DALI, sử dụng các phép biến đổi dữ liệu như resize, normalize,... và thử nghiệm hiệu năng vượt trội của nó khi so sánh với cách truyền thống thông qua Pytorch Dataloader.

Nguồn hình ảnh: NVIDIA DALI image.png

Triton Inference Server

Tiếp theo, với Triton Inference Server, mình đã chia sẻ thông tin tổng quan về Triton – một nền tảng inference mạnh mẽ do NVIDIA phát triển, hỗ trợ nhiều backend như TensorFlow, PyTorch, ONNX, TensorRT,... Mình cũng đã thử cấu hình pipeline để phục vụ inference thông qua Triton, bao gồm cách tổ chức file model repository, cấu hình config.pbtxt, và thực hiện các request inference thông qua HTTP/gRPC. Ngoài ra, mình cũng đã đo lường hiệu năng xử lý khi triển khai mô hình trên Triton bằng Triton Model Analyzer.

Nguồn hình ảnh: Triton Documentation

Screenshot from 2025-05-26 09-35-30.png

Introduction

Sau khi đã có thông tin nền tảng về tiền xử lý dữ liệu hiệu quả với DALI và triển khai mô hình inference với Triton, bài viết lần này sẽ đi thử vào việc tích hợp NVIDIA DALI và Triton Inference Server trong một hệ thống inference bằng cách nhúng trực tiếp DALI vào pipeline inference trong Triton – không chỉ để tiết kiệm thời gian tiền xử lý ở phía client mà còn để tạo ra một pipeline inference trên GPU.

Trong bài viết về Triton Tutorial, mình đã ensemble 2 models bao gồm model cho classification và segmentation trong cùng một pipeline, bởi vậy trong bài viết này, mình sẽ nghịch thêm về ensemble models trong Triton bằng việc kết hợp với DALI backend để tạo ra một inference pipeline hoàn chỉnh nhé.

Để biết thêm các thông tin khác hoặc cách thức triển khai với Triton Inference Server hay NVIDIA DALI, các bạn có thể đọc qua bài viết cũ của mình nhé :v :

Installation

Để bắt đầu quá trình thử nghiệm thì việc cài đặt môi trường luôn là bước đầu tiên rồi, ta sẽ sử dụng ngôn ngữ Python, môi trường thì bạn hoàn toàn có thể sử dụng conda hoặc pip, ở đây mình sẽ dùng conda:

conda create -n triton_dali python=3.11 -y
conda activate triton_dali

Tiếp theo bạn cần cài 1 số thư viện quan trọng phục vụ cho bài viết lần này, các bạn có thể cài thêm 1 số thư viện bổ sung tùy theo hoàn cảnh:

# CUDA 11.0
pip install nvidia-dali-cuda110 # CUDA 12.0
pip install nvidia-dali-cuda120 # Triton Inference
pip install tritonclient[all]

Đối với Triton Inference Server, cách dễ nhất để cài đặt là spre-built Docker image có trên NVIDIA GPU Cloud (NGC). Chắc hẳn các bạn vẫn nhớ bước đầu tiên cần làm là gì, còn nếu không nhớ thì đó là khởi tạo model repository bao gồm chuẩn bị model và file config.pbtxt. Các bạn có thể tự chuẩn bị sẵn model trong Model Repository. Bài viết này của mình chỉ mang tính tham khảo :v .

Model Preparation

NVIDIA DALI

DALI data pipeline được thể hiện trong Triton dưới dạng Model. Để tạo Model, bạn phải tạo một DALI Pipeline trong Python. Sau đó, bạn phải serialize (bằng cách gọi phương thức Pipeline.serialize) hoặc sử dụng Autoserialization để tạo tệp Model. Ở đây mình sẽ tạo 1 pipeline cơ bản, khá tương đồng với ví dụ trong bài viết trước:

import nvidia.dali as dali
import nvidia.dali.fn as fn
from nvidia.dali import types @dali.pipeline_def(batch_size=2, num_threads=2, device_id=0)
def pipe(): images = dali.fn.external_source(device="cpu", name="DALI_INPUT_0") images = dali.fn.decoders.image(images, device="mixed") images = dali.fn.resize(images, resize_x=224, resize_y=224) # Brightness Contrast images = fn.brightness_contrast( images, brightness=fn.random.uniform(range=(0.8, 1.2)), contrast=fn.random.uniform(range=(0.8, 1.2)) ) # Resize images = fn.resize(images, resize_x=768, resize_y=768) # Normalization images = fn.crop_mirror_normalize( images, dtype=types.FLOAT, output_layout="CHW", crop=(768, 768), mean=[0.485 * 255, 0.456 * 255, 0.406 * 255], std=[0.229 * 255, 0.224 * 255, 0.225 * 255], ) return images pipe = pipe()
pipe.serialize(filename="triton/model_repository/dali/1/model.dali")

Giải thích một chút, khi chạy đoạn code trên, dali model sẽ được serialize vào file model.dali phục vụ cho việc serving trong Triton. Còn về cấu trúc Model Repository mình sẽ để ở dưới. Tiếp theo, ta sẽ cần tạo 1 file config.pbtxt như sau

 name: "dali" backend: "dali" max_batch_size: 2 input [ { name: "DALI_INPUT_0" data_type: TYPE_UINT8 dims: [ -1 ] } ] output [ { name: "DALI_OUTPUT_0" data_type: TYPE_FP32 dims: [ 3, 768, 768 ] } ]

Ngoài cách tạo thủ công như trên, NVIDIA DALi cũng hỗ trợ kiểu Configuration auto-completePartial configuration. Để đơn giản hóa việc triển khai mô hình, Triton Server có thể infer các phần của file config từ chính file mô hình. Trong trường hợp DALI backend, thông tin về input, output, batch size có thể được chỉ định trong pipeline definition và không cần phải lặp lại trong tệp cấu hình. Dưới đây, bạn có thể xem cách đưa thông tin cấu hình vào định nghĩa Python pipeline, ví dụ:

import nvidia.dali as dali
from nvidia.dali.plugin.triton import autoserialize
import nvidia.dali.types as types @autoserialize
@dali.pipeline_def(batch_size=256, num_threads=4, device_id=0, output_dtype=[types.UINT8], output_ndim=[3])
def pipe(): images = dali.fn.external_source(device="cpu", name="DALI_INPUT_0", dtype=types.UINT8, ndim=1) images = dali.fn.image_decoder(images, device="mixed") images = dali.fn.resize(images, resize_x=768, resize_y=768) return images

Đối số dtypendim được thêm vào từ external source operation. Chúng cung cấp thông tin cần thiết để điền vào trường inputs trong file config. Để điền vào trường outputs, ta đã thêm các đối số output_dtypeoutput_ndim vào định nghĩa pipeline. Theo cách này, ta có thể giới hạn file config.pbtxt như sau:

name: "dali"
backend: "dali"

Ta có thể cung cấp một số thông tin nếu nó không có trong định nghĩa pipeline hoặc để ghi đè một số giá trị. Ví dụ:

 name: "dali" backend: "dali" max_batch_size: 2 input [ { name: "DALI_INPUT_0" data_type: TYPE_UINT8 dims: [ -1 ] } ] output [ { name: "DALI_OUTPUT_0" data_type: TYPE_FP32 dims: [ 3, 768, 768 ] } ]

Autoserialization

Ngoài ra, một cách chuẩn để biểu diễn model là serialized DALI model file vào đó và đặt tên tệp model.dali. Tính năng Autoserialization cho phép người dùng biểu diễn mô hình bằng mã Python trong Model Repository. Với mô hình do Python định nghĩa, DALI Backend sử dụng nternal serialization mechanism và miễn người dùng khỏi việc manual serialization.

Để sử dụng tính năng Autoserialization, ta cần đặt Python definition về DALI pipeline bên trong model file (model.dali theo mặc định, nhưng tên tệp mặc định có thể được định cấu hình trong config.pbtxt). Pipeline definition cần được decorator bằng @autoserialize, ví dụ:

import nvidia.dali as dali @dali.plugin.triton.autoserialize
@dali.pipeline_def(batch_size=3, num_threads=1, device_id=0)
def pipe(): ''' An identity pipeline with autoserialization enabled ''' data = dali.fn.external_source(device="cpu", name="DALI_INPUT_0") return data

Chi tiết về DALI Backend Autoserialization và precedence khi khởi tạo model bạn có thể đọc thêm trong đây: Autoserializarion

TorchScript Model

Mình sẽ tận dụng các file config.pbtxt cũng như model Classification và Segmentation từ bài viết trước của mình Triton Tutorial để đỡ phải viết lại, mình sẽ cung cấp lại 2 file model.pbtxt cho 2 model trên nhé:

Đây là config cho model Classification:

name: "resnet34"
platform: "pytorch_libtorch"
max_batch_size : 2
input [ { name: "input_image_resnet34" data_type: TYPE_FP32 dims: [ 3, 768, 768 ] }
]
output [ { name: "CLASSIFICATION_OUTPUT" data_type: TYPE_FP32 dims: [ -1 ] }
] instance_group [ { count: 1 kind: KIND_GPU }
] dynamic_batching {}

Còn đây là config cho model Segmentation:

name: "unet34"
platform: "pytorch_libtorch"
max_batch_size : 2
input [ { name: "input_image_unet34" data_type: TYPE_FP32 dims: [ 3, 768, 768 ] }
]
output [ { name: "SEGMENTATION_OUTPUT" data_type: TYPE_FP32 dims: [ 3, 768, 768 ] }
]
instance_group [ { count: 1 kind: KIND_GPU }
] dynamic_batching {} 

Model Repository

Model repository là thư mục nơi chưa các mô hình để serving. Cũng giống như bài Triton Tutorial lần trước, mình sẽ tái sử dụng 2 model là resnet34 và Unet cho 2 tác vụ phân loại và segment cho bài toán xác định tàu thuyển từ ảnh, nên lưu ý rằng các mô hình cần được convert thành dạng TorchScript nếu platform được sử dụng là Pytorch (hoặc dạng khác với backend khác). Dưới đây là cấu trúc của folder model_repository và một ví dụ của file config.pbtxt cho model unet:

# model_repository structure
model_repository
├── ensemble_model
│ ├── 1
│ └── config.pbtxt
├── resnet34
│ ├── 1
│ │ └── model.pt
│ └── config.pbtxt
├── dali
│ ├── 1
│ │ └── model.dali
│ └── config.pbtxt
└── unet34 ├── 1 │ └── model.pt └── config.pbtxt

Ngoài các file config.pbtxt mình cung cấp ở trên cho các model riêng biệt, trong bài viết lần này, mình sẽ ensemble cả NVIDIA DALI vào trong ensemble sẵn có từ trước nên mình sẽ phải modify lại file model_repository/ensemble_model/config.pbtxt một chút như dưới đây:

name: "ensemble_model"
platform: "ensemble"
max_batch_size: 2
input [ { name: "IMAGE" data_type: TYPE_UINT8 dims: [ -1 ] }
]
output [ { name: "CLASSIFICATION" data_type: TYPE_FP32 dims: [ -1 ] }, { name: "SEGMENTATION" data_type: TYPE_FP32 dims: [ 3, 768, 768 ] }
]
ensemble_scheduling { step [ { model_name: "dali" model_version: -1 input_map { key: "DALI_INPUT_0" value: "IMAGE" } output_map { key: "DALI_OUTPUT_0" value: "preprocessed_image" } }, { model_name: "resnet34" model_version: -1 input_map { key: "input_image_resnet34" value: "preprocessed_image" } output_map { key: "CLASSIFICATION_OUTPUT" value: "CLASSIFICATION" } }, { model_name: "unet34" model_version: -1 input_map { key: "input_image_unet34" value: "preprocessed_image" } output_map { key: "SEGMENTATION_OUTPUT" value: "SEGMENTATION" } } ]
} 

Bài viết toàn chữ nhỉ, để mình lấy 1 hình ảnh trực quan ra để mọi người nhìn cho dễ hiểu cách ensemble hoạt động. Trước tiên, đây là cách mà ta thường làm, ta sẽ xử lý dữ liệu như resize, normalize trước khi gửi inference request lên Triton Inference Server. Việc chạy các hoạt động xử lý đó bên trong Client rất tốn thời gian. Trên hết, hình ảnh decoded image làm tăng network traffic vì chúng lớn hơn encoder image. Tuy nhiên, nó lại dễ triển khai với các thư viện hiện tại.

Nguồn: Accelerate Inference

Screenshot from 2025-05-26 13-19-43.png

Ngoài ra, một cách với hiệu suất cao hơn là kịch bản triển khai preprocessing pipeline dưới dạng Triton Server backend. Trong trường hợp này, bạn có thể tận dụng các GPU đã được máy chủ sử dụng. Điều này đơn giản hóa kiến trúc của hệ thống vì tất cả các tác vụ tính toán đều được chuyển đến Triton Server, có thể dễ dàng mở rộng quy mô. Tuy nhiên, nếu ta đã train model bằng DALI pipeline, thì ta có thể tận dụng lại cho inference.

Nguồn: Accelerate Inference

Screenshot from 2025-05-26 13-30-52.png

Đây là cách mà DALI backend trở nên hữu ích. Mặc dù DALI ban đầu được thiết kế để loại bỏ preprocessing bottleneck trong quá trình training, một số tính năng cũng hữu ích trong inference , ta có thể xử lý ảnh bằng GPU DALI và gửi dữ liệu tới phần Inference.

Nguồn: Accelerate Inference

Screenshot from 2025-05-26 13-31-42.png

Lauching Triton

Như mình đã có trình bày, Triton đều có thể sử dụng trên các hệ thống có hoặc không có GPU. Cách sử dụng Triton dễ nhất là thông qua Docker. Bạn có thể bỏ tag --gpus nếu không có GPU.

docker run --gpus all --rm -p8000:8000 -p8001:8001 -p8002:8002 -v ${PWD}/model_repository:/models nvcr.io/nvidia/tritonserver:23.12-py3 tritonserver --model-repository=/models

Từ phiên bản tritonserver:20.11-py3, DALI Backend đã nằm trong Triton Server Docker container, ta chỉ cần download các phiên bản sau là được. Ngoài ra DALI, Triton Server, và DALI backend for Triton Server cũng được open-source nên các bạn cũng có thể build from source :v .

Nếu Docker images không tồn tại trên local, Triton image sẽ tự động được pull. Sau khi chạy câu docker run trên thì ta có thể check logs từ Triton để kiểm tra xem model đã được đẩy lên và READY chưa, ngoài ra có thể check các protocol sevice port, các config của triton container. Dựa trên logs mà ta có thể kiểm tra lại vấn đề nếu như model chưa được READY,

+----------------+---------+--------+
| Model | Version | Status |
+----------------+---------+--------+
| dali | 1 | READY |
| ensemble_model | 1 | READY |
| resnet34 | 1 | READY |
| unet34 | 1 | READY |
+----------------+---------+--------+

Các port cũng được expose như sau:

I1102 04:11:22.028262 1 grpc_server.cc:4819] Started GRPCInferenceService at 0.0.0.0:8001
I1102 04:11:22.028982 1 http_server.cc:3477] Started HTTPService at 0.0.0.0:8000
I1102 04:11:22.070508 1 http_server.cc:184] Started Metrics Service at 0.0.0.0:8002
W1102 04:11:23.023508 1 metrics.cc:603] Unable to get power limit for GPU 0. Status:Success, value:0.000000

Inference

Vậy là đã xong các bước khởi tạo model, config, và đã chạy thành công Triton Server rồi. Vậy ta hãy thử inference xem kết quả như nào. Tác vụ thì y hệt như bài Triton Tutorial trước nhé là phân loại, phân đoạn thuyền. Trước tiên ta cứ import thư viện trước:

import cv2
import torch
import numpy as np
import tritonclient.http as httpclient def load_image(img_path: str): return np.expand_dims(np.fromfile(img_path, dtype='uint8'), axis=0)

Sau đó, ta sẽ khởi tạo Triton Client cùng với input và output cho ensembles model:

# Set up Client
client = httpclient.InferenceServerClient(url="0.0.0.0:8000") # Inputs
image_input = load_image('ship/00a3ab3cc.jpg')
inputs = httpclient.InferInput("IMAGE", image_input.shape, datatype="UINT8")
inputs.set_data_from_numpy(image_input) # Outputs
outputs = []
outputs.append(httpclient.InferRequestedOutput("CLASSIFICATION"))
outputs.append(httpclient.InferRequestedOutput("SEGMENTATION"))

Và cuối cùng là mình inference và nhận kết quả thôi:

# Inference
results = client.infer(model_name="ensemble_model", inputs=[inputs], outputs=outputs) # RESNET MODEL
inference_output_resnet = results.as_numpy('CLASSIFICATION')
# UNET MODEL
inference_output_unet = results.as_numpy('SEGMENTATION').squeeze(0)

Và đây là kết quả minh họa sau khi postprocess mask của model segmentation:

Input Predicted
00a3ab3cc.jpg image.png

Summary

Trong bài viết này, chúng ta đã thử kết hợp NVIDIA DALI với Triton Inference Server để xây dựng một pipeline inference , nơi cả quá trình tiền xử lý dữ liệu và suy luận mô hình đều được thực hiện hiệu quả trên GPU. Bài viết mang tính chất tham khảo và cơ bản, tùy thuộc vào bài toán mà mọi người có thể custom thêm cho phù hợp nhé.

Reference

Bình luận

Bài viết tương tự

- vừa được xem lúc

GPU programming với Golang

GPU programming với Golang. Ở bài trước mình có giới thiệu về kĩ thuật lập trình GPU với OpenCL bằng C/C++.

0 0 34

- vừa được xem lúc

Allocating Memory on HPC ( Slurm Scripts)

Bài viết này giải thích cách yêu cầu bộ nhớ trong các Slurm Scripts và cách xử lý các lỗi thường gặp liên quan đến bộ nhớ CPU và GPU. Lưu ý rằng "memory" luôn đề cập đến RAM .

0 0 35

- vừa được xem lúc

[Lập trình song song] Bài 1: Giới thiệu về CPU-GPU

Trước khi tìm hiểu thế nào là lập trình song song cũng như cách code thì mình phải biết 1 chút về lịch sử hình thành nên ở bài 1 mình sẽ giới thiệu sơ lược những điều bạn nên biết ở lĩnh vực này. Chắc

0 0 32

- vừa được xem lúc

[Lập trình song song] Bài 2: Cài đặt môi trường code CudaC

Trước khi code thì chúng ta phải setup môi trường để code thì ở bài này mình sẽ hướng dẫn các bạn cách setup và đối với những ai sở hữu máy tính mà không có GPU thì cũng đừng có lo vì chúng ta sẽ code

0 0 25

- vừa được xem lúc

[Lập trình song song] Bài bonus 1: Cách thức hoạt động của máy tính

Ở bài này mình sẽ nói qua về cách máy tính hoạt động trong việc lấy và xử lí data qua ví dụ cực kì trực quan và dễ hiểu . Và xin lưu ý là ví dụ này sẽ được nhắc lại khá nhiều trong các bài học về lập

0 0 23

- vừa được xem lúc

[Lập trình song song] Bài bonus 2: Các thuật ngữ trong lập trình song song

Ở bài này mình sẽ giải thích các thuật ngữ thường hay được đề cập tới trong lập trình song song. .

0 0 32