Acelerando la compilación 2.5 veces con desacoplamiento de dependencias y contenerización de pruebas
El tiempo de compilación puede verse incrementado por dependencias internas y externas complejas que evolucionan a lo largo del proceso de desarrollo, así como por cambios en los entornos de compilación, como el sistema operativo o las arquitecturas de hardware. A continuación se presentan problemas comunes que se pueden encontrar al trabajar en proyectos de IA o MLOps a gran escala:
Compilación prohibitivamente larga - La integración de código se realiza cientos de veces al día. Con cientos de miles de líneas de código implementadas, incluso un pequeño cambio podría dar lugar a una compilación completa que normalmente tarda una o más horas.
Entorno de compilación complejo - El código del proyecto debe compilarse en diferentes entornos, que implican distintos sistemas operativos, como CentOS y Ubuntu, dependencias subyacentes, como GCC, LLVM y CUDA, y arquitecturas de hardware. Y la compilación en un entorno específico normalmente puede no funcionar en un entorno diferente.
Dependencias complejas - La compilación del proyecto implica más de 30 dependencias entre componentes y de terceros. El desarrollo del proyecto a menudo conduce a cambios en las dependencias, lo que inevitablemente causa conflictos de dependencias. El control de versiones entre dependencias es tan complejo que actualizar la versión de las dependencias afectará fácilmente a otros componentes.
La descarga de dependencias de terceros es lenta o falla - Los retrasos de red o las bibliotecas de dependencias de terceros inestables causan descargas lentas de recursos o fallos de acceso, lo que afecta gravemente la integración de código.
Al desacoplar las dependencias e implementar la contenedorización de pruebas, logramos disminuir el tiempo promedio de compilación en un 60% mientras trabajábamos en el proyecto de código abierto de búsqueda de similitud de embeddings Milvus.
Desacoplar las dependencias del proyecto
La compilación de proyectos suele implicar una gran cantidad de dependencias de componentes internos y externos. Cuantas más dependencias tiene un proyecto, más complejo se vuelve administrarlas. A medida que el software crece, se vuelve más difícil y costoso cambiar o eliminar dependencias, así como identificar los efectos de hacerlo. Se requiere mantenimiento regular durante todo el proceso de desarrollo para garantizar que las dependencias funcionen correctamente. Un mantenimiento deficiente, dependencias complejas o dependencias defectuosas pueden causar conflictos que ralentizan o detienen el desarrollo. En la práctica, esto puede significar descargas de recursos con retraso, fallos de acceso que afectan negativamente la integración de código y más. Desacoplar las dependencias del proyecto puede mitigar defectos y reducir el tiempo de compilación, acelerando las pruebas del sistema y evitando cargas innecesarias en el desarrollo de software.
Por lo tanto, recomendamos desacoplar las dependencias de su proyecto:
- Divida los componentes con dependencias complejas
- Use diferentes repositorios para la gestión de versiones.
- Use archivos de configuración para gestionar la información de versiones, las opciones de compilación, las dependencias, etc.
- Agregue los archivos de configuración a las bibliotecas de componentes para que se actualicen a medida que el proyecto itera.
Optimización de compilación entre componentes — Extraiga y compile el componente relevante de acuerdo con las dependencias y las opciones de compilación registradas en los archivos de configuración. Etiquete y empaquete los resultados de la compilación binaria y los archivos de manifiesto correspondientes, y luego súbalos a su repositorio privado. Si no se realiza ningún cambio en un componente o en los componentes de los que depende, reproduzca sus resultados de compilación de acuerdo con los archivos de manifiesto. Para problemas como retrasos de red o bibliotecas de dependencias de terceros inestables, intente configurar un repositorio interno o usar repositorios espejo.
Para optimizar la compilación entre componentes:
1.Crear un gráfico de relaciones de dependencias — Use los archivos de configuración en las bibliotecas de componentes para crear un gráfico de relaciones de dependencias. Use la relación de dependencias para recuperar la información de versión (Git Branch, Tag y Git commit ID) y las opciones de compilación y más de los componentes dependientes tanto upstream como downstream.
Figura 1.
2.Comprobar dependencias — Genera alertas sobre dependencias circulares, conflictos de versiones y otros problemas que surgen entre componentes.
3.Aplanar dependencias — Ordena las dependencias mediante búsqueda en profundidad (DFS) y fusiona por delante los componentes con dependencias duplicadas para formar un grafo de dependencias.
Figura 2.
4.Usa el algoritmo MerkleTree para generar un hash (Root Hash) que contenga las dependencias de cada componente según la información de versión, las opciones de compilación y más. Combinado con información como el nombre del componente, el algoritmo forma una etiqueta única para cada componente.
Figura 3.
5.Según la información de la etiqueta única del componente, comprueba si existe un archivo de compilación correspondiente en el repositorio privado. Si se recupera un archivo de compilación, descomprímelo para obtener el archivo de manifiesto para la reproducción; si no, compila el componente, marca los archivos objeto de compilación generados y el archivo de manifiesto, y súbelos al repositorio privado.
Implementar optimizaciones de compilación dentro de los componentes — Elige una herramienta de caché de compilación específica del lenguaje para almacenar en caché los archivos objeto compilados, y súbelos y almacénalos en tu repositorio privado. Para la compilación de C/C++, elige una herramienta de caché de compilación como CCache para almacenar en caché los archivos intermedios de compilación de C/C++, y luego archiva la caché local de CCache después de la compilación. Estas herramientas de caché de compilación simplemente almacenan en caché los archivos de código modificados uno por uno después de la compilación, y copian los componentes compilados del archivo de código sin cambios para que puedan participar directamente en la compilación final. La optimización de la compilación dentro de los componentes incluye los siguientes pasos:
- Añade las dependencias de compilación necesarias al Dockerfile. Usa Hadolint para realizar comprobaciones de cumplimiento en el Dockerfile y asegurarte de que la imagen se ajusta a las mejores prácticas de Docker.
- Replica el entorno de compilación según la versión del sprint del proyecto (versión + build), el sistema operativo y otra información.
- Ejecuta el contenedor del entorno de compilación replicado y transfiere el ID de la imagen al contenedor como una variable de entorno. Aquí tienes un comando de ejemplo para obtener el ID de la imagen: “docker inspect ‘ — type=image’ — format ‘{{.ID}}’ repository/build-env:v0.1-centos7”.
- Elige la herramienta de caché de compilación adecuada: Entra en tu contenedor para integrar y compilar tus códigos y comprueba en tu repositorio privado si existe una caché de compilación adecuada. Si es así, descárgala y extráela en el directorio especificado. Después de compilar todos los componentes, la caché generada por la herramienta de caché de compilación se empaqueta y se sube a tu repositorio privado según la versión del proyecto y el ID de la imagen.
Optimización adicional de la compilación
Nuestra compilación inicial ocupa demasiado espacio en disco y ancho de banda de red, y tarda mucho tiempo en desplegarse, por lo que tomamos las siguientes medidas:
- Elegir la imagen base más ligera para reducir el tamaño de la imagen, p. ej., alpine, busybox, etc.
- Reducir el número de capas de la imagen. Reutilizar las dependencias tanto como sea posible. Fusionar varios comandos con “&&”.
- Limpiar los productos intermedios durante la creación de la imagen.
- Usar la caché de imagen para crear la imagen tanto como sea posible.
A medida que nuestro proyecto continúa avanzando, el uso del disco y los recursos de red comenzaron a dispararse a medida que aumentaba la caché de compilación, mientras que algunas de las cachés de compilación están infrautilizadas. Luego hicimos los siguientes ajustes:
Limpiar regularmente los archivos de caché — Comprobar regularmente el repositorio privado (usando scripts, por ejemplo) y limpiar los archivos de caché que no han cambiado durante un tiempo o que no se han descargado mucho.
Caché de compilación selectiva — Almacenar en caché solo las compilaciones que demandan muchos recursos y omitir el almacenamiento en caché de compilaciones que no requieren muchos recursos.
Aprovechar las pruebas en contenedores para reducir errores, mejorar la estabilidad y la fiabilidad
Los códigos deben compilarse en diferentes entornos, que implican una variedad de sistemas operativos (p. ej., CentOS y Ubuntu), dependencias subyacentes (p. ej., GCC, LLVM y CUDA) y arquitecturas de hardware específicas. El código que se compila correctamente en un entorno específico falla en un entorno diferente. Al ejecutar pruebas dentro de contenedores, el proceso de pruebas se vuelve más rápido y más preciso.
La contenedorización garantiza que el entorno de pruebas sea consistente y que una aplicación funcione según lo esperado. El enfoque de pruebas contenedorizadas empaqueta las pruebas como contenedores de imagen y construye un entorno de pruebas verdaderamente aislado. Nuestros testers descubrieron que este enfoque era bastante útil, lo que terminó reduciendo los tiempos de compilación hasta en un 60%.
Garantizar un entorno de compilación consistente — Como los productos compilados son sensibles a los cambios en el entorno del sistema, pueden producirse errores desconocidos en diferentes sistemas operativos. Tenemos que etiquetar y archivar la caché del producto compilado de acuerdo con los cambios en el entorno de compilación, pero son difíciles de categorizar. Así que introdujimos la tecnología de contenedorización para unificar el entorno de compilación y resolver dichos problemas.
Conclusión
Al analizar las dependencias del proyecto, este artículo presenta diferentes métodos para la optimización de la compilación entre componentes y dentro de ellos, proporcionando ideas y mejores prácticas para construir una integración continua de código estable y eficiente. Estos métodos ayudaron a resolver la integración lenta de código causada por dependencias complejas, unificar las operaciones dentro del contenedor para garantizar la consistencia del entorno y mejorar la eficiencia de la compilación mediante la reproducción de los resultados de compilación y el uso de herramientas de caché de compilación para almacenar en caché los resultados intermedios de compilación.
Estas prácticas mencionadas anteriormente han reducido el tiempo de compilación del proyecto en un 60% de media, mejorando enormemente la eficiencia general de la integración de código. De cara al futuro, seguiremos paralelizando la compilación entre componentes y dentro de ellos para reducir aún más los tiempos de compilación.
Se utilizaron las siguientes fuentes para este artículo:
- “Desacoplar árboles de código fuente en componentes de nivel de compilación”
- “Factores a considerar al añadir dependencias de terceros a un proyecto”
- “Sobrevivir a las dependencias de software”
- “Comprender las dependencias: un estudio de los desafíos de coordinación en el desarrollo de software”
Sobre el autor
Zhifeng Zhang es un ingeniero senior de DevOps en Zilliz.com que trabaja en Milvus, una base de datos vectorial de código abierto, e instructor autorizado de la universidad de software de código abierto LF en China. Recibió su licenciatura en Internet de las cosas (IOT) del Software Engineering Institute of Guangzhou. Dedica su carrera a participar y liderar proyectos en el área de CI/CD, DevOps, gestión de infraestructura de TI, herramientas Cloud-Native, contenedorización y optimización del proceso de compilación.
Sigue leyendo

DeepSeek-OCR Explained: Optical Compression for Scalable Long-Context and RAG Systems
Discover how DeepSeek-OCR uses visual tokens and Contexts Optical Compression to boost long-context LLM efficiency and reshape RAG performance.

Announcing the General Availability of Zilliz Cloud BYOC on Google Cloud Platform
Zilliz Cloud BYOC on GCP offers enterprise vector search with full data sovereignty and seamless integration.

OpenAI o1: What Developers Need to Know
In this article, we will talk about the o1 series from a developer's perspective, exploring how these models can be implemented for sophisticated use cases.



