Veriff
LibraryblogCómo Veriff Comparte GPUs - Una guía técnica

Cómo Veriff Comparte GPUs - Una guía técnica

Siim Tiilen, un Ingeniero de Calidad en nuestro equipo de DevOps, explica prácticamente cómo Veriff comparte GPUs entre pods y cómo nos ha ayudado a reducir nuestro costo total de infraestructura.

Header image
Siim Tiilen
May 24, 2021
Publicación de Blog
Técnico
Share:

Debido a la creciente importancia de la IA en nuestra pila, estamos utilizando extensivamente unidades de procesamiento gráfico (GPUs) en Kubernetes para ejecutar diversas cargas de trabajo de aprendizaje automático (ML). En este blog, describiré cómo hemos estado compartiendo GPUs entre pods durante los últimos 2 años para reducir drásticamente nuestro costo de infraestructura.

Ejemplo de uso de GPU NVIDIA

Al usar GPU de NVIDIA, necesita utilizar el plugin de dispositivo de NVIDIA para Kubernetes que declara un nuevo recurso personalizado, nvidia.com/gpu, que puede usar para asignar GPUs a los pods de Kubernetes. El problema con este enfoque es que no puede dividirlos entre múltiples aplicaciones (PODs) y las GPU son un recurso muy costoso.

```yaml
apiVersion: v1
kind: Pod
metadata:
  name: gpu-example
spec:
  restartPolicy: OnFailure
  containers:
  - name: gpu-example
    image: eritikass/gpu-load-test
    imagePullPolicy: Always
    resources:
      limits:
        nvidia.com/gpu: 1
```

¿Qué sucede cuando despliegas una GPU usando aplicaciones como esta? Kubernetes está usando el recurso nvidia.com/gpu para desplegar este pod en un nodo donde hay una GPU disponible.

Si te conectas (ssh) al nodo donde este pod está corriendo, y usas el comando nvidia-smi allí, puedes obtener un resultado similar a este.

La información visible aquí es que nuestro nodo tiene 1 NVIDIA Tesla T4 GPU con 15109MiB de memoria y estamos usando 104MiB de eso con un proceso (nuestro pod desplegado).

Internamente hay una variable muy importante que se le da a cada pod y la aplicación la está utilizando para saber qué GPUs usar.

# connect to pod
kubectl exec -it pod/gpu-example -- bash

#and check
echo $NVIDIA_VISIBLE_DEVICES

En el ejemplo anterior, esta GPU - GPU-93955ff6-1bbe-3f6d-8d58-a2104edb62db - está siendo utilizada por la aplicación. Cuando tienes un nodo con múltiples GPUs, en realidad todos los pods pueden acceder a todas las GPUs, pero todas están usando esta variable para saber a qué GPU deben acceder.

Esta variable también tiene un valor "mágico": all. Usando esto puedes anular la asignación de GPU y dejar que tu aplicación sepa que puede usar cualquier (todas) las GPUs presentes en tu nodo.

Compartiendo GPU MVP

Sabemos que nuestra aplicación de ejemplo está usando 104MiB y la GPU tiene un total de 15109MiB, por lo que en teoría podemos ajustarla 145 veces en la misma GPU.

En el siguiente ejemplo necesita tener un clúster de AWS EKS con 1 instancia de GPU (g4dn.xlarge), y con algunas modificaciones debería ser posible usarlo en cualquier clúster de Kubernetes con nodos de GPU de Nvidia disponibles.

```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: gpu-example
spec:
  # En este ejemplo corremos 5 pods para fines de demostración.
  #
  # NB: los nodos aws g4 pueden correr un máximo de 29 pods debido a limitaciones de red,
  # https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/using-eni.html#AvailableIpPerENI
  replicas: 5
  selector:
    matchLabels:
      app: gpu-example
  template:
    metadata:
      labels:
        app: gpu-example
    spec:
      # Usemos afinidad para asegurarnos de que los pod(s) en este despliegue
      # puedan ser asignados solo a nodos que estén utilizando instancias g4dn.xlarge.
      # De esta manera podemos asegurar que hay GPU disponible.
      affinity:
        nodeAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
            nodeSelectorTerms:
            - matchExpressions:
              - key: node.kubernetes.io/instance-type
                operator: In
                values:
                - g4dn.xlarge
      containers:
      - name: gpu-example
        image: eritikass/gpu-load-test
        env:
          # esto le informará a la aplicación cuda que use cualquier GPU disponible
          - name: NVIDIA_VISIBLE_DEVICES
            value: all
```

Si verificamos después de eso, podemos ver que los 5 pods están asignados al mismo nodo.

Y cuando verificamos nvidia-smi en este nodo, podemos ver que hay 5 procesos GPU en ejecución de esos pods.

Usando recursos personalizados

Ahora que sabemos que múltiples pods que usan GPU pueden ejecutarse en el mismo nodo, necesitamos asegurarnos de que estén divididos entre nodos según la capacidad del nodo para manejar los requisitos del pod. Para esto podemos usar Recursos Personalizados para hacer saber a todos los nodos de GPU que tienen tanta memoria de GPU disponible para usar.

Usaremos DaemonSet para agregar un recurso de memoria de GPU personalizada (la nombramos veriff.com/gpu-memory) para añadir nodos con una GPU de NVIDIA adjunta. DaemonSet es un tipo especial de despliegue de Kubernetes que ejecutará 1 pod en algunos (o todos) los nodos del clúster, se usa con bastante frecuencia para desplegar cosas como recolectores de registros y herramientas de monitoreo. Nuestro DaemonSet tendrá nodeAffinity para asegurarse de que solo se ejecute en nodos g4dn.xlarge.

```yaml
---
#
# config maps that is holding the script that is run inside DaemonSet in all gpu nodes to set gpu memory
#
apiVersion: v1
kind: ConfigMap
metadata:
  name: add-gpu-memory
  namespace: kube-system
data:
  app.sh: |
    #!/bin/bash
    gpu_memory_value="15079Mi"
    timeout 240 kubectl proxy &
    sleep 3
    curl --header "Content-Type: application/json-patch+json" \
        --request PATCH \
        --max-time 10 --retry 10 --retry-delay 2 \
        --data "[{\"op\": \"add\", \"path\": \"/status/capacity/veriff.com~1gpu-memory\", \"value\": \"${gpu_memory_value}\"}]" \
        "http://127.0.0.1:8001/api/v1/nodes/${K8S_NODE_NAME}/status"
    echo " * ${gpu_memory_value} of gpu memory added to ${K8S_NODE_NAME}  (veriff.com/gpu-memory)"
    sleep infinity

---
#
# this DaemonSet will run in all g4dn.xlarge nodes and patching them to add them gpu memory custom resources
#
apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: add-gpu-memory
  namespace: kube-system
spec:
  selector:
    matchLabels:
      name: add-gpu-memory
  template:
    metadata:
      labels:
        name: add-gpu-memory
    spec:
      tolerations:
        - key: "special"
          operator: "Exists"
      affinity:
        nodeAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
            nodeSelectorTerms:
            - matchExpressions:
              - key: node.kubernetes.io/instance-type
                operator: In
                values:
                - g4dn.xlarge
      serviceAccountName: add-gpu-memory
      containers:
        - name: add-gpu-memory
          image: bitnami/kubectl
          resources:
            limits:
              cpu: 40m
              memory: 50M
            requests:
              cpu: 1m
              memory: 1M
          volumeMounts:
            - mountPath: /app.sh
              name: code
              readOnly: true
              subPath: app.sh
          command:
            - bash
            - /app.sh
          env:
            #
            # this variable is used in script to patch nodes to know what node he is running
            #
            - name: "K8S_NODE_NAME"
              valueFrom:
                fieldRef:
                  apiVersion: "v1"
                  fieldPath: "spec.nodeName"
      priorityClassName: system-node-critical
      volumes:
        - name: code
          configMap:
            name: add-gpu-memory
#
# rbac permissions used by daemonset to patch nodes
#
---
apiVersion: v1
kind: ServiceAccount
metadata:
  name: add-gpu-memory
  namespace: kube-system
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: add-gpu-memory
  namespace: kube-system
rules:
  - apiGroups:
    - "*"
    resources:
    - nodes
    verbs:
    - get
    - list
  - apiGroups:
    - "*"
    resources:
    - nodes/status
    verbs:
    - patch
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: add-gpu-memory
  namespace: kube-system
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: add-gpu-memory
subjects:
  - kind: ServiceAccount
    name: add-gpu-memory
    namespace: kube-system
```

Si verificas tu nodo usando “kubectl describe node/NAME” puedes ver que tiene el recurso veriff.com/gpu-memory disponible.

Ahora que sabemos que nuestro script para agregar memoria GPU funciona, escalemos nuestro clúster para que tengamos múltiples nodos GPU disponibles.

Para probarlo, modificaremos nuestro despliegue para usar este nuevo recurso.

```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: gpu-example
spec:
  replicas: 6
  selector:
    matchLabels:
      app: gpu-example
  template:
    metadata:
      labels:
        app: gpu-example
    spec:
      containers:
      - name: gpu-example
        image: eritikass/gpu-load-test
        env:
          # esto le informará a la aplicación cuda que use cualquier GPU disponible
          - name: NVIDIA_VISIBLE_DEVICES
            value: all
        resources:
          requests:
            veriff.com/gpu-memory: 104Mi
          limits:
            veriff.com/gpu-memory: 104Mi
```

Después de eso, es visible que los pods están distribuidos entre nodos.

Con recursos regulares como CPU y memoria, Kubernetes sabrá cuánto están usando realmente los pods (contenedores) y si alguien intenta usar más - Kubernetes lo restringirá. Sin embargo, con nuestro nuevo recurso personalizado, no hay ninguna salvaguarda real en su lugar que impida que algún pod “malévolo” no tome más memoria GPU de la declarada. Por lo tanto, debes tener mucho cuidado al establecer los recursos allí. Cuando los pods intentan usar más memoria GPU de la que hay disponible en un nodo, normalmente resulta en algunos accidentes muy feos.

En Veriff, resolvimos este problema con un monitoreo y alertas extensivas para el uso de nuestros nuevos Recursos Personalizados de GPU. También hemos desarrollado herramientas internamente para medir el uso de GPU en aplicaciones de ML bajo carga.

Todos los ejemplos de código de este post se pueden encontrar en https://github.com/Veriff/gpu-sharing-examples.