OpenAI

2 novembre 2026

Ingénierie

Le passage à l’échelle de Git chez OpenAI

Créer une gestion de versions à l’échelle des agents tout en investissant dans l’écosystème Git.

Par Aaron Friel, Taylor Blau et Ted Nyman, membres de l’équipe technique

Chargement...

Au cours de l’année écoulée, la taille de notre monodépôt et les sollicitations auxquelles il est soumis ont augmenté à un rythme vertigineux. Il était clair que nous devions refondre notre système de gestion de versions pour l’adapter au volume de travail simultané que Codex rend possible.

Nous avons développé Pando, un nouveau backend pour les dépôts Git, sans interrompre le travail d’ingénierie ni modifier la façon dont notre équipe utilise Git. Pando prend désormais en charge l’ensemble du développement dans le monodépôt d’OpenAI. Pando est conçu pour atteindre deux objectifs ambitieux : d’abord, permettre aux écritures d’évoluer horizontalement, en les optimisant pour des push simultanés vers de nombreuses branches. Ensuite, maximiser le débit de lecture, pour que les clients puissent télécharger les données assez vite pour saturer toute leur bande passante réseau disponible.

Nous avons atteint ces objectifs tout en conservant la compatibilité avec le protocole et le client Git, permettant à nos ingénieurs comme à nos agents d’intégrer Pando en toute transparence à leurs flux de travail existants. En production, Pando a divisé la latence médiane des push par près de cinq. Il distribue désormais près d’un pétaoctet de données Git par jour.

Nous présentons ici nos motivations pour créer un nouveau système de gestion de versions, ainsi que les raisons pour lesquelles nous avons choisi de rester compatibles avec Git. Nous comparerons notre système aux approches courantes du passage à l’échelle de Git, en expliquant pourquoi Pando est particulièrement à même de répondre aux besoins d’OpenAI. Nous aborderons aussi la suite et la façon dont nous continuerons à contribuer à Git pour prendre en charge le travail à l’échelle des agents.

Pourquoi il nous fallait un nouveau système de gestion de versions

Codex a changé l’échelle et le rythme du développement chez OpenAI, permettant aux ingénieurs d’accomplir beaucoup plus de travail en parallèle. Cela signifie davantage de branches, de push, de fusions, de récupérations et de clonages depuis les environnements de développement, les tâches de recherche et l’intégration continue (CI).

Ces sollicitations se cumulent. Un ingénieur a désormais plusieurs modifications prêtes pour la CI en même temps, et chaque push peut lancer plusieurs tâches qui récupèrent le même commit.

Vers la fin de l’année dernière, les systèmes prenant en charge notre monodépôt ont commencé à peiner à suivre le rythme. Nous avons subi des récupérations et des push lents, ainsi que des échecs de fusion, avec une fiabilité tombant parfois sous les 90 %. Avions-nous atteint une limite de Git lui-même, ou de la façon dont nous mettions notre monodépôt à disposition ? Git est né d’une communauté qui échangeait par e-mail des correctifs pour le noyau. Depuis plus de vingt ans, les développeurs adaptent leurs outils autour de Git à mesure que leurs pratiques de développement évoluent. Lorsque nous avons commencé à rencontrer des difficultés dans notre propre monodépôt, il nous importait de pouvoir continuer à nous appuyer sur ce travail.

Nous avons entrevu un moyen de conserver ce flux de travail familier tout en reconstruisant le backend pour traiter en parallèle les push vers différentes branches et fournir le même historique à de nombreux lecteurs simultanément. En fondant notre flux de gestion de versions sur Git, les améliorations du client pourraient être utiles au-delà d’OpenAI. Nous souhaitions proposer au projet d’origine ces améliorations issues de notre copie de Git⁠(ouverture dans une nouvelle fenêtre), afin d’en faire profiter toute la communauté.

Concevoir Git pour le passage à l’échelle

Le passage à l’échelle d’un système de gestion de versions comporte deux dimensions essentielles : de nombreux clients écrivent sur différentes branches, et de nombreux lecteurs récupèrent le même historique. Pando optimise les deux.

Premièrement, Pando coordonne les mises à jour de chaque branche indépendamment. Les mises à jour de branches indépendantes peuvent se dérouler en parallèle, et un lecteur qui récupère une branche obtient l’historique nécessaire à cette révision. En contrepartie, un lecteur qui consulte plusieurs branches peut les voir à des instants différents.

Deuxièmement, Pando réutilise les fichiers pack (le format de Git pour compresser plusieurs objets ensemble) d’une récupération à l’autre, au lieu de générer un fichier pack propre à chaque client. Un client peut télécharger certains objets qu’il possède déjà, mais le serveur peut fournir les mêmes octets à de nombreux clients avec peu de calculs.

Ces choix nous permettent de répartir les écritures entre les branches et de servir les lectures depuis des caches. Pour comprendre comment ces choix s’articulent, commençons par la façon dont Git stocke et transfère l’historique.

Les bases du stockage d’objets Git

Git doit pouvoir synchroniser deux composantes d’un dépôt : les objets et les références.

Un dépôt Git enregistre les instantanés des fichiers et l’historique qui les relie sous forme de graphe d’objets. Un objet peut être un blob, un arbre, un commit ou un tag. Les blobs contiennent le contenu d’un fichier individuel, et les arbres associent des noms à des blobs ou à d’autres arbres pour former une arborescence de répertoires. Les commits pointent vers un arbre racine et zéro, un ou plusieurs commits parents, formant ainsi l’historique de votre dépôt.

Les objets sont identifiés par une empreinte de hachage de leur type et de leur contenu. Les objets sont également immuables : enregistrer un fichier modifié crée un nouveau blob, tandis que les fichiers et répertoires inchangés peuvent réutiliser des objets existants.

Les branches et les tags sont des références, ou « refs », qui pointent vers des commits. Effectuer un commit sur une branche crée un nouveau commit et met à jour la référence de la branche pour qu’elle pointe vers lui.

La branche refs/heads/main pointe vers le commit C2, dont le parent est C1. C1 pointe vers l’arbre racine T1, où parser.py renvoie au blob B1. C2 pointe vers l’arbre racine T2, où parser.py renvoie au blob B2. Dans les deux arbres racines, docs/ renvoie au même arbre, T3. Son entrée README.md pointe vers le blob B3, et usage.md pointe vers le blob B4.

Les objets Git se référencent par leur identifiant : les commits pointent vers des arbres, et les arbres vers d’autres arbres et des blobs. La branche refs/heads/main pointe vers le commit C2, dont le parent est C1. C1 pointe vers l’arbre racine T1, où parser.py renvoie au blob B1. C2 pointe vers l’arbre racine T2, où parser.py renvoie au blob B2. Dans les deux arbres racines, docs/ renvoie au même arbre, T3. Son entrée README.md pointe vers le blob B3, et usage.md pointe vers le blob B4.

Git stocke les objets à l’état « non compacté » ou « compacté ». Les objets non compactés sont stockés dans des fichiers individuels identifiés par leur identifiant d’objet. Les objets compactés sont stockés ensemble dans un fichier pack, ce qui permet de stocker les objets similaires sous forme de deltas les uns par rapport aux autres.

Deux versions de parser.py apparaissent sous $GIT_DIR/objects. À l’état non compacté, le blob A contient parse(), et le blob B contient à la fois parse() et parse_file(). Le fichier pack stocke A en entier et représente B par des instructions qui copient tout A et y ajoutent parse_file(). Le fichier *.idx associé localise les deux blobs dans le fichier pack par leur identifiant d’objet.

Git peut stocker les blobs dans des fichiers distincts ou les regrouper dans un pack, en encodant un blob comme un delta par rapport à un autre.

Dans Git, un objet est accessible depuis un commit si Git peut le trouver en suivant les liens de ce commit à travers ses parents, ses arbres et ses blobs. Lors d’une récupération, le client indique au serveur les commits qu’il souhaite obtenir et ceux qu’il possède déjà. Le serveur trouve les objets accessibles depuis les commits demandés, exclut ceux dont il sait que le client dispose déjà, puis envoie le reste dans un fichier pack au même format que celui utilisé par Git sur disque. Un clonage complet démarre sans aucun objet et nécessite donc tous les objets accessibles depuis l’historique à cloner.

Un push transfère les objets dans l’autre sens en demandant au serveur de mettre à jour une ou plusieurs références. Le client utilise l’ensemble des références annoncées par le serveur pour éviter de lui envoyer des objets qu’il possède déjà. Le pack envoyé peut donc dépendre d’objets existants : recevoir le pack seul ne suffit pas à reconstituer le nouvel historique.

Répliquer un dépôt Git consiste à maintenir les objets et les références disponibles sur plusieurs serveurs. Avant qu’une réplique puisse fournir la nouvelle valeur d’une référence, elle doit être capable de fournir les objets accessibles depuis cette valeur. Le service doit aussi coordonner les mises à jour de références concurrentes, c’est-à-dire les push qui tentent de faire passer une même branche de la même ancienne tête à de nouveaux commits différents. Les systèmes qui imposent un ordre unique à toutes les écritures d’un dépôt peuvent obliger les push vers des branches indépendantes à s’attendre mutuellement.

Architectures existantes de services Git distribués

Lors de la conception de Pando, nous avons évalué des approches classiques du passage à l’échelle de la gestion de versions avec Git : l’une coordonne les mises à jour de références entre répliques du dépôt par une validation en trois phases (3PC, un protocole de coordination d’une validation sur plusieurs nœuds) ; l’autre ordonne les mises à jour via un journal d’écriture anticipée partagé (WAL, qui consigne les modifications avant leur application) dans un stockage de blobs.

Validation en trois phases (3PC)

Dans l’architecture de validation en trois phases que nous avons modélisée, le service Git conserve une copie complète du dépôt sur plusieurs répliques. Lorsqu’un serveur reçoit un push, le fichier pack entrant est copié et envoyé en parallèle à toutes les répliques. Pour publier le push, une majorité des répliques doit accepter la mise à jour de la référence. Le serveur qui gère cette opération est le coordinateur, et plusieurs échanges sont nécessaires entre lui et chacune des autres répliques.

Pour accepter une mise à jour de référence, chaque réplique verrouille sa copie de la référence et vérifie qu’elle pointe toujours vers le commit attendu par le client. Le coordinateur envoie ensuite un message de prévalidation aux répliques qui acceptent. Une fois qu’une majorité accuse réception du message de prévalidation, le coordinateur leur demande d’effectuer la mise à jour. Toute réplique à jour peut alors annoncer la nouvelle tête de branche depuis sa copie locale du dépôt.

Conserver des répliques complètes permet de servir les lectures localement et garantit aux clients une vue cohérente de toutes les références, quelle que soit la réplique choisie. Par conséquent, les références sont mises à jour une par une, et chaque push nécessite plusieurs allers-retours pour coordonner la mise à jour de référence entre les répliques.

L’attente d’un quorum fait dépendre le traitement des push des performances disque des répliques et de la latence de communication. À mesure que le volume d’écritures augmente, les répliques doivent suivre le rythme des packs entrants tout en traitant le trafic de lecture.

Journal d’écriture anticipée (WAL)

Dans l’architecture fondée sur un WAL que nous avons modélisée, le service Git utilise un manifeste partagé dans un stockage de blobs pour ordonner les mises à jour à l’échelle du dépôt. Lorsqu’un serveur reçoit un push, il téléverse le fichier pack entrant dans un stockage de blobs et prépare la mise à jour de référence dans sa copie locale du dépôt. Pour publier le push, il ajoute au manifeste partagé conservé dans le stockage de blobs une entrée qui consigne la mise à jour de référence et pointe vers le fichier pack. Le serveur Git met ensuite à jour le manifeste à l’aide d’une opération de comparaison et échange. Cette opération remplace atomiquement le manifeste ou rejette la modification. Si un autre client l’a mis à jour entre-temps, le serveur lit les entrées ajoutées dans l’intervalle, vérifie que sa mise à jour de référence reste valide et réessaie avec le fichier pack déjà téléversé.

Les clients peuvent téléverser leurs fichiers pack en parallèle, mais même les push vers des branches indépendantes nécessitent de mettre à jour le même manifeste. Le temps aller-retour d’une opération réussie de comparaison et échange limite la fréquence à laquelle le dépôt peut publier un nouveau manifeste. Un stockage de blobs à faible latence rend cette étape rapide. Le traitement par lots permet à un manifeste de mettre à jour plusieurs références à la fois, au prix d’une mise en file d’attente des mises à jour et d’une latence accrue.

Le stockage de blobs peut recevoir des fichiers pack en parallèle, mais les mises à jour du manifeste partagé restent en concurrence. Augmenter la seule capacité du stockage de blobs ne résout pas ce goulot d’étranglement. Chaque réplique doit toujours lire et indexer les packs depuis le stockage de blobs avant de pouvoir les fournir localement. Si les écritures entrantes dépassent ce rythme, les répliques prennent du retard et les clients attendent plus longtemps pour récupérer les commits nouvellement envoyés.

Concevoir l’architecture de Pando

Ce qui nous amène à Pando. Pando est une nouvelle architecture de mise à disposition de dépôts Git, conçue pour prendre en charge de nombreux clients effectuant des push simultanément. Voici comment elle fonctionne :

Repenser les contraintes

Avant de développer Pando, nous pressentions que nous pouvions répondre à nos besoins sans faire de compromis sur Git, mais il nous fallait revoir certaines exigences que la plupart des systèmes Git tiennent pour acquises :

  • Cohérence à l’échelle du dépôt : Fournir un instantané cohérent de toutes les références à un instant donné exige une coordination à l’échelle du dépôt. Dans un système qui ordonne globalement toutes les mises à jour de références, les push vers des branches indépendantes se disputent un même point de coordination.

  • Fichiers pack propres à chaque client : Si deux clients ayant des historiques locaux différents récupèrent la même branche, le serveur peut devoir générer un pack différent pour chacun afin de ne pas envoyer des objets qu’ils possèdent déjà. La création de ces réponses consomme du processeur et des entrées/sorties disque, ce qui complique la prise en charge d’un grand nombre de lecteurs.

Pando propose plutôt un modèle de cohérence assoupli (la cohérence causale) pour faire passer les écritures à l’échelle, et réutilise les fichiers pack pour faire de même avec les lectures.

Cohérence causale (CC)

Lorsqu’un client effectue un push vers une branche, Pando stocke le fichier pack entrant sous forme de blob immuable dans un stockage de blobs. À chaque avancée de la branche, Pando enregistre dans un stockage de métadonnées une version de référence contenant la nouvelle tête, le pack qui l’a fournie et toutes les versions de référence antérieures dont dépend ce pack. Le stockage de blobs et le stockage de métadonnées peuvent tous deux évoluer horizontalement.

Récupérer un commit ou une référence depuis Pando nécessite de rejouer un ensemble de versions de référence (fichiers pack) côté client, pour reconstituer l’état du client qui a créé la version de référence récupérée. Cela offre une forme de cohérence plus faible à l’échelle du dépôt, tout en garantissant la sérialisabilité des mises à jour de chaque référence. Les clients n’entrent en conflit que lorsqu’ils effectuent simultanément un push vers la même référence. Un client qui lit plusieurs branches ne peut pas supposer que ces lectures décrivent un même instant à l’échelle du dépôt.

À gauche, trois packs successifs de la branche principale contiennent les commits A, B et C ; D et E ; puis F. À droite, la branche principale et la branche de travail divergent en C. La branche principale avance jusqu’à G, tandis que la branche de travail passe par D, E et F. Le dernier pack de la branche de travail contient D, E et F. Un contour en pointillés indique que D et E figurent aussi dans le pack précédent de la branche de travail. La flèche bleue indique la dépendance du nouveau pack envers la branche principale en G. Le parent Git de F est E.

Un push vers la branche principale ajoute un pack incrémental contenant un ou plusieurs commits. Chaque push vers une branche secondaire inclut à nouveau les commits précédents de cette branche, afin qu’un client disposant de la version annoncée de la branche principale puisse récupérer la branche secondaire en un seul pack.

Il s’agit d’une forme de cohérence causale. Pour récupérer une référence, le lecteur observe l’historique du client qui l’a produite. Lors d’un push, Pando contrôle l’historique commun sur lequel le client et le serveur s’accordent, afin que les clients produisent des fichiers pack plus faciles à partager.

Par conséquent, Pando ne fournit ni un état de toutes les références à un instant donné pendant les opérations Git, ni un ordre des mises à jour de références au sein d’un clonage ou d’une récupération. Concrètement, supposons que les branches foo et bar soient toutes deux mises à jour pendant la négociation d’un clonage complet. Pendant la négociation, un client peut observer foo en version 1 et bar en version 2, tandis qu’un autre observe foo en version 2 et bar en version 1. En pratique, les utilisateurs et les agents ne dépendent pas de l’ordre global des mises à jour de références, mais de l’ordre des mises à jour d’une référence donnée.

Comparaison des approches

Dans cette simulation des trois modèles de cohérence, nous configurons 1 000 clients pour qu’ils sélectionnent chacun une branche distincte, puis y envoient des commits uniques de façon répétée. Nous mesurons la durée totale écoulée et la latence entre push et récupération. Cette dernière est une métrique importante pour les charges de travail, car elle mesure le temps aller-retour nécessaire aux utilisateurs et aux agents pour collaborer ou utiliser d’autres systèmes reposant sur la gestion de versions. La simulation modélise des machines virtuelles et des systèmes de stockage de blobs allant du standard au haut de gamme. Dans cette simulation, nous avons assoupli la cohérence à l’échelle du dépôt pour la validation en trois phases.

Simulating Git Server Implementations

Follow each writer through repeated pushes, and separately measure when an independent reader can check out each commit.

Three-phase commit

Ready
Simulated time
0 s
Mean push to fetch
—
Writers done
0 / 1,000

Each square represents one writer. All selected architectures run the same workload. Squares lighten as pushes are acknowledged and turn blue when a writer finishes. The chart shows writers finished over simulated time; independent readers can continue fetching afterward. Use arrow keys to select a writer, Home or End to jump, and Escape to clear.

0 / 10,000 pushes acknowledged · 0 fetched

Select a square to inspect that writer.

Object-store WAL

Ready
Simulated time
0 s
Mean push to fetch
—
Writers done
0 / 1,000

Each square represents one writer. All selected architectures run the same workload. Squares lighten as pushes are acknowledged and turn blue when a writer finishes. The chart shows writers finished over simulated time; independent readers can continue fetching afterward. Use arrow keys to select a writer, Home or End to jump, and Escape to clear.

0 / 10,000 pushes acknowledged · 0 fetched

Select a square to inspect that writer.

Pando

Ready
Simulated time
0 s
Mean push to fetch
—
Writers done
0 / 1,000

Each square represents one writer. All selected architectures run the same workload. Squares lighten as pushes are acknowledged and turn blue when a writer finishes. The chart shows writers finished over simulated time; independent readers can continue fetching afterward. Use arrow keys to select a writer, Home or End to jump, and Escape to clear.

0 / 10,000 pushes acknowledged · 0 fetched

Select a square to inspect that writer.

Writer progress through verified fetches

  • Three-phase commit
  • Object-store WAL
  • Pando
05001,00000 s
  • Waiting
  • Pushing
  • All pushes acknowledged
0 sLater

Synthetic teaching models, not production benchmarks. Mean push to fetch covers topic pushes, from the first attempt through retries until an independent reader verifies that commit. It includes waiting for a reader slot and excludes builds and tests. With merges enabled, writer completion includes the required fetches and trunk merge.

Experiment settings

Preparing simulation…

Results

Play the comparison to see the recorded results.

Synthetic teaching models, not production benchmarks. Mean push to fetch covers topic pushes, from the first attempt through retries until an independent reader verifies that commit. It includes waiting for a reader slot and excludes builds and tests. With merges enabled, writer completion includes the required fetches and trunk merge.

Lancez cette simulation pour comparer Pando à deux autres approches. Choisissez l’infrastructure, du matériel standard au haut de gamme, le nombre de clients et d’autres paramètres pour voir comment ces approches se comparent.

Le système de validation en trois phases modélisé présente une latence d’écriture nettement plus élevée sous charge lorsque ses disques saturent. Les systèmes à journal d’écriture anticipée présentent, eux, une latence de lecture nettement plus élevée pour la même raison, les serveurs peinant à suivre le rythme de lecture du journal. Dans nos tests réels sur notre monodépôt comme dans cette simulation, Pando prend en charge plusieurs milliers de clients capables d’effectuer des push simultanément sans dégrader les performances sous charge.

Lors du traitement d’un push, Pando valide le fichier pack entrant tout en le transmettant en flux au stockage de blobs, sans le décompacter dans un dépôt local. Lorsqu’un client lance une récupération, Pando sélectionne les packs stockés dont le lecteur a besoin, puis celui-ci les télécharge. Aucun serveur n’a besoin d’appliquer les fichiers pack à un dépôt local ni d’effectuer de coûteuses opérations de décompactage et de compactage. Comme ils effectuent principalement la négociation et la sélection des fichiers pack, les serveurs Pando peuvent fonctionner sur du matériel standard.

Pando peut ainsi monter en charge jusqu’aux limites des clients, du réseau qui les relie à Pando et du service de stockage de blobs. Avec une capacité suffisante, les clients travaillant sur des branches indépendantes peuvent chacun transférer des packs aussi vite que leur connexion le permet. Pour servir davantage de clients à ce débit, nous pouvons ajouter des serveurs Pando et augmenter en parallèle la capacité des stockages de métadonnées et de blobs.

Pushing and Fetching a Branch in Pando

Pando stores the writer’s packfile in blob storage and records the ref version in metadata service. On fetch, Pando selects the packs the reader needs, and Express serves them.

Stream the push

  • Packfile bytes
  • Metadata
  • Request / acknowledgment
  • Packfile URIs

Un agent modifie des fichiers, crée un commit localement et exécute git push, ce qui crée un fichier pack de ces modifications pour mettre à jour une branche. Pando reçoit le pack et le transmet en flux au stockage de blobs. Après avoir validé le push entrant par un décodage en une seule passe, Pando utilise une transaction multiclés pour enregistrer une version de référence dans notre stockage de métadonnées et mettre à jour atomiquement le pointeur de référence. Pando peut ainsi garantir la cohérence de « lecture de ses propres écritures ». Cette opération d’écriture complète nécessite O(1)O(1) lectures et écritures de métadonnées.

Lorsqu’un autre client récupère la branche, Pando sélectionne un ensemble de packs stockés en fonction de l’historique dont ce client dispose déjà. Cette opération nécessite O(log⁡Δ)O(\log \Delta) lectures de métadonnées, Δ\Delta étant la distance la plus courte, en versions de référence, entre ce que le client possède et ce qu’il demande. Le serveur renvoie les URI des fichiers pack, et le client les télécharge via Express, un service qui stocke les fichiers pack dans des caches régionaux.

Cette conception repose sur les trois services centraux de l’architecture de Pando : les push, les récupérations et le compactage.

Faire passer les push à l’échelle

Lorsqu’un client effectue un push, un serveur Git classique se comporte comme un joueur de poker qui montre toutes ses cartes, révélant au client toutes les branches qu’il possède. Le client rassemble ensuite les commits, les arbres et les blobs pour créer un fichier pack qui exclut les objets dont il sait que le serveur dispose déjà.

Annoncer davantage de références peut réduire la taille du push. Cela peut aussi rendre le pack plus difficile à réutiliser, car un autre client peut ne pas posséder les objets que le client émetteur a omis.

Pando adopte une approche différente. Au lieu de dévoiler tout son jeu, il n’annonce au client qu’un ensemble choisi de branches. Et ce, pour deux raisons :

  1. Fournir une vue cohérente de toutes les références coûte cher. Avec des millions de branches, la seule annonce des références peut dépasser 100 Mio. Pour la fournir, chaque serveur doit soit maintenir une table de correspondance à jour, avec un important surcoût de coordination, soit effectuer une requête en base de données dont le coût augmente avec le nombre de références.

  2. En ne fournissant au client qu’un sous-ensemble des références, on l’amène à générer un pack qui ne dépend que de ce que le serveur l’autorise explicitement à exclure. Le suivi des dépendances permet à Pando de réutiliser le fichier pack généré par un client lors d’un push en le fournissant en réponse à une récupération ultérieure.

Plus un serveur Git annonce de références, plus les clients qui effectuent un push disposent de sources permettant d’exclure des objets et de produire des packs plus petits. Toutefois, réutiliser ces fichiers pack exige que le serveur puisse prouver que les clients qui les récupèrent possèdent chacune des dépendances. Sinon, le fichier pack peut omettre des objets dont le client a besoin.

À l’inverse, si un serveur Git n’annonce aucune référence, le client génère un fichier pack contenant tout l’historique du dépôt. Ce fichier pack peut être utilisé par n’importe quel autre client, mais au prix élevé de transformer, dans les faits, toute récupération de branche en clonage complet.

Courbe montrant l’effet des références annoncées sur la taille du fichier pack : historique complet sans référence, puis annonce de la branche principale, jusqu’aux seuls nouveaux objets lorsque toutes les références sont annoncées.

Annoncer davantage de références produit des fichiers pack plus petits. L’essentiel de cette réduction peut être obtenu en n’en annonçant qu’une seule.

Par défaut, lorsqu’un client envoie une nouvelle branche de fonctionnalité, Pando n’annonce comme base qu’une branche principale désignée. L’annonce garde ainsi la même taille à mesure que le dépôt gagne des branches, mais cela implique des compromis. Lorsqu’un ingénieur effectue de nombreux push vers une même branche ou empile une branche de fonctionnalité sur une autre, le client doit renvoyer tout ce qui ne figure pas dans la branche principale.

Les push successifs vers une branche sont donc plus volumineux, contiennent des objets potentiellement redondants et consomment davantage de bande passante côté client. Dans notre monodépôt, la taille médiane des fichiers pack produits de cette façon était inférieure à 20 Kio, et le 99e percentile était inférieur à 3 Mio. Pour nous, c’était un compromis judicieux, car un lecteur disposant déjà de l’historique annoncé de la branche principale peut récupérer la branche avec ce seul fichier pack.

Après avoir choisi les références à annoncer au client, Pando accepte le push et met à jour son stockage de métadonnées. Pando divise la mise à jour des métadonnées en une partie synchrone et une partie asynchrone. La mise à jour synchrone des métadonnées garantit que les clients peuvent immédiatement récupérer le commit nouvellement mis à jour. Elle écrit une nouvelle version de référence et fait pointer la référence vers cette version, en stockant la nouvelle tête, les dépendances utilisées pour négocier le fichier pack et un lien vers le fichier pack envoyé.

Parallèlement, des tâches asynchrones sont enregistrées dans la même transaction, comme la planification du compactage. Pour les push très volumineux, seul un sous-ensemble des commits est écrit de façon synchrone. Cela nous permet de ne nécessiter qu’un nombre constant de lectures et d’écritures dans le stockage de métadonnées, quelle que soit la taille du push.

Point important : les serveurs Pando peuvent accepter indépendamment les push vers des références sans lien entre elles. La seule clé soumise à contention est le pointeur de référence, que nous protégeons par une opération atomique de type comparaison et échange. Nous pouvons augmenter le débit d’écriture en ajoutant des serveurs Pando, des unités de capacité au stockage de métadonnées ou des stockages de blobs.

Récupérations et clonages au débit maximal du réseau

Lorsqu’un client effectue une récupération, il fournit au serveur deux ensembles d’identifiants de commits, désignés dans le protocole par have et want. Un serveur Git classique calcule la différence entre les ensembles d’objets accessibles depuis les commits have et ceux accessibles depuis les commits want, compresse les objets concernés et envoie le résultat au client. Bien que le serveur Git open source dispose de divers index locaux pour accélérer cette opération, ces fichiers pack peuvent être coûteux à calculer et se prêtent généralement mal à la mise en cache.

Pando utilise plutôt les correspondances de commits enregistrées lors des push pour rechercher les have et les want du client et les associer à un ensemble de versions de référence contenant ces commits. Chacune de ces versions de référence renvoie vers un fichier pack précédemment reçu par Pando. Le serveur s’appuie sur ces informations pour déterminer les besoins du client et sélectionner les fichiers pack qui couvrent l’ensemble des objets nécessaires.

Planning fetches in Pando

Pando compares what a client has with what it wants, then selects the stored packfiles needed—including any missing branch dependencies.

Already heldFetchMissing branch dependency

Fetch 27 trunk packs.

Select a branch to include its trunk dependencies.

Pando associe les messages have et want du protocole Git à une séquence de versions de référence. Demander une branche secondaire implique de demander aussi l’historique jusqu’à ses dépendances sur la branche principale.

Le protocole Git n’autorise l’envoi que d’un seul fichier pack dans la réponse HTTP d’un clonage ou d’une récupération. Toutefois, une fonctionnalité méconnue du protocole Git, packfile-uri, permet à un serveur de renvoyer des liens pour télécharger séparément des packs supplémentaires. Pando utilise exclusivement cette fonctionnalité et transmet un fichier pack vide dans la réponse. Pour prendre en charge cette fonctionnalité, OpenAI a contribué au projet d’origine en apportant plusieurs améliorations à packfile-uri.

Les systèmes d’OpenAI fonctionnent dans de nombreuses régions géographiques ; nous maintenons donc des clusters Pando dotés de caches locaux dans chacune d’elles. Express, un service de distribution de packs, met en cache notre stockage de blobs, tandis qu’un service distinct met en cache le stockage de métadonnées. Ces caches permettent à Pando de répondre aux requêtes portant sur un identifiant de commit ou à une lecture spéciale de référence entièrement à partir de l’état mis en cache dans ce cluster. Nous utilisons le hachage cohérent pour acheminer les requêtes de fichiers pack vers les serveurs Express, avec un taux de succès du cache supérieur à 99 %.

Ce système de caches offre un débit extrêmement élevé, capable de saturer la capacité réseau entre les clients et Pando. Grâce à ces URI de fichiers pack et aux caches locaux, nous avons confirmé qu’avec de simples machines virtuelles standard de fournisseurs cloud, les opérations de clonage et de récupération saturent des liaisons réseau à 10 Gbit/s.

Compactage et délai jusqu’au premier octet

Pour la plupart des serveurs Git, le recompactage et le nettoyage des objets inutilisés sont nécessaires pour maintenir les performances d’un dépôt en croissance. Ces deux étapes libèrent de l’espace, mais consomment beaucoup de calcul et d’entrées/sorties.

Dans un dépôt évoluant rapidement comme le monodépôt d’OpenAI, il est irréaliste d’envoyer une URI de fichier pack distincte pour chaque push survenu depuis la dernière récupération. Chaque jour, un client devrait télécharger des dizaines de milliers de fichiers pack pour mettre à jour sa branche principale.

Pando combine plutôt les packs en arrière-plan pour réduire ce surcoût, selon une conception proche du compactage dans les arbres de fusion structurés en journal (LSM). Chaque push produit ce que Pando appelle un fichier pack « L0 », la plus petite unité de données qu’il peut transmettre. Au fil du temps, des processus en arrière-plan rassemblent ces fichiers pack en couvertures englobant une séquence de versions de référence, dont la taille croît géométriquement à partir de L0. Aujourd’hui, notre monodépôt utilise des couvertures de fichiers pack allant jusqu’au niveau L4.

Ces couvertures remplissent un double rôle. Lorsqu’une couverture est suffisamment remplie, un processus de compactage la prend en charge et produit un fichier pack unique. Cela améliore le taux de compression en réorganisant les objets à l’intérieur du fichier pack. Mais cela améliore aussi les performances de négociation des récupérations en jouant le rôle de liste à sauts sur les versions de référence. En parcourant simultanément les index de chaque niveau, Pando peut sélectionner en temps logarithmique un ensemble de packs à envoyer à un client. Ces couvertures de fichiers pack, recompactées ou non, permettent de planifier plus efficacement les récupérations et réduisent les exigences de débit comme de latence pour Pando.

Compaction in Pando

Small packfiles are grouped into larger covers across levels, improving compression and speeding up fetches.

Compaction in progressPublished cover

Le compactage combine en arrière-plan les fichiers pack de nombreuses versions de référence, permettant des récupérations plus efficaces avec moins de fichiers.

Contrairement à de nombreux serveurs Git, les fichiers pack sont conservés à chaque niveau, sans être supprimés ni éliminés par le nettoyage. Les packs de chaque niveau servent les clients en continu, en fonction des have et des want qu’ils indiquent lors d’une récupération. Pour les clients qui utilisent Pando comme une API, lire un objet quelconque dans un fichier pack nécessite généralement de lire moins de données dans un L0 que dans un L4, car il a moins de chances d’être compressé.

Le compactage dans Pando réduit nettement le volume total transféré aux clients et permet au coût de planification des packs de croître de manière sous-linéaire par rapport au retard du client. C’est particulièrement important pour les grands dépôts qui évoluent rapidement, lesquels devraient selon nous devenir la norme dans le développement logiciel.

Développer Pando

Nous avons développé Pando avec Codex, du lancement du projet à la mise en production, en seulement 10 semaines. Le cœur de Pando a été conçu par un seul ingénieur, implémenté par des équipes d’agents et déployé par une poignée d’ingénieurs. Pando a commencé à traiter le trafic de production en avril de cette année et fait partie d’un ensemble plus vaste d’outils de gestion de versions utilisés chez OpenAI.

Nous avons employé des approches très variées pour valider la conception de Pando. Nous avons appliqué nos principes pour construire des fondations logicielles plus sûres⁠. Comme nous partions de zéro, nous avons choisi d’implémenter Pando en Rust et sans dépendances héritées. Nous avons écrit une suite de tests de conformité pour que les agents puissent vérifier leurs modifications par rapport au comportement standard de Git. Nous avons aussi développé des harnais de test par simulation déterministe pour vérifier les invariants et les propriétés du système, afin de détecter et d’atténuer des conditions de concurrence subtiles ainsi que des régressions dans la négociation et la planification des packs.

De nombreux modèles ont contribué à ce système ; à mesure que de nouveaux modèles devenaient disponibles, nous les avons utilisés pour améliorer les performances et la fiabilité de Pando. Avec GPT‑6 Astra, des agents ont écrit du Lean et du TLA+ pour formaliser une « théorie de Pando », que nous avons utilisée pour détecter et corriger des bugs dans l’implémentation et démontrer des propriétés du système. Plusieurs sessions se sont déroulées en continu pendant des centaines d’heures pour détecter et corriger des cas limites dans Pando. Nous faisons aussi tourner des agents jour et nuit pour soumettre Git et Pando à des tests adversariaux à l’aide de modèles dotés des capacités Daybreak.

Déployer Pando

Nous avons d’abord déployé Pando comme proxy relayant les lectures et les écritures vers notre système de gestion de versions existant. Cela nous a permis de synchroniser Pando et l’ancien système, afin de transférer le trafic vers Pando en toute sécurité et sans interruption.

Dans le cadre de son mécanisme de cache, Pando peut aussi dédupliquer et combiner les requêtes avant de les relayer. Nous avons ainsi pu maintenir nos outils existants en bon état de fonctionnement tout en migrant les systèmes vers Pando un à un. À mesure que nous migrions davantage de charges de travail vers Pando, leurs push alimentaient son cache et amélioraient la fiabilité de notre ancien backend.

Pour optimiser les performances avec Pando, nous avons commencé à déployer notre distribution publique de Git sur les machines des développeurs et dans l’application de bureau ChatGPT. Pour migrer les clients vers Pando par étapes, nous avons utilisé une surcouche interne de l’interface en ligne de commande de Git. Nous avons créé de nombreux autres outils pour gérer les demandes de modification et la revue de code, ainsi que des API d’automatisation. En parallèle du trafic Git, nous avons migré des centaines d’intégrations qui dépendaient d’anciennes API, de webhooks et de téléchargements de dépôts.

Codex lui-même a aidé les développeurs et les équipes à effectuer cette transition. Nous avons créé des compétences pour préparer les copies de travail existantes et guider les agents dans l’utilisation du nouveau système. Les développeurs pouvaient ainsi simplement demander à Codex de migrer leurs environnements de développement et leurs services.

Le passage à Pando a amélioré la fiabilité des fusions et réduit la latence médiane des push de près de 80 %.

Pando peut distribuer un pétaoctet de données Git par jour avec seulement une poignée de cœurs de processeur. Pando a distribué plus de 57 téraoctets de packs Git en une seule heure, dont plus de 99 % des octets provenaient du cache disque local.

La réutilisation des packs permet à de nombreux clients de télécharger le même historique de dépôt avec peu de calculs sur les nœuds qui le distribuent. Cet exemple porte sur un seul dépôt, mais Pando est conçu pour en prendre en charge bien davantage. À mesure que la demande augmente, nous pouvons faire évoluer indépendamment la distribution des données et la négociation Git.

Contribuer en retour au projet Git

Développer Pando à partir du protocole et du client Git nous a aidés à identifier des bugs et des pistes d’amélioration des performances de Git lui-même.

Par exemple, Pando fournit des packs réutilisables directement aux clients ; leur téléchargement et leur indexation sont donc devenus essentiels pour rendre le système rapide et fiable. Ce travail a révélé des cas limites liés aux téléchargements simultanés, au nettoyage après des transferts interrompus et à la reprise de téléchargements déjà terminés. Nous avons corrigé ces chemins d’exécution, ajouté des validations lors de l’acceptation d’un pack existant pour une récupération et proposé ces modifications au projet d’origine.

Nous avions aussi besoin de savoir où passaient le temps et la bande passante. Nous avons donc ajouté le traçage des téléchargements packfile-uri et des compteurs d’octets pour la génération des packs, afin de déterminer plus facilement si une récupération lente passait son temps à générer les packs ou à les transférer.

Dans la version de Git d’OpenAI⁠(ouverture dans une nouvelle fenêtre), le client peut télécharger et indexer des packs indépendants en parallèle. Nous avons ajouté le concept de « have explicites » pour qu’un serveur puisse annoncer la tête actuelle d’une branche séparément de l’historique qu’un client effectuant un push peut omettre de son pack. Sur Pando, cela permet aux clients d’utiliser --force-with-lease pour vérifier qu’une branche de fonctionnalité n’a pas bougé et construire leur pack uniquement à partir de la branche principale. Tout client disposant de cet historique de la branche principale peut alors récupérer la branche avec le même pack.

Plusieurs de ces améliorations figurent déjà dans le projet Git d’origine, et beaucoup d’autres dans notre dépôt public. Nous continuons à travailler avec la communauté Git pour rendre ces améliorations utiles au-delà de Pando. Vous pouvez suivre ce travail dans notre outil public de suivi des correctifs⁠(ouverture dans une nouvelle fenêtre).

Et ensuite ?

En repensant les idées reçues sur le passage à l’échelle des dépôts Git, Pando nous permet de faire évoluer notre monodépôt sans modifier les flux de travail Git que nos ingénieurs et nos agents utilisent déjà. Dans notre monodépôt, Pando a permis un plus grand parallélisme, amélioré la fiabilité des fusions et réduit le temps nécessaire à la publication des modifications de code. Au-delà de notre monodépôt, nous explorons aussi d’autres usages de Pando au sein d’OpenAI, notamment pour des charges de travail où de nombreux agents doivent collaborer sur un dépôt Git partagé.

À mesure que les ingénieurs et les agents produisent davantage de logiciels, d’autres équipes en dehors d’OpenAI se heurteront aux limites que nous avons rencontrées en matière de gestion de versions. Nous étudions actuellement des moyens de partager avec nos utilisateurs la technologie qui rend Pando possible.

Pando s’appuie sur deux décennies d’expérience des utilisateurs et des opérateurs de dépôts Git. Nous continuerons à développer Pando tout en travaillant avec la communauté open source de Git pour partager les améliorations utiles à l’ensemble des utilisateurs de Git.

Auteur

OpenAI