同步推理硬限制60秒和6MB payload;异步推理通过S3中转请求,调用超时可达3600秒,适合长推理任务,但返回HTTP 202而非同步结果。
这一切的背后是一个硬性数字:SageMaker 模型容器必须在 60 秒内响应同步调用。如果你的推理超过这个时间,客户端的超时调优无济于事,而异步是官方支持的出路。
InvokeEndpoint API 参考文档明确指出:客户模型容器必须在 60 秒内响应,模型本身的最大处理时间为 60 秒;如果预期使用 50–60 秒,SDK socket 超时应该设为 70。还有一个硬性的 payload 上限——请求和响应体被限制在 6,291,456 字节。
异步推理解除了这两项限制。Payload 通过 S3 传输而非请求体,AWS 文档记录的调用超时可以高达 3,600 秒。作为交换,你需要放弃同步回复:API 返回 HTTP 202 并附带一个 location,答案最终会出现在那里。
模型本身没有变化。差异完全体现在 CreateEndpointConfig 上,它新增了一个 AsyncInferenceConfig。根据 AsyncInferenceConfig 参考文档,它持有一个必需的 OutputConfig 和一个可选的 ClientConfig。输出配置包含 S3OutputPath、S3FailurePath、可选的 KmsKeyId 以及 NotificationConfig。
创建端点配置时指定一个输出路径,理想情况下再指定一个独立的失败路径。将两者分开是值得的:没有 S3FailurePath,你就只能检查对象来判断它是答案还是堆栈跟踪。
sm.create_endpoint_config(
EndpointConfigName="doc-extract-async-cfg",
ProductionVariants=[{
"VariantName": "AllTraffic",
"ModelName": "doc-extract",
"InstanceType": "ml.g5.xlarge",
"InitialInstanceCount": 1,
}],
AsyncInferenceConfig={
"OutputConfig": {
"S3OutputPath": "s3://my-bucket/async/out/",
"S3FailurePath": "s3://my-bucket/async/fail/",
"NotificationConfig": {
"SuccessTopic": success_topic_arn,
"ErrorTopic": error_topic_arn,
},
},
"ClientConfig": {
"MaxConcurrentInvocationsPerInstance": 4,
},
},
)
用该配置创建端点,然后像对待实时端点一样等待其变为 InService 状态。
授予执行角色对输入前缀的读权限以及对两个输出前缀的写权限。这是静默失败的高发环节:调用成功返回 202,但输出位置永远不会有任何东西出现。
MaxConcurrentInvocationsPerInstance 是队列与容器之间的节流阀。将其设置为单个实例真正能够持有的并发请求数。设置过高会让队列失去对容器的保护作用——而这恰恰是你选择异步的主要原因。
将 payload 上传,调用 invoke_endpoint_async 并传入 InputLocation,保留返回值。InvokeEndpointAsync 参考文档记录了 HTTP 202 响应,包含 OutputLocation 和 FailureLocation 响应头,以及 body 中的 InferenceId。
import boto3, json
s3 = boto3.client("s3")
rt = boto3.client("sagemaker-runtime")
s3.put_object(Bucket="my-bucket", Key="async/in/job-1.json",
Body=json.dumps(payload).encode())
resp = rt.invoke_endpoint_async(
EndpointName="doc-extract-async",
InputLocation="s3://my-bucket/async/in/job-1.json",
ContentType="application/json",
InferenceId="job-1",
InvocationTimeoutSeconds=1800,
)
print(resp["OutputLocation"], resp["FailureLocation"])
有一个容易遗漏的小捷径:Body 接受最多 128,000 字节的内联 payload,且与 InputLocation 互斥。如果你的输入很小但处理时间很长,可以完全跳过 S3 往返,同时仍然获得异步执行模型。
收集结果意味着轮询 S3 的输出 key,或者订阅 SNS topic。优先使用 topic。轮询版本写得更频繁,但在特定方面更差:它通常只轮询 OutputLocation,因此失败的任务在轮询循环放弃之前看起来与慢任务完全相同。如果确实要轮询,两个位置都要轮询。
from botocore.exceptions import ClientError
def collect(bucket, out_key, fail_key):
for _ in range(120):
for key, kind in ((out_key, "ok"), (fail_key, "failed")):
try:
obj = s3.get_object(Bucket=bucket, Key=key)
return kind, obj["Body"].read()
except ClientError as e:
if e.response["Error"]["Code"] not in ("NoSuchKey", "404"):
raise
time.sleep(5)
return "timeout", None
这两个超时是分开的,含义不同,默认值也不是人们以为的数字。
InvocationTimeoutSeconds 是请求在被标记过期之前可以花费的处理时间。AWS 文档记录默认值为 900 秒,最大值为 3,600。其中重要的一半是默认值:如果你是因为任务需要 20 分钟才转向异步,而且没有设置这个字段,任务仍然会过期。
RequestTTLSeconds 是请求在队列中等待被标记过期之前可以停留的时间——文档记录的默认值为 21,600 秒(6 小时),最小值 60,最大值 21,600。这是一个积压策略。对于严重落后的队列,长的 TTL 意味着你最终会处理已经没有人想要的答案。
两者都是 InvokeEndpointAsync 上的每个请求参数,而非端点配置。它们可以因任务不同而不同,这是正确的设计,同时也意味着一个调用方设置的默认值不会保护另一个调用方。
异步端点也有自己的日志流布局。AWS 将端点日志组记录为 /aws/sagemaker/Endpoints/[EndpointName],异步端点在常规容器流之外增加了 [production-variant-name]/[instance-id]/data-log 流。当任务失败且失败对象无用时,数据日志是下一个该查的地方。
这是人们一周后才会惊讶的部分,值得在构建任何上层建筑之前了解。AWS 在其《异步端点的警报和日志》页面上指出,其异步指标列表是穷尽的,任何不在列表中的指标都不会为启用异步的端点发布。它给出的例子是 Invocations、InvocationsPerInstance 和 OverheadLatency。
所以标准的端点监控面板变空白了,更重要的是,标准的自动扩缩容策略变得不可用了。用于实时端点自动扩缩容的预定义 SageMakerVariantInvocationsPerInstance 指标类型基于异步端点不发布的指标构建。这不是对基于积压扩容的偏好;这就是为什么下一节中的自定义指标是唯一选项而非推荐选项。
一旦知道名称就明白了。四个指标只携带 EndpointName 维度,描述队列状态:ApproximateBacklogSize(排队中或进行中的项目数)、ApproximateBacklogSizePerInstance(同样的值除以实例数,AWS 表示这主要用于自动扩缩容)、ApproximateAgeOfOldestRequest(秒),以及 HasBacklogWithoutCapacity。对第三个指标设置警报,而非第一个:500 的积压如果没有上下文就毫无意义——不知道它是一分钟内排空还是卡住了一个小时,而 age 才是用户真正体验到的数字。
其余指标携带 EndpointName 和 VariantName,它们将请求的生命周期拆分成你原本需要猜测的各个阶段。TimeInBacklog 仅是排队时间,明确排除了下载、上传和模型延迟。TotalProcessingTime 是从接收到完成,包含上述所有时间。两者相减再与 ModelLatency 比较,你就知道一个慢任务是模型慢还是等待时间长——这决定了是加实例还是优化容器。RequestDownloadLatency 和 ResponseUploadLatency 分别计算 S3 阶段的耗时,这是意外大 payload 显现的地方。
失败计数器才是需要设置警报的,因为每个都代表一种独立的原因,否则它们只会表现为"输出从未出现":
RequestDownloadFailures — SageMaker 无法读取你的输入对象。几乎总是执行角色的问题,或者 KMS 密钥无权使用。
ResponseUploadFailures — 推理成功但答案无法写入。这是残酷的一种:你为这次工作付了钱却什么也没得到,而容器日志看起来完全正常。
ExpiredRequests — 请求在 RequestTTLSeconds 时在队列中死去。这里的非零值意味着端点在结构上容量不足,而不是某个任务有故障。
NotificationFailures — 工作完成了但 SNS 发布失败,所以通知驱动的消费者永远不会触发。没有这个警报,管道就直接停止而没有任何错误报告。
InvocationFailures 和 InvocationsProcesssed 用于计算总体比率。
AWS 的指标表将最后一个拼写为 InvocationsProcesssed,三个 s。无论是指标名称还是文档拼写错误,在将确切的字符串硬编码到警报之前,先在 CloudWatch 控制台中确认——拼写错误的指标名称会产生一个卡在 INSUFFICIENT_DATA 状态的警报,而不是报错。
在这里还有两件小事值得了解。SNS 通知可以通过 IncludeInferenceResponseIn 携带推理响应本身而不仅仅是指针——一个最多两个值的数组,取自 SUCCESS_NOTIFICATION_TOPIC 和 ERROR_NOTIFICATION_TOPIC——但 AWS 文档记录响应仅在小于等于 128 KB 时才会包含,所以建立在它之上的消费者仍然必须处理指针情况。数据日志流包含每个请求的推理 ID,这让你能够将失败映射回特定任务;在提交时传入你自己的 InferenceId,这个映射就成为你自己的任务标识符而非生成的那个。
由此引出的一个结构性问题:容器从不触碰 S3。SageMaker 下载输入对象并像对待实时请求一样 POST 到容器的 /invocations 端点,然后上传返回的任何内容。容器合约没有改变,这意味着现有的实时容器可以原样使用——同时也意味着在容器中写入 S3 读取代码是不必要的,而且这正是下载指标与实际不符的原因。
异步端点是 AWS 支持将实例缩容至零的唯一托管选项,这也是它们不仅是时长答案更是成本答案的原因。注册方式与实时端点相同,只是 MinCapacity 为 0,跟踪的指标是一个自定义指标:AWS/SageMaker 命名空间中的 ApproximateBacklogSizePerInstance,维度为 EndpointName。
这里有一个 AWS 有记录但容易跳过的陷阱。实例为零时,积压-per-instance 指标会除以零——所以仅靠目标跟踪策略无法恢复端点。因此 AWS 的《自动扩缩容异步端点》页面描述了第二个步进扩缩容策略,由 HasBacklogWithoutCapacity 指标上的 CloudWatch 警报驱动,当队列非空且容量为零时添加一个实例。
aas.put_scaling_policy(
PolicyName="HasBacklogWithoutCapacity-ScalingPolicy",
ServiceNamespace="sagemaker",
ResourceId=resource_id,
ScalableDimension="sagemaker:variant:DesiredInstanceCount",
PolicyType="StepScaling",
StepScalingPolicyConfiguration={
"AdjustmentType": "ChangeInCapacity",
"MetricAggregationType": "Average",
"Cooldown": 300,
"StepAdjustments": [
{"MetricIntervalLowerBound": 0, "ScalingAdjustment": 1}
],
},
)
没有这第二个策略,端点最终仍会恢复——但只有当积压超过目标跟踪值时才会,对于低流量端点,这意味着一天中的第一个请求要等很久。如果你的流量足够零散以至于这成了问题,可以对比一下 Serverless Inference,它用真正的按请求计费换取了长时间的天花板。
Autoscaling a SageMaker Endpoint
SageMaker Serverless Inference: Setup and Its Real Limits
Why a SageMaker Endpoint Costs More Than Expected