В Ситидрайве Kubernetes обновляют регулярно — инфраструктура большая, и актуальность версий критически важна. После апгрейда до версии 1.29.15 один из GPU-узлов внезапно «забыл» о своей видеокарте, и нам пришлось срочно искать решение. В этой статье я расскажу, в чём была причина бага и как Time-Slicing помог повысить утилизацию GPU. Статья будет полезна всем, кто работает с GPU в Kubernetes и хочет избежать подобных сюрпризов в продакшене.

Что находится под капотом GPU-кластера в Ситидрайве

Часть нашей инфраструктуры работает на GPU — в основном для задач ML-инженеров, которые обучают и тестируют AI-модели. Отдельный кластер с видеокартами помогает командам запускать обучение и inference без простоев, а backend-сервисы используют те же GPU для расчётов и бизнес-логики, где важна скорость.

Мы используем Tesla T4, так как из всех моделей, доступных у нашего облачного провайдера, она оказалась золотой серединой: достаточно производительная, энергоэффективная и не требует жертвоприношений при настройке CUDA-драйверов.

Процесс добавления нового GPU-узла выглядит просто, если смотреть со стороны — и не очень просто, если быть тем, кто его автоматизирует:

  1. получаем сервер,

  2. ставим всё необходимое для работы с видеокартой,

  3. запускаем тестовые контейнеры для бенчмарков,

  4. и позволяем Ansible сделать магию, конфигурируя всё под ключ.

Чтобы kubelet понимал, что рядом живёт видеокарта, в кластере работает DaemonSet nvidia-device-plugin — по сути, это прослойка между Kubernetes и самим GPU. Она сообщает kubelet, какие ресурсы доступны, и регистрирует их как nvidia.com/gpu.

После подготовки узла мы добавляем его в кластер с лейблом — чтобы сервисы, которым нужны вычислительные мощности, знали, куда идти. В Helm values указываем nodeSelector, после чего узел дружит с видеокартой, kubelet — с узлом, сервис — с GPU. Так мы жили долго и счастливо… до Kubernetes 1.29.15 ?

Но что-то пошло не по плану

В рамках задачи по обновлению кластера с версии 1.28.15 на 1.29.15 случился интересный кейс. Перед каждым апгрейдом мы в Ситидрайве традиционно проходим ритуал DevOps-инженера: читаем release notes, сверяем матрицы совместимости, составляем пошаговый план, и только потом трогаем kubeadm upgrade. Всё по документации, без самодеятельности — как завещал официальный гайд.

Но стоило обновить один из GPU-узлов — и Kubernetes внезапно «забыл», что у него вообще есть видеокарта.

Вот что было:

kubectl describe no gpu-node.tech
Capacity:
  cpu:                8
  ephemeral-storage:  103126288Ki
  hugepages-1Gi:      0
  hugepages-2Mi:      0
  memory:             32870700Ki
  nvidia.com/gpu:     0
  pods:               110

А вот, что должно было быть:

Capacity:
  cpu:                8
  ephemeral-storage:  103123028Ki
  hugepages-1Gi:      0
  hugepages-2Mi:      0
  memory:             32870700Ki
  nvidia.com/gpu:     1
  pods:               110

И сервер уверенно показывал обратное:

# lspci | grep -i nvidia
00:06.0 3D controller: NVIDIA Corporation TU104GL [Tesla T4] (rev a1)
# lsmod | grep nvidia 
nvidia_uvm 4476928 2 
nvidia_drm 81920 0 
nvidia_modeset 1339392 1 nvidia_drm 
nvidia 53993472 10 nvidia_uvm,nvidia_modeset 
drm_kms_helper 184320 2 virtio_gpu,nvidia_drm 
drm 495616 6 
drm_kms_helper,nvidia,virtio_gpu,nvidia_drm,ttm

Смотрю, что с подами nvdp-nvidia-device-plugin, а там такая картина:

NAME                              READY   STATUS                 RESTARTS      AGE   IP              NODE
nvdp-nvidia-device-plugin-5srhl   0/1     CreateContainerError   0             24s   29.64.143.233   gpu-node.tech

Событие у пода выглядело не менее ободряюще:

Error: container create failed: error executing hook /usr/bin/nvidia-container-toolkit

Результат — GPU-ресурс пропал из кластера, и все сервисы, завязанные на видеокарте, остались без вычислительных мощностей.

Начинаю расследование

Версий было несколько: новый Kubernetes, новая версия CRI-O, старый nvidia-device-plugin, или что-то совсем экзотическое из глубин зависимостей. Ведь с каждым минорным апдейтом Kubernetes «под капотом» подтягивается целый ворох изменений, и угадай потом, что именно пошло не так. Нашёл такие интересные материалы по теме:

Не буду здесь подробно разбирать каждое — в ссылках достаточно информации для тех, кто столкнётся с похожей историей. Оставлю их скорее как карту клада для инженера, который будет искать «почему GPU не видно».

Мне помог вот этот комментарий в обсуждении Podman:
?https://github.com/containers/podman/discussions/16101#discussioncomment-6977446

Попробовал повторить предложенное решение — и о чудо: под nvdp-nvidia-device-plugin начал «подниматься». Правда, ненадолго. Через пару секунд он уходил в CrashLoopBackOff, потому что hook oci-nvidia-hook.json не отрабатывал.

В логах красовалось следующее:

I0917 14:25:25.703510 1 main.go:256] Retreiving plugins. 
W0917 14:25:25.703841 1 factory.go:31] No valid resources detected, creating a null CDI handler 
I0917 14:25:25.703899 1 factory.go:107] Detected non-NVML platform: could not load NVML library: libnvidia-ml.so.1: cannot open shared object file: No such file or directory 
I0917 14:25:25.703935 1 factory.go:107] Detected non-Tegra platform: /sys/devices/soc0/family file not found 
E0917 14:25:25.703948 1 factory.go:115] Incompatible platform detected 
E0917 14:25:25.703952 1 factory.go:116] If this is a GPU node, did you configure the NVIDIA Container Toolkit?

То есть плагин внутри контейнера попросту не видел драйвер и/или CDI-спеку. GPU физически была, но с точки зрения Kubernetes — её не существовало.

План Б: GPU Operator

Продолжил копать. На GitHub наткнулся на issue, где в самом конце кто-то написал: «Перешёл на NVIDIA GPU Operator — и всё заработало».

Чтобы убедиться, что это не единичный случай, я пошёл искать подтверждения и полез в русскоязычный телеграм-чат Kubernetes RU. Нашёл сразу пару похожих кейсов (в одном даже версия Kubernetes совпадала — 1.29). Коллеги советовали: «Поставь NVIDIA GPU Operator и не грей голову». — Георг Гаал, источник 

Так GPU Operator из запасного плана Б стал вполне серьёзным кандидатом. 

Тем временем я обратил внимание на строчки из логов про CDI. Оказалось, что это новый механизм, появившийся в Kubernetes 1.26,  как раз заточен под работу с устройствами вроде GPU:
https://kubernetes.io/docs/concepts/extend-kubernetes/compute-storage-net/device-plugins/

И вот тут всё сошлось. Решил попробовать GPU Operator — и... всё действительно заработало! Буквально одной командой оператор делает все для работы с видеокартами:

helm install --wait --generate-name -n gpu-operator --create-namespace nvidia/gpu-operator --version=v25.3.3

В процессе очень помогла статья на Хабре от Selectel — натыкался на неё несколько раз в разных обсуждениях, и теперь понял, почему.

После того, как все компоненты GPU-оператора заработали, для kubelet вновь стал доступен ресурс GPU:

Capacity:
  cpu:                8
  ephemeral-storage:  103123028Ki
  hugepages-1Gi:      0
  hugepages-2Mi:      0
  memory:             32870700Ki
  nvidia.com/gpu:     1
  pods:               110

Для активации CDI достаточно прописать в helm values:

cdi:
  enabled: true
  default: true

Снимаем головную боль DevOps’а 

Погрузившись в документацию GPU Operator, я понял, насколько далеко ушёл Kubernetes в плане работы с GPU:

  1. NVIDIA Device Plugin — основная задача этого плагина — регистрировать GPU как ресурсы в kubernetes. Никаких драйверов, библиотек CUDA или мониторинга он не ставит. Это необходимо делать самостоятельно, как мы  и делали раньше через ansible. Вариант подходит, например, если на узлах кластера уже стоят драйверы NVIDIA и CUDA.

  2. NVIDIA GPU Operator — это более комплексное решение. Он управляет установкой всем GPU-стеком в кластере: разворачивает CUDA Toolkit, разворачивает сам Device Plugin, добавляет мониторинг GPU (DCGM, Exporter), может устанавливать NVIDIA Container Toolkit, CDI, следит за состоянием всех компонентов. Штука мощная. И действительно разгружает администратора кластера по многим пунктам. 

Благодаря операторам не нужно добавлять лейбл на узел вручную. Также теперь в values сервиса можно оперировать GPU как стандартными ресурсами:

        resources:
          limits:
            cpu: 300m
            memory: 3Gi
            nvidia.com/gpu: "1"

Делим GPU между командами: как мы пришли к Time-Slicing

Сам по себе ресурс видеокарты «из коробки» не позволяет использовать какую-то её часть. Как например с CPU и Memory, где мы можем указать 0.1 CPU или 50 Mi. В видеокартах вы указываете целое значение: одна видеокарта, две, три и так далее. Бывают приложения, которым не нужны мощности целой видеокарты. Поэтому инженеры придумали разные способы «порезать» GPU между задачами. Хороший обзор этой темы — в разборе от Selectel.

Как я упоминал ранее, в кластерах Ситидрайва используются Tesla T4. Эти карты не поддерживают технологию MIG (Multi-Instance GPU), которая позволяет делить GPU на аппаратные «слайсы» — как это делают A100 или H100. 

Зато у T4 есть поддержка Time-Slicing — мы пошли этим путём. Он реализует «виртуальное разделение» GPU по времени: приложения используют одну и ту же видеокарту, но по очереди, а оператор управляет планированием и балансом нагрузки.

Time-Slicing достаточно просто настраивается: создаём ConfigMap с нужным количеством квот (например мы установили на одном из кластеров 4, а на другом 5), патчим ресурс clusterpolicies.nvidia.com/cluster-policy и всё. Если потребуется что-то изменить, просто обновляем ConfigMap и перезапускаем daemonset/nvidia-device-plugin-daemonset. Оператор самостоятельно всё настраивает и оркестирует.

Видим такой результат:

Capacity:
  cpu:                16
  ephemeral-storage:  103112428Ki
  hugepages-1Gi:      0
  hugepages-2Mi:      0
  memory:             131910100Ki
  nvidia.com/gpu:     4
  pods:               110

(До этого у параметра nvidia.com/gpu было значение 1).

Но перед тем как выбрать способ шеринга, стоит внимательно изучить детали. У MIG и Time-Slicing свои плюсы и минусы. Например, у Time-Slicing могут быть:

  • долгие context switch между задачами,

  • общая память у приложений, что может привести к Out Of Memory, если одно приложение решит «съесть» чуть больше, чем положено.

Есть и альтернативные решения — например, проект HAMi, который динамически выделяет независимые GPU-квоты и обещает более гибкое управление.

Результат одного обновления

Time-Slicing позволил нам повысить утилизацию GPU и масштабировать задачи ML-команд без закупки дополнительных видеокарт. Теперь несколько инженеров (а иногда и целые команды) могут спокойно работать параллельно на одном узле.

Вывод простой:  если вы только начинаете строить GPU-инфраструктуру в Kubernetes — ставьте GPU Operator сразу. Он избавит вас от десятков мелких настроек и головной боли, а заодно даст мощную основу для гибкого «шеринга» GPU.

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