Dos sparks y un MoE: inferencia distribuida casera
Cómo unimos nuestras dos NVIDIA Spark para correr modelos que no caben en una sola: el setup exl3/vLLM que usamos en El Laboratorio, con sus errores y sus números reales.

La receta más ambiciosa del blog: la que da identidad a El Laboratorio. Dos máquinas, 256GB de memoria unificada sumados, y un objetivo: correr los modelos que en una GPU normal son ciencia ficción.
Los límites que empujaron al cluster
Con 121GB por máquina (en la práctica ~110GB útiles), un modelo de 97GB cuantizado entra justo… pero sin margen para la KV cache ni para el sistema. Contextos largos = swap = sufrimiento. La solución: repartir el modelo entre las dos sparks.
Opción A: Pipeline (capas) con llama.cpp RPC
La forma simple: cada máquina carga una parte de las capas del modelo.
# En la worker (gx10-2): sirve RPC
llama-server --rpc 0.0.0.0:50052 -m nada.gguf --no-webui
# En la head (gx10-1): carga el modelo distribuido
llama-server -m modelo-grande.gguf \
--rpc 100.64.10.12:50052 \
--host 0.0.0.0 --port 8080 \
-ngl 999 --ctx-size 32768
Resultado real: DeepSeek-V3-style MoE de ~97GB (UD-IQ3_XXS) corriendo con:
- decode: ~27 t/s
- prefill: ~370 t/s
- contexto 64K con KV en q8
Más que suficiente para chat y automatizaciones; no para servir 20 usuarios.
Opción B: vLLM con tensor parallel (misma máquina)
Si el modelo cabe en UNA spark pero quieres más throughput, --tensor-parallel-size no
aplica (una GPU unificada). Pero --data-parallel para servir varias copias sí:
# Dos instancias del mismo modelo en la misma máquina, puertos distintos
# y un nginx delante balanceando = doble throughput
Opción C: Router de modelos (lo que usamos a diario)
La configuración más útil no es “un modelo gigante”: es un router que decide qué máquina atiende qué:
┌─ gx10-1: modelo grande (MoE 97GB) ─── tareas serias
petición → nginx ───┤
└─ gx10-2: modelo pequeño (7-14B) ──── chat, automatización
Con n8n o un mini-script: peticiones simples al worker rápido, complejas al head. Latencia percibida 3x mejor, y el modelo grande no se calienta para responder “¿qué tiempo hace?”.
Números honestos (para que compares)
| Setup | Modelo | Decode | Prefill | Uso ideal |
|---|---|---|---|---|
| 1 spark, llama.cpp | 7B Q4 | ~60 t/s | ~800 t/s | chat fluido |
| 1 spark, llama.cpp | 70B Q3 | ~8 t/s | ~150 t/s | paciencia |
| 2 sparks (RPC) | MoE 97GB | ~27 t/s | ~370 t/s | el sweet spot ✓ |
| 2 sparks (router) | 14B + MoE | ~45 t/s (small) | — | casa completa ✓✓ |
(Medidas nuestras con GB10, contexto 32-64K, KV q8. Tu mileage may vary, pero poco.)
Los errores que nos hicieron más listos
- RPC por WiFi: 2 t/s y desesperación. El cluster va por ethernet o no va. (Los adapters Tailscale van genial para control, para inferencia: cable.)
- Contexto gigante por deporte: 256K de contexto comió toda la memoria unificada de las dos máquinas para una conversación de 10K tokens. KV cache = memoria. Mide antes.
- Dos modelos grandes en la misma máquina: OOM con estilo. Un modelo grande por máquina, y el resto cache pequeña.
- No monitorizar temperaturas: el throttling silencioso te roba 30% de rendimiento y
no te enteras.
nvtopo el panel de misión en la dash.
El resultado emocional
No es el cluster más rápido del mundo. Pero es nuestro. Y cuando sirves tu primera respuesta de un modelo de 100GB que vive en tu casa, con tu router casero decidiendo qué cerebro atiende cada petición… se te queda una cara de tonto que no paga nadie.