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:

  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

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:

  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

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ónstate
Todos los pipelines completos y todas las capas cubiertas por nodos vivoshealthy
Quedan nodos vivos pero falta un pipeline o hay capas sin cubrirdegraded
Ningún nodo vivofailed
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.