Escalar Git en OpenAI
Crear un sistema de control de código fuente a escala de agentes mientras invertimos en el ecosistema de Git.
Durante el último año, el tamaño de nuestro monorepositorio y las exigencias que debe afrontar han aumentado a un ritmo vertiginoso. Estaba claro que necesitábamos renovar nuestro sistema de control de código fuente para alcanzar el nivel de trabajo simultáneo que Codex hace posible.
Creamos Pando, un nuevo backend para repositorios Git, sin interrumpir el trabajo de ingeniería ni cambiar la forma en que nuestro equipo usa Git. Ahora, Pando sostiene todo el desarrollo en el monorepositorio 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 suficientemente rápido como para saturar todo el ancho de banda de red disponible.
Cumplimos estos objetivos sin perder la compatibilidad con el protocolo y el cliente de Git, lo que permitió que nuestros ingenieros y agentes adoptaran Pando sin problemas en sus flujos de trabajo existentes. En producción, Pando redujo la mediana de latencia de push a casi una quinta parte. Ahora entrega casi un petabyte de datos de Git al día.
Aquí explicaremos qué nos motivó a crear un nuevo sistema de control de código fuente y por qué decidimos mantener la compatibilidad con Git. Compararemos nuestro sistema con los enfoques habituales para escalar Git y describiremos qué hace a Pando especialmente capaz de satisfacer las necesidades de escalabilidad de OpenAI. También abordaremos los próximos pasos y cómo seguiremos contribuyendo a Git para admitir el trabajo a escala de agentes.
Codex ha cambiado la escala y el ritmo del desarrollo en OpenAI, al permitir que los ingenieros hagan mucho más trabajo en paralelo. Eso implica más ramas y pushes, más fusiones y más operaciones de fetch y clonación desde entornos de desarrollo, tareas de investigación e integración continua (CI).
Estas exigencias se potencian entre sí. Ahora, un ingeniero tiene varios cambios listos para CI al mismo tiempo, y cada push puede hacer que varias tareas obtengan el mismo commit mediante fetch.
Desde finales del año pasado, aproximadamente, los sistemas que sostienen nuestro monorepositorio empezaron a tener dificultades para seguir el ritmo. Sufrimos lentitud en las operaciones de fetch y push, además de fusiones fallidas, y en ocasiones la confiabilidad cayó por debajo del 90 %. ¿Habíamos alcanzado un límite de Git en sí o de la forma en que ofrecíamos acceso a nuestro monorepositorio? Git surgió de una comunidad que intercambiaba parches para el 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 monorepositorio, para nosotros era importante poder seguir construyendo sobre ese trabajo.
Vimos una forma de conservar ese flujo de trabajo conocido y, al mismo tiempo, reconstruir el backend para procesar pushes a distintas ramas en paralelo y entregar el mismo historial a muchos lectores simultáneamente. Usar Git como base de nuestro flujo de control de código fuente significaba que las mejoras al cliente podían ser útiles más allá de OpenAI. Queríamos aportar esas mejoras de nuestra copia de Git(se abre en una nueva ventana) al proyecto original y compartirlas con toda la comunidad.
Escalar un sistema de control de código fuente 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.
Primero, Pando coordina las actualizaciones de cada rama de forma independiente. Las actualizaciones de ramas independientes pueden avanzar en paralelo, y un lector que hace fetch de una rama obtiene el historial necesario para esa revisión. La contrapartida es que un lector que consulta varias ramas puede verlas en estados correspondientes a distintos momentos.
Segundo, Pando reutiliza archivos de paquetes (el formato de Git para comprimir varios objetos juntos) entre operaciones de fetch, en lugar de generar uno individual para cada cliente. Un cliente puede descargar algunos objetos que ya tiene, pero el servidor puede entregar los mismos bytes a muchos clientes con poco procesamiento.
Estas decisiones nos permiten escalar las escrituras entre ramas y atender las lecturas desde cachés. Para ver cómo encajan estas decisiones, empecemos por cómo Git almacena y transfiere el historial.
Git debe poder mantener sincronizadas dos partes de un repositorio: objetos y 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 padres, y así forman el historial de tu repositorio.
Los objetos se identifican mediante un hash de su tipo y contenido. Los objetos también son inmutables: registrar un archivo modificado crea un nuevo blob, 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 nuevo commit y actualiza la referencia de la rama para que apunte a él.
Los objetos de Git se referencian entre sí por su ID: los commits apuntan a árboles, y los árboles, a otros árboles y 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 de paquetes, lo que permite guardar objetos similares como deltas entre sí.
Git puede almacenar blobs como archivos separados o empaquetarlos juntos, codificando un blob como un delta respecto de 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 le indica al servidor qué commits solicita y cuáles ya tiene. El servidor encuentra los objetos alcanzables desde los commits solicitados, excluye aquellos que sabe que el cliente ya tiene y envía el resto en un archivo de paquetes con el mismo formato que Git usa en disco. Una clonación completa comienza sin ningún objeto, por lo que necesita todos los objetos alcanzables desde el historial que se está clonando.
Un push transfiere objetos en la dirección opuesta y solicita al servidor que actualice una o más referencias. El cliente usa el conjunto de referencias anunciadas por el servidor para evitar enviar objetos que este ya tiene. Por lo 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 ofrecer el nuevo valor de una referencia, debe poder proporcionar los objetos alcanzables desde ese valor. El servicio también necesita 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 hacia nuevos commits distintos. Los sistemas que imponen un único orden para todas las escrituras en un repositorio pueden hacer que los pushes a ramas independientes tengan que esperar unos por otros.
Durante el diseño de Pando, evaluamos enfoques convencionales para escalar el control de código fuente 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 de escritura anticipada compartido (WAL, un registro de cambios que se escriben antes de aplicarse) en almacenamiento de blobs.
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 de paquetes 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 necesita varias rondas de comunicación con 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 verifica que siga apuntando al commit que el cliente espera. Luego, el coordinador envía un mensaje de preconfirmación a las réplicas que aceptaron. Una vez que la mayoría acusa recibo del mensaje de preconfirmación, el coordinador les indica que realicen la actualización. A partir de ese momento, cualquier réplica al día puede anunciar el nuevo commit de punta de la rama desde su copia local del repositorio.
Mantener réplicas completas permite atender lecturas localmente y garantiza que los clientes obtengan una vista consistente de todas las referencias, sin importar qué réplica elijan. Como consecuencia, las referencias se actualizan de una en una, y cada push requiere varios viajes de ida y vuelta para coordinar la actualización de la referencia entre las réplicas.
Esperar a que haya cuórum hace que el desempeño de los discos de las réplicas y la latencia de comunicación afecten la ruta de ejecución del push. A medida que crece el volumen de escrituras, las réplicas deben seguir el ritmo tanto de los paquetes entrantes como del tráfico de lectura.
En el diseño basado en WAL que modelamos, el servicio Git usa un archivo de manifiesto compartido en almacenamiento de blobs para ordenar las actualizaciones de todo el repositorio. Cuando un servidor recibe un push, sube el archivo de paquetes 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, agrega a un archivo de manifiesto compartido en el almacén de blobs una entrada que registra la actualización de la referencia y apunta al archivo de paquetes. Luego, el servidor Git actualiza el archivo de manifiesto mediante una operación de comparar e intercambiar. Esta operación reemplaza de forma atómica el archivo de manifiesto o, de lo contrario, rechaza el cambio. Si otro escritor lo actualizó mientras tanto, el servidor lee las entradas intermedias, comprueba que su actualización de referencia siga siendo válida y vuelve a intentarlo con el archivo de paquetes que ya subió.
Los escritores pueden subir sus archivos de paquetes 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 exitosa de comparar e intercambiar limita la frecuencia con la que el repositorio puede publicar un nuevo archivo de manifiesto. Un almacén de blobs de baja latencia hace que este paso sea rápido. 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 de paquetes 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 aún necesita leer e indexar los paquetes del almacén de blobs antes de poder entregarlos localmente. Si las escrituras entrantes superan el ritmo de ese trabajo, las réplicas se atrasan y los clientes esperan más para hacer fetch de los commits recién enviados mediante push.
Esto nos lleva a Pando. Pando es una nueva arquitectura para ofrecer acceso a repositorios Git, diseñada para escalar a muchos clientes que hacen push simultáneamente. Así funciona:
Antes de desarrollar Pando, intuíamos que podíamos cumplir nuestros requisitos sin renunciar a Git, pero sí necesitábamos flexibilizar algunas condiciones que la mayoría de los sistemas Git consideran indispensables:
Consistencia en todo el repositorio: ofrecer una instantánea consistente de todas las referencias en un momento dado requiere 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 de paquetes únicos para cada cliente: si dos clientes con historiales locales distintos hacen fetch de la misma rama, el servidor puede necesitar generar un paquete diferente para cada uno a fin de evitar enviar objetos que ya tienen. Crear estas respuestas consume CPU y operaciones de E/S de disco, lo que dificulta escalar a grandes volúmenes de lectores.
En cambio, Pando ofrece un modelo de consistencia menos estricto (consistencia causal) para escalar las escrituras y reutiliza archivos de paquetes para escalar las lecturas.
Cuando un cliente hace push a una rama, Pando guarda el archivo de paquetes entrante como un blob inmutable en un almacén de blobs. Cada vez que la rama avanza, Pando registra en un almacén de metadatos una versión de referencia que contiene el nuevo commit de punta, el paquete que lo proporcionó 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.
Hacer fetch de un commit o una referencia desde Pando requiere reproducir en el cliente un conjunto de versiones de referencia (archivos de paquetes), para reconstruir el estado del cliente que creó la versión de referencia solicitada. Esto ofrece una forma de consistencia más débil en todo el repositorio, a la vez que 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 lee varias ramas no puede suponer que esas lecturas describen un mismo momento en todo el repositorio.
Un push a la rama principal agrega 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 obtenerla en un solo paquete.
Esta es una forma de consistencia causal. Para hacer fetch de una referencia, el lector observa el historial del escritor que la produjo. Durante un push, Pando controla qué historial compartido acuerdan el cliente y el servidor, para garantizar que los clientes produzcan archivos de paquetes más fáciles de compartir.
En consecuencia, Pando no ofrece un estado de todas las referencias correspondiente a un mismo momento durante las operaciones de Git, ni un orden de las actualizaciones de referencias dentro de una clonación o un fetch. Por ejemplo, supongamos que las ramas foo y bar se actualizan durante la negociación de una clonación completa. 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 individual.
En esta simulación de los tres modelos de consistencia, configuramos 1000 clientes para que cada uno seleccione una rama distinta y luego haga pushes repetidos de commits únicos. Medimos el tiempo total transcurrido y la latencia entre push y fetch. Esta última es una métrica importante para las cargas de trabajo, ya que mide el tiempo de ida y vuelta que necesitan los usuarios y agentes para colaborar o utilizar 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 alta gama. En esta simulación, flexibilizamos la consistencia en todo el repositorio para la 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.
Ejecuta esta simulación para comparar Pando con otros dos enfoques. Elige la infraestructura, desde hardware estándar hasta opciones de alta gama, el número de clientes y otros ajustes para comparar los resultados.
El sistema de confirmación en tres fases modelado muestra una latencia de escritura significativamente mayor bajo carga a medida que sus discos se saturan. Por su parte, los sistemas de registro de escritura anticipada muestran una latencia de lectura significativamente mayor por el mismo motivo, ya que los servidores tienen dificultades para mantener el ritmo de lectura del registro. Tanto en pruebas reales con nuestro monorepositorio como en esta simulación, Pando admite muchos miles de clientes que pueden hacer push simultáneamente sin afectar el desempeño bajo carga.
Al procesar un push, Pando valida el archivo de paquetes 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 el lector necesita, y este los descarga. Ningún servidor necesita aplicar archivos de paquetes a un repositorio local ni realizar operaciones costosas de desempaquetado y empaquetado. Como los servidores de Pando se ocupan principalmente de la negociación y la selección de archivos de paquetes, pueden ejecutarse en hardware estándar.
Esto permite que Pando escale hasta los límites de los clientes, la red que los conecta con Pando y el servicio de almacenamiento de blobs. Con capacidad suficiente, cada cliente que trabaja en una rama independiente puede transferir paquetes tan rápido como lo permita su propia conexión. Para atender a más clientes a esa velocidad, podemos agregar 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.
Un agente edita archivos, crea un commit localmente y ejecuta git push, que crea un archivo de paquetes con esos cambios para actualizar una rama. Pando recibe el paquete y lo transmite al almacenamiento de blobs. Tras validar el push entrante con una decodificación de una sola pasada, Pando usa una transacción de varias claves para registrar una versión de referencia en nuestro almacén de metadatos y actualizar de forma atómica el puntero de la referencia. Esto permite que Pando ofrezca consistencia de “lectura de tus propias escrituras”. Toda esta operación de escritura requiere 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 lecturas de metadatos, donde 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 de paquetes, y el cliente los descarga a través de Express, un servicio que los almacena en cachés regionales.
Este diseño se basa en los tres servicios centrales de la arquitectura de Pando: push, fetch y compactación.
Cuando un cliente hace push, un servidor Git convencional actúa como un jugador de póquer que muestra todas sus cartas: revela al cliente todas las ramas que tiene. Luego, el cliente reúne los commits, árboles y blobs para crear un archivo de paquetes 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 un enfoque diferente. En lugar de mostrar todas sus cartas, solo anuncia al cliente un conjunto selecto de ramas. Esto se hace por dos motivos:
Ofrecer una vista consistente de todas las referencias es costoso. Con millones de ramas, el anuncio de referencias por sí solo puede superar los 100 MiB. Para ofrecerlo, cada servidor debe mantener un mapa actualizado, lo que implica un costo considerable de coordinación, o realizar una consulta a la base de datos cuyo costo crece con el número de referencias.
Al proporcionar al cliente solo un subconjunto de las referencias, este genera un paquete que depende únicamente de lo que el servidor le permite excluir explícitamente. El seguimiento de las dependencias permite que Pando reutilice el archivo de paquetes que un cliente genera en un push y lo entregue 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 producir paquetes más pequeños. Sin embargo, para reutilizar estos archivos de paquetes, el servidor debe poder comprobar que los clientes que hacen fetch tienen todas las dependencias. De lo contrario, el archivo de paquetes podría omitir objetos que el cliente necesita.
Por otro lado, si un servidor Git no anuncia ninguna referencia, el cliente generaría un archivo de paquetes con todo el historial del repositorio. Cualquier otro cliente puede usar ese archivo de paquetes, pero con un costo considerable: hacer fetch de cualquier rama equivale, en la práctica, a una clonación completa.
Anunciar más referencias produce archivos de paquetes más pequeños. La mayor parte de la reducción de tamaño puede lograrse anunciando solo una.
De forma predeterminada, cuando un cliente hace push de una nueva rama de funcionalidad, Pando anuncia como base únicamente una rama designada como principal. Esto mantiene constante el tamaño del anuncio a medida que el repositorio incorpora ramas, pero tiene sus 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 no está en la rama principal.
Esto aumenta el tamaño de los pushes sucesivos a la rama, que pueden contener objetos redundantes, y consume más ancho de banda del cliente. En nuestro monorepositorio, la mediana del tamaño de los archivos de paquetes producidos de esta manera fue inferior a 20 KiB, y el percentil 99 fue inferior a 3 MiB. Para nosotros, era una contrapartida razonable porque un lector que ya tiene el historial anunciado de la rama principal puede hacer fetch de la rama con ese único archivo de paquetes.
Después de 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 partes síncronas y asíncronas. La actualización síncrona de metadatos garantiza que los clientes puedan hacer fetch del commit recién actualizado de inmediato. Esta operación escribe una nueva versión de referencia y actualiza el puntero para que apunte a ella. Guarda el nuevo commit de punta, las dependencias usadas para negociar el archivo de paquetes y un enlace al archivo enviado mediante push.
Al mismo tiempo, se registra trabajo asíncrono en la misma transacción, 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 requerir solo un número constante de lecturas y escrituras en el almacén de metadatos, independientemente del tamaño del push.
Es importante destacar que los servidores de Pando pueden aceptar de forma independiente pushes a referencias no relacionadas. La única clave por la que hay contención 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 agregando servidores de Pando, unidades de escalado del almacén de metadatos o almacenes de blobs.
Cuando un cliente hace 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 elementos want y el de los alcanzables desde los elementos have, comprime los objetos resultantes y los envía al cliente. Aunque el servidor Git de código abierto cuenta con diversos índices locales para acelerar esta operación, generar estos archivos de paquetes puede ser costoso y, por lo general, no se prestan bien al almacenamiento en caché.
En cambio, Pando usa las asignaciones de commits registradas durante los pushes para buscar los elementos have y want del cliente y resolverlos a un conjunto de versiones de referencia que contienen esos commits. Cada una de esas versiones de referencia enlaza a un archivo de paquetes que Pando recibió previamente. El servidor usa esta información para determinar qué necesita el cliente y selecciona los archivos de paquetes 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.
Fetch 27 trunk packs.
Select a branch to include its trunk dependencies.
Pando asigna los elementos have y want del protocolo Git a una secuencia de versiones de referencia. Solicitar una rama secundaria implica solicitar también el historial hasta sus dependencias en la rama principal.
El protocolo Git solo permite enviar un archivo de paquetes en la respuesta HTTP de una clonación o un fetch. Sin embargo, una función poco valorada del protocolo Git, packfile-uri, permite que un servidor devuelva enlaces para descargar paquetes adicionales por separado. Pando usa exclusivamente esta función y transmite un archivo de paquetes vacío dentro de la respuesta. Para admitir esta función, OpenAI aportó varias mejoras a packfile-uri en el proyecto original.
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. Express, un servicio de entrega de paquetes, mantiene una caché de nuestro almacén de blobs, y otro servicio mantiene una caché del almacén de metadatos. Estas cachés permiten que Pando atienda solicitudes de un ID de commit o de una lectura especial de una referencia usando únicamente el estado almacenado en la caché de ese clúster. Usamos hashing consistente para dirigir las solicitudes de archivos de paquetes a los servidores de Express, con tasas de aciertos de caché superiores al 99 %.
Este sistema de cachés ofrece un rendimiento extremadamente alto, capaz de saturar la capacidad de red entre los clientes y Pando. Con estas URI de archivos de paquetes y cachés locales, comprobamos que las operaciones de clonación y fetch saturan enlaces de red de 10 Gbps utilizando únicamente máquinas virtuales estándar de proveedores de nube.
En la mayoría de los servidores Git, el reempaquetado y la recolección de basura son necesarios para mantener el desempeño de un repositorio en crecimiento. Ambos procesos recuperan espacio, pero consumen muchos recursos de cómputo y E/S.
En un repositorio que cambia con rapidez, como el monorepositorio de OpenAI, no es viable enviar una URI de archivo de paquetes individual por cada push realizado desde el último fetch. Cada día, un cliente tendría que descargar decenas de miles de archivos de paquetes para poner al día su rama principal.
En cambio, Pando combina paquetes en segundo plano para reducir esta sobrecarga, con un diseño similar a la compactación de los árboles de fusión estructurados por registro. Cada push produce lo que Pando denomina un archivo de paquetes “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 de paquetes en agrupaciones que abarcan una secuencia de versiones de referencia y crecen geométricamente a partir de L0. Actualmente, nuestro monorepositorio usa agrupaciones de archivos de paquetes de hasta nivel L4.
Estas agrupaciones cumplen una doble función. Una vez que una agrupación está lo suficientemente llena, un proceso de compactación se hace cargo de ella y produce un único archivo de paquetes. Esto mejora la relación de compresión al reorganizar los objetos dentro del archivo de paquetes. También mejora el desempeño 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 de paquetes, reempaquetadas o no, ofrecen una solución más eficiente para planificar los fetches 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.
La compactación combina en segundo plano archivos de paquetes de muchas versiones de referencia, lo que permite hacer fetches más eficientes con menos archivos.
A diferencia de lo que ocurre en muchos servidores Git, los archivos de paquetes se conservan en todos los niveles: no se descartan ni se eliminan mediante recolección de basura. Los paquetes de todos los niveles se usan continuamente para atender a los clientes, según los elementos have y want que indican en un fetch. Además, para los clientes que usan Pando como API, leer un objeto arbitrario de un archivo de paquetes suele requerir 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 costo de planificar los paquetes crezca de forma sublineal respecto del atraso del cliente. Esto es especialmente importante para los repositorios grandes que cambian rápidamente, algo que esperamos que se vuelva habitual en el desarrollo de software.
Creamos 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 puso en producción. Pando comenzó 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 de código fuente utilizadas 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 nuevo, optamos por implementar Pando en Rust y sin dependencias heredadas. Escribimos una suite de pruebas de conformidad para que los agentes pudieran contrastar sus cambios con el comportamiento estándar de Git. También desarrollamos arneses de ejecución para pruebas de simulación determinista que nos permiten comprobar invariantes y propiedades del sistema. Así pudimos 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 estuvieron disponibles nuevos modelos, los usamos para mejorar el desempeño y la confiabilidad de Pando. Con GPT‑6 Astra, los agentes escribieron código en Lean y TLA+ para formalizar una “teoría de Pando” que usamos para encontrar y corregir errores en la implementación y demostrar propiedades del sistema. Varias sesiones continuas se ejecutaron durante cientos de horas para encontrar y corregir casos extremos en Pando. También ejecutamos agentes las 24 horas para realizar pruebas adversarias de Git y Pando con modelos que cuentan con capacidades de Daybreak.
Inicialmente, implementamos Pando como un proxy que reenviaba las lecturas y escrituras a nuestro sistema de control de código fuente 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 de su diseño de 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 por uno. A medida que migrábamos más cargas de trabajo a Pando, sus pushes llenaban la caché de Pando y mejoraban la confiabilidad de nuestro backend anterior.
Para optimizar el desempeño al usar Pando, comenzamos 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 etapas, usamos una capa interna que envolvía la interfaz de línea de comandos 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 hacer la transición. Creamos habilidades para preparar las copias de trabajo existentes y orientar a los agentes en el uso del nuevo sistema. Como resultado, los desarrolladores solo tenían que pedirle a Codex que migrara sus entornos de desarrollo y servicios.
El cambio a Pando mejoró la confiabilidad de las fusiones y redujo la mediana de latencia de push en casi un 80 %.
Pando puede entregar un petabyte de datos de Git al día con apenas unos pocos núcleos de CPU. Pando ha entregado más de 57 terabytes de paquetes de Git en una sola hora, con más del 99 % de esos bytes provenientes de la caché de disco local.
La reutilización de paquetes permite que muchos clientes descarguen el mismo historial del repositorio con poco procesamiento en los nodos que lo entregan. Este ejemplo muestra un solo repositorio, pero Pando está diseñado para admitir muchos más. A medida que crece la demanda, podemos escalar la entrega de datos y la negociación de Git de forma independiente.
Crear Pando sobre el protocolo y el cliente de Git nos ayudó a identificar errores y oportunidades para mejorar el desempeño del propio Git.
Por ejemplo, Pando entrega paquetes reutilizables directamente a los clientes, por lo que su descarga e indexación se convirtieron en un aspecto central para lograr un sistema rápido y confiable. Ese trabajo reveló casos extremos en las descargas simultáneas, la limpieza tras transferencias interrumpidas y la reanudación de descargas que ya habían finalizado. Corregimos esas rutas de ejecución, agregamos 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. Por eso, agregamos trazas para las descargas de packfile-uri y conteos de bytes para la generación de paquetes, lo que facilitó 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 nueva ventana), el cliente puede descargar e indexar paquetes independientes en paralelo. Agregamos el concepto de “elementos 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 que los clientes usen --force-with-lease para comprobar que una rama de funcionalidad no haya avanzado y crear su paquete tomando como base solo la rama principal. Cualquier cliente que tenga ese historial de la rama principal puede entonces 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 más allá de Pando. Puedes seguir ese trabajo en nuestro registro público de seguimiento de parches(se abre en una nueva ventana).
Al replantear las ideas establecidas sobre cómo escalar repositorios Git, Pando nos permite escalar nuestro monorepositorio sin cambiar los flujos de trabajo de Git que nuestros ingenieros y agentes ya utilizan. En nuestro monorepositorio, Pando ha permitido un mayor grado de paralelismo, ha mejorado la confiabilidad de las fusiones y ha reducido el tiempo necesario para publicar cambios de código. Más allá de nuestro monorepositorio, también exploramos otros usos de Pando dentro de OpenAI, incluidas cargas de trabajo en las que grandes cantidades de agentes necesitan colaborar en un repositorio Git compartido.
A medida que los ingenieros y agentes produzcan más software, más equipos fuera de OpenAI se encontrarán con los límites que alcanzamos en el control de código fuente. Actualmente exploramos formas de compartir con nuestros usuarios la tecnología que hace posible Pando.
Pando se apoya en dos décadas de experiencia de quienes usan y operan repositorios Git. Seguiremos desarrollando Pando y trabajando con la comunidad de código abierto de Git para compartir las mejoras que beneficien a sus usuarios en general.


