La noche en que mi computadora se reinició sola
Una noche, en mitad de un trabajo largo, toda mi computadora se apagó sola y volvió a encenderse. No el programa: la máquina. En un momento había un proceso corriendo; al siguiente estaba mirando una pantalla de inicio de sesión recién cargada, como si alguien hubiera tirado del cable de alimentación y lo hubiera vuelto a enchufar.
Había pasado días siendo cuidadoso con el único recurso que estaba seguro de que importaba. El fallo vino del único que nunca se me ocurrió vigilar.
El fallo sin mensaje
Lo más inquietante fue lo poco que había para leer. Sin stack trace, sin excepción, sin línea de registro que marcara el final. Solo un reinicio abrupto y, enterrado en los propios registros del sistema operativo, el equivalente digital de un encogimiento de hombros: el sistema se reinició sin apagarse limpiamente.
Un bug normal deja un cadáver. Este dejó una habitación vacía. Y un error sin mensaje es su propia clase de pista: normalmente significa que el fallo ocurrió en algún lugar por debajo de tu programa, en una capa que no se detiene a explicarse antes de llevárselo todo por delante.
Había presupuestado la memoria equivocada
Estaba ejecutando un lote de trabajo muy grande en la GPU, y había dimensionado ese lote con cuidado, contra la memoria de la GPU. Sabía cuánta VRAM tenía la tarjeta, sabía aproximadamente cuánto consumiría cada unidad de trabajo, y había hecho la aritmética para mantenerme con seguridad por debajo del techo. En lo que a mí respectaba, la memoria era un problema resuelto.
Estaba resuelto para la memoria equivocada. La VRAM de la tarjeta estuvo bien todo el tiempo; si hubiera estado mirando su medidor en el momento del fallo, habría parecido perfectamente tranquilo. El techo que realmente alcancé fue la RAM del sistema de la máquina anfitriona —la memoria ordinaria con la que funciona el resto de la computadora— y eso no lo había presupuestado en absoluto, porque en mi modelo mental simplemente no era la restricción.
A dónde se fue la RAM del anfitrión
Enviar un lote enorme de trabajo a una GPU no es gratis del lado del anfitrión. Antes de que la GPU pueda ejecutar nada, el trabajo tiene que prepararse, configurarse y entregarse, y esa preparación vive en la RAM del sistema. Cada pieza de esa configuración era modesta por sí sola. Multiplicada a lo largo de un lote lo bastante grande como para saturar la GPU, las piezas modestas se apilaron hasta convertirse en algo enorme.
La RAM del sistema se llenó. El sistema operativo hizo lo que hace cuando la memoria se agota: la volcó al disco, y luego también se quedó corto de eso. La presión severa de memoria no siempre se anuncia con una excepción capturable. Puede significar una asignación fallida, un proceso eliminado, o una máquina entera volviéndose inestable. La mía se volvió inestable lo bastante fuerte como para reiniciarse.
Por qué nunca lo vi venir
Lo frustrante en retrospectiva es que sí estaba monitoreando con cuidado: simplemente estaba monitoreando el indicador equivocado. Mis ojos estaban puestos en la GPU, el recurso que había optimizado, por el que me había preocupado y con el que me sentía astuto. Todo ese tiempo, la presión se acumulaba en un lugar en el que no tenía razón para mirar, porque ya había decidido que no era allí donde estaría el problema.
Ese es un patrón que ya he visto demasiadas veces como para llamarlo coincidencia: instrumentas la restricción en la que estás pensando, y te quedas ciego ante la que no. El bug no se esconde. Se sienta en un punto ciego que construiste tú mismo, al estar seguro de dónde estaba el límite.
La solución: espera el muro, y rebota en él
La reparación no fue un cálculo mejor. Fue renunciar a la idea de que podía calcular el tamaño seguro por adelantado en absoluto. En lugar de predecir exactamente cuán grande podía sobrevivir un lote en la máquina, dejé que la máquina me lo dijera: intentar el lote, y si choca con un error de memoria recuperable, tratar eso como una señal para retroceder: reducir la carga de trabajo de forma conservadora y reintentar, encogiéndola hasta que quepa.
Es un bucle humilde y pequeño, y es mucho más robusto de lo que mi cuidadosa aritmética jamás fue, precisamente porque no depende de que yo haya entendido cada costo oculto por adelantado. Ya no necesito saber exactamente dónde está el muro; para errores de memoria recuperables, el proceso puede tantear su camino hasta un tamaño viable en cualquier hardware en el que se ejecute. El agotamiento a nivel de anfitrión como el de aquella noche es el caso más difícil: todavía necesita límites externos y monitoreo, no solo un bucle de reintento.
Las suposiciones sobre los recursos son silenciosas hasta que dejan de serlo
La lección que me quedé tiene poco que ver con las GPU específicamente. Es que las suposiciones sobre los recursos son invisibles justo hasta el momento en que son catastróficas. Puedes tener toda la razón sobre el límite que estás imaginando y aun así verte derrotado por el que no estás imaginando, y el fallo, cuando llega, a menudo arriba sin explicación, porque la capa que se rompe está por debajo de la capa donde viven tus mensajes de error.
Así que dejé de intentar ser preciso con los recursos y empecé a intentar ser defensivo con ellos. La precisión asume que conoces todos los costos. La actitud defensiva asume que se te ha escapado al menos uno. En un sistema pensado para correr durante horas, sin supervisión, sobre una memoria que no controlas del todo, la segunda suposición es la única segura.
Un programa que se cuelga te deja un mensaje. Una máquina que se reinicia te deja un silencio. He llegado a tratar ese silencio como una de las cosas más honestas que mi hardware me ha dicho jamás: estabas presupuestando lo equivocado.
— Sin señales, sin retornos, esto no es asesoramiento de inversión.