# Convivencia con el host: la IA local cede el nodo > **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`: ```text 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: ```rust 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. ```bash ./target/release/hecate-node guard --ramp ``` ```text 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 de `sysinfo`, 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.