
Привет! Я Алиса, 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. А узнать еще больше о том, как мы работаем с инфраструктурой, можно в других статьях нашего блога:
GitOps для Airflow: как мы перешли на лёгкий K8s-native Argo Workflows
Infrastructure as Code на практике: как мы рефакторили сложный Ansible-репозиторий
Firezone, или как спрятать свою инфраструктуру от посторонних глаз
Как дать разработчикам свободу при деплое приложений и ускорить процессы в команде