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:
- Modelos pesados — 7B parâmetros = 14GB em FP16, 3.5GB em INT4
- Estado complexo — KV cache, contexto, session state
- Custo variável — cada request custa GPU time
- Qualidade subjetiva — não há "erro 500" para resposta ruim
- Drift 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ê |
|---|---|---|
| Weights | Arquivo .safetensors ou .pt | O modelo em si |
| Config | config.json, tokenizer_config.json | Hiperparâmetros, vocabulário |
| Tokenizer | Arquivo do tokenizer | Mesmo tokenizer = mesma tokenização |
| Checkpoint | Estado do treino (optimizer, scheduler) | Para resume de treino |
| Adapter | LoRA/QLoRA weights | Fine-tune específico |
| Prompt templates | Templates de sistema/instrução | Mesmo prompt = mesmo comportamento |
| Eval 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 |
|---|---|---|
| DVC | Versionamento de arquivos grandes | Git-like, open-source |
| MLflow Models | Registry de modelos | UI, experiment tracking |
| Weights & Biases Artifacts | Versionamento + lineage | Integração com W&B |
| HuggingFace Hub | Host de modelos | Comunidade, APIs |
| OCI Registry | Container de modelos | Padrão cloud-native |
Recomendaçã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)Uso: Ambiente único, sem necessidade de A/B testing.
B. Blue-Green
┌── Modelo v1 (blue) ──→ Produção
Usuário → LB ───────┤
└── Modelo v2 (green) ──→ StagingFluxo:
- Deploy v2 em green (não recebe tráfego)
- Testar v2 em staging
- Switch: LB aponta para green
- v1 fica em standby para rollback
Vantagem: Rollback imediato (segundos).
C. Canary
┌── Modelo v1 ─→ 90% do tráfego
Usuário → LB ───────
└── Modelo v2 ──→ 10% do tráfegoFluxo:
- v2 recebe 10% do tráfego
- Monitorar métricas por 24h
- Se OK → 25% → 50% → 100%
- Se problema → rollback para 0%
Vantagem: 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)Uso: 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 |
|---|---|---|
| Latência p50 | Tempo do primeiro token | < 500ms |
| Latência p95 | Tempo do primeiro token | < 2000ms |
| Latência p99 | Tempo do primeiro token | < 5000ms |
| Throughput | Tokens gerados / segundo | > 50 tok/s por GPU |
| Taxa de erro | Requests com erro / total | < 1% |
| Taxa de timeout | Requests timeout / total | < 0.5% |
| GPU utilization | % GPU em uso | > 70% |
| GPU memory | VRAM usada | < 90% |
| KV cache hit rate | Reuso de cache | > 30% (com RadixAttention) |
Monitoring de qualidade de output
| Métrica | Como medir |
|---|---|
| Eval score contínuo | Rodar eval automático em amostra de produção |
| Feedback do usuário | Thumbs up/down por resposta |
| Regeneração rate | % de vezes que usuário pede "try again" |
| Truncation rate | % de respostas cortadas (max tokens) |
| Empty 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 |
|---|---|---|
| Data drift | Distribuição dos inputs muda | Novas libs de código, novos frameworks |
| Concept drift | O que é "bom" muda | Boas práticas de código evoluem |
| Label drift | Preferência do usuário muda | Usuários preferem respostas mais curtas |
| Model 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 |
|---|---|---|
| Rate limiting | Máximo de requests por minuto | Todos os usuários |
| Token quota | Máximo de tokens por dia | Usuários free tier |
| Concurrency limit | Máximo de requests simultâneos | Prevenir overload |
| Priority queue | Usuários premium têm prioridade | Multi-tenant |
| Circuit 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 |
|---|---|---|---|
| Free | 10K | 10 | Baixa |
| Pro | 100K | 50 | Média |
| Enterprise | 1M | 200 | Alta |
| Internal | 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 equipeTempo 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 NAuto-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 | |
|---|---|---|
| Tensor Parallelism | Modelo > VRAM de 1 GPU | Dividir weights entre GPUs |
| Pipeline Parallelism | Modelo muito grande | Dividir layers entre GPUs |
| KV 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 |
|---|---|
| Batching | 30–50% (processar múltiplos requests juntos) |
| Speculative decoding | 30–50% (menos tokens gerados) |
| Quantização | 0% custo, mas permite GPU menor (50% economia) |
| KV cache reuse | 20–40% (RadixAttention para prompts repetidos) |
| Model 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 |