Source document

Model zoo: variantes adaptadas y selección device-aware

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

El catálogo vive en examples/model-zoo.json y es el contrato entre los dos planos: Rust (mesh_scheduler::ModelZoo) y Python (hecate.model_zoo.ModelZoo) leen el mismo fichero. Un test de cada lado lo verifica.

1. Qué es una variante

Un modelo no se despliega: se despliega una variante. La diferencia es lo que permite que un portátil de 2012 y una workstation con dos RTX 3080 sirvan la misma tarea:

tarea: chat
  ├── ncm-chat-fp16      16 bits   7.00 GB   precisión 0.912   180 ms (referencia)
  ├── ncm-chat-int8       8 bits   3.50 GB   precisión 0.905    95 ms
  ├── ncm-chat-int4       4 bits   1.75 GB   precisión 0.884    61 ms
  └── ncm-chat-ternary   1.58 b    0.30 GB   precisión 0.798    44 ms

resident_gb no es un campo: se calcula (params_millions × bytes_por_peso). Así una variante no puede declarar 0.3 GB y ocupar 7 (un test lo comprueba: la validación rechaza cualquier variante cuyos mínimos declarados no cubran su huella).

Este planteamiento sigue la línea de LMEdge (predecir precisión, latencia y recursos de cada combinación modelo-cuantización) y de JUGGLER (enrutamiento task/model/device-aware sobre perfiles de dispositivo). HECATE se queda con la parte de decisión; la predicción de precisión no la hace, la recibe del catálogo.

2. Las tres comprobaciones duras

Antes de puntuar nada, una variante tiene que pasar tres filtros. Si no, se descarta con el motivo escrito:

FiltroCondiciónMotivo del descarte
Memoriaresident_gb ≤ usable_memory_gb × 0.9 y min_vram_gb ≤ VRAM si la declaraInsufficientMemory
Latenciapredicted ≤ max_latency_ms si la tarea lo imponeOverLatencyBudget
Precisiónexpected_accuracy ≥ min_accuracy si la tarea lo imponeBelowAccuracyFloor

El 10 % de margen no es pesimismo: un nodo cuya memoria útil es exactamente la huella del modelo no puede ejecutarlo, porque el runtime, el contexto y la fragmentación también ocupan.

Los descartes se devuelven en la decisión (Selection::rejected), no se tiran. Cuando un nodo no puede servir una tarea, la respuesta incluye por qué, variante a variante:

ninguna variante sirve la tarea chat en este nodo:
  ncm-chat-fp16: no cabe: 7.00 GB de pesos frente a 1.20 GB disponibles;
  ncm-chat-int8: no cabe: 3.50 GB de pesos frente a 1.20 GB disponibles;
  ncm-chat-int4: no cabe: 1.75 GB de pesos frente a 1.20 GB disponibles;
  ncm-chat-ternary: excede latencia: 51.2 ms predichos frente a 10.0 ms de presupuesto

3. La banda de calidad (y el error que evita)

El objetivo Balanced no compara todas las variantes entre sí. Primero descarta las que pierden más de un 5 % de precisión relativa frente a la mejor viable (MAX_ACCURACY_LOSS), y sólo entonces puntúa.

Sin esa banda el criterio se rompe, y se rompe de una forma concreta: la normalización min-max da a la variante más pequeña un 1.0 en latencia y otro 1.0 en energía. Con un peso del 55 % entre ambas, la más barata gana siempre, en todos los nodos. El resultado sería servir 0.798 de precisión en un nodo que podía dar 0.905, y —peor— los tres nodos del parque recibirían la misma variante, que es justo lo que este rediseño quiere evitar.

Con la banda aplicada sobre la topología de ejemplo:

NodoVRAMVariante elegida (equilibrado)Motivo
Geekom (2× 3080)24 GBncm-chat-int80.905 de precisión a la mitad de huella que fp16
ThinkPad T146 GBncm-chat-int4fp16 no cabe; int8 cabe pero int4 aprovecha mejor el nodo
Portátil antiguo0 GBncm-chat-ternaryúnica que cabe en 1.2 GB libres

Quien quiera sacrificar más precisión lo pide explícitamente con Objective::Latency (--objective latency) y, si hace falta, un mínimo con --min-accuracy.

4. Predicción de latencia, y por qué no es un roofline

La latencia del catálogo está medida en un dispositivo de referencia (40 GFLOPS, 20 GB/s). Para otro nodo se reescala por la restricción dominante:

ratio_cómputo = gflops_efectivos / gflops_referencia
ratio_memoria = GB/s_efectivos   / GB/s_referencia
speed         = min(ratio_cómputo, ratio_memoria)
latencia      = latencia_referencia / speed · (tokens/128) · (1 + contención)

Por qué no el roofline clásico. La primera versión calculaba max(FLOPs/GFLOPS_medidos, bytes/GB_s_medidos). Con los GFLOPS que mide el perfilador (un matmul escalar sin SIMD) predecía 1.6 segundos para una variante de 3.5 B parámetros en un nodo con acelerador, porque un motor de inferencia real es uno o dos órdenes de magnitud más rápido que ese bucle. Anclar a la latencia medida en el catálogo y escalar por capacidades relativas evita tener que inventar una constante de "aceleración del runtime".

gflops_efectivos y GB/s_efectivos son las medidas del nodo multiplicadas por su ratio de aceleración (1 + 11·VRAM/(VRAM+8), que satura). Ese multiplicador es una aproximación declarada: el perfil no transporta el ancho de banda de la GPU. Es el sitio exacto donde hay que mejorar cuando se mida.

La predicción se multiplica por 1 + current_load: un nodo cargado servirá más lento, y fingir que está vacío produciría decisiones optimistas que se pagan en la cola.

5. Carga de la variante con cuantización real

python/hecate/model_zoo/quantization.py implementa y empaqueta:

EsquemaEscalaCódigos por byteVerificación
int8una por canal de salida1error ≤ span/127
int4una por grupo de 32 pesos2error ≤ span/7; bytes exactamente la mitad
ternariouna por canal de salida4códigos en {−1, 0, 1}; bytes exactamente un cuarto

Guardar códigos int4 en un tensor int8 no ahorra ni un byte: sólo cambia el rango. Empaquetar dos por byte es lo que convierte la promesa de compresión en memoria ocupada, y por eso los tests cuentan bytes además de medir el error.

load_variant construye una pila de bloques transformer con la profundidad y la anchura de la variante, con los pesos cuantizados en buffers. Aviso honesto: el repositorio no contiene pesos entrenados del NCM —el modelo se genera de forma sintética y determinista, igual que hecate.ncm_transformer— así que el módulo cargado es funcional y reproducible pero su tamaño es de kilobytes, no de gigabytes. Lo que sí es real y está verificado es la cuantización, el empaquetado y la reducción de huella.

6. Uso

# Qué variante corresponde a este nodo (perfilándolo antes)
./target/release/hecate-node zoo --task chat

# Contra un perfil ya medido, y con presupuesto de latencia
./target/release/hecate-node zoo --profile perfil-t14.json --latency-budget 20

# El catálogo que consumen los dos planos
./target/release/hecate-node zoo --emit > examples/model-zoo.json

7. Estado real de la campaña

SemanaEntregableEstado
2Selector y model zoo✅ catálogo versionado, selector en los dos planos, variantes distintas por nodo
2El nodo antiguo recibe una variante que cabe en 8 GB✅ 0.30 GB de pesos en 1.2 GB libres
4Benchmarks de latencia antes/después❌ no iniciados: exigen medir la inferencia real en cada nodo

8. Límites

  • Las cifras de precisión del catálogo son declaradas (campaña del proyecto), no medidas por este código. Son el dato más blando del sistema y el primero que hay que reemplazar.
  • La latencia predicha es una estimación para ordenar candidatas, no un benchmark.
  • El ratio de aceleración se deriva de la VRAM, no de un ancho de banda de GPU medido.
  • load_variant carga un modelo sintético, no pesos entrenados.
  • No hay caché de decisiones: dos selecciones idénticas se recalculan.