Source document

Perfilado de nodos: medir, no asumir

Spanish text, transcribed from the repository. The unformatted source is data-architecture/profiling.md.

Regla del documento: cada número que aparece aquí viene de una medición o de una detección, y está marcado como tal. Lo que no se puede medir se dice, no se rellena.

1. El problema

Un enjambre heterogéneo no puede tratar a todos los nodos igual. La forma habitual de distinguirlos es una tabla de especificaciones: "un ThinkPad T14 tiene 16 GB de RAM". Esa tabla miente por tres motivos, y los tres se han visto en este proyecto:

  1. El hardware se degrada. Un portátil con el disipador lleno de polvo rinde la mitad que el día que se compró, con las mismas especificaciones.
  2. La carga cambia. Un nodo al 85 % de CPU no sirve para lo mismo que uno vacío, aunque sean el mismo modelo.
  3. Las especificaciones no dicen lo que importa. "6 GB de VRAM" no dice nada sobre si esa VRAM se puede escribir a 700 GB/s o a 90.

ASA resuelve esto en entornos IoT heterogéneos perfilando primero y agrupando después: los tiers salen de los datos del parque, no de un catálogo de modelos de dispositivo. HECATE adopta la misma idea con dos diferencias: el perfil es continuo (no sólo de arranque) y cada campo lleva registrada su procedencia.

2. Qué mide el benchmark de arranque

crates/mesh-profiler/src/bench.rs. Cuatro pruebas, acotadas para que el perfilado completo ronde uno o dos segundos:

PruebaMétodoQué obtieneLímite declarado
Cómputomultiplicación de matrices 192³ × 3, orden i-k-jGFLOPS fp32sin SIMD ni BLAS: es un suelo, no el techo del nodo
Memoriasuma de un buffer de 32 MiB × 3 pasadas, cuatro acumuladoresGB/sun solo hilo: no satura un bus multicalanal
Discoescritura y lectura de 24 MiB con sync_allMB/ssecuencial; no mide IOPS
Redeco TCP en 127.0.0.1, 21 muestras, medianaRTT en msmide la pila del sistema, no el enlace entre máquinas

Los dos últimos "límite declarado" importan. El benchmark de red usa el bucle local: si un nodo tarda 2 ms en hablar consigo mismo, ningún enlace real será más rápido. Sirve como suelo, no como predicción.

Que el benchmark de cómputo sea un suelo es la razón de que el selector no use un roofline clásico sobre esos GFLOPS: predeciría segundos donde hay milisegundos. Ver model-zoo.md §4.

Medido en esta máquina (Windows 11, Core Ultra 7 255H, RTX PRO 500)

MétricaBinario debugBinario release
GFLOPS fp320,4934–37 (3 medidas)
Ancho de banda1,32 GB/s12–20 GB/s (varía entre ejecuciones)
Tier resultantemidmid

Dos conclusiones que no son obvias y conviene tener escritas:

  1. Hay que perfilar con el binario de release. La diferencia es de 70× en cómputo. Un perfil tomado con cargo run describe un nodo que no existe, y el tier podría caer a low en un equipo que no lo es. Los benchmarks viven en el mismo crate que el resto, así que el compilador de depuración también los ralentiza.
  2. El benchmark de memoria mejoró al romper la cadena de dependencias. Con un solo acumulador daba 7,5 GB/s; con cuatro, entre 12 y 20 según la ejecución. La primera cifra era del bucle, no de la memoria. La varianza de la segunda (casi 2× entre pasadas) es en sí un dato: tres pasadas de 32 MiB son pocas para un benchmark estable, y el selector sólo necesita el orden de magnitud. Si se quiere una cifra publicable, hay que subir MEMORY_BYTES y repetir. Aun así, 12–20 GB/s sigue por debajo de los ~50 GB/s que esta máquina sostiene con una herramienta multihilo: un hilo escalar no satura el bus, y por eso el límite está declarado en la tabla.

3. Qué detecta el sistema operativo

crates/mesh-profiler/src/detect.rs:

DatoFuentePlataforma
Núcleos, frecuencia, marca de CPUsysinfoLinux, macOS, Windows
RAM total y disponiblesysinfoLinux, macOS, Windows
Tipo de disco (SSD/HDD)sysinfoLinux, macOS, Windows
GPU y VRAMnvidia-smidonde haya driver NVIDIA
TOPS de NPUno se detectaninguna API portable los expone

Lo que no se detecta, y por qué

GPU AMD y Apple. rocm-smi e ioreg tienen formatos distintos y no hay máquina aquí para probarlos. Un nodo con GPU AMD se perfila como nodo sin acelerador: la respuesta conservadora. El efecto es que pide variantes para CPU, que funcionan; lo contrario —asumir una GPU que luego no está— daría un plan que falla al cargar.

TOPS de NPU. Los declara el operador con --npu-tops y el perfil los marca FieldSource::Declared, no Measured. La distinción es la que impide que un dato inventado se cuele en una decisión como si fuera una medición.

Multi-GPU

nvidia-smi devuelve todas las tarjetas. El perfil guarda la VRAM de la mayor, no la suma, y el nombre incluye el número (2x RTX 3080). Sumar llevaría a aceptar variantes que no caben en ninguna tarjeta por separado.

4. Procedencia: el campo sources

Cada perfil lleva un mapa campo → origen:

"sources": {
  "cpu_gflops": "measured",
  "memory_bandwidth_gbps": "measured",
  "ram_gb": "detected",
  "gpu_vram_gb": "absent",
  "npu_tops": "declared"
}

FieldSource::trustworthy() devuelve true sólo para Measured y Detected. Un consumidor que quiera ser estricto puede exigir que cpu_gflops sea medido antes de fiarse de una predicción; el dato está ahí para decidirlo, en lugar de tener que confiar en el formateador.

5. Clasificación en tiers

Dos caminos, con la misma puntuación de capacidad:

score = 1.0·(gflops/100) + 0.5·(GB/s/50) + 1.5·(VRAM/8) + 0.5·(RAM/16) + 1.5·(NPU/10)
        − 0.3·ln(1 + latencia_ms/10)
  • Por umbrales (classify_tier): ≥ 6.0 alto, ≥ 1.8 medio, resto bajo. Determinista y explicable. Sirve con pocos nodos.
  • Por histórico (classify_tier_from_history): K-Means con k = min(3, n) sobre los perfiles del parque. Los tiers son relativos: en un parque de portátiles antiguos, el mejor sigue siendo High. Es lo deseable, porque la etiqueta describe el parque y no un ideal absoluto.

El K-Means está implementado a mano (capability.rs) con inicialización por punto más lejano en lugar de k-means++ aleatorio: dos ejecuciones con el mismo histórico producen el mismo agrupamiento, que es lo que permite publicar y reproducir el resultado. Un test lo verifica.

Por qué la latencia entra con logaritmo

Un castigo lineal convertiría un nodo con buena GPU y 100 ms de red en el peor del parque. Con ln(1 + ms/10), 1 ms cuesta 0.03 puntos y 100 ms cuesta 0.72: penaliza sin anular. Otro test comprueba que un nodo con GPU y 100 ms sigue siendo mejor que uno sin GPU.

6. Telemetría continua y contención

crates/mesh-profiler/src/host.rs. Dos señales, no una:

  • Uso de CPU y memoria de sysinfo (porcentaje del sistema).
  • Contención medida: se ejecuta dos veces la misma carga aritmética (una al perfilar, otra al muestrear) y se compara el rendimiento. 1 − actual/línea_base es cuánto se ha ralentizado este proceso.

La segunda no se puede inferir del porcentaje de CPU: un nodo al 40 % con el planificador saturado rinde peor que uno al 85 % con núcleos libres. HostMetrics::effective_load() toma el máximo de las tres señales (CPU, memoria, contención) porque basta con que una esté mal para que la IA local deba ceder.

7. Uso

# Perfil completo del nodo local, con benchmarks de disco y red
./target/release/hecate-node profile --node-id t14

# Perfil rápido (sin disco ni red) guardado para reutilizarlo
./target/release/hecate-node profile --fast --out perfil-t14.json

Usa siempre ./target/release/...: con el binario de depuración las cifras no describen el nodo (ver §2).

8. Estado real de la campaña de perfiles

SemanaEntregableEstado
1Perfilador y clasificación por tiers✅ benchmarks reales, dos vías de clasificación · ❌ no se han perfilado los tres nodos físicos (Geekom, T14, ThinkPad): no están accesibles desde esta máquina
1El perfil se actualiza al cambiar la carga✅ HostSampler con contención medida · ❌ no hay bucle de re-perfilado periódico programado en el orquestador

Que la primera casilla esté a medias es el dato más importante de esta sección: el código perfila esta máquina, y la clasificación de los tres nodos del plan sigue siendo una predicción hasta que se ejecute en ellos.

9. Límites

  • El benchmark de cómputo no usa SIMD ni BLAS: sus GFLOPS son un suelo.
  • El benchmark de memoria usa un solo hilo: mide menos de la mitad del bus real.
  • Hay que perfilar con el binario de release; cargo run infravalora por un orden de magnitud.
  • El benchmark de red mide el bucle local, no el enlace con otros nodos.
  • No se detectan GPU AMD ni Apple, ni TOPS de NPU.
  • El re-perfilado no está programado; hay que invocarlo.
  • La contención se mide con una carga aritmética corta: un pico momentáneo puede colarse en una muestra. La histéresis del guardián está precisamente para eso.