Source document
Arquitectura de HECATE
Spanish text, transcribed from the repository. The unformatted
source is
architecture.md.
Documento de referencia del enjambre: qué vive en Rust, qué vive en Python, dónde está la frontera y qué garantías ofrece cada capa.
HECATE es un enjambre neuronal: varias copias completas del modelo conviven a la vez (Pilar A), el hardware nuevo se integra sin reiniciar (Pilar B) y las IAs locales ceden su conocimiento mediante adaptadores fusionados (Pilar C).
| Pilar | Documento |
|---|---|
| A · Multi-pipeline y redundancia | swarm-routing.md |
| B · Crecimiento por hardware | growth-protocol.md |
| C · Telomerasa | telomerase.md |
1. Vista de componentes
flowchart TD
subgraph Control["Plano de control · Rust + Tokio (crates/)"]
D[mesh-discovery<br/>mDNS + NodeProfile] --> S[mesh-scheduler<br/>plan_swarm + growth_decision]
S --> RT[mesh-router<br/>route_request + failover_in_swarm]
RT --> O[mesh-orchestrator<br/>Swarm + telomerase_round]
O --> W[mesh-worker-net<br/>PeerPool + clientes gRPC]
N[hecate-node CLI] -.-> O
N -.-> W
end
subgraph Proto["Contrato · proto/hecate.proto"]
PB[["DataPlane: Describe · Health · Forward · Profile<br/>Telomerase: Extract · Merge · Apply · Stats"]]
end
subgraph Data["Plano de datos · Python + PyTorch (python/hecate/)"]
T[ncm_tokenizer<br/>VQ-VAE residual] --> SVC[dataplane.service]
F[ncm_transformer<br/>NcmLayerFragment] --> SVC
L[telomerase<br/>LoRA + FedAvg] --> SVC
SVC --> GRPC[dataplane.server<br/>grpc.server + workers]
end
W ==>|"Forward"| PB
O ==>|"Extract/Merge/Apply"| PB
PB ==> GRPC
GRPC -.->|"Profile al descubrir"| O
- Plano de control: no mueve activaciones por sí mismo; decide quién ejecuta qué, mantiene el pool de sockets y reenvía. Rust con Tokio, sin
unsafe. - Contrato: un
.protogenera los stubs de ambos lados. Única fuente de verdad. - Plano de datos: solo hace matemáticas de tensor. No conoce la topología del enjambre.
2. Flujo de una petición
sequenceDiagram
participant C as Cliente
participant R as mesh-router
participant A as worker A (copia 0)
participant PA as DataPlane A
participant B as worker B (copia 0)
participant PB as DataPlane B
C->>R: petición(model="ncm-base")
R->>R: route_request → copia más barata
R-->>C: ruta ncm-base#0 = [A, B]
C->>A: forward(activaciones, next_layer=0)
activate A
A->>PA: Forward(layer_start=0, bytes)
PA-->>A: ForwardResponse(layer_end=5, bytes)
A->>B: forward(activaciones, next_layer=6)
activate B
B->>PB: Forward(layer_start=6, bytes)
PB-->>B: ForwardResponse(layer_end=11, bytes)
B-->>A: forward_result
deactivate B
A-->>C: forward_result (trace=[A,B])
deactivate A
El resultado se propaga sin transformación: el último nodo responde y los intermedios devuelven la misma cabecera añadiendo su latencia. trace acumula el recorrido, lo que hace trivial verificar en los tests qué nodos han participado.
3. Mapa de módulos
Plano de control (Rust)
| Crate | Símbolos públicos | Notas |
|---|---|---|
crates/mesh-proto | v1::{DataPlaneClient, TelomeraseClient, ForwardRequest, NodeProfile, AdapterPayload}, ActivationBuffer, encode_activations | Generado desde el .proto + validación de activaciones. |
crates/mesh-scheduler | NodeProfile, ExtensionWeights, plan_swarm, SwarmPlan, Pipeline, growth_decision, GrowthAction, apply_growth, merge_expert | Puro: sin E/S. Determinista. |
crates/mesh-router | SwarmState, route_request, failover_in_swarm, pipeline_cost, degraded_route | Elige copia por coste; failover por redundancia. |
crates/mesh-discovery | DiscoveredNode, NodeRegistry, MdnsDiscovery, to_profile | Anuncia capacidades y telomerasa. |
crates/mesh-worker-net | Worker, PeerPool, DataPlane, Telomerase, WorkerClient | Bucle de red + clientes gRPC. |
crates/mesh-orchestrator | Swarm, SwarmConfig, SwarmStatus, telomerase_round, TelomeraseLink, MergeReport | Ciclo de vida completo. |
crates/hecate-node | CLI | serve, orchestrate, plan, swarm, swarm-sim, swarm-live, probe, bench. |
Plano de datos (Python)
| Ruta | Símbolos públicos | Notas |
|---|---|---|
python/hecate/ncm_transformer/layer_fragment.py | FragmentConfig, NcmLayerFragment, NcmBlock, RMSNorm | PyTorch real: SDPA causal, SwiGLU. Parámetros congelados. |
python/hecate/ncm_tokenizer/tokenizer_wrapper.py | TokenizerConfig, VectorQuantizer, NcmTokenizer | Cuantización residual, entrenamiento EMA. |
python/hecate/telomerase/adapter.py | TelomeraseAdapter, LoraConfig, LoraLinear, ApplyReport | LoRA entrenable sobre el fragmento congelado. |
python/hecate/telomerase/federated.py | merge_payloads, merge_messages, federated_average | FedAvg ponderado por muestras. |
python/hecate/telomerase/codec.py | encode_tensor, decode_tensor, validate_payload | f32 LE, el mismo códec que las activaciones. |
python/hecate/telomerase/service.py | TelomeraseService | Extract/Merge/Apply/Stats. |
python/hecate/dataplane/service.py | DataPlaneService, PlaneSettings, HardwareProfile | Implementa los cuatro RPC del plano de datos. |
4. Protocolo de red
Hay dos protocolos, deliberadamente distintos.
4.1 Salto worker → worker (TCP crudo, Rust)
+----------------+----------------+------------------+---------------------+
| uint32 BE | uint32 BE | JSON (UTF-8) | payload binario |
| longitud total | longitud cabec. | cabecera | float32 LE |
+----------------+----------------+------------------+---------------------+
4 B 4 B variable rows*cols*4 B
Cabecera de petición:
{
"op": "forward",
"protocol": 1,
"request_id": "abc-123",
"rows": 8,
"cols": 24,
"next_layer": 7,
"trace": ["nodo-a"]
}
Decisiones:
- JSON para la cabecera: legible con
tcpdump/Wireshark y versionable (protocol). Cuesta unos 60 bytes y evita el codegen de un segundo contrato binario. - Binario para las activaciones:
float32little-endian, el mismo orden que usa el contrato gRPC; no hay byte swap en la frontera con Rust ni con NumPy. - Límite de 64 MiB por trama y 8 KiB de cabecera: corta mensajes corruptos antes de reservar memoria.
next_layer: imprescindible cuando un nodo tiene varios fragmentos.
4.2 Worker → DataPlane y orquestador → Telomerase (gRPC/HTTP-2)
activationsesbytesf32LE crudo, norepeated float: el varint de protobuf triplicaría el tamaño en la red.ForwardRequest.layer_startdebe ser el inicio exacto de un fragmento.Describe/Health/Profilequedan fuera de la ruta crítica.Telomerasese registra en el mismo puerto queDataPlane: un nodo no necesita dos servicios escuchando.AdapterPayloadreutiliza el códec de activaciones (f32 LE), de modo que no hay una segunda convención de extremidad que se rompa en otra arquitectura.
5. Concurrencia
5.1 Plano de control (Tokio)
- Cada nodo es un
Workerque escucha conTcpListenery lanza una tarea Tokio por conexión: no hay hilo por cliente ni bloqueo. - El
PeerPoolguarda hasta 32 conexiones ociosas por par, conTCP_NODELAY. - El contador de peticiones en vuelo y el EWMA viven bajo un
Mutexpequeño; la medición no toma el lock durante el trabajo. Swarmes síncrono y puro: plan y telemetría se actualizan sin await. Solotelomerase_roundes asíncrono, y está fuera de la ruta crítica.- El
Clusterarranca workers conDEFAULT_BASE_PORT = 0(puertos efímeros): dos clústeres conviven y los tests son deterministas.
5.2 Plano de datos (Python)
grpc.serverconThreadPoolExecutor(max_workers=8).- Contadores protegidos por
threading.Lock; el EWMA usaEWMA_ALPHA = 0.3. Forwarddecodifica contorch.frombuffery ejecuta bajo@torch.no_grad().- El entrenamiento del adaptador sí necesita gradiente, así que recorre los bloques directamente (
_forward_trainable) en lugar delforwarddecorado.
6. Despliegue
flowchart LR
subgraph Host1["portátil · worker :17010"]
W1[worker Rust] --> P1[dataplane :50251 + telomerase]
end
subgraph Host2["mini-PC · worker :17011"]
W2[worker Rust] --> P2[dataplane :50252 + telomerase]
end
subgraph Host3["sobremesa · worker :17012"]
W3[worker Rust] --> P3[dataplane :50253]
end
W1 --> W2 --> W3
O[orchestrator] -.-> W1
O -.-> W2
O -.-> W3
O -.->|"Extract / Apply"| P1
O -.->|"Extract / Apply"| P2
| Configuración | Cuándo | Cómo |
|---|---|---|
| Plano de datos compartido | Validar el control con un único PyTorch. | shared_data_plane: "http://host:50251". |
| Plano de datos por nodo | Despliegue real, cada nodo con su GPU. | data_plane: {"gpu": "http://...", "mini": "http://..."}. |
7. Modos de fallo
| Fallo | Detección | Respuesta |
|---|---|---|
| Nodo caído con réplica | Health falla failure_threshold veces | failover_in_swarm reencamina a la réplica de otra copia. |
| Nodo caído sin réplica completa | Igual | Empalme por capa; si falta alguna, degradación a ncm-tiny. |
| Todos los nodos caídos | Igual | state = failed; la ruta devuelve NoAliveNodes. |
| Plano de datos de un nodo caído | describe()/forward() fallan con Transport | El nodo se marca no saludable y el monitor lo trata como caído. |
| Activaciones con dimensión errónea | ActivationBuffer::validate (Rust) y decode_activations (Python) | INVALID_ARGUMENT antes de tocar un tensor. |
layer_start que no es inicio de fragmento | DataPlaneService.Forward | FAILED_PRECONDITION con la lista de inicios disponibles. |
| Nodo sin telomerasa | Extract/Apply responden FAILED_PRECONDITION | La ronda lo anota como fallo y sigue con los demás. |
| Adaptador incompatible (modelo, rango, formas) | Validación en Python | INVALID_ARGUMENT; el nodo conserva su adaptador. |
| Fusión con un solo payload | Regla de la ronda | No se viaja al fusionador: sería una identidad. |
| Nodo saturado | Health.load | El router lo evita si hay alternativa más barata. |
8. Puntos de extensión
- Contrato: añadir un RPC es editar el
.protoy regenerar;verify.shdetecta si alguien olvida regenerar. - Transporte: sustituir el framing TCP por QUIC o
io_uringsin tocar scheduler, router ni plano de datos: la frontera esFrame+HopHeader. - Modelo: cambiar
NcmLayerFragmentpor un transformer real manteniendoforward(activaciones) -> activaciones; el worker no se entera. - Crecimiento: añadir una cuarta
GrowthAction(por ejemplo, dividir un tramo para paralelismo de tensor) solo requiere extenderapply_growth. - Fusión: sustituir FedAvg por ponderación por pérdida sin tocar el servicio gRPC.
- Descubrimiento: cambiar mDNS por Consul; el
NodeRegistryes la única superficie.
9. Qué no hace (y por qué)
- No entrena el modelo base: es un plano de ejecución. La telomerasa ajusta adaptadores, que es barato y local.
- No balancea con colas reales:
loades una aproximación para el router, no un load balancer. - No garantiza orden entre peticiones concurrentes: cada una lleva
request_idy su tarea. - No persiste estado en el plano de control: el plan se recalcula al arrancar y en cada rewire.
- No verifica la calidad de un adaptador: solo su forma y su compatibilidad.