使用Amazon SageMaker多模型端点在GPU上部署成千上万个模型集合,以最小化托管成本
使用Amazon SageMaker多模型端点在GPU上部署模型集合,以降低成本
人工智能(AI)的采用在各行各业和用例中正在加速。深度学习(DL)、大型语言模型(LLMs)和生成式AI的最新科学突破使客户能够使用几乎与人类相似的高级解决方案。这些复杂模型通常需要硬件加速,因为它不仅能够加快训练速度,而且在实时应用中使用深度神经网络进行推理时也能加快推理速度。GPU的大量并行处理核心使其非常适合这些DL任务。
然而,除了模型调用之外,这些DL应用通常还需要推理流水线中的预处理或后处理。例如,用于目标检测的输入图像在提供给计算机视觉模型之前可能需要调整大小或裁剪,或者用于LLM的文本输入在使用之前需要进行标记化。NVIDIA Triton是一个开源的推理服务器,它使用户能够将这样的推理流水线定义为一个有向无环图(DAG)形式的模型集合。它专为在CPU和GPU上扩展运行模型而设计。Amazon SageMaker支持无缝部署Triton,使您能够同时使用Triton的功能并从SageMaker的能力中受益:托管、安全的环境与MLOps工具集成、托管模型的自动缩放等等。
亚马逊AWS致力于帮助客户实现最高的节省,并不断创新,不仅在定价选项和成本优化主动服务方面,还推出了多模型端点(MMEs)等节省成本的功能。MMEs是一种成本效益高的解决方案,可使用相同的资源群集和共享的服务容器部署大量模型。您可以通过部署多个模型而仅支付单个推理环境的费用,从而降低托管成本。此外,MME能够减少部署开销,因为SageMaker负责管理内存中的模型加载,并根据端点的流量模式进行扩展。
在本文中,我们将展示如何在具有SageMaker MME的GPU实例上运行多个深度学习集成模型。要按照此示例操作,您可以在公共SageMaker示例存储库中找到代码。
- 发布Swift Transformers:在Apple设备上运行的On-Device LLMs
- 用DPO对Llama 2进行微调
- “NVIDIA H100 Tensor Core GPU 现已正式发布,用于新的 Microsoft Azure 虚拟机系列”
SageMaker MME与GPU的工作原理
通过MME,一个容器可以托管多个模型。SageMaker控制托管在MME上的模型的生命周期,通过将它们加载和卸载到容器的内存中。SageMaker不会将所有模型下载到端点实例上,而是在调用时动态加载和缓存模型。
当发出对特定模型的调用请求时,SageMaker会执行以下操作:
- 首先将请求路由到端点实例。
- 如果模型尚未加载,它将从亚马逊简单存储服务(Amazon S3)下载模型文件到该实例的亚马逊弹性块存储卷(Amazon EBS)。
- 将模型加载到基于GPU加速的计算实例的容器内存中。如果模型已经加载到容器的内存中,则调用速度更快,因为不需要进一步的步骤。
当需要加载其他模型时,并且实例的内存利用率较高时,SageMaker会将未使用的模型从该实例的容器卸载,以确保有足够的内存。这些卸载的模型将保留在实例的EBS卷上,以便稍后可以将它们加载到容器的内存中,从而无需再次从S3存储桶下载。然而,如果实例的存储卷达到容量上限,SageMaker将从存储卷中删除未使用的模型。在MME接收到许多调用请求,并且存在额外的实例(或自动扩展策略)时,SageMaker会将一些请求路由到推理集群中的其他实例以适应高流量。
这不仅提供了一种节省成本的机制,还使您能够动态部署新模型和淘汰旧模型。要添加新模型,只需将其上传到MME配置为使用的S3存储桶并调用它。要删除模型,停止发送请求并从S3存储桶中删除它。向MME添加或删除模型不需要更新端点本身!
Triton集成
Triton模型集成表示一个由一个模型、预处理和后处理逻辑以及它们之间的输入和输出张量连接构成的流水线。对集成的单个推理请求将触发整个流水线作为一系列步骤的运行,使用集成调度器。调度器在每个步骤中收集输出张量,并根据规范将其作为输入张量提供给其他步骤。需要澄清的是:从外部视图来看,集成模型仍然被视为单个模型。
Triton服务器架构包括一个模型仓库:一个基于文件系统的模型仓库,Triton将在其中提供推理。Triton可以从一个或多个本地可访问的文件路径或从Amazon S3等远程位置访问模型。
模型仓库中的每个模型都必须包含一个模型配置,该配置提供有关模型的必需和可选信息。通常,此配置在一个config.pbtxt
文件中提供,该文件指定为ModelConfig protobuf。一个最小的模型配置必须指定平台或后端(如PyTorch或TensorFlow),max_batch_size
属性以及模型的输入和输出张量。
SageMaker上的Triton
SageMaker通过自定义代码实现了使用Triton服务器进行模型部署的功能。这个功能可以通过SageMaker管理的Triton推理服务器容器来实现。这些容器支持常见的机器学习(ML)框架(如TensorFlow、ONNX和PyTorch,以及自定义模型格式)和有用的环境变量,使您可以在SageMaker上优化性能。建议使用SageMaker深度学习容器(DLC)镜像,因为它们被维护并定期更新安全补丁。
解决方案演示
在本文中,我们在一个GPU实例上部署了两种不同类型的集合模型,使用了Triton和一个单独的SageMaker端点。
第一个集合由两个模型组成:一个用于图像预处理的DALI模型和一个用于实际推理的TensorFlow Inception v3模型。流水线集合接受编码图像作为输入,需要对其进行解码,调整为299×299分辨率,并进行归一化。这个预处理将由DALI模型处理。DALI是一个用于常见图像和语音预处理任务(如解码和数据增强)的开源库。Inception v3是一个图像识别模型,由对称和非对称卷积、平均和最大池化全连接层组成(因此非常适合在GPU上使用)。
第二个集合将原始的自然语言句子转换为嵌入向量,由三个模型组成。首先,将应用一个预处理模型对输入文本进行分词(使用Python实现)。然后,我们使用Hugging Face Model Hub中的预训练BERT(uncased)模型来提取标记嵌入。BERT是一个使用掩码语言建模(MLM)目标进行训练的英语语言模型。最后,我们应用一个后处理模型,将前一步的原始标记嵌入组合成句子嵌入。
在配置Triton使用这些集合之后,我们将展示如何配置和运行SageMaker MME。
最后,我们提供了每个集合调用的示例,如下图所示:
- 集合1 – 使用图像调用端点,指定DALI-Inception作为目标集合
- 集合2 – 使用文本输入调用相同的端点,请求预处理-BERT-后处理集合
设置环境
首先,我们设置所需的环境。这包括更新AWS库(如Boto3和SageMaker SDK)以及安装打包集合和使用Triton进行推理所需的依赖项。我们还使用SageMaker SDK的默认执行角色。我们使用此角色使SageMaker能够访问Amazon S3(存储我们的模型工件的地方)和容器注册表(从中使用NVIDIA Triton镜像)。请参阅以下代码:
import boto3, json, sagemaker, time
from sagemaker import get_execution_role
import nvidia.dali as dali
import nvidia.dali.types as types
# SageMaker变量
sm_client = boto3.client(service_name="sagemaker")
runtime_sm_client = boto3.client("sagemaker-runtime")
sagemaker_session = sagemaker.Session(boto_session=boto3.Session())
role = get_execution_role()
# 其他变量
instance_type = "ml.g4dn.4xlarge"
sm_model_name = "triton-tf-dali-ensemble-" + time.strftime("%Y-%m-%d-%H-%M-%S", time.gmtime())
endpoint_config_name = "triton-tf-dali-ensemble-" + time.strftime("%Y-%m-%d-%H-%M-%S", time.gmtime())
endpoint_name = "triton-tf-dali-ensemble-" + time.strftime("%Y-%m-%d-%H-%M-%S", time.gmtime())
准备集成模型
在这一步中,我们准备两个集成模型:使用DALI预处理的TensorFlow(TF)Inception和使用Python预处理和后处理的BERT。
这包括下载预训练模型、提供Triton配置文件,并在部署之前将相关文件打包存储在Amazon S3中。
准备TF和DALI集成模型
首先,我们准备存储模型和配置文件的目录:TF Inception(inception_graphdef
)、DALI预处理(dali
)和集成模型(ensemble_dali_inception
)。由于Triton支持模型版本控制,我们还将模型版本添加到目录路径中(这里表示为1,因为我们只有一个版本)。要了解有关Triton版本策略的更多信息,请参阅版本策略。接下来,我们下载Inception v3模型并将其解压缩后复制到inception_graphdef
模型目录中。请参考以下代码:
!mkdir -p model_repository/inception_graphdef/1
!mkdir -p model_repository/dali/1
!mkdir -p model_repository/ensemble_dali_inception/1
!wget -O /tmp/inception_v3_2016_08_28_frozen.pb.tar.gz \
https://storage.googleapis.com/download.tensorflow.org/models/inception_v3_2016_08_28_frozen.pb.tar.gz
!(cd /tmp && tar xzf inception_v3_2016_08_28_frozen.pb.tar.gz)
!mv /tmp/inception_v3_2016_08_28_frozen.pb model_repository/inception_graphdef/1/model.graphdef
现在,我们配置Triton来使用我们的集成模型流水线。在一个config.pbtxt
文件中,我们指定输入和输出张量的形状和类型,以及Triton调度程序需要执行的步骤(DALI预处理和用于图像分类的Inception模型):
%%writefile model_repository/ensemble_dali_inception/config.pbtxt
name: "ensemble_dali_inception"
platform: "ensemble"
max_batch_size: 256
input [
{
name: "INPUT"
data_type: TYPE_UINT8
dims: [ -1 ]
}
]
output [
{
name: "OUTPUT"
data_type: TYPE_FP32
dims: [ 1001 ]
}
]
ensemble_scheduling {
step [
{
model_name: "dali"
model_version: -1
input_map {
key: "DALI_INPUT_0"
value: "INPUT"
}
output_map {
key: "DALI_OUTPUT_0"
value: "preprocessed_image"
}
},
{
model_name: "inception_graphdef"
model_version: -1
input_map {
key: "input"
value: "preprocessed_image"
}
output_map {
key: "InceptionV3/Predictions/Softmax"
value: "OUTPUT"
}
}
]
}
接下来,我们配置每个模型。首先是DALI后端的模型配置:
%%writefile model_repository/dali/config.pbtxt
name: "dali"
backend: "dali"
max_batch_size: 256
input [
{
name: "DALI_INPUT_0"
data_type: TYPE_UINT8
dims: [ -1 ]
}
]
output [
{
name: "DALI_OUTPUT_0"
data_type: TYPE_FP32
dims: [ 299, 299, 3 ]
}
]
parameters: [
{
key: "num_threads"
value: { string_value: "12" }
}
]
接下来是我们之前下载的TensorFlow Inception v3的模型配置:
%%writefile model_repository/inception_graphdef/config.pbtxt
name: "inception_graphdef"
platform: "tensorflow_graphdef"
max_batch_size: 256
input [
{
name: "input"
data_type: TYPE_FP32
format: FORMAT_NHWC
dims: [ 299, 299, 3 ]
}
]
output [
{
name: "InceptionV3/Predictions/Softmax"
data_type: TYPE_FP32
dims: [ 1001 ]
label_filename: "inception_labels.txt"
}
]
instance_group [
{
kind: KIND_GPU
}
]
因为这是一个分类模型,我们还需要将Inception模型的标签复制到模型仓库中的inception_graphdef
目录中。这些标签包括来自ImageNet数据集的1,000个类别标签。
!aws s3 cp s3://sagemaker-sample-files/datasets/labels/inception_labels.txt model_repository/inception_graphdef/inception_labels.txt
接下来,我们配置和序列化DALI管道,用于处理我们的预处理工作。预处理工作包括读取图像(使用CPU),解码(使用GPU加速),以及调整尺寸和归一化图像。
@dali.pipeline_def(batch_size=3, num_threads=1, device_id=0)
def pipe():
"""创建一个管道,它读取图像和掩膜,解码图像并返回它们。"""
images = dali.fn.external_source(device="cpu", name="DALI_INPUT_0")
images = dali.fn.decoders.image(images, device="mixed", output_type=types.RGB)
images = dali.fn.resize(images, resize_x=299, resize_y=299) #将图像调整为默认的299x299大小
images = dali.fn.crop_mirror_normalize(
images,
dtype=types.FLOAT,
output_layout="HWC",
crop=(299, 299), #将图像裁剪为默认的299x299大小
mean=[0.485 * 255, 0.456 * 255, 0.406 * 255], #裁剪图像的中心区域
std=[0.229 * 255, 0.224 * 255, 0.225 * 255], #裁剪图像的中心区域
)
return images
pipe().serialize(filename="model_repository/dali/1/model.dali")
最后,我们将所有的组件打包在一起,并将它们作为一个单独的对象上传到Amazon S3:
!tar -cvzf model_tf_dali.tar.gz -C model_repository .
model_uri = sagemaker_session.upload_data(
path="model_tf_dali.tar.gz", key_prefix="triton-mme-gpu-ensemble"
)
print("S3模型URI:{}".format(model_uri))
准备TensorRT和Python集合
在这个示例中,我们使用了transformers库中的一个预训练模型。
您可以在ensemble_hf文件夹中找到所有的模型(预处理和后处理,以及config.pbtxt
文件)。我们的文件系统结构将包括四个目录(三个用于单独的模型步骤,一个用于集合)以及它们各自的版本:
ensemble_hf
├── bert-trt
| |── model.pt
| |──config.pbtxt
├── ensemble
│ └── 1
| └── config.pbtxt
├── postprocess
│ └── 1
| └── model.py
| └── config.pbtxt
├── preprocess
│ └── 1
| └── model.py
| └── config.pbtxt
在工作空间文件夹中,我们提供了两个脚本:第一个用于将模型转换为ONNX格式(onnx_exporter.py),第二个是TensorRT编译脚本(generate_model_trt.sh)。
Triton原生支持TensorRT运行时,使您能够轻松部署TensorRT引擎,从而针对所选的GPU架构进行优化。
为了确保我们使用与Triton容器中的版本兼容的TensorRT版本和依赖项,我们使用相应版本的NVIDIA PyTorch容器映像编译模型:
model_id = "sentence-transformers/all-MiniLM-L6-v2"
! docker run --gpus=all --rm -it -v `pwd`/workspace:/workspace nvcr.io/nvidia/pytorch:22.10-py3 /bin/bash generate_model_trt.sh $model_id
然后,我们将模型工件复制到之前创建的目录中,并在路径中添加一个版本:
! mkdir -p ensemble_hf/bert-trt/1 && mv workspace/model.plan ensemble_hf/bert-trt/1/model.plan && rm -rf workspace/model.onnx workspace/core*
我们使用Conda pack生成一个Conda环境,Triton Python后端将在预处理和后处理中使用该环境:
!bash conda_dependencies.sh
!cp processing_env.tar.gz ensemble_hf/postprocess/ && cp processing_env.tar.gz ensemble_hf/preprocess/
!rm processing_env.tar.gz
最后,我们将模型文物上传到Amazon S3:
!tar -C ensemble_hf/ -czf model_trt_python.tar.gz .
model_uri = sagemaker_session.upload_data(
path="model_trt_python.tar.gz", key_prefix="triton-mme-gpu-ensemble"
)
print("S3模型URI:{}".format(model_uri))
在SageMaker MME GPU实例上运行集成模型
现在我们的集成模型文物已经存储在Amazon S3上,我们可以配置并启动SageMaker MME。
我们首先获取Triton DLC镜像的容器URI,该镜像与我们所在区域的容器注册表中的镜像匹配(用于TensorRT模型编译):
account_id_map = {
"us-east-1": "785573368785",
"us-east-2": "007439368137",
"us-west-1": "710691900526",
"us-west-2": "301217895009",
"eu-west-1": "802834080501",
"eu-west-2": "205493899709",
"eu-west-3": "254080097072",
"eu-north-1": "601324751636",
"eu-south-1": "966458181534",
"eu-central-1": "746233611703",
"ap-east-1": "110948597952",
"ap-south-1": "763008648453",
"ap-northeast-1": "941853720454",
"ap-northeast-2": "151534178276",
"ap-southeast-1": "324986816169",
"ap-southeast-2": "355873309152",
"cn-northwest-1": "474822919863",
"cn-north-1": "472730292857",
"sa-east-1": "756306329178",
"ca-central-1": "464438896020",
"me-south-1": "836785723513",
"af-south-1": "774647643957",
}
region = boto3.Session().region_name
if region not in account_id_map.keys():
raise ("UNSUPPORTED REGION")
base = "amazonaws.com.cn" if region.startswith("cn-") else "amazonaws.com"
triton_image_uri = "{account_id}.dkr.ecr.{region}.{base}/sagemaker-tritonserver:23.03-py3".format(
account_id=account_id_map[region], region=region, base=base
)
接下来,我们在SageMaker中创建模型。在create_model
请求中,我们描述要使用的容器和模型文物的位置,并指定使用Mode
参数来说明这是一个多模型。
container = {
"Image": triton_image_uri,
"ModelDataUrl": models_s3_location,
"Mode": "MultiModel",
}
create_model_response = sm_client.create_model(
ModelName=sm_model_name, ExecutionRoleArn=role, PrimaryContainer=container
)
为了托管我们的集成模型,我们使用create_endpoint_config
API调用创建一个端点配置,然后使用create_endpoint
API创建一个端点。SageMaker会在托管环境中部署您为模型定义的所有容器。
create_endpoint_config_response = sm_client.create_endpoint_config(
EndpointConfigName=endpoint_config_name,
ProductionVariants=[
{
"InstanceType": instance_type,
"InitialVariantWeight": 1,
"InitialInstanceCount": 1,
"ModelName": sm_model_name,
"VariantName": "AllTraffic",
}
],
)
create_endpoint_response = sm_client.create_endpoint(
EndpointName=endpoint_name, EndpointConfigName=endpoint_config_name
)
虽然在这个例子中我们只设置了一个实例来托管我们的模型,但SageMaker MME完全支持设置自动缩放策略。有关此功能的更多信息,请参阅使用Amazon SageMaker多模型端点在GPU上运行多个深度学习模型。
创建请求载荷并调用每个模型的MME
在我们的实时MME部署之后,现在是使用我们使用的每个模型集合调用我们的端点的时候了。
首先,我们为DALI-Inception集合创建一个载荷。我们使用SageMaker公共数据集中的shiba_inu_dog.jpg
图像。我们将该图像加载为一个编码后的字节数组,以便在DALI后端中使用(要了解更多信息,请参阅图像解码器示例)。
sample_img_fname = "shiba_inu_dog.jpg"
import numpy as np
s3_client = boto3.client("s3")
s3_client.download_file(
"sagemaker-sample-files", "datasets/image/pets/shiba_inu_dog.jpg", sample_img_fname
)
def load_image(img_path):
"""
将图像加载为编码后的字节数组。
这是您在DALI后端中使用的典型方法
"""
with open(img_path, "rb") as f:
img = f.read()
return np.array(list(img)).astype(np.uint8)
rv = load_image(sample_img_fname)
print(f"图像形状 {rv.shape}")
rv2 = np.expand_dims(rv, 0)
print(f"扩展后的图像数组形状 {rv2.shape}")
payload = {
"inputs": [
{
"name": "INPUT",
"shape": rv2.shape,
"datatype": "UINT8",
"data": rv2.tolist(),
}
]
}
当我们的编码图像和载荷准备就绪时,我们调用端点。
注意,我们指定目标集合为model_tf_dali.tar.gz
工件。TargetModel参数是MME和单模型端点之间的区别,它使我们能够将请求发送到正确的模型。
response = runtime_sm_client.invoke_endpoint(
EndpointName=endpoint_name, ContentType="application/octet-stream", Body=json.dumps(payload), TargetModel="model_tf_dali.tar.gz"
)
响应包括有关调用的元数据(如模型名称和版本)和输出对象的数据部分中的实际推断响应。在此示例中,我们得到一个包含1,001个值的数组,其中每个值都是图像所属类别的概率(1,000个类别和1个其他类别)。接下来,我们再次调用MME,但这次目标是第二个集合。这里的数据只是两个简单的文本句子:
text_inputs = ["句子1", "句子2"]
为了简化与Triton的通信,Triton项目提供了几个客户端库。我们使用该库来准备请求中的载荷:
import tritonclient.http as http_client
text_inputs = ["句子1", "句子2"]
inputs = []
inputs.append(http_client.InferInput("INPUT0", [len(text_inputs), 1], "BYTES"))
batch_request = [[text_inputs[i]] for i in range(len(text_inputs))]
input0_real = np.array(batch_request, dtype=np.object_)
inputs[0].set_data_from_numpy(input0_real, binary_data=True)
outputs = []
outputs.append(http_client.InferRequestedOutput("finaloutput"))
request_body, header_length = http_client.InferenceServerClient.generate_request_body(
inputs, outputs=outputs
)
现在我们准备调用端点,这次目标模型是model_trt_python.tar.gz
集合:
response = runtime_sm_client.invoke_endpoint(
EndpointName=endpoint_name,
ContentType="application/vnd.sagemaker-triton.binary+json;json-header-size={}".format(
header_length
),
Body=request_body,
TargetModel="model_trt_python.tar.gz"
)
响应是可以用于各种自然语言处理(NLP)应用的句子嵌入。
清理
最后,我们清理并删除端点、端点配置和模型:
sm_client.delete_endpoint(EndpointName=endpoint_name)
sm_client.delete_endpoint_config(EndpointConfigName=endpoint_config_name)
sm_client.delete_model(ModelName=sm_model_name)
结论
在本文中,我们展示了如何在GPU加速实例上配置、部署和调用SageMaker MME与Triton集合。我们在一个实时推断环境中托管了两个集合,这样可以将成本降低了50%(对于g4dn.4xlarge实例而言,这相当于每年节省超过13000美元)。虽然本示例只使用了两个流水线,但SageMaker MME可以支持数千个模型集合,使其成为一种非凡的成本节约机制。此外,您可以使用SageMaker MME的动态能力来加载(和卸载)模型,以最小化在生产环境中管理模型部署的操作开销。