Привет! Я Алиса, DevOps-инженер в KTS.

В этой статье я расскажу о том, как мы настроили автоматическое обновление драйверов NVIDIA для работы с Jupyter и ML-стеком в управляемом кластере.

Проблема: когда контейнеры и ML-библиотеки обновляются чаще, чем системные образы GPU-нод, версия драйвера быстро перестает соответствовать версии CUDA в контейнере. В итоге при вызове  nvidia-smi возвращает ошибку Driver/library version mismatch, а CUDA просто не видит драйвер на хосте.

Нам нужно было обновить Jupyter с ML-стеком, зависящим от CUDA. Как следствие, встал вопрос обновления драйверов NVIDIA на GPU-нодах. Можно было выполнять его руками на каждой ноде, но такой способ нам не подходил, и мы выбрали автоматизацию, которой и посвящена моя статья. Ниже я разберу и ручное обновление, и варианты автоматизации, а также объясню, как мы решали проблему конфликта GPU Operator с предустановленными драйверами.

Оглавление

Исходная система

Для начала взглянем на конфигурацию.

  • Кластер: Yandex Managed Service for Kubernetes (управляемый через Terraform).

  • GPU-ноды: выделенная нод-группа с установленными GPU Nvidia Tesla V100. Для выбора определенной версии GPU необходимо указать тип платформы. Все возможные варианты можно посмотреть в официальной документации, я же буду разбирать обновление на примере Intel Cascade Lake with NVIDIA Tesla V100 (gpu-standard-v2).

Чтобы гарантировать, что на этих нодах запускаются только определенные рабочие нагрузки, требующие GPU, был применен специальный taint: node_taints = ["my-mode-jupyter=true:NoSchedule"]. Пример полного tf-конфига ноды:

Конфиг
   ```
    resource "yandex_kubernetes_node_group" "my-mode-test-v100" {
      cluster_id  = yandex_kubernetes_cluster.<cluster-name>.id
      name        = "my-mode-test-v100"
      version     = "1.28"
    
      node_labels = { "node.kubernetes.io/role" = "my-mode-jupyter", "inst_type" = "my-mode-large-v100"}
      node_taints = ["my-mode-jupyter=true:NoSchedule"]
    
      instance_template {
        name = "my-mode-test-v100-8-48-gpu-{instance.short_id}"
        platform_id = "gpu-standard-v2"
    
        resources {
          memory        = 48
          cores         = 8
          core_fraction = 100
          gpus          = 1
        }
    
        boot_disk {
          type = "network-ssd"
          size = 96
        }
    
        scheduling_policy {
          preemptible = true
        }
    
        metadata = {
          "serial-port-enable" = "1"
          ssh-keys = "<admin>:${<ssh-key>},ubuntu:$${<ssh-key>}"
        }
    
        gpu_settings {
          gpu_environment = "runc_drivers_cuda" 
        }
    
        network_interface {
          subnet_ids = [yandex_vpc_subnet.<subnet-name>.id]
        }
      }
      scale_policy {
        auto_scale {
          min     = 1
          max     = 3
          initial = 1
        }
      }
    
      allocation_policy {
        location {
          zone = yandex_vpc_subnet.<subnet-name>.zone
        }
      }
    
      maintenance_policy {
        auto_upgrade = false
        auto_repair  = true
      }
    
    }
    ```
  • На GPU-нодах развернут JupyterHub, который по запросу пользователей поднимает определенный под с Jupyter Notebook. Пользователь может выбрать размеры и характеристики подa: с GPU/без GPU или различные комбинации CPU-RAM.

  • На базе образа контейнера поднимается под на нодах. Образ на основе официального jupyter/scipy-notebook, дополненный необходимыми ML-библиотеками (PyTorch, CatBoost, etc.), рассчитанными под CUDA 12.

  • При создании GPU-нод в Yandex Cloud был выбран вариант с предустановленными драйверами NVIDIA (параметр Node Group gpu_environment = "runc_drivers_cuda").

Если нужно точечно поправить одну конкретную ноду, а не пересоздавать нод-группу и полностью переустанавливать ноды, то можно сделать это вручную. Это всегда подразумевает даунтайм именно этой ноды (cordon/drain + перезагрузка), но на остальные ноды и всю группу это не повлияет.


Ручное обновление драйвера NVIDIA

Yandex Cloud при gpu_environment = "runc_drivers_cuda" устанавливает драйвер внутри VM с помощью .run-файла NVIDIA. Версию драйвера выбрать нельзя, обновления происходят только с новыми образами узлов. Если вы хотите использовать CUDA 12.6, придется обновить драйвер вручную или ждать, пока Yandex Cloud выкатит обновление.

Вывод ноды из обслуживания

kubectl drain <node-name>

После дрейна новые поды перестают приземляться на ноду, и кубер штатно мигрирует существующие.

Привилегированный под с доступом к host root

Манифест ниже разворачивается в default (или любом другом неймспейсе) и монтирует / хостовой файловой системы (RW).

Манифест
apiVersion: v1
kind: Pod
metadata:
  name: privileged-jupyter-pod   
spec:
  nodeName: <node-name>          # жесткое задание ноды, для пода
  containers:
  - name: debug                 
    image: ubuntu:20.04          
    command: ["sleep", "infinity"] # Контейнер будет "висеть", пока его не остановят
    securityContext:
      privileged: true           # Контейнер получает доступ ко всем возможностям ядра. Это опасно, но необходимо для взаимодействия с host-системой
    volumeMounts:
    - name: host-root
      mountPath: /host           # Монтируем внутрь контейнера по пути /host
      mountPropagation: HostToContainer # Маунты, сделанные на хосте, видны контейнеру. Но маунты внутри контейнера не видны хосту
  volumes:
  - name: host-root
    hostPath:
      path: /                    # Монтируем корень хостовой файловой системы
      type: Directory            
  hostPID: true                  # Контейнер будет видеть PIDы хоста 
  hostNetwork: true              # Контейнер использует сеть хоста 

Запускаем под на ноду:

kubectl exec -it privileged-jupyter-pod -- chroot /host bash

Проверка текущей установки

Заходим в наш привилегированный под, чтобы изучить систему ноды и установить новый драйвер:

$ dpkg -l | grep nvidia
libnvidia-container-tools     1.16.2-1    amd64    NVIDIA container runtime library (command-line tools)libnvidia-container1:amd64    1.16.2-1    amd64    NVIDIA container runtime library
nvidia-container-toolkit      1.16.2-1    amd64    NVIDIA Container toolkit
nvidia-container-toolkit-base 1.16.2-1    amd64    NVIDIA Container Toolkit Base
nvidia-docker2                2.14.0-1    all      NVIDIA Container Toolkit meta-package

Здесь отсутствуют пакеты nvidia-driver-*, libnvidia-compute-*, nvidia-utils-* и другие компоненты, поставляемые через APT. Это указывает на то, что драйвер NVIDIA был установлен другим способом. Вероятно, через официальный .run-файл с сайта NVIDIA.

Наличие run‑драйвера подтверждается выводом nvidia-smi (версия 515.48.07) и симлинком /usr/bin/nvidia-uninstall → nvidia-installer.

$ ls -l /usr/bin/nvidia-uninstall
lrwxrwxrwx 1 root root 16 Nov 30  2023 /usr/bin/nvidia-uninstall -> nvidia-installer


Удаление через APT в этом случае не приведет к очистке установленных core-модулей и библиотек. Иногда оно даже может нарушить работу ноды.

Удаление старого драйвера

chroot /host /usr/bin/nvidia-uninstall
# на вопрос о восстановлении X-конфига ответ «No»
# В Kubernetes нет ни GNOME, ни KDE, ни Xorg, поэтому просто нет смысла восстанавливать конфиг, которого нет.

Run‑скрипт сам выгружает модули и удаляет файлы из /usr/lib/x86_64-linux-gnu/. По завершении модуль nvidia в lsmod отсутствует.

Установка нового драйвера

chroot /host 
apt-get update
apt-get install -y curl gnupg2 ca-certificates build-essential dkms wget
wget <https://developer.download.nvidia.com/compute/cuda/repos/ubuntu2004/x86_64/cuda-keyring_1.1-1_all.deb>
dpkg -i /cuda-keyring_1.1-1_all.deb
apt-get update
apt-get install -y nvidia-driver-525

cuda-keyring подписывает официальный репозиторий, после чего пакет nvidia-driver-525 подтягивает core‑модули: nvidia-drm, nvidia-uvm и прочие.

Перезагрузка

Пока загружен старый модуль, nvidia-smi возвращает ошибку NVML:

Failed to initialize NVML: Driver/library version mismatch

Выполняем chroot /host systemctl reboot, под завершается, нода перезагружается, kubelet поднимается заново. После рестарта проверяем еще раз:

nvidia-smi
# Driver Version: 525.147.05   CUDA Version: 12.0

Ноду можно вернуть в пул командой kubectl uncordon.

Автоматизация через DaemonSet

Процесс ручного обновления довольно тривиальный и не подразумевает вариативности, поэтому сначала мы попробуем упаковать его в DaemonSet со скриптом, чтобы просто снизить объем ручных операций при обновлении драйверов сразу на нескольких нодах.

Целевые ноды выбираются по label.taint.nodeSelector; даунтайм по каждой ноде будет отдельный, но процесс станет повторяемым. Этот вариант все так же не подразумевает пересоздание нод-группы.

Конфиг DaemonSet
apiVersion: apps/v1                    
kind: DaemonSet                          # DS запускает под на выбранном узле
metadata:
  name: nvidia-driver-upgrade           
  namespace: kube-system                
spec:
  selector:
    matchLabels:
      name: nvidia-driver-upgrade       # селектор, по которому DaemonSet будет ассоциировать поды с самим собой
  template:
    metadata:
      labels:
        name: nvidia-driver-upgrade     # метка, для селектору выше (ей и будет помечен под)
    spec:
      nodeSelector:
        nvidia-driver-upgrade: "true"   # DaemonSet запускается только на нодах где есть такая метка (поставить заранее)
      tolerations:
        - key: "my-mode-jupyter"        # запуск на taint-узлах с этим ключом
          operator: "Exists"
          effect: "NoSchedule"
      containers:
      - name: upgrade
        image: ubuntu:20.04             
        securityContext:
          privileged: true              # доступ к функциям хоста (для chroot)
        command: [bash, -c, |           # запуск с bash-скрипт
          set -eux                      # трассировки и выход при ошибках
          if [ -f /host/usr/bin/nvidia-uninstall ]; then
            chroot /host /usr/bin/nvidia-uninstall --silent || true 
          fi
          chroot /host apt-get update                                
          chroot /host apt-get install -y curl gnupg2 ca-certificates build-essential dkms wget   
          chroot /host wget <https://developer.download.nvidia.com/compute/cuda/repos/ubuntu2004/x86_64/cuda-keyring_1.1-1_all.deb>  
          chroot /host apt-get update                                
          chroot /host apt-get install -y nvidia-driver-525          
          chroot /host systemctl reboot                              
        ]
        volumeMounts:
          - name: host-root
            mountPath: /host
            mountPropagation: HostToContainer   # маунты хоста видны контейнеру
      volumes:
        - name: host-root
          hostPath:
            path: /                  # корень хоста в контейнер
            type: Directory
      hostPID: true                  # контейнер видит PID-ы хоста
      hostNetwork: true              # использует сеть хоста
      restartPolicy: OnFailure       # будет перезапущен только при ошибке выполнения

Далее запускаем DaemonSet в кластер, и он выполнит за вас всю работу. Казалось бы, все автоматизировано, но есть несколько «но».

Во-первых, любое изменение версии ядра или адреса репозитория требует правки скрипта. Во-вторых, скрипт не валидирует, совместима ли версия CUDA с драйвером, а версия библиотеки — с runtime контейнера. В-третьих, такая автоматизация не подразумевает откат, и любая ошибка в скрипте повесит ноду до тех пор, пока не вмешается DevOps-инженер.

Поэтому мы стали искать другие способы, и нашли NVIDIA GPU Operator.

GPU Operator

GPU Operator от NVIDIA автоматизирует установку драйверов, device plugin’а и мониторинга в Kubernetes. Однако такой способ требует пересоздания нод-группы при обновлении.

Официальная документация Yandex Cloud предлагает сначала создать GPU-ноды без предустановленного драйвера (режим gpu_environment = "runc") и уже затем ставить оператор. Он берет на себя управление жизненным циклом драйвера и накатывает совместимые версии.

В нашей модели пользователь запускает отдельную GPU-ноду под Jupyter на время сессии, а по завершении работы нода удаляется для экономии. На старте может прийти нода с неподходящей версией предустановленного драйвера или вообще без драйвера, а нам важно быстро привести ее к целевому состоянию без ручных действий. Поэтому мы реализовали вариант с GPU Operator с учетом специфики инфраструктуры.

Установка через Helm

helm repo add nvidia <https://nvidia.github.io/gpu-operator>
helm repo update
helm upgrade --install gpu-operator nvidia/gpu-operator \\
  -n gpu-operator --create-namespace \\
  -f my-values.yaml

Важно помнить, что GPU Operator работает только с нодами без предустановленных драйверов (то есть с gpu_environment = "runc"), иначе он не сможет переустановить драйвер.

Наш кастомный my-values.yaml выглядит следующим образом:

my-values.yaml
driver:
  enabled: true  
  nvidiaDriverCRD:
    enabled: false  # не создавать CR драйвера
    deployDefaultCR: true  # установить стандартные CR
    driverType: gpu 
  nodeSelector:
    inst_type: <node-label>  # применять только на узлах с этим лейблом

  kernelModuleType: "auto"  # подбирать модуль ядра драйвера
  usePrecompiled: false  # собирать драйвер прямо на ноде
  repository: <driver-repo>  
  image: <driver-image>  
  version: "<driver-version>"  
  imagePullPolicy: IfNotPresent  
  upgradePolicy:
    autoUpgrade: true  # включить автообновление драйвера 
    drain:
      enable: false  # не отключать узел при обновлении
    gpuPodDeletion:
      force: true  # удалять старые поды
    maxUnavailable: 100%  

daemonsets:
  labels: {}  
  annotations: {}  
  priorityClassName: system-node-critical  # приоритет пода оператора
  nodeSelector:
    inst_type: <node-label> 

  tolerations: 
    - key: "<taint-key1>"  
      operator: Equal
      value: "<taint-value1>"
      effect: NoSchedule

  updateStrategy: RollingUpdate  
  rollingUpdate:
    maxUnavailable: "1"  

node-feature-discovery:
  worker:
    nodeSelector:
      inst_type: <node-label>  # запускать только на нужных узлах

    tolerations:
      - key: nvidia.com/gpu  # применять на GPU-нодах
        operator: Exists
        effect: NoSchedule

Разберем его блоки по порядку.

Блок driver

Отвечает за установку драйвера NVIDIA на GPU-нодах. Без него не будет работать CUDA и Kubernetes device plugin. Здесь определяются тип, версия, способ установки и обновления драйвера.

  • nvidiaDriverCRD:

    • enabled: если true, можно создавать несколько CRD NVIDIADriver с разными настройками для разных нод (разные версии, типы gpu/vgpu).

    • deployDefaultCR: при true создаётся дефолтный CR без nodeSelector, применимый ко всем GPU-нодам. В свою очередь, при false требуется вручную задавать CR c селекторами, но driver.nodeSelector контролирует, на каких нодах будет запускаться DaemonSet драйвера.

  • kernelModuleType: при auto по умолчанию выбирается лучший вариант модуля ядра, исходя из GPU и версии драйвера.

  • usePrecompiled (true/false):

    • false: драйвер компилируется прямо на ноде внутри контейнера.

    • true: используется заранее собранный образ с драйвером, что ускоряет установку.

  • autoUpgrade (true/false):

    • При true включается контроллер обновлений драйвера.

    • При false все настройки игнорируются и обновления не выполняются, а лейбл nvidia.com/gpu-driver-upgrade-state не меняется. Эти лейблы ставятся специальными служебными подами и определяют состояние ноды. Эта настройка нужна на случай, если требуется временно приостановить обновления.

  • gpuPodDeletion.force: значение true указывает, что нужно принудительно удалить все GPU-поды. Это нужно, чтобы драйверные модули ядра можно было очистить перед установкой новой версии.

Блок daemonsets

Определяет параметры DaemonSet, которые запускают драйверные контейнеры на GPU нодах.

Параметр priorityClassName: system-node-critical гарантирует, что драйверные поды запустятся даже при сильной нагрузке на кластер.

Блок node-feature-discovery

Этот компонент автоматически обнаруживает аппаратные характеристики ноды (GPU, PCI, ОС, версии ядра и т.д.) и устанавливает соответствующие лейблы, которые позволяют драйверным DaemonSet правильно таргетироваться на нужные ноды.

Заключение

Если ваша инфраструктура еще не разрослась до тех масштабов, когда обновлению драйверов требуется отдельная автоматизация, то ручной апдейт вполне решает эту задачу. При работе же с большим количеством нод под ML-проекты я бы советовала сразу использовать GPU Operator, рекомендованный техподдержкой Yandex Cloud, но с небольшой оговоркой.

Oператор становится единственным источником истины о драйвере для ноды в Yandex Cloud, и его прямое внедрение может привести к конфликту версий и висячим модулям, если драйвер уже есть на хосте. Чтобы избежать таких проблем, мы используем самописный DS, с помощью которого удаляем предустановленный драйвер. Альтернатива DS — пересоздаем нод-группу на driverless-версию, и после этого можно доверить ноды оператору, не опасаясь конфликтов.

Напоследок оставляю ссылку на официальные Release Notes с минимальными версиями драйвера для каждой версии CUDA. А узнать еще больше о том, как мы работаем с инфраструктурой, можно в других статьях нашего блога:

Комментарии (0)