Mlops modelos grandes
MLOps para Modelos Grandes
Versionamento de modelos, canary deployment, monitoring de drift, rate limiting, quota management. Atualizado em abril de 2026.
Visão Geral
Deployar um LLM em produção é diferente de deployar uma API tradicional. Os desafios específicos incluem:
- *odelos pesados*— 7B parâmetros = 14GB em FP16, 3.5GB em INT4
- *stado complexo*— KV cache, contexto, session state
- *usto variável*— cada request custa GPU time
- *ualidade subjetiva*— não há "erro 500" para resposta ruim
- *rift silencioso*— modelo não "quebra", só fica pior gradualmente
Este documento cobre como operar LLMs em produção de forma confiável e econômica.
1. Versionamento de Modelos
O que versionar
| Artefato | O que é | Por quê |
|---|---|---|
| *eights* | Arquivo .safetensors ou .pt | O modelo em si |
| *onfig* | config.json, tokenizer_config.json | Hiperparâmetros, vocabulário |
| *okenizer* | Arquivo do tokenizer | Mesmo tokenizer = mesma tokenização |
| *heckpoint* | Estado do treino (optimizer, scheduler) | Para resume de treino |
| *dapter* | LoRA/QLoRA weights | Fine-tune específico |
| *rompt templates* | Templates de sistema/instrução | Mesmo prompt = mesmo comportamento |
| *val results* | Resultados de avaliação | Para comparação entre versões |
Estrutura de versionamento
models/
── kode-coder/
│ ├── v0.1.0/ ← Versão base (Qwen2.5-Coder-32B)
│ │ ├── model/ ← Weights
│ │ ├── config.json
│ │ ├── tokenizer/
│ │ ├── eval/ ← Resultados de avaliação
│ │ │ ├── swe-bench.json
│ │ │ ├── human-eval.json
│ │ │ └── privado.json
│ │ └── metadata.json ← Data, commit, notas
│ │
│ ├── v0.2.0/ ← Fine-tune SFT
│ │ ├── base: v0.1.0 ← Referência ao modelo base
│ │ ├── adapter/ ← LoRA weights
│ │ ├── config.json
│ │ ├── eval/
│ │ └── metadata.json
│ │
│ ├── v0.3.0/ ← Fine-tune SFT + DPO
│ │ ├── base: v0.2.0
│ │ ├── adapter/
│ │ ├── config.json
│ │ ├── eval/
│ │ ── metadata.json
│ │
│ ── current → v0.3.0 ← Symlink para versão ativaFerramentas de versionamento
| Ferramenta | Uso | Prós |
|---|---|---|
| *VC* | Versionamento de arquivos grandes | Git |
| *Lflow Models* | Registry de modelos | UI, experiment tracking |
| *eights & Biases Artifacts* | Versionamento + lineage | Integração com W&B |
| *uggingFace Hub* | Host de modelos | Comunidade, APIs |
| *CI Registry* | Container de modelos | Padrão cloud-native |
*ecomendação para o Kode:*W&B Artifacts + HuggingFace Hub (privado).
2. Deployment
Padrões de deployment
A. Single model (mais simples)
Usuário → Load Balancer → Modelo v1 (vLLM)*so:*Ambiente único, sem necessidade de A/B testing.
B. Blue-Green
┌── Modelo v1 (blue) ──→ Produção
Usuário → LB ───────┤
└── Modelo v2 (green) ──→ Staging*luxo:*
- Deploy v2 em green (não recebe tráfego)
- Testar v2 em staging
- Switch: LB aponta para green
- v1 fica em standby para rollback
*antagem:*Rollback imediato (segundos).
C. Canary
┌── Modelo v1 ─→ 90% do tráfego
Usuário → LB ───────
└── Modelo v2 ──→ 10% do tráfego*luxo:*
- v2 recebe 10% do tráfego
- Monitorar métricas por 24h
- Se OK → 25% → 50% → 100%
- Se problema → rollback para 0%
*antagem:*Exposição gradual, problema afeta poucos usuários.
D. Shadow
┌── Modelo v1 ──→ Responde ao usuário
Usuário → LB ──────┤
└── Modelo v2 ──→ Roda em shadow (sem resposta)*so:*Testar v2 sem afetar usuários. Comparar outputs de v1 vs v2.
Implementação com vLLM
# Deploy do modelo v1
vllm serve koder/kode-coder-v0.3.0 \
--port 8000 \
--tensor-parallel-size 2 \
--max-model-len 8192
# Deploy do modelo v2 (canary)
vllm serve koder/kode-coder-v0.4.0 \
--port 8001 \
--tensor-parallel-size 2 \
--max-model-len 8192Implementação com Kubernetes
apiVersion: apps/v1
kind: Deployment
metadata:
name: kode-model-v0-3-0
spec:
replicas: 3
selector:
matchLabels:
app: kode-model
version: v0.3.0
template:
metadata:
labels:
app: kode-model
version: v0.3.0
spec:
containers:
- name: vllm
image: vllm/vllm-openai:latest
args:
- --model
- koder/kode-coder-v0.3.0
- --tensor-parallel-size
- "2"
resources:
limits:
nvidia.com/gpu: 2
---
apiVersion: v1
kind: Service
metadata:
name: kode-model
spec:
selector:
app: kode-model
# version: v0.3.0 ← mudar para v0.4.0 para trocar versão
ports:
- port: 80
targetPort: 80003. Monitoring de Qualidade
Métricas de produção
| Métrica | Como medir | Threshold |
|---|---|---|
| *atência p50* | Tempo do primeiro token | < 500ms |
| *atência p95* | Tempo do primeiro token | < 2000ms |
| *atência p99* | Tempo do primeiro token | < 5000ms |
| *hroughput* | Tokens gerados / segundo | > 50 tok/s por GPU |
| *axa de erro* | Requests com erro / total | < 1% |
| *axa de timeout* | Requests timeout / total | < 0.5% |
| *PU utilization* | % GPU em uso | > 70% |
| *PU memory* | VRAM usada | < 90% |
| *V cache hit rate* | Reuso de cache | > 30% (com RadixAttention) |
Monitoring de qualidade de output
| Métrica | Como medir |
|---|---|
| *val score contínuo* | Rodar eval automático em amostra de produção |
| *eedback do usuário* | Thumbs up/down por resposta |
| *egeneração rate* | % de vezes que usuário pede "try again" |
| *runcation rate* | % de respostas cortadas (max tokens) |
| *mpty response rate* | % de respostas vazias ou só whitespace |
Pipeline de monitoring
Produção → Coleta de métricas (LangFuse, Prometheus) → Dashboard (Grafana)
↓
Alertas (Slack, email)
↓
Análise (semana)
↓
Decisão: manter, rollback, re-treinar4. Detecção de Drift
Tipos de drift
| Tipo | Descrição | Exemplo |
|---|---|---|
| *ata drift* | Distribuição dos inputs muda | Novas libs de código, novos frameworks |
| *oncept drift* | O que é "bom" muda | Boas práticas de código evoluem |
| *abel drift* | Preferência do usuário muda | Usuários preferem respostas mais curtas |
| *odel drift* | Modelo degrada com uso | KV cache corruption, quantização drift |
Detecção automática
def detect_drift(current_eval, baseline_eval, threshold=0.05):
"""Detectar drift comparando eval atual com baseline."""
metrics = ["code_pass_rate", "llm_judge_score", "latency_p95"]
drifts = []
for metric in metrics:
current = current_eval[metric]
baseline = baseline_eval[metric]
change = abs(current - baseline) / baseline
if change > threshold:
drifts.append({
"metric": metric,
"change": change,
"current": current,
"baseline": baseline,
})
return drifts
# Uso
drifts = detect_drift(eval_semanal, eval_baseline)
if drifts:
alert(f"Drift detectado: {drifts}")Ações por tipo de drift
| Drift detectado | Ação |
|---|---|
| Data drift | Adicionar novos dados ao dataset de treino |
| Concept drift | Re-treinar com dados recentes |
| Label drift | Atualizar reward model com preferências atuais |
| Model drift | Reload do checkpoint, verificar hardware |
5. Rate Limiting e Quota Management
Por que é necessário
LLMs são caros. Sem rate limiting:
- Um usuário pode consumir toda a GPU
- Loops infinitos de API calls esgotam quota
- Attacks (prompt injection) geram custo
Estratégias
| Estratégia | Como funciona | Uso |
|---|---|---|
| *ate limiting* | Máximo de requests por minuto | Todos os usuários |
| *oken quota* | Máximo de tokens por dia | Usuários free tier |
| *oncurrency limit* | Máximo de requests simultâneos | Prevenir overload |
| *riority queue* | Usuários premium têm prioridade | Multi-tenant |
| *ircuit breaker* | Parar se erro rate > threshold | Prevenir cascata |
Implementação
# Rate limiting com Redis
import redis
import time
class RateLimiter:
def __init__(self, redis_client, max_requests=100, window=60):
self.redis = redis_client
self.max_requests = max_requests
self.window = window
def is_allowed(self, user_id):
key = f"rate_limit:{user_id}"
current = self.redis.get(key)
if current is None:
self.redis.setex(key, self.window, 1)
return True
if int(current) >= self.max_requests:
return False
self.redis.incr(key)
return True
# Token quota
class TokenQuota:
def __init__(self, redis_client, daily_limit=100000):
self.redis = redis_client
self.daily_limit = daily_limit
def check_and_consume(self, user_id, tokens):
key = f"token_quota:{user_id}:{date.today()}"
used = int(self.redis.get(key) or 0)
if used + tokens > self.daily_limit:
return False
self.redis.incrby(key, tokens)
return TrueTiers de quota
| Tier | Tokens/dia | Requests/min | Prioridade |
|---|---|---|---|
| *ree* | 10K | 10 | Baixa |
| *ro* | 100K | 50 | Média |
| *nterprise* | 1M | 200 | Alta |
| *nternal* | Ilimitado | 500 | Máxima |
6. Rollback
Quando fazer rollback
| Sinal | Ação |
|---|---|
| Eval score cai > 5% | Rollback imediato |
| Taxa de erro > 5% | Rollback imediato |
| Latência p95 > 5s | Rollback em 1h |
| Feedback negativo > 20% | Investigar, possivelmente rollback |
| Drift detectado | Avaliar, possivelmente rollback |
Procedimento de rollback
# 1. Switch para versão anterior (Kubernetes)
kubectl set image deployment/kode-model \
vllm=vllm/vllm-openai:latest \
--record
# 2. Ou mudar symlink (file-based)
ln -sfn models/kode-coder/v0.3.0 models/kode-coder/current
# 3. Ou trocar weight no vLLM (hot reload)
curl -X POST http://localhost:8000/v1/reload \
-d '{"model": "koder/kode-coder-v0.3.0"}'
# 4. Verificar health
curl http://localhost:8000/health
# 5. Notificar equipe*empo alvo de rollback:*< 5 minutos.
7. Escalabilidade
Scaling horizontal
┌── Pod 1 (vLLM) ──→ GPU 1
Load Balancer ─────┼── Pod 2 (vLLM) ──→ GPU 2
┼── Pod 3 (vLLM) ──→ GPU 3
└── Pod N (vLLM) ──→ GPU N*uto-scaling:*
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: kode-model-hpa
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: kode-model
minReplicas: 2
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
- type: Pods
pods:
metric:
name: gpu_utilization
target:
type: AverageValue
averageValue: 80Scaling vertical (model parallelism)
Se o modelo não cabe em uma GPU:
| Técnica | Quando usar | |
|---|---|---|
| *ensor Parallelism* | Modelo > VRAM de 1 GPU | Dividir weights entre GPUs |
| *ipeline Parallelism* | Modelo muito grande | Dividir layers entre GPUs |
| *V cache offloading* | Contexto longo | Mover KV cache para CPU/disco |
8. Custo por Request
Cálculo
Custo por request = (GPU-hours por request) × (custo por GPU-hour)
GPU-hours por request = (tokens gerados × tempo por token) / 3600
Exemplo:
- 500 tokens gerados
- 50ms por token = 25s total
- 25s / 3600 = 0.007 GPU-hours
- Custo A100: $2.80/hora
- Custo por request: 0.007 × $2.80 = $0.02Otimização de custo
| Técnica | Economia |
|---|---|
| *atching* | 30–50% (processar múltiplos requests juntos) |
| *peculative decoding* | 30–50% (menos tokens gerados) |
| *uantização* | 0% custo, mas permite GPU menor (50% economia) |
| *V cache reuse* | 20–40% (RadixAttention para prompts repetidos) |
| *odel switching* | 50–80% (usar modelo menor para tasks simples) |
Para o Kode
Stack de MLOps recomendado
Versionamento: W&B Artifacts + HuggingFace Hub (privado)
Deployment: Kubernetes + vLLM
Monitoring: Prometheus + Grafana + LangFuse
Alertas: Slack + PagerDuty
Rate limiting: Redis + NGINX
CI/CD: GitHub Actions → build Docker → push → deployPipeline de deployment
1. Novo checkpoint → Eval automático (SWE-bench, eval privado)
2. Se eval OK → Push para HuggingFace Hub + W&B
3. Deploy canary (10% do tráfego)
4. Monitorar 24h
5. Se OK → Gradual ramp-up (25% → 50% → 100%)
6. Se problema → Rollback automáticoConfiguração inicial
| Item | Configuração |
|---|---|
| Modelos | v0.1.0 (base) + v0.2.0 (fine-tune) |
| GPUs | 2× RTX 4090 (desenvolvimento) |
| Serving | vLLM com tensor parallelism |
| Monitoring | LangFuse + Prometheus |
| Rate limit | 100 requests/min por usuário |
| Quota | 100K tokens/dia (free), 1M (pro) |
Referências
| Recurso | Descrição |
|---|---|
| vLLM docs | Serving de LLMs com alta throughput |
| LangFuse | Observabilidade de LLMs |
| W&B Artifacts | Versionamento de modelos |
| Prometheus | Monitoring de métricas |
| Kubernetes HPA | Auto-scaling horizontal |