English · Español
Teoría 02 — Flujo de datos de extremo a extremo: una petición, todas las capas¶
Una sola petición HTTP del grammar tutor atraviesa nueve capas, desde el accept(2) del kernel hasta el JSON en el socket de respuesta. Aquí seguimos cada byte: cuántos, en qué forma, en qué etapa del latency budget y con qué percentil. Sumar p95 por etapa no da el p95 total — esa falacia se demuestra al final del capítulo.
Por qué importa un recorrido a nivel de bytes¶
Las Fases 11–34 construyeron cada una una etapa del pipeline por separado. La ruta de petición del grammar tutor es la primera vez que un learner las ve compuestas. Tres cosas se rompen al componer que ninguna vista de etapa única expone:
- Desajustes de formato de bytes. El BPE de la Fase 11 emite
list[int]; el Mini-GPT de la Fase 17 consumetorch.LongTensorcon forma(batch, seq_len). El castlist[int]→ tensor está en algún sitio; si está en el sitio equivocado, asignas memoria dos veces. - Colisiones de latency budget. Cada etapa cree que tiene 500 ms. El usuario tiene 5 s totales. Las cuentas no salen a menos que alguien audite la suma, que es el trabajo de la Fase 39.
- Aritmética de percentiles. Ingenieros fluidos en percentiles igual la lían: \(p_{95}(A + B) \ne p_{95}(A) + p_{95}(B)\). El panel de p95 por etapa del dashboard del capstone es informativo; solo el p95 de extremo a extremo (calculado sobre trazas completas) es load-bearing.
Este capítulo recorre una petición — POST /v1/grammar/correct con body {"sentence": "Yesterday I goed to the store"} — a través de cada capa con números concretos.
Las nueve capas¶
| # | Capa | Fase | Entrada | Salida | Bytes típicos | ms típicos |
|---|---|---|---|---|---|---|
| 1 | HTTP ingress (FastAPI + uvicorn) | 33 | bytes TCP | objeto Request |
~600 B in, ~400 B out | 0.5 |
| 2 | Validación de schema (Pydantic + schema de salida Fase 30) | 30 | body de Request |
pydantic CorrectRequest |
n/a | 0.3 |
| 3 | Guardas de seguridad (rate-limit, body-size, filtro de inyección) | 33, 37 | CorrectRequest |
CorrectRequest (o 4xx) |
n/a | 0.2 |
| 4 | Tokenización BPE | 11 | str ("Yesterday I goed...") |
list[int] de longitud ~10 |
~30 B → 80 B (int64) | 1.0 |
| 5 | Recuperación RAG (bi-encoder + híbrido BM25) | 29 | list[int] o texto crudo |
top-5 chunks (~2 KB) | ~80 B → ~2 KB | 25 |
| 6 | Prefill de Mini-GPT (encode prompt + contexto) | 17, 22 | (1, ~80) int64 |
(1, 80, |V|) float32 |
~80 B → ~5 MB logits + ~3 MB KV cache | 800 |
| 7 | Decode de Mini-GPT (generar ~30 tokens) | 17, 21, 22 | KV cache | (1, ~30) int64 + 30 × appends a KV cache |
~30 × 100 KB = 3 MB | 2,500 |
| 8 | Formateo de salida (JSON estructurado vía schema Fase 30) | 30 | (1, ~30) int64 + decode BPE |
JSON CorrectResponse |
~80 B → ~400 B | 2.0 |
| 9 | Emisión de coste + flush de trace + escritura de respuesta | 34 | CorrectResponse |
bytes TCP | 400 B out + ~2 KB métricas | 1.5 |
Wall time total (típico, una petición, cache caliente): ≈ 3.330 ms.
Por objetivo: la DoD requiere p95 < 5.000 ms con 10 peticiones concurrentes (Plan §7). Los 3.3 s típicos dejan un buffer de 1.7 s para la cola de latencia y el queueing. La teoría de continuous-batching de la Fase 33 dice que la espera en cola domina la cola; el panel de queue depth del dashboard del capstone es el indicador adelantado.
Dónde están los bytes¶
Dos patrones para interiorizar:
- Las entradas son diminutas, el estado interno es grande. Body de la petición: 600 B. KV cache durante el decode: ~3 MB. Esa ratio (5000×) explica por qué importa la Fase 22 (KV cache): lo barato a optimizar serían los bytes que el usuario envía; lo valioso a optimizar son los bytes que el modelo mantiene en RAM.
- El coste dominante es el decode, no el prefill. El prefill son 800 ms para el prompt completo de una vez; el decode son 2.500 ms para 30 tokens uno a uno. La dominancia del decode es intrínseca a la generación autorregresiva y es el resultado titular del modelo de coste de la Fase 21.
El latency budget¶
Asignar 5.000 ms entre etapas. El Plan §2 de la Fase 39 dice: asignar proporcional a \(\sigma_i\) (desviación estándar de cada etapa). En la práctica para una demo CPU-only de un solo nodo:
| Etapa | Asignación | Justificación |
|---|---|---|
| HTTP + schema + guardas | 50 ms | Baja varianza; casi constante en proceso caliente |
| BPE tokenize | 20 ms | Baja varianza; bucle Python puro pero acotado |
| RAG retrieve | 200 ms | Varianza moderada; el tamaño del índice afecta a la cola |
| Mini-GPT prefill | 1.200 ms | Alta varianza con la longitud de la secuencia |
| Mini-GPT decode | 3.300 ms | Dominante. Varianza con la longitud del output |
| Format + cost + flush | 30 ms | Baja varianza |
| Buffer (queueing, pausas de GC) | 200 ms | Holgura |
| Budget total | 5.000 ms | Coincide con el objetivo DoD |
El panel latency budget del dashboard de Grafana renderiza esto como una barra apilada; el timing real por petición se renderiza debajo. Los diffs visuales (presupuesto vs real) atrapan violaciones de presupuesto en segundos tras la regresión.
Qué significa realmente "budget"¶
Un latency budget no es "ninguna etapa excederá jamás su asignación." Es "si una etapa lo excede repetidamente, eso es una deuda que pagar." La métrica budget burn rate del dashboard (tiempo de etapa / asignación) se monitoriza; un burn sostenido > 1,5× para cualquier etapa dispara un carry-over para la Fase 40.
La falacia de sumar percentiles¶
Un bug común en dashboards de rendimiento:
"Se muestra el p95 de cada etapa. La suma de p95s se reporta como 'p95 de extremo a extremo'."
Esto está mal. Matemáticamente:
La intuición: el p95 de la etapa A y el p95 de la etapa B se observan habitualmente en peticiones distintas. La petición lenta en la etapa A no es la misma petición lenta en la etapa B. Sumar p95s por etapa asume correlación pesimista; la distribución real de extremo a extremo tiene colas más ligeras.
Una demostración concreta¶
Toma dos etapas, cada una con latencia distribuida uniformemente en \([100, 1000]\) ms:
- \(p_{95}(A) = 955\) ms
- \(p_{95}(B) = 955\) ms
- Suma ingenua: \(1910\) ms
- \(p_{95}(A + B)\) real (vía simulación de \(10^6\) muestras): ≈ \(1690\) ms
La estimación ingenua sobreestima el p95 real ~13%. En un dashboard con 9 etapas, la sobreestimación se compone. Los ingenieros ven una "cola de latencia" que en realidad no existe y persiguen la optimización equivocada.
Mitigación: el dashboard calcula los percentiles de extremo a extremo a partir de trazas completas de petición (Tempo / Jaeger), no de estadísticas resumen por etapa. El p95 por etapa es informativo; te dice qué etapa es consistentemente la más lenta, no cuál es la cola de extremo a extremo.
El Lab 01 incluye un script puntual que recupera trazas recientes, calcula ambos números, e imprime su delta. La primera ejecución típicamente muestra un delta >10% — el capstone lo hace visible.
Propagación de trazas: hacer que la imagen funcione¶
La vista de latencia de extremo a extremo del dashboard depende de que cada etapa emita un span con el mismo trace_id y una relación padre-hijo. El contrato de la Teoría 01:
| Etapa | Emite span | Padre |
|---|---|---|
| 1 — HTTP ingress | http.request |
(root) |
| 2 — Validación | validate.body |
http.request |
| 3 — Guardas | security.check |
http.request |
| 4 — BPE | tokenize.bpe |
http.request |
| 5 — RAG | retrieve.hybrid |
http.request |
| 6 — Prefill | model.prefill |
http.request |
| 7 — Decode | model.decode |
http.request |
| 8 — Format | format.json |
http.request |
| 9 — Cost | cost.emit |
http.request |
Cada span lleva request_id como atributo. El emisor de coste de la Fase 34 escribe el coste como atributo de span (no como una línea de métrica aparte) para que la traza y el coste se unan por un único ID sin un JOIN downstream.
Frontera entre procesos: el tool MCP¶
Si la petición dispara una llamada a un tool MCP (raro en la ruta del grammar tutor; común en el run-through de seguridad del Lab 03), el proceso hijo debe heredar el contexto de traza. Mecanismo:
- El padre serializa
traceparent+tracestateen variables de entorno (spec W3C trace-context). - El subproceso las lee al arrancar y re-establece el contexto de span.
- El subproceso emite sus spans como hijos del padre.
El blueprint de tool de la Fase 31 codifica esto; la Fase 39 lo verifica en el Lab 03 afirmando que el span del payload malicioso aparezca como hijo del span de la petición de la demo.
Modo de fallo (Escollo 5 del Plan): si TRACEPARENT no se propaga, el subproceso emite spans huérfanos. El panel "Orphan span count" del dashboard dispara una alerta. El Lab 01 tiene una auditoría de una sola línea.
El viaje de los bytes, condensado¶
Una petición de 600 bytes se convierte en:
- 30 B de texto UTF-8 de la frase (entrada de la capa 4).
- 80 B de token IDs int64 (salida de la capa 4, entrada de la capa 6).
- 5 MB de logits float32 (interno de la capa 6).
- 3 MB de KV cache (interno de la capa 6 / 7).
- 400 B de respuesta JSON (salida de la capa 8, entrada de la capa 9).
- 600 B de respuesta HTTP + 2 KB de métricas + ~5 KB de datos de traza (salida de la capa 9).
La amplificación de 5.000× de la entrada al pico de estado interno es la raison d'être de cada optimización consciente de la memoria que tocó el currículo: compactness del tokenizer (Fase 11), cuantización (Fase 26), compartir KV cache (Fase 22), continuous batching (Fase 33). El capstone hace esta amplificación visible.
Qué NO cubre esta teoría¶
- Los internals de cada etapa. Cada uno está cubierto en la teoría de su fase de origen.
- Layout de memoria de GPU. Demo CPU-only; el análisis de memoria de GPU es un asunto de la Fase 35 / 36.
- Respuestas en streaming. La demo usa una sola respuesta JSON, no server-sent events. SSE es una extensión de la Fase 33 marcada como lectura de la Fase 40.
- Bucles de agente multi-paso. El grammar tutor es de un solo turno. Bucles de agente multi-turno con planning viven en el territorio de la Fase 32 y están fuera de alcance aquí.
Siguiente: theory/03-cost-and-observability-stitching.md — cómo la Fase 34 (coste) + Fase 38 (CpQU) + Prometheus + Grafana + Tempo se vuelven todos un único dashboard.