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

PilarDocumento
A · Multi-pipeline y redundanciaswarm-routing.md
B · Crecimiento por hardwaregrowth-protocol.md
C · Telomerasatelomerase.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 .proto genera 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)

CrateSímbolos públicosNotas
crates/mesh-protov1::{DataPlaneClient, TelomeraseClient, ForwardRequest, NodeProfile, AdapterPayload}, ActivationBuffer, encode_activationsGenerado desde el .proto + validación de activaciones.
crates/mesh-schedulerNodeProfile, ExtensionWeights, plan_swarm, SwarmPlan, Pipeline, growth_decision, GrowthAction, apply_growth, merge_expertPuro: sin E/S. Determinista.
crates/mesh-routerSwarmState, route_request, failover_in_swarm, pipeline_cost, degraded_routeElige copia por coste; failover por redundancia.
crates/mesh-discoveryDiscoveredNode, NodeRegistry, MdnsDiscovery, to_profileAnuncia capacidades y telomerasa.
crates/mesh-worker-netWorker, PeerPool, DataPlane, Telomerase, WorkerClientBucle de red + clientes gRPC.
crates/mesh-orchestratorSwarm, SwarmConfig, SwarmStatus, telomerase_round, TelomeraseLink, MergeReportCiclo de vida completo.
crates/hecate-nodeCLIserve, orchestrate, plan, swarm, swarm-sim, swarm-live, probe, bench.

Plano de datos (Python)

RutaSímbolos públicosNotas
python/hecate/ncm_transformer/layer_fragment.pyFragmentConfig, NcmLayerFragment, NcmBlock, RMSNormPyTorch real: SDPA causal, SwiGLU. Parámetros congelados.
python/hecate/ncm_tokenizer/tokenizer_wrapper.pyTokenizerConfig, VectorQuantizer, NcmTokenizerCuantización residual, entrenamiento EMA.
python/hecate/telomerase/adapter.pyTelomeraseAdapter, LoraConfig, LoraLinear, ApplyReportLoRA entrenable sobre el fragmento congelado.
python/hecate/telomerase/federated.pymerge_payloads, merge_messages, federated_averageFedAvg ponderado por muestras.
python/hecate/telomerase/codec.pyencode_tensor, decode_tensor, validate_payloadf32 LE, el mismo códec que las activaciones.
python/hecate/telomerase/service.pyTelomeraseServiceExtract/Merge/Apply/Stats.
python/hecate/dataplane/service.pyDataPlaneService, PlaneSettings, HardwareProfileImplementa 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: float32 little-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)

  • activations es bytes f32 LE crudo, no repeated float: el varint de protobuf triplicaría el tamaño en la red.
  • ForwardRequest.layer_start debe ser el inicio exacto de un fragmento.
  • Describe/Health/Profile quedan fuera de la ruta crítica.
  • Telomerase se registra en el mismo puerto que DataPlane: un nodo no necesita dos servicios escuchando.
  • AdapterPayload reutiliza 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 Worker que escucha con TcpListener y lanza una tarea Tokio por conexión: no hay hilo por cliente ni bloqueo.
  • El PeerPool guarda hasta 32 conexiones ociosas por par, con TCP_NODELAY.
  • El contador de peticiones en vuelo y el EWMA viven bajo un Mutex pequeño; la medición no toma el lock durante el trabajo.
  • Swarm es síncrono y puro: plan y telemetría se actualizan sin await. Solo telomerase_round es asíncrono, y está fuera de la ruta crítica.
  • El Cluster arranca workers con DEFAULT_BASE_PORT = 0 (puertos efímeros): dos clústeres conviven y los tests son deterministas.

5.2 Plano de datos (Python)

  • grpc.server con ThreadPoolExecutor(max_workers=8).
  • Contadores protegidos por threading.Lock; el EWMA usa EWMA_ALPHA = 0.3.
  • Forward decodifica con torch.frombuffer y ejecuta bajo @torch.no_grad().
  • El entrenamiento del adaptador sí necesita gradiente, así que recorre los bloques directamente (_forward_trainable) en lugar del forward decorado.

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ónCuándoCómo
Plano de datos compartidoValidar el control con un único PyTorch.shared_data_plane: "http://host:50251".
Plano de datos por nodoDespliegue real, cada nodo con su GPU.data_plane: {"gpu": "http://...", "mini": "http://..."}.

7. Modos de fallo

FalloDetecciónRespuesta
Nodo caído con réplicaHealth falla failure_threshold vecesfailover_in_swarm reencamina a la réplica de otra copia.
Nodo caído sin réplica completaIgualEmpalme por capa; si falta alguna, degradación a ncm-tiny.
Todos los nodos caídosIgualstate = failed; la ruta devuelve NoAliveNodes.
Plano de datos de un nodo caídodescribe()/forward() fallan con TransportEl nodo se marca no saludable y el monitor lo trata como caído.
Activaciones con dimensión erróneaActivationBuffer::validate (Rust) y decode_activations (Python)INVALID_ARGUMENT antes de tocar un tensor.
layer_start que no es inicio de fragmentoDataPlaneService.ForwardFAILED_PRECONDITION con la lista de inicios disponibles.
Nodo sin telomerasaExtract/Apply responden FAILED_PRECONDITIONLa ronda lo anota como fallo y sigue con los demás.
Adaptador incompatible (modelo, rango, formas)Validación en PythonINVALID_ARGUMENT; el nodo conserva su adaptador.
Fusión con un solo payloadRegla de la rondaNo se viaja al fusionador: sería una identidad.
Nodo saturadoHealth.loadEl router lo evita si hay alternativa más barata.

8. Puntos de extensión

  • Contrato: añadir un RPC es editar el .proto y regenerar; verify.sh detecta si alguien olvida regenerar.
  • Transporte: sustituir el framing TCP por QUIC o io_uring sin tocar scheduler, router ni plano de datos: la frontera es Frame + HopHeader.
  • Modelo: cambiar NcmLayerFragment por un transformer real manteniendo forward(activaciones) -> activaciones; el worker no se entera.
  • Crecimiento: añadir una cuarta GrowthAction (por ejemplo, dividir un tramo para paralelismo de tensor) solo requiere extender apply_growth.
  • Fusión: sustituir FedAvg por ponderación por pérdida sin tocar el servicio gRPC.
  • Descubrimiento: cambiar mDNS por Consul; el NodeRegistry es 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: load es una aproximación para el router, no un load balancer.
  • No garantiza orden entre peticiones concurrentes: cada una lleva request_id y 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.