{ "cells": [ { "cell_type": "markdown", "id": "9cbd5483", "metadata": {}, "source": [ "# Amazon SageMaker の PyTorch 推論機能をまとめてご紹介\n", "\n", "> *このノートブックは、Amazon SageMaker Studio の `Python 3 (PyTorch 1.6 Python 3.6 CPU Optimized)` カーネルや、SageMaker ノートブックインスタンスの `conda_pytorch_p36` カーネルでご利用ください。*\n", "\n", "このサンプルノートブックでは、Amazon SageMaker の主に推論関連の機能を PyTorch を使って使用する方法をご紹介します。\n", "\n", "以下の手順 1, 2 の[必須]の部分を実行したあとは、マルチモデルエンドポイント、非同期推論、カスタムコンテナイメージなどのどこから実施いただいても構いません。" ] }, { "cell_type": "markdown", "id": "531fbb19", "metadata": { "toc": true }, "source": [ "

Table of Contents

\n", "
" ] }, { "cell_type": "markdown", "id": "f71000cd", "metadata": {}, "source": [ "## [必須] ライブラリや SDK の準備\n", "\n", "以下のセルを実行したあと、一度このノートブックのカーネルを再起動してください。再起動は、メニューの「Kernel」->「Restart」で実行できます。再起動後、以下のセルを再度実行する必要はありません。\n", "\n", "以下のエラーが表示されることがありますが、ノートブックの実行には問題ありません。\n", "```\n", "ERROR: pip's dependency resolver does not currently take into account all the packages that are installed. This behaviour is the source of the following dependency conflicts.\n", "aiobotocore 1.3.0 requires botocore<1.20.50,>=1.20.49, but you have botocore 1.21.37 which is incompatible.\n", "```" ] }, { "cell_type": "code", "execution_count": null, "id": "eed84ac0", "metadata": {}, "outputs": [], "source": [ "import sys\n", "!{sys.executable} -m pip install --upgrade pip --quiet\n", "!{sys.executable} -m pip install -U awscli sagemaker boto3 --quiet" ] }, { "cell_type": "code", "execution_count": null, "id": "d41ba950", "metadata": {}, "outputs": [], "source": [ "import sagemaker\n", "sagemaker.__version__" ] }, { "cell_type": "code", "execution_count": null, "id": "dc49c589", "metadata": { "scrolled": true }, "outputs": [], "source": [ "# Python Built-Ins:\n", "from datetime import datetime\n", "import os\n", "import json\n", "import logging\n", "from tempfile import TemporaryFile\n", "import time\n", "\n", "# External Dependencies:\n", "import boto3\n", "from botocore.exceptions import ClientError\n", "import numpy as np\n", "import sagemaker\n", "from sagemaker.multidatamodel import MultiDataModel\n", "from sagemaker.pytorch import PyTorch\n", "\n", "sagemaker_session = sagemaker.Session()\n", "role = sagemaker.get_execution_role()\n", "\n", "boto_session = boto3.session.Session()\n", "sagemaker_client = boto_session.client(\"sagemaker\")\n", "sagemaker_runtime = boto_session.client(\"sagemaker-runtime\")\n", "region = boto_session.region_name\n", "account_id = boto3.client('sts').get_caller_identity().get('Account')\n", "\n", "# Configuration:\n", "bucket_name = sagemaker_session.default_bucket()\n", "prefix = \"mnist\"\n", "output_path = f\"s3://{bucket_name}/{prefix}\"" ] }, { "cell_type": "code", "execution_count": null, "id": "be06c94f", "metadata": {}, "outputs": [], "source": [ "import os, json\n", "NOTEBOOK_METADATA_FILE = \"/opt/ml/metadata/resource-metadata.json\"\n", "if os.path.exists(NOTEBOOK_METADATA_FILE):\n", " with open(NOTEBOOK_METADATA_FILE, \"rb\") as f:\n", " metadata = json.loads(f.read())\n", " domain_id = metadata.get(\"DomainId\")\n", " on_studio = True if domain_id is not None else False\n", "print(\"Is this notebook runnning on Studio?: {}\".format(on_studio))" ] }, { "cell_type": "markdown", "id": "e050db11", "metadata": {}, "source": [ "## [必須] サンプルデータセット: MNIST\n", "\n", "MNIST は、手書き数字の分類に広く用いられているデータセットです。このデータセットは、7万枚のラベル付き 28x28画素の手書き数字のグレースケール画像から構成されています。このデータセットは、6万枚の学習用画像と 1万枚のテスト用画像に分かれています。\n", "\n", "このサンプルノートブックでは、MNISTデータをパブリックの S3バケットからダウンロードし、`bucket_name` に設定したデフォルトの SageMakerバケットにアップロードします。" ] }, { "cell_type": "code", "execution_count": null, "id": "e14ee0ee", "metadata": {}, "outputs": [], "source": [ "!aws s3 cp s3://fast-ai-imageclas/mnist_png.tgz . --no-sign-request\n", "if on_studio:\n", " !tar -xzf mnist_png.tgz -C /opt/ml --no-same-owner\n", "else:\n", " !tar -xvzf mnist_png.tgz" ] }, { "cell_type": "code", "execution_count": null, "id": "637e3354", "metadata": {}, "outputs": [], "source": [ "from torchvision import datasets, transforms\n", "from torch.utils.data import DataLoader\n", "import torch\n", "import os\n", "\n", "root_dir_studio = '/opt/ml'\n", "data_dir = os.path.join(root_dir_studio,'data') if on_studio else 'data'\n", "training_dir = os.path.join(root_dir_studio,'mnist_png/training') if on_studio else 'mnist_png/training'\n", "test_dir = os.path.join(root_dir_studio,'mnist_png/testing') if on_studio else 'mnist_png/testing'\n", "\n", "os.makedirs(data_dir, exist_ok=True)\n", "\n", "training_data = datasets.ImageFolder(root=training_dir,\n", " transform=transforms.Compose([\n", " transforms.Grayscale(),\n", " transforms.ToTensor(),\n", " transforms.Normalize((0.1307,), (0.3081,))]))\n", "test_data = datasets.ImageFolder(root=test_dir,\n", " transform=transforms.Compose([\n", " transforms.Grayscale(),\n", " transforms.ToTensor(),\n", " transforms.Normalize((0.1307,), (0.3081,))]))\n", "\n", "training_data_loader = DataLoader(training_data, batch_size=len(training_data))\n", "training_data_loaded = next(iter(training_data_loader))\n", "torch.save(training_data_loaded, os.path.join(data_dir, 'training.pt'))\n", "\n", "test_data_loader = DataLoader(test_data, batch_size=len(test_data))\n", "test_data_loaded = next(iter(test_data_loader))\n", "torch.save(test_data_loaded, os.path.join(data_dir, 'test.pt'))" ] }, { "cell_type": "markdown", "id": "e8f568ee", "metadata": {}, "source": [ "training.pt, test.pt として保存した学習用、テスト用データを以下のコマンドで S3 にアップロードします。" ] }, { "cell_type": "code", "execution_count": null, "id": "2d4d0ff6", "metadata": {}, "outputs": [], "source": [ "inputs = sagemaker_session.upload_data(path=data_dir, bucket=bucket_name, key_prefix=os.path.join(prefix, 'data'))\n", "print('input spec (in this case, just an S3 path): {}'.format(inputs))" ] }, { "cell_type": "markdown", "id": "9cae3709", "metadata": {}, "source": [ "推論テスト用に、テスト用データセットからランダムに5つデータを取得します。" ] }, { "cell_type": "code", "execution_count": null, "id": "0b21e1b2", "metadata": {}, "outputs": [], "source": [ "%matplotlib inline\n", "import random\n", "import numpy as np\n", "import matplotlib.pyplot as plt\n", "\n", "raw_test_data = datasets.ImageFolder(root=test_dir,\n", " transform=transforms.Compose([\n", " transforms.Grayscale(),\n", " transforms.ToTensor()]))\n", "num_samples = 5\n", "indices = random.sample(range(len(raw_test_data) - 1), num_samples)\n", "raw_images = np.array([raw_test_data[i][0].numpy() for i in indices])\n", "raw_labels = np.array([raw_test_data[i][1] for i in indices])\n", "\n", "\n", "for i in range(num_samples):\n", " plt.subplot(1,num_samples,i+1)\n", " plt.imshow(raw_images[i].reshape(28, 28), cmap='gray')\n", " plt.title(raw_labels[i])\n", " plt.axis('off')\n", " \n", "images = np.array([test_data[i][0].numpy() for i in indices])" ] }, { "cell_type": "markdown", "id": "b23b1aff", "metadata": {}, "source": [ "## SageMaker SDK を使ってマルチモデルエンドポイントを作成\n", "\n", "Amazon SageMaker の [マルチモデルエンドポイント](https://docs.aws.amazon.com/sagemaker/latest/dg/multi-model-endpoints.html) では、最大数千のモデルをシームレスにホストするエンドポイントを作成することができます。これらのエンドポイントは、共通の推論コンテナから提供される多数のモデルのいずれかがオンデマンドで呼び出される必要があり、頻繁に呼び出されないモデルで推論する際に多少のレイテンシーが生じても問題ないユースケースに適しています。低い推論レイテンシーが常時必要とされるアプリケーションには、従来のエンドポイントが最適です。\n", "\n", "変動するレイテンシーが許容範囲内であり、コストの最適化がより重要な場合、マルチモデルエンドポイントのしくみを使って [A/B/n テスト](https://aws.amazon.com/jp/blogs/news/a-b-testing-ml-models-in-production-using-amazon-sagemaker/) を行うお客様もいらっしゃいます。\n", "\n", "このノートブックでは、マルチモデルエンドポイントの作成および使用方法を示すために、SageMaker の [PyTorchフレームワークコンテナ](https://sagemaker.readthedocs.io/en/stable/frameworks/pytorch/using_pytorch.html) で学習されたモデルを使用した例をご紹介します。ここでは簡単に A/B シナリオを取り上げ、2つのモデルを学習してエンドポイントにデプロイします。\n", "\n", "PyTorch で マルチモデルエンドポイントを作成するには、学習スクリプトに 2つの関数を含めておく必要があります。これは、推論時に使用するソースコードなどを model.tar.gz に含めるためです。このサンプルでは、`code/train.py` にマルチモデルエンドポイント用の関数 `enable_sm_oneclick_deploy()`, `enable_torchserve_multi_model()` が定義され、それらが `save_model()` の中でで呼び出されています。\n", "\n", "マルチモデルエンドポイントの仕組みについては [こちらのドキュメント](https://docs.aws.amazon.com/ja_jp/sagemaker/latest/dg/multi-model-endpoints.html) をご参照ください。\n", "\n", "カスタムコンテナイメージを使ってマルチモデルエンドポイントを作成する場合は、[こちらのサンプルコード](https://github.com/aws/amazon-sagemaker-examples/tree/master/advanced_functionality/multi_model_bring_your_own) や [こちらのドキュメント](https://docs.aws.amazon.com/sagemaker/latest/dg/build-multi-model-build-container.html) をご参照ください。\n", "\n" ] }, { "cell_type": "markdown", "id": "d2b68cfe", "metadata": {}, "source": [ "### ローカルモードでモデルを学習\n", "\n", "学習スクリプトを変更したときや、コンテナイメージを更新したとき、問題なく動作するかどうかを簡単に確認したいことがあります。ローカルモードを使うことで、新しいインスタンスを起動するのではなく、ノートブックインスタンス上で(ローカルPC で `estimator.fit()` を実行する場合はローカルPC上で)学習ジョブを立ち上げることができるため、インスタンス起動を待つことなくすぐに動作確認をすることができます。方法は、Estimator を作成する際の引数 `instance_type` に `local` と設定するだけです。**なお、SageMaker Studio ではローカルモードを使用することはできません。**" ] }, { "cell_type": "code", "execution_count": null, "id": "b49da815", "metadata": {}, "outputs": [], "source": [ "if on_studio:\n", " instance_type = 'ml.m4.xlarge'\n", "else:\n", " instance_type = 'local'\n", "\n", "estimator = PyTorch(entry_point=\"train.py\",\n", " source_dir='code_mme',\n", " role=role,\n", " framework_version='1.8.0',\n", " py_version='py3',\n", " instance_count=1,\n", " instance_type=instance_type,\n", " hyperparameters={\n", " 'batch-size':128,\n", " 'lr': 0.01,\n", " 'epochs': 1,\n", " 'backend': 'gloo'\n", " })" ] }, { "cell_type": "code", "execution_count": null, "id": "d8855bbc", "metadata": {}, "outputs": [], "source": [ "estimator.fit({'training': inputs})" ] }, { "cell_type": "markdown", "id": "f4fbbf7c", "metadata": {}, "source": [ "ローカルモードで、ノートブックインスタンス上に推論エンドポイントを作成します。\n", "\n", "モデルの学習に使用したスクリプト(このサンプルノートブックの場合 `train.py`)に推論用のコード(`model_fn()`: 必須、`input_fn()`: 任意)が記載されていれば、`estimatorA.deploy()` で推論エンドポイントをデプロイすることも可能です。今回は、推論用スクリプト `inference.py` を使用するため、`create_model` で作成した model を使って推論エンドポイントを起動します。" ] }, { "cell_type": "code", "execution_count": null, "id": "9eac0e9b", "metadata": {}, "outputs": [], "source": [ "model = estimator.create_model(role=role, source_dir=\"code_mme\", entry_point=\"inference.py\")\n", "predictor = model.deploy(initial_instance_count=1, instance_type=instance_type)" ] }, { "cell_type": "markdown", "id": "ef30b26b", "metadata": {}, "source": [ "上記セルの左側にある `In [*]` のアスタリスクが数字に変わったら、以下のセルを実行して推論します。" ] }, { "cell_type": "code", "execution_count": null, "id": "e626e3b1", "metadata": {}, "outputs": [], "source": [ "\n", "prediction = predictor.predict(images)\n", "predicted_label = prediction.argmax(axis=1)\n", "print('The predicted labels are: {}'.format(predicted_label))" ] }, { "cell_type": "markdown", "id": "70784d8b", "metadata": {}, "source": [ "問題なく推論できることが確認できました。以下のコマンドで predictor を削除します。" ] }, { "cell_type": "code", "execution_count": null, "id": "b02bf5b1", "metadata": {}, "outputs": [], "source": [ "predictor.delete_endpoint(delete_endpoint_config=True)" ] }, { "cell_type": "markdown", "id": "e0c9f805", "metadata": {}, "source": [ "### 複数のモデルを学習\n", "\n", "ここからは、同じデータセットを使って 2つのモデルを学習させます。モデルの学習には SageMaker の PyTorch フレームワークコンテナを使用します。\n", "\n", "シンプルな例として、同じソースコードを使って、ハイパーパラメタを少し変えてモデル A と B の2つを作成します。" ] }, { "cell_type": "code", "execution_count": null, "id": "1b95b394", "metadata": {}, "outputs": [], "source": [ "def get_estimator(base_job_name, hyperparam_overrides={}):\n", " hyperparameters = {\n", " \"epochs\": 20,\n", " \"lr\": 1e-3,\n", " }\n", " for k, v in hyperparam_overrides.items():\n", " hyperparameters[k] = v\n", "\n", "\n", " return PyTorch(\n", " base_job_name=base_job_name,\n", " entry_point=\"train.py\",\n", " source_dir=\"code_mme\", # directory of your training script\n", " role=role,\n", " framework_version=\"1.8.0\",\n", " py_version=\"py3\",\n", " instance_type=\"ml.c4.xlarge\",\n", " instance_count=1,\n", " output_path=output_path,\n", " hyperparameters=hyperparameters,\n", " )\n", "\n", "\n", "estimatorA = get_estimator(base_job_name=\"mnist-a\", hyperparam_overrides={\"batch-size\": 128})\n", "estimatorB = get_estimator(base_job_name=\"mnist-b\", hyperparam_overrides={\"batch-size\": 64})" ] }, { "cell_type": "markdown", "id": "60ab31d5", "metadata": {}, "source": [ "デフォルトでは、 [SageMaker Python SDK](https://sagemaker.readthedocs.io/en/stable/) の [Estimator.fit()](https://sagemaker.readthedocs.io/en/stable/api/training/estimators.html#sagemaker.estimator.EstimatorBase.fit) メソッドを呼び出すと、トレーニングジョブが完了するまで待機状態となり、進捗情報とログをノートブックにストリーミングします。\n", "\n", "しかし、サポートされている設定はこれだけではありません。例えば、`wait=False` を設定して非同期にジョブを開始したり、過去に開始されたジョブを遡って `wait()` することも可能です(オプションでログを取得することもできます)。\n", "\n", "以下のセクションでは、2つのトレーニングジョブを並行して起動し、Bの実行時にログをストリームし、Aが完了していなければAの完了を待ちます。" ] }, { "cell_type": "code", "execution_count": null, "id": "4275070c", "metadata": { "scrolled": true }, "outputs": [], "source": [ "estimatorA.fit({\"training\": inputs}, wait=False)\n", "print(\"Started estimator A training in background (logs will not show)\")\n", "\n", "print(\"Training estimator B with logs:\")\n", "estimatorB.fit({\"training\": inputs})\n", "\n", "print(\"\\nWaiting for estimator A to complete:\")\n", "estimatorA.latest_training_job.wait(logs=False)" ] }, { "cell_type": "markdown", "id": "e9f62417", "metadata": {}, "source": [ "### 単独でのモデルのデプロイ\n", "\n", "モデルA を推論エンドポイントにデプロイしてみます。エンドポイントの起動が完了するまでに 10分ほどかかることがあります。\n", "\n", "モデルの学習に使用したスクリプト(このサンプルノートブックの場合 `train.py`)に推論用のコード(`model_fn()`: 必須、`input_fn()`: 任意)が記載されていれば、`estimatorA.deploy(...)` で推論エンドポイントをデプロイすることも可能です。今回は、推論用スクリプト `inference.py` を使用するため、以下のコードで推論エンドポイントを起動します。" ] }, { "cell_type": "code", "execution_count": null, "id": "b6c7ce83", "metadata": {}, "outputs": [], "source": [ "modelA = estimatorA.create_model(role=role, source_dir=\"code_mme\", entry_point=\"inference.py\")" ] }, { "cell_type": "code", "execution_count": null, "id": "5ceccb15", "metadata": {}, "outputs": [], "source": [ "predictorA = modelA.deploy(\n", " initial_instance_count=1,\n", " instance_type=\"ml.c5.xlarge\",\n", ")" ] }, { "cell_type": "code", "execution_count": null, "id": "136c79ff", "metadata": {}, "outputs": [], "source": [ "start_time = time.time()\n", "prediction = predictorA.predict(images)\n", "duration = time.time() - start_time\n", "\n", "print(f\"Model took {int(duration * 1000/num_samples):,d} ms\")\n", "\n", "predicted_label = prediction.argmax(axis=1)\n", "\n", "print('The GT labels are: {}'.format(raw_labels))\n", "print('The predicted labels are: {}'.format(predicted_label))" ] }, { "cell_type": "markdown", "id": "993f98e8", "metadata": {}, "source": [ "すでに起動済みのエンドポイントを使って推論する場合は、エンドポイント名を使って以下の手順で predictor を作成することができます。" ] }, { "cell_type": "code", "execution_count": null, "id": "82547eda", "metadata": {}, "outputs": [], "source": [ "from sagemaker.pytorch.model import PyTorchPredictor\n", "\n", "predictorA2 = PyTorchPredictor(\n", " endpoint_name=predictorA.endpoint_name\n", ")\n" ] }, { "cell_type": "code", "execution_count": null, "id": "ef3414e4", "metadata": {}, "outputs": [], "source": [ "\n", "prediction = predictorA.predict(images)\n", "predicted_labelA = prediction.argmax(axis=1)\n", "\n", "prediction = predictorA2.predict(images)\n", "predicted_labelA2 = prediction.argmax(axis=1)\n", "\n", "print('The GT labels are: {}'.format(raw_labels))\n", "print('The predicted labels (A) are: {}'.format(predicted_labelA))\n", "print('The predicted labels (A2) are: {}'.format(predicted_labelA2))\n" ] }, { "cell_type": "markdown", "id": "76dc8a0d", "metadata": {}, "source": [ "作成した推論エンドポイントは、起動している間料金が発生するので、不要になったら速やかに削除しましょう。以下のコマンドを実行するか、SageMaker のコンソールのエンドポイント一覧画面から削除してください。" ] }, { "cell_type": "code", "execution_count": null, "id": "c5c79aa8", "metadata": {}, "outputs": [], "source": [ "predictorA.delete_endpoint(delete_endpoint_config=True)" ] }, { "cell_type": "markdown", "id": "458de3c9", "metadata": {}, "source": [ "### Estimator から SageMaker Model を作成\n", "\n", "マルチモデルエンドポイントは、共通のコンテナの中にあるモデルをロードします。そのため、まずいずれかの Estimator を使って Model を作成します。" ] }, { "cell_type": "code", "execution_count": null, "id": "091c0876", "metadata": {}, "outputs": [], "source": [ "model = estimatorA.create_model(role=role, source_dir=\"code_mme\", entry_point=\"inference.py\")" ] }, { "cell_type": "markdown", "id": "a5fd0f6c", "metadata": {}, "source": [ "### Amazon SaegMaker MultiDataModel エンティティを作成\n", "\n", " [MultiDataModel](https://sagemaker.readthedocs.io/en/stable/api/inference/multi_data_model.html) クラスを使用して、マルチモデルのエンドポイントを作成します。\n", "\n", "`sagemaker.model.Model` オブジェクトを直接渡して MultiDataModel を作成することができます。この場合、MultiDataModel がデプロイされると、エンドポイントは使用するイメージや環境変数、ネットワークの分離などの情報を引継ぎます。\n", "\n", "また、`sagemaker.model.Model` オブジェクトを明示的に渡さずに MultiDataModel を作成することも可能です。詳細については [ドキュメント](https://sagemaker.readthedocs.io/en/stable/api/inference/multi_data_model.html) をご参照ください。\n", "\n", "以下のセルを実行して、マルチモデルエンドポイントが参照するモデルを格納するためのパスを作成します。" ] }, { "cell_type": "code", "execution_count": null, "id": "02889376", "metadata": {}, "outputs": [], "source": [ "# This is where our MME will read models from on S3.\n", "multi_model_prefix = f\"{prefix}/multi-model/\"\n", "multi_model_s3uri = f\"s3://{bucket_name}/{multi_model_prefix}\"\n", "print(multi_model_s3uri)" ] }, { "cell_type": "code", "execution_count": null, "id": "19fa267c", "metadata": {}, "outputs": [], "source": [ "mme = MultiDataModel(\n", " name=\"mnist-multi-\" + datetime.now().strftime(\"%Y-%m-%d-%H-%M-%S\"),\n", " model_data_prefix=multi_model_s3uri,\n", " model=model, # passing our model\n", " sagemaker_session=sagemaker_session,\n", ")" ] }, { "cell_type": "markdown", "id": "0123a5e3", "metadata": {}, "source": [ "### マルチモデルエンドポイントをデプロイ\n", "\n", "マルチモデルエンドポイントで使用する予定のすべてのモデルの予測ワークロードに対して、適切なインスタンスタイプとインスタンス数を検討する必要があります。また、デプロイするモデルの数やサイズによって、必要なメモリ容量も変わってきます。" ] }, { "cell_type": "code", "execution_count": null, "id": "fd21c0d3", "metadata": {}, "outputs": [], "source": [ "try:\n", " predictor.delete_endpoint(delete_endpoint_config=True)\n", " print(\"Deleting previous endpoint...\")\n", " time.sleep(10)\n", "except (NameError, ClientError, FileNotFoundError):\n", " pass\n", "\n", "predictor = mme.deploy(\n", " initial_instance_count=1,\n", " instance_type=\"ml.c5.xlarge\",\n", ")\n" ] }, { "cell_type": "markdown", "id": "ea939984", "metadata": {}, "source": [ "既存のマルチモデルエンドポイントについても、単一モデルエンドポイントと同様に以下のコードで `predictor` を作成することができます。" ] }, { "cell_type": "code", "execution_count": null, "id": "55d39d03", "metadata": {}, "outputs": [], "source": [ "# from sagemaker.pytorch.model import PyTorchPredictor\n", "\n", "# mme_predictor = PyTorchPredictor(\n", "# endpoint_name=''\n", "# )" ] }, { "cell_type": "markdown", "id": "6bc13d11", "metadata": {}, "source": [ "### エンドポイントで利用可能なモデルを確認\n", "\n", "利用可能とは、`MultiDataModel` をセットアップする際に定義した S3 prefix (`model_data_prefix`) の下に、現在どのようなモデルアーティファクトが保存されているかということです。\n", "\n", "現在は、定義した S3 prefix の下にアーティファクト (例:`tar.gz` ファイル) が保存されていないため、エンドポイントでは推論リクエストに対応できるモデルが「利用できない状態になっています。\n", "\n", "以下では、エンドポイントでモデルを「利用可能」にする方法を説明します。" ] }, { "cell_type": "code", "execution_count": null, "id": "127f52e7", "metadata": {}, "outputs": [], "source": [ "# No models visible!\n", "list(mme.list_models())" ] }, { "cell_type": "markdown", "id": "570fd699", "metadata": {}, "source": [ "### 動的にモデルをエンドポイントにデプロイ\n", "\n", "`MultiDataModel` の `.add_model()` メソッドは、トレーニングジョブが保存したモデルアーティファクトを、エンドポイントが参照する場所にコピーします。\n", "\n", "以下のように、このメソッドを使い続けることで、必要に応じて稼働中のエンドポイントに動的にモデルをデプロイすることができます。\n", "\n", "`model_data_source` は、モデルアーティファクトの場所を指します(例:学習完了後に S3にアップロードされた場所)。\n", "\n", "`model_data_path` は、上記で指定した S3 prefix (例:`model_data_prefix`) への**相対パス**で、エンドポイントが推論に使用するモデルを特定するのに使用します。これは**相対パス**なので、モデルアーティファクト名のみを渡します。\n", "\n", "> **注**: ここで紹介しているように、トレーニングジョブの `model.tar.gz` の出力を直接利用するためには、トレーニングジョブが以下のような結果を出すようにする必要があります。\n", ">\n", "> 推論に必要なファイルが `code/` サブフォルダに保存されている\n", "> `code/` が(SageMaker PyTorch containers v1.6+を使用している場合)TorchServeと互換性がある形でパッケージ化されている\n", "> \n", "> この点については、`code/train.py` の `enable_sm_oneclick_deploy()` 関数と `enable_torchserve_multi_model()` 関数を参照してください。また、同じ手順を事後に行って、トレーニングジョブがアップロードした `model.tar.gz` から、マルチモデルエンドポイントに対応した新しい `model.tar.gz` を作成することもできます。" ] }, { "cell_type": "code", "execution_count": null, "id": "794b3965", "metadata": {}, "outputs": [], "source": [ "for name, est in {\"ModelA\": estimatorA, \"ModelB\": estimatorB}.items():\n", " artifact_path = est.latest_training_job.describe()[\"ModelArtifacts\"][\"S3ModelArtifacts\"]\n", " # This is copying over the model artifact to the S3 location for the MME.\n", " mme.add_model(model_data_source=artifact_path, model_data_path=name)" ] }, { "cell_type": "markdown", "id": "33a3fe56", "metadata": {}, "source": [ "### エンドポイントで利用可能なモデルを確認\n", "\n", "`MultiDataModel` の設定時に指定した S3 prefix に、モデルのアーティファクトがリストアップされていることがわかります。これにより、エンドポイントはこれらのモデルに対する推論リクエストを投げられるようになりました。" ] }, { "cell_type": "code", "execution_count": null, "id": "7d1e6bbc", "metadata": {}, "outputs": [], "source": [ "list(mme.list_models())" ] }, { "cell_type": "markdown", "id": "22c9ba16", "metadata": {}, "source": [ "### マルチモデルエンドポイントで推論の実行\n", "\n", "`mme.deploy()` を実行すると、`predictor` という変数に保存されている [RealTimePredictor](https://github.com/aws/sagemaker-python-sdk/blob/master/src/sagemaker/predictor.py#L35) が return されます。\n", "\n", "単一モデルエンドポイントと同様に、その `predictor` を使って推論を実行することができますが、マルチモデルエンドポイントでは `target_model` を使ってどのモデルを呼び出すかを推論時に指定する必要があります。\n", "\n", "以下のセルを実行することでマルチモデルエンドポイントを使った推論が実行されます。初めて以下のセルを実行したときと、2回目以降に実行したときで、推論にかかる時間はどのように変化したでしょうか?" ] }, { "cell_type": "code", "execution_count": null, "id": "5fae8b9c", "metadata": {}, "outputs": [], "source": [ "start_time = time.time()\n", "prediction = predictor.predict(images, target_model=\"ModelB\")\n", "duration = time.time() - start_time\n", "\n", "print(f\"Model took {int(duration * 1000/num_samples):,d} ms per image\")\n", "predicted_label = prediction.argmax(axis=1)\n", "\n", "print('The GT labels are: {}'.format(raw_labels))\n", "print('The predicted labels are: {}'.format(predicted_label))" ] }, { "cell_type": "markdown", "id": "695cb0de", "metadata": {}, "source": [ "### エンドポイントの削除\n", "\n", "エンドポイントは、 [SageMaker pricing page](https://aws.amazon.com/sagemaker/pricing/) に記載の通り、稼働している時間に応じて課金されるため、使用しなくなったら削除する必要があります。ここでは、整理整頓のためにエンドポイントの設定も削除します。SageMaker のコンソールのエンドポイント一覧画面で、エンドポイントが削除されているかどうかを確認できます(一覧に表示されていなければ削除されたということです)。" ] }, { "cell_type": "code", "execution_count": null, "id": "2c13a578", "metadata": {}, "outputs": [], "source": [ "predictor.delete_endpoint(delete_endpoint_config=True)" ] }, { "cell_type": "markdown", "id": "c7466c12", "metadata": {}, "source": [ "### マルチモデルエンドポイントのモデルの追加\n", "\n", "モデルを更新するには、上記と同じ方法で、例えば `ModelA-2` のように新しいモデルとして追加します。\n", "\n", "Amazon S3 に保存されたモデルアーティファクトを直接変更することは避けるべきです。なぜなら、古いバージョンのモデルがエンドポイントの実行中のコンテナや、エンドポイント上のインスタンスのストレージ・ボリュームにまだロードされている可能性があるからです。その場合、更新された S3 のモデルアーティファクトではなく、コンテナやストレージに保存されている古いバージョンのモデルが使用されることになります。\n", "\n", "一度エンドポイントを停止して、新しいモデルを再デプロイすることが可能です。\n", "\n" ] }, { "cell_type": "markdown", "id": "16f1d038", "metadata": {}, "source": [ "## マルチモデルエンドポイントのトラフィック分散\n", "\n", "ひとつのエンドポイントに複数のモデルをデプロイし、そのエンドポイントに推論リクエストを投げた際のトラフィックを指定した割合で分散させることができます。この機能を使って A/B テストを行うことができます。この機能を使う場合は Amazon SageMaker Python SDK ではなく AWS SDK for Python (boto3) を使用します。\n", "\n", "### 2つのモデルを学習\n", "\n", "以下のセルを実行して、エンドポイントにデプロイする 2つのモデルを学習します。**前のパートで既に estimatorA/B を学習済みの場合は以下のセルはスキップ可能です。**" ] }, { "cell_type": "code", "execution_count": null, "id": "87fdbaba", "metadata": {}, "outputs": [], "source": [ "def get_estimator(base_job_name, hyperparam_overrides={}):\n", " hyperparameters = {\n", " \"epochs\": 20,\n", " \"lr\": 1e-3,\n", " }\n", " for k, v in hyperparam_overrides.items():\n", " hyperparameters[k] = v\n", "\n", "\n", " return PyTorch(\n", " base_job_name=base_job_name,\n", " entry_point=\"train.py\",\n", " source_dir=\"code_mme\", # directory of your training script\n", " role=role,\n", " framework_version=\"1.8.0\",\n", " py_version=\"py3\",\n", " instance_type=\"ml.c4.xlarge\",\n", " instance_count=1,\n", " output_path=output_path,\n", " hyperparameters=hyperparameters,\n", " )\n", "\n", "\n", "estimatorA = get_estimator(base_job_name=\"mnist-a\", hyperparam_overrides={\"batch-size\": 128})\n", "estimatorB = get_estimator(base_job_name=\"mnist-b\", hyperparam_overrides={\"batch-size\": 64})\n", "\n", "estimatorA.fit({\"training\": inputs}, wait=False)\n", "print(\"Started estimator A training in background (logs will not show)\")\n", "\n", "print(\"Training estimator B with logs:\")\n", "estimatorB.fit({\"training\": inputs})\n", "\n", "print(\"\\nWaiting for estimator A to complete:\")\n", "estimatorA.latest_training_job.wait(logs=False)" ] }, { "cell_type": "markdown", "id": "3fe532d5", "metadata": {}, "source": [ "**estimatorA/B が学習済みの状態で以降のセルを実行してください。**" ] }, { "cell_type": "markdown", "id": "1235b6f8", "metadata": {}, "source": [ "`create_model` を使って 2つのモデルを作成します。作成されたモデルは SageMaker コンソールの [推論] -> [モデル] のメニューから確認できます。`ModelDataUrl` にデプロイしたいモデルが保存されている S3 パスを指定します。このサンプルでは estimator を使ってモデルの保存パスを取得していますが、デプロイしたいモデルが保存された S3 パスがわかっていれば直接 S3 パスを `ModelDataUrl` に指定すれば OK です。" ] }, { "cell_type": "code", "execution_count": null, "id": "d53d51c1", "metadata": {}, "outputs": [], "source": [ "from sagemaker import image_uris\n", "\n", "container_inference = image_uris.retrieve(\n", " region=region, framework=\"pytorch\", \n", " version=\"1.8.1\", instance_type='ml.c5.xlarge', image_scope='inference', py_version='py36')\n", "\n", "model_nameA = f\"DEMO-pytorch-mnist-predA-{datetime.now():%Y-%m-%d-%H-%M-%S}\"\n", "model_nameB = f\"DEMO-pytorch-mnist-predB-{datetime.now():%Y-%m-%d-%H-%M-%S}\"\n", "\n", "sagemaker_session.create_model(name=model_nameA, role=role, container_defs={\n", " 'Image': container_inference,\n", " 'ModelDataUrl': estimatorA.model_data\n", "})\n", "\n", "sagemaker_session.create_model(name=model_nameB, role=role, container_defs={\n", " 'Image': container_inference,\n", " 'ModelDataUrl': estimatorB.model_data\n", "})" ] }, { "cell_type": "markdown", "id": "3e2b2eec", "metadata": {}, "source": [ "作成したモデルからそれぞれのモデルに対応する `production_variant` を作成します。" ] }, { "cell_type": "code", "execution_count": null, "id": "06b0bf5c", "metadata": {}, "outputs": [], "source": [ "from sagemaker.session import production_variant\n", "\n", "variantA = production_variant(model_name=model_nameA,\n", " instance_type=\"ml.m5.xlarge\",\n", " initial_instance_count=1,\n", " variant_name='VariantA',\n", " initial_weight=1)\n", " \n", "variantB = production_variant(model_name=model_nameB,\n", " instance_type=\"ml.m5.xlarge\",\n", " initial_instance_count=1,\n", " variant_name='VariantB',\n", " initial_weight=1)" ] }, { "cell_type": "markdown", "id": "f73dd6d1", "metadata": {}, "source": [ "作成した `production_variant` を使ってエンドポイントをデプロイします。エンドポイントの起動状況は SageMaker コンソールの [推論] -> [エンドポイント] メニューから確認できます。" ] }, { "cell_type": "code", "execution_count": null, "id": "43601d8b", "metadata": {}, "outputs": [], "source": [ "endpoint_name = f\"DEMO-pytorch-mnist-pred-{datetime.now():%Y-%m-%d-%H-%M-%S}\"\n", "print(f\"EndpointName={endpoint_name}\")\n", "\n", "sagemaker_session.endpoint_from_production_variants(\n", " name=endpoint_name,\n", " production_variants=[variantA, variantB]\n", ")\n" ] }, { "cell_type": "markdown", "id": "fa139282", "metadata": {}, "source": [ "エンドポイントを使って推論します。以下のセルでは Amazon SageMaker Python SDK を使って推論しています。この方法では、推論結果の array のみが返ってきます。" ] }, { "cell_type": "code", "execution_count": null, "id": "b5562c84", "metadata": {}, "outputs": [], "source": [ "from sagemaker.pytorch.model import PyTorchPredictor\n", "\n", "predictor = PyTorchPredictor(\n", " endpoint_name=endpoint_name\n", ")\n", "\n", "start_time = time.time()\n", "prediction = predictor.predict(images)\n", "duration = time.time() - start_time\n", "\n", "print(f\"Model took {int(duration * 1000/num_samples):,d} ms per image\")\n", "predicted_label = prediction.argmax(axis=1)\n", "\n", "print('The GT labels are: {}'.format(raw_labels))\n", "print('The predicted labels are: {}'.format(predicted_label))\n", "\n", "print(prediction)" ] }, { "cell_type": "markdown", "id": "269d69f2", "metadata": {}, "source": [ "boto3 の `invoke_endpoint` を使って推論を実行すると、推論結果だけではなくどちらのモデルを使って推論が実行されたかなどの情報も併せて取得できます。" ] }, { "cell_type": "code", "execution_count": null, "id": "2216d36b", "metadata": {}, "outputs": [], "source": [ "import io\n", "buffer= io.BytesIO()\n", "np.save(buffer, images)\n", "\n", "response = sagemaker_runtime.invoke_endpoint(EndpointName=endpoint_name,\n", " ContentType=\"application/x-npy\",\n", " Body=buffer.getvalue())\n", "\n", "response" ] }, { "cell_type": "markdown", "id": "a2e698ae", "metadata": {}, "source": [ "推論結果は以下のコードで取得できます。" ] }, { "cell_type": "code", "execution_count": null, "id": "6cb30394", "metadata": {}, "outputs": [], "source": [ "import json\n", "body = response['Body']\n", "prediction = np.array(json.load(body))\n", "predicted_label = prediction.argmax(axis=1)\n", "\n", "print('The GT labels are: {}'.format(raw_labels))\n", "print('The predicted labels are: {}'.format(predicted_label))" ] }, { "cell_type": "markdown", "id": "d5a442f0", "metadata": {}, "source": [ "### エンドポイントの削除\n", "\n", "エンドポイントは、 [SageMaker pricing page](https://aws.amazon.com/sagemaker/pricing/) に記載の通り、稼働している時間に応じて課金されるため、使用しなくなったら削除する必要があります。ここでは、整理整頓のためにエンドポイントの設定も削除します。SageMaker のコンソールのエンドポイント一覧画面で、エンドポイントが削除されているかどうかを確認できます(一覧に表示されていなければ削除されたということです)。" ] }, { "cell_type": "code", "execution_count": null, "id": "ead6f7f7", "metadata": {}, "outputs": [], "source": [ "predictor.delete_endpoint(delete_endpoint_config=True)" ] }, { "cell_type": "markdown", "id": "2727475d", "metadata": {}, "source": [ "## マルチコンテナエンドポイント\n", "\n", "上でご紹介したマルチモデルエンドポイントは、ひとつの推論エンドポイントにひとつのコンテナ、複数のモデルをデプロイするものです。デプロイしたいモデルがそれぞれ異なるコンテナイメージを必要とする場合、マルチコンテナエンドポイントをご利用できます。\n", "\n", "このサンプルノートブックでは、SageMaker が用意したコンテナイメージを使っていますが、カスタムコンテナイメージを使ってマルチコンテナエンドポイントを作成することも可能です。カスタムコンテナイメージを使う方法は、このノートブックの「6 カスタムコンテナイメージで学習・推論」をご参照ください。\n", "\n", "### デプロイするモデルの学習\n", "\n", "PyTorch 1.8.0 と 1.8.1 のコンテナイメージを使ってそれぞれモデルを学習します。" ] }, { "cell_type": "code", "execution_count": null, "id": "3f3cfb67", "metadata": {}, "outputs": [], "source": [ "from sagemaker import image_uris\n", "\n", "# Specify an AWS container image and region as desired\n", "container_mce1 = image_uris.retrieve(\n", " region=region, framework=\"pytorch\", \n", " version=\"1.8.0\", instance_type='ml.c5.xlarge', image_scope='training', py_version='py36')\n", "container_mce1" ] }, { "cell_type": "code", "execution_count": null, "id": "0515d383", "metadata": {}, "outputs": [], "source": [ "estimator_mce1 = PyTorch(entry_point=\"train.py\",\n", " image_uri=container_mce1,\n", " source_dir='code_byoc',\n", " role=role,\n", " instance_count=1,\n", " instance_type='ml.c5.xlarge',\n", "# instance_type='local',\n", " hyperparameters={\n", " 'batch-size':128,\n", " 'lr': 0.01,\n", " 'epochs': 10,\n", " 'backend': 'gloo'\n", " })\n", "estimator_mce1.fit({'training': inputs}, wait=False)" ] }, { "cell_type": "code", "execution_count": null, "id": "88aec38f", "metadata": {}, "outputs": [], "source": [ "from sagemaker import image_uris\n", "\n", "# Specify an AWS container image and region as desired\n", "container_mce2 = image_uris.retrieve(\n", " region=region, framework=\"pytorch\", \n", " version=\"1.8.1\", instance_type='ml.c5.xlarge', image_scope='training', py_version='py36')\n", "container_mce2" ] }, { "cell_type": "code", "execution_count": null, "id": "624138e2", "metadata": {}, "outputs": [], "source": [ "estimator_mce2 = PyTorch(entry_point=\"train.py\",\n", " image_uri=container_mce2,\n", " source_dir='code_byoc',\n", " role=role,\n", " instance_count=1,\n", " instance_type='ml.c5.xlarge',\n", "# instance_type='local',\n", " hyperparameters={\n", " 'batch-size':128,\n", " 'lr': 0.01,\n", " 'epochs': 10,\n", " 'backend': 'gloo'\n", " })\n", "estimator_mce2.fit({'training': inputs})" ] }, { "cell_type": "markdown", "id": "2aefd4a7", "metadata": {}, "source": [ "以下のセルを実行して、推論用コンテナイメージの URI を取得します。" ] }, { "cell_type": "code", "execution_count": null, "id": "5d0769f4", "metadata": {}, "outputs": [], "source": [ "from sagemaker import image_uris\n", "\n", "# Specify an AWS container image and region as desired\n", "container_pt180_mce1 = image_uris.retrieve(\n", " region=region, framework=\"pytorch\", \n", " version=\"1.8.0\", instance_type='ml.c5.xlarge', image_scope='inference', py_version='py36')\n", "print(container_pt180_mce1)\n", "\n", "container_pt181_mce2 = image_uris.retrieve(\n", " region=region, framework=\"pytorch\", \n", " version=\"1.8.1\", instance_type='ml.c5.xlarge', image_scope='inference', py_version='py36')\n", "print(container_pt181_mce2)" ] }, { "cell_type": "markdown", "id": "84e4f223", "metadata": {}, "source": [ "### コンテナ呼び出し情報の設定\n", "\n", "エンドポイントにデプロイしたコンテナを呼び出す際に必要な情報を辞書形式でまとめて設定します。\n", "\n", "このノートブックでは、2つの PyTorch モデルをデプロイしていますが、PyTorch モデルと Tensorflow モデルをデプロイするなども可能です。Tensorflow のモデルをデプロイする場合は、以下のセルのコメントアウト部分の書式にのっとってコンテナの設定を作成してください。" ] }, { "cell_type": "code", "execution_count": null, "id": "d245883f", "metadata": {}, "outputs": [], "source": [ "# tensorflow_container = {\n", "# \"ContainerHostname\": \"tensorflow-mnist\",\n", "# \"Image\": tf_ecr_image_uri,\n", "# \"ModelDataUrl\": tf_mnist_model_data,\n", "# }\n", "\n", "pytorch_container1 = {\n", " \"ContainerHostname\": \"pytorch-180-mnist\",\n", " \"Image\": container_pt180_mce1,\n", " \"ModelDataUrl\": estimator_mce1.model_data,\n", " \"Environment\": {\n", " \"SAGEMAKER_PROGRAM\": \"inference.py\",\n", " \"SAGEMAKER_SUBMIT_DIRECTORY\": estimator_mce1.model_data,\n", " },\n", "}\n", "\n", "pytorch_container2 = {\n", " \"ContainerHostname\": \"pytorch-181-mnist\",\n", " \"Image\": container_pt181_mce2,\n", " \"ModelDataUrl\": estimator_mce2.model_data,\n", " \"Environment\": {\n", " \"SAGEMAKER_PROGRAM\": \"inference.py\",\n", " \"SAGEMAKER_SUBMIT_DIRECTORY\": estimator_mce2.model_data,\n", " },\n", "}" ] }, { "cell_type": "markdown", "id": "532f63e1", "metadata": {}, "source": [ "以下のセルでは、create_model APIを呼び出して、上記で作成した PyTorch 1.8.0 と PyTorch 1.8.1 の両方のコンテナの定義を含むモデルを作成します。これは、Containers 引数の下に両方のコンテナを供給する必要があります。また、InferenceExecutionConfigフィールドのModeパラメータを、各コンテナを直接呼び出す場合はDirect、コンテナを推論パイプラインとして使用する場合はSerialに設定します。デフォルトのモードはSerialです。詳細については、[Deploy multi-container endpoints](https://docs.aws.amazon.com/sagemaker/latest/dg/multi-container-endpoints.html) をご参照ください。\n", "\n", "このノートブックでは Direct 呼び出しの動作に焦点を当てているため、値を `Direct` に設定します。" ] }, { "cell_type": "code", "execution_count": null, "id": "519f2bb5", "metadata": {}, "outputs": [], "source": [ "model_name = \"mnist-multi-container\"\n", "endpoint_name = model_name + '-ep'\n", "endpoint_config_name = endpoint_name + '-config'\n", "\n", "create_model_response = sagemaker_client.create_model(\n", " ModelName=model_name,\n", " Containers=[pytorch_container1, pytorch_container2],\n", " InferenceExecutionConfig={\"Mode\": \"Direct\"},\n", " ExecutionRoleArn=role,\n", ")" ] }, { "cell_type": "code", "execution_count": null, "id": "969cb84b", "metadata": {}, "outputs": [], "source": [ "endpoint_config = sagemaker_client.create_endpoint_config(\n", " EndpointConfigName=endpoint_config_name,\n", " ProductionVariants=[\n", " {\n", " \"VariantName\": \"prod\",\n", " \"ModelName\": model_name,\n", " \"InitialInstanceCount\": 1,\n", " \"InstanceType\": \"ml.c5.4xlarge\",\n", " },\n", " ],\n", ")" ] }, { "cell_type": "markdown", "id": "40728b59", "metadata": {}, "source": [ "### マルチコンテナエンドポイントの作成\n", "\n", "作成したエンドポイント設定を使ってエンドポイントを作成します。" ] }, { "cell_type": "code", "execution_count": null, "id": "dfe63428", "metadata": {}, "outputs": [], "source": [ "endpoint = sagemaker_client.create_endpoint(\n", " EndpointName=endpoint_name, EndpointConfigName=endpoint_config_name\n", ")" ] }, { "cell_type": "code", "execution_count": null, "id": "5c234357", "metadata": {}, "outputs": [], "source": [ "describe_endpoint = sagemaker_client.describe_endpoint(EndpointName=endpoint_name)\n", "\n", "endpoint_status = describe_endpoint[\"EndpointStatus\"]\n", "\n", "while endpoint_status != \"InService\":\n", " print(\"Current endpoint status is: {}, Trying again...\".format(endpoint_status))\n", " time.sleep(60)\n", " resp = sagemaker_client.describe_endpoint(EndpointName=endpoint_name)\n", " endpoint_status = resp[\"EndpointStatus\"]\n", "\n", "print(\"Endpoint status changed to 'InService'\")" ] }, { "cell_type": "markdown", "id": "73c3f270", "metadata": {}, "source": [ "### 推論の実行\n", "\n", "デプロイしたそれぞれのコンテナを指定して推論を実行します。`invoke_endpoint` の引数 `TargetContainerHostname` に使用したいコンテナ設定を指定します。" ] }, { "cell_type": "code", "execution_count": null, "id": "cca798eb", "metadata": {}, "outputs": [], "source": [ "payload = json.dumps(images.tolist())\n", "result_mce1 = sagemaker_runtime.invoke_endpoint(\n", " EndpointName=endpoint_name,\n", " ContentType=\"application/json\",\n", " Accept=\"application/json\",\n", " TargetContainerHostname=pytorch_container1['ContainerHostname'],\n", " Body=payload\n", ")\n", "\n", "pt_body = result_mce1[\"Body\"].read().decode(\"utf-8\")\n", "predicted_label1 = np.argmax(np.array(json.loads(pt_body), dtype=np.float32), axis=1).tolist()\n", "\n", "result_mce2 = sagemaker_runtime.invoke_endpoint(\n", " EndpointName=endpoint_name,\n", " ContentType=\"application/json\",\n", " Accept=\"application/json\",\n", " TargetContainerHostname=pytorch_container2['ContainerHostname'],\n", " Body=payload\n", ")\n", "\n", "pt_body = result_mce2[\"Body\"].read().decode(\"utf-8\")\n", "predicted_label2 = np.argmax(np.array(json.loads(pt_body), dtype=np.float32), axis=1).tolist()\n", "\n", "print('The GT labels are: {}'.format(raw_labels))\n", "print('The predicted labels (PyTorch 1.8.0) are: {}'.format(predicted_label1))\n", "print('The predicted labels (PyTorch 1.8.1) are: {}'.format(predicted_label2))" ] }, { "cell_type": "code", "execution_count": null, "id": "b0da2226", "metadata": {}, "outputs": [], "source": [ "\n", "response = sagemaker_client.delete_endpoint(\n", " EndpointName=endpoint_name\n", ")\n", "response = sagemaker_client.delete_endpoint_config(\n", " EndpointConfigName=endpoint_config_name\n", ")\n", "response = sagemaker_client.delete_model(\n", " ModelName=model_name\n", ")" ] }, { "cell_type": "markdown", "id": "7a466e7f", "metadata": {}, "source": [ "## 非同期推論\n", "\n", "[2021年8月に、非同期推論のための仕組みが追加されました。](https://aws.amazon.com/jp/about-aws/whats-new/2021/08/amazon-sagemaker-asynchronous-new-inference-option/)非同期推論は、推論リクエストに含めるペイロードサイズが大きかったり(最大1GBまで)、推論に時間が必要(最大15分)な場合に有効な方法です。また、非同期推論においてはオートスケーリングの設定で、最少インスタンス数をゼロに設定できます。そのため、2、3 分のコールドスタートペナルティを許容できるユースケースでは、コスト削減を図ることも可能です。" ] }, { "cell_type": "code", "execution_count": null, "id": "0e10bd07", "metadata": {}, "outputs": [], "source": [ "bucket_prefix_async = \"async-inference-demo\"\n", "resource_name_async = \"AsyncInferenceDemo\"\n", "data_dir_async = \"data-async\"" ] }, { "cell_type": "markdown", "id": "22910d99", "metadata": {}, "source": [ "### デプロイするモデルの学習\n", "\n", "まず、非同期推論エンドポイントにデプロイするためのモデルを作成します。学習ジョブが作成する model.tar.gz には、学習済みモデルだけではなく、code フォルダ以下に格納された推論用コードも必要です。今回は、`code_async/train.py` の `save_model()` 関数で学習済みモデルを保存する際に、同じフォルダにソースコード一式も含めるようにしました。これにより、S3 にアップロードされる model.tar.gz の中に code フォルダと必要なスクリプトファイルが保存されます。" ] }, { "cell_type": "code", "execution_count": null, "id": "95770115", "metadata": {}, "outputs": [], "source": [ "estimator_async = PyTorch(entry_point=\"train.py\",\n", " source_dir='code_async',\n", " role=role,\n", " framework_version='1.8.0',\n", " py_version='py3',\n", " instance_count=1,\n", " instance_type='ml.c5.xlarge',\n", " hyperparameters={\n", " 'batch-size':128,\n", " 'lr': 0.01,\n", " 'epochs': 1,\n", " 'backend': 'gloo'\n", " })\n", "estimator_async.fit({'training': inputs})" ] }, { "cell_type": "markdown", "id": "600a4a89", "metadata": {}, "source": [ "### コンテナイメージの取得\n", "\n", "次にプライマリコンテナを取得します。プライマリコンテナとは、推論コード、アーティファクト(学習済みモデルなど)、推論コードが予測のためにモデルをデプロイする際に使用するカスタム環境マップを含む Dockerイメージです。この例では、SageMaker が用意した PyTorch のコンテナイメージを取得します。" ] }, { "cell_type": "code", "execution_count": null, "id": "f559d0f5", "metadata": {}, "outputs": [], "source": [ "from sagemaker import image_uris\n", "\n", "# Specify an AWS container image and region as desired\n", "container = image_uris.retrieve(\n", " region=region, framework=\"pytorch\", \n", " version=\"1.8.0\", instance_type='ml.c5.xlarge', image_scope='inference', py_version='py36')\n", "container" ] }, { "cell_type": "markdown", "id": "1fffccb8", "metadata": {}, "source": [ "### モデルの作成\n", "\n", "ModelName、ExecutionRoleARN (Amazon SageMaker がモデルのアーティファクトやデプロイ用の docker イメージにアクセスする際に想定される IAMロールの ARN)、PrimaryContainer を指定してモデルを作成します。" ] }, { "cell_type": "code", "execution_count": null, "id": "46723b03", "metadata": {}, "outputs": [], "source": [ "model_name = resource_name_async.format(\"Model\")\n", "try:\n", " sagemaker_client.delete_model(ModelName=model_name)\n", "except:\n", " pass\n", " \n", "create_model_response = sagemaker_client.create_model(\n", " ModelName=model_name,\n", " ExecutionRoleArn=role,\n", " PrimaryContainer={\n", " \"Image\": container,\n", " \"ModelDataUrl\": estimator_async.model_data,\n", " },\n", ")\n", "\n", "print(f\"Created Model: {create_model_response['ModelArn']}\")" ] }, { "cell_type": "markdown", "id": "33c7c253", "metadata": {}, "source": [ "### エンドポイント設定の作成\n", "\n", "モデルができたら、CreateEndpointConfig でエンドポイント設定を作成します。Amazon SageMaker ホスティングサービスは、この設定を使用してモデルをデプロイします。エンドポイント設定では、CreateModel APIを使用して作成された1つまたは複数のモデルを特定し、Amazon SageMaker にプロビジョニングさせたいリソースをデプロイします。AsyncInferenceConfig オブジェクトを指定し、OutputConfig には出力先の Amazon S3 の場所を指定します。オプションで、予測結果に関する通知を送信する Amazon SNS トピックを指定できます。" ] }, { "cell_type": "code", "execution_count": null, "id": "96d2055a", "metadata": {}, "outputs": [], "source": [ "endpoint_config_name = resource_name_async.format(\"EndpointConfig\")\n", "try:\n", " sagemaker_client.delete_endpoint_config(EndpointConfigName=endpoint_config_name)\n", "except:\n", " pass\n", "\n", "create_endpoint_config_response = sagemaker_client.create_endpoint_config(\n", " EndpointConfigName=endpoint_config_name,\n", " ProductionVariants=[\n", " {\n", " \"VariantName\": \"variant1\",\n", " \"ModelName\": model_name,\n", " \"InstanceType\": \"ml.m5.xlarge\",\n", " \"InitialInstanceCount\": 1,\n", " }\n", " ],\n", " AsyncInferenceConfig={\n", " \"OutputConfig\": {\n", " \"S3OutputPath\": f\"s3://{bucket_name}/{bucket_prefix_async}/output\",\n", " # Optionally specify Amazon SNS topics\n", " # \"NotificationConfig\": {\n", " # \"SuccessTopic\": \"arn:aws:sns:us-east-2:123456789012:MyTopic\",\n", " # \"ErrorTopic\": \"arn:aws:sns:us-east-2:123456789012:MyTopic\",\n", " # }\n", " },\n", " \"ClientConfig\": {\"MaxConcurrentInvocationsPerInstance\": 4},\n", " },\n", ")\n", "print(f\"Created EndpointConfig: {create_endpoint_config_response['EndpointConfigArn']}\")" ] }, { "cell_type": "markdown", "id": "c2ab1c2e", "metadata": {}, "source": [ "### エンドポイントの作成\n", "\n", "モデルとエンドポイントの設定ができたら、CreateEndpoint API を使ってエンドポイントを作成します。エンドポイント名は、AWS アカウントの AWSリージョン内で一意である必要があります。" ] }, { "cell_type": "code", "execution_count": null, "id": "b8bdcf43", "metadata": {}, "outputs": [], "source": [ "endpoint_name = str(resource_name_async).format(\"Endpoint\")\n", "try:\n", " sagemaker_client.delete_endpoint(EndpointName=endpoint_name)\n", "except:\n", " pass\n", "\n", "create_endpoint_response = sagemaker_client.create_endpoint(\n", " EndpointName=endpoint_name, EndpointConfigName=endpoint_config_name\n", ")\n", "print(f\"Created Endpoint: {create_endpoint_response['EndpointArn']}\")" ] }, { "cell_type": "markdown", "id": "18e845d9", "metadata": {}, "source": [ "以下のセルを実行して、エンドポイントの起動状況を確認します。ステータスが InService になっていれば推論を実行することができます。" ] }, { "cell_type": "code", "execution_count": null, "id": "2c283473", "metadata": { "scrolled": true }, "outputs": [], "source": [ "waiter = sagemaker_client.get_waiter(\"endpoint_in_service\")\n", "print(\"Waiting for endpoint to create...\")\n", "waiter.wait(EndpointName=endpoint_name)\n", "resp = sagemaker_client.describe_endpoint(EndpointName=endpoint_name)\n", "print(f\"Endpoint Status: {resp['EndpointStatus']}\")" ] }, { "cell_type": "markdown", "id": "17bbc8ba", "metadata": {}, "source": [ "### [オプション] オートスケールポリシーの設定\n", "\n", "このセクションでは、Application Autoscaling を使用して非同期エンドポイントにオートスケーリングを設定する方法について説明します。まず、エンドポイントのバリアントを Application Autoscaling に登録し、スケーリングポリシーを定義してから、スケーリングポリシーを適用する必要があります。この構成では、 ApproximateBacklogSizePerInstance というカスタムメトリック(CustomizedMetricSpecification)を使用します。非同期推論エンドポイントで利用可能なメトリクスの詳細なリストについては、[SageMaker 開発者ガイド](https://docs.aws.amazon.com/sagemaker/latest/dg/async-inference.html) をご参照ください。" ] }, { "cell_type": "code", "execution_count": null, "id": "bb6a840f", "metadata": { "scrolled": true }, "outputs": [], "source": [ "client = boto3.client(\n", " \"application-autoscaling\"\n", ") # Common class representing Application Auto Scaling for SageMaker amongst other services\n", "\n", "resource_id = (\n", " \"endpoint/\" + endpoint_name + \"/variant/\" + \"variant1\"\n", ") # This is the format in which application autoscaling references the endpoint\n", "\n", "# Configure Autoscaling on asynchronous endpoint down to zero instances\n", "response = client.register_scalable_target(\n", " ServiceNamespace=\"sagemaker\",\n", " ResourceId=resource_id,\n", " ScalableDimension=\"sagemaker:variant:DesiredInstanceCount\",\n", " MinCapacity=0,\n", " MaxCapacity=5,\n", ")\n", "\n", "response = client.put_scaling_policy(\n", " PolicyName=\"Invocations-ScalingPolicy\",\n", " ServiceNamespace=\"sagemaker\", # The namespace of the AWS service that provides the resource.\n", " ResourceId=resource_id, # Endpoint name\n", " ScalableDimension=\"sagemaker:variant:DesiredInstanceCount\", # SageMaker supports only Instance Count\n", " PolicyType=\"TargetTrackingScaling\", # 'StepScaling'|'TargetTrackingScaling'\n", " TargetTrackingScalingPolicyConfiguration={\n", " \"TargetValue\": 5.0, # The target value for the metric. - here the metric is - SageMakerVariantInvocationsPerInstance\n", " \"CustomizedMetricSpecification\": {\n", " \"MetricName\": \"ApproximateBacklogSizePerInstance\",\n", " \"Namespace\": \"AWS/SageMaker\",\n", " \"Dimensions\": [{\"Name\": \"EndpointName\", \"Value\": endpoint_name}],\n", " \"Statistic\": \"Average\",\n", " },\n", " \"ScaleInCooldown\": 600, # The cooldown period helps you prevent your Auto Scaling group from launching or terminating\n", " # additional instances before the effects of previous activities are visible.\n", " # You can configure the length of time based on your instance startup time or other application needs.\n", " # ScaleInCooldown - The amount of time, in seconds, after a scale in activity completes before another scale in activity can start.\n", " \"ScaleOutCooldown\": 300 # ScaleOutCooldown - The amount of time, in seconds, after a scale out activity completes before another scale out activity can start.\n", " # 'DisableScaleIn': True|False - ndicates whether scale in by the target tracking policy is disabled.\n", " # If the value is true , scale in is disabled and the target tracking policy won't remove capacity from the scalable resource.\n", " },\n", ")\n", "response" ] }, { "cell_type": "markdown", "id": "2420c44e", "metadata": {}, "source": [ "### 非同期推論エンドポイントで推論\n", "\n", "非同期推論を実行するには、推論の入力データを Amazon S3 にアップロードしておく必要があります。" ] }, { "cell_type": "code", "execution_count": null, "id": "6a5ea486", "metadata": {}, "outputs": [], "source": [ "if not os.path.exists(data_dir_async):\n", " os.mkdir(data_dir_async)\n", "image_list = []\n", "for i in indices:\n", " test_data_loader = DataLoader([test_data[i][0]], batch_size=len(test_data))\n", " test_data_loaded = next(iter(test_data_loader))\n", " filename = os.path.join(data_dir_async, 'test_'+str(i)+'_'+str(test_data[i][1])+'.pt')\n", " torch.save(test_data_loaded, filename)\n", " image_list.append(filename)" ] }, { "cell_type": "code", "execution_count": null, "id": "0c3e25bf", "metadata": {}, "outputs": [], "source": [ "image_list" ] }, { "cell_type": "code", "execution_count": null, "id": "b96f93a0", "metadata": {}, "outputs": [], "source": [ "inputs_async = sagemaker_session.upload_data(path=data_dir_async, bucket=bucket_name, key_prefix=os.path.join(bucket_prefix_async, 'data'))\n", "print('input spec (in this case, just an S3 path): {}'.format(inputs_async))" ] }, { "cell_type": "markdown", "id": "509767bf", "metadata": {}, "source": [ "**非同期推論の実行**\n", "\n", "`invoke_endpoint_async() `を使って、非同期エンドポイントにホストされているモデルを使って推論を実行します。InputLocation フィールドに推論データの場所を、EndpointName にエンドポイントの名前を指定します。response には、結果が保存されるAmazon S3 のパスが含まれます。\n", "\n", "このサンプルノートブックでは、5枚の画像を連続して推論します。" ] }, { "cell_type": "code", "execution_count": null, "id": "dcddc047", "metadata": {}, "outputs": [], "source": [ "output_locations = []\n", "for i in image_list:\n", " basename = os.path.basename(i)\n", " print(basename)\n", " response = sagemaker_runtime.invoke_endpoint_async(\n", " EndpointName=endpoint_name, InputLocation=os.path.join(inputs_async, basename)\n", " )\n", " output_locations.append([basename, response[\"OutputLocation\"]])\n", " print(f\"OutputLocation: {output_locations[-1]}\")" ] }, { "cell_type": "markdown", "id": "8e0c512d", "metadata": {}, "source": [ "**推論結果の確認**\n", "\n", "推論が処理されたかどうかをファイルの出力状況で確認します。推論リクエストの結果が S3 に保存されるまで、2秒ごとにファイルの有無を確認する関数 `get_output()` を定義します。" ] }, { "cell_type": "code", "execution_count": null, "id": "466c4551", "metadata": {}, "outputs": [], "source": [ "import urllib, time\n", "from botocore.exceptions import ClientError\n", "\n", "\n", "def get_output(output_location):\n", " output_url = urllib.parse.urlparse(output_location)\n", " bucket = output_url.netloc\n", " key = output_url.path[1:]\n", " while True:\n", " try:\n", " return sagemaker_session.read_s3_file(bucket=output_url.netloc, key_prefix=output_url.path[1:])\n", " except ClientError as e:\n", " if e.response[\"Error\"][\"Code\"] == \"NoSuchKey\":\n", " print(\"waiting for output...\")\n", " time.sleep(2)\n", " continue\n", " raise" ] }, { "cell_type": "markdown", "id": "7edee96f", "metadata": {}, "source": [ "以下のセルを実行して、推論結果が保存されていれば、その内容を表示します。" ] }, { "cell_type": "code", "execution_count": null, "id": "97d41eb6", "metadata": {}, "outputs": [], "source": [ "import ast, re\n", "for f, output_location in output_locations:\n", " gt = int(re.split('[_\\.]', f)[2])\n", " output = get_output(output_location)\n", " result = ast.literal_eval(output)[0]\n", " pred = np.argmax(result)\n", " print(f\"Ground Truth: {gt}, predict: {pred}, {gt==pred}\")" ] }, { "cell_type": "markdown", "id": "1e8fd56c", "metadata": {}, "source": [ "### リソースの削除\n", "\n", "非同期エンドポイントと、(設定を作成している場合)オートスケール設定を削除します。" ] }, { "cell_type": "code", "execution_count": null, "id": "a6585d32", "metadata": {}, "outputs": [], "source": [ "# オートスケール設定の削除\n", "response = client.describe_scaling_policies(\n", " ResourceId=resource_id,\n", " ServiceNamespace='sagemaker'\n", ")\n", "\n", "if len(response['ScalingPolicies']) > 0:\n", " response = client.deregister_scalable_target(\n", " ServiceNamespace='sagemaker',\n", " ResourceId=resource_id,\n", " ScalableDimension='sagemaker:variant:DesiredInstanceCount'\n", " )" ] }, { "cell_type": "code", "execution_count": null, "id": "24b4c919", "metadata": {}, "outputs": [], "source": [ "# エンドポイントの削除\n", "sagemaker_client.delete_endpoint(EndpointName=endpoint_name)" ] }, { "cell_type": "markdown", "id": "49bac2dd", "metadata": {}, "source": [ "## カスタムコンテナイメージで学習・推論\n", "\n", "自分で作成した PyTorch コンテナを使って推論を行う場合の手順をご紹介します。ビルドしたコンテナイメージを Amazon ECR に push して Amazon SageMaker から呼び出して推論を行います。そのために、ノートブックインスタンスにアタッチされている IAM role に以下の手順でロールを追加してください。なお、`pip install` でライブラリを追加するだけであれば、[スクリプトファイル(train.py など)と同じフォルダに requirements.txt を入れておく](https://sagemaker.readthedocs.io/en/stable/frameworks/pytorch/using_pytorch.html?highlight=requirements.txt#using-third-party-libraries) ことで、学習用インスタンスでコンテナが起動する際に requirements.txt に記載されたライブラリが自動的にインストールされるので、そちらをご利用いただくのも一案です。\n", "\n", "1. Amazon SageMaker コンソールからこのノートブックインスタンスの詳細画面を表示
\n", "(左側のメニューのインスタンス -> ノートブックインスタンス -> インスタンス名をクリック)\n", "1.  「アクセス許可と暗号化」の「IAM ロール ARN」のリンクをクリック(IAM のコンソールに遷移します)\n", "1.  「ポリシーをアタッチします」と書いてある青いボタンをクリック\n", "1.  検索ボックスに ec2containerregistry と入力し AmazonEC2ContainerRegistryFullAccess のチェックボックスをチェックする\n", "1.  「ポリシーのアタッチ」と書いてある青いボタンをクリック\n", "\n", "**Amazon SageMaker Studio では、docker コマンドを使用することができません。代わりのコマンド `sm-docker` がありますので、[こちらの手順](https://sagemaker-immersionday.workshop.aws/ja/lab3/option2.html) を参考にご利用ください。**\n", "\n", "カスタムコンテナを作成する場合、ベースイメージとして SageMaker が用意したコンテナイメージを使うと便利です。[SageMaker のコンテナイメージのリストはこちら](https://github.com/aws/deep-learning-containers/blob/master/available_images.md) でご確認いただけます。\n", "\n", "また、以下のコマンドでコンテナイメージの URI を取得することができます。" ] }, { "cell_type": "code", "execution_count": null, "id": "a8cdb2ce", "metadata": {}, "outputs": [], "source": [ "from sagemaker import image_uris\n", "\n", "# Specify an AWS container image and region as desired\n", "container = image_uris.retrieve(\n", " region=region, framework=\"pytorch\", \n", " version=\"1.8.0\", instance_type='ml.c5.xlarge', image_scope='training', py_version='py36')\n", "container" ] }, { "cell_type": "markdown", "id": "79131d16", "metadata": {}, "source": [ "コンテナイメージをビルドする際に、no space left というエラーが出てビルドが失敗することがあります。ノートブックインスタンスのストレージ(EBS ボリュームサイズ)を30GBくらいに設定した上で、以下のセルを実行して docker 関連のファイル保存場所を変更することをおすすめします。" ] }, { "cell_type": "code", "execution_count": null, "id": "18b971ab", "metadata": {}, "outputs": [], "source": [ "!sudo /etc/init.d/docker stop\n", "!sudo mv /var/lib/docker /home/ec2-user/SageMaker/docker\n", "!sudo ln -s /home/ec2-user/SageMaker/docker /var/lib/docker\n", "!sudo /etc/init.d/docker start" ] }, { "cell_type": "markdown", "id": "c3d2545d", "metadata": {}, "source": [ "### 学習用コンテナイメージの作成\n", "\n", "SageMaker の PyTorch コンテナイメージをベースに、カスタムコンテナイメージを作成します。SageMaker のビルトインイメージ以外をベースにする場合は、[こちらの手順](https://docs.aws.amazon.com/sagemaker/latest/dg/adapt-training-container.html#:~:text=Step%202%3A%20Create%20and%20upload%20the%20Dockerfile%20and%20Python%20training%20scripts) の通り Dockerfile に`RUN pip3 install sagemaker-training` を 記載するのを忘れないようにしてください。" ] }, { "cell_type": "code", "execution_count": null, "id": "95cd661d", "metadata": {}, "outputs": [], "source": [ "!mkdir -p docker/train\n", "!mkdir -p docker/inference" ] }, { "cell_type": "markdown", "id": "55e52850", "metadata": {}, "source": [ "以下のセルの FROM で始まる行の `ap-northeast-1` の部分は、このノートブックを実行しているノートブッインスタンスと同じリージョンにしてください。" ] }, { "cell_type": "code", "execution_count": null, "id": "a5e12deb", "metadata": {}, "outputs": [], "source": [ "%%writefile docker/train/Dockerfile\n", "\n", "FROM 763104351884.dkr.ecr.ap-northeast-1.amazonaws.com/pytorch-training:1.8.0-cpu-py36\n", " \n", "RUN pip3 install numpy\n", "\n", "WORKDIR /" ] }, { "cell_type": "code", "execution_count": null, "id": "73b323a8", "metadata": { "scrolled": true }, "outputs": [], "source": [ "ecr_repository_train = 'pytorch-byo-train'\n", "uri_suffix = 'amazonaws.com'\n", "tag = ':latest'\n", "train_repository_uri = '{}.dkr.ecr.{}.{}/{}'.format(account_id, region, uri_suffix, ecr_repository_train + tag)\n", "\n", "# Create ECR repository and push docker image\n", "!aws ecr get-login-password --region {region} | docker login --username AWS --password-stdin 763104351884.dkr.ecr.{region}.amazonaws.com\n", "!aws ecr get-login-password --region {region} | docker login --username AWS --password-stdin {account_id}.dkr.ecr.{region}.amazonaws.com\n", "!docker build -t $ecr_repository_train docker/train\n", "!aws ecr create-repository --repository-name $ecr_repository_train\n", "!docker tag {ecr_repository_train + tag} $train_repository_uri\n", "!docker push $train_repository_uri" ] }, { "cell_type": "markdown", "id": "98f40fae", "metadata": {}, "source": [ "作成した学習用コンテナを使ってモデルを学習します。" ] }, { "cell_type": "code", "execution_count": null, "id": "cf32dfc3", "metadata": {}, "outputs": [], "source": [ "estimator = PyTorch(entry_point=\"train.py\",\n", " image_uri=train_repository_uri,\n", " source_dir='code_byoc',\n", " role=role,\n", " instance_count=1,\n", " instance_type='ml.c5.xlarge',\n", "# instance_type='local',\n", " hyperparameters={\n", " 'batch-size':128,\n", " 'lr': 0.01,\n", " 'epochs': 10,\n", " 'backend': 'gloo'\n", " })\n", "estimator.fit({'training': inputs})" ] }, { "cell_type": "markdown", "id": "26a9af48", "metadata": {}, "source": [ "### 推論用コンテナイメージの作成\n", "\n", "このサンプルでは、[こちらの Dockerfile](https://github.com/aws/deep-learning-containers/blob/master/pytorch/inference/docker/1.9/py3/Dockerfile.cpu) を参考にして独自のコンテナイメージを作成します。Dockerfile の他に、必要なファイルをダウンロードします。" ] }, { "cell_type": "code", "execution_count": null, "id": "3d502efb", "metadata": {}, "outputs": [], "source": [ "!wget https://raw.githubusercontent.com/aws/deep-learning-containers/master/src/deep_learning_container.py -O docker/inference/deep_learning_container.py\n", "!wget https://raw.githubusercontent.com/aws/deep-learning-containers/master/pytorch/inference/docker/build_artifacts/config.properties -O docker/inference/config.properties\n", "!wget https://raw.githubusercontent.com/aws/deep-learning-containers/master/pytorch/inference/docker/build_artifacts/torchserve-entrypoint.py -O docker/inference/torchserve-entrypoint.py" ] }, { "cell_type": "markdown", "id": "0a62ff30", "metadata": {}, "source": [ "なお、コンテナイメージが用意されていない最新バージョンの PyTorch を使いたいなどでなければ、前の手順の学習用コンテナイメージ作成と同様に既存の SageMaker 推論用コンテナイメージをベースイメージにして独自のコンテナイメージを作成するのが簡単です。たとえば、以下のような Dockerfile で推論用カスタムイメージを作成することができます。このコードを利用する場合は、忘れずに `ap-northeast-1` の部分をお使いのリージョンに合わせて書き換えてください。" ] }, { "cell_type": "code", "execution_count": null, "id": "50bb1b2f", "metadata": {}, "outputs": [], "source": [ "# %%writefile docker/inference/Dockerfile\n", "\n", "# FROM 763104351884.dkr.ecr.ap-northeast-1.amazonaws.com/pytorch-inference:1.8.0-cpu-py36\n", " \n", "# RUN pip3 install numpy" ] }, { "cell_type": "markdown", "id": "ae4a0b39", "metadata": {}, "source": [ "以下のセルを実行すると、Dockerfile が作成されます。" ] }, { "cell_type": "code", "execution_count": null, "id": "81a84a46", "metadata": {}, "outputs": [], "source": [ "%%writefile docker/inference/Dockerfile\n", "\n", "FROM ubuntu:20.04\n", "\n", "LABEL maintainer=\"Amazon AI\"\n", "LABEL dlc_major_version=\"1\"\n", "LABEL com.amazonaws.sagemaker.capabilities.accept-bind-to-port=true\n", "LABEL com.amazonaws.sagemaker.capabilities.multi-models=true\n", "\n", "ARG PYTHON=python3\n", "ARG PYTHON_VERSION=3.8.10\n", "ARG OPEN_MPI_VERSION=4.0.1\n", "ARG TS_VERSION=0.4.2\n", "ARG PT_INFERENCE_URL=https://pytorch-ei-binaries.s3.us-west-2.amazonaws.com/r1.9.0_inference/20210730-020505/2dc3e2055764606a22a3c59ea955184e85ad6110/cpu/torch-1.9.0-cp38-cp38-manylinux1_x86_64.whl\n", "ARG PT_TORCHVISION_URL=https://pytorch-ei-binaries.s3.us-west-2.amazonaws.com/torchvision_build/0.10.0/cpu/torchvision-0.10.0-cp38-cp38-linux_x86_64.whl\n", "ARG PT_TORCHAUDIO_URL=https://pytorch-ei-binaries.s3.us-west-2.amazonaws.com/torchaudio_build/0.9.0/cpu/torchaudio-0.9.0-cp38-cp38-linux_x86_64.whl\n", "\n", "ENV LANG C.UTF-8\n", "ENV LD_LIBRARY_PATH /opt/conda/lib/:$LD_LIBRARY_PATH\n", "ENV PATH /opt/conda/bin:$PATH\n", "ENV SAGEMAKER_SERVING_MODULE sagemaker_pytorch_serving_container.serving:main\n", "ENV TEMP=/home/model-server/tmp\n", "# Set MKL_THREADING_LAYER=GNU to prevent issues between torch and numpy/mkl\n", "ENV MKL_THREADING_LAYER=GNU\n", "\n", "RUN apt-get update \\\n", "# TODO: Remove systemd upgrade once it is updated in base image\n", " && apt-get -y upgrade --only-upgrade systemd \\\n", " && apt-get install -y --no-install-recommends software-properties-common \\\n", " && add-apt-repository ppa:openjdk-r/ppa \\\n", " && apt-get update \\\n", " && apt-get install -y --no-install-recommends \\\n", " build-essential \\\n", " ca-certificates \\\n", " cmake \\\n", " curl \\\n", " emacs \\\n", " git \\\n", " jq \\\n", " libgl1-mesa-glx \\\n", " libglib2.0-0 \\\n", " libsm6 \\\n", " libxext6 \\\n", " libxrender-dev \\\n", " openjdk-11-jdk \\\n", " openssl \\\n", " vim \\\n", " wget \\\n", " unzip \\\n", " zlib1g-dev \\\n", " && rm -rf /var/lib/apt/lists/* \\\n", " && apt-get clean\n", "\n", "# https://github.com/docker-library/openjdk/issues/261 https://github.com/docker-library/openjdk/pull/263/files\n", "RUN keytool -importkeystore -srckeystore /etc/ssl/certs/java/cacerts -destkeystore /etc/ssl/certs/java/cacerts.jks -deststoretype JKS -srcstorepass changeit -deststorepass changeit -noprompt; \\\n", " mv /etc/ssl/certs/java/cacerts.jks /etc/ssl/certs/java/cacerts; \\\n", " /var/lib/dpkg/info/ca-certificates-java.postinst configure;\n", "\n", "RUN curl -L -o ~/miniconda.sh https://repo.continuum.io/miniconda/Miniconda3-latest-Linux-x86_64.sh \\\n", " && chmod +x ~/miniconda.sh \\\n", " && ~/miniconda.sh -b -p /opt/conda \\\n", " && rm ~/miniconda.sh \\\n", " && /opt/conda/bin/conda update conda \\\n", " && /opt/conda/bin/conda install -c conda-forge \\\n", " python=$PYTHON_VERSION \\\n", " && /opt/conda/bin/conda install -y \\\n", " # conda 4.10.0 requires ruamel_yaml to be installed. Currently pinned at latest.\n", " ruamel_yaml==0.15.100 \\\n", " cython \\\n", " ipython \\\n", " mkl-include \\\n", " mkl \\\n", " parso \\\n", " scipy \\\n", " typing \\\n", " && /opt/conda/bin/conda clean -ya\n", "\n", "# Conda installs links for libtinfo.so.6 and libtinfo.so.6.2 both\n", "# Which causes \"/opt/conda/lib/libtinfo.so.6: no version information available\" warning\n", "# Removing link for libtinfo.so.6. This change is needed only for ubuntu 20.04-conda, and can be reverted\n", "# once conda fixes the issue: https://github.com/conda/conda/issues/9680\n", "RUN rm -rf /opt/conda/lib/libtinfo.so.6\n", "\n", "RUN wget https://www.open-mpi.org/software/ompi/v4.0/downloads/openmpi-$OPEN_MPI_VERSION.tar.gz \\\n", " && gunzip -c openmpi-$OPEN_MPI_VERSION.tar.gz | tar xf - \\\n", " && cd openmpi-$OPEN_MPI_VERSION \\\n", " && ./configure --prefix=/home/.openmpi \\\n", " && make all install \\\n", " && cd .. \\\n", " && rm openmpi-$OPEN_MPI_VERSION.tar.gz \\\n", " && rm -rf openmpi-$OPEN_MPI_VERSION\n", "\n", "# The ENV variables declared below are changed in the previous section\n", "# Grouping these ENV variables in the first section causes\n", "# ompi_info to fail. This is only observed in CPU containers\n", "ENV PATH=\"$PATH:/home/.openmpi/bin\"\n", "ENV LD_LIBRARY_PATH=\"$LD_LIBRARY_PATH:/home/.openmpi/lib/\"\n", "RUN ompi_info --parsable --all | grep mpi_built_with_cuda_support:value\n", "\n", "RUN conda install -c \\\n", " conda-forge \\\n", " opencv \\\n", " && conda install -y \\\n", " scikit-learn \\\n", " pandas \\\n", " h5py \\\n", " requests \\\n", " && conda clean -ya \\\n", " && pip install --upgrade pip --trusted-host pypi.org --trusted-host files.pythonhosted.org \\\n", " && ln -s /opt/conda/bin/pip /usr/local/bin/pip3 \\\n", " && pip install packaging==20.4 \\\n", " enum-compat==0.0.3 \\\n", " numpy==1.20.3 \\\n", " \"cryptography>=3.3.2\"\n", "\n", "# Uninstall and re-install torch and torchvision from the PyTorch website\n", "RUN pip uninstall -y torch \\\n", " && pip install --no-cache-dir -U $PT_INFERENCE_URL \\\n", " && pip uninstall -y torchvision \\\n", " && pip install --no-deps --no-cache-dir -U $PT_TORCHVISION_URL \\\n", " && pip uninstall -y torchaudio \\\n", " && pip install --no-deps --no-cache-dir -U $PT_TORCHAUDIO_URL\n", "\n", "\n", "RUN conda install -y -c conda-forge \"pyyaml>=5.4,<5.5\"\n", "RUN pip install pillow==8.3.1 \"awscli<2\"\n", "RUN pip install --no-cache-dir \"sagemaker-pytorch-inference>=2\"\n", "\n", "RUN pip uninstall -y model-archiver multi-model-server \\\n", " && pip install captum \\\n", " && pip install torchserve==$TS_VERSION \\\n", " && pip install torch-model-archiver==$TS_VERSION\n", "\n", "RUN cd tmp/ \\\n", " && rm -rf tmp*\n", "\n", "RUN useradd -m model-server \\\n", " && mkdir -p /home/model-server/tmp /opt/ml/model \\\n", " && chown -R model-server /home/model-server /opt/ml/model\n", "\n", "COPY torchserve-entrypoint.py /usr/local/bin/dockerd-entrypoint.py\n", "COPY config.properties /home/model-server\n", "\n", "RUN chmod +x /usr/local/bin/dockerd-entrypoint.py\n", "\n", "COPY deep_learning_container.py /usr/local/bin/deep_learning_container.py\n", "\n", "RUN chmod +x /usr/local/bin/deep_learning_container.py\n", "\n", "RUN HOME_DIR=/root \\\n", " && curl -o ${HOME_DIR}/oss_compliance.zip https://aws-dlinfra-utilities.s3.amazonaws.com/oss_compliance.zip \\\n", " && unzip ${HOME_DIR}/oss_compliance.zip -d ${HOME_DIR}/ \\\n", " && cp ${HOME_DIR}/oss_compliance/test/testOSSCompliance /usr/local/bin/testOSSCompliance \\\n", " && chmod +x /usr/local/bin/testOSSCompliance \\\n", " && chmod +x ${HOME_DIR}/oss_compliance/generate_oss_compliance.sh \\\n", " && ${HOME_DIR}/oss_compliance/generate_oss_compliance.sh ${HOME_DIR} ${PYTHON} \\\n", " && rm -rf ${HOME_DIR}/oss_compliance*\n", "\n", "RUN curl -o /license.txt https://aws-dlc-licenses.s3.amazonaws.com/pytorch-1.9/license.txt\n", "\n", "EXPOSE 8080 8081\n", "ENTRYPOINT [\"python\", \"/usr/local/bin/dockerd-entrypoint.py\"]\n", "CMD [\"torchserve\", \"--start\", \"--ts-config\", \"/home/model-server/config.properties\", \"--model-store\", \"/home/model-server/\"]" ] }, { "cell_type": "markdown", "id": "78d58885", "metadata": {}, "source": [ "以下のコマンドを実行して、コンテナイメージのビルドと Amazon ECR への push を行います。ml.t2.medium のノートブックインスタンスを使用するとこのセルの実行完了までに数十分かかることがあります。念のため、セルの実行が完了したら Amazon ECR のコンソールで、ビルドしたコンテナイメージが push されているか確認しましょう。" ] }, { "cell_type": "code", "execution_count": null, "id": "d9b33c24", "metadata": { "scrolled": true }, "outputs": [], "source": [ "ecr_repository_inference = 'pytorch-byo-inference'\n", "uri_suffix = 'amazonaws.com'\n", "tag = ':latest'\n", "inference_repository_uri = '{}.dkr.ecr.{}.{}/{}'.format(account_id, region, uri_suffix, ecr_repository_inference + tag)\n", "\n", "# Create ECR repository and push docker image\n", "!docker build -t $ecr_repository_inference docker/inference\n", "!aws ecr get-login-password --region {region} | docker login --username AWS --password-stdin {account_id}.dkr.ecr.{region}.amazonaws.com\n", "!aws ecr create-repository --repository-name $ecr_repository_inference\n", "!docker tag {ecr_repository_inference + tag} $inference_repository_uri\n", "!docker push $inference_repository_uri" ] }, { "cell_type": "markdown", "id": "2cb0f181", "metadata": {}, "source": [ "### カスタムコンテナイメージを使って推論\n", "\n", "作成した推論用コンテナイメージを使って推論してみましょう。`PyTorchModel` クラスを使って PyTorch モデルを作成します。推論エンドポイントで使用するコンテナイメージ、PyTorch バージョン、学習済みモデルのパス、推論で使用するスクリプトファイル名などを引数で指定します。" ] }, { "cell_type": "code", "execution_count": null, "id": "20381772", "metadata": {}, "outputs": [], "source": [ "from sagemaker.pytorch.model import PyTorchModel\n", "\n", "model = PyTorchModel(role=role, model_data=estimator.model_data, framework_version='1.9.0', image_uri=inference_repository_uri, source_dir=\"code_byoc\", entry_point=\"inference.py\")\n", "predictor = model.deploy(initial_instance_count=1, instance_type='ml.c5.xlarge')" ] }, { "cell_type": "markdown", "id": "e7025cfa", "metadata": {}, "source": [ "すでに起動しているエンドポイントを使って推論する場合は endpoint_name にエンドポイント名を入力して、以下のように predictor を作成します。" ] }, { "cell_type": "code", "execution_count": null, "id": "0ae9910c", "metadata": {}, "outputs": [], "source": [ "# from sagemaker.pytorch.model import PyTorchPredictor\n", "\n", "# predictor = PyTorchPredictor(\n", "# endpoint_name=''\n", "# )" ] }, { "cell_type": "markdown", "id": "c97ee44a", "metadata": {}, "source": [ "それでは、カスタムコンテナイメージを使った推論エンドポイントで推論を実行します。" ] }, { "cell_type": "code", "execution_count": null, "id": "7c4f8bab", "metadata": {}, "outputs": [], "source": [ "prediction = predictor.predict(images)\n", "predicted_label = prediction.argmax(axis=1)\n", "\n", "print('The GT labels are: {}'.format(raw_labels))\n", "print('The predicted labels are: {}'.format(predicted_label))" ] }, { "cell_type": "markdown", "id": "55099387", "metadata": {}, "source": [ "使い終わったエンドポイントを削除します。" ] }, { "cell_type": "code", "execution_count": null, "id": "c900b454", "metadata": {}, "outputs": [], "source": [ "predictor.delete_endpoint(delete_endpoint_config=True)" ] }, { "cell_type": "markdown", "id": "16fe9299", "metadata": {}, "source": [ "## Inf1 インスタンスで推論\n", "\n", "Inf1インスタンスは、機械学習推論アプリケーションをサポートするためにゼロから構築されており、AWSが設計・開発した高性能機械学習推論チップである [AWS Inferentia チップ](https://aws.amazon.com/jp/machine-learning/inferentia/) を最大 16個搭載しています。このノートブックでは、学習した PyTorch モデルを Inf1インスタンスにデプロイする方法をご紹介します。Inf1 インスタンスで機械学習モデルを使用するには、モデルをコンパイルする必要があります。このノートブックでは、Amazon SageMaker Neo を使ってモデルをコンパイルします。\n", "\n", "### モデルの学習\n", "\n", "まずは、Inf1 で推論するためのモデルを学習します。PyTorch の場合、SageMaker Neo でモデルを変換する前にモデルに Input 情報を追加する必要があります。今回は、学習スクリプトの最後に Neo 用の変換をしておきます。詳細は、`code_inf1/train.py` の `transform_model_for_neo()` 関数をご参照ください。" ] }, { "cell_type": "code", "execution_count": null, "id": "8020c0d5", "metadata": {}, "outputs": [], "source": [ "input_shape = [1, 1, 28, 28]\n", "\n", "estimator_inf1 = PyTorch(entry_point=\"train.py\",\n", " source_dir='code_inf1',\n", " role=role,\n", " framework_version='1.8.0',\n", " py_version='py3',\n", " instance_count=1,\n", " instance_type='ml.c5.xlarge',\n", "# instance_type='local',\n", " hyperparameters={\n", " 'batch-size':128,\n", " 'lr': 0.01,\n", " 'epochs': 1,\n", " 'input-shape': str(input_shape)[1:-1],\n", " 'backend': 'gloo'\n", " })\n", "estimator_inf1.fit({'training': inputs})" ] }, { "cell_type": "markdown", "id": "f87149ed", "metadata": {}, "source": [ "### モデルのコンパイル\n", "\n", "モデルの学習が完了したら、SageMaker Neo でモデルをコンパイルします。まずは推論用スクリプトを指定した `PyTorchModel` を作成し、それを使って `compile` を実行します。コンパイルの際は `code_inf1/train,py` で指定した `input_size` をパラメタとして渡します。" ] }, { "cell_type": "code", "execution_count": null, "id": "b6066fd3", "metadata": {}, "outputs": [], "source": [ "from sagemaker.pytorch.model import PyTorchModel\n", "from datetime import datetime\n", "from dateutil import tz\n", "JST = tz.gettz('Asia/Tokyo')\n", "\n", "pytorch_model = PyTorchModel(\n", " entry_point='inference.py',\n", " source_dir='code_inf1',\n", " model_data=estimator_inf1.model_data,\n", " framework_version='1.8.0',\n", " py_version='py3',\n", " role=role,\n", ")\n", "\n", "timestamp = datetime.now(tz=JST).strftime('%Y%m%d-%H%M%S')\n", "compilation_job_name = \"pytorch-mnist-inf1-\" + timestamp\n", "compiled_model_path = \"s3://{}/{}/output\".format(bucket_name, compilation_job_name)\n", "\n", "neo_model = pytorch_model.compile(\n", " target_instance_family=\"ml_inf1\",\n", " input_shape={\"input0\": input_shape},\n", " output_path=compiled_model_path,\n", " framework=\"pytorch\",\n", " framework_version=\"1.8.0\",\n", " role=role,\n", " job_name=compilation_job_name,\n", ")" ] }, { "cell_type": "markdown", "id": "4c5fca84", "metadata": {}, "source": [ "### Inf1 インスタンスで推論エンドポイントを作成し推論" ] }, { "cell_type": "code", "execution_count": null, "id": "9c3a3a59", "metadata": {}, "outputs": [], "source": [ "predictor_inf1 = neo_model.deploy(instance_type=\"ml.inf1.xlarge\", initial_instance_count=1)" ] }, { "cell_type": "code", "execution_count": null, "id": "665af8ce", "metadata": {}, "outputs": [], "source": [ "results = []\n", "for i in images:\n", " prediction = predictor_inf1.predict([i])\n", " predicted_label = prediction.argmax(axis=1)\n", " results.append(predicted_label[0])\n", "\n", "print('The GT labels are: {}'.format(raw_labels))\n", "print('The predicted labels are: {}'.format(results))" ] }, { "cell_type": "markdown", "id": "fc0506da", "metadata": {}, "source": [ "## サーバレス推論 (preview)\n", "\n", "Amazon SageMaker で学習したモデルをサーバレスエンドポイントにデプロイして推論することができます。この機能は 2022年1月現在プレビュー中のため、本番環境での利用は非推奨です。\n", "\n", "### デプロイするモデルの学習\n", "\n", "サーバレスエンドポイントにデプロイするモデルを学習します。" ] }, { "cell_type": "code", "execution_count": null, "id": "4ca7c2f7", "metadata": {}, "outputs": [], "source": [ "estimator_lambda = PyTorch(entry_point=\"train.py\",\n", " source_dir='code_byoc',\n", " role=role,\n", " framework_version='1.8.1',\n", " py_version='py3',\n", " instance_count=1,\n", " instance_type='ml.c5.xlarge',\n", " hyperparameters={\n", " 'batch-size':128,\n", " 'lr': 0.01,\n", " 'epochs': 1,\n", " 'backend': 'gloo'\n", " })\n", "estimator_lambda.fit({'training': inputs})" ] }, { "cell_type": "markdown", "id": "d16491dc", "metadata": {}, "source": [ "### サーバレスエンドポイントの作成\n", "\n", "以下のセルを実行して、サーバレスエンドポイントを作成します。エンドポイントの起動が完了するまでに数分程度かかります。2022年1月現在、この操作は Amazon SageMaker Python SDK からではなく、AWS SDK for Python (boto3) からのみ可能です。" ] }, { "cell_type": "code", "execution_count": null, "id": "a61d5b1e", "metadata": {}, "outputs": [], "source": [ "pytorch_inference_container = sagemaker.image_uris.retrieve(\n", " region=region, framework=\"pytorch\", \n", " version=\"1.8.1\", instance_type='ml.c5.xlarge', image_scope='inference', py_version='py36')\n", " \n", "model_name = f\"DEMO-pytorch-serverless-{datetime.now():%Y-%m-%d-%H-%M-%S}\"\n", "\n", "endpoint_config_name = model_name + '-EndpointConfig'\n", "endpoint_name = model_name + '-Endpoint'\n", "response = sagemaker_client.create_model(\n", " ModelName=model_name,\n", " PrimaryContainer={\n", " 'Image': pytorch_inference_container,\n", " 'ModelDataUrl': estimator_lambda.model_data,\n", " },\n", " ExecutionRoleArn=role,\n", ")\n", "response = sagemaker_client.create_endpoint_config(\n", " EndpointConfigName=endpoint_config_name,\n", " ProductionVariants=[\n", " {\n", " 'VariantName': 'AllTrafic',\n", " 'ModelName': model_name,\n", " 'ServerlessConfig': { # 通常のリアルタイム推論とは違い、ServerlessConfig というキーで設定する\n", " 'MemorySizeInMB': 1024, # メモリサイズは 1024 , 2048, 3072, 4096, 5120, 6144 から選ぶ\n", " 'MaxConcurrency': 3 # 最大同時起動数\n", " }\n", " },\n", " ],\n", ")\n", "response = sagemaker_client.create_endpoint(\n", " EndpointName=endpoint_name,\n", " EndpointConfigName=endpoint_config_name,\n", ")\n", "\n", "describe_endpoint_response = sagemaker_client.describe_endpoint(EndpointName=endpoint_name)\n", "\n", "while describe_endpoint_response[\"EndpointStatus\"] == \"Creating\":\n", " describe_endpoint_response = sagemaker_client.describe_endpoint(EndpointName=endpoint_name)\n", " print(describe_endpoint_response[\"EndpointStatus\"])\n", " time.sleep(15)\n", "\n", "describe_endpoint_response" ] }, { "cell_type": "markdown", "id": "80f76ec7", "metadata": {}, "source": [ "### サーバレス推論の実行\n", "\n", "以下のセルを実行して推論してみましょう。初回の実行時は推論結果が返ってくるまでに数分かかることがありますが、続けて実行するとすぐに結果が返ってくるようになります。" ] }, { "cell_type": "code", "execution_count": null, "id": "d46f2ef3", "metadata": {}, "outputs": [], "source": [ "import io\n", "import json\n", "buffer= io.BytesIO()\n", "np.save(buffer, images)\n", "\n", "response = sagemaker_runtime.invoke_endpoint(EndpointName=endpoint_name,\n", " ContentType=\"application/x-npy\",\n", " Body=buffer.getvalue())\n", "\n", "body = response['Body']\n", "prediction = np.array(json.load(body))\n", "predicted_label = prediction.argmax(axis=1)\n", "\n", "print('The GT labels are: {}'.format(raw_labels))\n", "print('The predicted labels are: {}'.format(predicted_label))" ] }, { "cell_type": "markdown", "id": "eb8ac87c", "metadata": {}, "source": [ "使い終わったエンドポイントを削除します。" ] }, { "cell_type": "code", "execution_count": null, "id": "17386e25", "metadata": {}, "outputs": [], "source": [ "sagemaker_client.delete_endpoint(EndpointName=endpoint_name)" ] }, { "cell_type": "markdown", "id": "5235e8fe", "metadata": {}, "source": [ "## リソースの削除\n", "\n", "エンドポイントは、[SageMaker pricing page](https://aws.amazon.com/sagemaker/pricing/) に記載の通り、稼働している時間に応じて課金されるため、使用しなくなったら削除する必要があります。整理整頓のために、不要なエンドポイント設定も削除することをおすすめします。SageMaker のコンソールのエンドポイント一覧画面で、エンドポイントが削除されているかどうかを確認できます(一覧に表示されていなければ削除されたということです)。\n", "\n", "また、このノートブックを実行したノートブックインスタンスも削除が必要です。インスタンスを「停止」しただけでは EBS ボリュームへの課金は継続するので、完全に課金を止めるためにインスタンスを「停止」してから「削除」を実施してください。なお、削除したあとはインスタンスに保存されているファイルなどにアクセスすることはできません。" ] }, { "cell_type": "code", "execution_count": null, "id": "4d0679e3", "metadata": {}, "outputs": [], "source": [] } ], "metadata": { "kernelspec": { "display_name": "conda_pytorch_p37", "language": "python", "name": "conda_pytorch_p37" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.7.10" }, "toc": { "base_numbering": 1, "nav_menu": {}, "number_sections": true, "sideBar": true, "skip_h1_title": true, "title_cell": "Table of Contents", "title_sidebar": "Contents", "toc_cell": true, "toc_position": { "height": "calc(100% - 180px)", "left": "10px", "top": "150px", "width": "293.188px" }, "toc_section_display": true, "toc_window_display": true } }, "nbformat": 4, "nbformat_minor": 5 }