Dos cajas de clase DGX Spark, un modelo: una copia en cada una supera a la división
Operaciones
Durante siete semanas, nuestro modelo de lenguaje local se ejecutó dividido en dos máquinas NVIDIA GB10 con paralelismo de tensores. El 22 de octubre lo sustituimos por una copia completa del modelo en cada máquina y un proxy de menor conexión delante. El rendimiento pasó de unos 180 a 210 tokens por segundo, y ahora cualquiera de las máquinas puede reiniciarse sin dejar el modelo fuera de servicio.
Por qué lo habíamos dividido
Cada máquina tiene 128 GB de memoria compartida entre la CPU y la GPU. En septiembre, el modelo cabía en una máquina, pero su caché apenas lo hacía: la compilación de vLLM que usábamos no podía mantener la caché KV de este modelo en FP8, por lo que la caché estaba en BF16, y una máquina tenía espacio para 499.110 tokens de ella. Eso es menos de dos solicitudes en el contexto completo del modelo de 262.144 tokens. Dividido entre ambas máquinas con paralelismo de tensores a través de su enlace de red directo, la caché creció a más de tres millones de tokens. Habíamos dividido el modelo por espacio, no por velocidad.
Qué cambió
Una compilación más reciente de 4 bits del mismo modelo (NVFP4, creada con destilación consciente de la cuantización) ocupa unos 71 GiB de memoria, y una compilación comunitaria de vLLM para GB10 (eugr/spark-vllm-b12x) ejecuta su caché en FP8. Ahora una máquina contiene todo el modelo y una caché de 0,76 a 1,08 millones de tokens, espacio para 2,9 a 4,1 solicitudes en contexto completo, según cuánta memoria le asigne la máquina. Ambas copias usan la decodificación especulativa propia del modelo (MTP, cuatro tokens de borrador). Antes de cambiar, ejecutamos nuestra prueba de llamadas a herramientas en la nueva compilación: obtuvo 86, la compilación que reemplazó obtuvo 75.
Lo que medimos
| Dividido en dos máquinas | Una copia en cada máquina | |
|---|---|---|
| Caché KV | más de 3 millones de tokens | 0,76 y 1,08 millones de tokens |
| Solicitudes en contexto completo | unas 12 | 2,9 y 4,1 |
| Rendimiento bajo nuestra carga | unos 180 tokens/s | 210 tokens/s (111 + 99) |
| Inicio | unos 20 minutos | 5 a 13 minutos por copia |
| Reinicio de una máquina | el modelo está caído | la otra copia sirve |
Una copia por sí sola, según el número de solicitudes que atiende a la vez: con prompts cortos, 1 solicitud daba 36 tokens por segundo, 4 daban 82, 8 daban 109 y 16 daban 150. Con los prompts más largos de nuestro agente de correo, 4 daban 81, 8 daban 113 y 16 solo 87: más de ocho solicitudes de ese tipo, más lento.
Durante una tarde, una copia admitió como máximo 12 solicitudes a la vez y la otra 8. La primera producía de 91 a 93 tokens por segundo y terminaba de 26 a 27 solicitudes por minuto; la segunda, de 81 a 83 tokens por segundo y 22 solicitudes por minuto. Ahora ambas admiten 12.
Lo que sacrificamos
Espacio. El modelo dividido tenía una caché de más de tres millones de tokens; las copias tienen dos más pequeñas, y una solicitud solo puede usar la caché de la copia que la atiende. Nuestros agentes envían muchas solicitudes cortas y medianas, y para ellas el rendimiento era más importante. Si atiendes unas pocas contextos muy largos a la vez, dividir puede seguir siendo la mejor opción. Conservamos la configuración dividida, y volver a ella es un solo script.
Lo que salió mal durante el cambio
- Once minutos sin modelo, luego 30 segundos de errores 502. El modelo dividido tenía que detenerse antes de que una copia pudiera cargarse en las mismas máquinas, por lo que nuestros agentes esperaron unos once minutos. Cuando la primera copia se levantó, el proxy siguió respondiendo «502 no live upstreams» durante unos 30 segundos más: había marcado los backends como fallidos mientras estaban caídos (max_fails=3, fail_timeout=30s) y no los reintentaba hasta que expiraba ese tiempo.
- Un cliente que saltó el proxy. Uno de nuestros trabajos por lotes tenía la dirección de la primera máquina en su configuración, por lo que toda su carga fue a una copia mientras la otra tenía capacidad de sobra. Ahora todos los clientes pasan por el proxy; las direcciones propias de las máquinas son solo para comprobaciones de estado.
- Métricas que saltaban. Nuestro monitor de rendimiento leía los /metrics de vLLM a través del proxy. Con dos backends, las lecturas consecutivas provenían de copias distintas, los contadores saltaban de un lado a otro y el monitor informaba que el modelo estaba hambriento. Ahora lee cada copia directamente y las suma.
- Un watchdog antiguo que habría detenido la nueva copia. La configuración dividida tenía un watchdog que reiniciaba su servicio cuando el modelo dejaba de responder. El nuevo servicio declara Conflicts= con el anterior, por lo que ese reinicio habría detenido la nueva copia. Había que desactivarlo con
systemctl disable --now, no solo detenerlo, porque nuestra capa de operaciones reinicia las unidades que están habilitadas pero no en ejecución. - Memoria en la máquina que realiza otros trabajos. Una máquina también ejecuta nuestras bases de datos y compilaciones del sitio. Con gpu-memory-utilization de vLLM en 0.78, solo quedaban 5 GiB para todo lo demás, así que lo bajamos a 0.74 y reiniciamos solo esa copia mientras la otra asumía la carga. En estas máquinas, la parte de memoria que asignas a vLLM sale de la misma memoria que el resto.
Si tienes dos de estas máquinas
- Si el modelo y la caché que necesitas caben en una máquina, ejecuta una copia en cada una detrás de un proxy de mínima conexión. Divide el modelo solo cuando una máquina no pueda contener la caché que necesitas.
- Apunta todos los clientes al proxy, incluidos los trabajos por lotes y las configuraciones antiguas.
- Lee las métricas de cada copia directamente.
- Asigna a cada máquina su propia parte de memoria, según lo que más ejecute.
- Configura cuántas solicitudes atiende una copia a la vez según tu propio tráfico: para nuestra mezcla, 12 superaron a 8.
- Espera que el cambio deje el modelo fuera de servicio durante el tiempo que tarda una copia en iniciarse, y planifícalo.
Factory Floor incluye el proxy y las configuraciones de lanzamiento para una o dos máquinas, la comprobación de estado y la prueba de uso de herramientas que ejecutamos antes de cambiar de modelo, con los watchdogs y las salvaguardas de memoria para el resto de la máquina.