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:
- Configurar el endpoint
/v1/grammar/correctpara shadow B contra A durante 200 requests sacadas del eval set de la Fase 20. - 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).
- Producir un reporte comparando las dos variantes.
TODOs¶
Bloque A — modo shadow¶
- Añade
src/miniserve/traffic.pycon el router sin estado descrito entheory/02. API pública:assign(request_id: str, route_config: RouteConfig) -> Assignment.RouteConfiglleva strategy + arm SHAs + salt. - Configura
/v1/grammar/correctpara 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
requestsohttpxdesde un pequeño cliente Python). Confirma 200 entradas entraffic.log, cada una conresponse_Ayresponse_B.
Bloque B — modo A/B (solo latencia)¶
- Reconfigura
/v1/grammar/correcta A/B 50/50 víahash(request_id, salt="prod-2026-05") mod 2. - Envía otras 200 requests. Confirma que
traffic.logahora 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.jsonlista: 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 spantraffic.armen cada traza. Abre el dashboard Grafana del lab 03 de la Fase 34; filtra portraffic.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 unroute_config.jsonquesrc/miniserve/lee al arranque. - Añade
just shadow-offpara revertir a producción de un solo brazo. - Documenta ambos en el
README.mddel 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_iddado siempre cae en el mismo brazo dentro de una época de salt. Verifica enviando el mismorequest_iddos 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ñadetraffic.pydentro del existentesrc/miniserve/. Actualizasrc/miniserve/BLUEPRINT.mdcon una sección "Phase 38 extensions" listando esta adición.
Condiciones de parada¶
Hecho cuando:
traffic.logtiene 400 entradas (200 shadow + 200 A/B).report.mdincluye números de calidad (desde shadow) y latencia (desde A/B) con tests de significancia y desgloses por bucket.- La fila de modelo de amenazas está commiteada.
- El test de excepción del lado B en shadow pasa (B cayendo no afecta a A).
- El dashboard Grafana muestra el slice
traffic.armpoblado. - La recomendación operacional en
report.mdestá 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
awaitsobre 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 unrequest_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.pydel lab debe afirmargrader_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.