Skip to content

English · Español

Lab 03 — Curvas de coste: memoria y latencia vs longitud de contexto

Objetivo: medir el crecimiento de memoria y la latencia de decode por token vs la longitud de secuencia, con y sin caché. Comparar con las curvas teóricas derivadas en theory/02 y theory/03. Quedar dentro del 10% de la teoría.

Tiempo estimado: 2–4 horas.

Prerrequisito: lab 01 + lab 02 completos. Modelo MiniGPT de la Fase 17 en disco.


Lo que produces

Un directorio experiments/22-kv-cost/ que contenga:

  • bench.py — runner de medición.
  • memory.json — puntos de datos medidos cache_bytes vs S.
  • latency.json — puntos de datos medidos per_token_ms vs S (una curva con caché, otra sin).
  • memory.png — superposición de medido + teoría.
  • latency.png — superposición de medido + teoría.
  • manifest.json.
  • README.md — interpretación; declara explícitamente el gap medido vs teoría.

Las mediciones

Dos curvas, ambas en función de \(S\) (longitud actual del caché):

Curva 1: crecimiento de memoria

Para \(S \in \{8, 16, 32, 64, 128, 256, 512, 1024\}\): - Ejecuta prefill sobre un prompt de longitud \(S\). - Mide: cache.bytes_allocated() y RSS_growth_since_baseline (usa psutil.Process().memory_info().rss). - Predicción teórica: 2 · L · H · d_h · S_max · B · s (con \(S_\text{max} = 1024\) para todos los puntos — la asignación es constante; solo la porción usada crece). Dibuja tanto cache.bytes_allocated() (constante — confirma la pre-asignación) como 2 · L · H · d_h · S · B · s (bytes teóricos usados; esto crece linealmente).

Curva 2: latencia de decode por token

Para \(S \in \{8, 16, 32, 64, 128, 256, 512, 1024\}\): - Prefill de un prompt de longitud \(S\), luego generar 1 token adicional. Cronometra solo el paso de decode (no el prefill). - Repite 100×, toma la mediana. - Registra: per_token_ms_with_cache y per_token_ms_without_cache.

Teoría: - Con caché: \(T \approx \alpha + \beta \cdot S\) para constantes \(\alpha, \beta\) derivadas de theory/01 (el término de pesos \(12 L d^2\) contribuye \(\alpha\); el término de cache-attention \(2 L S d\) contribuye \(\beta S\)). - Sin caché: \(T \approx \gamma \cdot S^2\) para algún \(\gamma\) (cada paso hace un prefill completo sobre el prompt-hasta-ahora).

Ajusta las constantes a las mediciones de \(S\) bajo; comprueba que la predicción extrapola a las mediciones de \(S\) alto dentro del 10%.

TODOs

Bloque A — escribir bench.py

  • Carga el MiniGPT de la Fase 17 (entrenado con gramática de verbos según §A13). Para este lab necesitas subir temporalmente S_max a 1024 para ver la curva de crecimiento — los prompts reales de nuestro corpus están todos por debajo de 32 tokens, pero la forma de la curva es lo que revela el escalado.
  • Prompts sintéticos de longitud \(S\): en vez de muestrear del corpus de gramática (la frase natural más larga ~10 tokens), genera prompts de longitud \(S\) repitiendo fragmentos cortos de gramática. La salida del modelo puede ser sin sentido con \(S\) grande; eso está bien — medimos coste, no calidad.
  • Implementa el bucle de medición de memoria. Usa seed_everything(42). Usa np.empty para pre-asignaciones, no np.zeros (poner a cero pierde tiempo).
  • Implementa el bucle de medición de latencia. Usa time.perf_counter_ns(). 100 reps con 3 warm-ups.
  • Guarda ambos en JSON.

Bloque B — ajustar las constantes de la teoría

  • En un script o celda de notebook separado: carga latency.json. Ajusta \(\alpha, \beta\) desde la curva con caché (regresión lineal sobre \(S\)). Ajusta \(\gamma\) desde la curva sin caché (regresión cuadrática).
  • Reporta la calidad del ajuste (\(R^2\)). Documenta en README.md.

Bloque C — graficar

  • memory.png: x = \(S\), y = bytes (y log). Dos líneas: cache.bytes_allocated() (línea plana en bytes de \(S_\text{max}\)), bytes teóricos usados (línea 2LHd_h S B s). Más el crecimiento RSS medido como puntos. Anota: "la pre-asignación oculta el crecimiento — el caché es de tamaño constante por diseño".
  • latency.png: x = \(S\), y = per_token_ms. Dos líneas (con caché, sin caché) medidas + curvas teóricas punteadas. Anota el cruce de regímenes.

Bloque D — interpretar

En README.md, tres párrafos:

  1. Memoria. ¿Coincide el crecimiento RSS medido con los bytes usados teóricos dentro del 10%? ¿Por qué podría no hacerlo (otras asignaciones durante el forward)?
  2. Latencia. ¿Cómo de bien funciona el ajuste lineal-en-\(S\) para la curva con caché? ¿La curva sin caché realmente escala como \(S^2\), o la lectura de pesos del factor constante sigue dominando con \(S\) pequeño?
  3. Crossover. ¿En qué \(S\) empieza el caché a "merecer la pena" relativo al camino sin caché? Por debajo de ese \(S\), el caché es más lento por overhead. Documenta esto.

Bloque E — manifest

{
  "experiment": "22-kv-cost",
  "date": "YYYY-MM-DD",
  "seed": 42,
  "versions": {"python": "3.11.x", "numpy": "X.Y.Z", "matplotlib": "X.Y.Z", "psutil": "X.Y.Z"},
  "hardware": {
    "cpu_model": "Intel Core i5-8250U",
    "cores_threads": "4/8",
    "ram_gib": 62,
    "cpu_governor_at_run": "performance"
  },
  "model": {
    "name": "miniGPT-phase17",
    "L": null, "H": null, "d_h": null, "S_max": 1024, "dtype": "float32"
  },
  "config": {
    "S_points": [8, 16, 32, 64, 128, 256, 512, 1024],
    "reps_per_point": 100,
    "warmup_reps": 3
  },
  "results_summary": {
    "memory_max_relative_error_to_theory": null,
    "latency_with_cache_R2": null,
    "latency_without_cache_R2": null,
    "crossover_S": null
  }
}

Restricciones

  • CPU governor: performance. Si no, los números de latencia son inútiles.
  • Conectado a la red, otras apps cerradas. Igual que los labs de la Fase 1.
  • Sin threading. Decode de un solo hilo para mediciones limpias. Multi-hilo es Fase 35.
  • No cronometres el prefill. Solo el paso de decode. El timing de prefill es un experimento distinto (y el DoD de la Fase 22 no lo requiere).
  • without-cache reusa el mismo camino de código del modelo, solo desactiva el caché. No escribas un modelo separado; solo pasa cache=None y vuelve a llamar a generate por paso con un prompt creciente.

Condiciones de parada

Hecho cuando:

  1. La medición de memoria está dentro del 10% de la teoría en cada punto \(S\).
  2. El ajuste de latencia con caché tiene \(R^2 \geq 0.95\).
  3. La curva de latencia sin caché muestra claramente crecimiento \(O(S^2)\) (o has documentado por qué no lo hace en las escalas que mediste).
  4. Ambos PNG commiteados y renderizados correctamente.
  5. README.md responde a las tres preguntas del Bloque D.
  6. manifest.json commiteado con results_summary rellenado.

Escollos

  • bytes_allocated() es constante; eso es correcto. La pre-asignación es el punto entero. No lo "arregles".
  • El timing de la primera iteración es enorme. Page faults, JIT, cachés frías. Descarta las primeras 3 mediciones (warm-ups), reporta mediana de 100, no media.
  • Ruido de medición de memoria. La medición de RSS es ruidosa — Linux puede asignar páginas perezosamente, y np.empty asigna virtual pero no páginas físicas hasta el primer touch. La línea "medida" debería seguir los bytes usados (rellenados) teóricos, que son los que se tocan. Documenta esto.
  • without-cache más rápido que with-cache con \(S\) pequeño. Esperado. El overhead de append + read del caché domina con \(S < 50\)-tantos en MiniGPT. El cruce es real y merece entenderse.

Cuándo consultar solutions/

Después de que se cumplan todas las condiciones de parada. La referencia en solutions/03-cost-curves-ref.md (escrita al abrir la fase) muestra los números esperados y el código de ajuste.


Siguiente: PHASE_22_REPORT.md. La fase termina tras reporte + reflexión.