65.9K
CodeProject 正在变化。 阅读更多。
Home

在 Azure 上大规模部署模型(第二部分):部署和扩展 PyTorch 模型

starIconstarIconstarIconstarIconstarIcon

5.00/5 (3投票s)

2022 年 3 月 29 日

CPOL

13分钟阅读

viewsIcon

5937

如何部署和扩展 PyTorch 模型。

机器学习 (ML) 通常需要大量的处理能力。尽管您的 Python ML 项目可能超出了您当前计算机的能力,但您可以使用 Azure 来运行几乎任何规模的 ML 工作负载。

在本系列三篇文章的第一篇文章中,我们发布了一个 XGBoost 模型,该模型经过训练,能够识别来自知名 MNIST 数据集的数字手写体。我们使用了带有 Flask 的 Azure App Service,然后使用了机器学习在线终结点。在本教程中,我们将使用上一系列中的另一个模型。该模型要求稍高一些,但允许我们探索稍微更高级的场景。

我们的目标是为机器学习模型的实时推理创建自定义的 REST API 服务。我们将首先使用 Python、FastAPI 和 Azure App Service 发布一个 PyTorch 模型。然后,我们将使用相同的模型创建一个在线终结点,这是一个相对较新的 Azure Machine Learning 功能,仍处于预览阶段。

我们使用Azure CLI来编写易于理解且可重复的脚本,我们可以将这些脚本与我们的其他代码一起存储和版本控制。在专用的GitHub 存储库中查找示例代码、脚本、模型和一些测试图像。

入门

要遵循本文的示例,您需要Visual Studio Code、最近的 Python 版本 (3.7+) 以及用于 Python 包管理的 Conda。如果您没有其他偏好,请从Miniconda和 Python 3.9 开始。

安装 Conda 后,创建并激活一个新环境

$ conda create -n azureml python=3.9
$ conda activate azureml

除了 Python 和 Conda,我们还将使用 Azure 命令行工具 2.15.0 或更高版本,以及机器学习扩展

$ az extension add -n ml -y

最后但同样重要的是,如果您还没有免费 Azure 帐户,请注册一个,并享受数百美元的积分和各种服务的访问权限。

在所有这些资源都准备就绪后,登录到您的订阅。

$ az login

然后,设置以下环境变量以供脚本使用

export AZURE_SUBSCRIPTION="<your-subscription-id>"
export RESOURCE_GROUP="azureml-rg"
export AML_WORKSPACE="demo-ws"
export LOCATION="westeurope"

如果您没有遵循上一系列中的示例,您还需要创建一个 Azure Machine Learning 工作区

$ az ml workspace create --name $AML_WORKSPACE --subscription $AZURE_SUBSCRIPTION
--resource-group $RESOURCE_GROUP --location $LOCATION

我们已准备好开始准备模型进行部署。

注册模型

由于我们的模型很小,我们可以将其与应用程序代码一起打包并部署,但这并非最佳实践。实际模型可能非常大,因此我们应该能够独立于代码来管理模型的版本。我们将使用 Azure Machine Learning 工作区的内置模型注册表。

在上一系列文章的第二篇文章中,我们已经将模型保存到注册表中。如果您还没有,请运行此命令

$ az ml model create --name "mnist-pt-model" --local-path "./mnist.pt_model"
  --subscription $AZURE_SUBSCRIPTION --resource-group $RESOURCE_GROUP
  --workspace-name $AML_WORKSPACE

创建模型推理代码

与上一篇文章一样,我们 REST API 服务的核心部分是加载模型并在图像数据上运行推理的代码。我们将代码存储在 _inference_model.py_ 文件中。

文件以 import 开头

import numpy as np
from PIL import Image

import torch
from torch.nn import functional as F
from torch import nn

接下来,我们的模型需要一个 class。它必须与我们用于训练的代码相同。

class NetMNIST(nn.Module):
    def __init__(self):
        super().__init__()
        self.conv1 = nn.Conv2d(1, 10, kernel_size=5)
        self.conv2 = nn.Conv2d(10, 20, kernel_size=5)
        self.fc1 = nn.Linear(320, 50)
        self.fc2 = nn.Linear(50, 10)

    def forward(self, x):
        x = F.max_pool2d(F.relu(self.conv1(x)), (2,2))
        x = F.max_pool2d(F.dropout(F.relu(self.conv2(x)), p=0.2), (2,2))
        x = x.view(-1, 320)
        x = F.relu(self.fc1(x))
        x = F.dropout(x, p=0.2, training=self.training)
        x = self.fc2(x)
        return F.log_softmax(x, dim=1)

现在,我们可以添加我们的 InferenceModel

class InferenceModel():
    def __init__(self, model_path):
        is_cuda_available = torch.cuda.is_available()
        self.device = torch.device("cuda" if is_cuda_available else "cpu")
        self.model = NetMNIST().to(self.device)
        self.model.load_state_dict(torch.load(model_path))

    def _preprocess_image(self, image_bytes):
        image = Image.open(image_bytes)

        image = image.resize((28,28)).convert('L')

        image_np = (255 - np.array(image.getdata())) / 255.0

        return torch.tensor(image_np).float().to(self.device)

    def predict(self, image_bytes):
        image_data = self._preprocess_image(image_bytes)

        with torch.no_grad():
            prediction = self.model(image_data.reshape(-1,1,28,28)).cpu().numpy()

        return np.argmax(prediction, axis=1)

与我们之前的 XGBoost 模型一样,此文件包含一个类 InferenceModel。该类有三个方法:__init___preprocess_imagepredict

__init__ 方法从文件加载模型并将其存储以备后用。它还会检测是否有可用的 CUDA GPU。

_preprocess_image 方法调整图像大小并将其转换为模型可接受的格式。对于我们的 PyTorch 模型,它是一个 28x28 的单通道张量,其中包含 0.0 到 1.0 范围内的浮点数。请注意像素强度的反转。我们这样做是因为我们计划在标准的黑白图像上使用我们的模型,而 MNIST 训练数据集具有反转的黑白值。

最终的 predict 方法使用检测到的设备和加载的模型对提供的图像数据运行推理。

与上一系列中的训练代码相反,我们不使用数据加载器。我们直接将图像数据馈送到模型。数据加载器允许处理数据批次。我们的 API 一次处理一个图像,因此数据加载器是多余的。

构建 FastAPI 服务代码

现在我们有了处理预测的代码,可以在 REST API 服务中使用它。让我们创建一个自定义 Flask 服务来执行此任务。

新的 _main.py_ 文件将包含所有服务代码,文件结构将与上一篇文章中的 _app.py_ 文件类似

from fastapi import FastAPI, File

from io import BytesIO
from inference_model import InferenceModel

from azureml.core.authentication import MsiAuthentication
from azureml.core import Workspace
from azureml.core.model import Model

def get_inference_model():
    global model
    if model == None:
        auth = MsiAuthentication()
        ws = Workspace(subscription_id="<your-subscription-id>",
                        resource_group="azureml-rg",
                        workspace_name="demo-ws",
                        auth=auth)
        aml_model = Model(ws, 'mnist-pt-model', version=1)

        model_path = aml_model.download(target_dir='.', exist_ok=True)

        model = InferenceModel(model_path)

    return model

app = FastAPI(title="PyTorch MNIST Service API", version="1.0")

@app.post("/score")
async def score(image: bytes = File(...)):
    if not image:
        return {"message": "No image_file"}

    model = get_inference_model()
    preds = model.predict(BytesIO(image))

    return {"preds": str(preds)}

model = None

与之前一样,我们使用 MsiAuthentication 类进行身份验证,以访问 Azure Machine Learning 工作区中的资源。MsiAuthentication 类依赖于 Azure Active Directory 中的托管标识。我们将托管标识分配给 Azure 资源,例如虚拟机或 App Service。使用托管标识使我们无需维护任何凭据或机密。

与上一篇文章相比,get_inference_model 方法的唯一更改是模型名称。

最后一个方法 score 负责运行预测。与 FastAPI 中的每个 API 方法一样,它需要是异步的,因此使用了 async 关键字。请注意,score 方法声明中的 File(...) 语句不是占位符。

使用 Python 和 FastAPI 发布 App Service

我们几乎拥有了在 Azure 上使用 App Service 结合 Python 和 FastAPI 运行 REST API 服务的所有代码。

我们需要的最后一个文件是 _requirements.txt_,内容如下

fastapi
gunicorn
uvicorn
python-multipart==0.0.5
torch==1.9.0
pillow==8.3.2
azureml-defaults==1.35.0

此代码未为 fastapigunicornuvicorn 依赖项定义显式版本。这是故意的,我们将在本节结束前解释原因。

现在,我们可以使用 Azure CLI 通过以下命令(从包含文件的文件夹)发布我们的 App Service 应用程序

$ APP_SERVICE_PLAN="<your-app-service-plan-name>"
$ APP_NAME="<your-app-name>"
$ az webapp up --name $APP_NAME --plan $APP_SERVICE_PLAN --sku B2 --os-type Linux
   --runtime "python|3.7" --subscription $AZURE_SUBSCRIPTION --resource-group $RESOURCE_GROUP

请记住,APP_NAME 值必须全局唯一,因为它将成为服务 URL 的一部分。

应用程序的发布可能需要很长时间。在某些并不罕见的情况下,它可能根本不会结束。这可能与已安装依赖项的大小有关。仅 PyTorch 就超过 1 GB,这迫使将服务计划 SKU 从免费的 F1 层(具有 1 GB RAM)更改为至少 B2(具有 3.5 GB RAM)。即使这样,也发生过随机的部署超时,导致部署过程似乎永无止境或失败。

强制指定依赖项版本会进一步增加失败尝试的概率。这可能与默认 App Service 映像中的其他包存在冲突有关。已从 _requirements.txt_ 文件中删除了一些库的显式版本,以减少这些问题。

所有这些都表明,使用 App Service 部署来服务复杂模型可能并非始终是生产应用程序的最佳选择。幸运的是,Azure 为我们提供了许多替代方案。

一种是容器 Web 应用。我们可以使用自己的 Docker 容器完全控制所有依赖项,从而限制潜在问题。我们还可以使用容器实例AKS

不过,有一个新选项:托管在线终结点。我们将在本文稍后使用它。

现在,让我们回到 App Service 部署。当命令完成时,它应该返回类似于以下的 JSON

{
  "URL": "http://<your-app-name>.azurewebsites.net",
  "appserviceplan": "<your-app-service-plan-name>",
  "location": "westeurope",
  "name": "<your-app-name>",
  "os": "Linux",
  "resourcegroup": "azureml-rg",
  "runtime_version": "python|3.7",
  "runtime_version_detected": "-",
  "sku": "B2",
  "src_path": "<local-path-to-your-files>"
}

更新 FastAPI 的 App Service 配置

App Service 会自动检测并使用默认的 Gunicorn 工作进程来处理 Flask 应用程序。但是,这次我们使用的是 FastAPI。

我们需要使用以下 Azure CLI 命令显式设置应用程序的启动命令

$ az webapp config set --name $APP_NAME --startup-file "gunicorn --workers=2
  --worker-class uvicorn.workers.UvicornWorker main:app" --resource-group $RESOURCE_GROUP

此命令确保使用 Uvicorn 工作进程的 Gunicorn,这是运行 FastAPI 服务所必需的。

或者,您可以使用 Azure 门户,并在“常规设置”的“启动命令”字段中粘贴启动命令

授予 App Service 应用程序权限

我们授予权限的方式与 XGBoost 模型相同。我们的服务必须从 Azure Machine Learning 工作区下载已训练的模型,这需要授权。我们使用托管标识来避免凭据管理。

为我们的应用程序分配新的托管标识非常简单。输入以下命令

$ az webapp identity assign --name $APP_NAME --resource-group $RESOURCE_GROUP

完成后,我们应该会收到 JSON 输出

{
  "principalId": "<new-principal-id>",
  "tenantId": "<tenant-id>",
  "type": "SystemAssigned",
  "userAssignedIdentities": null
}

使用返回的 <new-principal-id> 值,我们可以添加所需的权限

$ az role assignment create --role reader --assignee <new-principal-id>
  --scope /subscriptions/$AZURE_SUBSCRIPTION/resourceGroups/$RESOURCE_GROUP/providers/
  Microsoft.MachineLearningServices/workspaces/$AML_WORKSPACE

读者角色应该足以满足我们的目的。如果我们的应用程序需要创建其他资源,我们应该改用贡献者角色。

测试服务

我们将使用我们示例代码中的图像(_test-images/d0.png_ 到 _d9.png_)来测试我们的服务

这些图像没有严格的要求。我们服务中的代码将对图像进行重新缩放并转换为预期的大小和格式。

我们需要发送 POST 请求来调用我们的服务。我们可以使用 Postmancurl。使用 curl,我们可以直接从命令行执行请求。

$ curl -X POST -F 'image=@./test-images/d6.png' https://$APP_NAME.azurewebsites.net/score

如果一切顺利,我们应该会收到以下响应

{"preds": [6]}

答案似乎是正确的。与上一篇文章中的简单 XGBoost 模型相比,我们的 PyTorch 模型应该工作得更好。

检查 App Service 日志

如果您遇到任何问题,您可能需要检查您的服务日志。您可以通过 Azure 门户的“监视 > 日志流”选项卡进行操作。

或者,您可以使用 Azure CLI 访问日志。

$ az webapp log tail --name $APP_NAME --subscription $AZURE_SUBSCRIPTION
  --resource-group $RESOURCE_GROUP

请注意,此处仅包含执行日志。如果部署出现问题,请尝试 Azure 门户的“部署中心”日志。

使用 Azure Machine Learning 和托管终结点

使用托管在 Azure App Service 上的自定义 Flask 应用程序发布模型非常简单。然而,我们的环境越复杂,我们在设置过程中遇到的问题就越多。我们可以使用容器 Web 应用托管在线终结点(预览版)来避免这些设置问题。在接下来的部分中,我们将使用终结点。

创建在线终结点代码

与上一篇文章一样,我们将使用为 _inference_model.py_ 文件中的 Flask 应用程序创建的同一个 InferenceModel 类,并将其复制到新文件夹 _endpoint-code_ 中。

由于 InferenceModel 类完全抽象了模型,因此 _endpoint-code/aml-score.py_ 文件中的终结点代码几乎与 XGBoost 模型完全相同

import os
import json
from inference_model import InferenceModel

from azureml.contrib.services.aml_request import rawhttp
from azureml.contrib.services.aml_response import AMLResponse

def init():
    global model
    model_path = os.path.join(
        os.getenv("AZUREML_MODEL_DIR"), "mnist.pt_model"
    )

    model = InferenceModel(model_path)

@rawhttp
def run(request):
    if request.method != 'POST':
        return AMLResponse(f"Unsupported verb: {request.method}", 400)

    image_data = request.files['image']
    preds = model.predict(image_data)

    return AMLResponse(json.dumps({"preds": preds.tolist()}), 200)

唯一的区别是模型名称 mnist.pt_model

其余部分保持不变。代码会在终结点启动时调用第一次 init 方法。这是加载模型的绝佳位置。我们使用 AZUREML_MODEL_DIR 环境变量来执行此操作,该变量指示模型文件的位置。

下面的 run 方法很简单。首先,我们确保只接受 POST 请求。接下来,我们从请求中检索图像,然后运行并返回预测。

注意 @rawhttp 装饰器。它访问原始请求数据,例如二进制图像内容。没有它,传递给 run 方法的请求参数将仅限于解析后的 JSON。

配置在线终结点

除了代码,我们还需要三个配置文件。

第一个文件 _endpoint-code/aml-env.yml_ 存储 Conda 环境定义。

channels:
  - pytorch
  - conda-forge
  - defaults
dependencies:
  - python=3.7.10
  - pytorch=1.9.0
  - cpuonly # Replace with cudatoolkit to use GPU
  - pillow=8.3.2
  - gunicorn=20.1.0
  - numpy=1.19.5
  - pip:
    - azureml-defaults==1.35.0
    - inference-schema[numpy-support]==1.3.0

接下来的两个文件与上一篇文章中的相同。它们包含终结点及其部署的配置。

终结点配置文件 _aml-endpoint.yml_ 包含

$schema: https://azuremlschemas.azureedge.net/latest/managedOnlineEndpoint.schema.json
name: mnistptoep
auth_mode: key

最后一个文件 _aml-endpoint-deployment.yml_ 包含

$schema: https://azuremlschemas.azureedge.net/latest/managedOnlineDeployment.schema.json
name: blue
endpoint_name: mnistptoep
model: azureml:mnist-pt-model:1
code_configuration:
  code:
    local_path: ./endpoint-code
  scoring_script: aml-score.py
environment:
  conda_file: ./endpoint-code/aml-env.yml
  image: mcr.microsoft.com/azureml/minimal-ubuntu18.04-py37-cpu-inference:latest
instance_type: Standard_F2s_v2
instance_count: 1

您可以使用自定义映像或 Microsoft 精选映像目录中的映像。可用映像的列表相当广泛,尽管在我们的案例中,很少有理由使用它们。无论您选择哪个映像,代码仍会根据您的规范创建一个新的 Conda 环境。

我们需要将通用组件包含在我们的环境中,即使它们已经存在于映像中。例如,我们可能会使用 azureml-inference-server-http(来自 azureml-defaults)和 gunicorn

不过,如果您需要 Conda 环境中未包含的某些系统级依赖项(如 Open MPI 或 CUDA 驱动程序),您很可能会在那里找到所需的内容。在我们的案例中,所选的最小映像已足够。

请注意,虽然 Microsoft 建议始终使用最新的映像标签,但在某些情况下,您可能需要考虑使用固定值以获得最大的可重现性。您可以使用以下 URL 模板查找给定映像的所有可用标签

https://mcr.microsoft.com/v2/<namespace>/<repository>/tags/list

例如,此 URL

https://mcr.microsoft.com/v2/azureml/minimal-ubuntu18.04-py37-cpu-inference/tags/list

returns

屏幕截图显示每月有多个版本,因此映像会频繁更新。

创建托管在线终结点

有了所有这些文件,我们就可以开始部署了。我们从终结点开始

$ ENDPOINT_NAME="<your-endpoint-name>"

$ az ml online-endpoint create -n $ENDPOINT_NAME -f aml-endpoint.yml
  --subscription $AZURE_SUBSCRIPTION --resource-group $RESOURCE_GROUP
  --workspace-name $AML_WORKSPACE

与之前的 App Service 应用程序名称一样,终结点名称在每个 Azure 区域内必须是唯一的,因为它将成为 URL 的一部分,格式如下

https://<your-endpoint-name>.<region-name>.inference.ml.azure.com/score

现在我们已经创建了终结点,我们终于可以部署我们的推理代码了

$ az ml online-deployment create -n blue
  --endpoint $ENDPOINT_NAME -f aml-endpoint-deployment.yml
  --all-traffic --subscription $AZURE_SUBSCRIPTION --resource-group $RESOURCE_GROUP
  --workspace-name $AML_WORKSPACE

经过漫长等待后,该命令应返回确认部署已完成且终结点已准备好使用。需要时间来配置虚拟机、下载基础映像并设置您的环境。

要查看终结点日志,请输入

$ az ml online-deployment get-logs -n blue --endpoint $ENDPOINT_NAME
  --subscription $AZURE_SUBSCRIPTION --resource-group $RESOURCE_GROUP
  --workspace-name $AML_WORKSPACE

输出应类似于以下屏幕截图

测试在线终结点

我们需要三个要素来调用我们的终结点:终结点 URL、终结点密钥和示例图像。我们已经使用了示例图像,因此只需要 URL 和密钥。

获取它们的一种方法是通过 Azure Machine Learning Studio,从终结点的“Consume”选项卡。

我们也可以使用 Azure CLI 获取这些值

$ SCORING_URI=$(az ml online-endpoint show -n $ENDPOINT_NAME -o tsv
  --query scoring_uri --resource-group $RESOURCE_GROUP --workspace $AML_WORKSPACE)

$ ENDPOINT_KEY=$(az ml online-endpoint get-credentials --name $ENDPOINT_NAME
  --subscription $AZURE_SUBSCRIPTION --resource-group $RESOURCE_GROUP
  --workspace-name $AML_WORKSPACE -o tsv --query primaryKey)

现在 SCORING_URIENDPOINT_KEY 变量已填充,我们可以调用我们的服务了。

$ curl -X POST -F 'image=@./test-images/d5.png' -H
  "Authorization: Bearer $ENDPOINT_KEY" $SCORING_URI

如果一切顺利,我们应该会得到与 Flask 应用程序相同的答案

{"preds": [5]}

本地部署

托管终结点的一个便捷功能是可以使用本地 Docker 实例将其部署在您的计算机上。这有助于调试部署问题,因为它使您可以完全访问 Docker 日志和创建的容器。

只需将 --local 参数添加到 az ml online-endpoint 命令即可使用此功能。例如,以下命令将创建您终结点的本地版本

$ az ml online-endpoint create --local -n $ENDPOINT_NAME -f aml-endpoint.yml
  --subscription $AZURE_SUBSCRIPTION --resource-group $RESOURCE_GROUP
  --workspace-name $AML_WORKSPACE

$ az ml online-deployment create --local -n blue
  --endpoint $ENDPOINT_NAME -f aml-endpoint-deployment.yml
  --all-traffic --subscription $AZURE_SUBSCRIPTION
  --resource-group $RESOURCE_GROUP --workspace-name $AML_WORKSPACE

大多数(如果不是全部)az ml online-endpoint / online-deployment 子命令都支持 --local 标志。例如,这是调用本地终结点 URI 并检查其日志的方法

$ LOCAL_SCORING_URI=$(az ml online-endpoint show --local -n $ENDPOINT_NAME -o tsv
  --query scoring_uri --resource-group $RESOURCE_GROUP --workspace $AML_WORKSPACE)

$ curl -X POST -F 'image=@./test-images/d5.png' $LOCAL_SCORING_URI

$ az ml online-deployment get-logs --local -n blue --endpoint $ENDPOINT_NAME
  --subscription $AZURE_SUBSCRIPTION --resource-group $RESOURCE_GROUP
  --workspace-name $AML_WORKSPACE

这里不需要 API_KEY,因为本地部署不需要身份验证。

还有更多。本地部署完成后,您可以完全访问相应的 Docker 映像和容器。例如

$ docker images

$ docker ps

默认情况下,映像名称与 $ENDPOINT_NAME 值匹配,标签等于部署名称(在我们的示例中为 blue)。

您可以像往常一样创建和运行自己的容器以获取映像,或将映像附加到现有容器。例如,您可以将映像附加到正在运行的容器并列出其中的可用 Conda 环境

$ docker exec -it <container-id> bash

amlenv 是基础映像中包含的默认 Azure Machine Learning 环境。我们使用 YAML 配置创建 inf-conda-env 环境。

当终结点启动时,它不会显式选择活动环境。相反,它将每个环境的二进制路径添加到 PATH 环境变量中。它最初添加 inf-conda-env,因此它首先考虑来自自定义环境的二进制文件。

当终结点启动时,它会执行 azureml-inference-server-http 命令。如果它不存在于自定义 inf-conda-env 环境中,则运行 amlenv 版本。通过这种方式,它隐式确定正在执行的 Conda 环境。

删除 Azure 资源

您可以删除所有不再需要的资源以减少 Azure 费用。请特别记住 App Service 计划和托管终结点。

后续步骤

在本文中,我们发布了一个用于识别数字手写体的 PyTorch 模型。我们使用了带有 App Service 的 FastAPI,然后是 Azure Machine Learning 在线终结点。

在本系列的第三篇也是最后一篇文章中,我们将使用在线终结点来发布 TensorFlow 模型。然后,我们将创建一个 Azure Function 作为该终结点的公共代理。此外,我们将探索托管终结点的配置选项,例如自动缩放和蓝绿部署概念。

继续阅读下一篇文章,发布 TensorFlow 模型

要了解如何使用在线终结点(预览版)来部署机器学习模型,请参阅使用在线终结点部署和评分机器学习模型

© . All rights reserved.