English · Español
05 — Gates de deploy en CI y vocabulario de capacidad¶
🇪🇸 Esta fase enseña dos cosas distintas en el mismo archivo: (1) el gate de CI — la única forma legítima de promover un checkpoint del tutor a producción, con su lógica de comparación contra una baseline versionada — y (2) un tour de vocabulario sobre capacidad (HPA por tokens/seg, MIG/MPS, spot vs on-demand), que aparece en toda la literatura aunque Borja no lo ejercite localmente.
Parte 1: CI como único camino a producción¶
La tesis¶
Un tutor de gramática que ocasionalmente acepta "he goed" es peor que ningún tutor — un aprendiz se fía de la corrección incorrecta y la interioriza. El suelo para la promoción tiene que ser impuesto por código, no por memoria o convención. La defensa estructural de la Fase 38 es: la promoción al tag semver de producción ocurre solo como el último paso exitoso de un workflow de CI que corrió la evaluación de la Fase 20 contra el candidato y comparó la precisión por bucket contra una baseline commiteada.
No hay flag --force. No hay "lo promociono local y lo compruebo luego". El registry.register_pending(run_id) local existe para desarrollo; el registry.promote(canonical_sha, semver) local está deshabilitado en el entorno LYNX_ENV=prod y solo puede ejecutarlo la identidad de GitHub Actions del workflow de CI.
La forma del workflow¶
.github/workflows/deploy-grammar-tutor.yml tiene seis etapas:
flowchart LR
A[trigger: PR labeled\n'deploy-candidate'] --> B[checkout + uv sync]
B --> C[dvc pull eval set + corpus]
C --> D[mlflow download artifact\n by run_id]
D --> E[run Phase 20 eval]
E --> F{per-bucket\nregression\n> 2pp?}
F -->|no| G[registry.promote\n+ semver tag]
F -->|yes| H[fail workflow\n+ comment on PR]
G --> I[notify channel:\n'promoted v0.X.Y']
Cada etapa es shell + un único script de Python:
- Trigger. Una PR con la etiqueta
deploy-candidatey un comentariomlflow_run_id=<id> semver=v0.3.1dispara el workflow. La etiqueta está gateada porCODEOWNERS— solo Borja puede etiquetar. - Checkout + sync. Estándar.
uv sync --frozenpara instalar las deps lockeadas; sin re-resolución. - DVC pull.
dvc pull data/eval/phase-20.jsonl.dvcy el corpus en vivo. El hash DVC del eval set se afirma contra el valor en el campoeval_set_dvc_hashdeeval_baseline.json— un mismatch falla inmediatamente. Esta es la garantía del rastro de auditoría de que estás evaluando contra el mismo eval set cada vez. - MLflow download.
mlflow.artifacts.download_artifacts(run_id=...)baja el bundle del candidato a./candidate/. - Eval.
python -m minieval --bundle ./candidate/ --eval-set data/eval/phase-20.jsonl --output candidate_eval.json. Escribe precisión por bucket. - Comparar y decidir.
python -m scripts.mlops.compare_baseline --candidate candidate_eval.json --baseline eval_baseline.json --tolerance 0.02. Exit code 0 → promover; no-cero → fallar.
La promoción es el último paso. Fallo en cualquier etapa anterior significa que no ocurre cambio en el registry. Este es el contrato de no-medio-estado.
El fichero baseline¶
eval_baseline.json vive en la raíz del repo, junto a LYNX_CORTEX.md. Su forma:
{
"eval_set_dvc_hash": "...",
"baseline_sha": "<previous_promoted_canonical_sha>",
"tolerance_pp": 0.02,
"buckets": {
"tense.present_simple": {"accuracy": 0.81, "n": 240},
"tense.past_simple": {"accuracy": 0.77, "n": 240},
"tense.past_participle":{"accuracy": 0.72, "n": 240},
"tense.simple_future": {"accuracy": 0.78, "n": 240},
"tense.infinitive": {"accuracy": 0.88, "n": 240},
"person.1sg": {"accuracy": 0.82, "n": 400},
"person.2sg": {"accuracy": 0.79, "n": 400},
"person.3sg": {"accuracy": 0.71, "n": 400},
"verb.go": {"accuracy": 0.74, "n": 60},
"verb.be": {"accuracy": 0.69, "n": 60}
}
}
(Los números son ilustrativos; Borja mide el baseline real en la apertura de fase.)
La regla de comparación: para cada bucket, precisión del candidato ≥ precisión del baseline − tolerance_pp. Cualquier bucket que regrese más que la tolerancia falla el workflow.
La tolerancia es por-bucket, no agregada, porque la precisión agregada puede ocultar catástrofes a nivel de bucket. Un modelo que gana 2pp en presente simple mientras pierde 8pp en participio pasado tiene agregada plana pero es materialmente peor para aprendices de participio pasado.
Actualizar el baseline¶
eval_baseline.json se commitea. Actualizarlo es una PR con tres reglas:
- Mínimo un revisor además del autor.
- El diff incluye una justificación en la descripción de la PR (enlace al experimento que estableció los nuevos números).
- El SHA del baseline anterior se preserva como
previous_baseline_shaen el nuevo fichero. La historia de baselines es la historia de git.
Actualizar el baseline es normal — lo haces siempre que una mejora legítima de modelo establece un nuevo suelo. La disciplina es que actualizarlo no es silencioso.
El escape hatch no-CI (y por qué existe)¶
Para desarrollo local, Borja puede llamar a registry.register_pending(run_id) directamente e incluso a registry.promote(canonical_sha, semver="v0.0.0-dev") si LYNX_ENV != prod. Esto permite iterar sin ciclos de CI.
El escape hatch está acotado:
- Los semvers dev están namespaceados y nunca son resueltos por src/miniserve/ en producción.
- El index.jsonl del registry registra el LYNX_ENV en tiempo de registro. Las auditorías pueden filtrar where env == prod.
- La receta del Justfile just register-model <run-id> avisa ruidosamente que no es el camino de CI.
El punto: la fricción es el diseño. La promoción local es posible pero explícita.
Lo que el gate NO cubre¶
- Regresiones de latencia. El gate chequea corrección, no throughput. El load test de la Fase 34 captura el drift de latencia; podría añadirse luego un workflow
latency-gate.ymlseparado, pero no es parte del scope de la Fase 38. - Regresiones de recursos. Huella de memoria, uso de VRAM — igual que la latencia, gate separado.
- Smoke tests sobre el stack de servicio real. El gate baja el candidato y corre la evaluación en aislamiento. Un smoke test "¿miniserve arranca de verdad con este modelo?" pertenece al capstone de la Fase 39.
- Dependencias multi-modelo. Si el tutor llama a un retriever (Fase 29) que tiene sus propias versiones, el gate no las compone. Trabajo futuro.
El gate es el chequeo de regresión de deploy mínimo. Es suficiente porque el modo principal de fallo del tutor de gramática es la regresión de corrección, y la comparación por bucket está enfocada en la corrección.
Parte 2: Vocabulario de capacidad (sin experimentos)¶
La spec de la Fase 38 lista "autoscaling, GPU-sharing, spot vs on-demand" como conceptos. Ninguno tiene un experimento en la Fase 38 — requerirían un stack de servicio en cloud a escala no-trivial, lo cual el currículo alcanza solo en el capstone (Fase 39, escala mínima) y explícitamente no persigue a escala de producción (anti-goal §10).
Esta sección es el tour de vocabulario: la comprensión suficiente para que Borja pueda leer papers de industria, docs de proveedores y playbooks de on-call sin que cada término sea una caja negra.
Autoscaling: HPA sobre la métrica correcta¶
Un Horizontal Pod Autoscaler (HPA) es el mecanismo de Kubernetes que crece o encoge un deployment basado en una métrica. Para un servicio web CPU-bound, la métrica es la utilización de CPU. Para un servicio de inferencia, esa es la métrica equivocada.
Un servidor de inferencia moderno es GPU-bound (o, en CPU, ancho-de-banda-de-memoria-bound), no CPU-bound. La CPU está haciendo tokenización, packing de batches y HTTP. Incluso saturando la GPU podrías dejar la CPU al 30%. HPA-sobre-CPU sub-escala el deployment y produce head-of-line blocking.
Las métricas correctas para inferencia LLM:
- Tokens-por-segundo por réplica. Si esto se satura, necesitas más réplicas. Requiere que el stack de servicio (Fase 33) la exporte como métrica Prometheus.
- Profundidad de cola (requests pendientes). Barato, predictivo de la cola de latencia. La métrica correcta para un servidor con auto-batching.
- Latencia p99. Downstream — para cuando la latencia se degrada, ya estuviste sobrecargado.
Métrica de HPA recomendada para el tutor de gramática (si y cuando deployemos a escala): profundidad de cola, con tokens/seg como señal secundaria de escalado.
Anti-patrón: HPA sobre una métrica que oscila. HPA lee la métrica cada N segundos. Si la métrica oscila (lo cual la profundidad de cola hace naturalmente), HPA puede spawn y matar réplicas cada N segundos — flapping. Mitigaciones: ventana de estabilización del HPA (stabilizationWindowSeconds), bandas de histéresis. Nada de esto importa hasta que deployees. Mencionado aquí para que el término no sorprenda a Borja luego.
Compartir GPU: MIG y MPS¶
Las GPUs modernas de NVIDIA pueden subdividirse para que varios procesos (o varios contenedores) compartan la misma tarjeta física.
MIG (Multi-Instance GPU). Disponible en A100, H100, B100. La GPU se particiona a nivel de hardware en 1–7 instancias, cada una con su propia partición de memoria, slice de cómputo y cola PCIe. Aislamiento duro: un proceso en la instancia 1 no puede starvear a la instancia 2. Caso de uso: inferencia multi-tenant donde quieres garantizar que el tráfico ruidoso de un tenant no degrade la latencia de otro. Tradeoff: cada partición tiene menos SMs y menos HBM — una A100 dividida 7-way es 7× más pequeña que una A100 entera, bien para modelos pequeños, terrible para un modelo de 70B que necesitaba toda la HBM.
MPS (Multi-Process Service). Multiplexing software de una sola GPU entre múltiples procesos CUDA. Sin partición hardware. Todos los procesos comparten la memoria completa de la GPU y los SMs; los contextos CUDA se fusionan para que kernels de procesos diferentes puedan correr concurrentemente cuando hay cómputo de sobra. Caso de uso: muchos procesos pequeños de inferencia que individualmente sub-utilizan la GPU. Tradeoff: sin aislamiento. Un proceso que aloque toda la memoria o corra un kernel largo puede starvear a los otros.
Time-sharing. Cada proceso obtiene la GPU completa por un quantum, luego cambia de contexto. Terrible para inferencia sensible a latencia (el cambio de contexto es caro). Mayormente entrenamiento batch.
Recomendación práctica para el tutor de gramática: inferencia single-tenant, modelo pequeño. Un proceso, una GPU (cuando hay GPU). No compartir. MIG/MPS se vuelven relevantes solo cuando aparecen escenarios multi-tenant o multi-modelo, ninguno de los cuales está en el scope de la Fase 39.
Spot vs on-demand¶
Las instancias GPU de cloud vienen en dos sabores:
- On-demand: pagas precio completo; el proveedor se compromete con la disponibilidad.
- Spot (AWS) / Preemptible (GCP): pagas 50–90% menos; el proveedor puede reclamar la instancia con 30 segundos a 2 minutos de aviso.
Cuándo tiene sentido spot: - Entrenamiento. Larga duración, checkpointable, reanudable. Una reclamación spot te cuesta los últimos minutos de entrenamiento, no la corrida entera. - Inferencia batch / evaluación. Misma lógica. - Servicio asíncrono con una cola que tolera interrupciones de minutos.
Cuándo no: - Inferencia online síncrona. Un aprendiz envió una frase hace 200ms. La instancia es reclamada. La request falla. No lo hagas. - Workloads con estado caro de reconstruir (KV cache de una sesión larga, cachés de tokenizer en memoria).
Para los jobs de entrenamiento Fase 23+ del tutor de gramática: spot. Para servicio online Fase 33: on-demand, posiblemente con spot como réplicas secundarias baratas para tráfico shadow.
Instancias reservadas. Comprométete a uno o tres años de uso a cambio de un descuento del 30–60%. Solo vale la pena con una carga estable conocida.
FinOps en un párrafo (cross-link a theory/04)¶
Cubrimos las matemáticas en theory/04. La nota operacional aquí: toda decisión de coste es un tradeoff. Menor CpQU es deseable; latencia sub-segundo es deseable; alta disponibilidad es deseable. No puedes maximizar las tres. La decisión pertenece a un humano leyendo la columna CpQU del registry, el dashboard de latencia del stack de servicio, y el SLO. La columna vertebral MLOps saca los números a la superficie — no toma la decisión.
Añadidos al glosario (añadidos a GLOSSARY.md en la apertura de fase)¶
- HPA — Horizontal Pod Autoscaler. Mecanismo de Kubernetes para escalar réplicas sobre una métrica.
- MIG — Multi-Instance GPU. Particionado hardware de A100/H100.
- MPS — Multi-Process Service. Multiplexing software de una sola GPU CUDA.
- Spot — Instancia cloud descontada, reclamable.
- Preemptible — Nombre de GCP para spot.
- Stabilization window — Setting de HPA para prevenir oscilación.
- CpQU — Cost per Quality Unit; definido en
theory/04. - PSI — Population Stability Index; definido en
theory/03. - Eval baseline —
eval_baseline.jsoncommiteado; el suelo que impone el gate de deploy de CI. - Deploy gate —
.github/workflows/deploy-grammar-tutor.yml; el único camino legítimo de promoción.
A qué esta fase no se compromete (edición capacidad)¶
La Fase 38 no produce:
- Un stack de servicio cloud deployado.
- Una demo de autoscaling.
- Un benchmark MIG/MPS.
- Una hoja de cálculo spot-vs-on-demand de coste para workloads reales.
Esos son material opcional infra/k8s/ de la Fase 39 si y solo si Borja elige deployear el capstone a un cluster cloud (no requerido por la spec; ver LYNX_CORTEX.md §4 PHASE 39).
Recapitulación en un párrafo¶
CI es el único camino a producción. El gate de deploy (.github/workflows/deploy-grammar-tutor.yml) baja el candidato vía MLflow, jala el eval set vía DVC, corre la evaluación de la Fase 20, y compara la precisión de conjugación por bucket contra eval_baseline.json con una tolerancia de 2pp — cualquier bucket que regrese más falla el workflow. La promoción es el último paso; fallo significa que no hay cambio en el registry. El desarrollo local tiene un escape hatch (namespace de semver dev) pero el camino de producción es CI-only. El vocabulario de capacidad (HPA, MIG, MPS, spot) se cubre para leer literatura; sin experimentos de Fase 38 — Borja no tiene GPU en el laptop y el capstone es single-node.
Siguiente: los labs — lab/00-registry-roundtrip.md.