# Perfilado de nodos: medir, no asumir > **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: | Prueba | Método | Qué obtiene | Límite declarado | | --- | --- | --- | --- | | Cómputo | multiplicación de matrices `192³` × 3, orden `i-k-j` | GFLOPS fp32 | sin SIMD ni BLAS: es un **suelo**, no el techo del nodo | | Memoria | suma de un buffer de 32 MiB × 3 pasadas, **cuatro acumuladores** | GB/s | un solo hilo: no satura un bus multicalanal | | Disco | escritura y lectura de 24 MiB con `sync_all` | MB/s | secuencial; no mide IOPS | | Red | eco TCP en `127.0.0.1`, 21 muestras, mediana | RTT en ms | mide 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`](model-zoo.md) §4. ### Medido en esta máquina (Windows 11, Core Ultra 7 255H, RTX PRO 500) | Métrica | Binario `debug` | Binario `release` | | --- | --- | --- | | GFLOPS fp32 | 0,49 | **34–37** (3 medidas) | | Ancho de banda | 1,32 GB/s | **12–20 GB/s** (varía entre ejecuciones) | | Tier resultante | `mid` | `mid` | 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`: | Dato | Fuente | Plataforma | | --- | --- | --- | | Núcleos, frecuencia, marca de CPU | `sysinfo` | Linux, macOS, Windows | | RAM total y disponible | `sysinfo` | Linux, macOS, Windows | | Tipo de disco (SSD/HDD) | `sysinfo` | Linux, macOS, Windows | | GPU y VRAM | `nvidia-smi` | donde haya driver NVIDIA | | TOPS de NPU | **no se detecta** | ninguna 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`: ```json "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: ```text 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 ```bash # 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 | Semana | Entregable | Estado | | --- | --- | --- | | 1 | Perfilador 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 | | 1 | El 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.