AWS 博客:在 Amazon SageMaker AI 上用实测方法替代 TTS 自动扩缩目标猜测
Stop guessing auto scaling targets for TTS on Amazon SageMaker AI
AWS 机器学习博客发布一篇教程,将 Kokoro-82M TTS 模型部署在 Amazon SageMaker AI 实时端点的自定义容器中,用两项实测数据替代猜测的自动扩缩目标:Q_max 为单实例不突破 SLO 的最大并发(实测 50),T_total 为一次扩缩全程耗时(实测 260.2 秒,加上限 60 秒检测滞后合计 320.2 秒)。
一次流量高峰打到您自托管的文本转语音(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 秒窗口内的最大值统计,这与基准测试客户端直接测得的平均在途请求数是不同的量。该工具链对此做了处理。在并发阶梯的每一级,它都在相同负载下同时测量这两个量,并记录两者之间的比值。然后利用该比值,将客户端测得的阈值换算为 Amazon CloudWatch 单位,再写入可部署的配置中。在本文的 Kokoro-82M 运行中,每一级测得的比值都恰好为 1.0,因此客户端测得的阈值与 Amazon CloudWatch 报告的阈值直接一致。
解决方案概览
该解决方案将 Kokoro-82M 部署在 Amazon SageMaker AI 实时端点的自定义容器中,然后用从该运行中端点实际测得的两个值取代猜测的自动扩缩目标:Q_max,即单个实例在 SLO 内可服务的并发上限;T_total,即一次真实的横向扩展所需的时间。一个合成请求会流经四层:客户端、Amazon SageMaker AI 端点、该端点背后的 GPU 实例上的自定义容器,以及容器内的 Kokoro-82M 模型。
图 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 中修复)。在任何非概念验证用途之前,请审查并升级该版本固定。
- 注意:示例容器固定使用了一个存在已知安全公告的较早 PyTorch 版本(
- 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 模型、端点配置和端点。
图 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-82mSpeechModelCacheStack 运行一个 AWS CodeBuild 项目,将模型的 Hugging Face 权重一次性同步到缓存的 Amazon S3 存储桶中。这样冷启动端点就无需从互联网拉取权重。在前一步 scaling_enabled 检查的门控之下,SpeechEndpointStack 构建容器镜像,将其推送到 Amazon ECR,并创建 Amazon SageMaker AI 模型、端点配置和端点。
该方法论被封装为一条命令行工具链,按固定顺序运行:
图 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 被打破。闭环方式可避免协调遗漏(coordinated omission)——这是固定到达率测试的缺陷:服务器变慢时客户端发送的请求反而变少,从而掩盖了真实的排队延迟。
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 | 否(超标) |
图 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 的比例 |
| capacity requested 到 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 |
图 5:一次真实扩容的八个测量阶段,从容量请求到流量恢复
主导阶段是 Amazon Elastic Compute Cloud(Amazon EC2)实例预置加上容器镜像拉取,耗时 181 秒,占计划所依据总时长的 56%。降低 T_total 影响最大的方法不是修改应用代码,而是减小镜像体积或使用温备容量池。“实例在日志中可见”之后的全部阶段加起来只有相对很小的 79 秒。
在测得的 260.2 秒之上附加的 60 秒并未在本次运行中测量。它是根据策略自身的配置(告警评估周期乘以评估次数,再加上冷却时间)得出界限的,因为本次运行有意绕过策略的检测逻辑,以隔离纯预置时间。plan 按照 320.2 秒的完整总时长来规划突发余量,因为这段反应时间是任何生产环境扩容中真实存在的一部分,尽管本次测量将其排除在外。
第 4 步:用 plan 推导已部署策略
plan 是唯一不会触碰 AWS 的命令。它读取 qmax 和 ttotal 产物,将它们与两个由操作者选择的输入相结合——SLO 为 3,000 ms p95 TTFAB,突发比例为 1.25x(即集群可在一个扩容周期内容忍流量增长 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.934sC_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 毫秒为阈值(即并发为 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-82mspeech-infra destroy 会删除 Amazon SageMaker AI 模型、终端节点配置、终端节点,以及挂载在其上的任何自动扩缩或 Amazon CloudWatch 资源。它不会清空用于缓存模型权重的 Amazon S3 存储桶,也不会删除存放容器镜像的 Amazon ECR 仓库,因为两者可以被多个模型共享。如果不再需要它们,请手动清空并删除 Amazon S3 存储桶,并删除 Amazon ECR 仓库,因为存储的模型检查点和容器镜像在被删除之前会持续产生少量存储费用。
结论
在 Amazon SageMaker AI 实时终端节点上以自定义容器方式部署 Kokoro-82M,并用针对实际终端节点测得的两个数值取代猜测: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 客户团队。
作者简介
来源: AWS Machine Learning Blog · aws.amazon.com




