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/02ytheory/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 medidoscache_bytesvsS.latency.json— puntos de datos medidosper_token_msvsS(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_maxa 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). Usanp.emptypara pre-asignaciones, nonp.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ínea2LHd_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:
- 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)?
- 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?
- 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-cachereusa el mismo camino de código del modelo, solo desactiva el caché. No escribas un modelo separado; solo pasacache=Noney vuelve a llamar ageneratepor paso con un prompt creciente.
Condiciones de parada¶
Hecho cuando:
- La medición de memoria está dentro del 10% de la teoría en cada punto \(S\).
- El ajuste de latencia con caché tiene \(R^2 \geq 0.95\).
- 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).
- Ambos PNG commiteados y renderizados correctamente.
README.mdresponde a las tres preguntas del Bloque D.manifest.jsoncommiteado conresults_summaryrellenado.
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.emptyasigna 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-cachemá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.