はじめに
過去の記事「TorchServeを使ってAzure 環境でPyTorchモデルをAPI化してみた」の続きみたいなものです。
前回は、TorchServeで「できることの紹介」というイメージで基本的な利用方法を浅く広く紹介しました。
今回は、「実際に活用する」という観点に立ち各種PythonファイルやDockerFileを実際に一つ一つ作成します。
そして前回とは異なり、TorchServeをDockerコンテナとして動かしています。
(前回は、TorchServeをプロセスとして直接動作させていました。)
参考にした記事
今回の記事を書くにあたっては以下のページをかなり参考にしました。
上記ページと比較した本記事の特徴は、「とりあえず動かせる」だけでなく「使い方を理解して動かせる」まで目指している点だと思います。
ゴールイメージ
このような形で、画像を送ると結果が返ってくるようなAPIをDockerコンテナ上に作成することを目指します。
今回は、APIとしてホストされるモデルはvgg11を用います。(vgg11である理由は特にないです)
前提
今回は、前回の記事で作成したAzure VMでにSSHで接続している状態を前提として手順の紹介を行っています。
もしVMが未作成の方は、以下の作業を先にお願いします。
Azure VMの作成
以下の設定で、Azure VMを作成します。
設定項目 | 設定値 |
---|---|
イメージ | Ubuntu Server18.04LTS- Gen1 |
リージョン | 米国中南米 |
サイズ | Standard_NC6_Promo - 6 vcpu |
その他 | デフォルト |
cuda関連パッケージのインストール
具体的な手順は、以前の記事をご参照ください。
手順
手順は以下のような流れとなっています。
- Dockerのインストール
- フォルダ構成の用意
- SSL化用の証明書作成
- TorchServeのサーバー用設定ファイルを用意
- デプロイするモデル(今回はVGG11)の用意
- 実行用スクリプトの用意
- DockerFileの作成
- DockerFileのビルドとRUN
Dockerのインストール
まずは、以下のコマンドを実行し、Azure VMにDockerをインストールします。
sudo apt-get update
sudo apt-get install \
apt-transport-https \
ca-certificates \
curl \
gnupg-agent \
software-properties-common
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -
sudo add-apt-repository \
"deb [arch=amd64] https://download.docker.com/linux/ubuntu \
$(lsb_release -cs) \
stable"
sudo apt-get update
sudo apt-get install docker-ce docker-ce-cli containerd.io
フォルダ構成の用意
Dockerのインストールが終了したら、今回の作業用ディレクトリtorchserve-docker
を作成します。
作業用ディレクトリは今回は、ホームディレクトリ直下に作成します。
その後、作成した作業用ディレクトリに移動します。
cd ~
mkdir torchserve-docker
cd torchserve-docker
そして、作業用ディレクトリの配下に以下のようなフォルダ構成を用意します。
torchserve-docker
├── .docker_torchserve_deploy // Dockerファイルを作成するためのディレクトリ
├── model_store // モデルアーカイブファイルを保存するディレクトリ
└── models // モデルのクラスファイル、学習済みの重みファイルなどを保存するディレクトリ
└── vgg11
SSL化用の証明書作成
今回は、SSLで通信できるようにAPIを構成します。
そこで、以下のコマンドを実行し、APIをSSL化するための秘密鍵と証明書(兼公開鍵)を生成します。
ここでは、opensslを使って自己署名証明書を作成しています。
openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout mykey.key -out mycert.pem
設定値は以下のようにしました。
Country Name (2 letter code) [AU]:JP
State or Province Name (full name) [Some-State]:Tokyo
Locality Name (eg, city) []:Shinagawa
Organization Name (eg, company) [Internet Widgits Pty Ltd]:Org
Organizational Unit Name (eg, section) []:OrgUnit
Common Name (e.g. server FQDN or YOUR name) []:torchserve-sample
Email Address []:example@example.com
これで、SSL用の秘密鍵と証明書が作成されました。ls
コマンドで確認すると、以下のようになります。
model_store models mycert.pem mykey.ke
TorchServeのサーバー用設定ファイルを用意
次に、サーバーに関する設定を記述するための設定ファイルを用意します。
以下のコマンドで、カレント・ディレクトリにconfig.properties
というファイルを作成してください。
touch config.properties
なお、config.properties
で設定できる内容についての詳細は、こちらのTorchServeドキュメントに記載してあります。
続いて、作成したファイルに、外部からアクセスするためのIPアドレスと、SSL通信に利用するためにさきほど作成した秘密鍵と証明書のパスを設定します。
config.properties
に以下のように設定をしてください。
inference_address=https://0.0.0.0:8443
management_address=https://0.0.0.0:8444
metrics_address=https://0.0.0.0:8445
private_key_file=mykey.key
certificate_file=mycert.pem
ここで設定している3つのIPアドレスは、それぞれ以下の3つのAPIに対応しています。
APIの種類 | 概要 | デフォルトのアドレス | ドキュメント |
---|---|---|---|
推論用API | モデルを使って推論をする際に使うエンドポイントです | http://127.0.0.1:8080 | Inference API |
モデル管理用API | モデルの登録、ステータス確認、ワーカー数設定といったモデルの管理をする際に使うAPIです。 | http://127.0.0.1:8081 | Management API |
指標用API | 指定したモデルの指標を確認するためのエンドポイントです。 また、このAPIを通じてモデルの指標をダッシュボードで閲覧することも可能です。 |
http://127.0.0.1:8082 | Metrics API |
また、前回の記事ではそれぞれにAPIについてより詳しく説明しています。
デプロイするモデル(今回はVGG11)の用意
今回は、画像分類用のモデルVGG11をTorchServe上にデプロイします。
そこで、デプロイするために必要な以下の4つのファイルを用意します。
- PyTorchのVGG11のモデルを記載したPythonファイル(
./models/vgg11/model.py
) - VGG11の学習済みの重みファイル(
./models/vgg11/vgg11-bbd30ac9.pth
) - ハンドラの処理を定義したPythonファイル(
./models/vgg11/vgg_handler.py
) - クラス名とインデックスの対応付けを記載したjsonファイル(
./models/index_to_name.json
)
PyTorchのVGG11のモデルを記載したPythonファイルを用意
まずは、VGG11のモデルをPyTorchで定義したPythonファイルを用意します。
以下のコマンドで、ファイルを作成してください。
touch ./models/vgg11/model.py
このファイルに、以下のようにVGG11のモデルを定義します。
from torchvision.models.vgg import VGG, make_layers, cfgs
class ImageClassifier(VGG):
"""VGGを継承してPyTorchのモデルクラスを定義"""
def __init__(self):
# VGGのPyTorchモデルをインスタンス化しているだけ
super(ImageClassifier, self).__init__(make_layers(cfgs['A'], False), **{'init_weights': False})
ご覧のように、TorchVisionで用意されているVGGのモデルを継承しているだけです。
VGG11の学習済みの重みファイルを用意
次に、以下のコマンドで、PyTorchが公開している学習済みのVGG11の重みファイルを取得します。
ここでは、PytorchのGitHub上で公開されているものをダウンロードしています。
wget https://download.pytorch.org/models/vgg11-bbd30ac9.pth -P "./models/vgg11"
ハンドラの処理を定義したPythonファイルを用意
そして、ハンドラを定義したファイルを作成します。
ハンドラは、TorchServe上でモデルをインスタンス化し、重みをロードする処理などをするPythonファイルです。
今回はファイルを作成しますが、デフォルトのハンドラを使うことも可能です。
以下のコマンドで、ハンドラのファイルを作成します。
touch ./models/vgg11/vgg_handler.py
今回は、ハンドラの役割についてイメージを持つために、以下の処理をするメソッドを記載しました。
- モデルのインスタンス化(
def _load_pickled_model()
) - 推論結果の後処理(
def postprocess()
)
import importlib
import os
import torch
import torch.nn.functional as F
from torchvision import transforms
from ts.torch_handler.vision_handler import VisionHandler
from ts.utils.util import list_classes_from_module, map_class_to_label
class VGGImageClassifier(VisionHandler):
"""
VisionHandlerを継承して、VGG用のハンドラを作成。
"""
topk = 5
# These are the standard Imagenet dimensions
# and statistics
image_processing = transforms.Compose([
transforms.Resize(256),
transforms.CenterCrop(224),
transforms.ToTensor(),
transforms.Normalize(mean=[0.485, 0.456, 0.406],
std=[0.229, 0.224, 0.225])
])
def _load_pickled_model(self, model_dir, model_file, model_pt_path):
"""モデルのインスタンス化をするメソッド
具体的には、クラスのファイルと学習済みの重みを読みとり、PyTorchモデルのオブジェクトを返します。"""
model_def_path = os.path.join(model_dir, model_file)
if not os.path.isfile(model_def_path):
raise RuntimeError("Missing the model.py file")
# 指定したPyTorchモデルを定義したPythonファイルをimport
module = importlib.import_module(model_file.split(".")[0])
# Pythonファイルに、モデルのクラスの定義が複数含まれていないか確認
model_class_definitions = list_classes_from_module(module)
# 複数含まれている場合はエラー
if len(model_class_definitions) != 1:
raise ValueError("Expected only one class as model definition. {}".format(
model_class_definitions))
# 一つだけの場合は、そのクラスを取得
model_class = model_class_definitions[0]
# 重みのファイルを読み込み
state_dict = torch.load(model_pt_path)
# クラスをインスタンス化
model = model_class()
# 重みをモデルにロード
model.load_state_dict(state_dict)
return model
def set_max_result_classes(self, topk):
"""推論時、TOPいくつまでの結果を返すかを設定(setter)"""
self.topk = topk
def get_max_result_classes(self):
"""推論時、TOPいくつまでの結果を返すかを取得(getter)"""
return self.topk
def postprocess(self, data):
"""推論結果を返す際の後処理をするメソッド"""
ps = F.softmax(data, dim=1)
probs, classes = torch.topk(ps, self.topk, dim=1)
probs = probs.tolist()
classes = classes.tolist()
# TorchServeのutilを利用して、推論結果のインデックス(数値)、ラベル(文字列)に変換
# self.mappingは、基底クラスの「BaseHandler」で定義
return map_class_to_label(probs, self.mapping, classes)
上記ファイルでは、モデルのインスタンス化と、推論結果の後処理のみハンドラに定義しています。
しかし、継承元のクラスでは、さらにデータの前処理なども実装されています。
カスタムのハンドラを定義することで、推論時にさらに複雑な処理を追加することも可能です。
クラス名とインデックスの対応付けを記載したjsonファイルを用意
インデックス(数値)とクラス名(文字列)の対応付けをするためのファイルを用意します。
これは、さきほど定義したハンドラのスクリプトで、推論結果をインデックス→ラベルに変換する際に利用することになります。
以下のコマンドで、TorchServeのGitHubのサンプルをダウンロードし、./models
フォルダに格納します。
wget https://raw.githubusercontent.com/pytorch/serve/master/examples/image_classifier/index_to_name.json -P "./models"
※ TorchServeのGitHubのサンプルから直接ダウンロードし、ファイルを作成てい頂いても大丈夫です。
行数が長いので一部だけサンプルとしてファイルの中身を掲載します。
{"0": ["n01440764", "tench"], "1": ["n01443537", "goldfish"], "2": ["n01484850", "great_white_shark"], "3": ["n01491361", "tiger_shark"], "4": ["n01494475", "hammerhead"], "5": ["n01496331", "electric_ray"], "6": ["n01498041", "stingray"], "7": ["n01514668", "cock"], "8": ["n01514859", "hen"], "9": ["n01518878", "ostrich"], "10": ["n01530575", "brambling"],
実行用スクリプトの用意
TorchServeを起動するためのシェルスクリプトが記述されたrun.sh
ファイルを作成します。
このシェルスクリプトは、Dockerコンテナの起動時に実行されることになります。
tourch run.sh
run.shでは、具体的には以下の処理を行います。
- デプロイするVGG11のモデルをアーカイブ(
torch-model-archiver
)- アーカイブとは、VGG11のモデルを、TorchServeで扱える形式(.mar拡張子)に変換することです。
-
torch-model-archiver
コマンド自体については、以前の記事の#モデルを.mar形式に変換で紹介しています。
- TorchServeの起動(
torchserve --start
)- このコマンドで、TorchServeが起動されVGG11はAPI化されます。
- またこの時、先ほど用意した
config.properties
で行った設定が反映され、APIはSSL化されます。 - こちらもコマンドの意味については、以前の記事の#TochServeの起動に記載しています。
#!/bin/bash
set -e
# パスを変数に設定
MODEL_DIR="${WORK_DIR}/models"
MODEL_FILE_PATH="${MODEL_DIR}/vgg11/model.py"
SERIALIZED_FILE_PATH="${MODEL_DIR}/vgg11/vgg11-bbd30ac9.pth"
HANDLER_FILE_PATH="${MODEL_DIR}/vgg11/vgg_handler.py"
INDEX_FILE_PATH="${MODEL_DIR}/index_to_name.json"
# デプロイするVGG11のモデルをアーカイブ
torch-model-archiver \
--model-name vgg11 \
--version 1.0 \
--model-file ${MODEL_FILE_PATH} \
--serialized-file ${SERIALIZED_FILE_PATH} \
--export-path ${MODEL_STORE_DIR} \
--handler ${HANDLER_FILE_PATH} \
--extra-files ${INDEX_FILE_PATH}
# TorchServeの起動
torchserve --start \
--model-store ${MODEL_STORE_DIR} \
--ts-config ${WORK_DIR}/config.properties \
--models vgg11=vgg11.mar
# dockerがexitしないためのコマンド
tail -f /dev/null
DockerFileの作成
最後に、DockerFileを作成します。
touch .docker_torchserve_deploy/DockerFile
Dockerファイルは、以下のように設定します。
今回はDockerのマルチステージビルドを利用し、ビルド用イメージを作成した後、改めてデプロイ用イメージを作成するという流れにしています。
# syntax = docker/dockerfile:experimental
ARG BASE_IMAGE=ubuntu:18.04
# =============ビルド用のイメージを作成=============
FROM ${BASE_IMAGE} AS build-image
ENV PYTHONUNBUFFERED TRUE
# aptのキャッシュディレクトリ「/var/cache/apt」をマウントした上で必要なパッケージをインストール
# 各パラメータの参考:https://qiita.com/ryuichi1208/items/d13ec434e694d672ab36
RUN --mount=type=cache,id=apt-dev,target=/var/cache/apt \
apt-get update && \
DEBIAN_FRONTEND=noninteractive apt-get install --no-install-recommends -y \
ca-certificates \
g++ \
python3-dev \
python3-distutils \
python3-venv \
openjdk-11-jre-headless \
curl \
&& rm -rf /var/lib/apt/lists/* \
&& cd /tmp \
&& curl -O https://bootstrap.pypa.io/get-pip.py \
&& python3 get-pip.py
# Python仮想環境の作成
RUN python3 -m venv /home/venv
# pathにPython仮想環境を追加
ENV PATH="/home/venv/bin:$PATH"
# 利用するPython、pipのバージョンを設定
# update-alternatives --install <シンボリックリンクのパス> <コマンド名> <実体へのパス> <優先度>
RUN update-alternatives --install /usr/bin/python python /usr/bin/python3 1 \
&& update-alternatives --install /usr/local/bin/pip pip /usr/local/bin/pip3 1
# cudaを使う際には設定する
RUN export USE_CUDA=1
# docker buildコマンドの実行時に指定がなかった場合の、環境変数の初期値を設定
ARG CUDA_VERSION=cu110
ARG TORCH_VER=1.7.1
ARG TORCH_VISION_VER=0.8.2
# PyTorch関連のパッケージをインストール
RUN pip install --no-cache-dir torch==$TORCH_VER+$CUDA_VERSION torchvision==$TORCH_VISION_VER+$CUDA_VERSION -f https://download.pytorch.org/whl/torch_stable.html;
# torchserve関連のパッケージをインストール
RUN pip install --no-cache-dir captum torchtext torchserve torch-model-archiver
# ===========デプロイ用のイメージを作成=============
FROM ${BASE_IMAGE} AS runtime-image
ENV PYTHONUNBUFFERED TRUE
# 実行フォルダ
ENV WORK_DIR /home/model-server
# モデルアーカイブディレクトリ
ENV MODEL_STORE_DIR ${WORK_DIR}/model_store
# カレント・ディレクトリを変更
WORKDIR ${WORK_DIR}
# 二回目のapt-getでは、ビルド時にマウントした「/var/cache/apt」からキャッスを利用してインストール
RUN --mount=type=cache,target=/var/cache/apt \
apt-get update && \
DEBIAN_FRONTEND=noninteractive apt-get install --no-install-recommends -y \
python3 \
python3-distutils \
python3-dev \
openjdk-11-jre-headless \
build-essential \
&& rm -rf /var/lib/apt/lists/* \
&& cd /tmp
# TorchServe用のユーザー「model-server」を追加
RUN useradd -m model-server \
&& mkdir -p ${WORK_DIR}/tmp \
$$ mkdir -p ${MODEL_STORE_DIR}
# ビルド用イメージ内にインストールしたPythonモジュールを利用する
COPY --chown=model-server --from=build-image /home/venv /home/venv
# pathにPython仮想環境を追加
ENV PATH="/home/venv/bin:$PATH"
# 8080:REST推論用API、8081:RESTモデル管理用API、8082:REST指標用API
EXPOSE 8443 8444 8445
# 環境変数TEMPを設定
ENV TEMP=${WORK_DIR}/tmp
# 全てのフォルダ・ファイルを実行用フォルダにコピー
COPY . ${WORK_DIR}
# 実行用ディレクトリ配下全ての所有者をmodel-serverに変更する
RUN chown -R model-server ${WORK_DIR}
# ユーザーをmdoel-serverに変更
USER model-server
# モデルのアーカイブと、TorchServeの起動
ENTRYPOINT ["/bin/bash","./run.sh"]
DockerFileのビルドとRUN
作成したDockerFileをビルドします。
が、その前にDockerファイルをビルドする際に利用する変数を設定します。
※具体的な設定値は利用している環境等にあわせて適宜変更してください。
DOCKER_TAG="torchserve-docker:gpu" # Dockerイメージの名前とタグ
BASE_IMAGE="ubuntu:18.04" # ベースイメージを指定
DOCKER_FILE=".docker_torchserve_deploy/Dockerfile" # Dockerファイルまでのパス
CUDA_VERSION="cu110" # CUDAのバージョン
TORCH_VER="1.7.1" # PyTochのバージョン
TORCH_VISION_VER="0.8.2" # TorchVisionのバージョン
続いて以下のコマンドを実行して、Dockerイメージをビルドします。
sudo DOCKER_BUILDKIT=1 docker build --file $DOCKER_FILE \
--build-arg BASE_IMAGE=${BASE_IMAGE} \
--build-arg CUDA_VERSION=${CUDA_VERSION} \
--build-arg TORCH_VER=${TORCH_VER} \
--build-arg TORCH_VISION_VER=${TORCH_VISION_VER} \
-t $DOCKER_TAG .
上記コマンドでは、docker buildの際にDOCKER_BUILDKIT=1
を設定することで、Buildkitを有効にしています。
これによって、dockerをビルドした時のキャッシュが保存され、次回から効率的にdockerのビルドが可能になります。
buildkitについては、以下のページがとても参考になりました。
- 「buildkit-におけるキャッシュインキャッシュアウトの仕組み」
- 「[Dockerイメージのビルドで使うキャッシュの種類 - レイヤーキャッシュ、BuildKitの--mount=type=cache」
ビルドが問題なく終了したら、以下のコマンドでDocker を実行します。
sudo docker run --rm -itd -p 8443:8443 -p 8444:8444 -p 8445:8445 ${DOCKER_TAG}
推論APIの実行
では、デプロイしたAPIに推論リクエストを送ってみましょう。
以下のコマンドで推論をするための画像を取得します。
wget https://raw.githubusercontent.com/pytorch/serve/master/docs/images/kitten_small.jpg
そして、この画像を推論用APIに対して送信します。
※ 今回は自己署名証明を使っているため、 -kを追加してSSLエラーを無視しています。
sudo curl "https://localhost:8443/predictions/vgg11" -T kitten_small.jpg -k
以下のような結果が返ってくれば、推論は成功です。
{
"tabby": 0.3414705693721771,
"Egyptian_cat": 0.329368531703949,
"lynx": 0.192706897854805,
"tiger_cat": 0.0975274071097374,
"Persian_cat": 0.009637265466153622
}
おわりに
今回は、TorchServeをDockerコンテナー化して利用する方法を紹介しました。
次回は、今回作成したDockerコンテナーをAzure Kubernetes Servicesにデプロイし、認証機能付きのAPIを作成したいと考えています。
(あくまで考えているだけなので悪しからず。。。)