Hogyan biztosít az OpenAI alacsony késleltetésű hangalapú MI-t nagy léptékben?
Yi Zhang és William McDonald, műszaki munkatársak tollából
A hangalapú MI csak akkor hat természetesnek, ha a beszélgetés a beszéd tempójában halad. Amikor a hálózat akadályozza ezt, az emberek azonnal észreveszik a kínos szüneteket, a levágott félbeszakításokat vagy a késleltetett közbevágást. Ez fontos a ChatGPT Hang esetében, a Realtime API-val dolgozó fejlesztőknek, az interaktív munkafolyamatokban működő ügynököknek, valamint azoknak a modelleknek, amelyeknek úgy kell feldolgozniuk a hangot, hogy a felhasználó még beszél.
Az OpenAI léptékén ez három konkrét követelményt jelent:
- Globális elérés több mint 900 millió heti aktív felhasználó számára
- Gyors kapcsolatfelépítés, hogy a felhasználó a munkamenet kezdetekor azonnal beszélhessen
- Alacsony és stabil médiaköridő, alacsony jitterrel és csomagvesztéssel, hogy a váltott megszólalás gördülékeny legyen
Az OpenAI valós idejű MI-interakciókért felelős csapata nemrég újratervezte WebRTC stackünket, hogy kezelje azt a három korlátot, amelyek nagy léptékben ütközni kezdtek egymással: a munkamenetenkénti egy portos médiavégződtetés nem illeszkedik jól az OpenAI infrastruktúrájához, az állapottartó ICE (Interactive Connectivity Establishment) és DTLS (Datagram Transport Layer Security) munkameneteknek stabil tulajdonjogra van szükségük, a globális útválasztásnak pedig alacsonyan kell tartania az első ugrás késleltetését. Ebben a bejegyzésben bemutatjuk az általunk felépített, szétválasztott relay plus transceiver architektúrát, amely megőrzi az ügyfelek számára a szabványos WebRTC-viselkedést, miközben megváltoztatja a csomagok útvonalát az OpenAI infrastruktúráján belül.
A WebRTC nyílt szabvány böngészők, mobilalkalmazások és szerverek közötti alacsony késleltetésű hang-, videó- és adatküldésre. Gyakran a peer-to-peer hívásokhoz kötik, de ügyfél–szerver valós idejű rendszerekhez is praktikus alapot ad, mert egységesíti az interaktív média nehéz részeit: ICE a kapcsolat felépítéséhez és a NAT (Network Address Translation) bejárásához, DTLS és SRTP (Secure Real-time Transport Protocol) a titkosított átvitelhez, kodekegyeztetés a hang tömörítéséhez és dekódolásához, RTCP (Real-time Transport Control Protocol) a minőségszabályozáshoz, valamint ügyféloldali funkciók, például visszhangkioltás és jitterpufferelés.
Ez a szabványosítás fontos az MI-termékeknél. WebRTC nélkül minden ügyfélnek más választ kellene adnia arra, hogyan hozzon létre kapcsolatot NAT-okon keresztül, hogyan titkosítsa a médiát, hogyan egyeztessen kodekeket (az átvitelhez és kitömörítéshez kiválasztott kódoló-dekódolókat), és hogyan alkalmazkodjon a változó hálózati feltételekhez. A WebRTC-vel olyan protokollstackre építhetünk, amelyet már megvalósítottak a böngészők és mobilplatformok, így saját munkánkat a valós idejű médiát a modellekhez kapcsoló infrastruktúrára összpontosíthatjuk.
Magára a WebRTC ökoszisztémára is építünk, beleértve az érett nyílt forráskódú implementációkat és azt a szabványosítási munkát, amely biztosítja a böngészők, mobilalkalmazások és szerverek együttműködését. Justin Uberti (a WebRTC egyik eredeti architektje) és Sean DuBois (a Pion megalkotója és karbantartója) alapvető munkája lehetővé tette, hogy a hozzánk hasonló csapatok kipróbált médiainfrastruktúrára építsenek ahelyett, hogy újra feltalálnák az alacsony szintű átvitel, titkosítás és torlódásszabályozás működését. Szerencsések vagyunk, hogy Justin és Sean ma már itt, az OpenAI-nál a kollégáink, és segítenek abban, hogyan hozzuk közelebb egymáshoz a WebRTC-t és a valós idejű MI-t.
Az MI számára a legfontosabb tulajdonság az, hogy a hang folyamatos adatfolyamként érkezik. Egy beszélő ügynök már akkor megkezdheti az átírást, az érvelést, az eszközök hívását vagy a beszéd generálását, miközben a felhasználó még beszél, ahelyett hogy megvárná a teljes feltöltést. Ez különbözteti meg a társalgásszerűnek ható rendszert attól, amely inkább adóvevőként működik.
Miután a WebRTC mellett döntöttünk, a következő kérdés az volt, hol végződtessük azt (ahol elfogadjuk és birtokoljuk a WebRTC-kapcsolatot — például a peremen), és hogyan kapcsoljuk ezeket a munkameneteket az inferencia-háttérrendszerhez. A végződtetés azért fontos, mert meghatározza, hogyan kezeljük a valós idejű munkamenetállapotot, a médiaátvitelt, az útválasztást, a késleltetést és a hibák elkülönítését.
Az SFU, vagyis a selective forwarding unit olyan médiaszerver, amely minden résztvevőtől fogad egy-egy WebRTC-adatfolyamot, és szelektíven továbbítja a többi résztvevőnek. Ebben a modellben az SFU minden résztvevő számára külön WebRTC-kapcsolatot végződtet, és az MI egy másik résztvevőként csatlakozik a munkamenethez. Ez jól illeszkedhet olyan eleve több résztvevős termékekhez, mint a csoporthívások, tantermek vagy együttműködő megbeszélések. Egy helyen tartja a hangkodekeket, az RTCP-üzeneteket, az adatcsatornákat, a rögzítést és az adatfolyamonkénti szabályokat.1
Még ügyfél–MI termékeknél is gyakran az SFU az alapértelmezett kiindulópont, mert lehetővé teszi, hogy a csapatok egy bevált rendszert használjanak újra jelzéskezelésre, médiaútválasztásra, rögzítésre, megfigyelhetőségre és jövőbeli bővítésekre, például emberi átvételre vagy további résztvevők hozzáadására.
A mi terhelésünk más. A legtöbb munkamenet 1:1 — egy felhasználó beszél egy modellel, vagy egy alkalmazás egy valós idejű ügynökkel —, és minden fordulóban érzékeny a késleltetésre. Ennél a forgalmi mintánál egy transceiver modellt választottunk: egy peremi WebRTC-szolgáltatás végződteti az ügyfélkapcsolatot, majd a médiát és eseményeket egyszerűbb belső protokollokká alakítja modellinferencia, átírás, beszédgenerálás, eszközhasználat és összehangolás céljára.
Ebben a kialakításban a transceiver az egyetlen szolgáltatás, amely birtokolja a WebRTC-munkamenet állapotát, beleértve az ICE-kapcsolatellenőrzéseket, a DTLS-kézfogást, az SRTP-titkosítási kulcsokat és a munkamenet életciklusát. A „végződtetés” itt azt jelenti, hogy a transceiver az a végpont, amely befejezi ezeket a kézfogásokat, és titkosítja vagy visszafejti a médiát. Az, hogy ez az állapot egy helyen marad, könnyebben átláthatóvá tette a munkamenet tulajdonjogát, és lehetővé tette, hogy a háttérszolgáltatások hagyományos szolgáltatásokként skálázódjanak ahelyett, hogy maguk is WebRTC-peerekként működnének.
Miután a transceiver modellt választottuk, első implementációnk egyetlen, Pionra épülő Go-szolgáltatás volt, amely a jelzéskezelést és a médiavégződtetést is végezte. Ez működteti a ChatGPT Hangot, a Realtime API WebRTC-végpontját és számos kutatási projektet.
Operatív szempontból a transceiver szolgáltatás két feladatot lát el:
- Jelzéskezelés: SDP-egyeztetés, kodekválasztás, ICE-hitelesítő adatok és munkamenet-beállítás
- Média: a lefelé irányuló WebRTC-kapcsolatok végződtetése és a háttérszolgáltatások felé irányuló kapcsolatok fenntartása inferencia és összehangolás céljára
Azt akartuk, hogy a szolgáltatás úgy fusson, mint infrastruktúránk többi része: Kubernetesen, ahol a terhelések fel- és leskálázhatók, és a kereslet változásával gazdagépek között mozgathatók. A hagyományos, munkamenetenként egy portos WebRTC-modell azonban rosszul illik ehhez a környezethez, mert nagy nyilvános UDP-porttartományokra támaszkodik, amelyeket nehéz kitenni, védeni és megőrizni, miközben podok kerülnek hozzáadásra, eltávolításra vagy újraütemezésre.2
Az első probléma maga a munkamenetenként egy portos modell volt. Nagy egyidejűség mellett ez nagyon nagy UDP-porttartományok kitettségét és kezelését jelenti.
- A felhős terheléselosztók és a Kubernetes-szolgáltatások nem arra készültek, hogy szolgáltatásonként több tízezer nyilvános UDP-port köré szerveződjenek. Minden további tartomány növeli az operatív bonyolultságot a terheléselosztó konfigurációjában, az állapotellenőrzésben, a tűzfalszabályokban és a biztonságos bevezetésben.3
- A nagy UDP-porttartományokat nehéz védeni, mert növelik a kívülről elérhető felületet, és nehezebbé teszik a hálózati szabályok auditálását.
- Az automatikus skálázáshoz sem illenek jól. A podok folyamatosan kerülnek hozzáadásra, eltávolításra és újraütemezésre a Kubernetesben. Ha minden podnak nagy, stabil porttartományt kell lefoglalnia és hirdetnie, az törékennyé teszi ezt a rugalmasságot.4
Ezért mozdul el sok WebRTC-rendszer az egy szerverhez egyetlen UDP-port irányába, alkalmazásszintű demultiplexeléssel e port mögött.5
Az egy szerverhez egyetlen portot használó kialakítások megoldják a portszám problémáját, de behoznak egy másodikat: meg kell őrizni minden munkamenet tulajdonjogát a teljes flottán keresztül.
Az ICE és a DTLS állapottartó protokollok. Annak a folyamatnak, amely létrehozta a munkamenetet, továbbra is meg kell kapnia annak a munkamenetnek a csomagjait, hogy ellenőrizhesse a kapcsolatpróbákat, befejezhesse a DTLS-kézfogást, visszafejthesse az SRTP-t, és feldolgozhassa a későbbi munkamenet-változásokat, például az ICE-újraindításokat. Ha ugyanazon munkamenet csomagjai egy másik folyamathoz érkeznek, a beállítás meghiúsulhat vagy a média megszakadhat.
Ez adott nekünk egy konkrét célt: kis, fix UDP-felületet tegyünk ki a nyilvános internet felé, miközben minden csomagot továbbra is ahhoz a transceiverhez irányítunk, amely a megfelelő WebRTC-munkamenet tulajdonosa.
Ennek elérésére több lehetőséget is megvizsgáltunk, köztük a TURN-t (Traversal Using Relays around NAT), ahol egy peremi relay végződteti az ügyfélallokációkat, és azok nevében továbbítja a forgalmat.2
Megközelítés | Előnyök | Hátrányok |
Egyedi IP:port munkamenetenként (más néven natív közvetlen UDP) | Közvetlen kliens–szerver médiaútvonal Nincs továbbító réteg az adatútvonalban | Munkamenetenként egy nyilvános UDP-portot igényel A nagy porttartományokat nehéz kitenni és védeni Rossz illeszkedés a Kuberneteshez és a felhős terheléselosztókhoz |
Egyedi IP:port szerverenként | A munkamenetenkénti kitettségnél jóval kisebb nyilvános UDP-lábnyom Egy megosztott socket szerverenként sok munkamenetet képes demultiplexelni | Egyetlen gazdagépen jól működik, de megosztott, terheléselosztott flottán önmagában nem Az egyetlen gazdagépen végzett munkamenet-demultiplexelés csak azután segít, hogy egy csomag elérte azt a gazdagépet; terheléselosztott flottában az első csomag továbbra is rossz példányra érkezhet, ezért továbbra is determinisztikus mód kell arra, hogy minden munkamenetet ahhoz a folyamathoz irányítsunk, amely birtokolja |
TURN relay (protokollt végződtető) | Az ügyfeleknek csak a TURN relay címét és portját kell elérniük A szabályok a peremen központosíthatók | A TURN-allokációk plusz beállítási körutakat adnak hozzá Az allokációk áthelyezése vagy helyreállítása a TURN-szerverek között továbbra is nehéz |
Állapotmentes továbbító + állapottartó végződtető (az OpenAI relay + transceiver megoldása) | Kis nyilvános UDP-lábnyom A transceiver továbbra is birtokolja a teljes WebRTC-munkamenetet | Egy továbbítási ugrást ad hozzá, mielőtt a média eléri a tulajdonos transceivert Egyedi koordinációt igényel a relay és a transceiver között |
Az általunk bevezetett architektúra szétválasztja a csomagútválasztást a protokollvégződtetéstől. A jelzéskezelés továbbra is eljut a transceiverhez a munkamenet beállításához, míg a média először a relayen keresztül érkezik. A relay egy könnyűsúlyú UDP-továbbító réteg kis nyilvános kitettséggel, a transceiver pedig a mögötte lévő állapottartó WebRTC-végpont.
A relay nem fejti vissza a médiát, nem futtat ICE-állapotgépeket, és nem vesz részt a kodekegyeztetésben. Éppen csak annyi csomagmetaadatot olvas, amennyi a cél kiválasztásához kell, majd továbbítja a csomagot ahhoz a transceiverhez, amely birtokolja a munkamenetet. A transceiver továbbra is normál WebRTC-folyamot lát, és továbbra is ő birtokolja a teljes protokollállapotot. Az ügyfél nézőpontjából semmi sem változik a WebRTC-munkamenetben.
Ebben a felépítésben az első csomag útválasztása a kulcslépés. Egy relaynek az ügyféltől érkező első csomagot már azelőtt útba kell igazítania, hogy maga a csomagútvonal bármiféle munkamenetet tartalmazna, nem pedig úgy, hogy megáll egy külső lekérdező szolgáltatásnál.
Minden WebRTC-munkamenet már eleve tartalmaz egy protokollnatív útválasztási kapaszkodót: az ICE username fragmentet, vagyis az ufragot, egy rövid azonosítót, amelyet a munkamenet beállításakor cserélnek ki, és amely visszaköszön a STUN kapcsolatellenőrzésekben. Úgy hozzuk létre a szerveroldali ufragot, hogy az éppen elegendő útválasztási metaadatot tartalmazzon ahhoz, hogy a relay következtetni tudjon a célklaszterre és a tulajdonos transceiverre.
A jelzéskezelés során a transceiver lefoglalja a munkamenet állapotát, és visszaad egy megosztott relay VIP-t és UDP-portot az SDP-válaszban. A VIP egy virtuális IP-cím, amely a relayflotta elé kerül; a porttal együtt egyetlen stabil célcímet ad az ügyfélnek, például 203.0.113.10:3478, még akkor is, ha sok relaypéldány ül mögötte. Az ügyfél első médiás útvonalcsomagja általában egy STUN (Session Traversal Utilities for NAT) binding request, amelyet az ICE arra használ, hogy ellenőrizze: a csomagok elérik-e a hirdetett címet.
A relay csak annyit elemez ebből az első STUN-csomagból, hogy kiolvassa a szerver ufragját, dekódolja az útválasztási tippet, és továbbítsa a csomagot a tulajdonos transceiverhez. Minden transceiver egy megosztott UDP socketen figyel, vagyis egyetlen operációsrendszer-végponton, amely egy belső IP:portra van kötve, nem pedig munkamenetenként egy socketen. Miután a relay létrehozott egy munkamenetet az ügyfél forrás IP:portja és e transceivercél között, a későbbi DTLS-, RTP- és RTCP-csomagok ezen a munkameneten belül áramlanak anélkül, hogy újra dekódolni kellene az ufragot.
A relay munkamenete szándékosan minimális, mindössze egy memóriabeli munkamenetből áll, amely a csomagtovábbítást irányítja, valamint a monitorozáshoz szükséges számlálókból és a munkamenet lejáratához, illetve tisztításához szükséges időzítőkből. Ez a tervezési döntés közvetlenül a csomagútvonalon tartja az útválasztást. Ha egy relay újraindul és elveszíti a munkamenetet, a következő STUN-csomag az ufrag útválasztási tippjéből újra felépíti azt. A még nagyobb megbízhatóság érdekében egy Redis-gyorsítótár tárolja a <ügyfél IP + Port, transceiver IP + Port> leképezést, miután az útvonal létrejött, így az jóval korábban helyreállítható, még a következő STUN-csomag megérkezése előtt.
Miután a nyilvános UDP-felületet kis számú stabil címre és portra csökkentettük, ugyanezt a relaymintát globálisan is telepíthettük. A Global Relay a földrajzilag elosztott relay belépési pontjaink flottája, amelyek mind ugyanazt a csomagtovábbítási viselkedést valósítják meg.
A széles földrajzi belépési lefedettség lerövidíti az ügyfél és az OpenAI közötti első ugrást, mert egy csomag a felhasználóhoz közeli relayen keresztül léphet be a hálózatunkba, mind földrajzi, mind hálózati topológiai értelemben, ahelyett hogy először egy távoli régióig haladna át a nyilvános interneten. Gyakorlatban ez alacsonyabb késleltetést, kisebb jittert és kevesebb elkerülhető veszteségi hullámot jelent, mielőtt a forgalom elérné gerinchálózatunkat.6
A jelzéskezeléshez a Cloudflare földrajzi és közelségi irányítását használjuk, hogy a kezdeti HTTP- vagy WebSocket-kérés egy közeli transceiverklaszterhez érjen. A kérés környezete határozza meg a munkamenet helyét, és azt, melyik Global Relay belépési pontot hirdetjük az ügyfélnek. Az SDP-válasz megadja a Global Relay címét, míg az ufrag elegendő információt tartalmaz ahhoz, hogy a Global Relay a kijelölt klaszterhez irányítsa a médiát, a relay pedig a cél transceiverhez továbbítsa azt.
A földrajzilag irányított jelzéskezelés és a Global Relay együtt közel helyezi mind a beállítást, mind a médiát a belépési útvonalhoz, miközben a munkamenetet egyetlen transceiverhez rögzíti. Ez csökkenti a jelzéskezelés és az első ICE-kapcsolatellenőrzés köridejét, ami közvetlenül lerövidíti azt az időt, ameddig a felhasználónak várnia kell, mielőtt a beszéd elkezdődhet.
A relay szolgáltatást Go nyelven írtuk, és szándékosan szűkre szabtuk az implementációt. Linuxon a kernel hálózati verem fogadja az UDP-csomagokat a gép hálózati interfészéről, és eljuttatja őket egy sockethez, ahhoz az operációsrendszer-végponthoz, amelyet egy folyamat IP:Port kötése után olvas. A relay felhasználói térben fut, így egy hagyományos Go-folyamat olvassa a csomagfejléceket erről a socketről, frissít egy kis mennyiségű folyamállapotot, és továbbítja a csomagokat anélkül, hogy végződtetné a WebRTC-t. Nem volt szükségünk kernel-bypass keretrendszerre, amely lehetővé tenné, hogy egy felhasználói térben futó folyamat közvetlenül kérdezze le a hálózati sorokat a nagyobb csomagsebesség érdekében, de operatív bonyolultságot is hozna.
Fő tervezési döntések:
- Nincs protokollvégződtetés: A relay csak a STUN-fejléceket/ufragot elemzi; a későbbi DTLS-, RTP- és RTCP-forgalomhoz gyorsítótárazott állapotot használ, így a csomagok átlátszatlanok maradnak.
- Efemer állapot: Egy kisméretű, rövid időtúllépésű, memóriabeli térképet tart fenn az ügyfélcím és a transceivercél között a folyamállapothoz és a megfigyelhetőséghez.
- Horizontális skálázhatóság: Több relaypéldány fut párhuzamosan egy terheléselosztó mögött. Az állapot nem kemény WebRTC-állapot, így az újraindítások minimális forgalomkiesést és gyors folyamhelyreállást okoznak.
Hatékonysági intézkedések:
- A
SO_REUSEPORTegy Linux socketopció, amely lehetővé teszi, hogy ugyanazon a gépen több relay worker ugyanahhoz az UDP-porthoz kötődjön. A kernel ezután elosztja a bejövő csomagokat e workerek között, elkerülve az egyetlen olvasási ciklus szűk keresztmetszetét. - A
runtime.LockOSThreadminden UDP-t olvasó goroutine-t egy adott OS-szálhoz rögzít. ASO_REUSEPORT-tal együtt ez hajlamos arra, hogy ugyanazon folyamból származó csomagokat (a forrás- és cél-IP:Port plusz a protokoll) ugyanazon CPU-maghoz tartsa, javítva a gyorsítótár-localitást és csökkentve a kontextusváltást. - Az előre lefoglalt pufferek és a minimális másolás alacsonyan tartják az elemzési és allokációs többletet, hogy elkerüljük a Go szemétgyűjtését.
Ez az implementáció viszonylag kis relaylábnyommal kezelte globális valós idejű médiaforgalmunkat, ezért a kernel-bypass út helyett megtartottuk az egyszerűbb megoldást.
Ez az architektúra lehetővé teszi számunkra, hogy a WebRTC-médiát Kubernetesben futtassuk anélkül, hogy több ezer UDP-portot kellene kitenni. Ez azért fontos, mert egy kisebb és fix UDP-felületet könnyebb védeni és terheléselosztani, és lehetővé teszi az infrastruktúra skálázását nagy nyilvános porttartományok lefoglalása nélkül. A Kubernetes jobb infrastruktúra-támogatásával és a kisebb felületből adódó nagyobb biztonsággal ez a kialakítás az ügyfelek számára a szabványos WebRTC-viselkedést is megőrzi, és megerősíti, hogy a mi terhelésünkhöz az SFU nélküli kialakítás volt a helyes alapértelmezés. A legtöbb munkamenetünk pont-pont kapcsolatú, késleltetésérzékeny, és könnyebben skálázható, ha az inferenciaszolgáltatásoknak nem kell WebRTC-peerként viselkedniük.
A tágabb tanulság az, hogy a bonyolultságot legjobb egy vékony útválasztási rétegben elhelyezni, nem minden háttérszolgáltatásban, és nem egyedi ügyfélviselkedésben. Az útválasztási metaadatok egy protokollnatív mezőbe kódolása determinisztikus elsőcsomag-útválasztást, kis nyilvános UDP-lábnyomot és elegendő rugalmasságot adott ahhoz, hogy a belépési pontokat világszerte közel helyezzük a felhasználókhoz.
Néhány döntés különösen fontos volt:
- Őrizzük meg a protokoll szemantikáját a peremen. Az ügyfelek továbbra is szabványos WebRTC-t használnak, ami érintetlenül hagyja a böngészős és mobilos interoperabilitást.
- Tartsuk a nehéz munkamenet-állapotokat egy helyen. A transceiver birtokolja az ICE-t, a DTLS-t, az SRTP-t és a munkamenet életciklusát; a relay csak továbbítja a csomagokat.
- Azokra az információkra útválasszunk, amelyek már jelen vannak a beállításban. Az ICE ufrag elsőcsomag-útválasztási kapaszkodót adott anélkül, hogy forró útvonalú lekérdezési függőséget kellett volna hozzáadnunk.
- Az általános esetre optimalizáljunk, mielőtt kernel-bypasshoz nyúlnánk. Egy szűk Go-implementáció a
SO_REUSEPORT, a szálrögzítés és az alacsony allokációjú elemzés körültekintő használatával elegendő volt a terhelésünkhöz.
A valós idejű hangalapú MI csak akkor működik, ha az infrastruktúra láthatatlanná teszi a késleltetést. Számunkra ez azt jelentette, hogy megváltoztattuk a WebRTC-telepítésünk formáját anélkül, hogy megváltoztattuk volna azt, amit az ügyfelek magától a WebRTC-től elvárnak.
Szerző
References
2. GitHub - l7mp/stunner: A Kubernetes media gateway for WebRTC(új ablakban nyílik meg)
3. WebRTC Ports in a nutshell [Examples] - BlogGeek.me(új ablakban nyílik meg)
4. Deploy to Kubernetes - LiveKit docs(új ablakban nyílik meg)
6. Cloudflare Calls: millions of cascading trees all the way down(új ablakban nyílik meg)


