{
"cells": [
{
"cell_type": "code",
"execution_count": 1,
"metadata": {},
"outputs": [],
"source": [
"import base64\n",
"import boto3\n",
"from datetime import datetime\n",
"from io import BytesIO\n",
"import json\n",
"import numpy as np\n",
"from PIL import Image\n",
"import sagemaker\n",
"from sagemaker.tensorflow import TensorFlowModel\n",
"import tarfile\n",
"import tensorflow as tf\n",
"import time\n",
"import os"
]
},
{
"cell_type": "code",
"execution_count": 2,
"metadata": {},
"outputs": [],
"source": [
"## sagemaker configuration\n",
"sm_client = boto3.client('sagemaker')\n",
"sm_session = sagemaker.Session()\n",
"sm_role = sagemaker.get_execution_role()\n",
"bucket = sm_session.default_bucket()\n",
"s3_client = boto3.client('s3')"
]
},
{
"cell_type": "code",
"execution_count": 3,
"metadata": {},
"outputs": [],
"source": [
"## load label dictionary\n",
"with open(\"../data/label_dictionary.json\", \"r\") as f:\n",
" label_dictionary = json.load(f)\n",
"with open(\"../data/inverted_label_dictionary.json\", \"r\") as f:\n",
" inverted_label_dictionary = json.load(f)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Get model output location\n",
"In the cell below, we will pull the trained model that was launched in the previous notebook. After training, the model is stored in S3, and its URI can be found in the SageMaker console by clicking on `Training Jobs` on the left-hand frame, then clicking on the training job, then finally scrolling down to the `Output` section where you will find `S3 model artifact` (see images below for reference).\n",
"\n",
"The S3 key of the model artifact will be assigned to the variable `s3key_model_artifact` in this notebook.\n",
"\n",
"
\n",
"
\n",
"
"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Local inference"
]
},
{
"cell_type": "code",
"execution_count": 4,
"metadata": {},
"outputs": [],
"source": [
"## get the output path of the training job\n",
"# pull the path from the training job using the sagemaker client\n",
"s3uri_model_artifact = sm_client.describe_training_job(\n",
" TrainingJobName='cinic-demo-horovod-2021-02-12-20-05-02-420')['ModelArtifacts']['S3ModelArtifacts']\n",
"s3key_model_artifact = '/'.join(s3uri_model_artifact.split('/')[3:])\n",
"\n",
"# or find output path in the console using the instructions in the figure above\n",
"base_prefix = 'distributed_training_demo/model'\n",
"# s3key_model_artifact = f'{base_prefix}/cinic-demo-horovod-2021-02-12-20-05-02-420/output/model.tar.gz'"
]
},
{
"cell_type": "code",
"execution_count": 5,
"metadata": {},
"outputs": [],
"source": [
"## download model\n",
"local_model_dir = '../data/models'\n",
"os.makedirs('../data/models', exist_ok=True)\n",
"s3_client.download_file(bucket, s3key_model_artifact, local_model_dir +'/'+ 'horovod_model.tar.gz')"
]
},
{
"cell_type": "code",
"execution_count": 6,
"metadata": {},
"outputs": [],
"source": [
"## untar and load models\n",
"tar_filepath = os.path.join(local_model_dir, 'horovod_model.tar.gz')\n",
"extracted_dir = os.path.join(local_model_dir, 'horovod_model')\n",
"with tarfile.open(tar_filepath) as tarred_file:\n",
" tarred_file.extractall(extracted_dir)\n",
"model_path = os.path.join(extracted_dir, 'cinic10_classifier', '1')\n",
"model = tf.keras.models.load_model(model_path)"
]
},
{
"cell_type": "code",
"execution_count": 7,
"metadata": {},
"outputs": [],
"source": [
"## load test dataset for evaluation\n",
"\n",
"test_path = f'../data/sharded_tfrecords/test/'\n",
"files = [os.path.join(r,file) for r,d,f in os.walk(test_path) for file in f]\n",
"test_set = tf.data.TFRecordDataset(files)\n",
"\n",
"def _dataset_parser(value):\n",
" \n",
" # create a dictionary describing the features \n",
" sample_feature_description = {\n",
" 'image': tf.io.FixedLenFeature([], tf.string),\n",
" 'label': tf.io.FixedLenFeature([], tf.int64),\n",
" }\n",
"\n",
" # parse to tf\n",
" example = tf.io.parse_single_example(value, sample_feature_description)\n",
" \n",
" # decode from bytes to tf types\n",
" # NOTE: example key must match the name of the Input layer in the keras model\n",
" example['image'] = tf.io.decode_raw(example['image'], tf.uint8)\n",
" example['image'] = tf.reshape(example['image'], (32,32,3))\n",
" \n",
" # preprocess for resnset\n",
" # see https://www.tensorflow.org/api_docs/python/tf/keras/applications/resnet_v2/preprocess_input\n",
" example['image'] = tf.cast(example['image'], tf.float32)\n",
" example['image'] = tf.keras.applications.resnet_v2.preprocess_input(example['image'])\n",
" \n",
" # parse for input to neural network and loss function\n",
" sample_data = {'image_input': example['image']}\n",
"\n",
" label = tf.cast(example['label'], tf.int32)\n",
" label = tf.one_hot(indices=label, depth=10)\n",
" \n",
" return sample_data, label\n",
"\n",
"test_set = test_set.map(_dataset_parser, num_parallel_calls=tf.data.experimental.AUTOTUNE)\n",
"test_set = test_set.batch(128)"
]
},
{
"cell_type": "code",
"execution_count": 8,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"211/211 [==============================] - 41s 196ms/step - loss: 0.7631 - categorical_accuracy: 0.7422\n"
]
}
],
"source": [
"## inference on test set\n",
"_ = model.evaluate(test_set)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Real-time endpoint inference"
]
},
{
"cell_type": "code",
"execution_count": 9,
"metadata": {},
"outputs": [
{
"name": "stderr",
"output_type": "stream",
"text": [
"update_endpoint is a no-op in sagemaker>=2.\n",
"See: https://sagemaker.readthedocs.io/en/stable/v2.html for details.\n",
"Using already existing model: cinic-10-classifier\n"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"---------------!CPU times: user 25.3 s, sys: 2.39 s, total: 27.7 s\n",
"Wall time: 8min\n"
]
}
],
"source": [
"%%time\n",
"## create model for batch transform (can also be used for real-time inference)\n",
"endpoint_name = 'cinic-10-classifier'\n",
"model_name = endpoint_name\n",
"instance_type = 'ml.g4dn.xlarge'\n",
"\n",
"## if specifying the exact container, use \"image_uri\"\n",
"# https://github.com/aws/deep-learning-containers/blob/master/available_images.md\n",
"image_uri = '763104351884.dkr.ecr.us-east-1.amazonaws.com/tensorflow-inference:2.3.1-gpu-py37-cu102-ubuntu18.04'\n",
"\n",
"## if specifying only the framework version, for which the container is subject to change, use \"framework_version\"\n",
"# https://sagemaker.readthedocs.io/en/stable/frameworks/tensorflow/sagemaker.tensorflow.html#tensorflow-serving-model\n",
"framework_version = '2.3'\n",
"\n",
"## create model and deploy\n",
"sm_model = TensorFlowModel(\n",
" model_data=f's3://{bucket}/{s3key_model_artifact}',\n",
" image_uri=image_uri,\n",
"# framework_version=framework_version,\n",
" source_dir='../source_directory/inference',\n",
" entry_point='inference.py',\n",
" role=sm_role,\n",
" sagemaker_session=sm_session,\n",
" name=endpoint_name,\n",
")\n",
"\n",
"sm_predictor = sm_model.deploy(\n",
" instance_type=instance_type,\n",
" initial_instance_count=1,\n",
" endpoint_name=endpoint_name,\n",
")"
]
},
{
"cell_type": "code",
"execution_count": 10,
"metadata": {},
"outputs": [],
"source": [
"## setup to invoke endpoint\n",
"sm_runtime_client = boto3.client('sagemaker-runtime')\n",
"image_np = np.random.randint(low=0, high=255, size=(32,32,3))\n",
"image_list = image_np.tolist()\n",
"image_json = json.dumps(image_list)"
]
},
{
"cell_type": "code",
"execution_count": 11,
"metadata": {},
"outputs": [],
"source": [
"## invoke endpoint for inference\n",
"response = sm_runtime_client.invoke_endpoint(\n",
" EndpointName=endpoint_name,\n",
" Body=image_json,\n",
" ContentType=\"application/json\",\n",
")\n",
"endpoint_vector = json.loads(response['Body'].read().decode('utf-8'))['predictions'][0]"
]
},
{
"cell_type": "code",
"execution_count": 12,
"metadata": {},
"outputs": [],
"source": [
"## local inference\n",
"image_np = np.expand_dims(image_np, axis=0)\n",
"local_vector = model.predict(tf.keras.applications.resnet_v2.preprocess_input(image_np))\n",
"local_vector = local_vector.tolist()[0]"
]
},
{
"cell_type": "code",
"execution_count": 13,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"All elements are within relative tolerance of 1e-05\n"
]
}
],
"source": [
"## check where (if) the endpoint prediction and local prediction disagree\n",
"rtol = 1e-5\n",
"not_close_idx = np.where(np.isclose(endpoint_vector, local_vector, rtol=rtol) == False)[0]\n",
"for i in not_close_idx:\n",
" print(\"Component {} does not agree:\".format(i))\n",
" print(\"\\tendpoint_vector: {}\".format(endpoint_vector[i]))\n",
" print(\"\\tlocal_vector: {}\".format(local_vector[i]))\n",
"if not_close_idx.size < 1:\n",
" print(\"All elements are within relative tolerance of {}\".format(rtol))"
]
},
{
"cell_type": "code",
"execution_count": 14,
"metadata": {},
"outputs": [],
"source": [
"## delete endpoint and model\n",
"sm_predictor.delete_endpoint()\n",
"sm_predictor.delete_model()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Batch job inference"
]
},
{
"cell_type": "code",
"execution_count": 15,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"........................................................................................................!\n",
"CPU times: user 25.8 s, sys: 2.38 s, total: 28.2 s\n",
"Wall time: 9min 7s\n"
]
}
],
"source": [
"%%time\n",
"env = {'SAGEMAKER_TFS_ENABLE_BATCHING': 'true',\n",
" 'SAGEMAKER_TFS_BATCH_TIMEOUT_MICROS': '50000',\n",
" 'SAGEMAKER_TFS_MAX_BATCH_SIZE': '16'}\n",
"\n",
"sm_model_transformer = sm_model.transformer(\n",
" instance_count=2,\n",
" strategy='SingleRecord',\n",
" instance_type='ml.p3.2xlarge',\n",
" max_concurrent_transforms=64,\n",
" output_path=f's3://{bucket}/distributed_training_demo/batch_transform_output/',\n",
" env=env,\n",
" assemble_with='Line')\n",
"\n",
"sm_model_transformer.transform(\n",
" data=f's3://{bucket}/distributed_training_demo/data/test/',\n",
" data_type='S3Prefix',\n",
" split_type='TFRecord',\n",
" content_type='application/x-tfexample',\n",
" wait=True,\n",
" logs=False,)"
]
},
{
"cell_type": "code",
"execution_count": 20,
"metadata": {},
"outputs": [
{
"data": {
"image/png": "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\n",
"text/plain": [
""
]
},
"metadata": {},
"output_type": "display_data"
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"dog\n"
]
}
],
"source": [
"## parse and confirm data in tfrecords\n",
"\n",
"test_index = 21\n",
"\n",
"# load and test the tf record files\n",
"test_image_s3key = 'distributed_training_demo/data/test/test_000.tfrecords'\n",
"test_set = tf.data.TFRecordDataset(f's3://{bucket}/{test_image_s3key}')\n",
"\n",
"# Create a dictionary describing the features.\n",
"sample_feature_description = {\n",
" 'image': tf.io.FixedLenFeature([], tf.string),\n",
" 'label': tf.io.FixedLenFeature([], tf.int64),\n",
"}\n",
"\n",
"def _parse_sample_function(example_proto):\n",
" return tf.io.parse_single_example(example_proto, sample_feature_description)\n",
"\n",
"test_set = test_set.map(_parse_sample_function)\n",
"test_set = test_set.batch(1)\n",
"for i, sample_features in enumerate(test_set):\n",
" image = tf.io.decode_raw(sample_features['image'], tf.uint8)\n",
" image = tf.reshape(image, (32,32,3))\n",
" \n",
" label = sample_features['label']\n",
" \n",
" if i==test_index: break\n",
"\n",
"display(Image.fromarray(image.numpy()).resize((128,128)))\n",
"print(inverted_label_dictionary[str(int(label.numpy()))])"
]
},
{
"cell_type": "code",
"execution_count": 21,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"dog\n"
]
}
],
"source": [
"## local inference\n",
"processed_image = tf.reshape(tf.keras.applications.resnet_v2.preprocess_input(image.numpy()), (1,32,32,3))\n",
"local_vector = model.predict(processed_image)[0]\n",
"local_prediction = np.argmax(local_vector)\n",
"print(inverted_label_dictionary[str(int(local_prediction))])"
]
},
{
"cell_type": "code",
"execution_count": 22,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"dog\n"
]
}
],
"source": [
"## get inference from batch transform job\n",
"test_image_s3key = 'distributed_training_demo/batch_transform_output/test_000.tfrecords.out'\n",
"bt_inference = s3_client.get_object(Bucket=bucket, Key=test_image_s3key)\n",
"predictions = bt_inference['Body'].read().decode(\"utf-8\")\n",
"bt_vector = np.array(json.loads(predictions.split()[test_index])['predictions'][0])\n",
"bt_prediction = np.argmax(bt_vector)\n",
"print(inverted_label_dictionary[str(int(bt_prediction))])"
]
},
{
"cell_type": "code",
"execution_count": 23,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"All elements are within relative tolerance of 1e-05\n"
]
}
],
"source": [
"## check where (if) the endpoint prediction and local prediction disagree\n",
"rtol = 1e-5\n",
"not_close_idx = np.where(np.isclose(local_vector, bt_vector, rtol=rtol) == False)[0]\n",
"for i in not_close_idx:\n",
" print(\"Component {} does not agree:\".format(i))\n",
" print(\"\\tendpoint_vector: {}\".format(endpoint_vector[i]))\n",
" print(\"\\tlocal_vector: {}\".format(local_vector[i]))\n",
"if not_close_idx.size < 1:\n",
" print(\"All elements are within relative tolerance of {}\".format(rtol))"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": []
}
],
"metadata": {
"kernelspec": {
"display_name": "conda_tensorflow2_p36",
"language": "python",
"name": "conda_tensorflow2_p36"
},
"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.6.10"
}
},
"nbformat": 4,
"nbformat_minor": 4
}