Passer au contenu principal
OpenAI

4 mai 2026

Ingénierie

Comment OpenAI fournit une IA vocale à faible latence à grande échelle

Par Yi Zhang et William McDonald, membres du personnel technique

L’IA vocale ne paraît naturelle que si la conversation avance au rythme de la parole. Lorsque le réseau s’interpose, les gens l’entendent immédiatement sous forme de silences gênants, d’interruptions tronquées ou de prises de parole retardées. C’est important pour ChatGPT Voix, pour les développeurs qui utilisent la Realtime API, pour les agents qui travaillent dans des workflows interactifs et pour les modèles qui doivent traiter l’audio pendant qu’un 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 hebdomadaires
  • Un établissement rapide de la connexion afin 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 perte de paquets, pour que l’alternance des tours de parole soit fluide

L’équipe d’OpenAI responsable des interactions IA en temps réel a récemment repensé notre pile WebRTC pour répondre à trois contraintes qui ont commencé à se télescoper à 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 ont un état, ont besoin d’une propriété stable, et le routage mondial doit maintenir une faible latence sur le premier saut. Dans cet article, nous présentons l’architecture scindée relay plus transceiver que nous avons conçue pour préserver le comportement WebRTC standard côté client tout en modifiant la manière dont les paquets sont routés à l’intérieur de l’infrastructure d’OpenAI.

WebRTC nous permet de créer des produits d’IA en temps réel

WebRTC est une norme ouverte pour envoyer 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 temps réel client-serveur, car elle standardise les aspects difficiles des médias interactifs : ICE pour l’établissement de la connectivité et la traversée du 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 qualité, et des fonctionnalités côté client comme l’annulation d’écho et le tampon de gigue.

Cette standardisation est importante pour les produits d’IA. Sans WebRTC, chaque client aurait besoin d’une réponse différente pour établir la connectivité à travers les NAT, chiffrer les médias, négocier les codecs (les codeurs-décodeurs sélectionnés pour la transmission et la décompression) et s’adapter aux conditions réseau changeantes. Avec WebRTC, nous pouvons nous appuyer sur une pile de protocoles déjà implémentée dans les navigateurs et les plateformes mobiles, et concentrer notre propre travail sur l’infrastructure qui relie les médias temps réel aux modèles.

Nous nous appuyons également sur l’écosystème WebRTC lui-même, notamment des implémentations open source matures et le travail de normalisation qui maintient l’interopérabilité entre navigateurs, applications mobiles et serveurs. Les travaux fondamentaux de Justin Uberti (l’un des architectes originels de WebRTC) et de Sean DuBois (créateur et mainteneur de Pion) ont permis à des équipes comme la nôtre de s’appuyer sur une infrastructure média éprouvée plutôt que de réinventer le transport bas niveau, le chiffrement et les mécanismes de contrôle de congestion. Nous avons la chance que Justin et Sean soient désormais nos collègues chez OpenAI, où ils aident à orienter la manière 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, au lieu 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 à du push-to-talk.

Choisir une architecture média

Une fois WebRTC retenu, la question suivante était de savoir où le terminer (où nous accepterions et prendrions 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 manière dont nous gérons l’état des sessions temps réel, le transport média, le routage, la latence et l’isolation des défaillances.

Option 1 : l’approche SFU inclut l’IA comme participant WebRTC

Un SFU, ou unité de transfert sélectif, 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 aux produits intrinsèquement multipartites, comme les appels de groupe, les salles de classe ou les réunions collaboratives. Cela regroupe en un seul endroit les codecs audio, les messages RTCP, les canaux de données, l’enregistrement et les politiques par flux.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é unique pour la signalisation, le routage média, l’enregistrement, l’observabilité et de futures extensions comme le transfert vers un humain ou l’ajout de participants supplémentaires.

Option 2 : l’approche transceiver termine WebRTC à la périphérie et le convertit en protocole backend

Notre charge de travail est différente. La plupart des sessions sont en 1:1 — un utilisateur parlant à un modèle, ou une application parlant à un agent en 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 événements en protocoles internes plus simples pour l’inférence du modèle, la transcription, la génération vocale, l’usage d’outils et l’orchestration.

Dans cette conception, le transceiver est le seul service à posséder 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 réalise ces négociations et chiffre ou déchiffre les médias. Garder cet état en un seul endroit a rendu la propriété de la session plus facile à raisonner, et a permis aux services backend de monter en charge comme des services ordinaires au lieu de se comporter eux-mêmes comme des pairs WebRTC.

Le problème central de déploiement : WebRTC rencontre Kubernetes

Après avoir choisi le modèle transceiver, notre première implémentation était un service Go unique construit sur Pion qui gérait à la fois la signalisation et la terminaison média. Il alimente ChatGPT Voix, l’endpoint WebRTC de la Realtime API et plusieurs projets de recherche.

Sur le plan opérationnel, le service transceiver remplit deux fonctions :

  • Signalisation : négociation SDP, sélection du codec, 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 fonctionne comme le reste de notre infrastructure : sur Kubernetes, où les charges de travail peuvent monter et descendre en charge, et se déplacer d’un hôte à l’autre selon la demande. Mais le modèle WebRTC classique avec un port par session s’adapte mal à cet environnement, car il dépend de larges plages de ports UDP publics difficiles à exposer, à sécuriser et à préserver à mesure que des pods sont ajoutés, supprimés ou replanifiés.2

Épuisement des ports

Le premier problème venait du modèle un-port-par-session lui-même. À forte concurrence, cela signifie qu’il faut exposer et gérer de très grandes plages de ports UDP.

  • Les équilibreurs de charge cloud et les services Kubernetes ne sont pas conçus autour de dizaines de milliers de ports UDP publics par service. Chaque plage supplémentaire ajoute de la complexité opérationnelle dans la configuration de l’équilibreur de charge, les vérifications d’état, la politique de pare-feu et la sécurité des déploiements.3
  • Les grandes plages de ports UDP sont difficiles à sécuriser, car elles élargissent la surface accessible depuis l’extérieur et rendent les politiques réseau plus difficiles à auditer.
  • Elles s’adaptent également mal à l’autoscaling. Les 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 port UDP unique par serveur, avec un démultiplexage au niveau applicatif derrière ce port.5

Affinité de l’état

Les conceptions à un seul port par serveur résolvent le problème du nombre de ports, mais elles introduisent un second problème : préserver la propriété de chaque session à l’échelle d’une flotte.

ICE et DTLS sont des protocoles à état. Le processus qui a créé une session doit continuer à recevoir les paquets de cette session pour pouvoir valider les vérifications de connectivité, terminer la négociation DTLS, déchiffrer SRTP et traiter les modifications de session ultérieures comme les redémarrages ICE. Si les paquets d’une même session arrivent sur un autre processus, la configuration peut échouer ou le média peut se dégrader.

Cela nous a donné un objectif précis : exposer à l’internet public une surface UDP petite et fixe, tout en acheminant chaque paquet vers le transceiver propriétaire de la session WebRTC correspondante.

Comparaison des architectures média WebRTC

Nous avons évalué plusieurs moyens d’y parvenir, notamment TURN (Traversal Using Relays around NAT), où un relay en périphérie termine les allocations client et transmet le trafic en leur nom.2

Approche

Avantages

Inconvénients

IP:port unique par session (également appelée UDP direct natif)

Chemin média direct du client au serveur

Pas de couche de transfert dans le chemin des données

Nécessite un port UDP public par session

Les grandes plages de ports sont difficiles à exposer et à sécuriser

Peu adapté à Kubernetes et aux équilibreurs de charge cloud

IP:port unique par serveur

Empreinte UDP publique bien plus réduite 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 sur une flotte partagée derrière un équilibrage de charge

Le démultiplexage des sessions sur un seul hôte n’aide qu’après qu’un paquet a atteint cet hôte ; sur une flotte équilibrée en charge, le premier paquet peut toujours arriver sur la mauvaise instance, il faut donc encore un moyen déterministe d’orienter chaque session vers le processus qui en est propriétaire


Relay TURN (avec terminaison de protocole)

Les clients n’ont besoin d’atteindre que l’adresse et le port du relay TURN

Peut centraliser les politiques à la périphérie

Les allocations TURN ajoutent des allers-retours lors de la configuration

Déplacer ou récupérer des allocations entre serveurs TURN reste difficile

Transmetteur sans état + terminateur avec état (relay + transceiver d’OpenAI)

Petite empreinte UDP publique

Le transceiver conserve la maîtrise de toute la session WebRTC

Ajoute un saut de transfert avant que le média n’atteigne le transceiver propriétaire

Nécessite une coordination personnalisée entre relay et transceiver

Vue d’ensemble de l’architecture : relay + transceiver

L’architecture que nous avons déployée sépare le routage des paquets de la terminaison de protocole. La signalisation continue d’atteindre le transceiver pour la configuration de la session, tandis que le média entre d’abord par le relay. Le relay est une couche légère de transfert UDP avec une petite empreinte publique, et le transceiver est l’endpoint WebRTC à état situé derrière.

Relay transfère les paquets au transceiver sans état

Le relay ne déchiffre pas les médias, n’exécute pas de machines d’état ICE et ne participe pas à la négociation des codecs. Il lit juste assez de métadonnées de paquets pour choisir une destination, puis transmet le paquet au transceiver propriétaire de la session. Le transceiver continue à voir un flux WebRTC normal et à posséder tout l’état du protocole. Du point de vue du client, rien ne change dans la session WebRTC.

Routage à partir des identifiants ICE

Le routage du premier paquet est l’étape clé de cette configuration. Un relay doit pouvoir acheminer le premier paquet d’un client avant même qu’une session n’existe sur le chemin des paquets lui-même, plutôt que de marquer une pause pour consulter un service de recherche externe.

Chaque session WebRTC dispose déjà d’un mécanisme de routage natif au protocole : le fragment de nom d’utilisateur ICE, ou ufrag, un identifiant court échangé pendant 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 à ce qu’il contienne juste assez de métadonnées de routage pour permettre au relay de déduire le cluster de destination et le transceiver propriétaire.

Le diagramme de séquence montre comment la connexion est établie

Pendant la signalisation, le transceiver alloue l’état de la session et renvoie une VIP relay partagée et un port UDP dans la réponse SDP. Une VIP est une adresse IP virtuelle placée devant la flotte de relay ; combinée au port, elle donne au client une destination unique et stable, comme `203.0.113.10:3478`, même si de nombreuses instances relay se trouvent derrière. Le premier paquet sur le chemin média du client est généralement une requête STUN (Session Traversal Utilities for NAT) de type binding, qu’ICE utilise pour vérifier que les paquets peuvent atteindre l’adresse annoncée.

Relay 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. Une fois que le relay crée une session à partir de l’IP:port source du client vers cette destination transceiver, les paquets DTLS, RTP et RTCP suivants circulent dans la session sans redécoder le ufrag.

La session du relay est volontairement minimale : elle se compose uniquement d’une session en mémoire pour informer le transfert des paquets, ainsi que des compteurs nécessaires pour la supervision et des temporisateurs pour l’expiration et le nettoyage des sessions. Ce choix de conception maintient le routage des paquets directement sur le chemin des paquets. Si un relay redémarre et perd la session, le paquet STUN suivant reconstruit la session à partir de l’indice de routage du ufrag. Pour rendre cela encore plus fiable, un cache Redis est utilisé pour conserver le mappage de <IP client + Port, IP transceiver + Port> une fois la route établie, afin qu’il puisse être récupéré bien plus tôt, avant l’arrivée du prochain paquet STUN.

Global Relay et signalisation orientée géographiquement

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 relay à l’échelle mondiale. Global Relay est notre flotte de points d’entrée relay répartis géographiquement, qui appliquent tous le même comportement de transfert de paquets.

Une large couverture géographique des points d’entrée raccourcit le premier saut client-OpenAI, car un paquet peut entrer dans notre réseau via un relay proche de l’utilisateur, à la fois en termes de géographie et de topologie réseau, au lieu de traverser d’abord l’internet public jusqu’à une région lointaine. En pratique, cela signifie une latence plus faible, moins de gigue et moins de rafales de perte évitables avant que le trafic n’atteigne notre backbone.6

La couche Global Relay reçoit les paquets du client et les transmet au cluster de transceivers

Nous utilisons la géolocalisation et le steering de proximité de Cloudflare pour la signalisation, de sorte que la requête HTTP ou WebSocket initiale atteigne un cluster transceiver proche. Le contexte de la requête détermine 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 suffisamment d’informations pour permettre à Global Relay d’acheminer le média vers le cluster désigné et au relay de le router vers le transceiver de destination.

Ensemble, la signalisation orientée géographiquement et Global Relay placent la configuration et le média sur un chemin d’entrée proche 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.

Implémentation et performances du relay

Nous avons écrit le service relay en Go et avons volontairement limité son implémentation. Sous Linux, la pile réseau du noyau reçoit les paquets UDP depuis l’interface réseau de la machine et les délivre à un socket, l’endpoint du système d’exploitation qu’un processus lit après avoir lié une IP:Port. Relay s’exécute en espace utilisateur, donc un processus Go ordinaire lit les en-têtes de paquets 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 framework de contournement du noyau, qui permettrait à un processus en espace utilisateur d’interroger directement les files réseau pour des débits de paquets plus élevés, mais qui ajouterait aussi de la complexité opérationnelle.

Choix de conception clés :

  • Pas de terminaison de protocole : Relay n’analyse que les en-têtes STUN/ufrag ; pour les paquets DTLS, RTP et RTCP suivants, il s’appuie sur un état en cache, en gardant les paquets opaques.
  • État éphémère : Il maintient une petite table en mémoire à expiration courte, associant l’adresse client à la destination transceiver pour l’état de flux et l’observabilité.
  • Scalabilité horizontale : Plusieurs instances relay s’exécutent en parallèle derrière un équilibreur de charge. L’état n’est pas un état WebRTC dur, donc les redémarrages entraînent des pertes de trafic minimes et une récupération rapide des flux.

Mesures d’efficacité :

  • SO_REUSEPORT est une option de socket Linux qui permet à plusieurs workers relay sur la 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 sur 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 (l’IP:Port source et destination, plus le protocole) sur le même cœur CPU, ce qui améliore la localité du cache et réduit les changements de contexte.
  • Des buffers préalloués et un minimum de copies maintiennent faibles les coûts d’analyse et d’allocation afin d’éviter la collecte des déchets en Go.

Cette implémentation a pris en charge notre trafic média temps réel mondial avec une empreinte relay relativement réduite, nous avons donc conservé cette conception plus simple plutôt que d’adopter une approche avec contournement du noyau.

Résultats et enseignements

Cette architecture nous permet d’exécuter les médias WebRTC sur Kubernetes sans exposer des milliers de ports UDP. C’est important, car une surface UDP plus petite et fixe est plus facile à sécuriser et à équilibrer, et permet à l’infrastructure de monter en charge sans réserver de grandes plages de ports publics. Avec un meilleur support infra de Kubernetes et plus de sécurité grâce à une surface réduite, cette conception préserve également 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 quand les services d’inférence n’ont pas besoin de se comporter comme des pairs WebRTC.

L’enseignement plus large est que le meilleur endroit où ajouter de la complexité est une fine couche de routage, pas dans chaque service backend, ni dans un comportement client personnalisé. L’encodage de métadonnées de routage dans un champ natif du protocole nous a donné un routage déterministe du premier paquet, une petite empreinte UDP publique et suffisamment de flexibilité pour placer des points d’entrée proches 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 à parler un WebRTC standard, ce qui préserve l’interopérabilité entre navigateurs et mobiles.
  • Conserver les états de session complexes en un seul endroit. Le transceiver possède ICE, DTLS, SRTP et le cycle de vie de la session ; relay ne fait que transférer les paquets.
  • Router à partir d’informations déjà présentes lors de la configuration. Le ufrag ICE nous a donné un mécanisme de routage du premier paquet sans ajouter de dépendance de consultation sur le chemin critique.
  • Optimiser pour le cas le plus fréquent avant de recourir au contournement du noyau. Une implémentation Go ciblée, avec un usage soigné de SO_REUSEPORT, de l’épinglage des threads et d’une analyse à faible allocation, a suffi pour notre charge de travail.

L’IA vocale en temps réel ne fonctionne que lorsque l’infrastructure rend la latence imperceptible. Pour nous, cela signifiait changer la forme de notre déploiement WebRTC sans modifier ce que les clients attendent de WebRTC lui-même.