OpenAI

2 de noviembre de 2026

Ingeniería

Escalar Git en OpenAI

Crear un sistema de control de versiones a escala de los agentes sin dejar de invertir en el ecosistema de Git.

Por Aaron Friel, Taylor Blau y Ted Nyman, miembros del equipo técnico

Cargando…

Durante el último año, el tamaño de nuestro monorrepositorio y las exigencias a las que está sometido han aumentado a un ritmo vertiginoso. Estaba claro que necesitábamos renovar nuestro sistema de control de versiones para adaptarlo al nivel de trabajo simultáneo que hace posible Codex.

Desarrollamos Pando, un nuevo backend para repositorios Git, sin interrumpir el trabajo de ingeniería ni cambiar la forma en que nuestro equipo utiliza Git. Ahora, Pando sustenta todo el desarrollo en el monorrepositorio de OpenAI. Pando está diseñado para cumplir dos objetivos ambiciosos: primero, ofrecer escrituras que escalen horizontalmente, optimizadas para pushes simultáneos a muchas ramas. Y segundo, maximizar el rendimiento de lectura para que los clientes puedan descargar datos lo bastante rápido como para aprovechar todo el ancho de banda de red disponible.

Cumplimos estos objetivos manteniendo la compatibilidad con el protocolo y el cliente de Git, lo que permitió a nuestros ingenieros y agentes incorporar Pando sin problemas a sus flujos de trabajo existentes. En producción, Pando redujo la mediana de latencia de los pushes a casi una quinta parte. Ahora sirve casi un petabyte de datos de Git al día.

Aquí explicaremos qué nos llevó a crear un nuevo sistema de control de versiones y por qué decidimos mantener la compatibilidad con Git. Compararemos nuestro sistema con los enfoques habituales para escalar Git y explicaremos qué hace que Pando sea especialmente adecuado para satisfacer las necesidades de escalabilidad de OpenAI. También abordaremos los próximos pasos y cómo seguiremos contribuyendo a Git para dar soporte al trabajo a escala de los agentes.

Por qué necesitábamos un nuevo sistema de control de versiones

Codex ha cambiado la escala y el ritmo de desarrollo en OpenAI, permitiendo a los ingenieros realizar mucho más trabajo en paralelo. Eso implica más ramas y pushes, más fusiones y más operaciones de fetch y clone desde entornos de desarrollo, tareas de investigación y sistemas de integración continua (CI).

Estas exigencias se multiplican. Ahora, un solo ingeniero tiene varios cambios listos para CI a la vez, y cada push puede desencadenar varias tareas que hagan fetch del mismo commit.

Hacia finales del año pasado, los sistemas que sustentaban nuestro monorrepositorio empezaron a tener dificultades para mantener el ritmo. Sufríamos operaciones de fetch y push lentas, además de fusiones fallidas, y la fiabilidad caía en ocasiones por debajo del 90 %. ¿Habíamos alcanzado un límite del propio Git o de la forma en que servíamos nuestro monorrepositorio? Git surgió de una comunidad que intercambiaba parches del kernel por correo electrónico. Durante más de dos décadas, los desarrolladores han adaptado sus herramientas en torno a Git a medida que cambiaba su forma de crear software. Cuando empezamos a encontrar dificultades en nuestro propio monorrepositorio, nos importaba poder seguir aprovechando ese trabajo.

Vimos una forma de mantener ese flujo de trabajo conocido y, a la vez, reconstruir el backend para gestionar en paralelo pushes a distintas ramas y servir el mismo historial a muchos lectores simultáneamente. Usar Git como base de nuestro flujo de control de versiones significaba que las mejoras del cliente podían ser útiles también fuera de OpenAI. Queríamos aportar esas mejoras de nuestra copia de Git⁠(se abre en una ventana nueva) al proyecto original y compartirlas con toda la comunidad.

Ingeniería para Git a gran escala

Escalar un sistema de control de versiones tiene dos dimensiones esenciales: muchos escritores que hacen push a distintas ramas y muchos lectores que hacen fetch del mismo historial. Pando está optimizado para ambas.

En primer lugar, Pando coordina las actualizaciones de cada rama de forma independiente. Las actualizaciones de ramas independientes pueden avanzar en paralelo, y un lector que haga fetch de una rama obtiene el historial necesario para esa revisión. La contrapartida es que un lector que consulte varias ramas puede verlas en estados correspondientes a distintos momentos.

En segundo lugar, Pando reutiliza archivos pack (el formato de Git para comprimir varios objetos juntos) entre operaciones de fetch, en vez de generar un archivo pack específico para cada cliente. Puede que un cliente descargue algunos objetos que ya tiene, pero el servidor puede servir los mismos bytes a muchos clientes con poco cómputo.

Estas decisiones nos permiten escalar las escrituras entre ramas y atender las lecturas desde cachés. Para entender cómo encajan, veamos primero cómo almacena y transfiere Git el historial.

Introducción al almacenamiento de objetos de Git

Git debe poder mantener sincronizadas dos partes de un repositorio: los objetos y las referencias.

Un repositorio Git registra instantáneas de archivos y el historial que las conecta como un grafo de objetos. Un objeto puede ser un blob, un árbol, un commit o una etiqueta. Los blobs contienen el contenido de un archivo individual, y los árboles asocian nombres con blobs u otros árboles para formar una estructura de directorios. Los commits apuntan a un árbol raíz y a cero o más commits padre, formando el historial de tu repositorio.

Los objetos se identifican mediante un hash de su tipo y contenido. Además, los objetos son inmutables: registrar un archivo modificado crea un blob nuevo, mientras que los archivos y directorios sin cambios pueden reutilizar objetos existentes.

Las ramas y las etiquetas son referencias, o «refs», que apuntan a commits. Hacer un commit en una rama crea un commit nuevo y actualiza la referencia de la rama para que apunte a él.

La rama refs/heads/main apunta al commit C2, cuyo padre es C1. C1 apunta al árbol raíz T1, donde parser.py hace referencia al blob B1. C2 apunta al árbol raíz T2, donde parser.py hace referencia al blob B2. En ambos árboles raíz, docs/ hace referencia al mismo árbol, T3. Su entrada README.md apunta al blob B3, y usage.md apunta al blob B4.

Los objetos de Git se referencian entre sí mediante su ID: los commits apuntan a árboles, y los árboles, a otros árboles y a blobs. La rama refs/heads/main apunta al commit C2, cuyo padre es C1. C1 apunta al árbol raíz T1, donde parser.py hace referencia al blob B1. C2 apunta al árbol raíz T2, donde parser.py hace referencia al blob B2. En ambos árboles raíz, docs/ hace referencia al mismo árbol, T3. Su entrada README.md apunta al blob B3, y usage.md apunta al blob B4.

Git almacena los objetos en estado «suelto» o «empaquetado». Los objetos sueltos se almacenan en archivos individuales identificados por su ID de objeto. Los objetos empaquetados se almacenan juntos en un archivo pack, lo que permite guardar objetos similares como diferencias entre sí.

Aparecen dos versiones de parser.py en $GIT_DIR/objects . Como objetos sueltos, el blob A contiene parse(), y el blob B contiene tanto parse() como parse_file(). El archivo pack almacena A completo y representa B como instrucciones para copiar todo A y añadir parse_file(). El archivo *.idx adjunto localiza ambos blobs en el archivo pack mediante su ID de objeto.

Git puede almacenar los blobs como archivos separados o empaquetarlos juntos, codificando un blob como una diferencia respecto a otro.

En Git, un objeto es alcanzable desde un commit si Git puede encontrarlo siguiendo los enlaces desde ese commit a través de sus padres, árboles y blobs. Durante un fetch, el cliente indica al servidor qué commits quiere y cuáles tiene ya. El servidor busca los objetos alcanzables desde los commits solicitados, excluye los que sabe que el cliente ya tiene y envía el resto como un archivo pack con el mismo formato que Git utiliza en disco. Un clon completo parte de cero, sin objetos, por lo que necesita todos los objetos alcanzables desde el historial que se está clonando.

Un push transfiere objetos en la dirección contraria y solicita al servidor que actualice una o más referencias. El cliente utiliza el conjunto de referencias anunciadas por el servidor para evitar enviar objetos que este ya tiene. Por tanto, el paquete enviado puede depender de objetos existentes: recibir solo el paquete no basta para reconstruir el nuevo historial.

Replicar un repositorio Git implica mantener disponibles tanto los objetos como las referencias en varios servidores. Antes de que una réplica pueda servir el nuevo valor de una referencia, debe poder proporcionar los objetos alcanzables desde ese valor. El servicio también debe coordinar las actualizaciones de referencias que compiten entre sí, es decir, pushes que intentan mover la misma rama desde el mismo commit de punta anterior a distintos commits nuevos. Los sistemas que imponen un único orden a todas las escrituras de un repositorio pueden hacer que los pushes a ramas independientes tengan que esperar unos a otros.

Patrones existentes de servicios Git distribuidos

Al diseñar Pando, evaluamos enfoques convencionales para escalar el control de versiones con Git: uno que coordina las actualizaciones de referencias entre réplicas del repositorio mediante confirmación en tres fases (3PC, un protocolo para coordinar una confirmación entre varios nodos), y otro que ordena las actualizaciones mediante un registro compartido de escritura anticipada (WAL, un registro de cambios que se escribe antes de aplicarlos) en el almacenamiento de blobs.

Confirmación en tres fases (3PC)

En el diseño de confirmación en tres fases que modelamos, el servicio Git mantiene una copia completa del repositorio en varias réplicas. Cuando cualquier servidor recibe un push, el archivo pack entrante se copia y se envía a todas las réplicas en paralelo. Para publicar el push, la mayoría de las réplicas debe aceptar la actualización de la referencia. El servidor que gestiona este proceso es el coordinador, y se requieren varias rondas de comunicación entre él y cada una de las demás réplicas.

Para aceptar una actualización de referencia, cada réplica bloquea su copia de la referencia y comprueba que siga apuntando al commit que el cliente espera. A continuación, el coordinador envía un mensaje de preconfirmación a las réplicas que aceptan. Una vez que la mayoría acusa recibo del mensaje de preconfirmación, el coordinador les indica que realicen la actualización. Después, cualquier réplica actualizada puede anunciar el nuevo commit de punta de la rama desde su copia local del repositorio.

Mantener réplicas completas permite atender las lecturas localmente y garantiza que los clientes obtengan una vista consistente de todas las referencias, independientemente de la réplica que elijan. Como consecuencia, las referencias se actualizan de una en una y cada push requiere varios intercambios de ida y vuelta para coordinar la actualización de la referencia entre réplicas.

La espera de un cuórum hace que el rendimiento de disco de las réplicas y la latencia de comunicación condicionen el proceso de push. A medida que crece el volumen de escrituras, las réplicas deben mantener el ritmo tanto de los paquetes entrantes como del tráfico de lectura.

Registro de escritura anticipada (WAL)

En el diseño basado en WAL que modelamos, el servicio Git utiliza un archivo de manifiesto compartido en el almacenamiento de blobs para ordenar las actualizaciones de todo el repositorio. Cuando un servidor recibe un push, sube el archivo pack entrante a un almacén de blobs y prepara la actualización de la referencia en su copia local del repositorio. Para publicar el push, añade una entrada a un archivo de manifiesto compartido guardado en el almacén de blobs, que registra la actualización de la referencia y apunta al archivo pack. Después, el servidor Git actualiza el archivo de manifiesto mediante una operación de comparar e intercambiar. Esta sustituye atómicamente el archivo de manifiesto o, en caso contrario, rechaza el cambio. Si otro escritor lo ha actualizado mientras tanto, el servidor lee las entradas añadidas desde entonces, comprueba que su actualización de referencia siga siendo válida y vuelve a intentarlo con el archivo pack que ya había subido.

Los escritores pueden subir sus archivos pack en paralelo, pero incluso los pushes a ramas independientes requieren actualizar el mismo archivo de manifiesto. El tiempo de ida y vuelta de una operación de comparar e intercambiar que se completa correctamente limita la frecuencia con la que el repositorio puede publicar un nuevo archivo de manifiesto. Un almacén de blobs de baja latencia acelera este paso. El procesamiento por lotes permite que un archivo de manifiesto actualice varias referencias a la vez, a costa de poner las actualizaciones en cola y aumentar la latencia.

El almacén de blobs puede recibir archivos pack en paralelo, pero las actualizaciones del archivo de manifiesto compartido siguen compitiendo entre sí. Aumentar únicamente la capacidad del almacén de blobs no resuelve ese cuello de botella. Cada réplica sigue necesitando leer e indexar los paquetes del almacén de blobs antes de poder servirlos localmente. Si las escrituras entrantes avanzan más rápido que ese trabajo, las réplicas se quedan atrás y los clientes esperan más para hacer fetch de los commits recién enviados.

Diseño de la arquitectura de Pando

Y así llegamos a Pando. Pando es una nueva arquitectura para servir repositorios Git, concebida para escalar a un gran número de clientes que hacen push simultáneamente. Así funciona:

Replantear las restricciones

Antes de desarrollar Pando, intuíamos que podíamos cumplir nuestros requisitos sin renunciar a Git, aunque sí tendríamos que renunciar a algunas condiciones que la mayoría de los sistemas Git consideran necesarias:

  • Consistencia en todo el repositorio: ofrecer una instantánea consistente de todas las referencias en un momento dado exige coordinación en todo el repositorio. En un sistema que ordena conjuntamente todas las actualizaciones de referencias, los pushes a ramas independientes compiten por un único punto de coordinación.

  • Archivos pack específicos para cada cliente: si dos clientes con historiales locales distintos hacen fetch de la misma rama, el servidor puede tener que generar un paquete distinto para cada uno para evitar enviarles objetos que ya tienen. Generar estas respuestas consume CPU y operaciones de E/S de disco, lo que dificulta escalar a un gran número de lectores.

En cambio, Pando ofrece un modelo de consistencia menos estricto (consistencia causal) para escalar las escrituras y reutiliza los archivos pack para escalar las lecturas.

Consistencia causal (CC)

Cuando un cliente hace push a una rama, Pando guarda el archivo pack entrante como un blob inmutable en un almacén de blobs. Cada vez que cambia la rama, Pando registra en un almacén de metadatos una versión de referencia que contiene el nuevo commit de punta, el paquete que lo aportó y las versiones de referencia anteriores de las que depende el paquete. Tanto el almacén de blobs como el de metadatos pueden escalar horizontalmente.

Obtener un commit o una referencia de Pando requiere reproducir en el cliente un conjunto de versiones de referencia (archivos pack), reconstruyendo el estado del cliente que creó la versión de referencia solicitada. Esto ofrece una consistencia menos estricta en el conjunto del repositorio, pero garantiza la serializabilidad de las actualizaciones de cada referencia. Los clientes solo entran en conflicto cuando hacen push a la misma referencia simultáneamente. Un cliente que lea varias ramas no puede dar por hecho que esas lecturas reflejen un mismo instante en todo el repositorio.

A la izquierda, tres paquetes sucesivos de la rama principal contienen los commits A, B y C; D y E; y F. A la derecha, la rama principal y la rama temática divergen en C. La rama principal avanza hasta G, mientras que la rama temática avanza por D, E y F. El último paquete de la rama temática contiene D, E y F. Un contorno discontinuo indica que D y E también están presentes en el paquete anterior de la rama temática. La flecha azul indica que el nuevo paquete depende de la rama principal en G. El padre de F en Git es E.

Un push a la rama principal añade un paquete incremental que contiene uno o más commits. Cada push a una rama secundaria vuelve a incluir los commits anteriores de esa rama, de modo que un cliente con la versión anunciada de la rama principal pueda obtener la rama secundaria en un solo paquete.

Esta es una forma de consistencia causal. Para obtener una referencia, el lector observa el historial del escritor que la generó. Durante un push, Pando controla qué historial compartido acuerdan el cliente y el servidor, garantizando que los clientes generen archivos pack más fáciles de compartir.

Por tanto, Pando no ofrece un estado de todas las referencias correspondiente a un mismo instante durante las operaciones de Git, ni un orden de las actualizaciones de referencias dentro de un clone o un fetch. Por ejemplo, supongamos que las ramas foo y bar se actualizan durante la negociación de un clon completo. Durante la negociación, un cliente puede observar foo en la versión 1 y bar en la versión 2, mientras que otro observa foo en la versión 2 y bar en la versión 1. En la práctica, los usuarios y los agentes no dependen del orden global de las actualizaciones de referencias, sino del orden de las actualizaciones de una referencia concreta.

Comparación de enfoques

En esta simulación de los tres modelos de consistencia, configuramos 1000 clientes para que cada uno seleccione una rama distinta y envíe repetidamente commits únicos mediante push. Medimos el tiempo real total transcurrido y la latencia entre el push y el fetch. Esta última es una métrica importante para las cargas de trabajo, ya que mide el tiempo de ida y vuelta necesario para que usuarios y agentes colaboren o utilicen otros sistemas que dependen del control de versiones. La simulación modela hardware que abarca desde máquinas virtuales y sistemas de almacenamiento de blobs estándar hasta opciones de gama alta. En esta simulación, hemos relajado la consistencia en todo el repositorio para el protocolo de confirmación en tres fases.

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.

Ejecuta esta simulación que compara Pando con otros dos enfoques. Elige la infraestructura, desde hardware estándar hasta equipos de gama alta, el número de clientes y otros ajustes para comparar los resultados.

El sistema modelado de confirmación en tres fases presenta una latencia de escritura mucho mayor bajo carga a medida que se saturan sus discos. Por su parte, los sistemas de registro de escritura anticipada muestran una latencia de lectura mucho mayor por el mismo motivo, ya que los servidores no logran mantener el ritmo de lectura del registro. Tanto en las pruebas reales con nuestro monorrepositorio como en esta simulación, Pando admite muchos miles de clientes que pueden hacer push simultáneamente sin afectar al rendimiento bajo carga.

Al procesar un push, Pando valida el archivo pack entrante mientras lo transmite al almacenamiento de blobs, sin desempaquetarlo en un repositorio local. Cuando un cliente inicia un fetch, Pando selecciona los paquetes almacenados que necesita el lector y este los descarga. Ningún servidor necesita aplicar archivos pack a un repositorio local ni realizar costosas operaciones de desempaquetado y empaquetado. Como estos servidores se encargan principalmente de la negociación y la selección de archivos pack, los servidores de Pando pueden ejecutarse en hardware estándar.

Esto permite a Pando escalar hasta los límites de los clientes, de la red entre estos y Pando, y del servicio de almacenamiento de blobs. Con suficiente capacidad, cada cliente que trabaje en una rama independiente puede transferir paquetes tan rápido como permita su conexión. Para atender a más clientes a esa velocidad, podemos añadir servidores de Pando y escalar junto con ellos los almacenes de metadatos y 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 agente edita archivos, crea un commit localmente y ejecuta git push, que genera un archivo pack con esos cambios para actualizar una rama. Pando recibe el paquete y lo transmite al almacenamiento de blobs. Tras validar el push entrante mediante una decodificación de una sola pasada, Pando utiliza una transacción multiclave para registrar una versión de referencia en nuestro almacén de metadatos y actualizar atómicamente el puntero de la referencia. Esto permite a Pando ofrecer una consistencia de «lectura de las propias escrituras». Toda esta operación de escritura requiere O(1)O(1) lecturas y escrituras de metadatos.

Cuando otro cliente hace fetch de la rama, Pando selecciona un conjunto de paquetes almacenados a partir del historial que el cliente ya tiene. Esta operación requiere O(log⁡Δ)O(\log \Delta) lecturas de metadatos, donde Δ\Delta es la distancia más corta, medida en versiones de referencia, entre lo que el cliente tiene y lo que solicita. El servidor devuelve las URI de los archivos pack y el cliente los descarga a través de un servicio, Express, que almacena archivos pack en cachés regionales.

Este diseño se apoya en los tres servicios centrales de la arquitectura de Pando: pushes, fetches y compactación.

Escalar los pushes

Cuando un cliente hace push, un servidor Git convencional es como un jugador de póquer que muestra todas sus cartas: revela al cliente todas las ramas que tiene. A continuación, el cliente reúne los commits, árboles y blobs para crear un archivo pack que excluye los objetos que sabe que el servidor ya tiene.

Anunciar más referencias puede reducir el tamaño del push. También puede dificultar la reutilización del paquete, porque a otro cliente podrían faltarle objetos que el escritor omitió.

Pando adopta otro enfoque. En lugar de mostrar todas sus cartas, solo anuncia al cliente un conjunto seleccionado de ramas. Lo hace por dos motivos:

  1. Ofrecer una vista consistente de todas las referencias es costoso. Con millones de ramas, solo el anuncio de referencias puede superar los 100 MiB. Para ofrecerlo, cada servidor debe mantener un mapa actualizado, con un coste de coordinación considerable, o realizar una consulta a la base de datos cuyo coste crezca con el número de referencias.

  2. Al proporcionar al cliente solo un subconjunto de referencias, este generará un paquete que dependa únicamente de lo que el servidor le permita excluir de forma explícita. Registrar las dependencias permite a Pando reutilizar el archivo pack que un cliente genera en un push y servirlo como respuesta a un fetch posterior.

Cuantas más referencias anuncia un servidor Git, más fuentes tienen los clientes que hacen push para excluir objetos, lo que les permite generar paquetes más pequeños. Sin embargo, para reutilizar estos archivos pack, el servidor debe poder demostrar que los clientes que hacen fetch tienen todas las dependencias. De lo contrario, el archivo pack podría omitir objetos que el cliente necesita.

Por otra parte, si un servidor Git no anuncia ninguna referencia, el cliente generaría un archivo pack con todo el historial del repositorio. Cualquier otro cliente puede utilizar ese archivo pack, pero con un coste considerable: hacer fetch de cualquier rama equivaldría, en la práctica, a obtener un clon completo.

Curva que muestra cómo afectan las referencias anunciadas al tamaño del archivo pack: desde el historial completo cuando no se anuncia ninguna, pasando por anunciar la rama principal, hasta incluir solo los objetos nuevos cuando se anuncian todas las referencias.

Anunciar más referencias da lugar a archivos pack más pequeños. La mayor parte de la reducción de tamaño puede lograrse anunciando solo una.

Por defecto, cuando un cliente hace push de una nueva rama de funcionalidad, Pando solo anuncia como base una rama principal designada. Esto mantiene constante el tamaño del anuncio a medida que el repositorio incorpora ramas, pero tiene contrapartidas. Cuando un ingeniero hace push muchas veces a una misma rama o apila una rama de funcionalidad sobre otra, el cliente debe reenviar todo lo que queda fuera de la rama principal.

Esto hace que los sucesivos pushes a la rama sean más grandes, contengan objetos potencialmente redundantes y consuman más ancho de banda del cliente. En nuestro monorrepositorio, la mediana del tamaño de los archivos pack generados así era inferior a 20 KiB, y el percentil 99 quedaba por debajo de 3 MiB. Para nosotros, era una contrapartida razonable, porque un lector que ya tenga el historial anunciado de la rama principal puede hacer fetch de la rama con ese único archivo pack.

Tras elegir el conjunto de referencias que anunciará al cliente, Pando acepta el push y actualiza su almacén de metadatos. Pando divide la actualización de metadatos en una parte síncrona y otra asíncrona. La actualización síncrona de metadatos garantiza que los clientes puedan hacer fetch de inmediato del commit recién actualizado. Esta escribe una nueva versión de referencia y actualiza el puntero para que apunte a ella, almacenando el nuevo commit de punta, las dependencias utilizadas para negociar el archivo pack y un enlace al archivo pack enviado.

Al mismo tiempo, se registra en la misma transacción el trabajo asíncrono, como la programación de la compactación. En los pushes muy grandes, solo se escribe de forma síncrona un subconjunto de los commits. Esto nos permite necesitar solo un número constante de lecturas y escrituras en el almacén de metadatos, independientemente del tamaño del push.

Un aspecto importante es que los servidores de Pando pueden aceptar pushes a referencias no relacionadas de forma independiente. La única clave por la que compiten es el puntero de la referencia, que protegemos mediante una operación atómica similar a comparar e intercambiar. Podemos aumentar el rendimiento de escritura añadiendo servidores de Pando, unidades de escalado del almacén de metadatos o almacenes de blobs.

Rendimiento de fetch y clone a la máxima velocidad de red

Cuando un cliente realiza un fetch, proporciona al servidor dos conjuntos de ID de commits, conocidos en el protocolo como have y want. En un servidor Git convencional, el servidor calcula la diferencia entre el conjunto de objetos alcanzables desde los have y el de los alcanzables desde los want, los comprime y envía el resultado al cliente. Aunque el servidor Git de código abierto dispone de diversos índices locales para acelerar esta operación, generar estos archivos pack puede ser costoso y, en general, son difíciles de almacenar en caché de forma eficaz.

En cambio, Pando utiliza las correspondencias de commits registradas durante los pushes para buscar los have y want del cliente y resolverlos en un conjunto de versiones de referencia que contienen esos commits. Cada una de esas versiones de referencia enlaza con un archivo pack que Pando recibió anteriormente. El servidor utiliza esta información para determinar qué necesita el cliente y seleccionar los archivos pack que cubren ese conjunto de objetos.

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 asigna los have y want del protocolo Git a una secuencia de versiones de referencia. Solicitar una rama secundaria implica solicitar también sus dependencias en la rama principal.

El protocolo Git solo permite enviar un archivo pack en la respuesta HTTP de una operación de clone o fetch. Sin embargo, una función poco valorada del protocolo Git, packfile-uri, permite al servidor devolver enlaces para descargar paquetes adicionales por separado. Pando utiliza exclusivamente este mecanismo y transmite un archivo pack vacío dentro de la respuesta. Para dar soporte a esta función, OpenAI aportó al proyecto original varias mejoras de packfile-uri.

Los sistemas de OpenAI operan en muchas regiones geográficas, por lo que mantenemos clústeres de Pando con cachés locales en cada una de ellas. Un servicio de entrega de paquetes, Express, mantiene una caché de nuestro almacén de blobs, y otro servicio independiente mantiene una caché del almacén de metadatos. Estas cachés permiten a Pando atender solicitudes de un ID de commit o de una lectura especial de una referencia íntegramente desde el estado almacenado en caché en ese clúster. Utilizamos hashing consistente para dirigir las solicitudes de archivos pack a los servidores de Express, con tasas de acierto de caché superiores al 99 %.

Este sistema de cachés ofrece un rendimiento extremadamente alto, capaz de saturar la capacidad de la red entre los clientes y Pando. Con estas URI de archivos pack y las cachés locales, hemos comprobado que, usando únicamente máquinas virtuales estándar de proveedores de nube, las operaciones de clone y fetch saturan enlaces de red de 10 Gbps.

Compactación y tiempo hasta el primer byte

En la mayoría de los servidores Git, el reempaquetado y la recolección de basura son necesarios para mantener el rendimiento de un repositorio en crecimiento. Ambos procesos recuperan espacio, pero consumen muchos recursos de cómputo y E/S.

En un repositorio con un ritmo de cambios tan alto como el monorrepositorio de OpenAI, no es viable enviar una URI de archivo pack individual por cada push realizado desde el último fetch. Cada día, un cliente tendría que descargar decenas de miles de archivos pack para poner al día su rama principal.

En su lugar, Pando combina paquetes en segundo plano para reducir esta sobrecarga, con un diseño similar al de la compactación en los árboles de fusión estructurados por registros. Cada push genera lo que Pando denomina un archivo pack «L0», que representa la unidad de datos más pequeña que puede transmitir. Con el tiempo, los procesos en segundo plano reúnen estos archivos pack en agrupaciones que abarcan una secuencia de versiones de referencia y crecen geométricamente a partir de L0. Actualmente, nuestro monorrepositorio utiliza agrupaciones de archivos pack de hasta el nivel L4.

Estas agrupaciones cumplen una doble función. Cuando una agrupación está lo bastante llena, un proceso de compactación se hace cargo de ella y genera un único archivo pack. Esto mejora la relación de compresión al reorganizar los objetos dentro del archivo pack. Pero también mejora el rendimiento de la negociación de fetch al actuar como una lista de saltos sobre las versiones de referencia. Al recorrer simultáneamente los índices de cada nivel, Pando puede seleccionar en tiempo logarítmico un conjunto de paquetes para enviar a un cliente. Estas agrupaciones de archivos pack, estén reempaquetadas o no, permiten planificar los fetches de forma más eficiente y reducen las exigencias tanto de rendimiento como de latencia para Pando.

Compaction in Pando

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

Compaction in progressPublished cover

La compactación combina en segundo plano archivos pack de muchas versiones de referencia, lo que permite realizar fetches más eficientes con menos archivos.

A diferencia de lo que ocurre en muchos servidores Git, los archivos pack se conservan en todos los niveles y no se descartan ni se eliminan mediante recolección de basura. Los paquetes de todos los niveles se utilizan continuamente para atender a los clientes, según los have y want que indiquen en un fetch. Además, para los clientes que usan Pando como API, leer un objeto cualquiera de un archivo pack suele exigir leer menos datos de un L0 que de un L4, porque es menos probable que esté comprimido.

La compactación en Pando reduce significativamente el volumen total transferido a los clientes y permite que el coste de planificar los paquetes crezca de forma sublineal respecto al retraso del cliente. Esto es especialmente importante para los repositorios grandes y con cambios rápidos, que prevemos que se convertirán en la norma en el desarrollo de software.

Desarrollo de Pando

Desarrollamos Pando con Codex y pasamos del inicio del proyecto a producción en solo 10 semanas. Un solo ingeniero diseñó el núcleo de Pando, equipos de agentes lo implementaron y un pequeño grupo de ingenieros lo desplegó. Pando empezó a atender tráfico de producción en abril de este año y forma parte de un sistema más amplio de herramientas de gestión del control de versiones que se utiliza en OpenAI.

Empleamos una gran variedad de enfoques para validar el diseño de Pando. Aplicamos nuestros principios para crear bases de software más seguras⁠. Como era un proyecto que partía de cero, decidimos implementar Pando en Rust y sin dependencias heredadas. Escribimos una batería de pruebas de conformidad para que los agentes pudieran contrastar sus cambios con el comportamiento estándar de Git. También desarrollamos arneses de pruebas de simulación determinista para comprobar invariantes y propiedades del sistema, lo que nos permitió detectar y mitigar condiciones de carrera sutiles y regresiones en la negociación y la planificación de paquetes.

Muchos modelos contribuyeron a este sistema; a medida que aparecían nuevos modelos, los utilizábamos para mejorar el rendimiento y la fiabilidad de Pando. Con GPT‑6 Astra, los agentes escribieron código en Lean y TLA+ para formalizar una «teoría de Pando» que utilizamos para detectar y corregir errores de implementación y demostrar propiedades del sistema. Varias sesiones se ejecutaron sin interrupción durante cientos de horas para detectar y corregir casos límite en Pando. También ejecutamos agentes las 24 horas para someter Git y Pando a pruebas adversarias con modelos con capacidades Daybreak.

Despliegue de Pando

Desplegamos Pando inicialmente como un proxy que reenviaba las lecturas y escrituras a nuestro sistema de control de versiones existente. Esto nos permitió mantener sincronizados Pando y el sistema anterior, para trasladar el tráfico a Pando de forma segura y sin interrupciones del servicio.

Como parte del diseño de su caché, Pando también puede deduplicar y combinar solicitudes antes de reenviarlas. Esto nos permitió mantener nuestras herramientas existentes en buen funcionamiento mientras trasladábamos los sistemas a Pando uno a uno. A medida que migrábamos más cargas de trabajo a Pando, sus pushes iban llenando la caché de Pando y mejoraban la fiabilidad de nuestro backend anterior.

Para optimizar el rendimiento al usar Pando, empezamos a distribuir nuestra versión pública de Git en los equipos de los desarrolladores y dentro de la aplicación de escritorio de ChatGPT. Para migrar los clientes a Pando por fases, utilizamos una capa interna sobre la CLI de Git. Creamos muchas otras herramientas para gestionar solicitudes de cambios y revisiones de código, además de API para automatizaciones. Junto con el tráfico de Git, migramos cientos de integraciones que dependían de API heredadas, webhooks y descargas de repositorios.

El propio Codex ayudó a los desarrolladores y equipos a dar el paso. Creamos habilidades para preparar las copias de trabajo existentes y guiar a los agentes en el uso del nuevo sistema. Así, los desarrolladores solo tenían que pedirle a Codex que migrara sus entornos de desarrollo y servicios.

El cambio a Pando mejoró la fiabilidad de las fusiones y redujo la mediana de latencia de los pushes en casi un 80 %.

Pando puede servir un petabyte de datos de Git al día con solo unos pocos núcleos de CPU. Pando ha servido más de 57 terabytes de paquetes de Git en una sola hora, con más del 99 % de esos bytes procedentes de la caché de disco local.

Reutilizar paquetes permite que muchos clientes descarguen el mismo historial del repositorio con poco cómputo en los nodos que lo sirven. Este ejemplo muestra un único repositorio, pero Pando está diseñado para admitir muchos más. A medida que crece la demanda, podemos escalar de forma independiente la entrega de datos y la negociación de Git.

Aportaciones al proyecto Git

Desarrollar Pando sobre el protocolo y el cliente de Git nos ayudó a detectar errores y oportunidades para mejorar el rendimiento del propio Git.

Por ejemplo, Pando sirve paquetes reutilizables directamente a los clientes, de modo que su descarga e indexación pasaron a ser fundamentales para que el sistema fuera rápido y fiable. Ese trabajo sacó a la luz casos límite en las descargas simultáneas, la limpieza tras transferencias interrumpidas y la reanudación de descargas que ya habían terminado. Corregimos esas rutas de ejecución, añadimos validaciones al aceptar un paquete existente para un fetch y aportamos ese trabajo al proyecto original.

También necesitábamos saber en qué se consumían el tiempo y el ancho de banda, así que añadimos trazas para las descargas de packfile-uri y recuentos de bytes para la generación de paquetes. Así resultaba más fácil determinar si un fetch lento dedicaba el tiempo a generar paquetes o a transferirlos.

En la versión de Git de OpenAI⁠(se abre en una ventana nueva), el cliente puede descargar e indexar paquetes independientes en paralelo. Añadimos el concepto de «have explícitos» para que un servidor pueda anunciar el commit de punta actual de una rama por separado del historial que un cliente que hace push puede omitir de su paquete. En Pando, esto permite a los clientes usar --force-with-lease para comprobar que una rama de funcionalidad no ha cambiado y generar su paquete tomando como base solo la rama principal. Después, cualquiera que tenga ese historial de la rama principal puede hacer fetch de la rama usando el mismo paquete.

Varias de estas mejoras ya están en el proyecto original de Git, y muchas otras, en nuestro repositorio público. Seguimos trabajando con la comunidad de Git para que estas mejoras sean útiles también fuera de Pando. Puedes seguir ese trabajo en nuestro sistema público de seguimiento de parches⁠(se abre en una ventana nueva).

Próximos pasos

Al replantear las ideas convencionales sobre cómo escalar repositorios Git, Pando nos permite escalar nuestro monorrepositorio sin cambiar los flujos de trabajo de Git que ya utilizan nuestros ingenieros y agentes. En nuestro monorrepositorio, Pando ha permitido un mayor grado de paralelismo, ha mejorado la fiabilidad de las fusiones y ha reducido el tiempo necesario para publicar cambios de código. Además de nuestro monorrepositorio, estamos explorando otros usos de Pando dentro de OpenAI, incluidas cargas de trabajo en las que un gran número de agentes necesitan colaborar en un repositorio Git compartido.

A medida que ingenieros y agentes produzcan más software, más equipos ajenos a OpenAI se encontrarán con los límites que alcanzamos en el control de versiones. Actualmente estamos explorando formas de compartir con nuestros usuarios la tecnología que hace posible Pando.

Pando se apoya en dos décadas de experiencia de usuarios y administradores de repositorios Git. Seguiremos desarrollando Pando y trabajando con la comunidad de código abierto de Git para compartir las mejoras que puedan beneficiar al conjunto de sus usuarios.

Autor

OpenAI