Logo IES Simarro
PP1-Simarro Centro de excelencia en IA y BD

Plataforma de Inferencia

Cómo servimos Qwen 3.8 Flash Next en paralelo sobre 1, 2 o 3 GPUs: pool elástico de réplicas vLLM, una cola de distribución HAProxy, gateway LiteLLM bilingüe en protocolos y un plan de memoria GPU→RAM. La ingeniería detrás de los números del banco de benchmarks del Titán.

1–3
réplicas TP=1
según la necesidad
:8000
endpoint único
HAProxy leastconn
2
protocolos
OpenAI + Anthropic
51,2 B
parámetros PLE
fuera de la VRAM

1. El problema de diseño

Servir un MoE de checkpoint mixto NVFP4+FP8 de 171 GiB en tres RTX PRO 6000 de 96 GB tiene un primer obstáculo aritmético: el modelo no cabe entero en ninguna tarjeta, y el tensor-parallel clásico (partir cada capa entre GPUs) introduce comunicación por PCIe/NVLink en el camino crítico de cada token. Nuestra elección fue la contraria: réplicas completas con paralelismo de tensor 1, y una cola de distribución delante. Cada GPU es un motor de inferencia autónomo; la elasticidad no la da el chip, la da la topología. Todo lo que sigue —memoria, balanceo, gateway— está diseñado alrededor de esa decisión.

2. Topología del pool

cliente (Claude Code · opencode · Hermes · web del centro) │ https (TLS con CA interna) ▼ nginx :443 /llmapi/ ▼ LiteLLM :4000 ← claves virtuales, límites, registro de consumo │ /v1/messages (Anthropic) │ /v1/chat/completions (OpenAI) ▼ HAProxy :8000 ← balance leastconn · health-check GET /health │ │ │ ▼ ▼ ▼ (solo loopback: nunca expuestas a la LAN) vLLM-0 vLLM-1 vLLM-2 GPU 0 GPU 1 GPU 2 cada réplica: pesos 72,1 GiB + KV 14,5 GiB :8100 :8101 :8102 + PLE offload ~100 GiB a DDR5 ECC

3. Servir con una, dos o tres GPUs: el pool es elástico

Con réplicas TP=1, escalar es una operación aritmética trivial — pero cada GPU tiene competidores. La GPU 2 del Titán también da servicio a TTS (6 voces), render de vídeo, OCR y embeddings. El pool se dimensiona por perfiles según la campaña:

Perfil Configuración Pico medido Concurrencia Cuándo lo usamos
3 réplicas GPU 0+1+2 dedicadas al LLM ~1.540–1.670 tok/s ~48 sesiones Máxima demanda docente/producción de contenido
2 réplicas (unificado) GPU 0+1 LLM · GPU 2 libre para voz/vídeo ~1.030 tok/s ~32 sesiones Estado por defecto del centro
1 réplica GPU 0 · mantenimiento del resto ~600 tok/s ~16 sesiones Updates, pruebas, fallo de una tarjeta

Números de nuestros benches y logs (ver benchmarks oficiales). El coste de sacrificar la tercera réplica —40 % de pico y un tercio de concurrencia— se asume deliberadamente: con dos réplicas completas la GPU 2 puede síntesis de voz y render de vídeo sin drenar el pool a mano cada vez. La tercera réplica está definida pero a replicas: 0: documentada y viva, a un flag de distancia.

Dos consecuencias elegidas de este diseño:

4. El plan de memoria: por qué TP=1 con offload gana al TP=2

  • Pesos (72,1 GiB GPU): la cuantización mixta NVFP4+FP8 de la familia Qwen deja los pesos que sí participan en cada token por debajo del umbral de los 96 GB. Con el bf16 original, TP=2 sería obligatorio — y pagaría comunicación entre GPUs en cada capa.
  • Tabla PLE — 51,2 B parámetros (RAM): el modelo arrastra una tabla de embeddings n-gram (PLE) que en BF16 ocupa ~95 GB y se accede de forma dispersa: es el componente ideal para vivir en DDR5 ECC y no en GDDR7. El offload a host (VLLM_PLE_CPU_OFFLOAD=1) libera la VRAM para lo que necesita los 1.792 GB/s de ancho de banda: pesos activos y KV.
  • KV cache (14,5 GiB GPU + paginado a RAM): la atención híbrida (36 capas lineales con estado fijo + 12 completas) recorta la KV a un cuarto; con 14,5 GiB reservados a mano caben 604.705 tokens de KV GPU, y el paginador extiende con la RAM cuando llegan ráfagas.
  • Balance por réplica: 72,1 + 14,5 + 0,3 (CUDA graphs) ≈ 87 GB de los 96 de VRAM · ~100–115 GiB de los ~450 visibles de RAM. Tres réplicas llenan el nodo sin fricción entre ellas porque cada una es autónoma.
# Producción real por réplica (abreviada; idéntica en las 3)
vllm serve /model \
  --served-model-name qwen3.8-flash-next \
  --tensor-parallel-size=1 --distributed-executor-backend=mp  # mp, no ray: requisito del offload PLE \
  --max-model-len=524288 \
  --gpu-memory-utilization=0.97 \
  --kv-cache-memory=15569256448 \
  --enable-prefix-caching \
  --enable-auto-tool-choice --tool-call-parser qwen3_coder \
  --reasoning-parser qwen3 --max-num-seqs=16 \
  --hf-overrides{"text_config":{"rope_parameters":{"rope_type":"yarn","factor":2.0,...}}}
env VLLM_PLE_CPU_OFFLOAD=1  VLLM_PLE_OFFLOAD_READY_TIMEOUT=1800
# nativo 262.144 ctx → YaRN×2 para 524.288; checkpoint con revisión pinneada, montado RO

5. La cola de distribución: HAProxy y lo que aprendimos balanceando

Con réplicas independientes, la calidad del servicio depende de la cola que hay delante. Nuestra configuración y las tres lecciones medidas que la justifican:

leastconn, no roundrobin

Con streams LLM largos lo que importa es cuántas generaciones vivas tiene cada réplica, no cuántas peticiones ha recibido nunca. El round-robin cuenta la historia pasada; el leastconn cuenta la realidad presente. Y los timeouts de la cola subidos a 1 h: una generación agéntica sobre un repositorio grande puede tardar muchos minutos y la cola no debe cortar el stream a mitad.

La afinidad por origen nos quemó (y la retiramos)

Probamos stick on src para no perder el prefijo cacheado de cada conversación agéntica. Error medido en 40 minutos: todo el tráfico entraba desde la misma IP (el contenedor del gateway), así que las tres sesiones se pegaron a la réplica 0 — 11 generaciones vivas y 5 encoladas en GPU 0, mientras dos GPUs enteras estaban al 0 %. Para afinidad por conversación hace falta una cabecera estable propagada (X-Session-Id); sin ella, leastconn reparte y eso es lo que importa. La publicamos como lección, no la borramos.

Salud agresiva, retirada limpia

Health-check GET /health con inter 3s fall 2 rise 2: una réplica que muere sale del pool en ~6 s (los 30 s del default dejaban una ventana de peticiones muriendo). Y el socket de administración de HAProxy es UNIX con grupo del usuario operador: drenar, poner en mantenimiento o devolver una réplica al servicio se hace sin sudo y sin exponer el plano de control por TCP.

El incidente del conector PLE (por qué cada réplica puede morir sin tumbar nada)

El 01-09-2026, bajo un benchmark de concurrencia 64, la réplica 0 cayó con el EngineCore entero. Causa raíz: la imagen de vLLM que usábamos traía el conector de offload PLE con su cola interna de maxsize=1; bajo carga el put_nowait lanzaba queue.Full y se llevaba por delante el motor. Fix: un conector corregido (cola dimensionada a max_concurrent_batches + pool de eventos CUDA para las copias GPU→host) que viaja dentro del propio checkpoint pinneado y se monta como overlay del paquete — cero descargas adicionales, revisión auditable. Este incidente es el argumento definitivo a favor de la topología de réplicas: murió un motor, el pool siguió respondiendo con las otras GPUs, y la réplica volvió tras el overlay sin que ningún cliente notara más que la cola de ese minuto.

6. El gateway: LiteLLM hablando dos dialectos

vLLM solo habla OpenAI; nuestro stack usa también Claude Code, que habla el Messages API de Anthropic (bloques tipados, tool_use, thinking). LiteLLM publica ambos protocolos (/v1/messages y /v1/chat/completions) sobre el mismo pool y añade lo que el motor de inferencia no debe gestionar nunca: claves virtuales por usuario, límites de gasto y registro de consumo. Delante, nginx publica el gateway por TLS con la CA interna del centro.

Dos perfiles de modelo, medidos y documentados en el propio fichero de configuración:

  • qwen3.8-flash-next (sin razonamiento): para respuestas de una tirada con formato fijo — clasificar, extraer, resumir. Medido: 182 tokens / 1,8 s frente a 2.154 tokens / 21,0 s del mismo prompt razonando.
  • qwen3.8-flash-next-thinking (con razonamiento): para trabajo agéntico. Sobre 222 turnos reales: el 45 % gasta <500 tokens de razonamiento, mediana 598 — el modelo se autorregula y solo se explaya donde decide algo. Con el pool ocioso el 76 % del tiempo en uso agéntico interactivo, esa latencia sale de capacidad que estaba parada.

7. Operación y control

¿Funciona? Los números, en los benchmarks oficiales

Todo lo descrito aquí se tradujo en 602 tok/s por GPU, 1.668 en el clúster y un TTFT mediano de 470 ms con prompts de 90K tokens — medidos en producción 24/7, con metodología y limitaciones publicadas.

VER BENCHMARKS DEL TITÁN →

Sobre este documento

Documento público y vivo de la plataforma de inferencia del IES Dr. Lluís Simarro, Centro de Excelencia Nacional en IA y Big Data. Descripción verificada contra el despliegue real del nodo Titán el 13 de septiembre de 2026 (composers, configuración de gateway y cola, logs de operación). Repositorio: aulaenlanube/pp1-simarro.

Volver al Manual del Titán