别再猜 Amazon SageMaker AI 上 TTS 的自动扩缩容目标值了
Stop guessing auto scaling targets for TTS on Amazon SageMaker AI
针对 Amazon SageMaker AI 上自托管 TTS 端点扩缩容目标值靠猜的问题,该方案将 8200 万参数的 Kokoro-82M 以自定义容器部署在实时端点上,用实测的 Q_max(单实例在 SLO 内可承受的最高并发)和 T_total(一次完整扩容耗时)通过算术推导出扩容阈值、缩容阈值及两个冷却时间。
流量高峰冲击你的自托管文本转语音(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 模型。
图 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 被打破。闭环避免了协调遗漏,即固定到达率测试中的缺陷:服务器变慢时,客户端发送更少的请求,而不是暴露真实的排队延迟。
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 是从请求更多容量那一刻起,到该容量开始服务健康流量那一刻止的实际耗时(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 |
图 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.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 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-82mspeech-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 是 AWS 生成式 AI 创新中心的一名机器学习工程师,他在那里设计和交付 AI 系统,帮助组织从想法走向生产。他的技术背景涵盖固件、软件和机器学习,在高吞吐量、事件驱动架构方面拥有深厚专业知识,使 AI 工作负载在大规模下保持高性能。Joseph 专注于最先进研究与运营现实的交叉点,交付能够实质性地改变组织及其运营系统的解决方案。
来源:AWS Machine Learning Blog · aws.amazon.com




