跳到正文
AWS Machine Learning Blog· Joseph Cottingham·· 11 天前AI 评分34

不再靠猜:为 Amazon SageMaker AI 上的 TTS 推导自动扩缩容目标

Stop guessing auto scaling targets for TTS on Amazon SageMaker AI

AI 导读

针对 Amazon SageMaker AI 上自托管 TTS 端点自动扩缩容目标靠猜的问题,该方案将 82M 参数的 Kokoro-82M 以自定义容器部署在实时端点上,用实测的 Q_max(单实例在 SLO 内可承受的最高并发)和 T_total(一次扩容端到端耗时)通过算术推导出扩容阈值、缩容阈值和两个冷却时间。

正文· 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 的模型相同的 无扩缩资源 输出。

该项目的 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 (ms) 满足 3,000 ms 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 是从请求更多容量那一刻起,到该容量开始服务健康流量那一刻止的实际耗时(wall clock)。测量方法是直接强制触发一次横向扩展,绕过已部署策略自身的检测逻辑,然后为每个可区分的阶段打上时间戳。

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 是移除一个实例时所处或低于的并发数,设为 (1 - 2h) 的 Q_max,这样只有当负载远低于会再次触发横向扩展的水平时,才会触发横向缩减。

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

"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 (warn): 将 C_scale_max(37.5)与 Q_max(50)对照,意味着扩容一触发,某个实例的利用率就会达到 97.4%,而替换实例需要整整 T_total 才能到位。突发比例预留了额外的队列槽位,但挺过真正的突发靠的是流量,即队列排空的速度,而在 97.4% 的利用率下,流量余量已所剩无几。
  • scale_in_safety (warn): 从 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阅读原文

相关阅读

专题与标签

相关资讯专题