Rendere Git scalabile in OpenAI
Creare un sistema di controllo di versione alla scala degli agenti, investendo nell'ecosistema Git.
Nell'ultimo anno, le dimensioni del nostro monorepo e le richieste a cui deve rispondere sono cresciute a un ritmo vertiginoso. Era chiaro che dovevamo rinnovare il nostro sistema di controllo di versione per gestire la quantità di lavoro simultaneo resa possibile da Codex.
Abbiamo creato Pando, un nuovo backend per i repository Git, senza interrompere il lavoro degli ingegneri né cambiare il modo in cui il nostro team usa Git. Oggi Pando supporta tutte le attività di sviluppo nel monorepo di OpenAI. Pando è progettato per raggiungere due obiettivi ambiziosi: il primo è rendere le scritture scalabili orizzontalmente, ottimizzandole per push simultanei su molti branch. Il secondo è massimizzare la capacità di elaborazione in lettura, affinché i client possano scaricare dati abbastanza velocemente da saturare tutta la banda di rete disponibile.
Abbiamo raggiunto questi obiettivi mantenendo la compatibilità con il protocollo e il client Git, consentendo ai nostri ingegneri e agenti di integrare Pando senza difficoltà nei flussi di lavoro esistenti. In produzione, Pando ha ridotto la latenza mediana dei push a quasi un quinto. Ora distribuisce quasi un petabyte di dati Git al giorno.
Qui spiegheremo le ragioni che ci hanno spinto a creare un nuovo sistema di controllo di versione e perché abbiamo scelto di mantenere la compatibilità con Git. Confronteremo il nostro sistema con gli approcci più comuni alla scalabilità di Git, spiegando cosa rende Pando particolarmente adatto a soddisfare le esigenze di scalabilità di OpenAI. Parleremo anche dei prossimi passi e di come continueremo a contribuire a Git per supportare il lavoro alla scala degli agenti.
Codex ha cambiato la scala e il ritmo dello sviluppo in OpenAI, permettendo agli ingegneri di svolgere molto più lavoro in parallelo. Questo significa più branch e push, più merge e più fetch e clone da ambienti di sviluppo, job di ricerca e sistemi di integrazione continua (CI).
Queste richieste si amplificano a vicenda. Ora un singolo ingegnere ha più modifiche pronte per la CI contemporaneamente, e ogni push può avviare diversi job che eseguono il fetch dello stesso commit.
Verso la fine dell'anno scorso, i sistemi a supporto del nostro monorepo hanno iniziato a faticare a tenere il passo. Abbiamo riscontrato fetch e push lenti e merge non riusciti, con un'affidabilità che a volte scendeva sotto il 90%. Avevamo raggiunto un limite di Git stesso o del modo in cui gestivamo l'accesso al nostro monorepo? Git è nato da una community che si scambiava patch per il kernel via email. Per oltre due decenni, gli sviluppatori hanno adattato i propri strumenti attorno a Git mentre cambiava il loro modo di creare software. Quando abbiamo iniziato a incontrare difficoltà nel nostro monorepo, per noi era importante poter continuare a costruire su quel lavoro.
Abbiamo individuato un modo per mantenere quel flusso di lavoro familiare, ricostruendo il backend per gestire in parallelo i push su branch diversi e fornire la stessa cronologia a molti client in lettura contemporaneamente. Basare il nostro flusso di controllo di versione su Git significava che i miglioramenti al client avrebbero potuto essere utili anche al di fuori di OpenAI. Volevamo contribuire al progetto originale con i miglioramenti della nostra copia di Git(si apre in una nuova finestra), condividendoli con l'intera community.
Rendere scalabile un sistema di controllo di versione comporta due aspetti essenziali: molti client in scrittura che eseguono push su branch diversi e molti client in lettura che recuperano la stessa cronologia. Pando ottimizza entrambi.
In primo luogo, Pando coordina gli aggiornamenti di ciascun branch in modo indipendente. Gli aggiornamenti a branch indipendenti possono procedere in parallelo, e un client che esegue il fetch di un branch riceve la cronologia necessaria per quella revisione. Il compromesso è che un client che legge più branch può vederli in stati relativi a momenti diversi.
In secondo luogo, Pando riutilizza i packfile (il formato di Git per comprimere più oggetti insieme) tra i fetch, anziché generare un packfile specifico per ciascun client. Un client potrebbe scaricare alcuni oggetti che possiede già, ma il server può fornire gli stessi byte a molti client con un carico di calcolo ridotto.
Queste scelte ci permettono di rendere scalabili le scritture su più branch e di servire le letture dalle cache. Per capire come si combinano questi aspetti, partiamo da come Git archivia e trasferisce la cronologia.
Git deve poter mantenere sincronizzate due parti di un repository: oggetti e riferimenti.
Un repository Git registra le istantanee dei file e la cronologia che le collega sotto forma di un grafo di oggetti. Un oggetto può essere un blob, un albero, un commit o un tag. I blob contengono i dati di un singolo file, mentre gli alberi associano nomi a blob o ad altri alberi per formare una struttura di directory. I commit puntano a un albero radice e a zero o più commit genitori, formando la cronologia del repository.
Gli oggetti sono identificati da un hash del loro tipo e contenuto. Gli oggetti sono anche immutabili: registrare un file modificato crea un nuovo blob, mentre i file e le directory invariati possono riutilizzare gli oggetti esistenti.
Branch e tag sono riferimenti, o “ref”, che puntano ai commit. Eseguire un commit su un branch crea un nuovo commit e aggiorna il riferimento del branch affinché punti a esso.
Gli oggetti Git si riferiscono gli uni agli altri tramite i rispettivi ID: i commit agli alberi, gli alberi ad altri alberi e ai blob. Il branch refs/heads/main punta al commit C2, il cui genitore è C1. C1 punta all'albero radice T1, in cui parser.py fa riferimento al blob B1. C2 punta all'albero radice T2, in cui parser.py fa riferimento al blob B2. In entrambi gli alberi radice, docs/ fa riferimento allo stesso albero, T3. La sua voce README.md punta al blob B3 e usage.md punta al blob B4.
Git archivia gli oggetti in forma “sciolta” o “impacchettata”. Gli oggetti sciolti sono archiviati in file individuali identificati dal rispettivo ID oggetto. Gli oggetti impacchettati sono archiviati insieme in un packfile, che permette di memorizzare oggetti simili come differenze, o delta, l'uno rispetto all'altro.
Git può archiviare i blob come file separati oppure impacchettarli insieme, codificando un blob come delta rispetto a un altro.
In Git, un oggetto è raggiungibile da un commit se Git può trovarlo seguendo i collegamenti da quel commit attraverso i suoi genitori, alberi e blob. Durante un fetch, il client comunica al server quali commit desidera e quali possiede già. Il server trova gli oggetti raggiungibili dai commit richiesti, esclude quelli che sa essere già presenti sul client e invia i restanti come packfile nello stesso formato usato da Git su disco. Un clone completo parte senza alcun oggetto, quindi richiede tutti gli oggetti raggiungibili dalla cronologia da clonare.
Un push trasferisce gli oggetti nella direzione opposta, chiedendo al server di aggiornare uno o più riferimenti. Il client usa l'insieme dei riferimenti annunciati dal server per evitare di inviare oggetti che il server possiede già. Il pack inviato può quindi dipendere da oggetti esistenti: ricevere il solo pack non basta a ricostruire la nuova cronologia.
Replicare un repository Git significa mantenere disponibili sia gli oggetti sia i riferimenti su più server. Prima che una replica possa fornire il nuovo valore di un riferimento, deve essere in grado di fornire gli oggetti raggiungibili da quel valore. Il servizio deve anche coordinare gli aggiornamenti concorrenti dei riferimenti, cioè i push che tentano di spostare lo stesso branch dal medesimo commit di punta precedente a nuovi commit diversi. I sistemi che impongono un unico ordine a tutte le scritture su un repository possono costringere i push su branch indipendenti ad attendersi a vicenda.
Durante la progettazione di Pando, abbiamo valutato gli approcci convenzionali per rendere scalabile il controllo di versione con Git: uno coordina gli aggiornamenti dei riferimenti tra le repliche del repository tramite il commit a tre fasi (3PC, un protocollo che coordina un commit su più nodi); l'altro ordina gli aggiornamenti tramite un write-ahead log condiviso (WAL, un registro delle modifiche scritto prima che vengano applicate) in un archivio di blob.
Nell'architettura basata sul commit a tre fasi che abbiamo modellato, il servizio Git mantiene una copia completa del repository su diverse repliche. Quando un qualsiasi server riceve un push, il packfile in arrivo viene copiato e inviato a tutte le repliche in parallelo. Per pubblicare il push, la maggioranza delle repliche deve accettare l'aggiornamento del riferimento. Il server che gestisce il processo è il coordinatore, e sono necessari più cicli di comunicazione tra esso e ciascuna delle altre repliche.
Per accettare l'aggiornamento di un riferimento, ogni replica blocca la propria copia del riferimento e verifica che punti ancora al commit atteso dal client. Il coordinatore invia quindi un messaggio di pre-commit alle repliche che accettano. Quando la maggioranza conferma il messaggio di pre-commit, il coordinatore comunica loro di eseguire l'aggiornamento. Qualsiasi replica aggiornata può quindi annunciare il nuovo commit di punta del branch dalla propria copia locale del repository.
Mantenere repliche complete permette di servire le letture localmente e garantisce ai client una vista coerente di tutti i riferimenti, indipendentemente dalla replica scelta. Di conseguenza, i riferimenti vengono aggiornati uno alla volta, e ogni push richiede più scambi di andata e ritorno per coordinare l'aggiornamento del riferimento tra le repliche.
L'attesa di un quorum fa sì che le prestazioni dei dischi delle repliche e la latenza di comunicazione incidano sul tempo del push. Con l'aumentare del volume di scritture, le repliche devono tenere il passo sia con i pack in arrivo sia con il traffico di lettura.
Nell'architettura basata su WAL che abbiamo modellato, il servizio Git usa un manifesto condiviso nell'archivio di blob per ordinare gli aggiornamenti dell'intero repository. Quando un server riceve un push, carica il packfile in arrivo in un archivio di blob e prepara l'aggiornamento del riferimento nella propria copia locale del repository. Per pubblicare il push, aggiunge una voce a un manifesto condiviso, memorizzato nell'archivio di blob, che registra l'aggiornamento del riferimento e punta al packfile. Il server Git aggiorna quindi il manifesto con un'operazione compare-and-swap. Questa sostituisce atomicamente il manifesto oppure rifiuta la modifica. Se un altro client in scrittura lo ha aggiornato nel frattempo, il server legge le voci intervenute, verifica che il proprio aggiornamento del riferimento sia ancora valido e riprova usando il packfile già caricato.
I client in scrittura possono caricare i propri packfile in parallelo, ma anche i push su branch indipendenti richiedono l'aggiornamento dello stesso manifesto. Il tempo di andata e ritorno di un'operazione compare-and-swap riuscita limita la frequenza con cui il repository può pubblicare un nuovo manifesto. Un archivio di blob a bassa latenza rende questo passaggio rapido. Il raggruppamento in batch permette a un manifesto di aggiornare più riferimenti contemporaneamente, al costo di accodare gli aggiornamenti e aumentare la latenza.
L'archivio di blob può ricevere packfile in parallelo, ma gli aggiornamenti al manifesto condiviso continuano a contendersi l'accesso. Aumentare soltanto la capacità dell'archivio di blob non risolve questo collo di bottiglia. Ogni replica deve comunque leggere e indicizzare i pack dall'archivio di blob prima di poterli fornire localmente. Se le scritture arrivano più velocemente di quanto queste operazioni possano essere eseguite, le repliche accumulano ritardo e i client devono aspettare più a lungo per recuperare i commit appena inviati.
Ed eccoci a Pando. Pando è una nuova architettura per servire repository Git, progettata per scalare fino a gestire molti client che eseguono push simultaneamente. Ecco come funziona:
Prima di sviluppare Pando, intuivamo di poter soddisfare le nostre esigenze senza rinunciare a Git, ma dovevamo scendere a compromessi su alcuni aspetti che la maggior parte dei sistemi Git considera requisiti imprescindibili:
Coerenza dell'intero repository: fornire un'istantanea coerente di tutti i riferimenti in un dato momento richiede un coordinamento a livello di repository. In un sistema che ordina insieme tutti gli aggiornamenti dei riferimenti, i push su branch indipendenti si contendono un unico punto di coordinamento.
Packfile specifici per ogni client: se due client con cronologie locali diverse eseguono il fetch dello stesso branch, il server potrebbe dover generare un pack diverso per ciascuno, per evitare di inviare oggetti già presenti. Generare queste risposte consuma risorse CPU e I/O su disco, rendendo difficile gestire un numero elevato di client in lettura.
Pando, invece, offre un modello di coerenza meno restrittivo (la coerenza causale) per rendere scalabili le scritture e riutilizza i packfile per rendere scalabili le letture.
Quando un client esegue un push su un branch, Pando memorizza il packfile in arrivo come blob immutabile in un archivio di blob. Ogni volta che il branch avanza, Pando registra in un archivio di metadati una versione del riferimento contenente il nuovo commit di punta, il pack che lo ha fornito e le eventuali versioni precedenti dei riferimenti da cui il pack dipende. Sia l'archivio di blob sia quello di metadati sono scalabili orizzontalmente.
Il fetch di un commit o di un riferimento da Pando richiede di riprodurre sul client un insieme di versioni dei riferimenti (packfile), ricostruendo lo stato del client che ha creato la versione del riferimento richiesta. Questo offre una forma di coerenza più debole sull'intero repository, garantendo al contempo la serializzabilità degli aggiornamenti ai singoli riferimenti. I client entrano in conflitto solo quando eseguono simultaneamente un push sullo stesso riferimento. Un client che legge più branch non può presumere che tali letture descrivano lo stato dell'intero repository in un unico istante.
Un push sul trunk aggiunge un pack incrementale contenente uno o più commit. Ogni push su un branch secondario include nuovamente i commit precedenti del branch, così un client con la versione annunciata del trunk può recuperarlo con un solo pack.
Si tratta di una forma di coerenza causale. Per recuperare un riferimento, il client in lettura osserva la cronologia del client in scrittura che lo ha prodotto. Durante un push, Pando controlla quale cronologia condivisa viene concordata tra client e server, assicurandosi che i client producano packfile più facili da condividere.
Di conseguenza, Pando non fornisce uno stato di tutti i riferimenti relativo a un unico istante durante le operazioni Git, né un ordinamento degli aggiornamenti dei riferimenti all'interno di un clone o di un fetch. Supponiamo, per esempio, che i branch foo e bar vengano entrambi aggiornati durante la negoziazione di un clone completo. Durante la negoziazione, un client potrebbe osservare foo alla versione 1 e bar alla versione 2, mentre un altro osserva foo alla versione 2 e bar alla versione 1. In pratica, utenti e agenti non dipendono dall'ordinamento globale degli aggiornamenti dei riferimenti, ma dall'ordine degli aggiornamenti a un singolo riferimento.
In questa simulazione dei tre modelli di coerenza, configuriamo 1.000 client affinché ciascuno selezioni un branch distinto e poi esegua ripetutamente push di commit univoci, misurando il tempo totale trascorso e la latenza tra push e fetch. Quest'ultima è una metrica importante per i carichi di lavoro, perché misura il tempo di andata e ritorno necessario a utenti e agenti per collaborare o usare altri sistemi che si basano sul controllo di versione. La simulazione riproduce macchine virtuali e sistemi di archiviazione di blob su hardware che va dalla fascia standard a quella premium. In questa simulazione abbiamo allentato i vincoli di coerenza dell'intero repository per il commit a tre fasi.
Simulating Git Server Implementations
Follow each writer through repeated pushes, and separately measure when an independent reader can check out each commit.
Esegui questa simulazione per confrontare Pando con altri due approcci. Scegli l'infrastruttura, dall'hardware standard a quello premium, il numero di client e altre impostazioni per confrontare i risultati.
Il sistema di commit a tre fasi simulato mostra una latenza di scrittura molto maggiore sotto carico, a causa della saturazione dei dischi; i sistemi basati su write-ahead log mostrano invece una latenza di lettura molto maggiore per lo stesso motivo, poiché i server faticano a tenere il passo con la lettura del registro. Sia nei test reali sul nostro monorepo sia in questa simulazione, Pando supporta molte migliaia di client che possono eseguire push simultanei senza compromettere le prestazioni sotto carico.
Durante la gestione di un push, Pando convalida il packfile in arrivo mentre lo trasmette in streaming all'archivio di blob, senza scompattarlo in un repository locale. Quando un client avvia un fetch, Pando seleziona i pack archiviati di cui il client ha bisogno, che poi li scarica. Nessun server deve applicare packfile a un repository locale o eseguire costose operazioni di scompattamento e impacchettamento. Poiché si occupano principalmente della negoziazione e della selezione dei packfile, i server Pando possono funzionare su hardware standard.
Questo permette a Pando di scalare fino ai limiti dei client, della rete tra i client e Pando e del servizio di archiviazione di blob. Con una capacità sufficiente, ciascun client che lavora su un branch indipendente può trasferire i pack alla massima velocità consentita dalla propria connessione. Per servire più client a quella velocità, possiamo aggiungere server Pando ed espandere di pari passo gli archivi di metadati e di blob.
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.
Un agente modifica i file, crea un commit in locale ed esegue git push, che crea un packfile con quelle modifiche per aggiornare un branch. Pando riceve il pack e lo trasmette in streaming all'archivio di blob. Dopo aver convalidato il push in arrivo con una decodifica a passaggio singolo, Pando usa una transazione multichiave per registrare una versione del riferimento nel nostro archivio di metadati e aggiornare atomicamente il puntatore al riferimento. Questo permette a Pando di garantire una coerenza di tipo “lettura delle proprie scritture”. L'intera operazione di scrittura richiede letture e scritture di metadati.
Quando un altro client esegue il fetch del branch, Pando seleziona un insieme di pack archiviati in base alla cronologia già presente sul client. Questa operazione richiede letture di metadati, dove è la distanza minima, in versioni dei riferimenti, tra ciò che il client possiede e ciò che richiede. Il server restituisce gli URI dei packfile e il client li scarica tramite Express, un servizio che memorizza i packfile in cache regionali.
Questa struttura si basa sui tre servizi centrali dell'architettura di Pando: push, fetch e compattamento.
Quando un client esegue un push, un server Git convenzionale si comporta come un giocatore di poker che scopre tutte le carte, rivelando al client ogni branch che possiede. Il client raccoglie quindi commit, alberi e blob per creare un packfile che esclude gli oggetti che sa essere già presenti sul server.
Annunciare più riferimenti può ridurre le dimensioni del push. Può però anche rendere il pack più difficile da riutilizzare, perché a un altro client potrebbero mancare gli oggetti esclusi dal client in scrittura.
Pando adotta un approccio diverso. Anziché scoprire tutte le carte, annuncia al client solo un insieme selezionato di branch. Lo fa per due motivi:
Fornire una vista coerente di tutti i riferimenti è costoso. Con milioni di branch, il solo annuncio dei riferimenti può superare i 100 MiB. Per fornirlo, ogni server deve mantenere una mappatura aggiornata, con un notevole costo di coordinamento, oppure eseguire una query al database il cui costo cresce con il numero di riferimenti.
Fornendo al client solo un sottoinsieme dei riferimenti, si fa sì che generi un pack dipendente soltanto da ciò che il server gli consente esplicitamente di escludere. Tracciare le dipendenze permette a Pando di riutilizzare il packfile generato da un client in un push, fornendolo come risposta a un fetch successivo.
Più riferimenti annuncia un server Git, più fonti hanno i client che eseguono push da cui escludere oggetti, potendo così produrre pack più piccoli. Tuttavia, per riutilizzare questi packfile, il server deve poter verificare che i client che eseguono il fetch possiedano tutte le dipendenze. Altrimenti, il packfile potrebbe omettere oggetti di cui il client ha bisogno.
D'altra parte, se un server Git non annuncia alcun riferimento, il client genera un packfile contenente l'intera cronologia del repository. Quel packfile può essere usato da qualsiasi altro client, ma con il costo notevole di rendere il fetch di qualsiasi branch, di fatto, un clone completo.
Annunciare più riferimenti produce packfile più piccoli. Gran parte della riduzione delle dimensioni può derivare dall'annuncio di un solo riferimento.
Per impostazione predefinita, quando un client esegue il push di un nuovo branch di funzionalità, Pando annuncia come base solo un branch designato come trunk. Questo mantiene costanti le dimensioni dell'annuncio al crescere dei branch nel repository, ma comporta dei compromessi. Quando un ingegnere esegue molti push su un singolo branch o crea un branch di funzionalità a partire da un altro, il client deve reinviare tutto ciò che non è nel trunk.
Questo rende più grandi i push successivi sul branch, che contengono oggetti potenzialmente ridondanti, e consuma più banda del client. Nel nostro monorepo, la dimensione mediana dei packfile prodotti in questo modo era inferiore a 20 KiB, e il 99° percentile era inferiore a 3 MiB. Per noi era un compromesso ragionevole, perché un client in lettura che possiede già la cronologia annunciata del trunk può recuperare il branch con quell'unico packfile.
Dopo aver scelto l'insieme di riferimenti da annunciare al client, Pando accetta il push e aggiorna il proprio archivio di metadati. Pando divide l'aggiornamento dei metadati in parti sincrone e asincrone. L'aggiornamento sincrono dei metadati garantisce che i client possano recuperare immediatamente il commit appena aggiornato. Questa operazione scrive una nuova versione del riferimento e aggiorna la mappatura del puntatore affinché punti a essa, memorizzando il nuovo commit di punta, le dipendenze usate per negoziare il packfile e un collegamento al packfile inviato con il push.
Allo stesso tempo, nella medesima transazione vengono registrate le attività asincrone, come la pianificazione del compattamento. Per push molto grandi, solo un sottoinsieme dei commit viene scritto in modo sincrono. Questo ci permette di richiedere solo un numero costante di letture e scritture nell'archivio di metadati, indipendentemente dalle dimensioni del push.
Un aspetto importante è che i server Pando possono accettare in modo indipendente push su riferimenti non correlati. L'unica chiave contesa è il puntatore al riferimento, che proteggiamo con un'operazione atomica simile a compare-and-swap. Possiamo aumentare la capacità di elaborazione in scrittura aggiungendo server Pando, unità di scalabilità dell'archivio di metadati o archivi di blob.
Quando un client esegue un fetch, fornisce al server due insiemi di ID di commit, noti nel protocollo come have e want. Un server Git convenzionale calcola la differenza tra l'insieme degli oggetti raggiungibili dai want e quello degli oggetti raggiungibili dagli have, comprime gli oggetti risultanti e li invia al client. Sebbene il server Git open source disponga di vari indici locali per accelerare questa operazione, generare questi packfile può essere costoso e in genere è difficile sfruttare la cache per riutilizzarli.
Pando, invece, usa le mappature dei commit registrate durante i push per cercare gli have e i want del client, risolvendoli in un insieme di versioni dei riferimenti che contengono quei commit. Ognuna di queste versioni dei riferimenti rimanda a un packfile ricevuto in precedenza da Pando. Il server usa queste informazioni per determinare ciò che serve al client, selezionando i packfile che coprono l'insieme degli oggetti necessari.
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.
Fetch 27 trunk packs.
Select a branch to include its trunk dependencies.
Pando associa gli have e i want del protocollo Git a una sequenza di versioni dei riferimenti. Richiedere un branch secondario implica richiedere anche la cronologia fino alle sue dipendenze sul trunk.
Il protocollo Git consente di inviare un solo packfile nella risposta HTTP a un clone o a un fetch. Tuttavia, una funzionalità poco valorizzata del protocollo Git, packfile-uri, permette a un server di restituire link per scaricare separatamente altri pack. Pando usa esclusivamente questa funzionalità e trasmette un packfile vuoto nella risposta stessa. Per supportarla, OpenAI ha contribuito al progetto originale con vari miglioramenti a packfile-uri.
I sistemi OpenAI operano in molte regioni geografiche, perciò manteniamo cluster Pando con cache locali in ciascuna di esse. Express, un servizio di distribuzione dei pack, funge da cache per il nostro archivio di blob, mentre un servizio separato fa da cache per l'archivio di metadati. Queste cache permettono a Pando di gestire le richieste per un ID di commit o una lettura speciale di un riferimento interamente dallo stato memorizzato nella cache di quel cluster. Usiamo l'hashing coerente per instradare le richieste di un packfile ai server Express, con tassi di riscontro in cache superiori al 99%.
Questo sistema di cache offre una capacità di elaborazione estremamente elevata, in grado di saturare la capacità di rete tra i client e Pando. Con questi URI dei packfile e le cache locali, abbiamo verificato che le operazioni di clone e fetch saturano collegamenti di rete da 10 Gbps anche usando soltanto macchine virtuali standard dei provider cloud.
Per la maggior parte dei server Git, il reimpacchettamento e la garbage collection sono necessari per mantenere le prestazioni di un repository in crescita. Entrambe le operazioni recuperano spazio, ma richiedono molte risorse di calcolo e I/O.
In un repository che cambia rapidamente come il monorepo di OpenAI, non è fattibile inviare un URI di packfile per ogni push avvenuto dall'ultimo fetch. Ogni giorno, un client dovrebbe scaricare decine di migliaia di packfile per aggiornare il proprio trunk.
Pando, invece, combina i pack in background per ridurre questo sovraccarico, adottando una struttura simile al compattamento negli alberi LSM (log-structured merge-tree). Ogni push produce quello che Pando chiama un packfile “L0”, la più piccola unità di dati che Pando può trasmettere. Nel tempo, i processi in background raccolgono questi packfile in raggruppamenti che coprono una sequenza di versioni dei riferimenti, crescendo geometricamente a partire da L0. Oggi il nostro monorepo usa raggruppamenti di packfile fino al livello L4.
Questi raggruppamenti hanno una duplice funzione. Quando un raggruppamento è sufficientemente pieno, un processo di compattamento lo prende in carico e produce un unico packfile. Questo migliora il rapporto di compressione riorganizzando gli oggetti all'interno del packfile. Ma migliora anche le prestazioni della negoziazione dei fetch, fungendo da skip list sulle versioni dei riferimenti. Scorrendo contemporaneamente gli indici di ciascun livello, Pando può selezionare in tempo logaritmico un insieme di pack da inviare a un client. Questi raggruppamenti di packfile, reimpacchettati o meno, consentono una pianificazione dei fetch più efficiente, riducendo i requisiti di Pando sia in termini di capacità di elaborazione sia di latenza.
Compaction in Pando
Small packfiles are grouped into larger covers across levels, improving compression and speeding up fetches.
Il compattamento combina in background i packfile di molte versioni dei riferimenti, consentendo fetch più efficienti con meno file.
A differenza di quanto avviene in molti server Git, i packfile vengono conservati a ogni livello e non vengono scartati né eliminati dalla garbage collection. I pack di ogni livello vengono usati continuamente per servire i client, in base agli have e ai want indicati nelle richieste di fetch. Per i client che usano Pando come API, inoltre, leggere un oggetto arbitrario da un packfile tende a richiedere meno dati da un L0 che da un L4, perché è meno probabile che l'oggetto sia compresso.
Il compattamento in Pando riduce notevolmente la quantità totale di dati trasferiti ai client e permette alla pianificazione dei pack di crescere in modo sublineare rispetto al ritardo del client. Questo è particolarmente importante per i repository grandi e in rapida evoluzione, che prevediamo diventeranno la norma nello sviluppo software.
Abbiamo creato Pando con Codex, passando dall'avvio del progetto alla produzione in sole 10 settimane. Il nucleo di Pando è stato progettato da un solo ingegnere, implementato da team di agenti e distribuito da un piccolo gruppo di ingegneri. Pando ha iniziato a gestire traffico di produzione nell'aprile di quest'anno e fa parte di un sistema più ampio di strumenti per il controllo di versione usati in OpenAI.
Abbiamo usato un'ampia varietà di approcci per convalidare la progettazione di Pando. Abbiamo applicato i nostri principi per creare basi software più sicure. Trattandosi di un progetto avviato da zero, abbiamo scelto di implementare Pando in Rust, senza dipendenze legacy. Abbiamo scritto una suite di test di conformità affinché gli agenti potessero verificare le proprie modifiche rispetto al comportamento standard di Git. Abbiamo anche sviluppato infrastrutture di test basate su simulazioni deterministiche per verificare invarianti e proprietà del sistema, individuando e mitigando sottili condizioni di gara e regressioni nella negoziazione e nella pianificazione dei pack.
Molti modelli hanno contribuito a questo sistema; man mano che ne diventavano disponibili di nuovi, li abbiamo usati per migliorare le prestazioni e l'affidabilità di Pando. Usando GPT‑6 Astra, gli agenti hanno scritto codice in Lean e TLA+ per formalizzare una “teoria di Pando”, che abbiamo usato per individuare e correggere bug nell'implementazione e dimostrare proprietà del sistema. Diverse sessioni sono proseguite ininterrottamente per centinaia di ore, individuando e correggendo casi limite in Pando. Facciamo inoltre lavorare agenti 24 ore su 24 per condurre test di red teaming su Git e Pando con modelli dotati delle capacità di Daybreak.
Inizialmente abbiamo distribuito Pando come proxy read-through e write-through per il nostro sistema di controllo di versione esistente. Questo ci ha permesso di mantenere sincronizzati Pando e il sistema precedente, così da spostare il traffico su Pando in sicurezza e senza interruzioni del servizio.
Grazie alla sua architettura di cache, Pando può anche deduplicare e combinare le richieste prima di inoltrarle. Questo ci ha permesso di mantenere efficienti gli strumenti esistenti mentre migravamo i sistemi su Pando uno alla volta. Man mano che migravamo più carichi di lavoro su Pando, i loro push ne popolavano la cache e miglioravano l'affidabilità del backend precedente.
Per ottimizzare le prestazioni con Pando, abbiamo iniziato a distribuire la nostra versione pubblica di Git sui computer degli sviluppatori e all'interno dell'app desktop di ChatGPT. Per migrare gradualmente i client su Pando, abbiamo usato un wrapper interno attorno alla CLI di Git. Abbiamo creato molti altri strumenti per gestire le richieste di modifica e la revisione del codice, oltre ad API per le automazioni. Insieme al traffico Git, abbiamo migrato centinaia di integrazioni che dipendevano da API legacy, webhook e download dei repository.
Codex stesso ha aiutato sviluppatori e team a effettuare il passaggio. Abbiamo creato skill per preparare le copie di lavoro esistenti e guidare gli agenti nell'uso del nuovo sistema. Di conseguenza, agli sviluppatori bastava chiedere a Codex di migrare i propri ambienti di sviluppo e servizi.
Il passaggio a Pando ha migliorato l'affidabilità dei merge e ridotto la latenza mediana dei push di quasi l'80%.
Pando può distribuire un petabyte di dati Git al giorno con appena pochi core CPU. Pando ha distribuito oltre 57 terabyte di pack Git in una sola ora, con più del 99% di quei byte proveniente dalla cache su disco locale.
Riutilizzare i pack permette a molti client di scaricare la stessa cronologia del repository con un carico di calcolo ridotto sui nodi che la distribuiscono. Questo esempio riguarda un solo repository, ma Pando è progettato per supportarne molti di più. Con la crescita della domanda, possiamo scalare la distribuzione dei dati e la negoziazione Git in modo indipendente.
Costruire Pando sul protocollo e sul client Git ci ha aiutato a individuare bug e opportunità per migliorare le prestazioni di Git stesso.
Per esempio, Pando fornisce pack riutilizzabili direttamente ai client, quindi il download e l'indicizzazione di quei pack sono diventati elementi centrali per rendere il sistema veloce e affidabile. Quel lavoro ha fatto emergere casi limite nei download simultanei, nella pulizia dopo trasferimenti interrotti e nella ripresa di download già completati. Abbiamo corretto quei percorsi di esecuzione, aggiunto controlli per l'accettazione di un pack esistente durante un fetch e contribuito con queste modifiche al progetto originale.
Dovevamo anche capire dove venivano impiegati tempo e banda, quindi abbiamo aggiunto il tracciamento dei download packfile-uri e il conteggio dei byte per la generazione dei pack, rendendo più facile capire se un fetch lento impiegava tempo a generare i pack o a trasferirli.
Nella build di Git di OpenAI(si apre in una nuova finestra), il client può scaricare e indicizzare pack indipendenti in parallelo. Abbiamo introdotto il concetto di “have espliciti”, così che un server possa annunciare il commit di punta attuale di un branch separatamente dalla cronologia che un client in fase di push può escludere dal proprio pack. Su Pando, questo permette ai client di usare --force-with-lease per verificare che un branch di funzionalità non sia cambiato e creare il relativo pack usando solo il trunk come base. Chiunque possieda quella cronologia del trunk può quindi recuperare il branch usando lo stesso pack.
Diversi di questi miglioramenti sono già nel progetto Git originale, e molti altri nel nostro repository pubblico. Continuiamo a collaborare con la community di Git per rendere questi miglioramenti utili anche al di fuori di Pando. Puoi seguire questo lavoro nel nostro tracker pubblico delle patch(si apre in una nuova finestra).
Ripensando gli approcci consolidati alla scalabilità dei repository Git, Pando ci permette di far crescere il nostro monorepo senza cambiare i flussi di lavoro Git già usati dai nostri ingegneri e agenti. Nel nostro monorepo, Pando ha consentito un maggior grado di parallelismo, migliorato l'affidabilità dei merge e ridotto il tempo necessario per pubblicare le modifiche al codice. Oltre al monorepo, stiamo esplorando altri usi di Pando all'interno di OpenAI, inclusi carichi di lavoro in cui un gran numero di agenti deve collaborare su un repository Git condiviso.
Man mano che ingegneri e agenti producono più software, anche altri team al di fuori di OpenAI incontreranno i limiti che abbiamo riscontrato nel controllo di versione. Stiamo valutando come condividere con i nostri utenti la tecnologia che rende possibile Pando.
Pando si basa su due decenni di esperienza di chi usa e gestisce repository Git. Continueremo a sviluppare Pando, collaborando con la community open source di Git per condividere tutti i miglioramenti utili anche alla più ampia base di utenti Git.


