# Enrutamiento del enjambre Cómo elige HECATE qué pipeline atiende una petición cuando hay varias copias del modelo vivas, y qué pasa cuando un nodo cae. ## 1. El problema Con redundancia (Pilar A) el clúster ya no tiene *un* camino: tiene `N` copias completas del modelo, cada una repartida por nodos distintos. Además, un mismo enjambre puede servir varios modelos a la vez (el base y los expertos que aporta la telomerasa). El router tiene que responder dos preguntas: 1. ¿Qué copia del modelo que pide el cliente es la más barata **ahora**? 2. Si un nodo cae, ¿quién le sustituye sin detener el servicio? ## 2. Estado del enjambre ```rust pub struct SwarmState { pub plan: SwarmPlan, // quién debería ejecutar qué pub nodes: NodeStates, // cómo está cada nodo ahora } ``` La separación es deliberada: el plan es caro de calcular (milisegundos) y cambia poco; la telemetría es barata y cambia todo el rato. Recalcular el reparto en cada petición sería insostenible; reevaluar el coste con telemetría fresca cuesta microsegundos. `SwarmState::from_plan` construye la telemetría en frío, antes de que el monitor tenga medidas. `from_profiles` añade las latencias declaradas por cada nodo. ## 3. Selección por modelo (Pilar A) ```rust let route = route_request("ncm-vision", RoutingRequest::default(), &state)?; ``` ```mermaid flowchart LR P["petición
model = ncm-vision"] --> F{"pipelines con
model_id == ncm-vision"} F -->|"copia #0"| C0["coste 12.4 ms"] F -->|"copia #1"| C1["coste 18.9 ms"] C0 --> W["se elige #0"] C1 --> W F -.->|"otros modelos"| X["descartados"] ``` Reglas, en orden: 1. **El modelo debe coincidir exactamente.** Servir otro modelo no es una respuesta degradada: es una respuesta incorrecta, con otra dimensión y otras capas. 2. **Se descartan los pipelines con algún nodo caído.** Una copia a medias no es una copia. 3. **Gana el coste menor**, con empate roto por `pipeline_id` para que dos ejecuciones consecutivas elijan lo mismo. ### Coste de un pipeline $$ \operatorname{coste}(P) = \sum_{n \in P} \operatorname{cost}(n) + w_h \cdot 15 \cdot \text{saltos} + w_p \cdot 10 \cdot \frac{\text{tokens}}{1024} $$ donde cada etapa cuesta, igual que en el router de un solo pipeline: $$ \operatorname{cost}(n) = w_\ell \cdot \text{latencia}_n + w_c \cdot \text{load}_n \cdot 120 + w_k \cdot \frac{100}{\max(\text{score}_n, 0.01)} $$ Un nodo sin telemetría **se penaliza**, no se ignora. Ignorarlo haría que el pipeline peor medido pareciera el mejor, y el router enviaría carga a ciegas justo a los nodos de los que no sabe nada. ## 4. Reparto de carga entre copias No hay un balanceador explícito: la carga se reparte sola porque el coste incluye `load`, y cada nodo actualiza su carga con su EWMA. Cuando la copia 0 se satura, su coste sube hasta que la copia 1 gana la comparación. Ventaja: no hay estado de asignación que mantener ni sesiones que romper. Límite: es un reparto **reactivo**, no predictivo. Con peticiones muy cortas y picos bruscos, varias peticiones seguidas pueden elegir la misma copia antes de que su coste refleje la carga. ## 5. Failover por redundancia ```rust let recovered = failover_in_swarm(&route, "gpu", &state, FailoverOptions::default())?; ``` La diferencia con el failover de un solo pipeline: **el sustituto puede venir de otra copia del mismo modelo**. ```mermaid flowchart TD A["cae gpu"] --> B{"¿otra copia del mismo modelo
cubre todas sus capas?"} B -->|Sí| C["se devuelve esa copia completa
degraded = false"] B -->|No| D{"¿cada capa perdida tiene
otro nodo vivo?"} D -->|Sí| E["empalme por capa
degraded = false, replaced = [...]"] D -->|No| F{"¿degradación permitida?"} F -->|Sí| G["ruta de los supervivientes
degraded = true, modelo de reserva"] F -->|No| H["RoutingError::Unrecoverable"] ``` ### Por qué se prefiere una copia completa a un empalme Un empalme por capa funciona, pero produce una cadena que **no es un pipeline planificado**: nadie garantizó que esos nodos tuvieran los fragmentos contiguos correctos. Una copia completa sí está garantizada por el plan. Por eso el orden es: copia intacta → empalme → degradación → error. ### El error controlado `RoutingError::Unrecoverable` no es un fallo del programa: es la señal que el orquestador espera para decidir si degrada o si apaga. Un failover que devolviera una ruta inválida sería peor que un error explícito. ## 6. El ciclo de vida en el orquestador `Swarm` encapsula la política completa y es **síncrono y puro** salvo por la telomerasa: ```rust let mut swarm = Swarm::new(profiles, SwarmConfig::new(12, 2))?; swarm.route(RoutingRequest::default())?; // plan + telemetría → ruta swarm.mark_down("gpu")?; // caída → ruta de rescate swarm.mark_alive("gpu")?; // vuelve → estado sano swarm.add_node(&newcomer)?; // crecimiento sin reinicio swarm.remove_node("laptop")?; // retirada + recolocación swarm.status(); // healthy | degraded | failed | stopped ``` ### Transiciones de estado | Situación | `state` | | --- | --- | | Todos los pipelines completos y todas las capas cubiertas por nodos vivos | `healthy` | | Quedan nodos vivos pero falta un pipeline o hay capas sin cubrir | `degraded` | | Ningún nodo vivo | `failed` | | `Swarm::stop()` | `stopped` | Nótese que **`failed` exige cero nodos vivos**, no cero pipelines. Si el único pipeline perdió un nodo pero quedan supervivientes, el enjambre está degradado y sigue sirviendo: matar el servicio porque la topología ideal ya no existe sería el peor de los resultados. ### Orden de operaciones en una caída `mark_down` calcula la ruta **antes** de marcar el nodo como muerto. Si lo marcara primero, el pipeline que lo usaba dejaría de ser elegible y no habría nada que reparar: el failover necesita saber qué se ha perdido, no solo que algo falta. ## 7. Simulación sin red `hecate-node swarm-sim` ejecuta el ciclo de vida completo en memoria: ```bash hecate-node swarm-sim --topology examples/swarm.json --kill gpu --pipelines 2 ``` Devuelve un JSON con cada paso (`start`, `failover`, `growth`), el estado del enjambre tras cada uno, la ruta elegida y si el plan final valida. Sirve para revisar una política de crecimiento antes de tocar hardware, y como banco de pruebas de la propia lógica. ## 8. Límites honestos - **El router no reserva capacidad.** Dos peticiones simultáneas pueden elegir la misma copia; el reparto es estadístico, no una garantía. - **La telemetría llega con retraso.** El coste se calcula con el último EWMA, no con el estado instantáneo del nodo. - **El failover no migra estado.** Las activaciones de una petición en vuelo se pierden si su nodo cae a mitad de pipeline: el cliente reintenta, no se reanuda. - **Un experto no sustituye al modelo base.** Son pipelines de modelos distintos; el router no los mezcla aunque el experto esté ocioso.