MAATRIX / Блог / Драйвер обновился сам, и CUDA перестала видеть карту

Драйвер обновился сам, и CUDA перестала видеть карту

MAATRIX

Инференс-сервис молчал не потому, что упало железо, и не потому, что кто-то полез в код руками — виноват был плановый ночной апдейт пакетов, который никто не связывал с GPU. Разбираем инцидент так, как он выглядел изнутри: с чего началось, какие версии произошедшего мы проверяли и отбрасывали, и где на самом деле лежала причина.

Что сломалось

Дежурному прилетел алерт: сервис инференса на выделенном GPU-сервере начал отвечать 500-ми на каждый запрос. Не деградация, не рост латентности — полный отказ конкретно тех эндпоинтов, что дергали модель через CUDA. Остальная часть приложения (API, очередь задач, база) работала штатно, что сразу сузило круг подозреваемых до GPU-стека.

Первое, что сделал дежурный — зашёл на сервер и попытался посмотреть состояние карты:

$ nvidia-smi
Failed to initialize NVML: Driver/library version mismatch

Карта физически была на месте (lspci | grep -i nvidia её видел), но userspace-инструменты драйвера отказывались с ней разговаривать. Контейнер с моделью при попытке инициализировать CUDA-контекст падал с похожим по духу, но другим по формулировке сообщением:

RuntimeError: CUDA error: no CUDA-capable device is detected
CUDA kernel errors might be asynchronously reported at some other API call

Два разных сообщения об одной и той же проблеме — это первая зацепка. Библиотека и модуль ядра не могут договориться, кто из них прав, и оба по-своему сообщают об этом.

Что видели в логах и метриках

Дальше — стандартный порядок действий при подозрении на драйвер: dmesg, журнал systemd, история пакетного менеджера. dmesg дал прямой ответ на вопрос «в чём разлад версий»:

$ dmesg | grep -i nvrm
NVRM: API mismatch: the client has the version 550.xx, but
      this kernel module has the version 535.xx.  Please
      make sure that this matches the version of your
      kernel module.

Модуль ядра, который реально загружен в память, — одной ветки. А библиотеки, с которыми пытается говорить процесс инференса (libnvidia-ml, CUDA runtime), — уже другой, более новой. Метрики мониторинга ничего аномального до момента отказа не показывали: температура карты в норме, использование памяти GPU — на обычном для ночной нагрузки уровне, PCIe-линк не деградировал, ECC-ошибок по карте не было. То есть с самим железом всё было в порядке — сломалось на программном уровне, ровно на стыке между ядром и userspace.

Журнал systemd подтвердил рамку по времени: незадолго до первых 500-х в логе unattended-upgrades появилась запись об установке новой версии пакета драйвера NVIDIA. apt-история дала точную картину:

$ zgrep -i nvidia /var/log/apt/history.log*
Start-Date: ...
Commandline: /usr/bin/unattended-upgrade
Upgrade: nvidia-driver-535-server:amd64 (535.xx, 550.xx),
         libnvidia-compute-535:amd64 (535.xx, 550.xx),
         nvidia-utils-535:amd64 (535.xx, 550.xx)
End-Date: ...

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

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать сервер

Какие гипотезы отбросили

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

  • Перегрев и троттлинг. Первая мысль при любой аномалии с GPU. Проверили температуру по IPMI и через nvidia-smi -q -d TEMPERATURE (пока она ещё отвечала на других процессах) — цифры обычные для этой нагрузки, троттлинга по clocks throttle reasons не было.
  • Просадка по питанию или деградация PCIe-линка. Бывает, что карта переходит на пониженную линию PCIe после проблем с разъёмом или блоком питания, и производительность падает, а не пропадает совсем. lspci -vv показал линк на штатной ширине и скорости, карта определялась шиной корректно — просто не отвечала на уровне драйвера.
  • ECC-ошибки видеопамяти. Проверили dmesg на Xid-события и вывод nvidia-smi -q -d ECC — ни одной записи. Если бы память сыпалась, картина была бы другой: частичные сбои, а не полный отказ инициализации.
  • OOM или проблема на уровне контейнера. Перезапустили контейнер с моделью отдельно, без остальной обвязки — ошибка воспроизводилась мгновенно и без OOM-killer в логах ядра. Значит, дело не в контейнере и не в лимитах памяти хоста.
  • Кто-то менял конфиг руками. Проверили список последних входов и историю команд операторов — в ночь инцидента никто на сервер не заходил. Единственным «действующим лицом» был unattended-upgrades, запущенный по таймеру.

Все пять версий закрылись за час-полтора проверки. Осталась одна: несовпадение версий модуля ядра и userspace-библиотек драйвера, которое видно прямо в dmesg.

В чём была реальная причина

NVIDIA-драйвер состоит из двух частей, которые должны быть строго одной версии: модуль ядра (.ko, то, что реально загружено в память и работает с железом) и набор userspace-библиотек (libnvidia-ml.so, libcuda.so, CUDA runtime и утилиты вроде nvidia-smi). Пакетный менеджер меняет файлы на диске сразу — новые .so встают на место старых в момент установки пакета. А модуль ядра, который уже загружен в память, остаётся старым до тех пор, пока его явно не выгрузят и не загрузят заново — а надёжный способ это сделать на живом сервере с работающими GPU-процессами один: перезагрузка (или как минимум выгрузка всех процессов, использующих карту, и rmmod/modprobe вручную, что на проде почти никогда не делают).

На этом сервере unattended-upgrades был настроен ставить security-обновления автоматически, но без автоматической перезагрузки — сознательное решение, чтобы не ронять GPU-нагрузку случайным ребутом посреди задачи. Разумно само по себе. Но при этом список пакетов, которые автообновления имеют право трогать, не был сужен: драйвер NVIDIA в репозитории дистрибутива попадает под ту же security-категорию, что и обычные системные пакеты, и апдейтится наравне с ними. В итоге получилась комбинация из двух по отдельности осмысленных решений, которая вместе дала дыру: пакеты обновляются автоматически, а перезагрузка — нет. Userspace-библиотеки ушли вперёд, модуль ядра остался на месте, и в момент, когда процесс инференса попытался создать новый CUDA-контекст, NVML отказался стартовать с чужой версией модуля.

Важный нюанс: сам инцидент проявился не в момент установки пакета, а позже — когда истёк жизненный цикл предыдущих CUDA-контекстов (переиспользуемые долгоживущие процессы могли до поры до времени работать на уже загруженных хендлах) и сервис попытался создать новый. Поэтому на первый взгляд казалось, что «ночью само сломалось без причины» — хотя причина была зафиксирована в логах на несколько часов раньше.

Что сделали, чтобы починить

Мгновенный фикс здесь один, и он не про откат пакетов, а про синхронизацию модуля с уже установленными библиотеками:

$ sudo systemctl stop <inference-service>
$ sudo reboot

После перезагрузки модуль ядра подхватывается той же версии, что и userspace-библиотеки на диске — обе части снова совпадают. Проверка сразу после старта:

$ nvidia-smi
$ cat /proc/driver/nvidia/version | head -1
$ dpkg -l | grep nvidia-driver

Все три команды должны называть одну и ту же версию драйвера. Если nvidia-smi отвечает без ошибок и цифры в /proc/driver/nvidia/version совпадают с версией установленного пакета — модуль и библиотеки снова в паре, можно поднимать сервис.

Откатывать пакет драйвера на предыдущую версию вместо перезагрузки — плохая идея: apt install nvidia-driver-535-server=535.xx теоретически работает, но требует ровно того же самого шага (выгрузки модуля из памяти), от которого пытались уйти, плюс тянет за собой пересборку зависимостей. Быстрее и предсказуемее просто перезагрузить сервер контролируемо, чем воспроизводить ту же проблему в обратную сторону.

Что изменили после инцидента

Разовый ребут решил конкретный инцидент, но не убрал причину — тот же сценарий повторился бы при следующем плановом апдейте. Поэтому после разбора внесли три изменения.

Исключили пакеты драйвера из автообновлений. Вместо того чтобы полагаться на то, что unattended-upgrades «сам разберётся», пакеты драйвера и связанные с ним библиотеки явно заморозили:

$ sudo apt-mark hold nvidia-driver-535-server \
    libnvidia-compute-535 nvidia-utils-535

и дополнительно добавили их в блок-лист самого unattended-upgrades, чтобы hold не потерялся при следующей переустановке метапакета:

# /etc/apt/apt.conf.d/50unattended-upgrades
Unattended-Upgrade::Package-Blacklist {
    "nvidia-*";
    "libnvidia-*";
};

Обновление драйвера теперь — осознанное отдельное действие с проверкой на тестовом узле, а не побочный эффект ночного крон-таймера.

Добавили проверку рассинхрона версий в мониторинг. Ключевая проблема этого инцидента в том, что рассинхронизация модуля и библиотек возникает молча — никакого предупреждения система не выдаёт до первого реального обращения к CUDA. Написали простой скрипт, который сверяет версию из /proc/driver/nvidia/version с версией установленного пакета и шлёт алерт при расхождении, ещё до того, как это долетит до продовых запросов:

#!/usr/bin/env bash
loaded=$(grep -oP 'Kernel Module\s+\K[0-9.]+' /proc/driver/nvidia/version)
installed=$(dpkg-query -W -f='${Version}' nvidia-driver-535-server 2>/dev/null | grep -oP '^[0-9.]+')
if [ "$loaded" != "$installed" ]; then
    echo "CRIT: nvidia kernel module ($loaded) != installed package ($installed)"
    exit 2
fi
echo "OK: driver versions match ($loaded)"

Скрипт подключили как обычную проверку в существующую систему мониторинга — подробно про то, какие метрики железа вообще стоит снимать на сервере, разбирали в статье про здоровье железа.

Пересмотрели readiness-проверку сервиса. До инцидента health-check дергал только HTTP-эндпоинт приложения, который отвечал «жив», даже если внутри CUDA-контекст падал при первом реальном запросе на инференс. Добавили в readiness-проверку лёгкий тестовый прогон через CUDA (создание контекста и выделение небольшого тензора), чтобы узел автоматически выпадал из балансировки при подобном рассинхроне, а не отдавал 500-е живым пользователям до тех пор, пока не проснётся дежурный.

Отдельно завели регламент: обновления пакетов, которые трогают ядро или драйверы GPU, накатываются на staging-узел той же конфигурации в дневное время, с последующим контролируемым ребутом, и только потом — на прод. Про похожие ситуации, когда штатный, казалось бы, апдейт кладёт продакшен, у нас есть отдельный разбор — что делать, если обновление положило прод, а общие грабли автообновлений безопасности на сервере собраны в статье про автообновления безопасности.

Если вы только выбираете конфигурацию под подобную нагрузку — присмотритесь к материалу про подбор выделенного сервера под машинное обучение: там же стоит сразу заложить политику обновлений в процесс эксплуатации, а не добавлять её постфактум после первого инцидента.

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать сервер

Нужны сами нейросети для контента?

Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.

Частые вопросы

Почему nvidia-smi и CUDA-приложение выдавали разные сообщения об одной проблеме?

Потому что это разные клиенты одного и того же модуля ядра. nvidia-smi использует NVML и явно сообщает про несовпадение версий. Приложение через CUDA runtime получает более общую ошибку инициализации устройства, потому что на этом уровне API не всегда разворачивает точную причину отказа — это компетенция NVML и логов ядра.

Можно ли было обойтись без перезагрузки сервера?

Технически да — выгрузить модуль (rmmod nvidia и связанные модули) и загрузить заново, предварительно остановив все процессы, использующие GPU (включая X-сервер, если он есть, Docker-контейнеры с --gpus, любые фоновые демоны вроде nvidia-persistenced). На практике на проде это сложнее и рискованнее, чем контролируемый ребут: модуль ядра часто держится занятым до последнего процесса, и в списке зависимостей легко что-то забыть.

Разве нельзя просто отключить автообновления совсем и обновлять всё руками?

Можно, но это отдельный компромисс: без автообновлений security-патчи для остальной системы (ядро, openssl, sshd и так далее) будут копиться и применяться реже, что тоже риск. Решение из этого разбора — не отключать автообновления целиком, а точечно исключить из них компоненты, для которых «тихое» обновление опаснее, чем задержка патча на несколько дней до ручной проверки.

Как понять заранее, что сервер уже в таком рассинхроне, до следующего падения приложения?

Именно для этого добавили сравнение версии загруженного модуля (/proc/driver/nvidia/version) с версией установленного пакета в мониторинг — это разовая проверка, которая ловит расхождение сразу после апдейта, а не постфактум по алертам от приложения.

Относится ли это только к bare-metal GPU-серверам или к виртуалкам с проброшенной картой тоже?

Механизм тот же: модуль ядра живёт в госте отдельно от того, что видит хост, и рассинхрон версий возможен и там. Разница только в том, что при пробросе GPU в виртуальную машину появляется ещё один уровень — совместимость версий драйвера хоста и гостя, которую тоже нужно держать под контролем при любом автообновлении.

Обсудить статью, задать вопрос или начать новую тему

Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.

Перейти в сообщество →