ai newsDo., 22. Okt.
EilmeldungNvidia launches Minerva to catch failing parts in giant AI data centres

Autoscaling für TTS auf Amazon SageMaker AI: Statt Schätzen gilt Messen

Wer eigene Text-to-Speech-Modelle auf Amazon SageMaker AI betreibt, setzt Autoscaling-Schwellenwerte oft nach Gefühl an. Das kostet entweder unnötig GPU-Budget oder gefährdet Latenz-SLOs. Ein Beispiel mit Kokoro-82M als Custom Container zeigt, wie sich eine Scaling-Policy aus Messungen an Live-Endpunkten ableiten lässt.

AWS ML BlogDonnerstag 18:30 · 22.10.DE15 Min. Lesezeit
Bild: AWS ML Blog

Ein Lastanstieg trifft Ihren selbst gehosteten Text-to-Speech-Endpunkt (TTS), und Anfragen beginnen sich zu stauen. Die Latenz steigt über Ihr Service Level Objective (SLO) hinaus, genau dann, wenn die meisten Anrufer zuhören. Die Auto-Scaling-Richtlinie hinter diesem Endpunkt sollte das verhindern, aber der Zielwert, auf dem sie skaliert, war eine Schätzung, und solche Schätzungen scheitern still, bis realer Verkehr sie widerlegt.

Liegt diese Zahl falsch, wird es auf eine von zwei Arten bezahlt, und keine davon löst einen Alarm aus. Schätzt man zu konservativ, laufen in der Flotte GPU-Instanzen, die meistens untätig sind. Diese Kosten erscheinen auf jeder Rechnung, führen aber nie zu einem Incident-Bericht. Schätzt man zu aggressiv, trägt die Flotte ein Risiko, das niemand sieht. Jede Instanz hält mehr gleichzeitige Anfragen, als sie innerhalb ihrer Latenz-Zusage bedienen kann. Nichts wirkt falsch, bis ein echter Lastanstieg die Wartezeit über das SLO hinaus drückt. Das passiert genau dann, wenn die meisten Anrufer zuhören. Das ist die verborgene Lücke. Eine Richtlinie, die in der Konsole unauffällig aussieht, kann die Personen, denen sie dienen soll, bereits im Stich lassen.

In diesem Beitrag deployen wir Kokoro-82M, ein TTS-Modell mit 82 Millionen Parametern, als Custom Container auf einem Amazon SageMaker AI Real-Time-Endpunkt. AWS Cloud Development Kit (AWS CDK) Stacks übernehmen das Deployment. Wir ersetzen das Rätselraten durch zwei Messungen am Live-Endpunkt. Q_max ist die höchste Concurrency, die eine Instanz aufrechterhalten kann, ohne das SLO zu verletzen, und T_total ist die Dauer eines Scale-outs von Ende zu Ende. Daraus leiten wir per Arithmetik den Scale-out-Schwellenwert, den Scale-in-Schwellenwert und beide Cooldown-Zeiten ab. Jede Zahl in diesem Beitrag stammt aus einem tatsächlichen Deployment und einer Messung des in diesem Beitrag referenzierten aws-samples Repositorys.

Die meisten öffentlichen Empfehlungen zu Amazon SageMaker AI Auto Scaling zielen auf kurze synchrone Request/Response-Aufrufe oder auf das Serving großer Sprachmodelle (LLM) ab, wo die natürliche Metrik generierte Tokens pro Sekunde ist. Die TTS-Synthese passt in keines dieser Muster: Eine einzelne Anfrage hält eine Verbindung mehrere Sekunden offen, ohne dass es eine Token-Generierungsschleife gibt, an die man eine Metrik anhängen könnte. Kokoro-82M ist nicht-autoregressiv, ein StyleTTS2-Derivat mit einem ISTFTNet-Decoder, das adversarial trainiert wurde und nicht durch autoregressive Next-Token-Prediction oder iteratives Denoising.

Es erzeugt eine komplette Äußerung in einem einzigen duration-aligned Durchlauf. Es gibt schlicht keine Generierungsschleife, die sich mit einer Per-Token-Metrik instrumentieren ließe. Was bestimmt, ob eine TTS-Instanz überlastet ist, ist einfacher, als es klingt: wie viele Anfragen gerade darauf in der Warteschlange stehen und ausgeführt werden.

Die deployte Richtlinie skaliert auf Basis von ConcurrentRequestsPerModel, einer nativen Metrik, die Amazon SageMaker AI selbst veröffentlicht, und nicht einer, die der Container-Code dieses Projekts selbst ausgeben müsste. Das Skalieren auf eine von Amazon SageMaker AI veröffentlichte Metrik beseitigt eine ganze Klasse von Fehlern, bei denen eine benutzerdefinierte Metrikkonfiguration auf dem Papier korrekt aussieht, aber tatsächlich nichts an sie veröffentlicht wird.

ConcurrentRequestsPerModel veröffentlicht eine Maximum-Statistik über ein 10-Sekunden-Fenster, eine andere Größe als die mittlere Anzahl in-flight Anfragen, die ein Benchmarking-Client direkt misst. Die Tooling-Kette berücksichtigt das. An jeder Stufe der Concurrency-Leiter misst sie beide Größen unter derselben Last und erfasst das Verhältnis zwischen ihnen. Anschließend nutzt sie dieses Verhältnis, um den client-gemessenen Schwellenwert in Amazon CloudWatch-Einheiten umzurechnen, bevor sie ihn in die deploybare Konfiguration schreibt. Beim Kokoro-82M-Lauf in diesem Beitrag betrug das gemessene Verhältnis an jeder Stufe exakt 1.0, sodass der client-gemessene und der von Amazon CloudWatch gemeldete Schwellenwert direkt übereinstimmen.

Diese Lösung deployt Kokoro-82M in einem Custom Container auf einem Amazon SageMaker AI Real-Time-Endpunkt und ersetzt dann einen geschätzten Auto-Scaling-Zielwert durch einen, der aus zwei Messungen an diesem Live-Endpunkt abgeleitet ist: Q_max, die Concurrency-Obergrenze, die eine Instanz innerhalb des SLO bedienen kann, und T_total, die Dauer eines echten Scale-outs. Eine Synthese-Anfrage durchläuft vier Schichten: einen Client, den Amazon SageMaker AI Endpunkt, einen Custom Container auf einer GPU-Instanz hinter diesem Endpunkt und das Kokoro-82M-Modell innerhalb des Containers.

Abbildung 1: Der Kokoro-82M Anfragepfad, vom Client über den Amazon SageMaker AI Endpunkt und den Custom Container bis zum Modell

Der Container unterstützt zwei Transportarten: Response Streaming (InvokeEndpointWithResponseStream), das Audio bereits während der Synthese zurückstreamet, sodass die Wiedergabe beginnt, bevor die Äußerung fertig ist, und bidirektionales Streaming (InvokeEndpointWithBidirectionalStream), das eine langlebige Sitzung für mehrere Nachrichten offen hält.

Der Betrieb von Kokoro-82M in einem Custom Container statt hinter einem verwalteten Inference-Toolkit ist das, was den Rest dieses Beitrags überhaupt ermöglicht. Ein Container, den Sie selbst kontrollieren, kann eigene Admission-Limits durchsetzen und genau das Concurrency-Signal bereitstellen, das Amazon SageMaker AI ohnehin veröffentlicht, statt eine geliehene Metrik nachträglich auf eine Workload zu übertragen, für die sie nicht gebaut wurde. Die AWS CDK Stacks, die diesen Endpunkt deployen, hängen Auto-Scaling-Ressourcen nur an, wenn ein echter, gemessener Plan für das Modell existiert, sodass eine geschätzte Konfiguration niemals still die Produktion erreichen kann. Der Rest dieses Beitrags ist genau diese Messung.

Am Ende dieses Walkthroughs verfügen Sie über eine live geschaltete Amazon SageMaker AI Auto-Scaling-Richtlinie für Kokoro-82M, die vollständig aus zwei Messungen an Ihrem eigenen Endpunkt abgeleitet ist und nicht aus einer Schätzung.

Der Endpunkt wird über drei AWS CDK Stacks deployt, die in dieser Reihenfolge angewendet werden: SpeechFoundationStack erstellt die IAM-Rollen und den Amazon S3 Bucket, der Modellgewichte zwischenspeichert, SpeechModelCacheStack synchronisiert die Hugging Face Gewichte eines Modells einmalig in diesen Bucket, und SpeechEndpointStack baut das Container-Image, pusht es zu Amazon ECR und erstellt das Amazon SageMaker AI Modell, die Endpunkt-Konfiguration und den Endpunkt.

Abbildung 2: Die drei AWS CDK Stacks und die Prüfung, die entscheidet, ob Auto-Scaling-Ressourcen synthetisiert werden

Die Prüfung im dritten Stack verhindert, dass eine Standard-Scaling-Konfiguration ohne Messung die Produktion erreicht. scaling_enabled ist ein statisches Flag, das true ist, sobald max_instances größer als min_instances konfiguriert ist, und sagt für sich genommen nichts darüber aus, ob die dahinterstehenden Zahlen jemals gemessen wurden. Der Stack fügt zum Synthetisierzeitpunkt eine zweite, unabhängige Prüfung hinzu: Existiert ein echtes, umsetzbares Plan-Artefakt für dieses Modell? Nur wenn beide Bedingungen erfüllt sind, synthetisiert der Stack Scaling-Ressourcen: ein EndpointAutoscaling Konstrukt, das die Scaling-Richtlinie und Alarme verdrahtet, und ein EndpointObservability Konstrukt, das die Dashboards und Alarme zur Überwachung hinzufügt.

Ein Modell mit scaling_enabled=True, aber ohne dahinterstehende Messung, erhält dieselbe Ausgabe ohne Scaling-Ressourcen wie eines mit scaling_enabled=False.

Die CLI des Projekts kapselt die drei Stacks hinter einem einzigen Befehl und wendet sie in Abhängigkeitsreihenfolge an:

uv run speech-infra deploy kokoro-82m SpeechModelCacheStack startet ein AWS CodeBuild Projekt, das die Hugging Face Gewichte des Modells einmalig in den gecachten Amazon S3 Bucket synchronisiert. Ein Kaltstart des Endpunkts muss sie dann nicht aus dem Internet laden. Hinter der vorherigen scaling_enabled Prüfung baut SpeechEndpointStack das Container-Image, pusht es zu Amazon ECR und erstellt das Amazon SageMaker AI Modell, die Endpunkt-Konfiguration und den Endpunkt.

Die Methodik ist als Kommandozeilen-Toolkette paketiert und wird in fester Reihenfolge ausgeführt:

Abbildung 3: Die Toolkette, in Reihenfolge ausgeführt, wobei jeder Befehl ein Artefakt an den nächsten weitergibt

drift läuft immer zuerst, bevor irgendetwas Geld kostet oder einen Endpunkt unter Last setzt. Es ist ein schreibgeschützter Abgleich zwischen dem tatsächlich Deployten (scalable targets, Scaling-Richtlinien, Alarmzustände) und dem, was die Konfigurationsdatei als deployt vorsieht. Eine verwaiste Richtlinie eines früheren Modells oder ein gestoppter oder abgebrochener qmax- oder ttotal-Lauf würde sonst mitten in der Messung still Kapazität verändern und die gemessenen Zahlen verfälschen.

uv run tts-bench drift Ein sauberes Ergebnis meldet keine Abweichungen und beendet sich mit dem Status preflight-ok und ebnet damit den Weg für qmax. Ein gestoppter oder abgebrochener qmax- oder ttotal-Lauf ist die häufigste Ursache einer gemeldeten Abweichung, weil er Auto Scaling auf einer einzelnen Instanz einfrieren kann. uv run tts-bench thaw --endpoint speech-kokoro-82m stellt die deployte Richtlinie wieder her, bevor Sie fortfahren, da qmax und ttotal beide voraussetzen, dass der deployte Zustand exakt der Konfigurationsdatei entspricht.

Q_max ist die höchste Concurrency, also Anfragen in der Warteschlange plus Anfragen in Ausführung auf einer Instanz, bei der eine Anfrage das erste Audio-Byte noch innerhalb des SLO beim 95. Perzentil erreicht. Gemessen wird sie mit einer geschlossenen Lastleiter: Man hält eine feste Concurrency N exakt ein und erhöht N Stufe für Stufe, bis das SLO bricht. Eine geschlossene Schleife vermeidet Coordinated Omission, den Fehler bei Tests mit fester Ankunftsrate, bei dem ein verlangsamender Server dazu führt, dass der Client weniger Anfragen sendet, anstatt die echte Warteschlangenzeit sichtbar zu machen.

uv run tts-bench qmax --model kokoro-82m --dry-run uv run tts-bench qmax --model kokoro-82m --dry-run gibt den Zeitplan und eine Wanduhr-Schätzung aus, ohne einen einzigen AWS-Aufruf auszulösen, sodass man die Kosten eines echten Laufs von rund 36 Minuten vorher einplanen kann. Der echte Lauf friert die Auto-Scaling-Funktion ein und legt die Kapazität für die Dauer auf eine einzige Instanz fest, was beim Beenden automatisch zurückgesetzt wird. Die ermittelte Zahl ist daher eine echte Obergrenze pro Instanz.

Die Laststaffel lieferte folgende Ergebnisse:

Abbildung 4: p95 time-to-first-audio-byte in Abhängigkeit von der Concurrency, mit der SLO-Linie und dem Überschreitungspunkt bei Concurrency 60

Q_max = 50, und das Ergebnis ist eingegrenzt: Die Stufe 60 hat das SLO tatsächlich überschritten, wodurch 50 eine echte Obergrenze und nicht nur eine untere Schranke ist. Derselbe Lauf maß auch die unbeanstandete Servicezeit, also die Zeit, die eine Anfrage ohne jegliches Queueing benötigt: im Mittel 61,3 ms, p95 66,1 ms. Diese Zahl treibt einen zweiten, unabhängigen Alarm an, der später in diesem Beitrag beschrieben wird.

T_total ist die Wanduhr-Zeit von dem Moment, in dem mehr Kapazität angefordert wird, bis zu dem Zeitpunkt, an dem diese Kapazität gesunden Traffic bedient. Gemessen wird, indem ein Scale-out direkt erzwungen wird, wodurch die eigene Erkennungslogik der bereitgestellten Richtlinie umgangen wird, und indem jede unterscheidbare Phase mit Zeitstempeln versehen wird.

uv run tts-bench ttotal --model kokoro-82m --qmax <qmax-artifact> ttotal benötigt das qmax-Artefakt als Eingabe, da seine Recovery-Bewertung auf den selbst gemessenen Stufen dieser Staffel basiert, und verweigert die Ausführung gegen eine Staffel, die über einen anderen Wire-Transport gemessen wurde.

Der erzwungene Scale-out ließ sich in acht gemessene Phasen unterteilen:

Abbildung 5: Die acht gemessenen Phasen eines echten Scale-outs, von der Kapazitätsanforderung bis zum wiederhergestellten Traffic

Die dominante Phase ist die Bereitstellung der Amazon Elastic Compute Cloud (Amazon EC2)-Instanz nebst dem Container-Image-Pull mit 181 Sekunden, 56 Prozent der insgesamt einkalkulierten Zeit. Der wirksamste Hebel zur Reduzierung von T_total ist nicht der Anwendungscode, sondern eine kleinere Image-Größe oder der Einsatz vorgewärmter Kapazitätspools. Alles, was auf den Moment der Sichtbarkeit der Instanz in den Logs folgt, macht zusammen vergleichsweise geringe 79 Sekunden aus.

Die 60 Sekunden zusätzlich zu den gemessenen 260,2 Sekunden wurden in diesem Lauf nicht gemessen. Sie werden aus der Konfiguration der Richtlinie selbst abgeleitet (Alarm-Auswertungszeitraum mal Anzahl der Auswertungsperioden, plus Cooldown), weil dieser Lauf die Erkennungslogik der Richtlinie absichtlich umging, um die reine Bereitstellungszeit zu isolieren. plan dimensioniert den Surge-Headroom gegen die vollen 320,2 Sekunden, weil diese Reaktionszeit ein realer Bestandteil jedes Produktions-Scale-outs ist, auch wenn sie von dieser Messung ausgeschlossen wurde.

plan ist der einzige Befehl, der AWS niemals berührt. Er liest die qmax- und ttotal-Artefakte, kombiniert sie mit zwei vom Betreiber gewählten Eingaben, einem SLO von 3.000 ms p95 TTFAB und einem Surge-Faktor von 1,25x (die Flotte verträgt ein Verkehrswachstum von 25 Prozent innerhalb eines Scale-out-Zyklus, ohne das SLO zu verletzen), und führt die folgende Rechnung aus. Anschließend gibt er einen Bericht mit einem Urteil für jeden Befund aus: ok, warn oder, bei einem wirklich nicht umsetzbaren Plan, stop.

uv run tts-bench plan --qmax <qmax-artifact> --ttotal <ttotal-artifact> --peak-rps 5000 Das Flag --peak-rps (hier 5.000 Anfragen pro Sekunde, neben einem angenommenen Minimum von 10) dimensioniert die Instanzanzahl der Flotte. In die folgende Schwellenwert-Arithmetik fließt er nicht ein.

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 ist die Concurrency, bei der eine weitere Instanz hinzugefügt wird, um den Surge-Anteil h unter Q_max, sodass die Flotte noch Queue-Headroom besitzt, wenn sie mehr Kapazität anfordert. C_scale_min ist die Concurrency, bei oder unter der eine Instanz entfernt wird, festgelegt auf (1 - 2h) von Q_max, sodass Scale-in erst auslöst, wenn die Last deutlich unter den Punkt gefallen ist, an dem ein erneuter Scale-out greifen würde.

Die final bereitgestellte Konfiguration, exakt so, wie sie vom Tooling erzeugt wurde:

"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 ) Die beiden Cooldowns sind absichtlich asymmetrisch. Bei einem Scale-out ist es günstig, sich zu irren: Im schlimmsten Fall hat man für dreißig zusätzliche Sekunden für eine Instanz bezahlt, die man nicht brauchte. Beim Scale-in ist der Aufwand nicht symmetrisch, denn entfernte und später wieder benötigte Kapazität kostet eine volle T_total (320 Sekunden), um sie zu ersetzen.

Der Scale-in-Cooldown ist auf rund das Dreifache von T_total gesetzt, sodass eine Scale-in-Entscheidung nur auslöst, wenn die Last wirklich und nachhaltig gefallen ist.

Die eigene Admission Control des Containers erzwingt separat queue_max_depth=50, gleich Q_max: Oberhalb dieser Tiefe wird eine Anfrage sofort mit einer 503 queue_saturated-Antwort abgelehnt, statt sie in die Warteschlange zu stellen, obwohl sie das SLO nicht mehr einhalten könnte. Ein FirstChunkLatencyP95-Alarm mit einem Schwellenwert von 65 ms (die gemessene p95-Servicezeit bei Concurrency 1) überwacht die Servicezeit auf einer bereits bedienenden Instanz und ist damit enger gefasst als das Ende-zu-Ende-SLO, das auch das Queueing einschließt.

Zwei Befunde aus dem Kokoro-82M-Lauf sind erwähnenswert, weil es sich dabei um Fälle handelt, in denen das Tool einen bekannten und akzeptierten Kompromiss korrekt aufzeigt und keinen Defekt:

Der Dateiname jedes qmax- oder ttotal-Artefakts kodiert die Konfiguration, gegen die gemessen wurde, wobei Instanztyp und Container-Image-Digest zu einem Slug verdichtet sind (zum Beispiel g5xlarge-8b1b96bc). ttotal und plan verweigern beide die Paarung von Artefakten, deren Fingerabdrücke nicht übereinstimmen. Das lässt sich mit --allow-config-mismatch zu einer Warnung herabstufen, die Verweigerung ist jedoch der Standard.

validate ist der letzte Befehl in der Kette und neben drift der zweite, der nichts verändert. Er vergleicht die tatsächlich bereitgestellte Scaling-Richtlinie mit den Werten im plan-Artefakt und bestätigt, dass beide übereinstimmen, bevor man die Konfiguration als live behandelt.

uv run tts-bench validate --model kokoro-82m --plan <plan-artifact> Ein sauberes Ergebnis bestätigt, dass scaling_target_value, scale_in_threshold und beide Cooldowns des bereitgestellten skalierbaren Ziels dem entsprechen, was plan abgeleitet hat. Jede Abweichung bedeutet, dass die bereitgestellte Richtlinie und der gemessene Plan auseinandergedriftet sind, was behoben werden sollte, bevor man sich auf das Auto-Scaling-Verhalten des Endpunkts verlässt.

Jedes in diesem Beitrag referenzierte qmax-, ttotal- und plan-Artefakt ist eine committete Datei im Repository und keine einmalige lokale Ausgabe. Das vorangehende durchgerechnete Beispiel, einschließlich jeder Zahl, ist unabhängig reproduzierbar, indem man plan erneut gegen die beiden committeten Messdateien ausführt, ohne AWS-Aufrufe und ohne neue Kosten:

$ 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 Aufräumen Um die in dieser Anleitung erstellten Ressourcen zu entfernen, baut man die Stacks mit einem einzigen Befehl in umgekehrter Reihenfolge ab:

uv run speech-infra destroy kokoro-82m speech-infra destroy entfernt das Amazon SageMaker AI-Modell, die Endpunktkonfiguration, den Endpunkt sowie alle zugehörigen Auto-Scaling- oder Amazon CloudWatch-Ressourcen. Der Amazon S3-Bucket, der die Modellgewichte zwischenspeichert, wird nicht geleert, und das Amazon ECR-Repository mit dem Container-Image wird nicht gelöscht, da beide modellübergreifend geteilt werden können. Wenn man sie nicht mehr benötigt, leert und löscht man den Amazon S3-Bucket und entfernt das Amazon ECR-Repository manuell, denn ein gespeicherter Modell-Checkpoint und ein Container-Image verursachen bis zu ihrer Entfernung geringe Speicherkosten.

Man deployt Kokoro-82M als custom container auf einem Amazon SageMaker AI Real-Time-Endpoint und ersetzt Rätselraten durch zwei Messungen am live geschalteten Endpunkt: Q_max, die höchste Concurrency, die eine Instanz aufrechterhalten kann, ohne das SLO zu verletzen, und T_total, wie lange ein echter Scale-out dauert. Aus diesen beiden Zahlen leitet man Scale-out-Schwellenwert, Scale-in-Schwellenwert und beide Cooldowns per Arithmetik ab. Dass man auf ConcurrentRequestsPerModel skaliert, eine native, von AWS veröffentlichte Metrik, und deren Einheitenumrechnung direkt misst statt sie anzunehmen, hält diese Konfiguration an der echten Concurrency verankert.

Das Ergebnis ist eine Auto-Scaling-Richtlinie, bei der jede Zahl auf eine echte Messung am eigenen Endpunkt zurückgeht und nicht auf eine Annahme, die aus einer anderen Workload-Form übernommen wurde. Genau dieser Unterschied verwandelt eine Konfigurationsschätzung, die stillschweigend zu viel bezahlt oder stillschweigend ihr SLO verletzt, in eine Richtlinie, die man mit einer konkreten Concurrency-Obergrenze und einer konkreten Scale-out-Zeit verteidigen kann, beides gemessen, nicht behauptet.

Dieses Vier-Zahlen-Modell, bei dem SLO und Surge Ratio gewählt sowie Q_max und T_total gemessen und der Rest abgeleitet wird, ist nicht spezifisch für TTS. Es lässt sich auf latenzempfindliche, nebenläufigkeitsbegrenzte Realtime-Endpoints von Amazon SageMaker AI verallgemeinern. Beispielsweise unterliegt ein Realtime-Endpoint für Image-Embeddings, der jede Anfrage während eines GPU-Forward-Pass offen hält, derselben Nebenläufigkeitsgrenze und nicht einer Durchsatzmetrik.

Dieselbe Kette aus qmax, ttotal und Planung gilt unverändert. Der tatsächliche Engpass besteht dort in gleichzeitig bearbeiteten Anfragen und nicht in einer Durchsatzmetrik, die aus einem anderen Workload-Muster entliehen wurde. Die konkreten Zahlen hier, Q_max=50 und T_total=320s, gelten für Kokoro-82M auf ml.g5.xlarge.

Für ein anderes Modell oder einen anderen Instance-Typ werden sie abweichen. Die Methodik und das Tooling, die diese Zahlen liefern, jedoch nicht. Die Fünf-Befehle-Kette aus drift, qmax, ttotal, plan und validate lässt sich unverändert auf das nächste Modell übertragen, das Sie auf diese Weise deployen.

Klonen Sie das Beispiel-Repository und führen Sie dann die Fünf-Befehle-Kette aus drift, qmax, ttotal, plan und validate gegen Ihren eigenen Endpoint aus, um eine gemessene Richtlinie zu erzeugen. Sehen Sie sich die Dokumentation zum Auto Scaling von Amazon SageMaker AI an, um die Schwellenwerte an Ihr SLO anzupassen, und öffnen Sie die SageMaker-AI-Konsole, um zu beobachten, wie die Richtlinie auf echten Verkehr reagiert. Wenn Sie Unterstützung bei einem ähnlichen messungsbasierten Deployment wünschen, wenden Sie sich an Ihr AWS-Kontoteam.

Joseph ist Machine Learning Engineer beim AWS Generative AI Innovation Center, wo er KI-Systeme entwirft und bereitstellt, die Organisationen von der Idee bis zur Produktion führen. Sein technischer Hintergrund umfasst Firmware, Software und Machine Learning mit tiefgreifender Expertise in hochdurchsatzfähigen, ereignisgesteuerten Architekturen, die KI-Workloads auch bei großem Umfang performant halten. Joseph konzentriert sich auf die Schnittstelle zwischen aktueller Forschung und betrieblicher Realität und liefert Lösungen, die Organisationen und die von ihnen betriebenen Systeme nachhaltig verändern.

Automatisch übersetzt. Das Original auf Englisch ist unten aufklappbar.

Original (Englisch) anzeigen

A traffic spike hits your self-hosted text-to-speech (TTS) endpoint, and requests start queueing. Latency climbs past your service level objective (SLO) right when the most callers are listening. The auto scaling policy behind that endpoint was supposed to prevent this, but the target value it scales on was a guess, and guesses like that fail silently until real traffic proves them wrong.

Get that number wrong, and it’s paid for in one of two ways, and neither shows up as an alarm. Guess too conservative, and the fleet runs GPU instances that sit mostly idle. That cost lands on every invoice but never trips an incident report. Guess too aggressive, and the fleet carries a risk nobody sees. Each instance holds more concurrent requests than it can serve inside its latency promise. Nothing looks wrong until a real traffic spike pushes queueing time past the SLO. That happens right when the most callers are listening. That’s the hidden gap. A policy that looks fine on the console can already be failing the people it’s supposed to serve.

In this post, we deploy Kokoro-82M, an 82-million-parameter TTS model, as a custom container on an Amazon SageMaker AI real-time endpoint. AWS Cloud Development Kit (AWS CDK) stacks handle the deployment. We replace guesswork with two measurements taken against the live endpoint. Q_max is the highest concurrency one instance sustains before breaching the SLO, and T_total is how long a scale-out takes end to end. We derive the scale-out threshold, scale-in threshold, and both cooldowns from those two numbers by arithmetic. Every number in this post comes from an actual deployment and measurement run of the aws-samples repository referenced in this post.

Most public guidance on Amazon SageMaker AI auto scaling targets short synchronous request/response calls, or large language model (LLM) serving, where the natural metric is tokens generated per second. TTS synthesis fits neither pattern: a single request holds a connection open for several seconds with no token-generation loop to attach a metric to. Kokoro-82M is non-autoregressive, a StyleTTS2 derivative with an ISTFTNet decoder, trained adversarially rather than by autoregressive next-token prediction or iterative denoising.

It produces a full utterance from one duration-aligned pass. There’s no generation loop to instrument with a per-token metric in the first place. What determines whether a TTS instance is overloaded is simpler than it sounds: how many requests are queued and executing on it right now.

The deployed policy scales on ConcurrentRequestsPerModel, a native metric Amazon SageMaker AI publishes itself rather than one this project’s own container code has to emit. Scaling on a metric Amazon SageMaker AI publishes removes an entire class of failure where a custom metric configuration looks correct on paper but nothing is actually publishing to it.

ConcurrentRequestsPerModel publishes a Maximum statistic over a 10-second window, a different quantity from the mean in-flight count a benchmarking client measures directly. The tooling accounts for this. At every rung of the concurrency ladder it measures both quantities against the same load and records the ratio between them. It then uses that ratio to convert the client-measured threshold into Amazon CloudWatch units before writing it into the deployable configuration. For the Kokoro-82M run in this post, the measured ratio came out to exactly 1.0 at every rung, so the client-measured and Amazon CloudWatch-reported thresholds match directly.

This solution deploys Kokoro-82M in a custom container on an Amazon SageMaker AI real-time endpoint, then replaces a guessed auto scaling target with one derived from two measurements taken against that live endpoint: Q_max, the concurrency ceiling one instance serves inside the SLO, and T_total, how long a real scale-out takes. A synthesis request flows through four layers: a client, the Amazon SageMaker AI endpoint, a custom container on a GPU instance behind that endpoint, and the Kokoro-82M model inside the container.

Figure 1: The Kokoro-82M request path, from client through the Amazon SageMaker AI endpoint and custom container to the model

The container supports two wire transports: response streaming ( InvokeEndpointWithResponseStream ), which streams audio back as it’s synthesized so playback starts before the utterance finishes, and bidirectional streaming ( InvokeEndpointWithBidirectionalStream ), which holds one long-lived session open for multiple messages.

Running Kokoro-82M in a custom container, rather than behind a managed inference toolkit, is what makes the rest of this post possible. A container you control can enforce its own admission limits and expose the exact concurrency signal Amazon SageMaker AI already publishes, instead of retrofitting a borrowed metric onto a workload it wasn’t built for. The AWS CDK stacks that deploy this endpoint only attach auto scaling resources once a real, measured plan exists for the model, so a guessed configuration can never reach production silently. The rest of this post is that measurement.

By the end of this walkthrough, you will have a live Amazon SageMaker AI auto scaling policy for Kokoro-82M, derived entirely from two measurements taken against your own endpoint rather than from a guess.

The endpoint deploys through three AWS CDK stacks, applied in order: SpeechFoundationStack creates the IAM roles and the Amazon S3 bucket that caches model weights, SpeechModelCacheStack syncs a model’s Hugging Face weights into that bucket once, and SpeechEndpointStack builds the container image, pushes it to Amazon ECR, and creates the Amazon SageMaker AI model, endpoint configuration, and endpoint.

Figure 2: The three AWS CDK stacks and the gate that decides whether auto scaling resources get synthesized

The gate on the third stack keeps a default, unmeasured scaling configuration from reaching production. scaling_enabled is a static flag, true whenever max_instances is configured greater than min_instances, and it says nothing on its own about whether the numbers behind it were ever measured. The stack adds a second, independent check at synthesis time: does a real, feasible plan artifact exist for this model? Only when both conditions hold does the stack synthesize scaling resources: an EndpointAutoscaling construct that wires the scaling policy and alarms, and an EndpointObservability construct that adds the dashboards and alarms used to monitor it. A model with scaling_enabled=True but no measurement behind it gets the same no scaling resources output as one with scaling_enabled=False.

The project’s CLI wraps the three stacks behind a single command, applying them in dependency order:

uv run speech-infra deploy kokoro-82m SpeechModelCacheStack runs an AWS CodeBuild project that syncs the model’s Hugging Face weights into the cached Amazon S3 bucket once. A cold endpoint start doesn’t need to pull them from the internet. Gated by the preceding scaling_enabled check, SpeechEndpointStack builds the container image, pushes it to Amazon ECR, and creates the Amazon SageMaker AI model, endpoint configuration, and endpoint.

The methodology packages as a command-line tool chain, run in a fixed order:

Figure 3: The tool chain, run in order, each command handing an artifact to the next

drift runs first, every time, before anything that costs money or holds an endpoint under load. It’s a read-only reconciliation between what’s actually deployed (scalable targets, scaling policies, alarm states) and what the config file says should be deployed. An orphaned policy from an earlier model, or a stopped or canceled qmax or ttotal run, would otherwise silently move capacity mid-measurement and corrupt the numbers being measured.

uv run tts-bench drift A clean result reports no mismatches and exits with a preflight-ok status, clearing the way for qmax. A stopped or canceled qmax or ttotal run is the most common source of a reported mismatch, because it can leave auto scaling frozen on a single instance. uv run tts-bench thaw --endpoint speech-kokoro-82m restores the deployed policy before you proceed, since qmax and ttotal both assume the deployed state matches the config file exactly.

Q_max is the largest concurrency, requests queued plus requests executing on one instance, at which a request still reaches first audio byte inside the SLO at the 95th percentile. It’s measured with a closed-loop load ladder: hold a fixed concurrency N exactly, and walk N upward one rung at a time until the SLO breaks. A closed loop avoids coordinated omission, the flaw in fixed-arrival-rate tests where a slowing server makes the client send fewer requests instead of surfacing real queueing delay.

uv run tts-bench qmax --model kokoro-82m --dry-run uv run tts-bench qmax --model kokoro-82m --dry-run prints the schedule and a wall-clock estimate with zero AWS calls, so you can budget the roughly 36-minute cost of a live run first. The live command freezes auto scaling and pins capacity to one instance for the duration, restored automatically on exit, so the number it produces is a genuine per-instance ceiling.

The load ladder recorded the following results:

Figure 4: p95 time-to-first-audio-byte against concurrency, with the SLO line and the breach point at concurrency 60

Q_max = 50, and it’s a bracketed result: rung 60 actually crossed the SLO, which makes 50 a real ceiling rather than a lower bound. The same run measured the uncontended service time, the time one request takes with no queueing at all: mean 61.3 ms, p95 66.1 ms. That number drives a second, independent alarm described later in this post.

T_total is how long it takes, wall clock, from the moment more capacity is requested to the moment that capacity serves healthy traffic. It’s measured by forcing a scale-out directly, bypassing the deployed policy’s own detection logic, then timestamping every distinguishable stage.

uv run tts-bench ttotal --model kokoro-82m --qmax <qmax-artifact> ttotal requires the qmax artifact as input, since its recovery judgment reads off that ladder’s own measured rungs, and refuses to run against a ladder measured on a different wire transport.

The forced scale-out broke down into eight measured stages:

Figure 5: The eight measured stages of a real scale-out, from capacity request to recovered traffic

The dominant stage is Amazon Elastic Compute Cloud (Amazon EC2) instance provisioning plus the container image pull, at 181 seconds, 56 percent of the planned-against total. The highest-impact way to reduce T_total isn’t application code, it’s reducing image size or using warm capacity pools. Everything downstream of “instance visible in logs” is a comparatively small 79 seconds combined.

The 60-second addition on top of the measured 260.2 seconds isn’t measured in this run. It’s bounded from the policy’s own configuration (alarm evaluation period times evaluation periods, plus cooldown), because this run bypassed the policy’s detection logic on purpose to isolate pure provisioning time. plan sizes surge headroom against the full 320.2-second total, because that reaction time is a real part of any production scale-out even though this measurement excluded it.

plan is the only command that never touches AWS. It reads the qmax and ttotal artifacts, combines them with two operator-chosen inputs, an SLO of 3,000 ms p95 TTFAB and a surge ratio of 1.25x (the fleet tolerates traffic growing 25 percent within one scale-out cycle without breaching the SLO), and does the following arithmetic. It then prints a report with a verdict for each finding: ok, warn, or, for a genuinely infeasible plan, stop.

uv run tts-bench plan --qmax <qmax-artifact> --ttotal <ttotal-artifact> --peak-rps 5000 The --peak-rps flag (5,000 requests per second here, alongside an assumed trough of 10) sizes the fleet’s instance count. It doesn’t factor into the threshold arithmetic below.

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 is the concurrency at which one more instance is added, below Q_max by the surge fraction h, so the fleet still has queue headroom when it asks for more capacity. C_scale_min is the concurrency at or below which one instance is removed, set at (1 - 2h) of Q_max so scale-in triggers only once load has fallen well below where scale-out would trigger again.

The final deployed configuration, exactly as produced by the tooling:

"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 ) The two cooldowns are asymmetric on purpose. Scaling out is cheap to be wrong about: worst case, you paid for one instance you didn’t need for thirty extra seconds. Scaling in isn’t symmetric in cost, since capacity removed and later needed again costs a full T_total (320 seconds) to replace.

The scale-in cooldown is set to roughly three times T_total, so a scale-in decision only fires once load has genuinely, sustainedly dropped.

The container’s own admission control separately enforces queue_max_depth=50, equal to Q_max: past that depth, a request is refused immediately with a 503 queue_saturated response rather than left to queue past the point it could meet the SLO. A FirstChunkLatencyP95 alarm, thresholded at 65 ms (the measured p95 service time at concurrency 1), watches service time on an instance already serving, tighter than the end-to-end SLO, which includes queuing.

Two findings from the Kokoro-82M run are worth including, because they’re the tool correctly surfacing a known, accepted trade-off rather than a defect:

Every qmax or ttotal artifact’s file name encodes the configuration it was measured against, instance type and container image digest condensed into a slug (for example, g5xlarge-8b1b96bc ). ttotal and plan both refuse to pair artifacts whose fingerprints disagree. That’s downgradable to a warning with --allow-config-mismatch, but refusal is the default.

validate is the last command in the chain, and the second one, alongside drift, that never mutates anything. It compares the scaling policy actually deployed against the values in the plan artifact, confirming the two match before you treat the configuration as live.

uv run tts-bench validate --model kokoro-82m --plan <plan-artifact> A clean result confirms scaling_target_value, scale_in_threshold, and both cooldowns on the deployed scalable target match what plan derived. Any mismatch means the deployed policy and the measured plan have drifted apart, and should be resolved before relying on the endpoint’s auto scaling behavior.

Every qmax, ttotal, and plan artifact referenced in this post is a committed file in the repository, not a one-off local output. The preceding worked example, including every number, is independently reproducible by re-running plan against the two committed measurement files, with zero AWS calls and zero new spend:

$ 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 Clean up To remove the resources created in this walkthrough, tear down the stacks in reverse order with a single command:

uv run speech-infra destroy kokoro-82m speech-infra destroy removes the Amazon SageMaker AI model, endpoint configuration, endpoint, and any auto scaling or Amazon CloudWatch resources attached to it. It doesn’t empty the Amazon S3 bucket that caches model weights or delete the Amazon ECR repository holding the container image, since both can be shared across models. If you no longer need them, empty and delete the Amazon S3 bucket and remove the Amazon ECR repository manually, since a stored model checkpoint and container image continue to incur small storage charges until removed.

Deploy Kokoro-82M as a custom container on an Amazon SageMaker AI real-time endpoint, and replace guesswork with two measurements taken against the live endpoint: Q_max, the highest concurrency one instance sustains before breaching the SLO, and T_total, how long a real scale-out takes. You derive the scale-out threshold, scale-in threshold, and both cooldowns from those two numbers by arithmetic. Scaling on ConcurrentRequestsPerModel, a native, AWS-published metric, and measuring its unit conversion directly rather than assuming it, is what keeps this configuration tied to real concurrency.

The result is an auto scaling policy where every number traces back to a real measurement against your own endpoint, not an assumption carried over from a different workload shape. That distinction is what turns a config-file guess, one that silently overpays or silently breaches its SLO, into a policy you can defend with a specific concurrency ceiling and a specific scale-out time, both measured, not asserted.

Nothing about this four-number model, SLO and surge ratio chosen, Q_max and T_total measured, the rest derived, is specific to TTS. It generalizes to latency-sensitive, concurrency-bound, real-time Amazon SageMaker AI endpoints. For example, a real-time image-embedding endpoint that holds each request open through a GPU forward pass is bound by the same concurrency ceiling, not by a throughput metric.

The same qmax / ttotal / plan chain applies unchanged. The real bottleneck there is concurrent in-flight requests rather than a throughput metric borrowed from a different workload shape. The specific numbers here, Q_max=50 and T_total=320s, are true for Kokoro-82M on ml.g5.xlarge.

They will differ for a different model or instance type. The methodology and tooling that produce them don’t. The five-command tool chain, drift, qmax, ttotal, plan, and validate, carries over unchanged to the next model you deploy this way.

Clone the sample repository, then run the five-command chain, drift, qmax, ttotal, plan, and validate, against your own endpoint to produce a measured policy. Review the Amazon SageMaker AI auto scaling documentation to adapt the thresholds to your SLO, and open the SageMaker AI console to watch the policy react to live traffic. If you’d like help building a similar measurement-driven deployment, contact your AWS account team.

Joseph is a Machine Learning Engineer with the AWS Generative AI Innovation Center, where he designs and delivers AI systems that move organizations from idea to production. His technical background spans firmware, software, and machine learning, with deep expertise in high-throughput, event-driven architectures that keep AI workloads performant at scale. Joseph focuses on the intersection of state-of-the-art research and operational reality, delivering solutions that materially transform organizations and the systems they operate.

Originaltitel: Stop guessing auto scaling targets for TTS on Amazon SageMaker AIQuelle: AWS ML Blog (AWS ML Blog). Original öffnen