Saltar al contenido
Aula en la nube
Ficha técnica · 05/11/2026Curso de especialización · IATodo en el centro, sin nube

TITAN: la infraestructura de IA de un instituto

Del metal al chat de clase: inferencia local, pool elástico, RAG y las lecciones de cada decisión

Esta página es la versión técnica de cómo está montado el sistema de IA del IES Simarro: el servidor, el modelo, el balanceador, el gateway, la plataforma y la red. Está pensada para gente que ya sabe de qué va un token, un batch o un índice de similaridad: cada bloque va al detalle —flags concretos, métricas reales extraídas de la máquina en vivo y las decisiones que costaron un incidente—. Los números de esta página se extrajeron del sistema en marcha el 5 de noviembre de 2026, no de una presentación comercial.

3 × 96 GB

RTX PRO 6000 Blackwell (GDDR7, SM120)

512k

tokens de contexto por réplica vLLM

87 %

acierto de prefix cache (métrica viva)

96

secuencias simultáneas de techo

01El servidor TITAN

Un único nodo físico en el centro hace de motor de todo: inferencia LLM, síntesis de voz, OCR, embeddings y la plataforma web. No es un rack de datacenter: es una workstation de gama profesional reconvertida en servidor.

RecursoDetallePor qué importa
3 × GPUNVIDIA RTX PRO 6000 Blackwell (Max-Q WS), 96 GB GDDR7 c/uArquitectura SM120: tensor cores nativos FP4/FP8 → el modelo cabecero va cuantizado mixed NVFP4+FP8 sin penalización
CPUAMD Threadripper PRO 9995WX · 96 núcleos / 192 hilos, un solo dominio NUMAEl offload de pesos a RAM necesita CPU y memoria suelta; y nadie quiere paginar a swap una carga de 180 GB
RAM498 GB DDR5Descarga de checkpoints de +180 GB y VLLM_PLE_CPU_OFFLOAD por réplica (§03)
AlmacenamientoRAID software md0 · 15 TB, 16 % usadoModelos, corpus RAG, grabaciones de clase y backups en el mismo sitio: copia 3-2-1 sin contratos
SO / motorUbuntu 26.04 LTS · Docker 29 · Compose · vLLM en imagen a medida (build 8e685d1) · CUDA 13Todo versionado en ficheros compose; nada de instaladores mágicos ni estado solo «en la máquina»
Driver595.99.02Detalle con historia: si el driver sube sin reiniciar el host, nvidia-smi miente (NVML driver/library mismatch) aunque la inferencia siga volando

La idea de «una máquina, todo dentro» es deliberada: el centro no paga API por token, no saca datos del edificio y no depende de Internet para que un claustro entero use la IA a la vez. El coste real es la disciplina operativa que describe esta página: pools elásticos, watchdogs y runbooks, porque aquí no hay un equipo SRE de un proveedor detrás.

02El modelo: por qué este y no una API

El modelo cabecero es Qwen3.8-Flash-Next con checkpoint mixed-NVFP4-FP8 —revisión pinneada, montada solo lectura; 183,8 GB en disco, 72,07 GiB de pesos por GPU según el log de arranque del motor—. No se eligió por el ranking de moda, sino por tres propiedades verificables en su config.json:

PropiedadValor realConsecuencia de ingeniería
MoE muy disperso512 expertos, 10 activos por token (moe_intermediate_size 640)La velocidad sigue a los parámetros activos, no a los totales: capacidad grande con coste de decode pequeño
Atención híbrida48 capas: 36 lineales + 12 completas (una cada 4)El KV cache solo crece con las 12 capas completas → 512k de contexto caben en 14,5 GiB por réplica
GQA extremo24 cabezas de consulta, solo 2 de KV · head_dim 256KV por token diminuto: multiplicar réplicas no multiplica la memoria de contexto
Contexto262.144 nativo → 524.288 con YaRN (factor 2,0, mrope interleave vía --hf-overrides)Un dossier de oposición de cientos de páginas entra entero en una sola petición, sin truncar ni ventana deslizante

La aritmética del KV cache, con cifras de la máquina

Cada token en contexto cuesta KV en las 12 capas completas: 2 cabezas × 256 dimensiones × 2 (K y V) × 2 bytes (bf16) ≈ 24,6 KB/token. La reserva se fija a mano con --kv-cache-memory=15569256448 (14,5 GiB, sin dejar que el perfilado automático decida), y el motor lo traduce en 604.705 tokens de KV —literal del log—: cuadra con el coste por token. Con esa reserva la máxima concurrencia para una petición del contexto completo es 1,15×: una conversación puede ocupar los 512k enteros, pero entonces la tarjeta queda llena de contexto y las demás esperan. La reserva de KV es, literalmente, el presupuesto de memoria compartida entre todas las conversaciones vivas.

Cuánto cuesta «no usar la API» (y por qué sale a cuenta)

La pregunta de todo experto: ¿por qué no una API de terceros, que no obliga a mantener nada? Cuatro razones que sostienen esto en un centro público: protección de datos con menores (los datos del alumnado no salen del edificio); coste marginal cero (una tanda de ~25 exámenes generados por agentes, con ≈21k tokens de salida cada uno, sería una factura considerable en cualquier API: aquí son horas de GPU amortizada); disponibilidad soberana (la IA funciona con la fibra del instituto caída); y valor docente: el alumnado del ciclo puede auditarlo todo —del cmdline de vLLM a los percentiles de latencia— porque el sistema es suyo.

03El pool elástico: tres réplicas, un endpoint

Sobre las 3 GPU corren réplicas idénticas de vLLM con TP=1 (una tarjeta cada una: el reparto de tensores entre GPUs solo compensa cuando el modelo no cabe, y aquí cabe cuantizado; además TP añade comunicación inter-GPU y acopla los fallos). Un HAProxy 3.0 en red host las agrega tras un único endpoint OpenAI-compatible, :8000/v1, con balance leastconn —cada petición va a la réplica con menos conexiones vivas, que en streaming LLM es el proxy correcto de «menos carga»—.

La palabra elástico es la parte interesante: las réplicas no tienen el mismo estatus.

GPUPapelPolítica
GPU 0 · qwen0El chat del institutoNunca sale del pool: es el único server sin disabled en el cfg. Si HAProxy arranca de cero y nadie reconcilia, el endpoint sigue sirviendo. Es la garantía de servicio mínima; los scripts de gestión se niegan a bajar de una réplica sana sin --force
GPU 1 · qwen1Capacidad prestadaArranca fuera del pool y entra solo tras un smoke test con contenido: que genere texto, no solo que responda /health. Así una recarga del balanceador no puede mandar tráfico a una GPU que se prestó a otro modelo
GPU 2 · qwen2Prestada + auxiliaresCompartida con voz (TTS), OCR y embeddings: o Qwen o auxiliares, nunca ambos (una réplica pide ~90 GiB). Los cuatro servicios auxiliares forman un solo interruptor; mientras la GPU sirve Qwen, la voz y la ingesta RAG están caídas

El ciclo de vida lo gobierna un runbook con tres verbos: titanctl release N --purpose X (drena sin cortar: espera hasta 30 minutos a que terminen las generaciones en vuelo, y solo entonces para y libera la GPU para otro modelo), titanctl restore N (arranca, espera, smoke test y alta en el pool) y titanctl reconcile (ajusta el pool a la realidad de las GPUs). Un timer systemd cada 30 s hace de guardián: si el endpoint muere o la realidad y la configuración divergen, lo reconcilia él.

Los flags de cada réplica, tal cual corren

docker exec titan-qwen38-0 sh -c 'tr "\0" " " < /proc/1/cmdline'
vllm serve /model --served-model-name qwen3.8-flash-next
  --tensor-parallel-size=1          # una GPU por réplica, sin TP
  --max-model-len=524288            # 512k vía YaRN (hf-overrides factor 2.0)
  --gpu-memory-utilization=0.97     # 94,4 GiB de los 96
  --kv-cache-memory=15569256448     # 14,5 GiB A MANO → 604.705 tokens de KV
  --max-num-seqs=32                 # techo por réplica → 96 secuencias en el pool
  --enable-prefix-caching           # 87 % de acierto medido (§04)
  --enable-auto-tool-choice --tool-call-parser=qwen3_coder
  --reasoning-parser=qwen3          # razonamiento visible en el stream
  env: VLLM_PLE_CPU_OFFLOAD=1       # pesos PLE (Per-Layer Embeddings) en RAM del host
  env: HF_HUB_OFFLINE=1             # imposible «tirar de la descarga»: aire puro

Leer /proc/1/cmdline como fuente de verdad no es un truco cosmético: cuando se cambian flags de rendimiento de noche sobre varias réplicas, lo normal es que una quede con el flag nuevo y las otras con el viejo. La auditoría de uniformidad se hace al proceso, no al compose.

04Números medidos (no estimados)

Todo esto sale de /metrics de vLLM, del CSV de stats de HAProxy y de docker logs, con el método anotado —que es la mitad del dato—. La regla de oro de la medición en casa: un pico con 1-2 peticiones corriendo no es un techo de throughput, y un bench de 5 hilos no es la carga real.

QuéNúmeroCómo se midió
Prefill agregado≈ 17.400 tok/sDelta de vllm:prompt_tokens_total en las 3 réplicas, ventana 45 s, con 17-21 sesiones de agentes reales activas
Generación agregada825 tok/s en carga real · 441 tok/s bench idle · 104 tok/s en serieÍdem con generation_tokens_total; el bench usa ignore_eos y max_tokens fijo (5 flujos × 600 tok)
TTFT medio≈ 390 msSum/count de time_to_first_token_seconds de una réplica (1.136 peticiones desde su arranque)
Latencia e2e media12,2 s/peticiónSum/count de e2e_request_latency_seconds: son respuestas largas de agentes, no frases de chat
Prefix cache22,6 M aciertos / 26,0 M consultas = 87 %Contadores prefix_cache_hits/queries_total de una réplica, acumulados desde su arranque
Ratio prompt:generación26,0 M : 1,28 M ≈ 20 : 1El workload real es prefill-dominated: los agentes reenvían contextos enormes que el prefix-cache rescata en gran parte
Curva de escalado4 sesiones ≈ 380 tok/s · 16 ≈ 1.150 · 25 a la vez = contraproducenteBins de «Avg generation throughput … Running: N reqs» en los logs del logger de vLLM

Lección aprendida

La curva de escalado destruyó la hipótesis más ingenua: con 3×32 secuencias disponibles, meter 25 agentes a la vez baja el rendimiento por sesión (mediana de 77-108 min por subagente, y una tanda que hubo que relanzar) frente a oleadas de 8-12. Cada agente extra no añade GPU: reparte la misma. Y el cuello real resultó ser el prefill, no el decode —por eso las palancas efectivas fueron prompts más ligeros y afinidad de prefix-cache, y no subir --max-num-seqs a ciegas (16→32 sí se midió: +57 % de agregado; la siguiente palanca es cabecear ráfagas, no subir el techo medio)—.

05Gateway: quién llama a qué

Entre los consumidores y el pool hay dos puertas con funciones distintas:

PuertaQué esPara quién
HAProxy :8000Balance puro al pool vLLM: health-check GET /health cada 3 s (fall 2 rise 2), redispatch ante error de conexión, timeouts de 1 h para streaming largoLa plataforma del centro: backend, grabadora, agentes internos. Una URL vale para cualquier número de réplicas vivas
LiteLLM :8300Gateway con API keys por cliente, presupuestos, registro de uso y max_parallel_requests por clave; expone variantes del mismo modelo (p. ej. -thinking con enable_thinking)Herramientas externas de agentes (CLI de código, asistentes) que entran por VPN bajo TLS con <K>/llmapi/v1</K>: una clave = identidad + límite + trazabilidad
haproxy.cfg (extracto real, réplicas 1 y 2 fuera del pool a propósito)
frontend openai_front
  bind 0.0.0.0:8000
  monitor-uri /haproxy-health
  default_backend qwen_pool

backend qwen_pool
  balance leastconn
  option httpchk
  http-check send meth GET uri /health
  http-check expect status 200
  default-server check inter 3s fall 2 rise 2
  retries 2
  option redispatch
  server qwen0 127.0.0.1:8100            # chat: siempre en el pool
  server qwen1 127.0.0.1:8101 disabled   # prestada: entra con smoke test
  server qwen2 127.0.0.1:8102 disabled

Lección aprendida

El rate-limit que parecía «GPU saturada» era la clave de API: una tanda de agentes devolvía 429 con el pool vacío. Diagnóstico que corta el ruido: un 429 que llega en menos de 100 ms es la puerta, no el motor. El límite real vive en la tabla de tokens de LiteLLM (max_parallel_requests: 16 → 64) y subirlo no exige reiniciar nada. Regla de dimensionado: una tanda de N agentes paralelos exige límite ≥ N en su clave, o tormenta de 429 y turnos muertos a medias sin que nada se caiga.

06La plataforma del centro

La interfaz que usa el claustro y el alumnado —chat con voz, resúmenes, corrección de exámenes, corpus, grabadora de clases— es un monorepo desplegado en el mismo titan:

PiezaStackDetalle técnico relevante
BackendPython · FastAPI · SQLAlchemy 2 async · Alembic (52 migraciones)Proxy fino: no ejecuta modelos, llama al pool por HTTP (host.docker.internal:8000/v1, el truco host-gateway de Linux para cruzar la red de Docker). Streaming SSE y WebSocket con proxy_buffering off en nginx para que el primer token salga ya
FrontendsReact 18 (chat) + React 19 (dashboard) · Vite · TailwindSPAs servidas por el mismo nginx: un solo origen, sin CORS. i18n en tres lenguas (es/en/va)
DatosPostgreSQL 15 · Redis 7Roles admin / profesorado / alumnado + departamento; el alta de usuarios viene de Moodle (auth externa), no hay registro libre
Permisos«Herramienta = permiso»Cada herramienta del chat es una constante en un frozenset concedible y su tarjeta solo se pinta si está concedida. Añadir herramienta = constante + include_router con su dependency: el RBAC no se reescribe nunca
Workersasyncio in-process, sin Celery ni Redis-colaSemáforo por tipo de tarea + recover en el lifespan con backend de proceso único. Menos componentes que caen: el patrón completo levanta con un docker compose up -d —y muere igual de rápido—

El flujo del chat, entero: navegador → nginx (TLS, CA propia) → FastAPI → HAProxy → réplica vLLM → stream SSE de vuelta. Todo local, todo en la misma máquina, todo auditable.

07RAG, OCR y voz: el resto del stack

Vector search: Qdrant + BGE-M3 híbrido

El gateway RAG (servicio propio, FastAPI en :8001) usa Qdrant con cuatro colecciones separadas por semántica de recuperación, no por capricho: materiales del centro (la que consulta el chat), normativa —allí la recuperación significa otra cosa: manda la vigencia, las fuentes se enseñan con enlace y todo se re-ingesta por curso—, exámenes y ejercicios. Los embeddings los produce BAAI/bge-m3 (1024 dimensiones, denso + sparse + multilingüe) servido por vLLM en CPU: es IO-bound y no debe comer VRAM del chat. La recuperación es híbrida (BM25 + vectorial + reranking): en un corpus normativo con términos exactos, el vectorial puro pierde frente a lo que siempre supo Lucene.

El corpus del centro: cinco decisiones que dan clase

La herramienta con la que el centro mete documentos en su base de conocimiento tiene detalles que merecen explicarse uno a uno: (1) va a la misma colección que consulta el chat, por eso un documento subido a las 10:00 cuenta a las 10:01 sin tocar el camino crítico; (2) el texto se extrae dónde toca —txt/csv/ xlsx los lee el backend in-process, y PDF/DOCX/imágenes van al gateway, que tiene el OCR por GPU DeepSeek-OCR— porque la GPU que presta el OCR se libera a veces y el corpus no puede depender de eso para un simple .txt; (3) dos modos de ingesta, inmediata o nocturna (barrido a las 02:30: un despliegue a media mañana no se pone a vectorizar cuarenta documentos en horario de clase); (4) metadatos obligatorios desde el día 1 —departamento, nivel, etiquetas—, porque nadie rellena una taxonomía después sobre cien documentos ya subidos; y (5) verificación de ingesta con búsqueda de prueba desde el panel, en vez de «ya saldrá o no saldrá» en una conversación.

Voz y vídeo: TTS y render como servicios

Qwen3-TTS (1,7 B, 9 voces, 10 idiomas) sirve la API de audio de OpenAI (POST /v1/audio/speech) en seis réplicas de ~4,8 GiB de VRAM cada una —la co-localización forzada de §03; el modelo cabe en cualquier GPU libre—. El renderizador produce las vídeo-lecciones (guion → render → locución) en otro contenedor, y la grabadora de clase sube solo audio: faster-whisper en GPU transcribe con semáforo=1 para no pisar el pool, y el resumen se genera contra el mismo pool de siempre. Cada capacidad es un servicio con su puerto, su healthcheck y su palanca de apagado —nada de librerías incrustadas que solo puedas usar «aquí»—.

08Segunda línea: los DGX Spark

Titan es un punto de fallo único, y eso en una infraestructura de centro no se lamenta: se combate con una segunda línea barata. Dos pares de NVIDIA DGX Spark (módulos GB10, 128 GB unificados CPU+GPU), cada par con su HAProxy propio (:8400 y :8401), y LiteLLM repartiendo entre pares por least-busy: la misma clave de usuario vale en cualquiera de los dos; si un par se apaga, el otro absorbe sin que nadie cambie nada.

Detalles que a un experto le van a gustar: cada par puede correr el modelo en perfil DUAL —paralelismo entre los dos nodos por fibra ConnectX-7 con NCCL— o en perfil independiente (dos máquinas pequeñas separadas, cada una con su pool). El par B todavía no puede con el DUAL: el cable funciona —los dos nodos se ven por IPv6 link-local a 0,9 ms—, pero su netplan no declara la dirección IPv4 link-local que NCCL necesita. Y un clásico del campo: los hostnames van cruzados respecto a las IPs (spark1 es la .114); el alias SSH manda, nunca el orden de las direcciones. Es un clúster de cuatro nodos sin presupuesto de clúster de cuatro nodos, con sus runbooks gemelos del de titan.

09Red y seguridad: perímetro sin nube

El sistema vive en una red privada del centro. La superficie expuesta está pensada al revés que la de un SaaS: por defecto nada, y lo que se saca se saca con identidad.

TramoMontajeRazón técnica
Dentro del servidorPrácticamente todos los servicios publican solo en 127.0.0.1: vLLM 8100-8102, HAProxy :8000, LiteLLM :8300, backend :8200, render :8893…Nada de la IA es alcanzable desde la LAN del centro ni desde fuera: la única puerta pública del host es el nginx :443 de la plataforma
TLSnginx :443 (TLS 1.2/1.3) con CA propia del centro, certificado desplegado en los equiposSin depender de un emisor externo para servicios que nunca salen del edificio; la confianza distribuida es además control de dispositivos
Acceso remotoOpenVPN cliente (2.7 con DCO) sobre UDP, con ping 10 / ping-restart 120El acceso de confianza entra <em>dentro</em> de la red, no al revés: titan nunca inicia conexiones salientes hacia casa
Agentes desde fueraVPN + LiteLLM bajo TLS con clave por herramientaIdentidad, límite de concurrencia por clave y registro de uso: el rate limiting es un feature, no un obstáculo

Lección aprendida

El túnel zombi. Un VPN puede estar active (running), con su interfaz UP y en verde en el monitor, y no pasar un solo paquete: pasa tras un re-handshake TLS si la config no declara keepalive. Los síntomas que delatan el túnel y no la GPU: 100 % de pérdida al gateway del túnel, el rx_packets de tun0 plano con el tx creciendo, mientras el servidor responde a ping por la puerta normal. Diagnóstico: mide deltas rx/tx, no mires el estado del servicio. Fix permanente en la config: ping + ping-restart → se recupera solo en ~2 minutos.

10Operación: el sistema se defiende solo (porque le obligamos)

Infraestructura local en un centro no es «montar y olvidar»: es un ciclo de vigilancia, medición y runbooks. Lo que hay en marcha hoy:

MecanismoQué hace
Watchdog horarioRevisa en cadena túnel, ruta, SSH, motor de vídeo y el nginx :443; silencio = OK, alerta si algo cae tras reintento
Guardián del pool (30 s)Reconcilia el endpoint del pool: si :8000 muere, lo recupera él. Es red de seguridad, no permiso para romperlo
Reconciliar al arrancarTras un reinicio del host, el pool se ajusta solo a la realidad de las GPUs: qué réplicas están vivas manda, no la configuración
Smoke test con contenidoUna réplica no entra al pool por responder /health: tiene que generar texto
BASELINES.mdToda medida se anota con su configuración y su contexto (tanda activa vs idle): sin contexto, un número no es un dato, es una anécdota
drain → recreate → restoreEl ritual para cambiar flags de vLLM en producción sin cortar conversaciones en vuelo (y sin descubrir a las 3 de la mañana que solo se aplicó a una de tres réplicas)

Lección aprendida

El incidente que educó la operación: se recargó HAProxy con docker restart… y el endpoint estuvo muerto 12 minutos. Por qué: con restart: unless-stopped, una parada deliberada no se relanza — el gestor entiende que querías pararlo—. Desde entonces: recarga solo en caliente y validada (comprobar config → -sf → reconciliar), jamás docker restart del balanceador, y el guardián de 30 s como red. Lección general: en un sistema pequeño, cada componente crítico necesita un gesto protegido y su opuesto prohibido, escritos en un runbook.

Y el cierre, que es también la demostración: quien ha redactado esta ficha es un modelo corriendo sobre la máquina que describe, leyendo sus propios logs y métricas —la infraestructura auditándose a sí misma—. Eso es exactamente lo que ninguna API de terceros te deja hacer, y es la mejor diapositiva de la tarde.

Ficha técnica · IES Simarro · 5 de noviembre de 2026. Los números se extrajeron en vivo del sistema en marcha (docker inspect, /proc/1/cmdline, /metrics de vLLM, stats de HAProxy y logs de arranque). Sin claves, sin IPs de acceso, sin datos de usuarios. Los picos de throughput con pocas peticiones son cargas reales, no barridos controlados de concurrencia: consúltese el método de cada cifra antes de citarla.