Jak OpenAI zajišťuje hlasovou AI s nízkou latencí ve velkém měřítku
Yi Zhang a William McDonald, členové technického týmu
Hlasová AI působí přirozeně jen tehdy, když se konverzace odvíjí rychlostí řeči. Když do toho zasáhne síť, lidé to hned slyší jako nepříjemné pauzy, useknutá přerušení nebo opožděné vstupy do řeči. To je důležité pro hlas v ChatGPT, pro vývojáře vytvářející řešení pomocí Realtime API, pro agenty pracující v interaktivních pracovních postupech i pro modely, které potřebují zpracovávat zvuk, zatímco uživatel stále mluví.
V měřítku OpenAI se to promítá do tří konkrétních požadavků:
- Globální dosah pro více než 900 milionů aktivních uživatelů týdně
- Rychlé navázání spojení, aby uživatel mohl začít mluvit hned po zahájení relace
- Nízká a stabilní doba oběhu médií tam a zpět, s nízkým kolísáním a ztrátovostí paketů, aby střídání mluvčích působilo svižně
Tým OpenAI odpovědný za interakce s AI v reálném čase nedávno přepracoval náš stack WebRTC, aby vyřešil tři omezení, která se ve velkém měřítku začala střetávat: ukončování médií stylem jeden port na relaci se do infrastruktury OpenAI příliš nehodí, stavové relace ICE (Interactive Connectivity Establishment) a DTLS (Datagram Transport Layer Security) potřebují stabilní vlastnictví a globální směrování musí udržet nízkou latenci prvního skoku. V tomto článku popisujeme rozdělenou architekturu relay plus transceiver, kterou jsme vytvořili, abychom zachovali standardní chování WebRTC pro klienty a zároveň změnili způsob směrování paketů uvnitř infrastruktury OpenAI.
WebRTC je otevřený standard pro přenos zvuku, videa a dat s nízkou latencí mezi prohlížeči, mobilními aplikacemi a servery. Často se spojuje s peer-to-peer voláním, ale je také praktickým základem pro klient-server systémy v reálném čase, protože standardizuje obtížné části interaktivních médií: ICE pro navázání konektivity a průchod NAT (Network Address Translation), DTLS a SRTP (Secure Real-time Transport Protocol) pro šifrovaný přenos, vyjednávání kodeků pro kompresi a dekódování zvuku, RTCP (Real-time Transport Control Protocol) pro řízení kvality a funkce na straně klienta, jako jsou potlačení ozvěny a jitter buffer.
Tato standardizace je pro produkty AI důležitá. Bez WebRTC by každý klient potřeboval jiné řešení pro navázání spojení přes NAT, šifrování médií, vyjednání kodeků (kodérů a dekodérů zvolených pro přenos a dekompresi) a přizpůsobení se měnícím se síťovým podmínkám. S WebRTC můžeme stavět na protokolovém stacku, který je už implementován napříč prohlížeči a mobilními platformami, a soustředit vlastní práci na infrastrukturu, která propojuje média v reálném čase s modely.
Stavíme také na samotném ekosystému WebRTC, včetně vyspělých open-source implementací a standardizační práce, která udržuje interoperabilitu mezi prohlížeči, mobilními aplikacemi a servery. Základní práce Justina Ubertiho (jednoho z původních architektů WebRTC) a Seana DuBoise (tvůrce a správce Pion) umožnila týmům, jako je ten náš, stavět na prověřené mediální infrastruktuře místo toho, abychom znovu vynalézali nízkoúrovňové chování přenosu, šifrování a řízení zahlcení. Máme štěstí, že Justin i Sean jsou nyní našimi kolegy v OpenAI a pomáhají usměrňovat, jak sbližujeme WebRTC a AI v reálném čase.
Pro AI je nejdůležitější vlastností to, že zvuk přichází jako souvislý proud. Mluvený agent může začít přepisovat, uvažovat, volat nástroje nebo generovat řeč, zatímco uživatel stále mluví, místo aby čekal na úplné nahrání. To je rozdíl mezi systémem, který působí konverzačně, a systémem, který připomíná vysílačku.
Jakmile jsme zvolili WebRTC, další otázkou bylo, kde jej ukončit (kde budeme přijímat a vlastnit spojení WebRTC - například na okraji sítě) a jak tyto relace propojit s backendem pro inferenci. Místo ukončení je důležité, protože určuje, jak budeme řešit stav relace v reálném čase, přenos médií, směrování, latenci a izolaci selhání.
SFU, tedy selective forwarding unit, je mediální server, který přijímá jeden proud WebRTC od každého účastníka a vybraně předává proudy ostatním. V tomto modelu SFU ukončuje samostatné spojení WebRTC pro každého účastníka a AI se připojuje jako další účastník relace. To může dobře vyhovovat produktům, které jsou ze své podstaty víceúčastnické, jako jsou skupinové hovory, výuka nebo spolupráce na schůzkách. Udržuje zvukové kodeky, zprávy RTCP, datové kanály, nahrávání i zásady pro jednotlivé proudy na jednom místě.1
I u produktů klient-AI bývá SFU často výchozím bodem, protože týmům umožňuje znovu použít jeden osvědčený systém pro signalizaci, směrování médií, nahrávání, observabilitu a budoucí rozšíření, jako je předání člověku nebo přidání dalších účastníků.
Naše zátěž je jiná. Většina relací je 1:1 — jeden uživatel mluví s jedním modelem nebo jedna aplikace komunikuje s jedním agentem v reálném čase — s citlivostí na latenci při každém střídání. Pro takový tvar provozu jsme zvolili model transceiver: okrajová služba WebRTC ukončí klientské spojení a pak převede média a události do jednodušších interních protokolů pro inferenci modelu, přepis, generování řeči, používání nástrojů a orchestraci.
V tomto návrhu je transceiver jedinou službou, která vlastní stav relace WebRTC, včetně kontrol konektivity ICE, handshake DTLS, šifrovacích klíčů SRTP a životního cyklu relace. „Ukončení“ zde znamená, že transceiver je koncovým bodem, který tyto handshaky dokončuje a média šifruje nebo dešifruje. Udržení tohoto stavu na jednom místě usnadnilo přemýšlení o vlastnictví relace a zároveň umožnilo backendovým službám škálovat jako běžné služby místo toho, aby samy vystupovaly jako peery WebRTC.
Po zvolení modelu transceiveru byla naše první implementace jediná služba v Go postavená na Pion, která obsluhovala signalizaci i ukončování médií. Pohání hlas v ChatGPT, koncový bod WebRTC v Realtime API a řadu výzkumných projektů.
Provozně plní služba transceiveru dvě úlohy:
- Signalizace: vyjednání SDP, výběr kodeku, přihlašovací údaje ICE a nastavení relace
- Média: ukončování downstream spojení WebRTC a udržování upstream spojení k backendovým službám pro inferenci a orchestraci
Chtěli jsme, aby služba běžela stejně jako zbytek naší infrastruktury: na Kubernetes, kde se zátěž může škálovat nahoru i dolů a přesouvat mezi hostiteli podle změn poptávky. Konvenční model WebRTC s jedním portem na relaci se však do tohoto prostředí hodí špatně, protože závisí na velkých veřejných rozsazích UDP portů, které je obtížné vystavit, zabezpečit a zachovat při přidávání, odebírání nebo přeplánování podů.2
Prvním problémem byl samotný model jednoho portu na relaci. Při vysoké souběžnosti to znamená vystavit a spravovat velmi rozsáhlé rozsahy UDP portů.
- Cloud load balancery a služby Kubernetes nejsou navrženy pro desítky tisíc veřejných UDP portů na službu. Každý další rozsah přidává provozní složitost v konfiguraci load balanceru, kontrolách stavu, pravidlech firewallu a bezpečnosti nasazování.3
- Velké rozsahy UDP portů se těžko zabezpečují, protože rozšiřují externě dosažitelnou plochu a ztěžují audit síťových politik.
- Jsou také nevhodné pro automatické škálování. Pody se v Kubernetes neustále přidávají, odebírají a přeplánovávají. Požadavek, aby si každý pod rezervoval a inzeroval velký stabilní rozsah portů, činí tuto elasticitu křehkou.4
Proto se mnoho systémů WebRTC posouvá k jednomu UDP portu na server s demultiplexováním na úrovni aplikace za tímto portem.5
Návrhy s jedním portem na server řeší počet portů, ale přinášejí druhý problém: zachování vlastnictví každé relace napříč flotilou.
ICE a DTLS jsou stavové protokoly. Proces, který relaci vytvořil, musí dál přijímat pakety této relace, aby mohl ověřovat kontroly konektivity, dokončit handshake DTLS, dešifrovat SRTP a zpracovávat pozdější změny relace, jako jsou restarty ICE. Pokud pakety stejné relace dopadnou do jiného procesu, může nastavení selhat nebo se média přeruší.
To nám dalo konkrétní cíl: vystavit veřejnému internetu malou, pevnou UDP plochu, a přitom stále směrovat každý paket k transceiveru, který vlastní odpovídající relaci WebRTC.
Hodnotili jsme několik způsobů, jak toho dosáhnout, včetně TURN (Traversal Using Relays around NAT), kdy okrajový relay ukončuje klientské alokace a předává provoz jejich jménem.2
Přístup | Výhody | Nevýhody |
Jedinečná IP:port pro každou relaci (také známé jako nativní přímé UDP) | Přímá mediální cesta klient-server Žádná předávací vrstva v datové cestě | Vyžaduje jeden veřejný UDP port pro každou relaci Velké rozsahy portů je obtížné vystavit a zabezpečit Špatně se hodí pro Kubernetes a cloudové load balancery |
Jedinečná IP:port pro server | Mnohem menší veřejná UDP stopa než při vystavení pro každou relaci Jeden sdílený socket na server může demultiplexovat mnoho relací | Funguje čistě na jednom hostiteli, ale sám o sobě ne napříč sdílenou flotilou za load balancerem Demultiplexování relací na jednom hostiteli pomáhá až poté, co paket dorazí na tento hostitel; napříč flotilou za load balancerem může první paket stále dopadnout na špatnou instanci, takže stále potřebujete deterministický způsob, jak každou relaci nasměrovat k procesu, který ji vlastní |
TURN relay (s ukončováním protokolu) | Klienti potřebují dosáhnout jen na adresu a port TURN relay Lze centralizovat zásady na okraji | Alokace TURN přidávají další kola komunikace při nastavování Přesouvání nebo obnova alokací mezi servery TURN je stále obtížná |
Bezstavový forwarder + stavový terminátor (relay + transceiver OpenAI) | Malá veřejná UDP stopa Transceiver stále vlastní celou relaci WebRTC | Přidává jeden předávací skok, než média dosáhnou vlastnického transceiveru Vyžaduje vlastní koordinaci mezi relay a transceiverem |
Architektura, kterou jsme nasadili, odděluje směrování paketů od ukončování protokolu. Signalizace stále přichází k transceiveru kvůli nastavení relace, zatímco média nejprve vstupují přes relay. Relay je odlehčená vrstva pro předávání UDP s malou veřejnou stopou a transceiver je stavový koncový bod WebRTC za ní.
Relay média nedešifruje, neprovozuje stavové automaty ICE ani se neúčastní vyjednávání kodeků. Přečte jen tolik metadat paketu, aby vybral cíl, a pak paket přepošle transceiveru, který vlastní danou relaci. Transceiver stále vidí normální tok WebRTC a stále vlastní veškerý stav protokolu. Z pohledu klienta se na relaci WebRTC nic nemění.
Klíčovým krokem v tomto uspořádání je směrování prvního paketu. Relay musí umět směrovat první paket od klienta ještě dřív, než na samotné paketové trase existuje nějaká relace, a to přímo podle cesty paketu, nikoli zastavením kvůli externí vyhledávací službě.
Každá relace WebRTC už nese přirozený směrovací háček protokolu: fragment uživatelského jména ICE neboli ufrag, krátký identifikátor vyměňovaný při nastavování relace a vracený v kontrolách konektivity STUN. Generujeme ufrag na straně serveru tak, aby obsahoval právě dost směrovacích metadat, která relay umožní odvodit cílový cluster a vlastnický transceiver.
Během signalizace transceiver alokuje stav relace a v odpovědi SDP vrátí sdílenou VIP relay a UDP port. VIP je virtuální IP adresa před relay flotilou; v kombinaci s portem dává klientovi jediný stabilní cíl, například 203.0.113.10:3478, i když za ním stojí mnoho instancí relay. První paket klienta po mediální cestě je obvykle požadavek STUN (Session Traversal Utilities for NAT) na binding, který ICE používá k ověření, že pakety dosáhnou inzerované adresy.
Relay z tohoto prvního paketu STUN parsuje jen tolik, aby přečetl serverový ufrag, dekódoval směrovací nápovědu a předal paket vlastnickému transceiveru. Každý transceiver naslouchá na sdíleném UDP socketu, tedy na jednom koncovém bodě operačního systému navázaném na interní IP:port, nikoli na jednom socketu pro každou relaci. Jakmile relay vytvoří relaci z klientského zdrojového IP:portu na cíl tohoto transceiveru, následné pakety DTLS, RTP a RTCP už proudí v rámci relace bez opětovného dekódování ufragu.
Relace v relay je záměrně minimální a tvoří ji jen relace v paměti, která slouží k předávání paketů, spolu s potřebnými čítači pro monitoring a časovači pro expiraci a úklid relace. Tato volba návrhu udržuje směrování paketů přímo na trase paketu. Pokud se relay restartuje a relaci ztratí, další paket STUN relaci znovu vytvoří podle směrovací nápovědy v ufragu. Aby to bylo ještě spolehlivější, používá se mezipaměť Redis, která uchovává mapování <IP klienta + port, IP transceiveru + port>, jakmile je trasa navázána, takže ji lze obnovit mnohem dříve, ještě než dorazí další paket STUN.
Jakmile jsme zmenšili veřejnou UDP plochu na malý počet stabilních adres a portů, mohli jsme stejný model relay nasadit globálně. Global Relay je naše flotila geograficky distribuovaných vstupních bodů relay, které všechny implementují stejné chování předávání paketů.
Široce rozprostřený geografický vstup zkracuje první skok mezi klientem a OpenAI, protože paket může vstoupit do naší sítě v relay blízko uživatele, a to jak geograficky, tak z hlediska síťové topologie, místo aby nejprve cestoval veřejným internetem do vzdáleného regionu. Prakticky to znamená nižší latenci, menší kolísání a méně zbytečných výpadků paketů, než provoz dorazí do naší páteřní sítě.6
Pro signalizaci používáme geografické a proximity směrování Cloudflare, aby počáteční požadavek HTTP nebo WebSocket dorazil do blízkého clusteru transceiverů. Kontext požadavku určuje umístění relace i to, který vstupní bod Global Relay se klientovi inzeruje. Odpověď SDP poskytne adresu Global Relay, zatímco ufrag obsahuje dostatek informací, aby Global Relay dokázal směrovat média do určeného clusteru a relay do cílového transceiveru.
Společně zajišťují geograficky řízená signalizace a Global Relay to, že nastavení i média jdou blízkou vstupní trasou, zatímco relace zůstává ukotvená k jednomu transceiveru. Tím se zkracuje doba cesty tam a zpět pro signalizaci i pro první kontrolu konektivity ICE, což přímo zkracuje dobu, kterou uživatel čeká, než může začít řeč.
Službu relay jsme napsali v Go a záměrně udrželi úzce zaměřenou implementaci. V Linuxu síťový stack jádra přijímá UDP pakety ze síťového rozhraní stroje a doručuje je do socketu, což je koncový bod operačního systému, který proces čte po navázání na IP:port. Relay běží v uživatelském prostoru, takže běžný proces v Go čte z tohoto socketu hlavičky paketů, aktualizuje malé množství stavových informací o toku a pakety přeposílá, aniž by ukončoval WebRTC. Nepotřebovali jsme žádný framework pro obejití jádra, který by sice umožnil procesu v uživatelském prostoru přímo číst síťové fronty pro vyšší paketové rychlosti, ale také by přidal provozní složitost.
Klíčové návrhové volby:
- Bez ukončování protokolu: Relay parsuje pouze hlavičky STUN a ufrag; pro následné DTLS, RTP a RTCP používá stav v cache, takže pakety zůstávají neprůhledné.
- Pomíjivý stav: Udržuje malou mapu v paměti s krátkým timeoutem, která mapuje adresu klienta na cíl transceiveru pro stav toku a observabilitu.
- Horizontální škálovatelnost: Za load balancerem paralelně běží více instancí relay. Stav není tvrdý stav WebRTC, takže restarty způsobují minimální výpadky provozu a rychlé obnovení toků.
Opatření pro efektivitu:
SO_REUSEPORTje možnost socketu v Linuxu, která umožňuje více workerům relay na stejném stroji navázat stejný UDP port. Jádro pak rozděluje příchozí pakety mezi tyto workery, čímž se vyhne úzkému hrdlu v jediné čtecí smyčce.runtime.LockOSThreadpřipne každou goroutine čtoucí UDP ke konkrétnímu vláknu OS. V kombinaci sSO_REUSEPORTto má tendenci udržet pakety stejného toku (zdrojová a cílová IP:port plus protokol) na stejném jádru CPU, což zlepšuje lokalitu cache a snižuje přepínání kontextu.- Předem alokované buffery a minimum kopírování udržují nízké režijní náklady na parsování a alokaci, aby se v Go omezil garbage collection.
Tato implementace zvládla náš globální provoz médií v reálném čase s relativně malou stopou relay, takže jsme zachovali jednodušší návrh místo toho, abychom se vydali cestou obejití jádra.
Tato architektura nám umožňuje provozovat média WebRTC v Kubernetes bez vystavení tisíců UDP portů. To je důležité, protože menší a pevná UDP plocha se snáze zabezpečuje a vyvažuje a umožňuje infrastruktuře škálovat bez rezervace velkých rozsahů veřejných portů. Díky lepší podpoře infrastruktury ze strany Kubernetes a vyšší bezpečnosti díky menší ploše tento návrh zároveň zachovává standardní chování WebRTC pro klienty a potvrzuje, že návrh bez SFU byl pro naši zátěž správnou výchozí volbou. Většina našich relací je point-to-point, citlivá na latenci a snáze škálovatelná, když služby inference nemusí fungovat jako peery WebRTC.
Širší ponaučení je, že nejlepší místo pro přidání složitosti je tenká směrovací vrstva, ne každá backendová služba a ne vlastní chování klienta. Zakódování směrovacích metadat do pole, které je protokolu vlastní, nám dalo deterministické směrování prvního paketu, malou veřejnou UDP stopu a dostatečnou flexibilitu umístit vstup blízko uživatelům po celém světě.
Několik rozhodnutí bylo obzvlášť důležitých:
- Zachovat sémantiku protokolu na okraji. Klienti stále používají standardní WebRTC, což udržuje interoperabilitu mezi prohlížeči a mobily.
- Udržet obtížný stav relace na jednom místě. Transceiver vlastní ICE, DTLS, SRTP i životní cyklus relace; relay pouze přeposílá pakety.
- Směrovat podle informací, které už jsou v nastavení přítomny. ICE ufrag nám poskytl háček pro směrování prvního paketu bez přidání závislosti na vyhledávání v horké cestě.
- Optimalizovat pro běžný případ, než sáhneme po obejití jádra. Úzce zaměřená implementace v Go s pečlivým využitím
SO_REUSEPORT, připínání vláken a parsování s nízkým počtem alokací pro naši zátěž stačila.
Hlasová AI v reálném čase funguje jen tehdy, když infrastruktura způsobí, že latence je prakticky neviditelná. Pro nás to znamenalo změnit podobu našeho nasazení WebRTC, aniž bychom měnili to, co klienti od samotného WebRTC očekávají.
Autor
Reference
2. GitHub - l7mp/stunner: Mediální brána pro Kubernetes a WebRTC(otevře se v novém okně)
3. Porty WebRTC ve zkratce [příklady] - BlogGeek.me(otevře se v novém okně)
4. Nasazení do Kubernetes - dokumentace LiveKit(otevře se v novém okně)
6. Cloudflare Calls: miliony kaskádových stromů až dolů(otevře se v novém okně)


