Hvordan OpenAI leverer stemme-AI med lav latenstid i stor skala
Af Yi Zhang og William McDonald, medlemmer af den tekniske stab
Stemme-AI føles kun naturlig, hvis samtalen bevæger sig i samme tempo som tale. Når netværket står i vejen, hører folk det straks som akavede pauser, afbrudte afbrydelser eller forsinket afbrydelse. Det er vigtigt for ChatGPT stemme, for udviklere, der bygger med Realtime API, for agenter, der arbejder i interaktive workflows, og for modeller, der skal behandle lyd, mens en bruger stadig taler.
I OpenAI’s skala bliver det til tre konkrete krav:
- Global rækkevidde til mere end 900 millioner ugentligt aktive brugere
- Hurtig forbindelsesopsætning, så en bruger kan begynde at tale, så snart en session starter
- Lav og stabil round-trip-tid for medier med lav jitter og pakketab, så skift mellem talere føles skarpt
Teamet hos OpenAI, der er ansvarligt for AI-interaktioner i realtid, har for nylig ombygget vores WebRTC-stack for at håndtere tre begrænsninger, der begyndte at kollidere i stor skala: medieterminering med én port pr. session passer dårligt til OpenAI’s infrastruktur, tilstandsfulde ICE- (Interactive Connectivity Establishment) og DTLS-sessioner (Datagram Transport Layer Security) kræver stabilt ejerskab, og global routing skal holde latenstiden på første hop lav. I dette indlæg gennemgår vi den opdelte relay plus transceiver-arkitektur, vi byggede for at bevare standard-WebRTC-adfærd for klienter, samtidig med at vi ændrede, hvordan pakker routes i OpenAI’s infrastruktur.
WebRTC er en åben standard til at sende lyd, video og data med lav latenstid mellem browsere, mobilapps og servere. Den forbindes ofte med peer-to-peer-opkald, men er også et praktisk fundament for realtidssystemer mellem klient og server, fordi den standardiserer de svære dele af interaktive medier: ICE til etablering af forbindelser og NAT-traversering (Network Address Translation), DTLS og SRTP (Secure Real-time Transport Protocol) til krypteret transport, codec-forhandling til komprimering og afkodning af lyd, RTCP (Real-time Transport Control Protocol) til kvalitetskontrol og funktioner på klientsiden som ekkoannullering og jitterbuffer.
Den standardisering er vigtig for AI-produkter. Uden WebRTC ville hver klient have brug for et forskelligt svar på, hvordan man etablerer forbindelse på tværs af NAT’er, krypterer medier, forhandler codecs (de coder-decoders, der vælges til transmission og dekomprimering) og tilpasser sig skiftende netværksforhold. Med WebRTC kan vi bygge på en protokolstack, der allerede er implementeret på tværs af browsere og mobile platforme, og fokusere vores eget arbejde på den infrastruktur, der forbinder realtidsmedier med modeller.
Vi bygger også på selve WebRTC-økosystemet, herunder modne open source-implementeringer og det standardiseringsarbejde, der holder browsere, mobilapps og servere interoperable. Grundlæggende arbejde af Justin Uberti (en af WebRTC’s oprindelige arkitekter) og Sean DuBois (skaber og vedligeholder af Pion) gjorde det muligt for teams som vores at bygge på gennemprøvet medieinfrastruktur i stedet for at genopfinde transport, kryptering og congestion control på lavt niveau. Vi er heldige, at både Justin og Sean nu er kolleger her hos OpenAI og hjælper med at vejlede, hvordan vi bringer WebRTC og AI i realtid tættere sammen.
For AI er den vigtigste egenskab, at lyd ankommer som en kontinuerlig strøm. En talt agent kan begynde at transskribere, ræsonnere, kalde værktøjer eller generere tale, mens brugeren stadig taler, i stedet for at vente på en fuld upload. Det er forskellen på et system, der føles samtalebaseret, og et, der føles som push-to-talk.
Da vi havde valgt WebRTC, var det næste spørgsmål, hvor vi skulle terminere det (hvor vi ville acceptere og eje WebRTC-forbindelsen—f.eks. ved kanten) og hvordan vi skulle forbinde disse sessioner til inferens-backenden. Terminering er vigtig, fordi den afgør, hvordan vi håndterer realtidssessionstilstand, medietransport, routing, latenstid og isolering af fejl.
En SFU, eller selective forwarding unit, er en medieserver, der modtager én WebRTC-strøm fra hver deltager og selektivt videresender strømme til de andre. I denne model terminerer SFU’en en separat WebRTC-forbindelse for hver deltager, og AI’en deltager som endnu en deltager i sessionen. Det kan passe godt til produkter, der i sagens natur har flere deltagere, såsom gruppesamtaler, klasseværelser eller samarbejdsmøder. Det samler lydcodecs, RTCP-beskeder, datakanaler, optagelse og politik pr. strøm ét sted.1
Selv i produkter mellem klient og AI er en SFU ofte det naturlige udgangspunkt, fordi den gør det muligt for teams at genbruge ét gennemprøvet system til signalering, medierouting, optagelse, observerbarhed og fremtidige udvidelser såsom overdragelse til mennesker eller tilføjelse af flere deltagere.
Vores workload er anderledes. De fleste sessioner er 1:1—én bruger, der taler med én model, eller én applikation, der taler med én agent i realtid—med latenstidsfølsomhed i hver tur. Til den form for trafik valgte vi en transceiver-model: en WebRTC-edge-tjeneste terminerer klientforbindelsen og konverterer derefter medier og hændelser til enklere interne protokoller til modelinferens, transskription, talegenerering, værktøjsbrug og orkestrering.
I dette design er transceiveren den eneste tjeneste, der ejer WebRTC-sessionstilstanden, herunder ICE-forbindelsestjek, DTLS-handshaken, SRTP-krypteringsnøgler og sessionens livscyklus. “Terminering” betyder her, at transceiveren er det endepunkt, der fuldfører disse handshakes og krypterer eller dekrypterer medierne. At holde denne tilstand samlet ét sted gjorde det lettere at ræsonnere om sessionsejerskab, og det gjorde det muligt for backendtjenester at skalere som almindelige tjenester i stedet for selv at fungere som WebRTC-peers.
Efter at have valgt transceiver-modellen var vores første implementering en enkelt Go-tjeneste bygget på Pion, som håndterede både signalering og medieterminering. Den driver ChatGPT stemme, Realtime API’s WebRTC-endepunkt og en række forskningsprojekter.
Operationelt udfører transceiver-tjenesten to opgaver:
- Signalering: SDP-forhandling, codec-valg, ICE-legitimationsoplysninger og sessionsopsætning
- Medier: Terminering af downstream-WebRTC-forbindelser og vedligeholdelse af upstream-forbindelser til backendtjenester til inferens og orkestrering
Vi ønskede, at tjenesten skulle køre som resten af vores infrastruktur: på Kubernetes, hvor workloads kan skaleres op og ned og flyttes mellem værter, når efterspørgslen ændrer sig. Men den konventionelle WebRTC-model med én port pr. session passer dårligt til det miljø, fordi den afhænger af store offentlige UDP-portintervaller, som er svære at eksponere, sikre og bevare, når pods tilføjes, fjernes eller omlægges.2
Det første problem var selve modellen med én port pr. session. Ved høj samtidighed betyder det, at man skal eksponere og administrere meget store UDP-portintervaller.
- Cloud-load balancers og Kubernetes-tjenester er ikke designet omkring titusindvis af offentlige UDP-porte pr. tjeneste. Hvert ekstra interval øger den operationelle kompleksitet i load balancer-konfiguration, helbredstjek, firewallpolitik og sikker udrulning.3
- Store UDP-portintervaller er svære at sikre, fordi de udvider den eksternt tilgængelige angrebsflade og gør netværkspolitikker sværere at auditere.
- De passer også dårligt til autoskalering. Pods tilføjes, fjernes og omlægges konstant i Kubernetes. Hvis hver pod skal reservere og annoncere et stort stabilt portinterval, bliver elasticiteten skrøbelig.4
Det er derfor, mange WebRTC-systemer bevæger sig mod én enkelt UDP-port pr. server med demultipleksering på applikationsniveau bag den port.5
Design med én port pr. server løser antallet af porte, men introducerer et andet problem: at bevare ejerskabet af hver session på tværs af en flåde.
ICE og DTLS er tilstandsfulde protokoller. Den proces, der oprettede en session, skal fortsat modtage pakkene for den session, så den kan validere forbindelsestjek, fuldføre DTLS-handshaken, dekryptere SRTP og behandle senere sessionændringer såsom ICE-genstarter. Hvis pakker for den samme session lander på en anden proces, kan opsætningen mislykkes, eller medier kan bryde sammen.
Det gav os et konkret mål: at eksponere en lille, fast UDP-flade mod det offentlige internet, mens hver pakke stadig routes til den transceiver, der ejer den tilsvarende WebRTC-session.
Vi evaluerede flere måder at nå dertil på, herunder TURN (Traversal Using Relays around NAT), hvor et edge-relay terminerer klientallokeringer og videresender trafik på deres vegne.2
Tilgang | Fordele | Ulemper |
Unik IP:port pr. session (også kendt som native direct UDP) | Direkte medievej fra klient til server Intet videresendelseslag i datastien | Kræver én offentlig UDP-port pr. session Store portintervaller er svære at eksponere og sikre Passer dårligt til Kubernetes og cloud-load balancers |
Unik IP:port pr. server | Meget mindre offentligt UDP-fodaftryk end eksponering pr. session En delt socket pr. server kan demultiplekse mange sessioner | Fungerer fint på en enkelt vært, men ikke alene på tværs af en delt load-balanceret flåde Sessionsdemultipleksering på en enkelt vært hjælper først, efter at en pakke har nået den vært; på tværs af en load-balanceret flåde kan den første pakke stadig lande på den forkerte instans, så du har stadig brug for en deterministisk måde at styre hver session til den proces, der ejer den |
TURN-relay (protokolterminerende) | Klienter behøver kun at nå TURN-relayets adresse og port Kan centralisere politik ved kanten | TURN-allokeringer tilføjer round trips ved opsætning Det er stadig svært at flytte eller gendanne allokeringer på tværs af TURN-servere |
Stateless forwarder + stateful terminator (OpenAI’s relay + transceiver) | Lille offentligt UDP-fodaftryk Transceiveren ejer stadig hele WebRTC-sessionen | Tilføjer ét videresendelseshop, før medier når den ejende transceiver Kræver brugerdefineret koordinering mellem relay og transceiver |
Den arkitektur, vi satte i drift, opdeler pakkerouting fra protokolterminering. Signalering når stadig transceiveren til sessionsopsætning, mens medier først kommer ind gennem relayet. Relayet er et letvægtslag til UDP-videresendelse med et lille offentligt fodaftryk, og transceiveren er det tilstandsfulde WebRTC-endepunkt bag det.
Relayet dekrypterer ikke medier, kører ikke ICE-state machines og deltager ikke i codec-forhandling. Det læser nok pakkemetadata til at vælge en destination og videresender derefter pakken til den transceiver, der ejer sessionen. Transceiveren ser stadig et normalt WebRTC-flow og ejer fortsat al protokoltilstand. Set fra klientens perspektiv ændrer intet sig ved WebRTC-sessionen.
Routing af den første pakke er det afgørende trin i denne opsætning. Et relay skal route den første pakke fra en klient, før der overhovedet findes en session på selve pakkestien, i stedet for at stoppe op ved en ekstern opslagstjeneste.
Hver WebRTC-session har allerede en protokolindbygget routingmekanisme: ICE username fragment, eller ufrag, en kort identifikator, der udveksles under sessionsopsætning og gentages i STUN-forbindelsestjek. Vi genererer server-side-ufrag, så den indeholder præcis nok routingsmetadata til, at relayet kan udlede destinationsklyngen og den ejende transceiver.
Under signalering allokerer transceiveren sessionstilstand og returnerer en delt relay-VIP og UDP-port i SDP-svaret. En VIP er en virtuel IP-adresse foran relayflåden; kombineret med porten giver den klienten én stabil destination, såsom `203.0.113.10:3478`, selv om mange relayinstanser ligger bag den. Klientens første pakke på mediestien er som regel en STUN-binding request, som ICE bruger til at verificere, at pakker kan nå den annoncerede adresse.
Relayet parser kun nok af den første STUN-pakke til at læse serverens ufrag, afkode routinghintet og videresende pakken til den ejende transceiver. Hver transceiver lytter på en delt UDP-socket, hvilket betyder ét operativsystem-endepunkt bundet til en intern IP:port, ikke én socket pr. session. Når relayet har oprettet en session fra klientens IP:port til denne transceiverdestination, flyder efterfølgende DTLS-, RTP- og RTCP-pakker i sessionen uden at afkode ufrag’en igen.
Relayets session er bevidst minimal og består kun af en session i hukommelsen, der informerer pakkevideresendelse, sammen med nødvendige tællere til overvågning og timere til sessionsudløb og oprydning. Dette designvalg holder pakkerouting direkte på pakkestien. Hvis et relay genstarter og mister sessionen, genopbygger den næste STUN-pakke sessionen ud fra routinghintet i ufrag’en. For at gøre det endnu mere pålideligt bruges en Redis-cache til at holde mappingen af <klient-IP + port, transceiver-IP + port>, når ruten er etableret, så den kan gendannes langt tidligere, før den næste STUN-pakke ankommer.
Da vi havde reduceret den offentlige UDP-flade til et lille antal stabile adresser og porte, kunne vi udrulle det samme relaymønster globalt. Global Relay er vores flåde af geografisk distribuerede relay-indgangspunkter, som alle implementerer den samme pakkevideresendelsesadfærd.
Bred geografisk ingress forkorter det første hop fra klient til OpenAI, fordi en pakke kan komme ind i vores netværk ved et relay tæt på brugeren, både geografisk og i netværkstopologi, i stedet for først at krydse det offentlige internet til en fjern region. I praksis betyder det lavere latenstid, mindre jitter og færre undgåelige tabsspidser, før trafikken når vores backbone.6
Vi bruger Cloudflare geo- og proximity steering til signalering, så den indledende HTTP- eller WebSocket-anmodning når en nærliggende transceiverklynge. Anmodningens kontekst bestemmer sessionens placering, og hvilket Global Relay-ingresspunkt der annonceres til klienten. SDP-svaret angiver Global Relay-adressen, mens ufrag’en indeholder tilstrækkelig information til, at Global Relay kan route medier til den udpegede klynge, og relayet kan route til destinationstransceiveren.
Sammen sørger geostyret signalering og Global Relay for, at både opsætning og medier kommer ind ad en nærliggende sti, samtidig med at sessionen forankres til én transceiver. Det reducerer round-trip-tiden for signalering og for det første ICE-forbindelsestjek, hvilket direkte forkorter, hvor længe en bruger skal vente, før tale kan begynde.
Vi skrev relaytjenesten i Go og holdt bevidst implementeringen snæver. På Linux modtager kernens netværksstack UDP-pakker fra maskinens netværksinterface og leverer dem til en socket, det operativsystem-endepunkt en proces læser fra efter at have bundet en IP:Port. Relay kører i userspace, så en almindelig Go-proces læser pakkeheadere fra denne socket, opdaterer en lille mængde flowtilstand og videresender pakker uden at terminere WebRTC. Vi havde ikke brug for nogen kernel-bypass-ramme, som ville lade en userspace-proces polle netværkskøer direkte for højere pakkerater, men også tilføre operationel kompleksitet.
Vigtige designvalg:
- Ingen protokolterminering: Relay parser kun STUN-headere/ufrag; det bruger cachet tilstand til efterfølgende DTLS, RTP og RTCP og holder dermed pakkerne opaque.
- Ephemeral tilstand: Det vedligeholder et lille kort med kort timeout i hukommelsen fra klientadresse til transceiverdestination til flowtilstand og observerbarhed.
- Horisontal skalerbarhed: Flere relayinstanser kører parallelt bag en load balancer. Tilstanden er ikke hård WebRTC-tilstand, så genstarter medfører minimalt trafiktab og hurtig gendannelse af flow.
Effektivitetstiltag:
SO_REUSEPORTer en Linux-socketindstilling, der gør det muligt for flere relay-workers på samme maskine at binde den samme UDP-port. Kernen fordeler derefter indgående pakker mellem disse workers, hvilket undgår en flaskehals i en enkelt læseløkke.runtime.LockOSThreadfastlåser hver UDP-læsende goroutine til en bestemt OS-tråd. Kombineret medSO_REUSEPORTbetyder det typisk, at pakker fra det samme flow (kilde- og destinations-IP:Port plus protokol) bliver på den samme CPU-kerne, hvilket forbedrer cachelokalitet og reducerer context switching.- Forhåndsallokerede buffere og minimal kopiering holder parsing- og allokeringsoverhead lav for at undgå garbage collection i Go.
Denne implementering håndterede vores globale realtidsmedietrafik med et relativt lille relay-fodaftryk, så vi beholdt det enklere design i stedet for at gå kernel-bypass-vejen.
Denne arkitektur gør det muligt for os at køre WebRTC-medier i Kubernetes uden at eksponere tusindvis af UDP-porte. Det er vigtigt, fordi en mindre og fast UDP-flade er lettere at sikre og load balancere, og fordi infrastrukturen kan skalere uden at reservere store offentlige portintervaller. Med bedre infrastruktursupport fra Kubernetes og mere sikkerhed på grund af mindre angrebsflade bevarer dette design også standard-WebRTC-adfærd for klienter og bekræfter, at et design uden SFU var det rette standardvalg til vores workload. De fleste af vores sessioner er punkt-til-punkt, latenstidsfølsomme og lettere at skalere, når inferenstjenester ikke behøver at opføre sig som WebRTC-peers.
Den bredere læring er, at det bedste sted at tilføje kompleksitet er i et tyndt routinglag, ikke i hver backendtjeneste og ikke i brugerdefineret klientadfærd. Indkodning af routingsmetadata i et protokolindbygget felt gav os deterministisk routing af den første pakke, et lille offentligt UDP-fodaftryk og nok fleksibilitet til at placere ingress tæt på brugere over hele verden.
Nogle få valg var især vigtige:
- Bevar protokollens semantik ved kanten. Klienter taler stadig standard-WebRTC, hvilket holder interoperabiliteten mellem browser og mobil intakt.
- Hold hård sessionstilstand samlet ét sted. Transceiveren ejer ICE, DTLS, SRTP og sessionens livscyklus; relayet videresender kun pakker.
- Route på information, der allerede er til stede under opsætning. ICE-ufrag gav os en routingmekanisme til den første pakke uden at tilføje en afhængighed af opslag på hot path.
- Optimér til det almindelige tilfælde, før I griber til kernel bypass. En snæver Go-implementering med omhyggelig brug af
SO_REUSEPORT, trådfastlåsning og parsing med lav allokering var nok til vores workload.
Stemme-AI i realtid virker kun, når infrastrukturen får latenstid til at føles usynlig. For os betød det at ændre formen på vores WebRTC-implementering uden at ændre det, klienter forventer af WebRTC selv.
Skrevet af
Referencer
2. GitHub - l7mp/stunner: En Kubernetes-mediegateway til WebRTC(åbner i et nyt vindue)
3. WebRTC-porte kort fortalt [eksempler] - BlogGeek.me(åbner i et nyt vindue)
4. Implementer på Kubernetes - LiveKit-dokumentation(åbner i et nyt vindue)
6. Cloudflare Calls: millioner af kaskaderende træer hele vejen ned(åbner i et nyt vindue)


