GPU scheduling: rodando workloads de ML no cluster

1. Fundamentos de GPU no Kubernetes

1.1. Por que GPU scheduling é crítico para ML

Workloads de Machine Learning (ML), especialmente treinamento de deep learning e inferência em tempo real, demandam processamento paralelo massivo. GPUs oferecem aceleração de 10x a 100x comparado a CPUs para operações matriciais e tensoriais. Sem um scheduler inteligente, recursos caros de GPU ficam ociosos ou mal distribuídos, comprometendo custo e desempenho.

1.2. Arquitetura do device plugin

O Kubernetes não gerencia GPUs nativamente. O device plugin é um agente que roda como DaemonSet em cada node, implementando a interface gRPC definida pelo kubelet. Ele expõe GPUs como recursos estendidos (extended resources) que podem ser solicitados via resources.requests e resources.limits nos pods.

O fluxo básico:
1. Kubelet descobre GPUs via device plugin
2. Device plugin reporta quantidade e健康状态
3. Scheduler aloca pods para nodes com GPUs disponíveis
4. Device plugin monta dispositivos e bibliotecas dentro do container

1.3. Diferenças entre fabricantes

  • NVIDIA: Ecossistema mais maduro com device plugin oficial, suporte a MIG (Multi-Instance GPU), time-slicing e CUDA
  • AMD: Device plugin com suporte a ROCm, menos integração com ferramentas de ML populares
  • Intel: Foco em inferência com OpenVINO, device plugin experimental para GPUs integradas

Para ambientes de produção com ML, NVIDIA domina o mercado devido à compatibilidade com PyTorch, TensorFlow e frameworks de inferência.

2. Instalação e Configuração do NVIDIA Device Plugin

2.1. Pré-requisitos

Antes de instalar o device plugin, garanta que cada node GPU tenha:

# Drivers NVIDIA (versão 450.80.02 ou superior)
nvidia-smi

# nvidia-docker2 ou nvidia-container-toolkit
sudo apt-get install -y nvidia-container-toolkit

# Container runtime configurado (containerd ou docker)
sudo nvidia-ctk runtime configure --runtime=containerd
sudo systemctl restart containerd

2.2. Deploy via Helm

helm repo add nvdp https://nvidia.github.io/k8s-device-plugin
helm repo update

helm upgrade -i nvidia-device-plugin \
  nvdp/nvidia-device-plugin \
  --namespace nvidia-device-plugin \
  --create-namespace \
  --set runtimeClassName=nvidia

Ou via manifesto YAML direto:

kubectl apply -f https://raw.githubusercontent.com/NVIDIA/k8s-device-plugin/v0.14.0/nvidia-device-plugin.yml

2.3. Verificação da disponibilidade

# Verificar nodes com GPU
kubectl describe nodes | grep -A5 "Capacity"

# Deve mostrar algo como:
# nvidia.com/gpu:  4

# Executar nvidia-smi dentro de um pod de teste
kubectl run gpu-test --rm -t \
  --image=nvidia/cuda:12.2.0-base-ubuntu22.04 \
  --restart=Never \
  -- nvidia-smi

3. Estratégias de Scheduling para Workloads de ML

3.1. NodeSelector e taints/tolerations

Para garantir que workloads de ML sejam alocados apenas em nodes GPU:

# Adicionar taint nos nodes GPU
kubectl taint nodes gpu-node-1 nvidia.com/gpu=present:NoSchedule

# Pod com toleration
apiVersion: v1
kind: Pod
metadata:
  name: ml-training
spec:
  tolerations:
  - key: "nvidia.com/gpu"
    operator: "Equal"
    value: "present"
    effect: "NoSchedule"
  containers:
  - name: trainer
    image: tensorflow/tensorflow:2.13.0-gpu
    resources:
      limits:
        nvidia.com/gpu: 1

3.2. Topology-aware scheduling com MIG

Para GPUs NVIDIA A100 ou H100, MIG permite particionar fisicamente a GPU:

# Configurar MIG no node (exemplo: 3 instâncias 1g.5gb)
nvidia-smi mig -cgi 19,19,19 -C

# No device plugin, habilitar MIG
helm upgrade nvidia-device-plugin nvdp/nvidia-device-plugin \
  --set migStrategy=mixed

3.3. Bin packing vs. spread

  • Bin packing: Agrupa pods no menor número de nodes GPU, maximizando utilização
  • Spread: Distribui pods entre nodes para isolamento e resiliência

Use podAntiAffinity para spread:

spec:
  affinity:
    podAntiAffinity:
      preferredDuringSchedulingIgnoredDuringExecution:
      - weight: 100
        podAffinityTerm:
          labelSelector:
            matchLabels:
              app: ml-inference
          topologyKey: "kubernetes.io/hostname"

4. Gerenciamento de Recursos e Limites

4.1. Definindo requests e limits para GPU

GPUs são recursos contáveis (não compressíveis). Cada pod solicita um número inteiro de GPUs:

resources:
  limits:
    nvidia.com/gpu: 1
  requests:
    nvidia.com/gpu: 1

Para compartilhamento, use time-slicing:

# ConfigMap para time-slicing
apiVersion: v1
kind: ConfigMap
metadata:
  name: time-slicing-config
  namespace: nvidia-device-plugin
data:
  config.yaml: |
    version: v1
    sharing:
      timeSlicing:
        resources:
        - name: nvidia.com/gpu
          replicas: 4

4.2. MPS (Multi-Process Service)

MPS permite que múltiplos processos CUDA compartilhem uma GPU com melhor desempenho que time-slicing:

# Ativar MPS no node
nvidia-cuda-mps-control -d

# Pod com variável de ambiente
env:
- name: CUDA_MPS_PIPE_DIRECTORY
  value: "/tmp/nvidia-mps"

4.3. Monitoramento com DCGM e Prometheus

# Instalar DCGM exporter
helm repo add gpu-helm-charts \
  https://nvidia.github.io/gpu-monitoring-tools/helm-charts

helm install dcgm-exporter \
  gpu-helm-charts/dcgm-exporter \
  --namespace monitoring

Métricas exportadas incluem:
- DCGM_FI_DEV_GPU_UTIL — Utilização da GPU
- DCGM_FI_DEV_MEM_COPY_UTIL — Utilização de memória
- DCGM_FI_DEV_POWER_USAGE — Consumo de energia

5. Exemplos Práticos de Workloads de ML

5.1. Treinamento distribuído com PyTorch e Kubeflow

# Instalar Kubeflow Training Operator
kubectl apply -k "github.com/kubeflow/training-operator/manifests/overlays/standalone"

# PyTorchJob para treinamento distribuído
apiVersion: "kubeflow.org/v1"
kind: PyTorchJob
metadata:
  name: pytorch-distributed
spec:
  pytorchReplicaSpecs:
    Master:
      replicas: 1
      template:
        spec:
          containers:
          - name: pytorch
            image: pytorch/pytorch:2.0.1-cuda11.7-cudnn8-runtime
            args: ["--backend", "nccl"]
            resources:
              limits:
                nvidia.com/gpu: 1
    Worker:
      replicas: 3
      template:
        spec:
          containers:
          - name: pytorch
            image: pytorch/pytorch:2.0.1-cuda11.7-cudnn8-runtime
            resources:
              limits:
                nvidia.com/gpu: 1

5.2. Inferência com NVIDIA Triton Inference Server

apiVersion: apps/v1
kind: Deployment
metadata:
  name: triton-server
spec:
  replicas: 2
  selector:
    matchLabels:
      app: triton
  template:
    metadata:
      labels:
        app: triton
    spec:
      containers:
      - name: triton
        image: nvcr.io/nvidia/tritonserver:23.08-py3
        args:
        - tritonserver
        - --model-repository=/models
        - --backend-config=tensorflow,version=2
        ports:
        - containerPort: 8000
        - containerPort: 8001
        volumeMounts:
        - name: models
          mountPath: /models
        resources:
          limits:
            nvidia.com/gpu: 1
      volumes:
      - name: models
        persistentVolumeClaim:
          claimName: model-storage

5.3. Jobs batch com GPU

kubectl create job --image=tensorflow/tensorflow:2.13.0-gpu \
  --limits="nvidia.com/gpu=1" \
  fine-tuning-job -- python train.py --epochs=10

6. Otimização e Troubleshooting

6.1. Diagnóstico de falhas

# Verificar logs do device plugin
kubectl logs -n nvidia-device-plugin \
  -l app.kubernetes.io/name=nvidia-device-plugin

# Verificar eventos de scheduling
kubectl get events --sort-by='.lastTimestamp' | grep -i gpu

# Testar alocação manual
kubectl run gpu-test --rm -t \
  --image=nvidia/cuda:12.2.0-base-ubuntu22.04 \
  --limits="nvidia.com/gpu=1" \
  -- nvidia-smi

6.2. Autoscaling com KEDA e DCGM

apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
  name: gpu-inference-scaler
spec:
  scaleTargetRef:
    name: triton-server
  triggers:
  - type: prometheus
    metadata:
      serverAddress: http://prometheus.monitoring:9090
      query: |
        avg(DCGM_FI_DEV_GPU_UTIL{job="dcgm-exporter"})
      threshold: "70"

6.3. Segurança e isolamento com MIG

# Criar perfil MIG para isolamento total
nvidia-smi mig -cgi 1g.10gb -C

# Pod com device request específico
resources:
  limits:
    nvidia.com/gpu: 1
    nvidia.com/mig-profile: "1g.10gb"

7. Considerações para Ambientes Multi-Tenant

7.1. ResourceQuota por namespace

apiVersion: v1
kind: ResourceQuota
metadata:
  name: gpu-quota
  namespace: team-ml
spec:
  hard:
    requests.nvidia.com/gpu: "4"
    limits.nvidia.com/gpu: "4"

7.2. Priorização com PriorityClass

apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
  name: high-priority-ml
value: 1000000
preemptionPolicy: PreemptLowerPriority

# Pod de treinamento crítico
spec:
  priorityClassName: high-priority-ml

7.3. Rastreamento de custos

Use labels e ferramentas como Kubecost ou OpenCost:

kubectl label ns team-ml cost-center=research
kubectl label ns team-ml project=llm-training

O OpenCost pode então reportar gastos de GPU por namespace, permitindo chargeback para equipes.


Referências