GPU に推論を: Triton Inference Server でかんたんデプロイ

Kazuhiro Yamasaki
NVIDIA Japan
Published in
28 min readMar 4, 2021

おはようございます / こんにちは / こんばんは / お疲れさまです。エヌビディアの山崎です。みなさんディープラーニング、してますか?今回は、じわじわと認知度が上がってきている (と信じたいところである)、Triton Inference Server について解説しようと思います。一昔前は、若干とっつきにくい感のあるサービング フレームワークでしたが、ずいぶん便利になったということもあり、ぜひ一度お試しいただければ。
なおこの記事は、執筆時点の最新版である v2.6.0 を前提として構成されています。

忙しい人のためのまとめ

  • Triton Inference Server は GPU の性能を最大限引き出して、オンライン推論を高速実行できるようにするための機構を多数持つ
  • さまざまなフォーマットの学習結果をデプロイ可能
  • ディープラーニングのフレームワークだけではなく、任意の処理を実行させることも可能 (Python or C++)
  • 実は CPU だけのマシンでも動く

Triton Inference Server とは?

集約効率を最大化しつつ、高速低遅延で推論結果を返却できるよう最適化されたサービング フレームワークで、活発に開発が続けられているオープンソース プロジェクトです。
https://github.com/triton-inference-server/server
過去、TensorRT Inference Server (TRTIS) と呼ばれていたプロジェクトをご存じの方は、リネームされ、開発継続した発展版だとご理解いただいて問題ないかと思います。なお現コードネームである “Triton” は、「とりとん」ではなく「とらいとん」と呼んでいただければ幸いです。

Triton Inference Server の全体像
Triton Inference Server の全体像

Triton には、GPU の利用率を最大化しながら大量アクセスを上手に捌くために、リクエストキューを利用して動的にバッチ構築する機構が実装されていたり、ひとつの GPU 上で複数のモデルや単一モデルのコピーを並列で動作させられるなど、自前実装しようとすると面倒な機能が大量に実装されています (そのほか、GPU が複数枚搭載されているサーバ向けの機構もありますが今回は割愛)。 ちょっとした検証やデモのために使うもよし、本番の高負荷な環境に投入するもよし、という地味に便利なサービング フレームワークです。

今回は Triton の使い方の中でも、最も基本的かつ典型的と思われるユースケースに絞って説明をします。より発展的な使い方などについては、のちのち記事化したいと思います。もし「こういうことって可能なの?」などあれば、お気軽にコメント等いただければ幸いです。

いちばん基本の使い方

なにはともあれ、一度動かしてみましょう。典型的には以下の流れで、学習済みモデルをデプロイします。

  • 学習済みモデルを、サーバ上にコピー
  • 設定ファイルを書く
  • Triton を起動
  • クライアント側を実装
  • 完!


……
………
…………
これだけではなんのことやら、という感じですので、それぞれ簡単に確認していきます。

まず、学習済みモデルをサーバ上に配置します。このとき、ディレクトリ構造は以下のように、一定の規約に則って作成する点に注意してください。

models/
├── modelA/
│ ├── 1/
│ │ └── model/
│ │ ├── saved_model.pb
│ │ └── variables/
│ │ ├── variables.data-00000-of-00001
│ │ └── variables.index
│ ├── 2/
: :
│ └── config.pbtxt

├── modelB/
: :

トップディレクトリ (models) の下に、デプロイしたいモデルごと (modelA や modelB) にディレクトリを作成します。各モデルディレクトリ内には、固定の名前の設定ファイル (config.pbtxt) とバージョンごとのディレクトリを作成します。バージョンごとのディレクトリ名は数字で、1 はじまりが基本となります。

次に設定ファイルを記述します。Protocol Buffers 形式に従って、テキストファイルとして定義します。以下に一例を示します。

name: "modelA"
platform: "tensorflow_savedmodel"
max_batch_size: 64
input [
{
name: "input_1"
data_type: TYPE_FP32
format: FORMAT_NHWC
dims: [ 224, 224, 3 ]
}
]
output [
{
name: "predictions"
data_type: TYPE_FP32
dims: [ 1000 ]
}
]
default_model_filename: "model"

各項目は以下の内容を設定しています。

  • name
    モデル名を記述。設定ファイルのあるディレクトリ名と一致させる必要があります。
  • platform
    デプロイ対象のモデル形式を記述。上記の例では、TensorFlow の SavedModel なので “tensorflow_savedmodel” としています。その他、TensorRT、ONNX Runtime、TorchScript、Python、DALI などがサポートされています。詳細は、後述のリンクから “Backends” のセクションを参照してください。
  • max_batch_size
    バッチサイズの最大値を指定。モデルの定義によって、可変バッチサイズをサポートしていないことがあるため、0 を指定する (または max_batch_size を記述しない) 場合がある点に注意してください。
  • input / output
    入出力テンソルの情報 (入出力名、データ型、次元など) を指定。入出力の名前は、TensorFlow の SavedModel は、saved_model_cli コマンドを利用すると簡単に確認できます (参考: https://www.tensorflow.org/guide/saved_model#show_command)。
    TorchScript の場合、一定の命名規則に従って、入出力に名前が自動付与されます。詳細は以下のページに記述されていますが、一例としてたとえば、”INPUT__0"、”INPUT__1"、”OUTPUT__0"、”OUTPUT__1" のように、”INPUT / OUTPUT” とインデックスを、ふたつのアンダースコアで繋いだ文字列となります。
  • default_model_filename
    デフォルトのモデル名を上書き。各 platform ごとにデフォルトのモデル名 (e.g., TensorRT の場合 “model.plan” など) は設定されていますが、別の名前でデプロイすることもできます。
    また、GPU の Compute Capability (CC) ごとにファイルを変更することもでき、その場合は ”cc_model_filenames” に対応関係を記述します。このとき、記述されなかった CC の GPU 上で Triton を動かす場合は、前述の “default_model_filename” もしくは初期設定のデフォルト名でモデルをロードしようとします。

それぞれの項目に関する詳細は、以下のリンクからたどることができます。

上記の例からも分かる通り、どことなく JSON めいた雰囲気があるので、形式自体は難しくないのですが、各機能をどのように設定内部に書くのか、という点で少し戸惑うかもしれません。以下のページに設定の詳細と、各項目の定義が書かれています。
https://github.com/triton-inference-server/server/blob/v2.6.0/docs/model_configuration.md
https://github.com/triton-inference-server/server/blob/v2.6.0/src/core/model_config.proto

(余談: https://github.com/triton-inference-server/server/blob/v2.6.0/docs/model_configuration.md#auto-generated-model-configuration には、 --strict-model-config=false を起動時オプションとして付与することで最小限の設定以外は自動推定してくれる、ということが書かれています。モデル定義等にもよりますが、設定ファイルがなくとも動かせるようになるので、まずこちらを試していただいてもよいかもしれません)

モデルファイルと設定ファイルを配置できたら、Triton の起動準備完了です。Triton 自体をソースコードからビルドしてインストールし、起動、というのも良いのですが、ここではビルド済みの docker コンテナイメージを利用します。以下のページにあるコマンドの通り、
https://github.com/triton-inference-server/server/blob/v2.6.0/docs/quickstart.md#run-triton

docker run --gpus=1 --rm -p8000:8000 -p8001:8001 -p8002:8002 \
-v /path/to/models:/models nvcr.io/nvidia/tritonserver:20.12-py3 \
tritonserver --model-repository=/models

と実行してみましょう。最終的に、

...
I0130 14:51:48.867256 1 grpc_server.cc:3979] Started GRPCInferenceService at 0.0.0.0:8001
I0130 14:51:48.867676 1 http_server.cc:2717] Started HTTPService at 0.0.0.0:8000
I0130 14:51:48.909774 1 http_server.cc:2736] Started Metrics Service at 0.0.0.0:8002

のようなログメッセージが出力されれば、起動成功です。コマンドの意味は、以下の通りです。

  • -p8000:8000 -p8001:8001 -p8002:8002
    HTTP / gRPC / metrics のそれぞれにアクセスするために、ポートをホスト側にマッピングしています。Triton の起動時オプションである --http-port、--grpc-port、 --metrics-port で、それぞれのポート番号を変更可能です。
  • -v /path/to/models:/models
    ホスト側で /path/to/models というディレクトリの下にすべてのモデルが配置されていると仮定して、それをコンテナ内の /models としてマウントしています。
  • tritonserver --model-repository=/models
    tritonserver が、Triton の実行バイナリです。 --model-repository で、コンテナ内でのモデルが格納されているディレクトリを指定して、Triton を起動しています。

なお Triton 自体のオプションを表示したい場合は、(やや面倒ですが) 例えば以下のようにすると標準出力に表示されます。

docker run --rm nvcr.io/nvidia/tritonserver:20.12-py3 tritonserver --help
...
Usage: tritonserver [options]
--help
Print usage
...

ここまで順調にサーバが起動したように見えるのですが、念のため、HTTP リクエストを送って本当に問題がないか確認してみましょう。Triton は KFServing の Predict Protocol, version 2 に準拠しており、ヘルスチェック用のエンドポイントが提供されています。
https://github.com/kubeflow/kfserving/blob/master/docs/predict-api/v2/required_api.md#httprest

たとえば、

curl -v -o - localhost:8000/v2/health/ready

のようにリクエストを投げると、

...
< HTTP/1.1 200 OK
< Content-Length: 0
< Content-Type: text/plain
<
* Connection #0 to host localhost left intact

という出力が得られます。同様に、各モデルが問題なくロードできているかどうかは、

curl -v -o - localhost:8000/v2/models/modelA/ready
* Trying 127.0.0.1...
* TCP_NODELAY set
* Connected to localhost (127.0.0.1) port 8000 (#0)
> GET /v2/models/modelA/ready HTTP/1.1
> Host: localhost:8000
> User-Agent: curl/7.60.0
> Accept: */*
>
< HTTP/1.1 200 OK
< Content-Length: 0
< Content-Type: text/plain
<
* Connection #0 to host localhost left intact

のように 200 OK が返却されることで確認できます。
(余談 2: 実際には標準以外の API も必要になるため、そうしたものは extension として別途定義されています。https://github.com/triton-inference-server/server/tree/v2.6.0/docs/protocol)

最後に、リクエストを投げる側、クライアントを実装します。Triton では HTTP もしくは gRPC を通信用のインタフェースとして提供しています。簡易に動作確認する場合は curl などのコマンドを利用したくなりますが、リクエストに乗せる JSON のフォーマットが定められているため、少々面倒です。そこでここでは、Triton で公式に提供しているクライアント ライブラリを利用することとします。
https://github.com/triton-inference-server/server/blob/v2.6.0/docs/client_libraries.md

インストールの方法には、ソースからの手動ビルド以外に、ビルド済みバイナリ、pip 経由、ビルド済み docker コンテナ イメージが存在します。今回はコンテナを利用してみます。以下のコマンドを実行すると、クライアントライブラリがインストールされたコンテナ内部で、任意の操作が可能になります。

docker run -it --rm --net=host nvcr.io/nvidia/tritonserver:20.12-py3-sdk

このコンテナには、各種サンプルコードが /workspace/src/clients/python/examples/ などに置かれていますが、ここでは、ステップ バイ ステップで利用方法を説明します。

まずサーバにリクエストを投げるためのクライアント オブジェクトは、tritonclient.http.InferenceServerClient もしくは tritonclient.grpc.InferenceServerClient を利用して、たとえば以下のように生成します。

If protocol == 'http':
import tritonclient.http as client_module
port = 8000
elif protocol == 'grpc':
import tritonclient.grpc as client_module
port = 8001
with client_module.InferenceServerClient(f'localhost:{port}') as client:

これにより client オブジェクトを介して、サーバへリクエストを投げられるようになります。次に、リクエストの入出力を定義します。それぞれ、tritonclient.http.InferInput / tritonclient.grpc.InferInput および tritonclient.http.InferRequestedOutput / tritonclient.grpc.InferRequestedOutput を使って定義します。以下はその一例です。

inputs = [
client_module.InferInput(
'input_1',
images.shape,
np_to_triton_dtype(images.dtype)),
]
inputs[0].set_data_from_numpy(images)
outputs = [
client_module.InferRequestedOutput('predictions'),
]

このとき、np_to_triton_dtype() は tritonclient.utils に定義されている関数で、images はリクエストに含めたいデータの numpy.ndarray です。推論リクエスト自体は以下の関数呼び出しにより実行されます。

response = client.infer(
'modelA',
inputs,
request_id=str(1),
outputs=outputs)

レスポンスは以下のようにすると、numpy の ndarray 形式で取得できます。

predictions = response.as_numpy('predictions')

このクライアントライブラリはこの他、

  • 非同期リクエスト
  • ヘルスチェック API
  • サーバ / モデルメタデータ API
  • モデル制御 API

など、定義済みのサーバ側インタフェースを一通り提供しています。

モデルの並列実行

ここまでの内容で、基本的なモデルの API 化が可能になりました。通常 API サーバには、多数のリクエストが送信されます。一方みなさんご存知の通り、GPU はシーケンシャルに実行するより、並列に多数の処理を実行するほうが効率的なことが多いです。素直な API サーバの実装では、到着順に GPU へ処理を流し、結果が得られたものからレスポンスを返すことになります。しかしこれでは、GPU の高い並列性能を活かしきることができません。

そこで Triton では、モデルの並列実行をサポートする仕組みを提供しています。ひとつは、ここまでで説明してきた様々なモデルを同時にデプロイできるという仕組みの延長で、デプロイ済みのモデルはいずれも並列実行されます。これに加えて、それぞれのモデルについても、設定を加えることで複数のコピーを並列に実行できるようになります。これによって、大量のリクエストが送信された場合でも、遅延を大幅に悪化させるような事態を防ぐことができるようになります。

設定例は以下のとおりです。

instance_group [
{
count: 2
kind: KIND_GPU
}
]

(詳細: https://github.com/triton-inference-server/server/blob/master/docs/model_configuration.md#instance-groups & https://github.com/triton-inference-server/server/blob/v2.6.0/src/core/model_config.proto#L133-L235)

イメージは以下の図のように、あるモデルに対して複数のコピー (インスタンスと呼んでいます) を準備しておき、多数のリクエストが来たら、適宜空いているインスタンスに割り当てて処理を並列実行する、というものです。

モデル並列実行の例
モデル並列実行の例

この図の例は、ResNet50 のインスタンスが 12 個設定されている状況で、並列に 14 リクエストが送信されると、最初の 12 リクエストが即座に並列実行され、2 つのリクエストが先行する 12 リクエストの処理待ちに入っている、というシナリオになります。

Dynamic batching

モデルの並列実行に加えて、Triton が提供するもう一つの重要な機能が、dyanmic batching です。これはリクエストをキューイングし、動的にバッチを構成する機構で、こちらも設定だけで有効にできます。具体的には、各モデルの config.pbtxt に以下のような記述を追加するだけで OK です。

dynamic_batching {
preferred_batch_size: [ 4 ]
max_queue_delay_microseconds: 10000
}

(詳細: https://github.com/triton-inference-server/server/blob/master/docs/model_configuration.md#dynamic-batcher & https://github.com/triton-inference-server/server/blob/v2.6.0/src/core/model_config.proto#L974-L1047)

この設定は、以下の図のようなイメージです。複数のリクエストが連続的にサーバへ到着し、サーバ側には 2 つのモデルインスタンスが存在する状況を考えます。Dynamic batching を設定していない場合、それぞれのリクエストは到着順に独立して処理されます。そのため、仮に GPU の能力が余っていて、遅れて到着したリクエストを同時処理する余裕があっても、先着のリクエストが処理を終えるまで、待たされることになります。

一方、dynamic batching が有効な状況では、以下の図のように、指定された時間 (設定中の max_queue_delay_microseconds) 内に到着したリクエストは、サーバ側で指定されたサイズの範囲 (設定中の preferred_batch_size) で一つにまとめられ、同時に処理されます。これにより、平均待ち時間を削減しつつ、GPU の使用効率を向上させることが可能になります。

複数のリクエストが連続して到着する例
複数のリクエストが連続して到着する例
Dynamic batching による割り当て例
Dynamic batching による割り当て例

パイプライン化

単一のファイルから構成されるモデルをデプロイする場合には、ここまでの内容で概ね十分なのですが、一方、複数のモデルを連結させたい場合や、前処理や後処理で独自の処理を記述したい場合もあるかと思います。そうした用途向けに、モデルをパイプライン化する機構も、Triton では提供されます。

(名前が紛らわしいとよく言われているというか私も紛らわしいと思っているのですが、) Triton のドキュメントで “Ensemble Models” と呼ばれている機能が、モデルをパイプライン化する機能になります。
(詳細: https://github.com/triton-inference-server/server/blob/master/docs/architecture.md#ensemble-models & https://github.com/triton-inference-server/server/blob/v2.6.0/src/core/model_config.proto#L1280-L1335)

具体的には以下のように、パイプライン化したいモデルを列挙し、個々のモデルの入出力を記述するだけで処理を連結してくれるというものです。

ensemble_scheduling {
step [
{
model_name: "image_preprocess_model"
model_version: -1
input_map {
key: "RAW_IMAGE"
value: "IMAGE"
}
output_map {
key: "PREPROCESSED_OUTPUT"
value: "preprocessed_image"
}
},
{
...

実際には、さらに以下の作業が必要です。

  • 上記 ensemble_scheduling を含むひとつの config.pbtxt を作成し、ひとつの独立したモデルとして config.pbtxt とディレクトリ 1/ の入ったディレクトリを作成
    (config.pbtxt は platform: “ensemble” としておく)
  • 各ステップに相当するモデルは、それぞれ独立したモデルとして配置

上記の例は、以下のイメージ図の前処理部分に相当する設定です。このように、前処理を途中で分岐させ、複数の後続処理を同時に処理させるようなことも可能です。

パイプラインの分岐例
パイプラインの分岐例

また別の例として以下のように、途中の処理をバイパスして出力を後続の処理に渡すこともできます。

パイプラインのバイパス例
パイプラインのバイパス例

各種バックエンド

パイプライン化の中で、前処理や後処理としての独自処理について言及していました。Triton では、一般的なフレームワークを含め、各処理系をバックエンドとして定義しています。
(詳細: https://github.com/triton-inference-server/backend/blob/r20.12/README.md#backends)
この中で、独自処理を実装する際に特に重要なのが、C/C++ と Python のバックエンドです。ここでは Python バックエンドについて、少し説明をしておきます。

Python バックエンドも、基本的なデプロイの流れは他のフレームワークベースのバックエンドと同様です。

  1. config.pbtxt を記述
  2. モデルファイルを配置

大きな違いは以下のふたつです。

  • backend: “python” とする (platform を記述してはならない)
  • モデルファイルとして model.py を各バージョンディレクトリの下に置く

モデルファイルである model.py の書き方は公式のドキュメント (https://github.com/triton-inference-server/python_backend/tree/r20.12#usage) を見ていただくほうが良いのですが、主に以下の仕様を満たせば、概ねなんでもできます。

  • ファイル名は model.py にする
  • model.py の中に TritonPythonModel クラスを定義し、initialize / execute / finalize の三つのメソッドを定義
  • execute からの返却値は、入力のリストと同じ長さを保つ必要がある

(余談 3: 「概ねなんでもできます」は、たとえば補助情報を得るために外部 DB への通信を走らせたり、パスを適切に通すことでモジュール化しておいたスクリプトをロードしたり、等々)
(余談 4: 主に custom backend 向けの機能として、config.pbtxt に独自のパラメータを定義できたりもします。これを使うと、静的な情報をリクエストに乗せなくとも、モデルに渡すことができるようになります)

CPU のみのマシンで動かすには

ここまでの内容 (を含む、多くの機能) は、CPU しかないマシンでも利用することができます。コンテナの起動コマンドを以下のように — gpus オプションのない形に変更し、

docker run --rm -p8000:8000 -p8001:8001 -p8002:8002 \
-v /path/to/models:/models nvcr.io/nvidia/tritonserver:20.12-py3 \
tritonserver --model-repository=/models

(詳細: https://github.com/triton-inference-server/server/blob/v2.6.0/docs/quickstart.md#run-on-cpu-only-system)

instance_group を未設定 (=デフォルト) もしくは、その中の kind を以下のように KIND_CPU に変更すると、GPU のないマシンでも、Triton を利用することができます。

instance_group [
{
count: 1
kind: KIND_CPU
}
]

デバッグ用途、検証用途、CPU からの移行準備として、さまざまに活用できるのではないでしょうか。

その他実装されている機能

ここまででも十分長くなってしまっているのですが、実際のところ、全部を把握しきれないレベルで機能が実装されています。紹介したもの以外にも、以下のような機能が存在しています。

  • モデル制御 API
    デプロイされたモデルを、動的にロードしたりアンロードしたりすることができます。
  • ストリーミング API
    系列データに対する、ストリーミング処理のための機構です。RNN のような、直前の状態を利用して処理するタイプのモデルで活用されます。
  • 複数バージョンのデプロイ
    同じモデルの異なるバージョンのものを、複数デプロイできます。クライアント側でバージョンが明示された場合、そのバージョンのモデルに対して処理が実行されます。
  • 優先度付きリクエスト
    リクエストキューを利用している場合など、リクエストごとに優先度を設定できます。
  • Shared memory
    共有メモリを介して、リクエストを送受信できます。これにより、たとえば同一マシン上にクライアントとサーバプロセスが存在する場合、通信のオーバーヘッドを削減することができます。
  • 組み込みライブラリ化
    Triton のコア機能を libtritonserver.so として、独自のサーバプロセスに組み込むこともできます。
  • Prometheus フォーマットの Metrics
    GPU 使用率やリクエストキューでの滞留時間など、複数の指標について、起動中に情報を取得できます。
  • など……

これらの機能もうまく活用して、高速で効率の良い推論システムを目指しましょう!

関連情報

--

--