Entornos GPU reproducibles: por qué se degradan los setups de ML, y cómo evitarlo

EEquipo Clodei6 min de lectura
Entornos GPU reproducibles: por qué se degradan los setups de ML, y cómo evitarlo

Un pipeline de diffusers que el lunes iba perfecto lanzó un ImportError el jueves. Nadie tocó el código. Nadie cambió los pins. El notebook era idéntico a la ejecución que funcionó tres días antes. Simplemente dejó de importar.

Le pasa a alguien cada semana, y casi nunca es culpa suya. Es un problema de packaging, y vale la pena entenderlo, porque la solución es la misma que el resto del software adoptó hace años y que el alquiler de GPU se saltó casi por completo.

Qué se rompió de verdad

Este es el mecanismo. El usuario había pineado diffusers a una versión exacta. Lo que no había pineado, porque casi nadie lo hace, era huggingface_hub, una librería de la que diffusers depende y que importa. Entre el lunes y el jueves, huggingface_hub publicó una versión nueva que eliminó una función auxiliar que diffusers seguía llamando. La siguiente vez que el contenedor montó su entorno, pip resolvió huggingface_hub a esa versión más reciente, porque nada le dijo lo contrario, y la cadena de imports se partió.

El código estaba congelado. La variable era el entorno. Ese es el bug entero.

Pinear torch==2.5 no pinea nada

Cuando escribes torch==2.5.1 en un requirements, has pineado exactamente un paquete. Instalarlo arrastra numpy, sympy, networkx, filelock, fsspec, una pila de wheels CUDA nvidia-cu*, triton, y las dependencias de cada uno de ellos. Añade diffusers, transformers y accelerate y un setup de fine-tuning realista resuelve a bastante más de cien paquetes. Un entorno que guardamos la semana pasada fijó 146.

Nombraste uno. pip eligió los otros 145 en vivo, en el momento de instalar, usando la versión compatible más reciente que hubiera en PyPI ese día.

Esos paquetes que no nombraste son tus dependencias transitivas: las dependencias de tus dependencias. Cada una tiene sus propios maintainers y su propia idea de qué cuenta como cambio que rompe. torch no se movió. Un paquete tres niveles por debajo sí, y bastó para tirar el entorno entero.

"La semana pasada iba" es el mismo bug que "en mi máquina funciona"

Las dos quejas son un solo problema, separado por tiempo en vez de por máquina. En ambas, el código está fijo y el entorno que hay debajo es, sin avisar, distinto. En tu portátil es distinto porque tu compañero resolvió sus paquetes otro día. Semanas después es distinto porque los resolverías otro día.

Los números de versión no te salvan aquí. El versionado semántico es una promesa de los maintainers, no una garantía que el instalador aplique, y buena parte del ecosistema de ML vive por debajo de 1.0, donde la convención dice explícitamente que no promete nada sobre estabilidad. Un salto de 0.24 a 0.25 puede borrar una función tranquilamente. Y hasta la release "menor" de un maintainer cuidadoso puede romper código que se apoyaba en un comportamiento que nunca documentó.

La tentación es pinear más paquetes a mano. No escala. El conjunto transitivo es demasiado grande para seguirlo, se reordena en cuanto cambia cualquier pin directo, y acabarías manteniendo un fichero de doscientas líneas que describe una instalación que nunca ejecutaste.

Qué hace falta de verdad para la reproducibilidad

Tres cosas, y necesitas las tres.

Primero, un lockfile de verdad con hashes. No la lista de paquetes que pediste, sino la lista completa de lo que acaba instalado, con cada paquete fijado a una versión exacta y al SHA-256 del fichero exacto. Así el instalador no tiene margen para improvisar. Si un wheel descargado no coincide con su hash, la instalación falla en voz alta en lugar de ejecutar en silencio algo que no probaste. Es lo que producen npm, cargo, poetry y uv, y es la parte que la mayoría de plataformas de GPU se saltan.

Segundo, una copia duradera de los artefactos. Un lockfile que apunta a PyPI solo sigue siendo reproducible mientras PyPI conserve esos ficheros exactos. Los maintainers retiran releases. Los proyectos se borran. Un hash que no puedes descargar es el recibo de un paquete que ya no puedes instalar, así que la reproducibilidad de verdad implica guardar los propios wheels en un sitio que controlas tú.

Tercero, verificación continua. Un lockfile demuestra que una instalación es idéntica byte a byte a la anterior. No demuestra que esa instalación siga funcionando: que los wheels de CUDA sigan cargando contra el driver de la máquina, que nada del catálogo se haya degradado para un entorno nuevo que resuelva hoy. La única forma de saberlo es instalarlo e importarlo de forma periódica, y enterarte antes de que lo haga un usuario.

Cómo lo hace Clodei

Las versiones no cambian solas. Cada imagen se construye a partir de un lockfile con hashes, y cada add-on que eliges al lanzar se resuelve contra ese mismo lock y se verifica por SHA-256 antes de instalarse. El stack que probaste es el que se ejecuta. No se cuela ninguna actualización sorpresa entre un lanzamiento y el siguiente.

Sigue funcionando un año después. Guardamos un mirror privado de los wheels exactos que usa cada entorno, direccionados por su hash de contenido, en nuestro propio almacenamiento en lugar de en PyPI. Guarda un entorno hoy y se reconstruye igual el año que viene, aunque esas versiones exactas ya se hayan retirado o desaparecido del índice público.

Se comprueba cada semana. CI instala e importa cada paquete del catálogo sobre las imágenes GPU reales, cada semana, más una pasada mensual que propone subidas de versión como pull requests revisables. Cuando algo se rompe aguas arriba, se rompe primero en nuestro pipeline, no en tu notebook.

Los entornos guardados capturan lo que pasó de verdad. Cuando guardas un entorno, registramos las versiones que se instalaron realmente, el conjunto resuelto y no la lista corta que pediste, y lo relanzamos exactamente desde ahí. Comparamos byte a byte el lock que entregamos contra el guardado antes de servirlo, así que un relanzamiento coincide hasta el fichero.

La parte honesta

Nada de esto es ingenioso. Lockfiles con hashes, un mirror de tus propios artefactos y un job de CI que reinstala todo son higiene de supply chain de toda la vida. Los que hacen aplicaciones lo tienen desde hace años. Lo que sí es raro es encontrarlo aplicado a una GPU que alquilas por minutos, donde lo normal es pip install contra un objetivo en movimiento y un encogimiento de hombros cuando se rompe.

Un entorno que se degrada en silencio no es el peaje de trabajar en ML. Es una decisión de packaging que alguien tomó por ti, y se puede tomar de otra forma. El entorno que guardas debería ser el que recuperas, esta semana y dentro de un año.

Leer a continuación