Triển khai NVIDIA Triton Inference Server trên Docker: Multi-Model Serving & Dynamic Batching

AI tutorial - IT technology blog
AI tutorial - IT technology blog

Bối cảnh & Lý do

Khởi chạy một container FastAPI hoặc Flask với endpoint /predict là lựa chọn ổn cho các bản prototype xử lý từ 5 đến 10 request mỗi phút. Nhưng khi đưa cấu hình đó vào môi trường production với một pipeline đa phương thức (multimodal)—chẳng hạn mô hình thị giác ResNet-50 chạy song song với bộ phân loại RoBERTa—các microservice viết bằng Python sẽ nhanh chóng quá tải khi lưu lượng tăng cao.

Cơ chế Global Interpreter Lock (GIL) của Python, độ trễ tuần tự hóa JSON và việc thiếu cơ chế quản lý hàng đợi GPU native khiến hiệu năng phần cứng bị suy giảm nghiêm trọng. Trên một instance NVIDIA A10G hoặc T4, mức sử dụng GPU (GPU utilization) thường chỉ lẹt đẹt ở mức đáng thất vọng từ 15% đến 20%. Khi các request đồng thời gửi tới các web worker thông thường, chúng sẽ được thực thi tuần tự. Các Tensor Core đắt đỏ rơi vào tình trạng đói dữ liệu trong lúc chờ copy bộ nhớ host-to-device qua PCIe, đẩy độ trễ p99 từ 15ms vọt lên hơn 600ms.

NVIDIA Triton Inference Server xóa bỏ nút thắt cổ chai này nhờ vào lõi C++ cho thông lượng cực cao. Triton đóng vai trò như một gateway suy luận (inference gateway) thống nhất trên khắp TensorRT, ONNX Runtime, PyTorch và OpenVINO mà không đòi hỏi các Python wrapper tùy biến.

Khả năng nổi bật nhất của Triton là dynamic batching. Thay vì xử lý từng request đơn lẻ ngay khi chúng đến, Triton gom các payload riêng lẻ đến trong một khoảng thời gian microsecond thành một batch duy nhất trên GPU. Các client gửi truy vấn đến API hoàn toàn độc lập, nhưng GPU lại xử lý chúng cùng một lúc.

Trong các bài benchmark trên GPU NVIDIA L4, việc chuyển đổi từ stack Uvicorn/FastAPI sang Triton tích hợp dynamic batching thường giúp thông lượng tăng vọt từ 120 QPS lên 980 QPS, đồng thời cắt giảm độ trễ p95 khoảng 65%.

Cài đặt

Các container Triton tăng tốc bằng GPU yêu cầu GPU NVIDIA có compute capability từ 6.0 trở lên, driver tương thích trên host (CUDA 12+), Docker Engine và NVIDIA Container Toolkit.

1. Kiểm tra NVIDIA Drivers và Container Toolkit

Xác minh hệ điều hành host giao tiếp trực tiếp với GPU của bạn:

nvidia-smi

Tiếp theo, xác nhận Docker có thể truy cập GPU thông qua runtime NVIDIA Container Toolkit:

docker run --rm --gpus all nvidia/cuda:12.2.0-base-ubuntu22.04 nvidia-smi

Nếu thành công, bảng trạng thái GPU sẽ được in ra trực tiếp từ môi trường container.

2. Kéo image Docker chính thức của Triton

NVIDIA phát hành các image Triton sẵn sàng sử dụng trên catalog công khai của NGC. Bạn không cần tài khoản hay API key để kéo các bản phát hành tiêu chuẩn:

docker pull nvcr.io/nvidia/tritonserver:24.08-py3

Server sẽ lắng nghe trên ba cổng mạng mặc định:

  • 8000: API suy luận và quản trị qua HTTP/REST
  • 8001: API suy luận qua gRPC (khuyên dùng cho các pipeline production cần độ trễ thấp)
  • 8002: Endpoint thu thập Prometheus metrics

Cấu hình

Triton phát hiện và quản lý các mô hình thông qua cấu trúc thư mục gọi là Model Repository. Khác với các script Python sử dụng đường dẫn file cứng, Triton tự động giám sát cấu trúc repository này để nhận diện file binary và metadata của mô hình.

1. Thiết lập cấu trúc Model Repository

Tạo cấu trúc thư mục cho hai mô hình riêng biệt—ResNet-50 cho thị giác máy tính và một bộ phân loại văn bản cho NLP:

mkdir -p model_repository/resnet50_onnx/1
mkdir -p model_repository/text_classifier_onnx/1

Mỗi thư mục mô hình đều yêu cầu các thư mục con được đánh số theo phiên bản cùng với một file schema config.pbtxt:

model_repository/
├── resnet50_onnx/
│   ├── 1/
│   │   └── model.onnx
│   └── config.pbtxt
└── text_classifier_onnx/
    ├── 1/
    │   └── model.onnx
    └── config.pbtxt

2. Cấu hình ResNet50 với Dynamic Batching

Viết cấu hình cho model_repository/resnet50_onnx/config.pbtxt:

name: "resnet50_onnx"
platform: "onnxruntime_onnx"
max_batch_size: 64

input [
  {
    name: "input"
    data_type: TYPE_FP32
    dims: [ 3, 224, 224 ]
  }
]
output [
  {
    name: "output"
    data_type: TYPE_FP32
    dims: [ 1000 ]
  }
]

dynamic_batching {
  preferred_batch_size: [ 8, 16, 32, 64 ]
  max_queue_delay_microseconds: 5000
}

instance_group [
  {
    count: 2
    kind: KIND_GPU
    gpus: [ 0 ]
  }
]

Ý nghĩa và vai trò của từng tham số cấu hình:

  • max_batch_size: 64: Kích hoạt dynamic scheduler của Triton. Triton sẽ tự động coi chiều đầu tiên của mọi tensor đầu vào là trục batch (batch axis).
  • dynamic_batching: Chỉ định các ngưỡng gom batch mục tiêu. Triton tích lũy các request đến trong tối đa 5.000 microsecond (5ms). Nếu gom đủ 8 hoặc 16 request chỉ trong 1.5ms, nó sẽ gửi đi xử lý ngay lập tức mà không cần chờ hết toàn bộ khoảng thời gian.
  • instance_group: Khởi chạy hai execution instance song song của mô hình trên GPU 0. Trong khi instance 1 thực thi các compute kernel trên Tensor Core, instance 2 có thể truyền các tensor hình ảnh mới qua PCIe.

3. Cấu hình mô hình thứ hai (Text Classifier)

Tiếp theo, thêm cấu hình cho model_repository/text_classifier_onnx/config.pbtxt:

name: "text_classifier_onnx"
platform: "onnxruntime_onnx"
max_batch_size: 32

input [
  {
    name: "input_ids"
    data_type: TYPE_INT64
    dims: [ 128 ]
  },
  {
    name: "attention_mask"
    data_type: TYPE_INT64
    dims: [ 128 ]
  }
]
output [
  {
    name: "probabilities"
    data_type: TYPE_FP32
    dims: [ 2 ]
  }
]

dynamic_batching {
  preferred_batch_size: [ 4, 8, 16, 32 ]
  max_queue_delay_microseconds: 2000
}

instance_group [
  {
    count: 1
    kind: KIND_GPU
    gpus: [ 0 ]
  }
]

Scheduler của Triton xử lý đồng thời hàng đợi request cho cả hai mô hình trên cùng một GPU. Cơ chế này chuyển đổi execution context chỉ trong vài microsecond, loại bỏ hoàn toàn overhead khi phải chạy nhiều container riêng biệt.

4. Khởi chạy Triton bằng Docker

Mount repository và khởi chạy Triton ở chế độ chạy nền (detached mode):

docker run -d --name triton-server \
  --gpus all \
  --shm-size=1g \
  -p 8000:8000 \
  -p 8001:8001 \
  -p 8002:8002 \
  -v $(pwd)/model_repository:/models \
  nvcr.io/nvidia/tritonserver:24.08-py3 \
  tritonserver --model-repository=/models --strict-model-config=true

Hãy đặc biệt lưu ý tham số --shm-size=1g. Docker mặc định chỉ cấp phát mức shared memory rất nhỏ là 64MB. Triton dựa vào POSIX shared memory để xử lý IPC nhanh; việc thiếu dung lượng này sẽ gây ra lỗi crash SIGBUS âm thầm khi chịu tải đồng thời cao.

Kiểm thử & Giám sát

Xác nhận cả hai mô hình đã được tải thành công và kiểm tra hoạt động suy luận trước khi định tuyến lưu lượng truy cập thực tế từ người dùng.

1. Kiểm tra trạng thái Server và Mô hình

Kiểm tra trực tiếp log khởi tạo của container:

docker logs -f triton-server

Màn hình console sẽ in ra một bảng ASCII xác nhận cả resnet50_onnx và text_classifier_onnx đều đang ở trạng thái READY. Bạn cũng có thể kiểm tra trạng thái hoạt động tự động qua HTTP:

# Kiểm tra trạng thái hoạt động của server
curl -v http://localhost:8000/v2/health/live

# Kiểm tra trạng thái sẵn sàng của mô hình ResNet50
curl -v http://localhost:8000/v2/models/resnet50_onnx/ready

Cả hai lệnh gọi đều phải trả về mã trạng thái HTTP 200 OK.

2. Thực thi thử nghiệm suy luận bằng Python

Cài đặt thư viện client chính thức trong môi trường Python cục bộ của bạn:

pip install tritonclient[http] numpy

Chạy script Python này để gửi một payload mẫu:

import numpy as np
import tritonclient.http as httpclient

client = httpclient.InferenceServerClient(url="localhost:8000")

# Chuẩn bị tensor ảnh giả lập đơn lẻ (shape: 1 x 3 x 224 x 224)
input_data = np.random.randn(1, 3, 224, 224).astype(np.float32)

infer_input = httpclient.InferInput("input", input_data.shape, "FP32")
infer_input.set_data_from_numpy(input_data)

infer_output = httpclient.InferRequestedOutput("output")

response = client.infer(
    model_name="resnet50_onnx",
    inputs=[infer_input],
    outputs=[infer_output]
)

output_tensor = response.as_numpy("output")
print(f"Inference hoàn tất. Kích thước tensor phản hồi: {output_tensor.shape}")

Server nhận tensor đơn lẻ kích thước [1, 3, 224, 224] và trả về xác suất phân loại (logits) cho toàn bộ 1.000 danh mục ImageNet.

3. Giám sát hiệu năng với Prometheus Metrics

Triton cung cấp sẵn các metric định dạng Prometheus trên cổng 8002:

curl http://localhost:8002/metrics

Tích hợp các chỉ số telemetry quan trọng này vào dashboard Prometheus và Grafana:

  • nv_inference_request_duration_us: Theo dõi tổng độ trễ của request, phân tách chi tiết thành thời gian chờ hàng đợi và thời gian tính toán thực tế. Nếu thời gian chờ hàng đợi tăng đột biến trong khi tải GPU vẫn dưới 40%, hãy giảm giá trị max_queue_delay_microseconds.
  • nv_inference_request_success: Ghi nhận số lượng request suy luận thành công, phân loại chi tiết theo tên mô hình và phiên bản.
  • nv_gpu_utilization và nv_gpu_memory_used_bytes: Giám sát mức độ hoạt động của GPU core và lượng VRAM tiêu thụ trong thời gian thực.

Vận hành Triton trên Docker giúp thu hẹp khoảng cách giữa code machine learning thử nghiệm và hạ tầng production chuẩn chỉnh. Khi được cấu hình dynamic batching và instance groups hợp lý, một GPU đơn lẻ hoàn toàn có thể xử lý mượt mà hàng trăm request đồng thời mà không bị sụt khung hình hay vượt ngưỡng độ trễ cho phép.

Share: