Comment OpenAI offre l’IA vocale à faible latence à grande échelle
Par Yi Zhang et William McDonald, membres du personnel technique
L’IA vocale semble naturelle seulement si la conversation progresse au rythme de la parole. Quand le réseau nuit à l’échange, les gens l’entendent tout de suite sous forme de pauses maladroites, d’interruptions coupées ou d’intrusions vocales retardées. C’est important pour ChatGPT Voix, pour les développeurs qui créent avec l’API Realtime, pour les agents qui travaillent dans des flux de travail interactifs, et pour les modèles qui doivent traiter l’audio pendant que l’utilisateur parle encore.
À l’échelle d’OpenAI, cela se traduit par trois exigences concrètes :
- Une portée mondiale pour plus de 900 millions d’utilisateurs actifs chaque semaine
- Un établissement de connexion rapide pour qu’un utilisateur puisse commencer à parler dès le début d’une session
- Un temps d’aller-retour média faible et stable, avec peu de gigue et de pertes de paquets, pour que les tours de parole restent fluides
L’équipe d’OpenAI responsable des interactions IA en temps réel a récemment réarchitecturé notre pile WebRTC pour répondre à trois contraintes qui commençaient à entrer en collision à grande échelle : la terminaison média avec un port par session s’intègre mal à l’infrastructure d’OpenAI, les sessions ICE (Interactive Connectivity Establishment) et DTLS (Datagram Transport Layer Security), qui sont avec état, ont besoin d’une attribution stable, et le routage mondial doit maintenir une faible latence au premier saut. Dans ce billet, nous présentons l’architecture divisée relais plus transceiver que nous avons conçue pour préserver le comportement WebRTC standard côté client tout en changeant la façon dont les paquets sont routés dans l’infrastructure d’OpenAI.
WebRTC est une norme ouverte pour transmettre de l’audio, de la vidéo et des données à faible latence entre navigateurs, applications mobiles et serveurs. On l’associe souvent aux appels pair à pair, mais c’est aussi une base pratique pour les systèmes en temps réel client-serveur, car elle normalise les parties difficiles des médias interactifs : ICE pour l’établissement de la connectivité et la traversée NAT (Network Address Translation), DTLS et SRTP (Secure Real-time Transport Protocol) pour le transport chiffré, la négociation de codecs pour compresser et décoder l’audio, RTCP (Real-time Transport Control Protocol) pour le contrôle de la qualité, ainsi que des fonctions côté client comme l’annulation d’écho et le tampon de gigue.
Cette normalisation compte pour les produits d’IA. Sans WebRTC, chaque client aurait besoin d’une réponse différente quant à la façon d’établir la connectivité à travers les NAT, de chiffrer les médias, de négocier les codecs (les codeurs-décodeurs choisis pour la transmission et la décompression) et de s’adapter à l’évolution des conditions réseau. Avec WebRTC, nous pouvons nous appuyer sur une pile de protocoles déjà mise en œuvre dans les navigateurs et les plateformes mobiles, et concentrer notre travail sur l’infrastructure qui relie les médias en temps réel aux modèles.
Nous nous appuyons aussi sur l’écosystème WebRTC lui-même, y compris des implémentations libres mûres et le travail de normalisation qui maintient l’interopérabilité entre navigateurs, applications mobiles et serveurs. Le travail fondamental de Justin Uberti (l’un des architectes d’origine de WebRTC) et de Sean DuBois (créateur et mainteneur de Pion) a permis à des équipes comme la nôtre de bâtir sur une infrastructure média éprouvée plutôt que de réinventer les comportements de bas niveau liés au transport, au chiffrement et au contrôle de congestion. Nous avons la chance que Justin et Sean soient maintenant collègues ici, chez OpenAI, où ils aident à orienter la façon dont nous rapprochons WebRTC et l’IA en temps réel.
Pour l’IA, la propriété la plus importante est que l’audio arrive sous forme de flux continu. Un agent vocal peut commencer à transcrire, raisonner, appeler des outils ou générer de la parole pendant que l’utilisateur parle encore, plutôt que d’attendre un téléversement complet. C’est la différence entre un système qui paraît conversationnel et un système qui ressemble à un bouton parler-pour-transmettre.
Une fois WebRTC choisi, la question suivante était de savoir où le terminer (où accepter et prendre en charge la connexion WebRTC — par exemple, en périphérie) et comment relier ces sessions au backend d’inférence. Le point de terminaison est important, car il détermine la façon dont nous gérons l’état des sessions en temps réel, le transport média, le routage, la latence et l’isolation des pannes.
Un SFU, ou selective forwarding unit, est un serveur média qui reçoit un flux WebRTC de chaque participant et transmet sélectivement les flux aux autres. Dans ce modèle, le SFU termine une connexion WebRTC distincte pour chaque participant, et l’IA rejoint la session comme un autre participant. Cela peut bien convenir à des produits intrinsèquement multipartites, comme les appels de groupe, les salles de classe ou les réunions collaboratives. Cela garde les codecs audio, les messages RTCP, les canaux de données, l’enregistrement et les politiques par flux au même endroit.1
Même dans les produits client-vers-IA, un SFU est souvent le point de départ par défaut, car il permet aux équipes de réutiliser un système éprouvé pour la signalisation, le routage média, l’enregistrement, l’observabilité et les extensions futures, comme le transfert à un humain ou l’ajout de participants supplémentaires.
Notre charge de travail est différente. La plupart des sessions sont 1:1 — un utilisateur qui parle à un modèle, ou une application qui parle à un agent temps réel — avec une sensibilité à la latence à chaque tour de parole. Pour cette forme de trafic, nous avons choisi un modèle de transceiver : un service WebRTC en périphérie termine la connexion client, puis convertit les médias et les événements en protocoles internes plus simples pour l’inférence de modèle, la transcription, la génération vocale, l’utilisation d’outils et l’orchestration.
Dans cette conception, le transceiver est le seul service qui possède l’état de session WebRTC, y compris les vérifications de connectivité ICE, la négociation DTLS, les clés de chiffrement SRTP et le cycle de vie de la session. Ici, « terminaison » signifie que le transceiver est l’endpoint qui termine ces négociations et chiffre ou déchiffre les médias. Garder cet état à un seul endroit a rendu l’attribution des sessions plus facile à raisonner, et cela a permis aux services backend de monter en charge comme des services ordinaires au lieu d’agir eux-mêmes comme des pairs WebRTC.
Après avoir choisi le modèle transceiver, notre première implémentation était un service Go unique fondé sur Pion qui gérait à la fois la signalisation et la terminaison média. Il alimente ChatGPT Voix, l’endpoint WebRTC de l’API Realtime et plusieurs projets de recherche.
Sur le plan opérationnel, le service de transceiver remplit deux rôles :
- Signalisation : négociation SDP, sélection des codecs, identifiants ICE et configuration de la session
- Média : terminaison des connexions WebRTC descendantes et maintien des connexions montantes vers les services backend pour l’inférence et l’orchestration
Nous voulions que le service s’exécute comme le reste de notre infrastructure : sur Kubernetes, où les charges de travail peuvent augmenter ou diminuer et se déplacer entre les hôtes selon la demande. Mais le modèle WebRTC classique avec un port par session s’adapte mal à cet environnement, parce qu’il dépend de grandes plages de ports UDP publics qui sont difficiles à exposer, à sécuriser et à préserver à mesure que des pods sont ajoutés, supprimés ou replanifiés.2
Le premier problème était le modèle lui-même, avec un port par session. À forte concurrence, cela signifie qu’il faut exposer et gérer de très grandes plages de ports UDP.
- Les équilibreurs de charge infonuagiques et les services Kubernetes ne sont pas conçus pour des dizaines de milliers de ports UDP publics par service. Chaque plage additionnelle ajoute de la complexité opérationnelle à la configuration de l’équilibreur, aux vérifications d’état, aux politiques de pare-feu et à la sécurité des déploiements.3
- Les grandes plages de ports UDP sont difficiles à sécuriser, parce qu’elles élargissent la surface accessible de l’extérieur et rendent les politiques réseau plus difficiles à auditer.
- Elles conviennent aussi mal à la mise à l’échelle automatique. Des pods sont constamment ajoutés, supprimés et replanifiés dans Kubernetes. Exiger que chaque pod réserve et annonce une grande plage de ports stable rend cette élasticité fragile.4
C’est pourquoi de nombreux systèmes WebRTC évoluent vers un seul port UDP par serveur, avec démultiplexage au niveau de l’application derrière ce port.5
Les conceptions à un seul port par serveur règlent le nombre de ports, mais elles introduisent un deuxième problème : préserver l’attribution de chaque session à travers une flotte.
ICE et DTLS sont des protocoles avec état. Le processus qui a créé une session doit continuer à recevoir les paquets de cette session pour valider les vérifications de connectivité, terminer la négociation DTLS, déchiffrer le SRTP et traiter les changements ultérieurs de la session, comme les redémarrages ICE. Si les paquets d’une même session arrivent sur un autre processus, l’établissement peut échouer ou les médias peuvent cesser de fonctionner.
Cela nous a donné un objectif précis : exposer une petite surface UDP fixe à l’internet public, tout en acheminant chaque paquet vers le transceiver qui possède la session WebRTC correspondante.
Nous avons évalué plusieurs façons d’y parvenir, y compris TURN (Traversal Using Relays around NAT), où un relais en périphérie termine les allocations client et transfère le trafic en leur nom.2
Approche | Avantages | Inconvénients |
IP:port unique par session (aussi appelé UDP direct natif) | Chemin média direct entre client et serveur Aucune couche de transfert dans le chemin de données | Nécessite un port UDP public par session Les grandes plages de ports sont difficiles à exposer et à sécuriser Convient mal à Kubernetes et aux équilibreurs de charge infonuagiques |
IP:port unique par serveur | Empreinte UDP publique beaucoup plus petite qu’une exposition par session Un socket partagé par serveur peut démultiplexer de nombreuses sessions | Fonctionne bien sur un seul hôte, mais pas à lui seul à travers une flotte partagée derrière équilibrage de charge La démultiplexion des sessions sur un seul hôte n’aide qu’après l’arrivée d’un paquet sur cet hôte; à l’échelle d’une flotte équilibrée, le premier paquet peut encore arriver sur la mauvaise instance, donc il faut quand même un moyen déterministe d’orienter chaque session vers le processus qui en est propriétaire |
Relais TURN (avec terminaison du protocole) | Les clients n’ont qu’à atteindre l’adresse et le port du relais TURN Peut centraliser les politiques en périphérie | Les allocations TURN ajoutent des allers-retours à la configuration Le déplacement ou la récupération des allocations entre serveurs TURN reste difficile |
Transfert sans état + terminaison avec état (relais + transceiver d’OpenAI) | Petite empreinte UDP publique Le transceiver reste propriétaire de l’ensemble de la session WebRTC | Ajoute un saut de transfert avant que les médias n’atteignent le transceiver propriétaire Exige une coordination personnalisée entre le relais et le transceiver |
L’architecture que nous avons déployée sépare le routage des paquets de la terminaison des protocoles. La signalisation atteint toujours le transceiver pour la configuration de la session, tandis que les médias entrent d’abord par le relais. Le relais est une couche légère de transfert UDP avec une petite empreinte publique, et le transceiver est l’endpoint WebRTC avec état derrière lui.
Le relais ne déchiffre pas les médias, n’exécute pas les machines d’état ICE et ne participe pas à la négociation des codecs. Il lit juste assez de métadonnées de paquet pour choisir une destination, puis transfère le paquet au transceiver qui possède la session. Le transceiver continue de voir un flux WebRTC normal et continue de posséder tout l’état du protocole. Du point de vue du client, rien ne change dans la session WebRTC.
Le routage du premier paquet est l’étape clé de cette configuration. Un relais doit acheminer le premier paquet d’un client avant même qu’une session existe sur le chemin de ce paquet, plutôt que de s’arrêter sur un service de recherche externe.
Chaque session WebRTC comporte déjà un crochet de routage natif au protocole : le fragment de nom d’utilisateur ICE, ou ufrag, un court identifiant échangé lors de la configuration de la session et répété dans les vérifications de connectivité STUN. Nous générons le ufrag côté serveur de manière à y intégrer juste assez de métadonnées de routage pour permettre au relais d’inférer le cluster de destination et le transceiver propriétaire.
Pendant la signalisation, le transceiver alloue l’état de session et renvoie une VIP de relais partagée ainsi qu’un port UDP dans la réponse SDP. Une VIP est une adresse IP virtuelle placée devant la flotte de relais; combinée au port, elle fournit au client une destination stable unique, comme `203.0.113.10:3478`, même si de nombreuses instances de relais se trouvent derrière. Le premier paquet sur le chemin média du client est généralement une demande de liaison STUN (Session Traversal Utilities for NAT), qu’ICE utilise pour vérifier que les paquets peuvent atteindre l’adresse annoncée.
Le relais analyse juste assez ce premier paquet STUN pour lire le ufrag serveur, décoder l’indice de routage et transférer le paquet vers le transceiver propriétaire. Chaque transceiver écoute sur un socket UDP partagé, c’est-à-dire un endpoint du système d’exploitation lié à une IP:port interne, et non un socket par session. Après que le relais a créé une session reliant l’IP:port source du client à cette destination de transceiver, les paquets DTLS, RTP et RTCP suivants circulent dans la session sans qu’il soit nécessaire de redécoder le ufrag.
La session du relais est volontairement minimale : elle se compose seulement d’une session en mémoire servant au transfert des paquets, avec les compteurs nécessaires à la surveillance et des minuteries pour l’expiration et le nettoyage des sessions. Ce choix de conception maintient le routage des paquets directement sur leur chemin. Si un relais redémarre et perd la session, le paquet STUN suivant reconstruit la session à partir de l’indice de routage contenu dans le ufrag. Pour rendre le tout encore plus fiable, un cache Redis est utilisé pour conserver la correspondance de <IP client + port, IP transceiver + port> une fois la route établie, afin qu’elle puisse être récupérée beaucoup plus tôt, avant l’arrivée du prochain paquet STUN.
Une fois la surface UDP publique réduite à un petit nombre d’adresses et de ports stables, nous avons pu déployer le même modèle de relais à l’échelle mondiale. Global Relay est notre flotte de points d’entrée relais géographiquement distribués qui appliquent tous le même comportement de transfert de paquets.
Une large présence géographique en entrée raccourcit le premier saut entre le client et OpenAI, parce qu’un paquet peut entrer dans notre réseau par un relais proche de l’utilisateur, tant sur le plan géographique que de la topologie réseau, au lieu de traverser d’abord l’internet public jusqu’à une région éloignée. En pratique, cela se traduit par une latence plus faible, moins de gigue et moins de pointes de perte évitables avant que le trafic n’atteigne notre dorsale.6
Nous utilisons la géolocalisation et l’orientation par proximité de Cloudflare pour la signalisation, de sorte que la requête HTTP ou WebSocket initiale atteigne un cluster de transceivers à proximité. Le contexte de la requête dicte l’emplacement de la session et le point d’entrée Global Relay annoncé au client. La réponse SDP fournit l’adresse Global Relay, tandis que le ufrag contient assez d’information pour permettre à Global Relay d’acheminer les médias vers le cluster désigné et au relais de les acheminer vers le transceiver de destination.
Ensemble, la signalisation géo-orientée et Global Relay placent à proximité à la fois la configuration et les médias, tout en gardant la session ancrée à un seul transceiver. Cela réduit le temps d’aller-retour pour la signalisation et pour la première vérification de connectivité ICE, ce qui raccourcit directement le temps d’attente avant qu’un utilisateur puisse commencer à parler.
Nous avons écrit le service de relais en Go et gardé volontairement une mise en œuvre ciblée. Sous Linux, la pile réseau du noyau reçoit les paquets UDP depuis l’interface réseau de la machine et les remet à un socket, l’endpoint du système d’exploitation qu’un processus lit après avoir lié une IP:port. Le relais s’exécute en espace utilisateur; un processus Go ordinaire lit donc les en-têtes de paquet depuis ce socket, met à jour une petite quantité d’état de flux et transfère les paquets sans terminer WebRTC. Nous n’avons pas eu besoin d’un cadre de contournement du noyau, qui permettrait à un processus en espace utilisateur d’interroger directement les files réseau pour obtenir des débits de paquets plus élevés, mais ajouterait aussi de la complexité opérationnelle.
Choix de conception clés :
- Aucune terminaison de protocole : le relais n’analyse que les en-têtes STUN et le ufrag; pour les paquets DTLS, RTP et RTCP suivants, il utilise l’état mis en cache, ce qui garde les paquets opaques.
- État éphémère : il maintient en mémoire une petite table à délai d’expiration court associant l’adresse client à la destination du transceiver pour l’état des flux et l’observabilité.
- Mise à l’échelle horizontale : plusieurs instances de relais s’exécutent en parallèle derrière un équilibreur de charge. L’état n’est pas un état WebRTC critique, de sorte que les redémarrages causent peu de pertes de trafic et une récupération rapide des flux.
Mesures d’efficacité :
SO_REUSEPORTest une option de socket Linux qui permet à plusieurs workers de relais sur une même machine de lier le même port UDP. Le noyau répartit ensuite les paquets entrants entre ces workers, ce qui évite un goulot d’étranglement lié à une seule boucle de lecture.runtime.LockOSThreadépingle chaque goroutine de lecture UDP à un thread spécifique du système d’exploitation. Combiné àSO_REUSEPORT, cela tend à garder les paquets d’un même flux (IP:port source et destination, plus protocole) sur le même cœur CPU, ce qui améliore la localité du cache et réduit les changements de contexte.- Des tampons préalloués et un minimum de copies gardent faibles les coûts d’analyse et d’allocation afin d’éviter le ramasse-miettes en Go.
Cette mise en œuvre a pris en charge notre trafic média temps réel mondial avec une empreinte de relais relativement petite; nous avons donc conservé cette conception plus simple plutôt que d’adopter une voie de contournement du noyau.
Cette architecture nous permet d’exécuter les médias WebRTC dans Kubernetes sans exposer des milliers de ports UDP. C’est important parce qu’une surface UDP plus petite et fixe est plus facile à sécuriser et à équilibrer, et qu’elle permet à l’infrastructure de monter en charge sans réserver de grandes plages de ports publics. Grâce à une meilleure prise en charge par Kubernetes et à une sécurité accrue liée à une surface réduite, cette conception préserve aussi le comportement WebRTC standard côté client et confirme qu’une conception sans SFU était le bon choix par défaut pour notre charge de travail. La plupart de nos sessions sont point à point, sensibles à la latence, et plus faciles à faire évoluer lorsque les services d’inférence n’ont pas à se comporter comme des pairs WebRTC.
L’enseignement plus large, c’est que le meilleur endroit pour ajouter de la complexité se trouve dans une fine couche de routage, pas dans chaque service backend, ni dans un comportement client personnalisé. En encodant des métadonnées de routage dans un champ natif au protocole, nous avons obtenu un routage déterministe du premier paquet, une petite empreinte UDP publique et assez de flexibilité pour placer les points d’entrée près des utilisateurs partout dans le monde.
Quelques choix ont été particulièrement importants :
- Préserver la sémantique du protocole en périphérie. Les clients continuent de parler un WebRTC standard, ce qui maintient l’interopérabilité avec les navigateurs et les appareils mobiles.
- Garder les états de session critiques à un seul endroit. Le transceiver possède ICE, DTLS, SRTP et le cycle de vie de la session; le relais ne fait que transférer les paquets.
- Acheminer selon l’information déjà présente lors de la configuration. Le ufrag ICE nous a fourni un crochet de routage du premier paquet sans ajouter de dépendance de recherche sur le chemin critique.
- Optimiser pour le cas le plus courant avant d’envisager le contournement du noyau. Une mise en œuvre Go ciblée, avec une utilisation soignée de
SO_REUSEPORT, de l’épinglage des threads et d’une analyse à faible allocation, suffisait pour notre charge de travail.
L’IA vocale en temps réel ne fonctionne que lorsque l’infrastructure rend la latence invisible. Pour nous, cela voulait dire changer la forme de notre déploiement WebRTC sans changer ce que les clients attendent de WebRTC lui-même.


