Tensorflow Serving Productivityを70%増加させた方法

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の予測リクエストを作成します。



 #!/usr/bin/env python from __future__ import print_function import argparse import numpy as np import time tt = time.time() import cv2 import tensorflow as tf from grpc.beta import implementations from tensorflow_serving.apis import predict_pb2 from tensorflow_serving.apis import prediction_service_pb2 parser = argparse.ArgumentParser(description='incetion grpc client flags.') parser.add_argument('--host', default='0.0.0.0', help='inception serving host') parser.add_argument('--port', default='9000', help='inception serving port') parser.add_argument('--image', default='', help='path to JPEG image file') FLAGS = parser.parse_args() def main(): # create prediction service client stub channel = implementations.insecure_channel(FLAGS.host, int(FLAGS.port)) stub = prediction_service_pb2.beta_create_PredictionService_stub(channel) # create request request = predict_pb2.PredictRequest() request.model_spec.name = 'resnet' request.model_spec.signature_name = 'serving_default' # read image into numpy array img = cv2.imread(FLAGS.image).astype(np.float32) # convert to tensor proto and make request # shape is in NHWC (num_samples x height x width x channels) format tensor = tf.contrib.util.make_tensor_proto(img, shape=[1]+list(img.shape)) request.inputs['input'].CopyFrom(tensor) resp = stub.Predict(request, 30.0) print('total time: {}s'.format(time.time() - tt)) if __name__ == '__main__': main()
      
      





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のドキュメントを参照してください。



既存のものに基づいて作業イメージを作成します。



 #!/bin/bash USER=$1 TAG=$2 TF_SERVING_VERSION_GIT_BRANCH="r1.13" git clone --branch="${TF_SERVING_VERSION_GIT_BRANCH}" https://github.com/tensorflow/serving TF_SERVING_BUILD_OPTIONS="--copt=-mavx --copt=-mavx2 --copt=-mfma --copt=-msse4.1 --copt=-msse4.2" cd serving && \ docker build --pull -t $USER/tensorflow-serving-devel:$TAG \ --build-arg TF_SERVING_VERSION_GIT_BRANCH="${TF_SERVING_VERSION_GIT_BRANCH}" \ --build-arg TF_SERVING_BUILD_OPTIONS="${TF_SERVING_BUILD_OPTIONS}" \ -f tensorflow_serving/tools/docker/Dockerfile.devel . cd serving && \ docker build -t $USER/tensorflow-serving:$TAG \ --build-arg TF_SERVING_BUILD_IMAGE=$USER/tensorflow-serving-devel:$TAG \ -f tensorflow_serving/tools/docker/Dockerfile .
      
      





ModelServerは、同時実行性をサポートするためにTensorFlowフラグを使用して構成されます。 次のオプションは、並列操作用に2つのスレッドプールを構成します。



 intra_op_parallelism_threads
      
      







 inter_op_parallelism_threads
      
      







デフォルトでは、両方のパラメーターは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 ... # ensure NHWC shape and build tensor proto tensor_shape = [1]+list(img.shape) dims = [tensor_shape_pb2.TensorShapeProto.Dim(size=dim) for dim in tensor_shape] tensor_shape = tensor_shape_pb2.TensorShapeProto(dim=dims) tensor = tensor_pb2.TensorProto( dtype=types_pb2.DT_FLOAT, tensor_shape=tensor_shape, float_val=list(img.reshape(-1))) request.inputs['inputs'].CopyFrom(tensor)
      
      





完全な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バッチ処理の一般的な用途:





クライアント側のバッチ処理



クライアント側のバッチ処理は、複数の着信要求を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に移行する前に、コスト(金銭的、運用的、技術的)を慎重に分析してビジネス要件を検討し、最大のメリット(待ち時間の短縮、高スループット)を実現してください。



All Articles