Docker上でNVIDIA Triton Inference Serverをデプロイ:複数モデルの配信と動的バッチ処理

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

背景と導入の理由

毎分5〜10件程度のリクエストを処理するプロトタイプであれば、FastAPIやFlaskのコンテナを立ち上げて/predictエンドポイントを設けるだけでも十分です。しかし、ResNet-50の画像認識モデルとRoBERTaのテキスト分類モデルを組み合わせたマルチモーダルパイプラインなどを本番環境に投入すると、Pythonベースのマイクロサービスは高負荷に耐えきれず破綻してしまいます。

Pythonのグローバルインタプリタロック(GIL)、JSONシリアライズのオーバーヘッド、そしてネイティブなGPUキュー管理の欠如は、ハードウェア効率を著しく損ないます。NVIDIA A10GやT4インスタンスを使用しても、GPU使用率は15%〜20%という低水準にとどまるケースが少なくありません。一般的なWebワーカーに同時リクエストが殺到すると順次処理され、高価なTensor CoreはPCIeを介したホスト・デバイス間メモリコピーの待機で遊休状態(starve)となり、結果としてp99レイテンシが15msから600ms超へと急上昇してしまいます。

NVIDIA Triton Inference Serverは、高スループットなC++コアによってこのボトルネックを解消します。TritonはカスタムのPythonラッパーを必要とせず、TensorRT、ONNX Runtime、PyTorch、OpenVINOを横断する統合推論ゲートウェイとして機能します。

特筆すべき機能が動的バッチ処理(Dynamic Batching)です。リクエストが到着するたびに1件ずつ処理するのではなく、マイクロ秒単位の時間枠内で個々の受信ペイロードを集約し、GPU上で単一のバッチにまとめます。クライアントは個別にAPIを呼び出しますが、GPUはそれらを一括で効率的に処理します。

NVIDIA L4 GPUでのベンチマークでは、Uvicorn/FastAPIスタックから動的バッチ処理を有効にしたTritonへ移行することで、スループットが120 QPSから980 QPSへと大幅に向上し、p95レイテンシが約65%削減されることが実証されています。

インストール

GPUアクセラレーションを有効にしたTritonコンテナを実行するには、Compute Capability 6.0以上のNVIDIA GPU、互換性のあるホストドライバ(CUDA 12以降)、Docker Engine、およびNVIDIA Container Toolkitが必要です。

1. NVIDIAドライバとContainer Toolkitの確認

ホストOSがGPUと正常に通信できているか確認します。

nvidia-smi

次に、NVIDIA Container Toolkitランタイムを介してDockerがGPUにアクセスできることを確認します。

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

実行が成功すると、コンテナ環境から直接GPUステータステーブルが出力されます。

2. 公式Triton Dockerイメージのプル

NVIDIAは公開のNGCカタログ上で、すぐに実行可能なTritonイメージを提供しています。標準リリースのプルにアカウントやAPIキーは不要です。

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

サーバーはデフォルトで次の3つのネットワークポートをリッスンします。

  • 8000: HTTP/REST推論および管理API
  • 8001: gRPC推論API(低遅延が求められる本番パイプラインで推奨)
  • 8002: Prometheusメトリクスエンドポイント

設定

Tritonは、モデルリポジトリ(Model Repository)と呼ばれるディレクトリーツリーを通じてモデルを検出し、管理します。ファイルシステムパスをハードコードしたPythonスクリプトとは異なり、Tritonはこのリポジトリ構造を動的に監視してモデルバイナリとメタデータを読み込みます。

1. モデルリポジトリ構造のセットアップ

画像認識用のResNet-50とNLP用のテキスト分類モデルという、2つの個別モデル用のディレクトリ構造を作成します。

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

各モデルフォルダには、数値で採番されたバージョンサブディレクトリとconfig.pbtxtスキーマが必要です。

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

2. 動的バッチ処理を適用したResNet50の設定

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 ]
  }
]

各パラメータの役割は以下のとおりです。

  • max_batch_size: 64: Tritonの動的スケジューラを有効化します。Tritonはすべての入力テンソルの第1次元をバッチ軸として自動的に扱います。
  • dynamic_batching: バッチ生成の目標条件を指定します。Tritonは最大5,000マイクロ秒(5ms)の間、受信リクエストを収集します。仮に1.5msの間に8件または16件のリクエストが到着した場合、時間枠の上限を待たずに即座に実行キューへ投入します。
  • instance_group: GPU 0上で2つのモデル実行インスタンスを並行起動します。インスタンス1がTensor Coreで計算カーネルを実行している間に、インスタンス2がPCIe経由で次の画像テンソルを転送できます。

3. 2つ目のモデル(テキスト分類)の設定

続いて、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 ]
  }
]

Tritonのスケジューラは、単一のGPU上で両モデルのリクエストキューを並行して処理します。コンテキストスイッチはマイクロ秒単位で行われるため、個別にコンテナを立ち上げるようなオーバーヘッドを回避できます。

4. Docker経由でのTritonの起動

リポジトリをマウントし、デタッチドモードでTritonを実行します。

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

--shm-size=1gの指定には特に注意してください。Dockerのデフォルトの共有メモリ割り当てはわずか64MBです。Tritonは高速なプロセス間通信(IPC)にPOSIX共有メモリを利用しているため、メモリが不足すると高負荷・高並列時に通知なしでSIGBUSクラッシュを引き起こす原因になります。

動作検証とモニタリング

本番のユーザートラフィックをルーティングする前に、両方のモデルが正常にロードされたことを確認し、推論動作を検証します。

1. サーバーのヘルスチェックとモデルステータスの確認

コンテナの初期化ログを直接確認します。

docker logs -f triton-server

コンソールにASCIIテーブルが出力され、resnet50_onnxとtext_classifier_onnxの双方がREADY状態であることが確認できます。HTTP経由でプログラムからヘルスチェックを行うことも可能です。

# サーバーの稼働状態(liveliness)を確認
curl -v http://localhost:8000/v2/health/live

# ResNet50モデルの準備状態(readiness)を確認
curl -v http://localhost:8000/v2/models/resnet50_onnx/ready

どちらの呼び出しもHTTPステータス200 OKを返す必要があります。

2. Pythonによるテスト推論リクエストの実行

ローカルのPython環境に公式クライアントライブラリをインストールします。

pip install tritonclient[http] numpy

以下のPythonテストスクリプトを実行して、サンプルペイロードを送信します。

import numpy as np
import tritonclient.http as httpclient

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

# 単一のダミー画像テンソルを準備(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"推論が完了しました。レスポンステンソルの形状: {output_tensor.shape}")

サーバーは単一の[1, 3, 224, 224]テンソルを受け取り、ImageNetの1,000カテゴリすべてに対するクラス確率ロジットを返します。

3. Prometheusメトリクスによるパフォーマンス監視

Tritonはポート8002でPrometheusメトリクスを標準で公開しています。

curl http://localhost:8002/metrics

以下の主要なテレメトリ指標をPrometheusおよびGrafanaダッシュボードに取り込みます。

  • nv_inference_request_duration_us: キュー待機時間と実際の計算時間に細分化された、リクエスト全体のレイテンシを追跡します。GPU負荷が40%未満のままキュー時間のみが急増している場合は、max_queue_delay_microsecondsの値を小さく調整してください。
  • nv_inference_request_success: モデル名とバージョンごとに分類された、正常に処理された推論リクエスト数を記録します。
  • nv_gpu_utilizationおよびnv_gpu_memory_used_bytes: GPUコアのアクティビティとVRAM消費量をリアルタイムで監視します。

Docker上でTritonを稼働させることで、プロトタイプ段階の機械学習コードと本番インフラストラクチャとのギャップを埋めることができます。動的バッチ処理とインスタンスグループを適切に設定すれば、単一のGPUであってもフレーム落ちやレイテンシ上限の超過を起こすことなく、数百件の同時リクエストを余裕を持って処理できるようになります。

Share: