TF Servingを使用してKubernetes上に🤗 ViTをデプロイする
Using TF Serving to deploy 🤗 ViT on Kubernetes.
前の投稿では、TensorFlow Servingを使用して🤗 TransformersからVision Transformer(ViT)モデルをローカルに展開する方法を示しました。ビジョントランスフォーマーモデル内での埋め込み前処理および後処理操作、gRPCリクエストの処理など、さまざまなトピックをカバーしました!
ローカル展開は、有用なものを構築するための優れたスタート地点ですが、実際のプロジェクトで多くのユーザーに対応できる展開を実行する必要があります。この投稿では、前の投稿のローカル展開をDockerとKubernetesでスケーリングする方法を学びます。したがって、DockerとKubernetesに関する基本的な知識が必要です。
この投稿は前の投稿に基づいていますので、まずそれをお読みいただくことを強くお勧めします。この投稿で説明されているコードは、このリポジトリで確認することができます。
私たちの展開をスケールアップする基本的なワークフローは、次のステップを含みます:
- Hugging FaceのTensorFlowの哲学
- Skopsの紹介
- transformers、accelerate、bitsandbytesを使用した大規模トランスフォーマーの8ビット行列乗算へのやさしい入門
-
アプリケーションロジックのコンテナ化:アプリケーションロジックには、リクエストを処理して予測を返すサービスモデルが含まれます。コンテナ化するために、Dockerが業界標準です。
-
Dockerコンテナの展開:ここにはさまざまなオプションがあります。最も一般的に使用されるオプションは、DockerコンテナをKubernetesクラスターに展開することです。Kubernetesは、展開に便利な機能(例:自動スケーリングとセキュリティ)を提供します。ローカルでKubernetesクラスターを管理するためのMinikubeのようなソリューションや、Elastic Kubernetes Service(EKS)のようなサーバーレスソリューションを使用することもできます。
SagemakerやVertex AIのような、MLデプロイメント固有の機能をすぐに利用できる時代に、なぜこのような明示的なセットアップを使用するのか疑問に思うかもしれません。それは考えるのは当然です。
上記のワークフローは、業界で広く採用され、多くの組織がその恩恵を受けています。長年にわたってすでに実戦投入されています。また、複雑な部分を抽象化しながら、展開に対してより細かな制御を持つことができます。
この投稿では、Google Kubernetes Engine(GKE)を使用してKubernetesクラスターをプロビジョニングおよび管理することを前提としています。GKEを使用する場合、請求を有効にしたGCPプロジェクトが既にあることを想定しています。また、GKEで展開を行うためにgcloud
ユーティリティを構成する必要があります。ただし、Minikubeを使用する場合でも、この投稿で説明されているコンセプトは同様に適用されます。
注意:この投稿で表示されるコードスニペットは、gcloud
ユーティリティとDocker、kubectl
が構成されている限り、Unixターミナルで実行できます。詳しい手順は、付属のリポジトリで入手できます。
サービングモデルは、生のイメージ入力をバイトとして処理し、前処理および後処理を行うことができます。
このセクションでは、ベースのTensorFlow Servingイメージを使用してそのモデルをコンテナ化する方法を示します。TensorFlow Servingは、モデルをSavedModel
形式で消費します。前の投稿でSavedModel
を取得した方法を思い出してください。ここでは、SavedModel
がtar.gz
形式で圧縮されていることを前提としています。万が一必要な場合は、ここから入手できます。その後、SavedModel
は<MODEL_NAME>/<VERSION>/<SavedModel>
という特別なディレクトリ構造に配置する必要があります。これにより、TensorFlow Servingは異なるバージョンのモデルの複数の展開を同時に管理できます。
Dockerイメージの準備
以下のシェルスクリプトは、SavedModel
を親ディレクトリmodelsの下のhf-vit/1
に配置します。Dockerイメージの準備時には、それ内のすべてをコピーします。この例ではモデルが1つしかないですが、これは一般的に適用できるアプローチです。
$ MODEL_TAR=model.tar.gz
$ MODEL_NAME=hf-vit
$ MODEL_VERSION=1
$ MODEL_PATH=models/$MODEL_NAME/$MODEL_VERSION
$ mkdir -p $MODEL_PATH
$ tar -xvf $MODEL_TAR --directory $MODEL_PATH
以下に、ケースごとのmodels
ディレクトリの構造を示します。
$ find /models
/models
/models/hf-vit
/models/hf-vit/1
/models/hf-vit/1/keras_metadata.pb
/models/hf-vit/1/variables
/models/hf-vit/1/variables/variables.index
/models/hf-vit/1/variables/variables.data-00000-of-00001
/models/hf-vit/1/assets
/models/hf-vit/1/saved_model.pb
カスタムTensorFlow Servingイメージは、ベースイメージを基に構築する必要があります。これには様々なアプローチがありますが、公式ドキュメントに示されているようにDockerコンテナを実行することで行います。まずはバックグラウンドモードでtensorflow/serving
イメージを実行し、その後、models
ディレクトリ全体を以下のように実行中のコンテナにコピーします。
$ docker run -d --name serving_base tensorflow/serving
$ docker cp models/ serving_base:/models/
ベースとして公式のTensorFlow Serving Dockerイメージを使用しましたが、ソースからビルドしたイメージを使用することもできます。
注意: TensorFlow ServingはAVX512などの命令セットを活用したハードウェア最適化の恩恵を受けています。これらの命令セットは深層学習モデルの推論を高速化することができます。したがって、モデルがデプロイされるハードウェアを知っている場合は、TensorFlow Servingイメージの最適化ビルドを入手し、それを使用することがしばしば有益です。
実行中のコンテナに必要なファイルが適切なディレクトリ構造で揃ったところで、これらの変更を含んだ新しいDockerイメージを作成する必要があります。以下のdocker commit
コマンドでこれを行い、$NEW_IMAGE
という名前の新しいDockerイメージが生成されます。重要なポイントとして、この場合、モデル名であるhf-vit
を環境変数MODEL_NAME
に設定する必要があることに注意してください。これにより、TensorFlow Servingにデプロイするモデルを指定することができます。
$ NEW_IMAGE=tfserving:$MODEL_NAME
$ docker commit \
--change "ENV MODEL_NAME $MODEL_NAME" \
serving_base $NEW_IMAGE
ローカルでDockerイメージを実行する
最後に、新しく構築したDockerイメージをローカルで実行して正常に動作するか確認できます。以下はdocker run
コマンドの出力です。出力が冗長なため、重要な部分に絞って表示しています。また、gRPCエンドポイントとHTTP/RESTエンドポイントのポート8500
および8501
が開放されることも注目に値します。
$ docker run -p 8500:8500 -p 8501:8501 -t $NEW_IMAGE &
---------OUTPUT---------
(Re-)adding model: hf-vit
Successfully reserved resources to load servable {name: hf-vit version: 1}
Approving load for servable version {name: hf-vit version: 1}
Loading servable version {name: hf-vit version: 1}
Reading SavedModel from: /models/hf-vit/1
Reading SavedModel debug info (if present) from: /models/hf-vit/1
Successfully loaded servable version {name: hf-vit version: 1}
Running gRPC ModelServer at 0.0.0.0:8500 ...
Exporting HTTP/REST API at:localhost:8501 ...
Dockerイメージのプッシュ
最後のステップは、Dockerイメージをイメージリポジトリにプッシュすることです。ここではGoogle Container Registry (GCR)を使用します。次のコードでこれを行うことができます:
$ GCP_PROJECT_ID=<GCP_PROJECT_ID>
$ GCP_IMAGE=gcr.io/$GCP_PROJECT_ID/$NEW_IMAGE
$ gcloud auth configure-docker
$ docker tag $NEW_IMAGE $GCP_IMAGE
$ docker push $GCP_IMAGE
GCRを使用しているため、Dockerイメージのタグ(他の形式も注意)をgcr.io/<GCP_PROJECT_ID>
で接頭辞付けする必要があります。Dockerイメージが準備され、GCRにプッシュされたので、Kubernetesクラスタにデプロイする準備が整いました。
Kubernetesクラスタ上でのデプロイには以下が必要です:
-
Google Kubernetes Engine (GKE)を使用してKubernetesクラスタをプロビジョニングすること。ただし、他のプラットフォームやツール(EKSやMinikubeなど)を使用することもできます。
-
デプロイを行うためにKubernetesクラスタへの接続。
-
YAMLマニフェストの作成。
-
マニフェストを使用してデプロイを実行するためのユーティリティツールである
kubectl
。
それぞれのステップについて見ていきましょう。
GKE上でKubernetesクラスタをプロビジョニングする
この場合、次のようなシェルスクリプトを使用して実行できます(こちらで入手可能):
$ GKE_CLUSTER_NAME=tfs-cluster
$ GKE_CLUSTER_ZONE=us-central1-a
$ NUM_NODES=2
$ MACHINE_TYPE=n1-standard-8
$ gcloud container clusters create $GKE_CLUSTER_NAME \
--zone=$GKE_CLUSTER_ZONE \
--machine-type=$MACHINE_TYPE \
--num-nodes=$NUM_NODES
GCPでは、さまざまなマシンタイプを提供しており、デプロイメントを希望通りに構成することができます。詳細についてはドキュメントを参照することをお勧めします。
クラスターがプロビジョニングされたら、デプロイメントを実行するために接続する必要があります。ここではGKEが使用されているため、自分自身を認証する必要もあります。次のようなシェルスクリプトを使用して、これらの両方を行うことができます:
$ GCP_PROJECT_ID=<GCP_PROJECT_ID>
$ export USE_GKE_GCLOUD_AUTH_PLUGIN=True
$ gcloud container clusters get-credentials $GKE_CLUSTER_NAME \
--zone $GKE_CLUSTER_ZONE \
--project $GCP_PROJECT_ID
gcloud container clusters get-credentials
コマンドは、クラスターへの接続と認証の両方を処理します。これが完了すると、マニフェストの作成準備が整います。
Kubernetesマニフェストの作成
KubernetesマニフェストはYAMLファイルで作成されます。デプロイメントを実行するために単一のマニフェストファイルを使用することも可能ですが、別々のマニフェストファイルを作成することは、関心の分離を委任するためによく利用されます。これを達成するために、通常は次の3つのマニフェストファイルを使用します:
-
deployment.yaml
は、デプロイメントの望ましい状態を定義します。Dockerイメージの名前、Dockerイメージを実行する際の追加の引数、外部アクセスのために開くポート、リソースの制限などを指定します。 -
service.yaml
は、外部クライアントとKubernetesクラスタ内のポッドとの接続を定義します。 -
hpa.yaml
は、デプロイメントを構成するポッドの数をスケールアップおよびスケールダウンするためのルールを定義します。たとえば、CPU使用率の割合などです。
この記事の関連するマニフェストはこちらで見つけることができます。以下では、これらのマニフェストの受け取り方についての概要を示します。
次に、各マニフェストの重要な部分について説明します。
deployment.yaml
:
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: tfs-server
name: tfs-server
...
spec:
containers:
- image: gcr.io/$GCP_PROJECT_ID/tfserving-hf-vit:latest
name: tfs-k8s
imagePullPolicy: Always
args: ["--tensorflow_inter_op_parallelism=2",
"--tensorflow_intra_op_parallelism=8"]
ports:
- containerPort: 8500
name: grpc
- containerPort: 8501
name: restapi
resources:
limits:
cpu: 800m
requests:
cpu: 800m
...
tfs-server
、tfs-k8s
のような名前を任意の方法で構成することができます。containers
では、デプロイメントが使用するDockerイメージのURIを指定します。現在のリソース使用状況は、コンテナのためのresources
の許容範囲を設定することで監視されます。これにより、Horizontal Pod Autoscaler(後述)がコンテナの数をスケールアップまたはスケールダウンするかどうかを決定できます。 requests.cpu
は、オペレータが正しく動作するためにコンテナに必要な最小のCPUリソースです。ここでは800mは全体のCPUリソースの80%を意味します。したがって、HPAはすべてのポッドのrequests.cpu
の合計から平均CPU使用率を監視し、スケーリングの決定を行います。
Kubernetes固有の設定に加えて、args
でTensorFlow Serving固有のオプションを指定することもできます。この場合、以下の2つがあります:
-
tensorflow_inter_op_parallelism
は、独立した操作を実行するために並列に実行するスレッドの数を設定します。推奨値は2です。 -
tensorflow_intra_op_parallelism
は、個々の操作を実行するために並列に実行するスレッドの数を設定します。推奨値はデプロイメントCPUの物理コア数です。
これらのオプション(およびその他)についての詳細や、デプロイメントのためのチューニングのヒントについては、こちらとこちらから詳細を学ぶことができます。
service.yaml
:
apiVersion: v1
kind: Service
metadata:
labels:
app: tfs-server
name: tfs-server
spec:
ports:
- port: 8500
protocol: TCP
targetPort: 8500
name: tf-serving-grpc
- port: 8501
protocol: TCP
targetPort: 8501
name: tf-serving-restapi
selector:
app: tfs-server
type: LoadBalancer
サービスのタイプを「LoadBalancer」に設定することで、エンドポイントがKubernetesクラスターの外部に公開されるようになります。指定したポートを介して外部クライアントとの接続を行うために、「tfs-server」デプロイメントが選択されます。「8500」と「8501」という2つのポートをgRPCとHTTP/RESTの接続用に開放します。
hpa.yaml
:
apiVersion: autoscaling/v1
kind: HorizontalPodAutoscaler
metadata:
name: tfs-server
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: tfs-server
minReplicas: 1
maxReplicas: 3
targetCPUUtilizationPercentage: 80
HPAはHorizontal Pod Autoscalerの略です。ターゲットデプロイメントのPodの数をスケーリングするタイミングを決定するための基準を設定します。Kubernetesが内部的に使用するオートスケーリングアルゴリズムについては、こちらで詳しく学ぶことができます。
ここでは、Kubernetesがどのようにオートスケーリングを処理するかを指定します。特に、オートスケーリングを実行するために制約条件を定義します – minReplicas
とmaxReplicas
、およびターゲットCPU利用率です。targetCPUUtilizationPercentage
はオートスケーリングにとって重要なメトリックです。以下のスレッドは、それが意味するものを適切にまとめたものです(こちらから引用):
CPU使用率は、デプロイメントのすべてのPodの直近1分間の平均CPU使用率を、このデプロイメントの要求されたCPUで割ったものです。PodのCPU使用率の平均が、定義したターゲットよりも高い場合、レプリカが調整されます。
デプロイメントマニフェストでresources
を指定することを思い出してください。resources
を指定することで、Kubernetesコントロールプレーンはメトリックの監視を開始し、targetCPUUtilization
が機能します。それ以外の場合、HPAはデプロイメントの現在の状態を把握できません。
要件に基づいてこれらを必要な数値に設定して実験することができますが、GKEはこれらのリソースを管理するためにGoogle Compute Engineを内部的に使用するため、オートスケーリングは利用可能なクオータに依存します。
デプロイの実行
マニフェストが準備できたら、kubectl apply
コマンドを使用して現在接続されているKubernetesクラスターに適用することができます。
$ kubectl apply -f deployment.yaml
$ kubectl apply -f service.yaml
$ kubectl apply -f hpa.yaml
デプロイを行うために各マニフェストを個別に適用することは問題ありませんが、異なるマニフェストが多数ある場合は、Kustomizeのようなユーティリティが役立ちます。以下のようにkustomization.yaml
という名前の別の仕様を定義するだけです。
commonLabels:
app: tfs-server
resources:
- deployment.yaml
- hpa.yaml
- service.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
その後、実際のデプロイを実行するためのコマンドはわずか1行です。
$ kustomize build . | kubectl apply -f -
詳しい手順はこちらでご覧いただけます。デプロイが完了したら、次のようにしてエンドポイントのIPを取得できます。
$ kubectl rollout status deployment/tfs-server
$ kubectl get svc tfs-server --watch
---------OUTPUT---------
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
tfs-server LoadBalancer xxxxxxxxxx xxxxxxxxxx 8500:30869/TCP,8501:31469/TCP xxx
利用可能になったら、外部IPをメモしてください。
これで、Kubernetes上でモデルを展開するためのすべてのステップが完了しました! Kubernetesは、自動スケーリングやクラスタ管理などの複雑な要素に対する抽象化を優雅に提供しながら、モデルの展開時に重要な要素に重点を置くことができます。これには、リソース利用、セキュリティ(ここではカバーしていません)、レイテンシなどのパフォーマンスに関連する重要な要素が含まれます。
エンドポイント用に外部IPを取得した場合、次のリストを使用してテストできます:
import tensorflow as tf
import json
import base64
image_path = tf.keras.utils.get_file(
"image.jpg", "http://images.cocodataset.org/val2017/000000039769.jpg"
)
bytes_inputs = tf.io.read_file(image_path)
b64str = base64.urlsafe_b64encode(bytes_inputs.numpy()).decode("utf-8")
data = json.dumps(
{"signature_name": "serving_default", "instances": [b64str]}
)
json_response = requests.post(
"http://<ENDPOINT-IP>:8501/v1/models/hf-vit:predict",
headers={"content-type": "application/json"},
data=data
)
print(json.loads(json_response.text))
---------OUTPUT---------
{'predictions': [{'label': 'エジプトの猫', 'confidence': 0.896659195}]}
もし、この展開がより多くのトラフィックに対応する場合のパフォーマンスを知りたい場合は、この記事を参照することをお勧めします。Locustを使用してロードテストを実行し、結果を視覚化するための対応するリポジトリを参照してください。
TensorFlow Servingは、アプリケーションの使用ケースに基づいて展開をカスタマイズするためのさまざまなオプションを提供しています。以下では、いくつかのオプションについて簡単に説明します。
enable_batching
は、一定のタイミングウィンドウで入力リクエストを収集し、バッチとしてまとめ、バッチ推論を実行し、各リクエストの結果を適切なクライアントに返すバッチ推論機能を有効にします。 TensorFlow Servingは、展開のニーズに合わせて設定可能なオプション(max_batch_size
、num_batch_threads
など)を豊富に提供しています。詳細については、こちらをご覧ください。バッチ処理は、モデルからの予測を即座に必要としないアプリケーションに特に有益です。その場合、通常、予測のために複数のサンプルをバッチでまとめて集め、それらのバッチを予測に送信します。幸運なことに、TensorFlow Servingは、バッチ処理機能を有効にするとこれらすべてを自動で設定できます。
enable_model_warmup
は、遅延的にインスタンス化される一部のTensorFlowコンポーネントをダミーの入力データで暖機化します。これにより、すべてが適切にロードされ、実際のサービス時間中に遅延がないことを保証できます。
この記事と関連するリポジトリでは、🤗 TransformersのVision TransformerモデルをKubernetesクラスターに展開する方法について学びました。初めてこれを行う場合、手順は少し難しく見えるかもしれませんが、一度理解すれば、すぐにツールボックスの重要な要素になります。このワークフローにすでに慣れている場合でも、この記事が役立つことを願っています。
同じVision TransformerモデルのONNX最適化バージョンに対しても同じ展開ワークフローを適用しました。詳細については、このリンクをご覧ください。ONNX最適化モデルは、展開にx86 CPUを使用している場合に特に有益です。
次の投稿では、Vertex AIを使用してこの展開を行う方法を、model.deploy(autoscaling_config=...)
のようなコードの量を大幅に削減して示します。私たちと同じくらいワクワクしていることを願っています。
実験を実施するためにGCPクレジットを提供してくれたGoogleのML Developer Relations Programチームに感謝します。
We will continue to update VoAGI; if you have any questions or suggestions, please contact us!
Was this article helpful?
93 out of 132 found this helpful
Related articles