Source document
Convivencia con el host: la IA local cede el nodo
Spanish text, transcribed from the repository. The unformatted
source is
data-architecture/host-coexistence.md.
El criterio de aceptación que ordena todo este documento: ningún nodo debe acaparar más del 30 % de la CPU ni de la RAM del host cuando el usuario lo está usando.
1. El problema
Un nodo del enjambre no es un servidor dedicado. Es el portátil de alguien que además quiere abrir el navegador. Si la IA local ocupa la CPU sin límite, el usuario percibe el equipo como roto aunque el modelo funcione perfectamente. Y desde dentro del proceso, "funciona" y "estorba" son compatibles.
La solución no es limitar más, es ceder: cambiar de variante, o parar, antes de competir. Eso es la plasticidad en el sentido literal —el sistema se adapta al estado del nodo, no al revés— y es la misma idea que aplican EdgeMLBalancer (cambio dinámico de modelo cuando el nodo empeora) y NEXUS (coexistencia explícita con la carga del usuario).
2. Tres acciones, dos umbrales, una histéresis
crates/mesh-orchestrator/src/host_guard.rs:
carga = max(cpu_utilization, memory_utilization, contención_medida)
carga ≥ 0.92 → Pause la IA local se detiene del todo
carga ≥ 0.70 → Downgrade se baja un nivel de variante
carga < 0.60 (0.70−0.10) → Continue vuelve a servir
entre 0.60 y 0.70 con el estado degradado activo → se mantiene la degradación
Por qué la histéresis no es un adorno
Sin ella, un host que oscila alrededor del 70 % produce esta secuencia en cada muestra: downgrade → continue → downgrade → continue. Cada transición descarga un modelo y carga otro: la IA local pasaría el día cargando pesos y el usuario vería el disco trabajando sin motivo. Con la histéresis hay que bajar del umbral menos el margen (0.60) para recuperarse, y un test lo comprueba explícitamente:
guard.should_throttle(&metrics(0.75, 0.2)); // downgrade
guard.should_throttle(&metrics(0.68, 0.2)); // downgrade ← sigue degradado
guard.should_throttle(&metrics(0.55, 0.2)); // continue ← sólo aquí se recupera
Por qué la degradación es escalonada
La primera versión pedía al selector "cualquier variante que no sea la activa". Con la carga alta eso producía un efecto absurdo: de int4 se pasaba a int8 —es decir, subía de huella— porque int8 es más precisa y el selector no sabía que el motivo del cambio era la presión del host. Lo detectó un test de la rampa de carga, no una revisión.
Ahora cada evaluación baja un nivel, filtrando antes por memoria: bajar a una variante que no cabe no es degradar, es fallar. Con la histéresis, si la carga sigue alta se seguirá bajando en las siguientes evaluaciones; y un pico momentáneo no hace que el nodo pierda precisión de golpe.
./target/release/hecate-node guard --ramp
carga 0.20 → continue activa: ncm-chat-int8
carga 0.75 → downgrade activa: ncm-chat-int4 (3.50 GB → 1.75 GB)
carga 0.85 → downgrade activa: ncm-chat-ternary (1.75 GB → 0.30 GB)
carga 0.95 → pause activa: (ninguna)
carga 0.30 → continue activa: ncm-chat-ternary (recuperado)
3. Huella mínima: qué se hace y qué no
La tercera pata del diseño es que la IA local ocupe cientos de megabytes o pocos GB, no decenas. Lo que está implementado:
| Técnica | Estado | Dónde |
|---|---|---|
| Cuantización agresiva (int4 con empaquetado 2 bits/byte… ) | ✅ | model_zoo/quantization.py |
| Ternario empaquetado a 2 bits por peso | ✅ | pack_ternary |
| Carga bajo demanda con caché por variante | ✅ | DeviceAwareModelZoo.load_variant(use_cache) |
| Descarga explícita de lo cargado | ✅ | unload_all() |
| Ceder el nodo bajo carga | ✅ | HostResourceGuard::enforce |
| Dimensionamiento adaptativo de contexto/KV | ❌ | no implementado |
Carga perezosa real por mmap de pesos | ❌ | load_variant construye el módulo completo |
| Aceleración por NPU para liberar la CPU | ❌ | sin backend de NPU en este proyecto |
Las tres últimas casillas son las que impedirían afirmar hoy que la huella está en "cientos de megabytes": la cuantización reduce la huella de forma verificable (los tests cuentan bytes), pero el modelo cargado es sintético y pequeño. La técnica está; la escala real exige pesos entrenados.
4. Lo que el guardián no hace
No impone límites al sistema operativo. No hay cgroups ni Job Objects ni nice: el proceso cambia de variante y, si hace falta, se para desde dentro.
Eso tiene una consecuencia que conviene decir: frente a un proceso desbocado, un cgroup sería más robusto, porque un límite del sistema operativo se aplica aunque el proceso ignore la política. Aquí, si la IA local se quedara atascada en un bucle de cómputo, el guardián no podría arrebatarle la CPU: sólo puede decidir no pedirle más trabajo.
Implementarlo exige privilegios y código por plataforma, y no está hecho.
5. Medición
El guardián consume HostMetrics, que puede venir de dos fuentes:
HostSampler(Rust,mesh_profiler::host): uso de CPU y memoria desysinfo, más contención medida comparando el rendimiento actual con la línea base del nodo.- Métricas externas: el tipo es plano y serializable, de modo que un agente de telemetría puede inyectarlas sin tocar la política.
La contención medida es la señal que el porcentaje de CPU no da: un nodo al 40 % con el planificador saturado rinde peor que uno al 85 % con núcleos libres. Un test verifica que la contención sola dispara la degradación aunque la CPU declarada sea baja.
6. Estado real
| Semana | Entregable | Estado |
|---|---|---|
| 3 | Simular carga al 80 % y verificar degradación sin bloquear | ✅ guard --ramp y tests de rampa: degrada, no bloquea |
| 4 | Ningún nodo supera el 30 % de CPU/RAM | ❌ no medido: requiere instrumentar un nodo real sirviendo con el modelo cargado, que no existe todavía |
La segunda fila es el hueco importante de este documento. La política está implementada y probada con métricas inyectadas; lo que falta es la medida sobre hardware real con un modelo de verdad, y sin ella la cifra del 30 % sigue siendo un objetivo, no un resultado.
7. Límites
- Sin
cgroups/Job Objects: la política se aplica desde dentro del proceso. - La contención se mide con una carga corta; un pico puede colarse en una muestra.
- El umbral del 30 % no está verificado en hardware real.
- Pausar significa "dejar de servir", no "liberar la memoria": el módulo cargado sigue residente hasta que alguien llame a
unload_all. Un guardián que descargara al pausar necesitaría coordinación con el plano de datos, y eso no está hecho.