多模型端点将ModelDataUrl指向S3前缀而非单个模型文件,模型在调用时才从S3发现并加载;添加模型只需上传S3,删除只需S3删除,但首次调用会有下载等待时间。
多模型端点(Multi-Model Endpoint)是普通 SageMaker 端点,只改了两个字段,却带来一个很大的行为变化。前者只需五分钟,后者——即一部分请求在模型从 S3 下载并加载到容器后才能获得响应——才是整个设计的核心,也是这篇文章主要讨论的内容。
只有两处改动,都在传递给 CreateModel 的容器定义中。将 Mode 设为 MultiModel,并将 ModelDataUrl 指向一个 S3 前缀而非单个 model.tar.gz。下游所有内容——CreateEndpointConfig、CreateEndpoint、生产变体、实例类型——与单模型端点完全一致。Amazon 在创建多模型端点页面 上记录了这两处变更。
第二个变更的后果是人们容易忽略的。因为端点拿到的是一个前缀而非清单,它并不知道存在哪些模型,而是在调用时才去发现它们。所以添加模型只需上传到 S3 而无需更新端点,删除模型只需从 S3 删除,而在调用时模型名称拼写错误则是一个运行时错误而非部署时错误。
SageMaker AI 在多模型端点页面描述了加载生命周期:请求一个非常驻的模型时,它将制品从 S3 下载到实例的存储卷,然后加载到容器的内存中。当内存不足时会卸载最少使用的模型——它们仍留在存储卷上,所以后续重新加载时可以跳过下载——而当存储卷满时,也会从磁盘删除未使用的制品。所以是三层而非两层:容器内存中、本地磁盘上、S3 中。每次未命中代价都比上一次更高。
首先将制品布局在一个前缀下。每一个都是你所使用的任意Serving容器的正常tarball,相对于前缀的键名就是调用时传递的名称。
s3://my-bucket/models/
tenant-a/scorer.tar.gz
tenant-b/scorer.tar.gz
tenant-c/scorer.tar.gz
用两处变更的字段定义容器。镜像必须支持多模型模式;AWS在其多模型支持页面维护了支持的镜像列表,对于GPU支撑的端点,必须是NVIDIA Triton Inference Server镜像。
import boto3
sm = boto3.client("sagemaker")
container = {
"Image": image_uri,
"ModelDataUrl": "s3://my-bucket/models/",
"Mode": "MultiModel",
}
创建模型,然后是端点配置,最后是端点。AWS建议至少两个实例,使端点跨越多个可用区;在多模型端点上这也会提高聚合缓存命中率,因为每个实例都保留自己的一套已加载模型。
sm.create_model(
ModelName="tenant-scorers",
ExecutionRoleArn=role_arn,
Containers=[container],
)
sm.create_endpoint_config(
EndpointConfigName="tenant-scorers-cfg",
ProductionVariants=[{
"VariantName": "AllTraffic",
"ModelName": "tenant-scorers",
"InstanceType": "ml.m5.xlarge",
"InitialInstanceCount": 2,
"InitialVariantWeight": 1,
}],
)
sm.create_endpoint(
EndpointName="tenant-scorers",
EndpointConfigName="tenant-scorers-cfg",
)
等待端点达到 InService 状态。使用 waiter 而非 sleep 循环,因为端点创建时间取决于实例类型和镜像大小。
sm.get_waiter("endpoint_in_service").wait(EndpointName="tenant-scorers")
还有一个值得了解的可选字段。MultiModelConfig 接受一个 ModelCacheSetting,默认值为 Enabled;设为 Disabled 会让容器在每次使用后卸载模型。这听起来像是个坏主意,通常也确实是,但当你的制品大到两个无法同时共存于内存时,这是正确答案,因为缓存过载导致的颠簸比完全不缓存更糟糕。
在 InvokeEndpoint 上额外带一个参数:TargetModel,在传输时作为 X-Amzn-SageMaker-Target-Model 头携带。其值是制品相对于前缀的键名。
rt = boto3.client("sagemaker-runtime")
resp = rt.invoke_endpoint(
EndpointName="tenant-scorers",
TargetModel="tenant-b/scorer.tar.gz",
ContentType="application/json",
Body=json.dumps(payload),
)
print(resp["Body"].read())
其他调用方式不变。InvokeEndpoint API 参考文档中记录的 6,291,456 字节请求和响应体限制仍然适用,同一页面上模型容器必须在 60 秒内响应的规则也同样适用——而现在这个时限包含了下载和加载模型所花费的时间。
令人们惊讶的失败不是慢,而是 429。AWS 在 InvokeEndpoint 的 ModelNotReadyException 文档中说明,该异常覆盖"多模型端点仍在下载或加载目标模型"的情况,其指示是等待并重试。如果你的客户端将 429 视为速率限制并退避一分钟,那么对冷模型的首次调用看起来就像被限流了。实际上不是;它是缓存未命中,只是带了 HTTP 状态码。用短而有界的重试处理,而非限流策略。
其余信息可在 CloudWatch 中看到,AWS 在多模型端点指标页面为此发布了五个指标。按这个顺序阅读:
ModelCacheHit,位于 AWS/SageMaker 命名空间。作为 Average 时它表示模型已加载的请求比例。这是唯一能说明端点是否按预期工作的数字。如果它不接近 1,你调优的其他任何东西都不会起作用。
ModelLoadingWaitTime,以微秒为单位,是调用者实际感受到的:请求等待下载、加载或两者的时间。看它的 Max 而非 Average——Average 被命中主导。
ModelDownloadingTime 和 ModelLoadingTime 将等待拆分为 S3 获取和容器的 LoadModel 调用。下载时间长意味着更大的实例(更多磁盘)或更少、更小的制品;加载时间长意味着模型本身初始化慢,磁盘无济于事。
LoadedModelCount,位于 /aws/sagemaker/Endpoints,按实例发出。跨实例求和并与你的模型数量比较:如果总和不断上升然后崩塌,你正在观察的是驱逐。
多模型端点不支持按模型维度的指标。你无法仅从 CloudWatch 判断哪个租户慢,所以如果这很重要,从调用方发出你自己的维度。
AWS 对其适用场景异常直接:相同框架、相同Serving容器、大小和延迟相近、有高频和低频访问模型混合、应用程序能容忍偶尔的冷启动延迟。每个条款都是真正的约束。一个容器意味着一个框架,所以 PyTorch 模型和 XGBoost 模型不能共用一个端点。大小相近很重要因为缓存是共享的:一个制品比其他的大几倍,每次加载都会驱逐好几个。
真正起决定作用的条款是最后一个。AWS自己的指导是,吞吐量或延迟需求明显更高的模型应放在专用端点上。稳定提供流量的模型从这个架构中毫无收益——它会常驻内存,所以你为复杂性付出的代价毫无意义,而且继承了一个其他租户可以干扰的缓存。这个模式在长尾场景才能发挥价值:成百上千个模型,加起来所需的实例数如果拆成独立端点是你不会考虑的规模。
一个结构性警告,这就是为什么这篇文章带有刷新衰减。SageMaker后来新增了推理组件(Inference Components),这是一种不同的机制,可以将多个模型打包到一个端点上,具有明确的按模型资源预留和按副本自动扩缩,通过 InferenceComponentName 而非 TargetModel 调用。它不是这个功能的改名——组件是声明式而非发现式的,而且不会互相驱逐——但它覆盖了一些相同的场景,而且AWS倾向于引导你使用哪个一直在变化。在基于 Mode: MultiModel 构建长期项目之前,请阅读当前的主机概述。
端点启动后,接下来的两个问题是它如何扩展以及成本如何。扩展在多模型端点上有其特殊性,因为增加一个实例会启动一个全新的空缓存——参见自动扩缩 SageMaker 端点。而当容器真的发生故障而非仅仅卡住时,你收到的是 424,详见修复 SageMaker 端点上的 ModelError。
相关资源