{ "cells": [ { "cell_type": "markdown", "id": "54c7d6db", "metadata": {}, "source": [ "# Deploy Serverless endpoint - Korean NLP (Sentiment Classification for Naver Movie corpus)\n", "---\n", "\n", "\n", "## Overview\n", "\n", "re:Invent 2020에 소개된 Lambda 컨테이너 기능 지원으로 기존 Lambda에서 수행하기 어려웠던 대용량 머신 모델에 대한 추론을 보다 수월하게 실행할 수 있게 되었습니다. Lambda 컨테이너 이미지를 Amazon ECR(Amazon Elastic Container Registry)에 푸시하였다면 Lambda 함수를 생성하여 직접 컨테이너 이미지를 배포하거나 SageMaker의 API 호출로 Serverless endpoint를 쉽게 배포할 수 있습니다.\n", "\n", "자세한 내용은 아래 링크를 참조해 주세요.\n", "- AWS Lambda의 새로운 기능 — 컨테이너 이미지 지원: https://aws.amazon.com/ko/blogs/korea/new-for-aws-lambda-container-image-support/\n", "- SageMaker Serverless Inference: https://sagemaker.readthedocs.io/en/stable/overview.html?highlight=lambdamodel#serverless-inference\n", "- AWS Builders Online - AWS Lambda 컨테이너 이미지 서비스 활용하기 (김태수 SA): https://www.youtube.com/watch?v=tTg9Lp7Sqok" ] }, { "cell_type": "markdown", "id": "a3fac3f4", "metadata": {}, "source": [ "
\n", "\n", "## 1. Preparation\n", "---\n", "\n", "필요한 함수들을 정의하고 Serverless 추론에 필요한 권한을 아래와 같이 설정합니다. 참고로, 직접 Lambda Container 함수를 배포 시에는 ECR 리포지토리에 대한 억세스를 자동으로 생성해 줍니다.\n", "\n", "- SageMaker과 연결된 role 대해 ECR 억세스를 허용하는 policy 생성 및 연결\n", "- SageMaker 노트북에서 lambda를 실행할 수 있는 role 생성\n", "- Lambda 함수가 ECR private 리포지토리에 연결하는 억세스를 허용하는 policy 생성 및 연결 " ] }, { "cell_type": "code", "execution_count": null, "id": "be7bddc5", "metadata": {}, "outputs": [], "source": [ "import json\n", "import time\n", "import boto3\n", "import sagemaker\n", "import base64\n", "from sagemaker import get_execution_role\n", "iam = boto3.client('iam')\n", "ecr = boto3.client('ecr')\n", "\n", "sm_role_arn = get_execution_role()\n", "sm_role_name = sm_role_arn.split('/')[-1]\n", "boto_session = boto3.session.Session()\n", "region = boto_session.region_name\n", "account = boto3.client('sts').get_caller_identity()['Account']" ] }, { "cell_type": "code", "execution_count": null, "id": "f7db9908", "metadata": {}, "outputs": [], "source": [ "def attach_sm_ecr_policy(sm_role_name):\n", " iam = boto3.client('iam')\n", " try:\n", " policy_response = iam.attach_role_policy(\n", " RoleName=sm_role_name,\n", " PolicyArn='arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryFullAccess'\n", " )\n", " return policy_response\n", " except iam.exceptions.from_code('iam:AttachRolePolicy'):\n", " print(f'[ERROR] SageMaker is not authorized to perform: iam:AttachRolePolicy on {sm_role_name}. Please add iam policy to this role') \n", "\n", "def attach_private_ecr_policy(repository_name, region, account):\n", " ecr = boto3.client('ecr') \n", " ecr_policy_json = {\n", " \"Version\": \"2008-10-17\",\n", " \"Statement\": [\n", " {\n", " \"Sid\": \"LambdaECRImageRetrievalPolicy\",\n", " \"Effect\": \"Allow\",\n", " \"Principal\": {\n", " \"Service\": \"lambda.amazonaws.com\"\n", " },\n", " \"Action\": [\n", " \"ecr:BatchGetImage\",\n", " \"ecr:DeleteRepositoryPolicy\",\n", " \"ecr:GetDownloadUrlForLayer\",\n", " \"ecr:GetRepositoryPolicy\",\n", " \"ecr:SetRepositoryPolicy\"\n", " ],\n", " \"Condition\": {\n", " \"StringLike\": {\n", " \"aws:sourceArn\": f\"arn:aws:lambda:{region}:{account}:function:*\"\n", " }\n", " }\n", " }\n", " ]\n", " }\n", " \n", " try:\n", " response = ecr.set_repository_policy(repositoryName=repository_name, policyText=json.dumps(ecr_policy_json))\n", " return response\n", " except ecr.exceptions.from_code('AccessDeniedException'):\n", " print(f'Please add ECR policy on {sm_role_name}') \n", " \n", "\n", "def create_lambda_role(role_name):\n", " iam = boto3.client('iam')\n", " lambda_policy = {\n", " \"Version\": \"2012-10-17\",\n", " \"Statement\": [\n", " {\n", " \"Effect\": \"Allow\",\n", " \"Principal\": {\n", " \"Service\": \"lambda.amazonaws.com\"\n", " },\n", " \"Action\": [ \n", " \"sts:AssumeRole\"\n", " ]\n", " }\n", " ]\n", " } \n", " \n", " response = iam.create_role(\n", " RoleName=role_name,\n", " AssumeRolePolicyDocument=json.dumps(lambda_policy)\n", " ) \n", " print(response)\n", "\n", " policy_response = iam.attach_role_policy(\n", " RoleName=role_name,\n", " PolicyArn='arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole'\n", " )\n", " return response['Role']['Arn']\n", " \n", " \n", "def delete_lambda_role(role_name):\n", " iam = boto3.client('iam')\n", " response = iam.detach_role_policy(\n", " RoleName=role_name,\n", " PolicyArn='arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole'\n", " )\n", " response = iam.delete_role(RoleName=role_name)" ] }, { "cell_type": "markdown", "id": "9d87aa7a", "metadata": {}, "source": [ "### Attach SageMaker policy" ] }, { "cell_type": "code", "execution_count": null, "id": "36a68a5e", "metadata": {}, "outputs": [], "source": [ "attach_sm_ecr_policy(sm_role_name)" ] }, { "cell_type": "markdown", "id": "d7b74626", "metadata": {}, "source": [ "### Create Lambda Role for Serverless Inference" ] }, { "cell_type": "code", "execution_count": null, "id": "27945459", "metadata": {}, "outputs": [], "source": [ "role_name = 'lambda-role-kornlp-hol'\n", "repository_name = 'kornlp-nsmc'\n", "lambda_role_arn = create_lambda_role(role_name)\n", "attach_private_ecr_policy(repository_name, region, account)\n", "time.sleep(10)" ] }, { "cell_type": "markdown", "id": "3a8fd1d4", "metadata": {}, "source": [ "
\n", "\n", "## 2. Deploy & Test\n", "---\n", "\n", "도커 이미지가 ECR에 푸시되고 적절한 Lambda Role이 생성되었다면, 단 두 줄의 코드로 `LambdaModel` 및 `LambdaPredictor` 리소스를 순차적으로 생성하여 Serverless Endpoint를 쉽게 생성할 수 있습니다. Serverless Endpoint는 내부적으로 Lambda Container 함수와 동일하므로 Endpoint에 대한 내역을 AWS Console 페이지의 AWS Lambda에서 확인할 수 있으며, 배포 전 Lambda 콘솔 창에서 직접 테스트를 수행할 수도 있습니다. " ] }, { "cell_type": "markdown", "id": "16c3db05", "metadata": {}, "source": [ "### Deploy\n" ] }, { "cell_type": "code", "execution_count": null, "id": "31e9bb36", "metadata": {}, "outputs": [], "source": [ "from sagemaker.serverless import LambdaModel\n", "image_uri = f'{account}.dkr.ecr.{region}.amazonaws.com/{repository_name}:latest'\n", "model = LambdaModel(image_uri=image_uri, role=lambda_role_arn)\n", "predictor = model.deploy(\"my-lambda-function-nlp\", timeout=50, memory_size=2048)" ] }, { "cell_type": "markdown", "id": "340d4467", "metadata": {}, "source": [ "### Test\n", "\n", "Lambda 최초 호출 시 Cold start로 지연 시간이 발생하지만, 최초 호출 이후에는 warm 상태를 유지하기 때문에 빠르게 응답합니다. 물론 수 분 동안 호출이 되지 않거나 요청이 많아지면 cold 상태로 바뀐다는 점을 유의해 주세요." ] }, { "cell_type": "code", "execution_count": null, "id": "9fd33ce3", "metadata": {}, "outputs": [], "source": [ "def get_kornlp_prediction(input_str):\n", " input_json = {\n", " \"body\": \"{\\\"text\\\": \\\"\" + input_str + \"\\\"}\"\n", " }\n", "\n", " results = predictor.predict(input_json) \n", " return json.loads(results['body']) " ] }, { "cell_type": "code", "execution_count": null, "id": "79889792", "metadata": {}, "outputs": [], "source": [ "input_str = \"개인적으로 액션을 좋아하지 않지만, 이 영화는 예외입니다. 반전을 거듭하는 멋진 스토리와 박력 있는 연출이 일품!\"\n", "get_kornlp_prediction(input_str)" ] }, { "cell_type": "markdown", "id": "bee6e32d", "metadata": {}, "source": [ "최초 호출 이후에는 빠르게 추론 결과를 얻을 수 있습니다." ] }, { "cell_type": "code", "execution_count": null, "id": "8e47c47f", "metadata": {}, "outputs": [], "source": [ "input_str = \"어휴, 이렇게 재미없을 수가 있을까요? 시간이 아깝습니다.\"\n", "get_kornlp_prediction(input_str)" ] }, { "cell_type": "markdown", "id": "86479eaf", "metadata": {}, "source": [ "여러분이 작성한 문장으로 자유롭게 테스트해 보세요. 아래 코드 셀을 반복해서 실행하셔도 됩니다." ] }, { "cell_type": "code", "execution_count": null, "id": "5eb578ce", "metadata": {}, "outputs": [], "source": [ "your_input_str = input()\n", "print(get_kornlp_prediction(your_input_str))" ] }, { "cell_type": "markdown", "id": "8082a915", "metadata": {}, "source": [ "### Check Model Latency" ] }, { "cell_type": "code", "execution_count": null, "id": "82a0b446", "metadata": {}, "outputs": [], "source": [ "import time\n", "start = time.time()\n", "for _ in range(100):\n", " result = get_kornlp_prediction(input_str)\n", "inference_time = (time.time()-start)\n", "print(f'Inference time is {inference_time:.4f} ms.')" ] }, { "cell_type": "markdown", "id": "c41594c5", "metadata": {}, "source": [ "
\n", "\n", "## Clean up\n", "---\n", "테스트를 완료했으면 `delete_model()` 및 `delete_predictor()` 메소드를 사용하여 LambdaModel 및 LambdaPredictor 리소스를 해제합니다." ] }, { "cell_type": "code", "execution_count": null, "id": "00e4ba97", "metadata": {}, "outputs": [], "source": [ "model.delete_model()\n", "predictor.delete_predictor()\n", "delete_lambda_role(role_name)" ] } ], "metadata": { "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" } }, "nbformat": 4, "nbformat_minor": 5 }