# 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.