Décimo post da série. No anterior, controlamos custos com Spot VMs, right-sizing e FinOps. Agora: como parar de ser um help desk humano pra GPU.

O canal do Slack que comeu sua agenda

Seis meses atrás, você provisionou uma VM GPU pro time de ML. Configurou drivers, montou storage, fechou o ticket. Pareceu mais um request normal de infraestrutura.

Hoje, você tem quatro times, três clusters AKS, dezenas de GPU node pools e uma coleção crescente de endpoints Azure OpenAI. Cada time quer seus recursos, suas quotas e seus SLAs. Seus DMs viraram help desk: “Dá pra dar mais GPUs?” “Por que meu training job está Pending?” “Quem tá usando todas as A100s?”

Esse é o ponto de inflexão. Você saiu de “suportar projetos de AI” pra “ser o gargalo de uma plataforma AI”. Trabalhar mais horas não resolve. O que resolve é montar os sistemas, políticas e automação que deixam os times se auto-servirem sem você perder o controle.

De projeto AI pra plataforma AI

Platform engineering não é novo. Você já faz isso há anos com web apps, bancos de dados e CI/CD. A ideia é infraestrutura reutilizável e self-service que os times consomem sem abrir ticket. Golden paths e workflows opinados, do código à produção.

AI infra segue o mesmo princípio. Em vez de provisionar GPU VMs ad hoc, você constrói templates. Em vez de criar namespaces manualmente, oferece portal self-service. Em vez de responder “como faço deploy de modelo?”, oferece um pipeline que faz.

Tradução infra ↔ AI: Platform engineering é a mesma disciplina que você já conhece, agora aplicada a GPU compute, model registries e inference endpoints em vez de web apps e SQL databases. As camadas de abstração mudam; o raciocínio não.

O que automatizar vs. o que controlar

CategoriaSelf-serviceRequer aprovação
Namespaces dev/test
GPU alocações pequenas (1-2 GPUs)
Endpoints de inference em produção
Training jobs grandes (8+ GPUs)
Provisionamento de novo cluster
Ambientes Jupyter notebook
Criação de endpoint Azure OpenAI
Volumes de storage pra datasets

Regra: Se um erro custa menos que centenas de dólares e pode ser revertido em minutos, faça self-service. Se envolve recursos caros, tráfego de produção ou impacto cross-team, coloque um gate.

Multi-tenancy: isolamento vs. eficiência

Nível de isolamentoEficiência de custoBoundary de segurançaOverhead operacionalMelhor pra
Namespace⭐⭐⭐⭐⭐BaixoBaixoTimes trusted compartilhando cluster
Node pool⭐⭐⭐⭐MédioMédioTimes precisando GPU types dedicados
Cluster⭐⭐⭐AltoAltoTimes com compliance requirements diferentes
Subscription⭐⭐Muito altoMuito altoWorkloads regulados, billing separado

A maioria das organizações cai num híbrido: um ou dois clusters compartilhados com namespaces por time e node pools GPU dedicados, mais clusters separados pra inference em produção e workloads regulados.

RBAC scoping por namespace

# team-data-science-role.yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  namespace: team-data-science
  name: gpu-workload-role
rules:
  - apiGroups: [""]
    resources: ["pods", "services", "configmaps", "persistentvolumeclaims"]
    verbs: ["get", "list", "create", "delete", "watch"]
  - apiGroups: ["batch"]
    resources: ["jobs"]
    verbs: ["get", "list", "create", "delete"]
  - apiGroups: ["apps"]
    resources: ["deployments"]
    verbs: ["get", "list", "create", "update", "delete"]

Associe essa role ao grupo Entra ID do time. Eles deployam workloads no namespace deles, mas não tocam em recursos de outros times nem em objetos cluster-level.

Resource quotas (sem isso, um time come todas as GPUs)

# team-data-science-quota.yaml
apiVersion: v1
kind: ResourceQuota
metadata:
  namespace: team-data-science
  name: gpu-quota
spec:
  hard:
    requests.cpu: "64"
    requests.memory: 256Gi
    requests.nvidia.com/gpu: "8"
    limits.cpu: "128"
    limits.memory: 512Gi
    pods: "50"

Isso limita o time a 8 GPUs, 64 CPU cores e 256 GiB de memória. Podem distribuir entre pods como quiserem, um job com 8 GPUs ou oito jobs com 1 GPU cada, mas não passam do total.

Cuidado: ResourceQuota atua na admissão, não no scheduler. Se você abaixar uma quota abaixo do uso atual, os pods existentes continuam rodando. Os próximos creates é que passam a falhar. Planeje mudanças de quota em maintenance windows.

Network isolation entre namespaces

# deny-cross-namespace.yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  namespace: team-data-science
  name: deny-other-namespaces
spec:
  podSelector: {}
  policyTypes:
    - Ingress
  ingress:
    - from:
        - podSelector: {}

Pods dentro do namespace conversam entre si; tráfego de outros namespaces é bloqueado. Adicione regras explícitas pra serviços compartilhados (model registries, monitoring).

GPU scheduling e filas

O problema de sempre

GPU é finito e caro. Um nó Standard_NC24ads_A100_v4 custa algo perto de $3,67 por hora em pay-as-you-go no East US. Com 20 nós e 4 times, scheduling first-come-first-served vira atrito constante. Training monopoliza GPU por horas. Inference fica sem espaço. Data scientist manda 10 jobs de uma vez e depois pergunta por que só 2 rodam.

Priority classes

# priority-classes.yaml
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
  name: production-inference
value: 1000000
globalDefault: false
preemptionPolicy: PreemptLowerPriority
description: "Inference de produção. Nunca sofre preemption de training."
---
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
  name: scheduled-training
value: 100000
globalDefault: false
preemptionPolicy: PreemptLowerPriority
description: "Training jobs com deadline."
---
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
  name: exploratory
value: 1000
globalDefault: true
preemptionPolicy: Never
description: "Notebooks e experimentos. Podem sofrer preemption."

Com essa hierarquia: inference em produção preempta training se GPUs estão escassas. Training preempta notebooks exploratórios. Mas exploratórios nunca preemptam nada; esperam na fila.

Dica: Use preemptionPolicy: Never pra workloads exploratórios. Previne stampede onde 50 pods de notebook tentam preemptar uns aos outros.

Kueue: fair scheduling pra batch AI

Kubernetes nativo não entende job queuing. Se você submete 100 jobs e tem capacidade pra 10, Kubernetes cria 100 pods pendentes. Kueue adiciona uma camada de queue que admite jobs baseado em capacidade disponível e fair-share.

# cluster-queue.yaml
apiVersion: kueue.x-k8s.io/v1beta1
kind: ClusterQueue
metadata:
  name: gpu-cluster-queue
spec:
  namespaceSelector: {}
  resourceGroups:
    - coveredResources: ["cpu", "memory", "nvidia.com/gpu"]
      flavors:
        - name: a100-spot
          resources:
            - name: "cpu"
              nominalQuota: 128
            - name: "memory"
              nominalQuota: 512Gi
            - name: "nvidia.com/gpu"
              nominalQuota: 16
        - name: a100-ondemand
          resources:
            - name: "cpu"
              nominalQuota: 64
            - name: "memory"
              nominalQuota: 256Gi
            - name: "nvidia.com/gpu"
              nominalQuota: 8
  preemption:
    withinClusterQueue: LowerPriority
---
# local-queue.yaml
apiVersion: kueue.x-k8s.io/v1beta1
kind: LocalQueue
metadata:
  namespace: team-nlp
  name: team-nlp-queue
spec:
  clusterQueue: gpu-cluster-queue

Times submetem jobs pra sua LocalQueue e o ClusterQueue enforça a capacidade global. Os jobs ficam na fila até ter espaço de verdade. Isso elimina o festival de “100 pods pendentes”.

Volcano: gang scheduling pra distributed training

Training distribuído precisa de múltiplas GPUs em múltiplos nós iniciando ao mesmo tempo. O scheduler padrão do Kubernetes pode agendar 3 dos 4 pods necessários e deixar todo mundo esperando pelo quarto.

Volcano garante: todos os pods de um job começam juntos, ou nenhum começa.

# distributed-training-volcano.yaml
apiVersion: batch.volcano.sh/v1alpha1
kind: Job
metadata:
  name: distributed-llm-training
  namespace: team-nlp
spec:
  minAvailable: 4
  schedulerName: volcano
  tasks:
    - replicas: 4
      name: worker
      template:
        spec:
          containers:
            - name: trainer
              image: myregistry.azurecr.io/llm-trainer:v1.0
              resources:
                limits:
                  nvidia.com/gpu: "4"
          restartPolicy: OnFailure

minAvailable: 4 diz ao Volcano pra não agendar nenhum worker até conseguir subir os quatro. Isso evita alocação parcial, que costuma virar GPU-hour queimada em training distribuído.

GPU em Kubernetes pede um valor só. Para recursos estendidos como nvidia.com/gpu, o valor normalmente vai em limits e o scheduler trata isso como reserva exclusiva. Se você também declarar em requests, os dois precisam ser iguais.

Quota e capacity management

O stack de quotas

CamadaMecanismoQuem gerencia
Azure subscriptionRegional vCPU quotasCloud admin (portal ou support request)
AKS clusterNode pool scaling limitsPlatform team
Kubernetes namespaceResourceQuota objectsPlatform team
KueueClusterQueue nominal quotasPlatform team
Team-levelLocalQueue admissionSelf-service dentro dos limites

Capacity reservation pra produção

# Reservar capacidade garantida pra inference
az capacity reservation group create \
  --resource-group rg-ai-platform \
  --name crg-inference-prod \
  --location eastus

az capacity reservation create \
  --resource-group rg-ai-platform \
  --capacity-reservation-group crg-inference-prod \
  --name cr-a100-inference \
  --sku Standard_NC24ads_A100_v4 \
  --capacity 4

Você paga pela capacidade reservada usando ou não, mas garante que os VMs existem quando precisar. Pra inference em produção servindo tráfego em tempo real, esse tradeoff quase sempre vale.

Monitoramento de quota

# Verificar uso de quota GPU na região
az vm list-usage \
  --location eastus \
  --query "[?contains(localName, 'NC') || contains(localName, 'ND')].{Name:localName, Used:currentValue, Limit:limit}" \
  --output table

No próximo post

Plataforma operando com self-service, quotas e scheduling inteligente. No próximo post, o foco é Azure OpenAI em produção: deployments, rate limiting, failover multi-região, content filtering e patterns de production readiness.