Source document
Enrutamiento consciente del coste
Spanish text, transcribed from the repository. The unformatted
source is
routing.md.
1. Objetivo
Elegir, para cada petición, la secuencia de nodos que cubre las capas solicitadas con el menor coste estimado. El coste está en milisegundos equivalentes, así que se puede comparar directamente con la latencia que observa el usuario.
2. Modelo de coste
2.1 Coste de una etapa
\operatorname{cost}(n) = w_\ell \cdot \text{latencia}_n
+ w_c \cdot \text{load}_n \cdot 120
+ w_k \cdot \frac{100}{\max(\operatorname{score}_n,\ 0.01)}
| Símbolo | Peso por defecto | Significado |
|---|---|---|
| w_\ell | 1.0 | La latencia medida pesa 1 a 1. |
| w_c | 1.0 | Un nodo al 100 % de carga añade 120 ms equivalentes. |
| w_k | 0.35 | Un nodo con score 1 añade 35 ms; con score 28, 1.25 ms. |
2.2 Coste de una ruta
\operatorname{coste}(R) = \sum_{n \in R} \operatorname{cost}(n)
+ w_h \cdot 15 \cdot (|R| - 1)
+ w_p \cdot 10 \cdot \frac{\text{tokens\_prompt}}{1024}
- Término de saltos: cada frontera entre nodos cuesta 15 ms equivalentes.
- Término de prefill: un prompt largo penaliza por igual todas las rutas, pero hace que las diferencias de latencia pesen proporcionalmente menos.
- Ruta vacía → \infty.
3. Camino mínimo por fronteras de fragmento
Una restricción manda sobre todo lo demás: un fragmento se ejecuta completo. El nodo que recibe next_layer = L solo puede continuar si tiene un fragmento que empieza en L; entrar en mitad de un fragmento recalcularía capas que otro nodo ya ha hecho.
Por eso score_route() resuelve un camino mínimo en un grafo acíclico:
- Vértices: las fronteras de capa
first, first+1, ..., last+1. - Aristas: cada fragmento
(L, E)de un nodo vivo, que va deLaE+1con costecost(nodo) + coste_salto(el salto solo se cobra si cambia de nodo). - Solución: programación dinámica en orden ascendente de capa, con desempate por número de etapas y después por
node_id(determinismo total).
Un nodo con dos fragmentos contiguos cuenta como una sola etapa; un nodo que reaparece más adelante (por ejemplo, sirviendo la cabeza replicada y después la cola) cuenta dos veces, porque ejecuta dos tramos.
Si no existe camino completo se lanza NoRouteAvailable con la ventana en el mensaje.
4. Ejemplo numérico
Tres nodos, gpu saturado pero potente, mini sano y laptop con réplica implícita:
| Nodo | Latencia | Carga | Score | \operatorname{cost}(n) |
|---|---|---|---|---|
| gpu | 40 ms | 0.8 | 10 | 40 + 96 + 3.5 = 139.5 |
| mini | 6 ms | 0.2 | 6 | 6 + 24 + 5.8 = 35.8 |
| laptop | 9 ms | 0.0 | 3 | 9 + 0 + 11.7 = 20.7 |
- Camino
gpu (0-3) → mini (4-7) → laptop (8-11): 139.5 + 35.8 + 20.7 + 2·15 = 226.0 - Camino
mini (0-7) → laptop (8-11): 35.8 + 20.7 + 15 = 71.5 ← óptimo
Resultado: ("mini", "laptop"). Es el caso que verifica test_score_route_prefers_the_cheapest_covering_pipeline.
El DP explora mini para la cabeza (0-3) y, como también es dueño de 4-7, lo mantiene en la misma etapa: no se cobra un salto ficticio ni se lista dos veces.
5. Ventana de capas
RoutingRequest.layer_range permite pedir un subconjunto:
None: la ventana es[primera capa servida, última capa servida]de los nodos vivos.(8, 11): solo la cola; el router puede devolver un único nodo.
prompt_tokens no cambia la cobertura, solo el coste relativo del prefill.
6. Failover
failover(route, redundancy_map, failed_node, ...):
- Si
failed_nodeno está en la ruta → se devuelve la ruta intacta con la razón "no participa en la ruta". No es un error: el orquestador puede sondear nodos ociosos. - Se determinan las capas del nodo caído (de su
NodeState.served_layerssi hay telemetría; si no, deredundancy_map), recortadas a la ventana de la ruta. - Para cada capa se busca el holder vivo más barato distinto del caído. Si todas las capas tienen sustituto, el sustituto ocupa la posición del caído. Solo se colapsan duplicados consecutivos: si el sustituto ya estaba más adelante en la ruta y además es dueño de otro tramo, reaparece al final para no perder cobertura.
- Si falta alguna capa: -
fallback_modelno nulo → ruta degradada con los supervivientes (degraded=True). -fallback_model=None→RoutingFailure(excepción controlada).
7. Telemetría
NodeState se puede construir desde diccionarios (NodeState.from_dict) para que el router funcione con respuestas de health llegadas por red:
{
"node_id": "mini",
"latency_ms": 6.4,
"current_load": 0.2,
"capacity_score": 13.0,
"served_layers": [[7, 9]],
"alive": true
}
WorkerState::health() devuelve además served_requests, failed_requests, load y latency_ms, que son útiles para diagnóstico pero no entran en el coste. El load del plano de datos llega por la RPC Health y se agrega en el nodo.
8. Ajuste de pesos
use mesh_router::{RoutingRequest, RouteWeights, score_route};
// Prioriza latencia sobre capacidad (LAN rápida, nodos pequeños)
let rapido = RouteWeights {
latency: 2.0,
load: 1.5,
capacity: 0.1,
hop: 0.05,
prompt: 0.2,
};
let ruta = score_route(RoutingRequest::default(), &estados, rapido, "ncm")?;
println!("{} saltos, coste {:.1}", ruta.hops(), ruta.cost);
Recomendaciones:
- Ethernet 1 GbE: subir
hopa 0.3-0.5 si el pipeline tiene 3+ nodos. - Nodos muy heterogéneos: subir
capacitypara no cargar el mini-PC. - Carga variable: subir
loadpara evitar el nodo caliente antes de que se sature. - Plano de datos remoto: el salto worker → DataPlane por gRPC no se modela como
hop; entra enlatencyporque el nodo lo mide con su propio EWMA.
9. Lo que el router no hace
- No reserva capacidad ni impone cuotas: dos peticiones pueden elegir el mismo nodo.
- No mide la red en tiempo real: la latencia llega del EWMA del worker.
- No reintenta: si el nodo elegido falla durante la petición, el orquestador recompone el pipeline para las siguientes.