Source document
Enrutamiento del enjambre
Spanish text, transcribed from the repository. The unformatted
source is
swarm-routing.md.
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:
- ¿Qué copia del modelo que pide el cliente es la más barata ahora?
- Si un nodo cae, ¿quién le sustituye sin detener el servicio?
2. Estado del enjambre
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)
let route = route_request("ncm-vision", RoutingRequest::default(), &state)?;
flowchart LR
P["petición<br/>model = ncm-vision"] --> F{"pipelines con<br/>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:
- El modelo debe coincidir exactamente. Servir otro modelo no es una respuesta degradada: es una respuesta incorrecta, con otra dimensión y otras capas.
- Se descartan los pipelines con algún nodo caído. Una copia a medias no es una copia.
- Gana el coste menor, con empate roto por
pipeline_idpara 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
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.
flowchart TD
A["cae gpu"] --> B{"¿otra copia del mismo modelo<br/>cubre todas sus capas?"}
B -->|Sí| C["se devuelve esa copia completa<br/>degraded = false"]
B -->|No| D{"¿cada capa perdida tiene<br/>otro nodo vivo?"}
D -->|Sí| E["empalme por capa<br/>degraded = false, replaced = [...]"]
D -->|No| F{"¿degradación permitida?"}
F -->|Sí| G["ruta de los supervivientes<br/>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:
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:
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.