Skip to content

English · Español

Lab 01 — Routing shadow + A/B sobre el endpoint del tutor de gramática

Objetivo: cablear src/miniserve/traffic.py (un fichero añadido dentro del módulo existente, no un módulo nuevo) en el servidor de la Fase 33 y producir un reporte lado-a-lado de calidad de corrección para dos variantes del tutor de gramática.

Tiempo estimado: 4–6 horas.

Prerequisito: lab 00 hecho; src/miniserve/ acepta requests para el endpoint del tutor de gramática (/v1/grammar/correct); el harness de evaluación de la Fase 20 puede evaluar salidas contra la tabla canónica de conjugación.


Lo que produces

experiments/38-shadow-ab/ conteniendo:

  • route.py — driver que cablea shadow + A/B en el stack de servicio.
  • traffic.log — las decisiones de routing por request (un JSONL).
  • compare.py — script que diffea salidas A y B y produce un reporte.
  • report.md — el reporte lado-a-lado (precisión de conjugación, latencia p50/p95/p99, estadísticas de diff).
  • manifest.json.

El escenario

Dos modelos registrados del lab 00: - A = baseline FP32 de la Fase 18 (semver v0.1.0). Mini-GPT plano sin especialización de tutor de gramática. - B = tutor de gramática LoRA de la Fase 28 (semver v0.3.0). Adaptador LoRA sobre A, entrenado en la rejilla de conjugación de verbos.

Vas a:

  1. Configurar el endpoint /v1/grammar/correct para shadow B contra A durante 200 requests sacadas del eval set de la Fase 20.
  2. Reconfigurar el mismo endpoint a A/B 50/50 durante otras 200 requests (un A/B de latencia, no de calidad — la calidad se evalúa offline contra la tabla canónica).
  3. Producir un reporte comparando las dos variantes.

TODOs

Bloque A — modo shadow

  • Añade src/miniserve/traffic.py con el router sin estado descrito en theory/02. API pública: assign(request_id: str, route_config: RouteConfig) -> Assignment. RouteConfig lleva strategy + arm SHAs + salt.
  • Configura /v1/grammar/correct para que en cada frase entrante:
  • El router decida "production_arm=A, shadow_arm=B".
  • El handler despache la inferencia de A síncronamente, sirva la corrección de A al cliente.
  • El handler despache la inferencia de B como tarea asyncio en background (no la awaitees en el request path).
  • Cuando B completa, loggea ambas correcciones A y B, ambas latencias, el assignment, y el trace ID a traffic.log (una línea JSON por request).
  • Confirma por inspección: la latencia cara-al-usuario es ≈ la latencia de A, no A + B.
  • Envía 200 requests sampleadas del eval set de la Fase 20 (usa requests o httpx desde un pequeño cliente Python). Confirma 200 entradas en traffic.log, cada una con response_A y response_B.

Bloque B — modo A/B (solo latencia)

  • Reconfigura /v1/grammar/correct a A/B 50/50 vía hash(request_id, salt="prod-2026-05") mod 2.
  • Envía otras 200 requests. Confirma que traffic.log ahora muestra ~100 entradas con A servida y ~100 con B servida (se espera algo de varianza por el hash).
  • Importante: registra solo la respuesta servida y su latencia. No estamos coleccionando calidad online — solo latencia. Los A/Bs de calidad online para el tutor de gramática son un anti-patrón (ver theory/02).
  • Verifica stickiness: envía el mismo request_id dos veces seguidas, confirma que ambas respuestas vienen del mismo brazo.

Bloque C — grading offline y comparación

  • En compare.py:
  • Para las trazas shadow (Bloque A), evalúa ambas correcciones A y B contra la tabla canónica de conjugación de la Fase 20. Reporta conjugation_accuracy_A vs conjugation_accuracy_B.
  • Calcula la distribución de diff: de las 200 requests, ¿en cuántas A y B produjeron correcciones diferentes? De esas, ¿cuántas fueron correctas para A vs correctas para B?
  • Para las trazas A/B (Bloque B), calcula percentiles de latencia (p50, p95, p99) por brazo.
  • Escribe report.md:
  • Calidad de conjugación (desde shadow). Precisión A = X%, Precisión B = Y%. Resultado del test z de dos proporciones. IC 95% sobre el diff. Desglose por bucket (por tiempo, por persona, por verbo).
  • Latencia (desde A/B). p50 A vs B, p95 A vs B, p99 A vs B. Significancia vía U de Mann-Whitney sobre las muestras de latencia.
  • Detalles de diff. A y B desacordaron en N/200 casos. De esos, B mejoró en M (p. ej., A dijo que "I goed" era correcto, B dijo "I went"), regresó en K (p. ej., A aceptó correctamente "I went", B lo machacó), neutral en N-M-K.
  • Recomendación operacional. Basada en lo anterior + el CpQU del lab 03 (escribe el placeholder; llénalo tras el lab 03): promover B / sostener B / re-entrenar.

Bloque D — manifest + hook de auditoría + chequeo de observabilidad

  • manifest.json lista: los dos SHAs canónicos registrados usados; el hash DVC del eval set de la Fase 20; la config de tráfico; semillas.
  • Añade una fila a security/THREATS.md: el routing shadow introduce un nuevo code path que corre B incondicionalmente — confirma que la salida de B nunca se filtra al cliente ni siquiera ante una excepción del lado B. Testea con un B que siempre lanza; la respuesta de A debe seguir llegando al cliente y la latencia no debe regresar.
  • Confirma que src/miniobserve/ (Fase 34) ve el atributo de span traffic.arm en cada traza. Abre el dashboard Grafana del lab 03 de la Fase 34; filtra por traffic.arm == "shadow_B"; confirma que los paneles se llenan.

Bloque E — recetas del Justfile

  • Añade just shadow-on <production_sha> <shadow_sha> para flipear el endpoint a modo shadow escribiendo un route_config.json que src/miniserve/ lee al arranque.
  • Añade just shadow-off para revertir a producción de un solo brazo.
  • Documenta ambos en el README.md del lab.

Restricciones

  • Router sin estado. La decisión es una función pura de (request_id, salt) y la config de ruta. Sin DB, sin Redis. (El lab 02 de la Fase 33 ya impone esto.)
  • Assignment sticky. Un request_id dado siempre cae en el mismo brazo dentro de una época de salt. Verifica enviando el mismo request_id dos veces; ambos deben rutear al mismo modelo.
  • Sin A/B de calidad online. El grading es offline contra la tabla canónica de la Fase 20. Si tus trazas A/B requieren revisión experta, el experimento está malformado.
  • Correr en CPU. Ambos modelos son lo suficientemente pequeños para servir desde CPU en el hardware de Borja. Si la latencia es demasiado lenta para llegar a 200 requests en un tiempo razonable, batchéalas.
  • Sin nuevo src/<module>/. Añade traffic.py dentro del existente src/miniserve/. Actualiza src/miniserve/BLUEPRINT.md con una sección "Phase 38 extensions" listando esta adición.

Condiciones de parada

Hecho cuando:

  1. traffic.log tiene 400 entradas (200 shadow + 200 A/B).
  2. report.md incluye números de calidad (desde shadow) y latencia (desde A/B) con tests de significancia y desgloses por bucket.
  3. La fila de modelo de amenazas está commiteada.
  4. El test de excepción del lado B en shadow pasa (B cayendo no afecta a A).
  5. El dashboard Grafana muestra el slice traffic.arm poblado.
  6. La recomendación operacional en report.md está escrita (incluso si el placeholder dice "pendiente lab 03").

Pitfalls

  • Threading del cómputo shadow. Si corres la inferencia de B síncronamente en la misma tarea que sirve A, has duplicado la latencia. El shadow debe ser fire-and-forget (tarea background asyncio o thread pool). Verifica midiendo la latencia cara-al-usuario — debería ser ≈ latencia solo-A, no 2× A.
  • Fugas de excepción en shadow. Si B lanza y haces await sobre ello en el request path, la excepción burbujea y el cliente ve un 500. El handler debe capturar todas las excepciones de la tarea shadow y loggearlas; el cliente solo ve la respuesta de A.
  • Salt para hash routing. Usa un salt fijo por entorno (prod-2026-05). Cambiar el salt re-baraja el assignment y contamina las comparaciones A/B.
  • Reuso de request_id. Si el cliente no envía un request_id, debes generarlo server-side (UUIDv4). No hashees la URL o el input — eso rompe el stickiness a nivel usuario.
  • IDs de tracking en logs. El logging estructurado de la Fase 34 debe incluir el campo traffic.arm. Si no lo hace, los dashboards Grafana que la Fase 39 construirá no podrán slicear por brazo de tráfico. Verifica en el dashboard.
  • Drift de versión del grader. El grader de la Fase 20 debe ser el mismo SHA en las corridas de grading de A y B. El compare.py del lab debe afirmar grader_sha_in_use == eval_baseline.json["grader_sha"] y fallar ruidosamente si no.

Cuándo consultar solutions/

Tras los cinco bloques. solutions/01-shadow-ab-ref.md (apertura de fase) revisa tu diseño de routing, el patrón de manejo de excepción shadow, y la estructura del reporte.


Siguiente lab: lab/02-drift-detection.md.