Tensorflowは、業界でも研究でも人気のある機械学習(ML)の標準プラットフォームになっています。 MLモデルのトレーニングとメンテナンスのために、多くの無料のライブラリ、ツール、およびフレームワークが作成されています。 Tensorflow Servingプロジェクトは、分散実稼働環境でのMLモデルのサービスを支援します。
Muxサービスでは、インフラストラクチャのいくつかの部分でTensorflow Servingを使用していますが、ビデオタイトルのエンコードでのTensorflow Servingの使用については既に説明しました。 今日は、予測サーバーとクライアントの両方を最適化することにより、レイテンシーを改善する方法に焦点を当てます。 モデル予測は通常(アプリケーションを要求するクリティカルパスでの)「オンライン」操作であるため、最適化の主な目標は、可能な限り少ない遅延で大量の要求を処理することです。
Tensorflow Servingとは何ですか?
Tensorflow Servingは、MLモデルを展開および保守するための柔軟なサーバーアーキテクチャを提供します。 モデルがトレーニングされ、予測に使用できる状態になると、Tensorflow Servingは互換性のある(サービス可能な)形式にエクスポートする必要があります。
Servableは、
Tensorflowオブジェクトをラップする中心的な抽象概念です。 たとえば、モデルは1つ以上のServableオブジェクトとして表すことができます。 したがって、Servableは、クライアントが計算を実行するために使用する基本オブジェクトです。 使用可能なサイズは重要です:モデルが小さいほど、スペースが少なくなり、メモリの使用量が少なくなり、ロードが速くなります。 Predict APIを使用してダウンロードおよび保守するには、モデルがSavedModel形式である必要があります。
Tensorflow Servingは、基本コンポーネントを組み合わせて、複数のMLモデル(または複数のバージョン)を提供するgRPC / HTTPサーバーを作成し、監視コンポーネントとカスタムアーキテクチャを提供します。
Dockerを使用したTensorflowの提供
標準のTensorflowサービング設定(CPU最適化なし)でパフォーマンスを予測する際の基本的な遅延メトリックを見てみましょう。
まず、TensorFlow Dockerハブから最新の画像をダウンロードします。
docker pull tensorflow/serving:latest
この記事では、すべてのコンテナーは、15 GB、Ubuntu 16.04の4つのコアを持つホストで実行されます。
TensorflowモデルをSavedModel形式にエクスポート
Tensorflowを使用してモデルをトレーニングする場合、出力を変数制御ポイント(ディスク上のファイル)として保存できます。 出力は、モデルの制御点を復元するか、フリーズされたフリーズグラフ形式(バイナリファイル)で直接実行されます。
Tensorflow Servingの場合、このフリーズグラフはSavedModel形式にエクスポートする必要があります。 Tensorflow
ドキュメントには、トレーニング済みモデルをSavedModel形式にエクスポートする例が含まれています。
Tensorflowは、実験、研究、または生産の出発点として、多くの
公式および研究モデルも提供します。
例として、
深層残差ニューラルネットワーク(ResNet)モデルを使用して、1000クラスからImageNetデータセットを分類します。
事前に ResNet-50 v2
モデル、具体的にはSavedModelのChannels_last(NHWC)
オプションを
ダウンロードします。原則として、CPUでより適切に動作します。
RestNetモデルディレクトリを次の構造にコピーします。
models/ 1/ saved_model.pb variables/ variables.data-00000-of-00001 variables.index
Tensorflow Servingは、バージョン管理のために数値的に順序付けられたディレクトリ構造を想定しています。 この場合、ディレクトリ
1/
は、モデルの重み(変数)のスナップショットを持つ
saved_model.pb
モデルのアーキテクチャを含むバージョン1モデルに対応します。
SavedModelのロードと処理
次のコマンドは、DockerコンテナーでTensorflow Servingモデルサーバーを起動します。 SavedModelをロードするには、予想されるコンテナディレクトリにモデルディレクトリをマウントする必要があります。
docker run -d -p 9000:8500 \ -v $(pwd)/models:/models/resnet -e MODEL_NAME=resnet \ -t tensorflow/serving:latest
コンテナログを確認すると、ModelServerが稼働しており、gRPCおよびHTTPエンドポイントで
resnet
モデルの出力リクエストを処理していることがわかります。
... I tensorflow_serving/core/loader_harness.cc:86] Successfully loaded servable version {name: resnet version: 1} I tensorflow_serving/model_servers/server.cc:286] Running gRPC ModelServer at 0.0.0.0:8500 ... I tensorflow_serving/model_servers/server.cc:302] Exporting HTTP/REST API at:localhost:8501 ...
予測クライアント
Tensorflow Servingは、
プロトコルバッファー (protobufs)形式でAPIスキーマを定義します。 予測APIのGRPCクライアント実装は、Pythonパッケージ
tensorflow_serving.apis
としてパッケージ化されています。 ユーティリティ関数用に別のPythonパッケージ
tensorflow
が必要になります。
依存関係をインストールして、単純なクライアントを作成します。
virtualenv .env && source .env/bin/activate && \ pip install numpy grpcio opencv-python tensorflow tensorflow-serving-api
ResNet-50 v2
モデルでは、フォーマット化されたchannels_last(NHWC)データ構造の浮動小数点テンソルの入力が必要です。 したがって、入力画像はopencv-pythonを使用して読み取られ、numpy配列(高さ×幅×チャンネル)にfloat32データ型としてロードされます。 以下のスクリプトは、予測クライアントスタブを作成し、JPEGデータをnumpy配列にロードし、それをtensor_protoに変換してgRPCの予測リクエストを作成します。
JPEG入力を受信すると、動作中のクライアントは次の結果を生成します。
python tf_serving_client.py --image=images/pupper.jpg total time: 2.56152906418s
結果のテンソルには、整数値と符号の確率の形式の予測が含まれます。
outputs { key: "classes" value { dtype: DT_INT64 tensor_shape { dim { size: 1 } } int64_val: 238 } } outputs { key: "probabilities" ...
単一のリクエストの場合、このような遅延は許容されません。 しかし、驚くことではありません。TensorflowServingバイナリは、ほとんどのユースケースで最も幅広い機器向けにデフォルトで設計されています。 おそらく、標準のTensorflow Servingコンテナのログに次の行があることに気づいたでしょう。
I external/org_tensorflow/tensorflow/core/platform/cpu_feature_guard.cc:141] Your CPU supports instructions that this TensorFlow binary was not compiled to use: AVX2 FMA
これは、最適化されていないCPUプラットフォームで実行されているTensorFlow Servingバイナリを示します。
最適化されたバイナリを構築する
Tensorflowの
ドキュメントによると、バイナリが機能するホスト上のCPUで利用可能なすべての最適化を使用して、ソースからTensorflowをコンパイルすることをお勧めします。 アセンブル時に、特別なフラグを使用して、特定のプラットフォームのCPU命令セットをアクティブ化できます。
命令セット | 旗 |
---|
AVX | --copt = -mavx |
AVX2 | --copt = -mavx2 |
FMA | --copt = -mfma |
SSE 4.1 | --copt = -msse4.1 |
SSE 4.2 | --copt = -msse4.2 |
プロセッサですべてサポートされています | --copt = -march =ネイティブ |
特定のバージョンのTensorflowサービングを複製します。 私たちの場合、これは1.13(この記事の公開時の最後)です。
USER=$1 TAG=$2 TF_SERVING_VERSION_GIT_BRANCH="r1.13" git clone --branch="$TF_SERVING_VERSION_GIT_BRANCH" https://github.com/tensorflow/serving
Tensorflow Serving devイメージは、Baselツールを使用してビルドします。 CPU命令の特定のセット用に構成します。
TF_SERVING_BUILD_OPTIONS="--copt=-mavx --copt=-mavx2 --copt=-mfma --copt=-msse4.1 --copt=-msse4.2"
十分なメモリがない場合は、フラグ
--local_resources=2048,.5,1.0
使用して、ビルドプロセス中のメモリ消費を制限します。 フラグの詳細については、
Tensorflow ServingとDockerのヘルプ、
および Bazelのドキュメントを参照してください。
既存のものに基づいて作業イメージを作成します。
ModelServerは、同時実行性をサポートするために
TensorFlowフラグを使用して構成されます。 次のオプションは、並列操作用に2つのスレッドプールを構成します。
intra_op_parallelism_threads
- 1つの操作の並列実行のスレッドの最大数を制御します。
- 本質的に独立したサブ操作を持つ操作を並列化するために使用されます。
inter_op_parallelism_threads
- 独立した操作の並列実行のスレッドの最大数を制御します。
- Tensorflow Graph操作は互いに独立しており、したがって、異なるスレッドで実行できます。
デフォルトでは、両方のパラメーターは
0
設定されてい
0
。 これは、システム自体が適切な数を選択することを意味し、ほとんどの場合、コアごとに1つのスレッドを意味します。 ただし、マルチコアの同時実行のためにパラメータを手動で変更できます。
次に、前のものと同じ方法でServingコンテナーを開始します。今回は、ソースからコンパイルされたDockerイメージと、特定のプロセッサーのTensorflow最適化フラグを使用します。
docker run -d -p 9000:8500 \ -v $(pwd)/models:/models/resnet -e MODEL_NAME=resnet \ -t $USER/tensorflow-serving:$TAG \ --tensorflow_intra_op_parallelism=4 \ --tensorflow_inter_op_parallelism=4
コンテナログには、未定義のCPUに関する警告が表示されなくなります。 同じ予測リクエストのコードを変更しない場合、遅延は約35.8%削減されます。
python tf_serving_client.py --image=images/pupper.jpg total time: 1.64234706879s
クライアント予測の速度向上
加速することはまだ可能ですか? CPUのサーバー側を最適化しましたが、1秒以上の遅延は依然として大きすぎるようです。
そのため、
tensorflow_serving
および
tensorflow
ライブラリのロードが遅延に大きく寄与しました。
tf.contrib.util.make_tensor_proto
不要な呼び出しごとに、1
tf.contrib.util.make_tensor_proto
も追加されます。
「Tensorflowサーバーへの予測リクエストを実際に行うためにTensorFlow Pythonパッケージは必要ないのですか?」と
tensorflow
ます。実際、
tensorflow_serving
および
tensorflow
パッケージは実際には
必要ありません。
前述したように、Tensorflow予測APIはプロトバッファーとして定義されています。 したがって、2つの外部依存関係を対応する
tensorflow
および
tensorflow_serving
置き換えることができます。そして、クライアント上の(重い)Tensorflowライブラリ全体をプルする必要はありません。
まず、
tensorflow
と
tensorflow_serving
を
grpcio-tools
、
grpcio-tools
パッケージを追加します。
pip uninstall tensorflow tensorflow-serving-api && \ pip install grpcio-tools==1.0.0
tensorflow/tensorflow
および
tensorflow/serving
リポジトリを複製し、次のprotobufファイルをクライアントプロジェクトにコピーします。
tensorflow/serving/ tensorflow_serving/apis/model.proto tensorflow_serving/apis/predict.proto tensorflow_serving/apis/prediction_service.proto tensorflow/tensorflow/ tensorflow/core/framework/resource_handle.proto tensorflow/core/framework/tensor_shape.proto tensorflow/core/framework/tensor.proto tensorflow/core/framework/types.proto
これらのprotobufファイルを、元のパスを保存したまま
protos/
ディレクトリにコピーします。
protos/ tensorflow_serving/ apis/ *.proto tensorflow/ core/ framework/ *.proto
簡単に
するために、サービスで指定された他のRPCのネストされた依存関係をダウンロードしないように、predict_service.protoを単純化してPredict RPCのみを実装できます。
以下は 、単純化された
prediction_service.
例です。
grpcio.tools.protocを使用してPython gRPC実装を作成します。
PROTOC_OUT=protos/ PROTOS=$(find . | grep "\.proto$") for p in $PROTOS; do python -m grpc.tools.protoc -I . --python_out=$PROTOC_OUT --grpc_python_out=$PROTOC_OUT $p done
これで、
tensorflow_serving
モジュール全体を削除できます。
from tensorflow_serving.apis import predict_pb2 from tensorflow_serving.apis import prediction_service_pb2
...そして
protos/tensorflow_serving/apis
から生成されたprotobuffersに置き換えます:
from protos.tensorflow_serving.apis import predict_pb2 from protos.tensorflow_serving.apis import prediction_service_pb2
make_tensor_proto
ライブラリは、ヘルパー関数
make_tensor_proto
を使用するためにインポートされます。これは、python / numpyオブジェクトをTensorProtoオブジェクトとして
ラップする
ために必要です 。
したがって、次の依存関係とコードフラグメントを置き換えることができます。
import tensorflow as tf ... tensor = tf.contrib.util.make_tensor_proto(features) request.inputs['inputs'].CopyFrom(tensor)
プロトバッファをインポートし、TensorProtoオブジェクトを構築します。
from protos.tensorflow.core.framework import tensor_pb2 from protos.tensorflow.core.framework import tensor_shape_pb2 from protos.tensorflow.core.framework import types_pb2 ...
完全なPythonスクリプトは
こちらです。 最適化されたTensorflow Servingの予測リクエストを行う更新されたスタータークライアントを実行します。
python tf_inception_grpc_client.py --image=images/pupper.jpg total time: 0.58314920859s
次の図は、標準と比較した、10回以上の最適化されたTensorflow Servingの予測実行時間を示しています。
平均遅延は約3.38倍減少しました。
帯域幅の最適化
Tensorflow Servingは、大量のデータを処理するように構成できます。 通常、帯域幅の最適化は、厳密な遅延境界が厳密な要件ではない「スタンドアロン」バッチ処理に対して実行されます。
サーバー側のバッチ処理
ドキュメントに記載されて
いるように 、サーバー側のバッチ処理はTensorflow Servingでネイティブにサポートされています。
レイテンシとスループットのトレードオフは、バッチ処理パラメーターによって決まります。 ハードウェアアクセラレータで可能な最大帯域幅を達成できます。
パッケージ化を有効にするには、--
--batching_parameters_file
および
--batching_parameters_file
設定します。 パラメーターは
SessionBundleConfigに従って設定され
ます 。 CPU上のシステムの場合、
num_batch_threads
を使用可能なコアの数に設定します。 GPUについては、
ここで適切なパラメーターを参照して
ください 。
サーバー側でパッケージ全体に記入した後、発行要求は1つの大きな要求(テンソル)に結合され、結合された要求と共にTensorflowセッションに送信されます。 この状況では、CPU / GPUの並列処理が実際に関係しています。
Tensorflowバッチ処理の一般的な用途:
- 非同期クライアント要求を使用して、サーバー側のパケットを作成します
- モデルグラフのコンポーネントをCPU / GPUに転送することでバッチ処理を高速化
- 単一サーバーからの複数のモデルからのリクエストに対応する
- 大量のリクエストの「オフライン」処理には、バッチ処理を強くお勧めします
クライアント側のバッチ処理
クライアント側のバッチ処理は、複数の着信要求を1つにグループ化します。
ResNetモデルはNHWC形式の入力を待機しているため(最初の次元は入力の数です)、複数の入力イメージを1つのRPC要求に結合できます。
... batch = [] for jpeg in os.listdir(FLAGS.images_path): path = os.path.join(FLAGS.images_path, jpeg) img = cv2.imread(path).astype(np.float32) batch.append(img) ... batch_np = np.array(batch).astype(np.float32) dims = [tensor_shape_pb2.TensorShapeProto.Dim(size=dim) for dim in batch_np.shape] t_shape = tensor_shape_pb2.TensorShapeProto(dim=dims) tensor = tensor_pb2.TensorProto( dtype=types_pb2.DT_FLOAT, tensor_shape=t_shape, float_val=list(batched_np.reshape(-1))) request.inputs['inputs'].CopyFrom(tensor)
N個の画像のパケットの場合、応答の出力テンソルには同じ数の入力の予測結果が含まれます。 この例では、N = 2です。
outputs { key: "classes" value { dtype: DT_INT64 tensor_shape { dim { size: 2 } } int64_val: 238 int64_val: 121 } } ...
ハードウェアアクセラレーション
GPUについて一言。
ディープニューラルネットワークの構築には最適なソリューションを実現するために大規模な計算が必要になるため、学習プロセスではGPUでの並列化が自然に使用されます。
しかし、結果を出力するために、並列化はそれほど明白ではありません。 多くの場合、GPUへのニューラルネットワークの出力を高速化できますが、慎重に機器を選択してテストし、技術的および経済的な詳細な分析を行う必要があります。 ハードウェアの並列化は、「自律的な」結論(大量のボリューム)のバッチ処理にとってより価値があります。
GPUに移行する前に、コスト(金銭的、運用的、技術的)を慎重に分析してビジネス要件を検討し、最大のメリット(待ち時間の短縮、高スループット)を実現してください。