跳到正文
AWS Machine Learning Blog· Joseph Cottingham·· 3 小时前精选AI 评分60

AWS 用实测数据替代 TTS 在 SageMaker AI 上的自动扩缩猜测

Stop guessing auto scaling targets for TTS on Amazon SageMaker AI

AI 导读

AWS 在 Amazon SageMaker AI 实时端点上部署 Kokoro-82M 语音合成模型,用两项实测数据替代凭猜测设定的自动扩缩目标值。

推荐理由

原文给出从实测并发上限和扩容耗时推导自动扩缩阈值的完整算例,可迁移到其他延迟敏感的实时端点。

正文 · AI 翻译

流量高峰冲击你自托管的文本转语音(TTS)端点,请求开始排队。延迟在你服务等级目标(SLO)之上攀升,而这恰恰发生在最多调用方正在收听的时候。该端点背后的自动扩缩策略本应防止这种情况,但它用来扩缩的目标值只是一个猜测,而这类猜测会静默失败,直到真实流量证明它们是错的。

把这个数字搞错,代价会以两种方式之一付出,而这两种方式都不会以告警的形式出现。猜得太保守,集群就会运行大部分时间处于空闲状态的 GPU 实例。这笔成本会落在每一张账单上,却永远不会触发事件报告。猜得太激进,集群就会承担无人察觉的风险。每个实例承载的并发请求数超过了它能在延迟承诺内服务的数量。在真实流量高峰把排队时间推过 SLO 之前,一切看起来都没问题。而这恰恰发生在最多调用方正在收听的时候。这就是隐藏的缺口。一个在控制台上看起来没问题的策略,可能已经在辜负它本应服务的人。

在本文中,我们将 Kokoro-82M(一个 8200 万参数的 TTS 模型)作为自定义容器部署到 Amazon SageMaker AI 实时端点上。AWS Cloud Development Kit(AWS CDK) 堆栈负责部署。我们用针对实时端点进行的两次测量取代猜测。Q_max 是一个实例在突破 SLO 之前能维持的最高并发数,T_total 是一次扩容端到端需要多长时间。我们通过算术从这两个数字推导出扩容阈值、缩容阈值以及两个冷却时间。本文中的每个数字都来自对本文所引用的 aws-samples 仓库的实际部署和测量运行。

为什么默认自动扩缩指标不适用于 TTS

大多数关于 Amazon SageMaker AI 自动扩缩的公开指南针对的是短同步请求/响应调用,或大语言模型(LLM)服务,其自然指标是每秒生成的 token 数。TTS 合成不符合这两种模式:单个请求会让连接保持打开数秒,且没有可供挂接指标的 token 生成循环。Kokoro-82M 是非自回归的,是 StyleTTS2 的衍生模型,带有 ISTFTNet 解码器,通过对抗训练而非自回归下一 token 预测或迭代去噪来训练。它通过一次时长对齐的前向传递生成完整话语。首先就不存在可供逐 token 指标插桩的生成循环。决定 TTS 实例是否过载的因素比听起来更简单:当前有多少请求在它上面排队和执行。

所部署的策略基于 ConcurrentRequestsPerModel 进行扩缩,这是 Amazon SageMaker AI 自身发布的原生指标,而不是本项目自己的容器代码必须发出的指标。基于 Amazon SageMaker AI 发布的指标进行扩缩,消除了一整类故障:自定义指标配置在纸面上看起来正确,但实际上没有任何东西在向它发布数据。

ConcurrentRequestsPerModel 在 10 秒窗口内发布 Maximum 统计量,这与基准测试客户端直接测量的平均在途请求数是不同的量。工具链考虑到了这一点。在并发阶梯的每一级,它都会在相同负载下测量这两个量,并记录它们之间的比值。然后,它使用该比值将客户端测得的阈值转换为 Amazon CloudWatch 单位,再写入可部署配置。对于本文中的 Kokoro-82M 运行,测得的比值在每一级都恰好为 1.0,因此客户端测得的阈值与 Amazon CloudWatch 报告的阈值直接匹配。

解决方案概述

此解决方案在 Amazon SageMaker AI 实时端点的自定义容器中部署 Kokoro-82M,然后用基于对该实时端点进行的两次测量得出的自动扩缩目标,替换原先猜测的目标:Q_max,即单个实例在 SLO 内可服务的并发上限;以及 T_total,即真实横向扩展所需的时间。一个合成请求流经四层:客户端、Amazon SageMaker AI 端点、该端点背后 GPU 实例上的自定义容器,以及容器内的 Kokoro-82M 模型。

Diagram showing a client connecting to an Amazon SageMaker AI real-time endpoint, which contains a custom GPU container exposing two routes, POST /invocations for response streaming and WS /invocations-bidirectional-stream for bidirectional streaming, both feeding into a KPipeline running Kokoro-82M with lang_code=“a”

图 1:Kokoro-82M 请求路径,从客户端经过 Amazon SageMaker AI 端点和自定义容器到模型

该容器支持两种传输方式:响应流式传输(InvokeEndpointWithResponseStream),在合成音频的同时将其流式返回,因此播放可以在话语结束前开始;以及双向流式传输(InvokeEndpointWithBidirectionalStream),保持一个长期会话打开以处理多条消息。

在自定义容器中运行 Kokoro-82M,而不是将其置于托管推理工具包之后,正是本文其余部分得以实现的原因。你控制的容器可以强制执行自己的准入限制,并暴露 Amazon SageMaker AI 已经发布的精确并发信号,而不是将借来的指标硬套到并非为其构建的工作负载上。部署此端点的 AWS CDK 堆栈只有在模型存在真实、经过测量的 plan 后才会附加自动扩缩资源,因此猜测的配置永远不会悄无声息地进入生产环境。本文的其余部分就是这项测量。

先决条件

要跟随本演练,你需要:

  • An AWS account with permissions to create AWS Identity and Access Management (IAM) roles, Amazon Simple Storage Service (Amazon S3) buckets, Amazon Elastic Container Registry (Amazon ECR) repositories, and Amazon SageMaker AI resources.
    • 注意:示例容器固定了较早版本的 PyTorch,该版本存在已知公告(PYSEC-2026-2286,已在 torch 2.10.0 中修复)。在任何非概念验证用途之前,请审查并升级该固定版本。
  • AWS CDK v2,已安装并在你的目标账户和区域中完成引导。
  • Python 3.11 或更高版本、uv 以及用于构建容器镜像的 Docker。
  • 一个 ml.g5.xlarge 服务配额,在运行本文中的横向扩展测量之前,至少比你的基线多一个空闲槽位。
  • 本文引用的 GitHub 仓库 中的部署代码。

演练

完成本演练后,你将拥有一个适用于 Kokoro-82M 的实时 Amazon SageMaker AI 自动扩缩策略,它完全基于对你自己的端点进行的两次测量得出,而不是基于猜测。

部署端点堆栈

端点通过三个 AWS CDK 堆栈部署,按顺序应用:SpeechFoundationStack 创建 IAM 角色和用于缓存模型权重的 Amazon S3 存储桶,SpeechModelCacheStack 将模型的 Hugging Face 权重一次性同步到该存储桶,SpeechEndpointStack 构建容器镜像、将其推送到 Amazon ECR,并创建 Amazon SageMaker AI 模型、端点配置和端点。

Diagram showing three AWS CDK stacks in sequence, SpeechFoundationStack, SpeechModelCacheStack, and SpeechEndpointStack, feeding into a decision gate labeled scaling_enabled AND a feasible measured plan artifact exists, branching to EndpointAutoscaling plus EndpointObservability on yes, or no scaling resources synthesized on no

图 2:三个 AWS CDK 堆栈以及决定是否合成自动扩缩资源的门控

第三个堆栈上的门控会阻止默认的、未经测量的扩缩配置进入生产环境。scaling_enabled 是一个静态标志,只要 max_instances 被配置为大于 min_instances 就为 true,而它本身并不能说明其背后的数字是否曾被测量过。该堆栈在合成时还会增加第二项独立检查:是否存在针对该模型的真实、可行的 plan 产物?只有两个条件都成立时,堆栈才会合成扩缩资源:一个 EndpointAutoscaling 构造,用于连接扩缩策略和告警;以及一个 EndpointObservability 构造,用于添加监控所需的仪表盘和告警。一个具有 scaling_enabled=True 但背后没有测量数据的模型,会得到与具有 scaling_enabled=False 的模型相同的 no scaling resources 输出。

该项目的 CLI 将三个堆栈封装在单个命令之后,并按依赖顺序应用它们:

uv run speech-infra deploy kokoro-82m

SpeechModelCacheStack 运行一个 AWS CodeBuild 项目,将模型的 Hugging Face 权重同步到缓存的 Amazon S3 存储桶中一次。冷启动端点无需从互联网拉取它们。受前面的 scaling_enabled 检查门控,SpeechEndpointStack 构建容器镜像,将其推送到 Amazon ECR,并创建 Amazon SageMaker AI 模型、端点配置和端点。

该方法论打包为命令行工具链,按固定顺序运行:

Diagram showing five tools in sequence, tts-bench drift labeled read-only seconds, tts-bench qmax labeled mutates about 36 minutes, tts-bench ttotal labeled mutates up to about 28 minutes, tts-bench plan labeled read-only seconds, and tts-bench validate labeled read-only seconds, connected by edges labeled preflight ok, qmax artifact, qmax plus ttotal artifacts, and plan artifact

图 3:工具链按顺序运行,每个命令将产物交给下一个命令

步骤 1:检查漂移

drift 每次都会最先运行,先于任何花钱或让端点承受负载的操作。它是在实际部署的内容(可扩缩目标、扩缩策略、告警状态)与配置文件声称应部署的内容之间进行只读对账。否则,来自早期模型的孤立策略,或已停止或已取消的 qmax 或 ttotal 运行,可能会在测量过程中悄悄改变容量,并破坏正在测量的数字。

uv run tts-bench drift

干净的结果会报告无任何不匹配,并以 preflight-ok 状态退出,为 qmax 扫清道路。已停止或已取消的 qmax 或 ttotal 运行是报告不匹配的最常见来源,因为它可能使自动扩缩冻结在单个实例上。uv run tts-bench thaw --endpoint speech-kokoro-82m 会在你继续之前恢复已部署的策略,因为 qmax 和 ttotal 都假定已部署状态与配置文件完全一致。

步骤 2:测量 Q_max,即并发上限

Q_max 是最大并发数(排队请求加上在单个实例上执行的请求),在该并发数下,请求仍能在第 95 百分位内于 SLO 内到达首个音频字节。它通过闭环负载阶梯进行测量:精确保持固定并发 N,并将 N 逐级向上提升,直到 SLO 被打破。闭环可避免协调遗漏,即固定到达率测试中的缺陷:服务器变慢时,客户端会发送更少的请求,而不是暴露真实的排队延迟。

uv run tts-bench qmax --model kokoro-82m --dry-run
uv run tts-bench qmax --model kokoro-82m

--dry-run 会在零 AWS 调用的情况下打印时间表和实际耗时估计,因此你可以先为一次实际运行约 36 分钟的成本做预算。实际运行命令会在持续期间冻结自动扩缩并将容量固定到一个实例,退出时自动恢复,因此它产生的数字是真正的单实例上限。

负载阶梯记录了以下结果:

并发数 p95 TTFAB(毫秒) 满足 3,000 毫秒 SLO?
1 65.4 是
5 286.2 是
10 568.5 是
20 1,129.3 是
30 1,689.8 是
40 2,245.0 是
50 2,805.7 是(Q_max)
60 3,374.8 否(违反)

Line chart showing p95 time to first audio byte in milliseconds rising with concurrency from 65.4 ms at concurrency 1 to 3374.8 ms at concurrency 60, crossing a dashed SLO line at 3000 milliseconds between concurrency 50 and 60, with Q_max equals 50 annotated at the last point that still meets the SLO

图 4:p95 首个音频字节时间与并发数的关系,包含 SLO 线以及并发数 60 处的违反点

Q_max = 50,而且这是一个带括号的结果:第 60 级实际上越过了 SLO,这使得 50 成为真正的上限,而不是下限。同一次运行还测量了无争用服务时间,即一个请求在完全没有排队的情况下所需的时间:均值 61.3 ms,p95 66.1 ms。这个数字驱动了本文后面描述的第二个独立告警。

步骤 3:测量 T_total,即真实的横向扩展时间

T_total 是从请求更多容量到该容量开始服务健康流量所经过的挂钟时间。测量方法是直接强制触发一次横向扩展,绕过已部署策略自身的检测逻辑,然后为每个可区分的阶段打上时间戳。

uv run tts-bench ttotal --model kokoro-82m --qmax <qmax-artifact>

ttotal 需要 qmax 产物作为输入,因为它的恢复判断读取的是该阶梯自身测得的各级结果,并且拒绝针对在不同网络传输上测得的阶梯运行。

强制横向扩展分解为八个测量阶段:

阶段 时长 占 T_total 的比例
请求容量到 desired_set 30.0 s 9.4%
desired_set 到实例在日志中可见 180.8 s 56.5%(主导)
实例可见到容器启动 3.7 s 1.2%
容器启动到 framework_init 0.0 s 0.0%
framework_init 到 weights_ready 6.5 s 2.0%
weights_ready 到 warmup_done 1.0 s 0.3%
warmup_done 到 in_service 20.7 s 6.5%
in_service 到流量恢复 47.4 s 14.8%
实测总计(t_total_s) 260.2 s
+ 策略检测延迟,有界(未测量) 60.0 s
规划所依据的 T_total 320.2 s

Stacked horizontal bar chart showing eight stages of a scale-out timeline for Kokoro-82M on ml.g5.xlarge, a 30 second capacity request stage, a 181 second dominant provisioning stage, several small stages for container start and warmup, a 21 second in-service stage, and a 47 second traffic recovery stage

图 5:一次真实横向扩展的八个测量阶段,从请求容量到流量恢复

主导阶段是 Amazon Elastic Compute Cloud(Amazon EC2)实例预配加上容器镜像拉取,耗时 181 秒,占规划所依据总时长的 56%。降低 T_total 影响最大的方式不是应用代码,而是减小镜像大小或使用热容量池。“实例在日志中可见”之后的所有环节合计只有相对较小的 79 秒。

在实测的 260.2 秒之上增加的 60 秒并未在本次运行中测量。它是根据策略自身配置(告警评估周期乘以评估周期数,再加上冷却时间)界定出来的,因为本次运行故意绕过了策略的检测逻辑,以隔离纯粹的预配时间。plan 按照完整的 320.2 秒总时长来规划突发余量,因为尽管本次测量排除了这部分反应时间,它仍是任何生产横向扩展的真实组成部分。

步骤 4:用 plan 推导已部署策略

plan 是唯一从不接触 AWS 的命令。它读取 qmax 和 ttotal 产物,将它们与两个由操作员选择的输入——3,000 ms p95 TTFAB 的 SLO 和 1.25 倍的突发比例(集群能容忍流量在一个横向扩展周期内增长 25% 而不违反 SLO)——结合起来,并执行以下算术运算。然后它打印一份报告,对每项发现给出结论:ok、warn,或者对于确实不可行的计划给出 stop。

uv run tts-bench plan --qmax <qmax-artifact> --ttotal <ttotal-artifact> --peak-rps 5000

--peak-rps 标志(此处为每秒 5,000 个请求,同时假设低谷为 10)决定集群的实例数量。它不参与下面的阈值算术。

h  = surge_ratio - 1                    = 1.25 - 1 = 0.25

C_scale_max = (1 - h)     x Q_max       = 0.75 x 50   = 37.50
C_scale_min = (1 - 2h)    x Q_max       = 0.50 x 50   = 25.00

W_max = SLO - S_p95                     = 3.000s - 0.066s = 2.934s

C_scale_max 是再增加一个实例时的并发数,比 Q_max 低出突发比例 h,因此集群在请求更多容量时仍有队列余量。C_scale_min 是移除一个实例时所处或低于的并发数,设为 Q_max 的 (1 - 2h),这样只有当负载远低于会再次触发横向扩展的水平时,才会触发横向缩减。

最终部署的配置,与工具链生成的完全一致:

"kokoro-82m": ModelEndpointConfig(
    model_name="kokoro-82m",
    instance_type="ml.g5.xlarge",
    min_instances=1,             # trough 10 rps; 3 is the smallest safe floor for scale-in
    max_instances=9,             # peak 5000 rps
    scaling_target_value=37.500, # C_scale_max, in Amazon CloudWatch units (ratio measured as 1.0)
    scale_in_threshold=25.000,   # C_scale_min, in Amazon CloudWatch units
    ttfab_slo_ms=3000,
    queue_max_depth=50,          # enforced by the container's own admission gate
    scale_out_cooldown_s=30,     # short: one instance added at a time
    scale_in_cooldown_s=960,     # long: see below
)

这两个冷却时间的不对称是有意为之。扩容判断出错的代价很低:最坏情况下,你只是为一个并不需要的实例多付了三十秒的费用。缩容的代价则不对称,因为被移除的容量若之后再次需要,需要整整 T_total(320 秒)才能补回。缩容冷却时间被设为大约 T_total 的三倍,因此只有在负载真正、持续下降之后,才会触发缩容决策。

容器自身的准入控制会单独强制执行 queue_max_depth=50,其值等于 Q_max:超过该深度后,请求会立即被拒绝并返回 503 queue_saturated 响应,而不是任其排队,直到超过仍能满足 SLO 的时点。一个 FirstChunkLatencyP95 告警的阈值为 65 ms(并发为 1 时实测的 p95 服务时间),用于监视已在服务的实例上的服务时间,比包含排队在内的端到端 SLO 更严格。

Kokoro-82M 运行中有两项发现值得纳入,因为它们体现的是工具正确揭示了一个已知且被接受的权衡,而非缺陷:

  • surge_survival (警告): 将 C_scale_max(37.5)与 Q_max(50)对照,意味着扩容一触发,某个实例的利用率就会达到 97.4%,而替换实例需要整整 T_total 才能到位。突发比例预留了额外的队列槽位,但能否扛过真正的突发取决于流量,即队列排空的速度,而在 97.4% 的利用率下,流量余量已所剩无几。
  • scale_in_safety (警告): 从 N 个实例的集群中移除一个实例,会使每个剩余实例的并发量乘以 N / (N-1)。只有当该比值保持在 C_scale_max / C_scale_min(此处为 1.5)以下时,缩容才保持稳定,而这要求 N >= 3。在实例下限为 1 的情况下,从 2 缩容到 1 会使幸存实例恰好落在 Q_max,正好回到触发另一次扩容的上限。较长的冷却时间抑制了这种抖动,但并未消除它。将下限提高到 3 可从结构上消除该问题,代价是在低流量期间保留常备容量。

每个 qmax 或 ttotal 产物的文件名都编码了它所对应的测量配置,实例类型和容器镜像摘要被压缩成一个 slug(例如 g5xlarge-8b1b96bc)。ttotal 和 plan 都会拒绝配对指纹不一致的产物。可通过 --allow-config-mismatch 将其降级为警告,但默认行为是拒绝。

第 5 步:用 validate 确认已部署的策略

validate 是命令链中的最后一条命令,也是与 drift 并列的第二条从不修改任何内容的命令。它将实际部署的扩缩容策略与 plan 产物中的值进行比较,在你将配置视为生效之前确认两者一致。

uv run tts-bench validate --model kokoro-82m --plan <plan-artifact>

干净的结果可确认已部署的可扩缩目标上的 scaling_target_value、scale_in_threshold 以及两个冷却时间都与 plan 推导出的值一致。任何不匹配都意味着已部署的策略与测量出的方案已经出现偏差,应在依赖该端点的自动扩缩容行为之前加以解决。

测试与验证

本文引用的每个 qmax、ttotal 和 plan 产物都是仓库中已提交的文件,而非一次性的本地输出。前面的示例(包括每一个数字)都可以通过针对两个已提交的测量文件重新运行 plan 来独立复现,且零 AWS 调用、零新增开销:

$ uv run tts-bench plan --qmax <qmax-artifact> --ttotal <ttotal-artifact> --peak-rps 5000
scaling_target_value : 37.500 (C_scale_max)
scale_in_threshold    : 25.000 (C_scale_min)
surge_survival        : warn
scale_in_safety       : warn

清理

要移除本演练中创建的资源,请用一条命令按相反顺序拆除这些堆栈:

uv run speech-infra destroy kokoro-82m

speech-infra destroy 会删除 Amazon SageMaker AI 模型、端点配置、端点以及附加到它的任何自动扩缩或 Amazon CloudWatch 资源。它不会清空缓存模型权重的 Amazon S3 存储桶,也不会删除存放容器镜像的 Amazon ECR 存储库,因为这两者都可以跨模型共享。如果你不再需要它们,请手动清空并删除 Amazon S3 存储桶,并移除 Amazon ECR 存储库,因为存储的模型检查点和容器镜像在被移除之前会持续产生少量存储费用。

结论

将 Kokoro-82M 作为自定义容器部署在 Amazon SageMaker AI 实时端点上,并用针对实时端点进行的两次测量取代猜测:Q_max,即单个实例在违反 SLO 之前所能维持的最高并发数;以及 T_total,即一次真实扩容所需的时间。你通过算术运算从这两个数字推导出扩容阈值、缩容阈值以及两个冷却时间。基于 ConcurrentRequestsPerModel(一个由 AWS 发布的原生指标)进行扩缩,并直接测量其单位换算而不是假设它,正是让此配置与真实并发保持关联的关键。

结果就是一个自动扩缩策略,其中每个数字都可追溯到针对你自己端点进行的真实测量,而不是从不同工作负载形态沿用而来的假设。正是这一区别,将配置文件式的猜测(可能悄悄多付费用或悄悄违反 SLO)转变为你可以用具体并发上限和具体扩容时间来辩护的策略,而这两者都是测量得出的,而非断言。

这个四数字模型——选定 SLO 和激增比率、测量 Q_max 和 T_total、其余推导得出——没有任何一点是 TTS 特有的。它可以推广到对延迟敏感、受并发约束的实时 Amazon SageMaker AI 端点。例如,一个实时图像嵌入端点,如果每个请求都要经过一次 GPU 前向传递才保持打开,那么它同样受并发上限约束,而不是受吞吐量指标约束。同样的 qmax/ttotal/plan 链条原样适用。那里真正的瓶颈是并发在途请求,而不是从不同工作负载形态借来的吞吐量指标。这里的特定数字 Q_max=50 和 T_total=320s 对于 ml.g5.xlarge 上的 Kokoro-82M 成立。对于不同的模型或实例类型,它们会有所不同。但产生它们的方法和工具不会变。五命令工具链 drift、qmax、ttotal、plan 和 validate 可原样沿用到你以这种方式部署的下一个模型。

克隆示例存储库,然后针对你自己的端点运行五命令链 drift、qmax、ttotal、plan 和 validate,以生成经过测量的策略。查阅 Amazon SageMaker AI 自动扩缩文档,根据你的 SLO 调整阈值,并打开 SageMaker AI 控制台观察策略对实时流量的反应。如果你需要帮助构建类似的测量驱动部署,请联系你的 AWS 客户团队。


关于作者

Joseph Cottingham

Joseph Cottingham

Joseph 是 AWS 生成式 AI 创新中心的一名机器学习工程师,他在那里设计和交付 AI 系统,帮助组织从想法走向生产。他的技术背景涵盖固件、软件和机器学习,在高吞吐、事件驱动架构方面拥有深厚专业知识,能够使 AI 工作负载在大规模下保持高性能。Joseph 专注于最先进研究与运营现实的交叉点,交付能够实质性改变组织及其运营系统的解决方案。

来源:AWS Machine Learning Blog · aws.amazon.com