ARTICULO

Introducción

Si ejecutás workloads con GPU en Kubernetes —vLLM, Triton, jobs de entrenamiento o stacks de inferencia agentica—, probablemente hayas notado que el autoscaling por defecto solo considera CPU y memoria. Las GPUs, que hacen el trabajo pesado, quedan invisibles para el Horizontal Pod Autoscaler (HPA). Esto genera:

  • Desperdicio de capacidad: GPUs subutilizadas sin escalar.
  • Latencia alta: Los pods se quedan pendientes de recursos que no se liberan.
  • Mayor consumo energético: Cada ciclo de GPU no aprovechado suma a las emisiones Scope 3.

Para resolverlo, necesitás un scaler que entienda métricas reales de GPU: utilización de cómputo, memoria, temperatura y consumo de energía. KEDA no puede hacerlo nativamente porque usa CGO_ENABLED=0, y las librerías de NVIDIA (como NVML) requieren CGO. La solución es un ExternalScaler personalizado que corra en cada nodo GPU y exponga métricas via gRPC, siguiendo el mismo patrón que los device plugins o el metrics-server.

En esta guía, implementarás un ExternalScaler que:

  • Lee métricas de GPU usando go-nvml.
  • Las sirve via gRPC usando la interfaz de KEDA.
  • Permite escalar tu workload (ej: vLLM) en base a memoria usada, temperatura o utilización.

Qué es y para qué sirve

¿Por qué un ExternalScaler para GPUs?

KEDA soporta escalamiento basado en métricas externas (Prometheus, Kafka, etc.), pero no tiene un scaler nativo para GPUs porque:

  1. Restricción técnica: KEDA se compila con CGO_ENABLED=0, pero NVML requiere CGO para interactuar con el driver de NVIDIA.
  2. Acceso local: NVML solo puede leer métricas del GPU en el mismo nodo. No podés consultar el GPU 0 del nodo-A desde un pod en el nodo-B.

Arquitectura propuesta

graph LR
    A[KEDA Operator] -->|HPA| B[ExternalScaler<br>(gRPC)]
    B -->|DaemonSet| C[Nodo GPU 1<br>GPU A, B]
    B -->|DaemonSet| D[Nodo GPU 2<br>GPU C]
    C -->|go-nvml| E[Driver NVIDIA]
    D -->|go-nvml| E
  • DaemonSet personalizado: Corre en cada nodo GPU y expone métricas via gRPC.
  • KEDA Operator: Consume las métricas via el ExternalScaler y ajusta la réplica del HPA.
  • go-nvml: Librería en Go que interactúa con NVML sin CGO (wrapper seguro).

Métricas disponibles

El scaler expone estas métricas por GPU (o agregadas por nodo):

MétricaTipoDescripción
BLOCK17PrometheusPorcentaje de uso de cómputo (0-100)
BLOCK18PrometheusMemoria usada en MiB
BLOCK19PrometheusMemoria total en MiB
BLOCK20PrometheusTemperatura en Celsius
BLOCK21PrometheusConsumo de energía en watts
## Prerequisitos

Software y versiones

ComponenteVersión mínimaNotas
Kubernetes1.25+Cluster con soporte para GPUs
KEDA2.12.0+Instalado via Helm
Helm3.12+Para instalar el scaler
NVIDIA GPU Operator1.13+Opcional, pero recomendado para drivers
NVIDIA Container Toolkitv1.14+Para montar los dispositivos GPU
go-nvmlv0.10.0+Librería usada por el scaler
### Permisos y accesos
  • Cluster: Acceso kubectl con permisos para crear DaemonSets, Services, ClusterRoles y CustomResourceDefinitions (CRDs).
  • Nodos GPU: Los nodos deben tener:
– Etiqueta nvidia.com/gpu.present=true.

– Taint nvidia.com/gpu:NoSchedule (opcional, pero común en clusters con GPUs exclusivas).

– Drivers NVIDIA instalados (versión 535+ recomendada).

  • Red: Los pods del DaemonSet deben poder comunicarse con el API de Kubernetes y el HPA de tu workload.

Repositorio de referencia

Usaremos el código abierto keda-gpu-scaler como base. Clonalo para seguir los pasos:

git clone https://github.com/epower/keda-gpu-scaler.git
cd keda-gpu-scaler

Guía paso a paso

1. Instalar KEDA y el ExternalScaler

Si aún no tenés KEDA instalado, hazlo via Helm:

helm repo add kedacore https://kedacore.github.io/charts
helm repo update
kubectl create namespace keda
helm install keda kedacore/keda --namespace keda --version 2.12.0

2. Configurar el ExternalScaler

El scaler se despliega como un DaemonSet. Revisá los valores por defecto en values.yaml:

# values.yaml
image:
  repository: ghcr.io/epower/keda-gpu-scaler
  tag: v0.10.0
  pullPolicy: IfNotPresent

# Configuración de NVML
nvmlHostMounts:
  enabled: false  # Cambia a true si no tenés NVIDIA Container Toolkit

# Métricas a exponer
metrics:
  - name: gpu_memory_used
    type: Utilization
    targetValue: 80  # Escalar si se usa >80% de memoria
    aggregation: max  # Para nodos multi-GPU: max, min, avg, sum
    gpuIndex: -1     # -1 = todos los GPUs; 0,1,... = índice específico

  - name: gpu_utilization
    type: Utilization
    targetValue: 70
    aggregation: avg

> Nota: Si tu cluster no usa NVIDIA Container Toolkit, montá los dispositivos GPU directamente con nvmlHostMounts.enabled=true. Esto requiere permisos privilegiados (privileged: true en el pod).

3. Desplegar el scaler

Aplica la configuración:

helm install keda-gpu-scaler ./charts/keda-gpu-scaler \
  --namespace keda \
  --set nvmlHostMounts.enabled=true \
  --set metrics[0].targetValue=85

Verificá que el DaemonSet esté corriendo:

kubectl get pods -n keda -l app.kubernetes.io/name=keda-gpu-scaler
# Ejemplo de salida:
# NAME                              READY   STATUS    RESTARTS   AGE
# keda-gpu-scaler-abc12          1/1     Running   0          10s
# keda-gpu-scaler-xyz34          1/1     Running   0          10s

4. Configurar KEDA para usar el ExternalScaler

Crea un ScaledObject que use el scaler. Ejemplo para un deployment de vLLM:

# scaledobject-vllm.yaml
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
  name: vllm-gpu-scaler
  namespace: ai
spec:
  scaleTargetRef:
    name: vllm-deployment  # Nombre de tu deployment
  triggers:
    - type: external
      metadata:
        scalerAddress: "keda-gpu-scaler.keda.svc.cluster.local:9090"  # Nombre del Service del scaler
        metricName: "gpu_memory_used"
        targetValue: "80"  # Escalar si memoria >80%
        activationTargetValue: "50"  # Desescalar si memoria <50%

Aplica la configuración:

kubectl apply -f scaledobject-vllm.yaml

5. Verificar el escalamiento

Observá los eventos del ScaledObject:

kubectl describe scaledobject vllm-gpu-scaler -n ai

Buscá eventos como:

Events:
  Type    Reason             Age   From                   Message
  ----    ------             ----  ----                   -------
  Normal  ScalingActive      5s    keda.scaler.external    Scaling is active
  Normal  ScaledObjectReady  5s    keda.scaler.external    ScaledObject is ready

6. Probar el scaler en modo mock (opcional)

El repositorio incluye un modo mock para probar sin GPUs reales. Actívalo con:

helm upgrade keda-gpu-scaler ./charts/keda-gpu-scaler \
  --namespace keda \
  --set mockMode.enabled=true

Los tests E2E validan el flujo completo:

make test

> Resultado esperado: 11 tests pasan, incluyendo perfiles, agregación de métricas y manejo de errores.

Consideraciones y buenas prácticas

Limitaciones conocidas

  • Multi-GPU: El scaler soporta nodos con múltiples GPUs, pero la agregación (max, min, avg, sum) se aplica por métrica. Si necesitas escalar un GPU específico, usá gpuIndex.
  • Drivers NVIDIA: El scaler requiere acceso al driver via /dev/nvidia* o NVML. Si usás Kubernetes en un entorno sin drivers instalados (ej: AKS con GPUs), podés usar NVIDIA GPU Operator para montarlos automáticamente.
  • Latencia de NVML: NVML tiene un overhead mínimo (~1ms por llamada). En clusters con cientos de GPUs, considerá cachear métricas localmente (ej: cada 5 segundos).

Alternativas

  • Prometheus + GPU Exporter: Si ya tenés Prometheus, podés usar nvidia_gpu_prometheus_exporter y configurar KEDA para consumir esas métricas. El ExternalScaler es más eficiente para escalamiento en tiempo real.
  • KEDA 3.0+: En futuras versiones, podrían soportar CGO o un scaler nativo. Monitorea los releases de KEDA.

Seguridad

  • Permisos: El DaemonSet necesita acceso a /dev/nvidia* y al socket de NVML. Usá un SecurityContext restringido:
  securityContext:
    privileged: false
    capabilities:
      add: ["SYS_ADMIN"]
    readOnlyRootFilesystem: true
  
  • Red: Exponé el puerto gRPC solo dentro del cluster (ClusterIP en el Service). No uses NodePort o LoadBalancer.

Monitoreo

Exponé métricas adicionales para debugging:

metrics:
  - name: gpu_memory_used
    type: Utilization
    targetValue: 80
    aggregation: max
  - name: gpu_memory_free
    type: Utilization
    targetValue: 20
    aggregation: max

Luego, consultá las métricas via:

kubectl port-forward svc/keda-gpu-scaler -n keda 9090:9090
curl http://localhost:9090/metrics

Conclusión

Implementar un ExternalScaler para GPUs en Kubernetes te permite:

  1. Escalar en base a métricas reales (memoria, temperatura, utilización), no solo CPU/memoria.
  2. Reducir costos y emisiones al evitar desperdiciar ciclos de GPU.
  3. Extender KEDA sin modificar su core, usando su interfaz estándar de ExternalScaler.

El código del keda-gpu-scaler es un punto de partida listo para producción. Si tenés workloads como vLLM, Triton o jobs de entrenamiento, integrá este scaler y ajustá los valores de targetValue según tu caso de uso.

Para clusters con GPUs compartidas, considerá combinar este scaler con cuotas (LimitRange) y prioridades de pods (PriorityClass) para evitar starving.

Fuentes

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *