{
"cells": [
{
"cell_type": "markdown",
"id": "83a27336",
"metadata": {
"papermill": {
"duration": 0.018003,
"end_time": "2021-06-03T00:09:48.368659",
"exception": false,
"start_time": "2021-06-03T00:09:48.350656",
"status": "completed"
},
"tags": []
},
"source": [
"# Deployment Guardrails to update Endpoint\n",
"---\n",
"\n",
"## Introduction\n",
"---\n",
"\n",
"SageMaker 배포 가드레일(Deployment Guardrail)은 프로덕션 환경에서 현재 모델에서 새 모델로 안전하게 업데이트하기 위한 완전 관리형 블루/그린 배포 가드레일 서비스입니다.\n",
"카나리 및 선형과 같은 트래픽 전환 모드를 사용하여 업데이트 과정에서 현재 모델에서 새 모델로 트래픽 전환 프로세스를 세부적으로 제어할 수 있습니다. 또한 문제를 조기에 포착하고 프로덕션에 영향을 미치지 않게 자동 롤백과 같은 보호 기능을 제공합니다.\n",
"\n",
"트래픽 전환 모드는 엔드포인트 트래픽이 업데이트가 포함된 새 집합으로 라우팅되는 방식을 지정하는 구성으로, 엔드포인트 업데이트 프로세스에 대한 다양한 제어 수준을 제공합니다.\n",
"\n",
"- **All-At-Once Traffic Shifting** : 모든 엔드포인트 트래픽을 블루 플릿에서 그린 플릿으로 전환합니다. 트래픽이 그린 플릿으로 이동하면 미리 지정된 Amazon CloudWatch 알람이 설정된 시간(\"베이킹 기간; baking period\") 동안 그린 플릿 모니터링을 시작합니다. 베이킹 기간 동안 알람이 트리거되지 않으면 블루 플릿이 종료됩니다.\n",
"- **Canary Traffic Shifting** : 트래픽의 작은 부분(\"카나리\")을 그린 플릿으로 이동하고 베이킹 기간 동안 모니터링합니다. 카나리 배포가 그린 플릿에서 성공하면 나머지 트래픽은 블루 플릿을 종료하기 전에 블루 플릿에서 그린 플릿으로 이동합니다.\n",
"- **Linear Traffic Shifting**: 트래픽 이동 단계를 n개로 확장하여 각 단계에 대해 이동할 트래픽 비율에 대해 세부적으로 지정할 수 있습니다.\n",
"\n",
"본 실습에서는 트래픽 이동 및 자동 롤백 기능을 보여주기 위해 아래와 같은 기능들을 체험해 봅니다.\n",
"\n",
"- 모델 1~모델 3에 대한 모델 및 엔드포인트 구성 생성\n",
" - 모델 1: 정상 동작, PyTorch 1.7.1\n",
" - 모델 2: 에러 발생, PyTorch 1.7.1 \n",
" - 모델 3: 정상 동작, PyTorch 1.8.1\n",
"- 모델 1의 엔드포인트 설정으로 엔드포인트 시작\n",
"- 롤백을 트리거하는 데 사용되는 CloudWatch 알람 지정\n",
"- 모델 2의 엔드포인트 설정을 가리키도록 엔드포인트 업데이트 \n",
" - 일정 시간 경과 후 일부 트래픽이 모델 2를 호출하는 플릿으로 이동되며, CloudWatch 알람에서 오류 이벤트를 감지하여 자동으로 모델 1 플릿으로 롤백 \n",
"- 모델 3의 엔드포인트 설정을 가리키도록 엔드포인트 업데이트 \n",
" - 일정 시간 경과 후 일부 트래픽이 모델 3을 호출하는 플릿으로 이동되며, 오류가 없다면 점진적으로 모든 트래픽이 모델 3 플릿으로 이동\n",
"\n",
"### References\n",
"- Take advantage of advanced deployment strategies using Amazon SageMaker deployment guardrails: https://aws.amazon.com/ko/blogs/machine-learning/take-advantage-of-advanced-deployment-strategies-using-amazon-sagemaker-deployment-guardrails/"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "15a17b66",
"metadata": {},
"outputs": [],
"source": [
"import os\n",
"import json\n",
"import sys\n",
"import logging\n",
"import boto3\n",
"import sagemaker\n",
"import time\n",
"from datetime import datetime, timedelta\n",
"from sagemaker.huggingface import HuggingFaceModel\n",
"from sagemaker import session\n",
"from transformers import ElectraConfig\n",
"from transformers import (\n",
" ElectraModel, ElectraTokenizer, ElectraForSequenceClassification\n",
")\n",
"from src.utils import print_outputs, upload_model_artifact_to_s3, NLPPredictor \n",
"\n",
"\n",
"logging.basicConfig(\n",
" level=logging.INFO, \n",
" format='[{%(filename)s:%(lineno)d} %(levelname)s - %(message)s',\n",
" handlers=[\n",
" logging.FileHandler(filename='tmp.log'),\n",
" logging.StreamHandler(sys.stdout)\n",
" ]\n",
")\n",
"logger = logging.getLogger(__name__)\n",
"\n",
"role = sagemaker.get_execution_role()\n",
"region = boto3.Session().region_name\n",
"sess = sagemaker.Session()\n",
"boto_session = boto3.session.Session()\n",
"sm_client = boto_session.client(\"sagemaker\")\n",
"sm_runtime = boto3.Session().client(\"sagemaker-runtime\")"
]
},
{
"cell_type": "markdown",
"id": "54216bbb",
"metadata": {},
"source": [
"
\n",
"\n",
"## 1. Create and Deploy Models\n",
"---\n",
"\n",
"사전 훈련된 한국어 자연어 처리 모델(네이버 감성 분류 긍정/부정 판별)을 배포합니다."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "65864aa5",
"metadata": {},
"outputs": [],
"source": [
"bucket = sess.default_bucket()\n",
"prefix = 'deployment-guardraril-kornlp-nsmc'"
]
},
{
"cell_type": "markdown",
"id": "f917566e",
"metadata": {},
"source": [
"모델 파라메터, 토크나이저와 추론 코드를 `model.tar.gz`으로 압축하여 S3로 복사합니다. 압축 파일명은 자유롭게 지정할 수 있으나, 반드시 `tar.gz`로 압축해야 합니다."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "8a359602",
"metadata": {},
"outputs": [],
"source": [
"model_variant = 'modelA'\n",
"nlp_task = 'nsmc'\n",
"model_path = f'model-{nlp_task}'\n",
"model_s3_uri = upload_model_artifact_to_s3(model_variant, model_path, bucket, prefix)"
]
},
{
"cell_type": "markdown",
"id": "bc18ee18",
"metadata": {},
"source": [
"### Create Models"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "cabf6566",
"metadata": {},
"outputs": [],
"source": [
"from sagemaker.image_uris import retrieve\n",
"\n",
"ecr_image_uri1 = retrieve(\n",
" framework='pytorch',\n",
" region=region,\n",
" version='1.7.1',\n",
" py_version='py3',\n",
" instance_type='ml.m5.xlarge',\n",
" accelerator_type=None,\n",
" image_scope='inference'\n",
")\n",
"\n",
"ecr_image_uri2 = retrieve(\n",
" framework='pytorch',\n",
" region=region,\n",
" version='1.7.1',\n",
" py_version='py3',\n",
" instance_type='ml.m5.xlarge',\n",
" accelerator_type=None,\n",
" image_scope='inference'\n",
")\n",
"\n",
"ecr_image_uri3 = retrieve(\n",
" framework='pytorch',\n",
" region=region,\n",
" version='1.8.1',\n",
" py_version='py3',\n",
" instance_type='ml.m5.xlarge',\n",
" accelerator_type=None,\n",
" image_scope='inference'\n",
")\n",
"\n",
"print(f\"Model Image 1: {ecr_image_uri1}\")\n",
"print(f\"Model Image 2: {ecr_image_uri2}\")\n",
"print(f\"Model Image 3: {ecr_image_uri3}\")"
]
},
{
"cell_type": "markdown",
"id": "ed727648",
"metadata": {},
"source": [
"`inference_nsmc_error.py`은 인위적으로 에러를 발생하기 위해 '12'/'34'의 더미 코드를 삽입했습니다."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "6d6a0271",
"metadata": {},
"outputs": [],
"source": [
"model_name1 = f\"model1-{prefix}-{datetime.now():%Y-%m-%d-%H-%M-%S}\"\n",
"model_name2 = f\"model2-{prefix}-{datetime.now():%Y-%m-%d-%H-%M-%S}\"\n",
"model_name3 = f\"model3-{prefix}-{datetime.now():%Y-%m-%d-%H-%M-%S}\"\n",
"\n",
"resp1 = sm_client.create_model(\n",
" ModelName=model_name1,\n",
" Containers=[\n",
" {\n",
" \"Image\": ecr_image_uri1,\n",
" \"Mode\": \"SingleModel\",\n",
" \"ModelDataUrl\": model_s3_uri,\n",
" \"Environment\": {\n",
" \"SAGEMAKER_CONTAINER_LOG_LEVEL\": \"20\",\n",
" \"SAGEMAKER_PROGRAM\": \"inference_nsmc.py\",\n",
" \"SAGEMAKER_SUBMIT_DIRECTORY\": model_s3_uri,\n",
" }, \n",
" } \n",
" \n",
" ],\n",
" ExecutionRoleArn=role,\n",
")\n",
"\n",
"resp2 = sm_client.create_model(\n",
" ModelName=model_name2,\n",
" Containers=[\n",
" {\n",
" \"Image\": ecr_image_uri2,\n",
" \"Mode\": \"SingleModel\",\n",
" \"ModelDataUrl\": model_s3_uri,\n",
" \"Environment\": {\n",
" \"SAGEMAKER_CONTAINER_LOG_LEVEL\": \"20\",\n",
" \"SAGEMAKER_PROGRAM\": \"inference_nsmc_error.py\",\n",
" \"SAGEMAKER_SUBMIT_DIRECTORY\": model_s3_uri,\n",
" }, \n",
" } \n",
" \n",
" ],\n",
" ExecutionRoleArn=role,\n",
")\n",
"\n",
"resp3 = sm_client.create_model(\n",
" ModelName=model_name3,\n",
" Containers=[\n",
" {\n",
" \"Image\": ecr_image_uri3,\n",
" \"Mode\": \"SingleModel\",\n",
" \"ModelDataUrl\": model_s3_uri,\n",
" \"Environment\": {\n",
" \"SAGEMAKER_CONTAINER_LOG_LEVEL\": \"20\",\n",
" \"SAGEMAKER_PROGRAM\": \"inference_nsmc.py\",\n",
" \"SAGEMAKER_SUBMIT_DIRECTORY\": model_s3_uri,\n",
" }, \n",
" } \n",
" \n",
" ],\n",
" ExecutionRoleArn=role,\n",
")\n",
"\n",
"print(f\"Created Model1: {resp1['ModelArn']}\") \n",
"print(f\"Created Model2: {resp2['ModelArn']}\")\n",
"print(f\"Created Model3: {resp3['ModelArn']}\") "
]
},
{
"cell_type": "markdown",
"id": "2d2c8ba2",
"metadata": {},
"source": [
"### Create Endpoint Configs"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "c1de7a90",
"metadata": {},
"outputs": [],
"source": [
"endpoint_config_name1 = f\"endpoint-config1-{datetime.now():%Y-%m-%d-%H-%M-%S}\"\n",
"endpoint_config_name2 = f\"endpoint-config2-{datetime.now():%Y-%m-%d-%H-%M-%S}\"\n",
"endpoint_config_name3 = f\"endpoint-config3-{datetime.now():%Y-%m-%d-%H-%M-%S}\"\n",
"\n",
"config_resp1 = sm_client.create_endpoint_config(\n",
" EndpointConfigName=endpoint_config_name1,\n",
" ProductionVariants=[\n",
" {\n",
" \"VariantName\": \"AllTraffic\",\n",
" \"InstanceType\": \"ml.m5.xlarge\",\n",
" \"InitialInstanceCount\": 3,\n",
" \"ModelName\": model_name1, \n",
" },\n",
" ],\n",
")\n",
"\n",
"config_resp2 = sm_client.create_endpoint_config(\n",
" EndpointConfigName=endpoint_config_name2,\n",
" ProductionVariants=[\n",
" {\n",
" \"VariantName\": \"AllTraffic\",\n",
" \"InstanceType\": \"ml.m5.xlarge\",\n",
" \"InitialInstanceCount\": 3,\n",
" \"ModelName\": model_name2, \n",
" },\n",
" ],\n",
")\n",
"\n",
"config_resp3 = sm_client.create_endpoint_config(\n",
" EndpointConfigName=endpoint_config_name3,\n",
" ProductionVariants=[\n",
" {\n",
" \"VariantName\": \"AllTraffic\",\n",
" \"InstanceType\": \"ml.m5.xlarge\",\n",
" \"InitialInstanceCount\": 3,\n",
" \"ModelName\": model_name3, \n",
" },\n",
" ],\n",
")\n",
"\n",
"print(f\"Created EndpointConfig1: {config_resp1['EndpointConfigArn']}\")\n",
"print(f\"Created EndpointConfig2: {config_resp2['EndpointConfigArn']}\")\n",
"print(f\"Created EndpointConfig3: {config_resp3['EndpointConfigArn']}\") "
]
},
{
"cell_type": "markdown",
"id": "b83e70ba",
"metadata": {},
"source": [
"### Create Endpoint\n",
"\n",
"엔드포인트 설정1(베이스라인)에 대해 호스팅 엔드포인트를 배포합니다."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "7e008b29",
"metadata": {},
"outputs": [],
"source": [
"endpoint_name = f\"endpoint-canary-{nlp_task}-{datetime.now():%Y-%m-%d-%H-%M-%S}\"\n",
"endpoint_resp = sm_client.create_endpoint(\n",
" EndpointName=endpoint_name, \n",
" EndpointConfigName=endpoint_config_name1\n",
")\n",
"\n",
"print(f\"Creating Endpoint: {endpoint_resp['EndpointArn']}\")"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "a6dee7a3",
"metadata": {},
"outputs": [],
"source": [
"from IPython.core.display import display, HTML\n",
"\n",
"def make_endpoint_link(region, endpoint_name, endpoint_task):\n",
" endpoint_link = f'{endpoint_task} Review Endpoint' \n",
" return endpoint_link \n",
"\n",
"def wait_for_endpoint_in_service(endpoint_name):\n",
" waiter = boto3.client('sagemaker').get_waiter('endpoint_in_service')\n",
" print(\"Waiting for endpoint to create...\")\n",
" waiter.wait(EndpointName=endpoint_name)\n",
" resp = sm_client.describe_endpoint(EndpointName=endpoint_name)\n",
" print(f\"Endpoint Status: {resp['EndpointStatus']}\")\n",
" \n",
"endpoint_link = make_endpoint_link(region, endpoint_name, '[Deploy model from S3]')\n",
"display(HTML(endpoint_link))"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "def5f438",
"metadata": {},
"outputs": [],
"source": [
"sm_client.describe_endpoint(EndpointName=endpoint_name)\n",
"wait_for_endpoint_in_service(endpoint_name)"
]
},
{
"cell_type": "markdown",
"id": "717e3da7",
"metadata": {},
"source": [
"
\n",
"\n",
"## 2. Invoke Endpoint\n",
"---\n",
"\n",
"엔드포인트 배포가 완료되었다면 실시간 추론을 수행할 수 있습니다. 테스트를 위해 최대 호출 횟수(maximum invocations) 및 대기 간격(waiting interval)을 지정하여 엔드포인트를 여러 번 호출합니다."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "8ae73edb",
"metadata": {},
"outputs": [],
"source": [
"sample_file = f'sample_{nlp_task}.txt'\n",
"with open(sample_file, mode='wt', encoding='utf-8') as f:\n",
" f.write('{\"text\": [\"이 영화는 최고의 영화입니다\"]}\\n') \n",
" f.write('{\"text\": [\"최악이에요. 배우의 연기력도 좋지 않고 내용도 너무 허접합니다\"]}')\n",
" \n",
"with open(sample_file, mode='rb') as f:\n",
" payloads = f.read() "
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "98bc9c54",
"metadata": {},
"outputs": [],
"source": [
"from datetime import datetime, timedelta, timezone\n",
"\n",
"def invoke_endpoint(payloads, endpoint_name, target_variant=None):\n",
" start = time.time()\n",
" if target_variant is not None:\n",
" response = sm_runtime.invoke_endpoint(\n",
" EndpointName=endpoint_name,\n",
" ContentType=\"application/jsonlines\", \n",
" Accept=\"application/jsonlines\", \n",
" TargetVariant=target_variant,\n",
" Body=payloads,\n",
" ) \n",
" else: \n",
" response = sm_runtime.invoke_endpoint(\n",
" EndpointName=endpoint_name,\n",
" ContentType=\"application/jsonlines\", \n",
" Accept=\"application/jsonlines\", \n",
" Body=payloads,\n",
" )\n",
" latency = (time.time() - start) * 1000\n",
" variant = response[\"InvokedProductionVariant\"]\n",
" logger.info(f'[{variant}] Latency: {latency:.3f} ms')\n",
" \n",
" outputs = response['Body'].read().decode()\n",
" return outputs\n",
"\n",
"def invoke_endpoint_many(payloads, endpoint_name, num_requests=250, sleep_secs=0.5, should_raise_exp=False):\n",
" for i in range(num_requests):\n",
" try:\n",
" response = sm_runtime.invoke_endpoint(\n",
" EndpointName=endpoint_name,\n",
" ContentType=\"application/jsonlines\", \n",
" Accept=\"application/jsonlines\", \n",
" Body=payloads,\n",
" )\n",
" outputs = response['Body'].read().decode()\n",
" print(\".\", end=\"\", flush=True)\n",
" except Exception as e:\n",
" print(\"E\", end=\"\", flush=True) \n",
" if should_raise_exp:\n",
" raise e\n",
"\n",
" time.sleep(sleep_secs)\n",
" print('\\nDone!')"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "d2d2e953",
"metadata": {},
"outputs": [],
"source": [
"outputs = invoke_endpoint(payloads, endpoint_name)\n",
"print_outputs(outputs)"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "06003fe9",
"metadata": {},
"outputs": [],
"source": [
"invocation_start_time = datetime.now()\n",
"invoke_endpoint_many(payloads, endpoint_name, 250, 0.5)\n",
"time.sleep(20) # give metrics time to catch up"
]
},
{
"cell_type": "markdown",
"id": "f9cff4ca",
"metadata": {},
"source": [
"### Invocations Metrics\n",
"\n",
"Amazon SageMaker는 Amazon CloudWatch를 쿼리하여 레이턴시 및 호출(invocations)과 같은 지표들을 모니터링할 수 있습니다. 모니터링 가능한 지표들은 아래 링크를 참조해 주세요.\n",
"- https://docs.aws.amazon.com/sagemaker/latest/dg/monitoring-cloudwatch.html"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "5a9711ab",
"metadata": {},
"outputs": [],
"source": [
"import pandas as pd\n",
"\n",
"cw = boto3.Session().client(\"cloudwatch\", region_name=region)\n",
"\n",
"def get_sagemaker_metrics(\n",
" endpoint_name,\n",
" endpoint_config_name,\n",
" variant_name,\n",
" metric_name,\n",
" statistic,\n",
" start_time,\n",
" end_time,\n",
"):\n",
" dimensions = [\n",
" {\"Name\": \"EndpointName\", \"Value\": endpoint_name},\n",
" {\"Name\": \"VariantName\", \"Value\": variant_name},\n",
" ]\n",
" if endpoint_config_name is not None:\n",
" dimensions.append({\"Name\": \"EndpointConfigName\", \"Value\": endpoint_config_name})\n",
" metrics = cw.get_metric_statistics(\n",
" Namespace=\"AWS/SageMaker\",\n",
" MetricName=metric_name,\n",
" StartTime=start_time,\n",
" EndTime=end_time,\n",
" Period=60,\n",
" Statistics=[statistic],\n",
" Dimensions=dimensions,\n",
" )\n",
" rename = endpoint_config_name if endpoint_config_name is not None else \"ALL\"\n",
" if len(metrics[\"Datapoints\"]) == 0:\n",
" return\n",
" return (\n",
" pd.DataFrame(metrics[\"Datapoints\"])\n",
" .sort_values(\"Timestamp\")\n",
" .set_index(\"Timestamp\")\n",
" .drop([\"Unit\"], axis=1)\n",
" .rename(columns={statistic: rename})\n",
" )\n",
"\n",
"\n",
"def plot_endpoint_invocation_metrics(\n",
" endpoint_name,\n",
" endpoint_config_name,\n",
" variant_name,\n",
" metric_name,\n",
" statistic,\n",
" start_time=None,\n",
"):\n",
" start_time = start_time or datetime.now(timezone.utc) - timedelta(minutes=60)\n",
" end_time = datetime.now(timezone.utc)\n",
" metrics_variants = get_sagemaker_metrics(\n",
" endpoint_name,\n",
" endpoint_config_name,\n",
" variant_name,\n",
" metric_name,\n",
" statistic,\n",
" start_time,\n",
" end_time,\n",
" )\n",
" if metrics_variants is None:\n",
" return\n",
" metrics_variants.plot(title=f\"{metric_name}-{statistic}\")\n",
" return metrics_variants"
]
},
{
"cell_type": "markdown",
"id": "0f390943",
"metadata": {},
"source": [
"### Plot endpoint invocation metrics\n",
"\n",
"아래 코드 셀을 호출하면, 엔드포인트에 대한 Invocation,Invocation4XXErrors,Invocation5XXErrors,ModelLatency 및 OverheadLatency를 표시하는 그래프를 플롯합니다.\n",
"\n",
"현재는 정상적으로 동작하는 모델 버전과 설정을 사용하고 있으므로 Invocation4XXErrors 및 Invocation5XXErrors는 플랫 라인(y축의 값이 일정하게 0)에 있어야 함을 알 수 있습니다. 또한 ModelLatency 및 OverheadLatency는 시간이 지남에 따라 감소하기 시작합니다."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "c6ab75a3",
"metadata": {},
"outputs": [],
"source": [
"invocation_metrics = plot_endpoint_invocation_metrics(\n",
" endpoint_name, endpoint_config_name1, \"AllTraffic\", \"Invocations\", \"Sum\", invocation_start_time\n",
")\n",
"invocation_4xx_metrics = plot_endpoint_invocation_metrics(\n",
" endpoint_name, None, \"AllTraffic\", \"Invocation4XXErrors\", \"Sum\", invocation_start_time\n",
")\n",
"invocation_5xx_metrics = plot_endpoint_invocation_metrics(\n",
" endpoint_name, None, \"AllTraffic\", \"Invocation5XXErrors\", \"Sum\", invocation_start_time\n",
")\n",
"model_latency_metrics = plot_endpoint_invocation_metrics(\n",
" endpoint_name, None, \"AllTraffic\", \"ModelLatency\", \"Average\", invocation_start_time\n",
")\n",
"overhead_latency_metrics = plot_endpoint_invocation_metrics(\n",
" endpoint_name, None, \"AllTraffic\", \"OverheadLatency\", \"Average\", invocation_start_time\n",
")"
]
},
{
"cell_type": "markdown",
"id": "be6222c2",
"metadata": {},
"source": [
"
\n",
"\n",
"## 3. Create CloudWatch alarms to monitor Endpoint performance\n",
"---\n",
"\n",
"본 섹션에서는 아래 지표들을 사용하여 엔드포인트 성능을 모니터링하는 CloudWatch 알람을 생성합니다.\n",
"* Invocation5XXErrors\n",
"* ModelLatency\n",
"\n",
"CloudWatch 알람을 생성하는 메소드(`put_metric_alarm`)의 인자값에서 Dimension은 엔드포인트 설정 및 variant별로 지표를 선택하는 데 사용됩니다. \n",
"* EndpointName\n",
"* VariantName"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "ed109b92",
"metadata": {},
"outputs": [],
"source": [
"def create_auto_rollback_alarm(\n",
" alarm_name, endpoint_name, variant_name, metric_name, statistic, threshold\n",
"):\n",
" cw.put_metric_alarm(\n",
" AlarmName=alarm_name,\n",
" AlarmDescription=\"Test SageMaker endpoint deployment auto-rollback alarm\",\n",
" ActionsEnabled=False,\n",
" Namespace=\"AWS/SageMaker\",\n",
" MetricName=metric_name,\n",
" Statistic=statistic,\n",
" Dimensions=[\n",
" {\"Name\": \"EndpointName\", \"Value\": endpoint_name},\n",
" {\"Name\": \"VariantName\", \"Value\": variant_name},\n",
" ],\n",
" Period=60,\n",
" EvaluationPeriods=1,\n",
" Threshold=threshold,\n",
" ComparisonOperator=\"GreaterThanOrEqualToThreshold\",\n",
" TreatMissingData=\"notBreaching\",\n",
" )"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "42a592ce",
"metadata": {},
"outputs": [],
"source": [
"error_alarm = f\"TestAlarm-5XXErrors-{endpoint_name}\"\n",
"latency_alarm = f\"TestAlarm-ModelLatency-{endpoint_name}\"\n",
"\n",
"# alarm on 5xx error rate for 1 minute\n",
"create_auto_rollback_alarm(\n",
" error_alarm, endpoint_name, \"AllTraffic\", \"Invocation5XXErrors\", \"Average\", 0.1\n",
")\n",
"# alarm on model latency >= 400 ms for 1 minute\n",
"create_auto_rollback_alarm(\n",
" latency_alarm, endpoint_name, \"AllTraffic\", \"ModelLatency\", \"Average\", 400000\n",
")"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "ca8c15d6",
"metadata": {},
"outputs": [],
"source": [
"cw.describe_alarms(AlarmNames=[error_alarm, latency_alarm])\n",
"time.sleep(60)"
]
},
{
"cell_type": "markdown",
"id": "469a9cf1",
"metadata": {},
"source": [
"
\n",
"\n",
"## 4. Update Endpoint with deployment configurations\n",
"---\n",
"\n",
"엔드포인트를 업데이트하고 CloudWatch 지표에서 성능을 모니터링합니다.\n",
"\n",
"### BlueGreen update policy with Canary/Linear traffic shifting\n",
"\n",
"트래픽이 이전 스택에서 새 스택으로 이동하는 블루/그린 업데이트를 쉽게 수행할 수 있습니다. 카나리(Canary)/선형(Linear) 모드를 사용하면 호출 요청이 신규 버전의 모델로 점진적으로 이동하여 오류가 트래픽의 100%에 영향을 미치는 것을 방지합니다. 새 버전의 모델에서 일정 이상의 오류 발생 시 자동으로 이전 버전의 모델로 롤백함으로써, \n",
"신규 버전 모델 배포에 대한 리스크를 최소화할 수 있습니다. "
]
},
{
"cell_type": "markdown",
"id": "cd96db2d",
"metadata": {},
"source": [
"### Rollback Case \n",
"\n",
"호환되지 않는 모델 버전(모델-2, 엔드포인트 config-2)으로 엔드포인트를 업데이트하여 오류를 시뮬레이션하고 롤백을 트리거합니다."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "dcbc277e",
"metadata": {},
"outputs": [],
"source": [
"# canary_deployment_config\n",
"canary_deployment_config = {\n",
" \"BlueGreenUpdatePolicy\": {\n",
" \"TrafficRoutingConfiguration\": {\n",
" \"Type\": \"CANARY\",\n",
" \"CanarySize\": {\n",
" \"Type\": \"INSTANCE_COUNT\", # or use \"CAPACITY_PERCENT\" as 30%, 50%\n",
" \"Value\": 1,\n",
" },\n",
" \"WaitIntervalInSeconds\": 300, # wait for 5 minutes before enabling traffic on the rest of fleet\n",
" },\n",
" \"TerminationWaitInSeconds\": 120, # wait for 2 minutes before terminating the old stack\n",
" \"MaximumExecutionTimeoutInSeconds\": 1800, # maximum timeout for deployment\n",
" },\n",
" \"AutoRollbackConfiguration\": {\n",
" \"Alarms\": [{\"AlarmName\": error_alarm}],\n",
" },\n",
"}\n",
"\n",
"# linear_deployment_config\n",
"linear_deployment_config = {\n",
" \"BlueGreenUpdatePolicy\": {\n",
" \"TrafficRoutingConfiguration\": {\n",
" \"Type\": \"LINEAR\",\n",
" \"LinearStepSize\": {\n",
" \"Type\": \"CAPACITY_PERCENT\",\n",
" \"Value\": 33, # 33% of whole fleet capacity (33% * 3 = 1 instance)\n",
" },\n",
" \"WaitIntervalInSeconds\": 180, # wait for 3 minutes before enabling traffic on the rest of fleet\n",
" },\n",
" \"TerminationWaitInSeconds\": 120, # wait for 2 minutes before terminating the old stack\n",
" \"MaximumExecutionTimeoutInSeconds\": 1800, # maximum timeout for deployment\n",
" },\n",
" \"AutoRollbackConfiguration\": {\n",
" \"Alarms\": [{\"AlarmName\": error_alarm}],\n",
" },\n",
"}\n",
"\n",
"# update endpoint request with new DeploymentConfig parameter\n",
"sm_client.update_endpoint(\n",
" EndpointName=endpoint_name,\n",
" EndpointConfigName=endpoint_config_name2,\n",
" DeploymentConfig=linear_deployment_config,\n",
")"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "ba2db1b8",
"metadata": {},
"outputs": [],
"source": [
"sm_client.describe_endpoint(EndpointName=endpoint_name)"
]
},
{
"cell_type": "markdown",
"id": "68c39b41",
"metadata": {},
"source": [
"### Invoke the endpoint during the update operation is in progress\n",
"\n",
"아래 코드 셀을 실행하면 카나리/선형 플릿의 오류를 시뮬레이션합니다. 일정 시간 경과 후 확률적으로 오류(E)가 표시됩니다."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "9ef0c86a",
"metadata": {},
"outputs": [],
"source": [
"invoke_endpoint_many(payloads, endpoint_name, 400, 0.8)"
]
},
{
"cell_type": "markdown",
"id": "8448b6f1",
"metadata": {},
"source": [
"엔드포인트 업데이트 작업이 완료될 때까지 기다렸다가 자동 롤백을 확인합니다."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "9543e860",
"metadata": {},
"outputs": [],
"source": [
"wait_for_endpoint_in_service(endpoint_name)\n",
"sm_client.describe_endpoint(EndpointName=endpoint_name)"
]
},
{
"cell_type": "markdown",
"id": "3879404c",
"metadata": {},
"source": [
"이전 모델로 다시 롤백되어서 추론이 잘 이루어지고 있음을 확인할 수 있습니다. 만약 엔드포인트"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "85ce35b4",
"metadata": {},
"outputs": [],
"source": [
"outputs = invoke_endpoint(payloads, endpoint_name)\n",
"print_outputs(outputs)"
]
},
{
"cell_type": "markdown",
"id": "f1aa6cab",
"metadata": {},
"source": [
"아래 코드 셀을 실행하면 Invocations,Invocation5XXErrors 및 ModelLatency를 엔드포인트에 대해 표시하는 그래프를 플롯합니다.\n",
"\n",
"신규 엔드포인트 config-2(오류가 발생하는 모델-2)로 엔드포인트를 업데이트하면, 일정 시간 경과 후 CloudWatch 알람이 발생하고 엔드포인트 config-1로 롤백됩니다. 이 롤백 단계에서 Invocation5XXErrors가 증가하는 것을 아래 그래프에서 볼 수 있습니다."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "5207517e",
"metadata": {},
"outputs": [],
"source": [
"invocation_metrics = plot_endpoint_invocation_metrics(\n",
" endpoint_name, None, \"AllTraffic\", \"Invocations\", \"Sum\"\n",
")\n",
"metrics_epc_1 = plot_endpoint_invocation_metrics(\n",
" endpoint_name, endpoint_config_name1, \"AllTraffic\", \"Invocations\", \"Sum\"\n",
")\n",
"metrics_epc_2 = plot_endpoint_invocation_metrics(\n",
" endpoint_name, endpoint_config_name2, \"AllTraffic\", \"Invocations\", \"Sum\"\n",
")\n",
"\n",
"metrics_all = invocation_metrics.join([metrics_epc_1, metrics_epc_2], how=\"outer\")\n",
"metrics_all.plot(title=\"Invocations-Sum\")\n",
"\n",
"invocation_5xx_metrics = plot_endpoint_invocation_metrics(\n",
" endpoint_name, None, \"AllTraffic\", \"Invocation5XXErrors\", \"Sum\"\n",
")\n",
"model_latency_metrics = plot_endpoint_invocation_metrics(\n",
" endpoint_name, None, \"AllTraffic\", \"ModelLatency\", \"Average\"\n",
")"
]
},
{
"cell_type": "markdown",
"id": "48abaf58",
"metadata": {},
"source": [
"### Success Case\n",
"\n",
"이번에는 동일한 카나리/선형 배포 설정을 사용하지만 유효한 엔드포인트 설정을 사용하는 성공 사례를 살펴보겠습니다.\n",
"\n",
"먼저, 정상적으로 동작하는 엔드포인트 config-3으로 엔드포인트를 업데이트합니다."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "38e04e44",
"metadata": {},
"outputs": [],
"source": [
"# update endpoint request with new DeploymentConfig parameter\n",
"sm_client.update_endpoint(\n",
" EndpointName=endpoint_name,\n",
" EndpointConfigName=endpoint_config_name3,\n",
" RetainDeploymentConfig=True,\n",
")"
]
},
{
"cell_type": "markdown",
"id": "1f2ba7f5",
"metadata": {},
"source": [
"아래 코드 셀을 실행하면 카나리/선형 플릿을 시뮬레이션합니다. 오류가 발생하지 않는 모델이므로 정상적으로 수행됩니다."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "3136bb66",
"metadata": {},
"outputs": [],
"source": [
"invoke_endpoint_many(payloads, endpoint_name, 300, 0.8)"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "0a7e5fe2",
"metadata": {},
"outputs": [],
"source": [
"wait_for_endpoint_in_service(endpoint_name)\n",
"sm_client.describe_endpoint(EndpointName=endpoint_name)"
]
},
{
"cell_type": "markdown",
"id": "f355a2d1",
"metadata": {},
"source": [
"신규 엔드포인트 config-3(올바르게 동작하는 모델-3)으로 엔드포인트를 업데이트하면, 오류 없이 엔드포인트 config-2(오류가 발생하는 모델-2)를 인수합니다. 이 전환 단계에서 Invocation5XXErrors가 감소하는 것을 아래 그래프에서 볼 수 있습니다."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "b9d752a4",
"metadata": {},
"outputs": [],
"source": [
"invocation_metrics = plot_endpoint_invocation_metrics(\n",
" endpoint_name, None, \"AllTraffic\", \"Invocations\", \"Sum\"\n",
")\n",
"metrics_epc_1 = plot_endpoint_invocation_metrics(\n",
" endpoint_name, endpoint_config_name1, \"AllTraffic\", \"Invocations\", \"Sum\"\n",
")\n",
"metrics_epc_2 = plot_endpoint_invocation_metrics(\n",
" endpoint_name, endpoint_config_name2, \"AllTraffic\", \"Invocations\", \"Sum\"\n",
")\n",
"metrics_epc_3 = plot_endpoint_invocation_metrics(\n",
" endpoint_name, endpoint_config_name3, \"AllTraffic\", \"Invocations\", \"Sum\"\n",
")"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "12a2adc3",
"metadata": {},
"outputs": [],
"source": [
"metrics_all = invocation_metrics.join([metrics_epc_1, metrics_epc_2, metrics_epc_3], how=\"outer\")\n",
"metrics_all.plot(title=\"Invocations-Sum\")\n",
"\n",
"invocation_5xx_metrics = plot_endpoint_invocation_metrics(\n",
" endpoint_name, None, \"AllTraffic\", \"Invocation5XXErrors\", \"Sum\"\n",
")\n",
"model_latency_metrics = plot_endpoint_invocation_metrics(\n",
" endpoint_name, None, \"AllTraffic\", \"ModelLatency\", \"Average\"\n",
")"
]
},
{
"cell_type": "markdown",
"id": "c70d0377",
"metadata": {},
"source": [
"
\n",
"\n",
"## Clean up\n",
"---"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "401c69c3",
"metadata": {},
"outputs": [],
"source": [
"sm_client.delete_endpoint(EndpointName=endpoint_name)"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "10f722dc",
"metadata": {},
"outputs": [],
"source": [
"sm_client.delete_endpoint_config(EndpointConfigName=endpoint_config_name1)\n",
"sm_client.delete_endpoint_config(EndpointConfigName=endpoint_config_name2)\n",
"sm_client.delete_endpoint_config(EndpointConfigName=endpoint_config_name3)"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "38b84406",
"metadata": {},
"outputs": [],
"source": [
"sm_client.delete_model(ModelName=model_name1)\n",
"sm_client.delete_model(ModelName=model_name2)\n",
"sm_client.delete_model(ModelName=model_name3)"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "de0d2fc4",
"metadata": {},
"outputs": [],
"source": [
"cw.delete_alarms(AlarmNames=[error_alarm, latency_alarm])"
]
}
],
"metadata": {
"anaconda-cloud": {},
"instance_type": "ml.t3.medium",
"kernelspec": {
"display_name": "conda_pytorch_latest_p37",
"language": "python",
"name": "conda_pytorch_latest_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.12"
},
"notice": "Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. Licensed under the Apache License, Version 2.0 (the \"License\"). You may not use this file except in compliance with the License. A copy of the License is located at http://aws.amazon.com/apache2.0/ or in the \"license\" file accompanying this file. This file is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.",
"papermill": {
"default_parameters": {},
"duration": 199.476853,
"end_time": "2021-06-03T00:13:06.967499",
"environment_variables": {},
"exception": true,
"input_path": "a_b_testing.ipynb",
"output_path": "/opt/ml/processing/output/a_b_testing-2021-06-03-00-05-59.ipynb",
"parameters": {
"kms_key": "arn:aws:kms:us-west-2:521695447989:key/6e9984db-50cf-4c7e-926c-877ec47a8b25"
},
"start_time": "2021-06-03T00:09:47.490646",
"version": "2.3.3"
}
},
"nbformat": 4,
"nbformat_minor": 5
}