Два процесса делили одну видеокарту, и оба работали втрое медленнее
На сервере с одной видеокартой одновременно оказались два процесса — и вместо ожидаемого «каждому по половине карты» оба начали работать примерно втрое медленнее, чем поодиночке. Арифметика не сходилась: делёж ресурсов должен был просадить производительность вдвое, а не втрое. Разбираем, как искали причину, какие версии отбросили и почему виноват оказался не дефицит памяти и не троттлинг, а то, как GPU в принципе исполняет код нескольких процессов без специальной настройки.
Содержание
Что сломалось
Ситуация обычная для небольшой команды: на арендованном сервере с одной картой крутится боевой инференс-сервис — API, который в реальном времени отвечает на запросы продукта, — и рядом иногда запускается фоновая задача: пересчёт эмбеддингов каталога, дообучение LoRA-адаптера или просто скрипт для генерации превью пачкой. Обычно фоновая задача ставится в ночное окно, но в этот раз коллега запустил пересчёт вручную днём, не сверившись с тем, что на карте уже что-то работает.
Через несколько минут в мониторинге сработал алерт по времени ответа API — задержки выросли кратно относительно обычных значений. Первая реакция была логичной: решили, что кто-то соседний по карте «съел» ресурсы, и после завершения фоновой задачи всё вернётся в норму само. Но когда время ответа не пришло в норму даже после того, как оба процесса явно продолжали работать (оба показывали активность и не падали с ошибками), стало ясно, что дело не в банальной конкуренции за память — оба процесса физически помещались на карте с запасом.
Ключевая деталь, которая и превратила случай в инцидент для разбора: если бы карта просто честно делила вычисления пополам между двумя процессами, каждый из них замедлился бы примерно вдвое — неприятно, но предсказуемо и объяснимо. На деле оба ощутимо просели сильнее, чем в два раза — и именно это несоответствие «ожидаемая математика vs факт» стало поводом копать глубже, а не списывать всё на «просто карта перегружена».
Что показали метрики в момент проблемы
Первым делом посмотрели на саму карту:
nvidia-smi
Картина была ровной и на первый взгляд ничего не объясняла: оба процесса присутствовали в списке, у каждого была своя память, суммарно она укладывалась в объём карты с запасом, а загрузка compute у обоих процессов колебалась около высоких значений. То есть карта не простаивала и не была перегружена памятью — она была занята работой. Проблема была не в том, что кто-то не может попасть на карту, а в том, что суммарная полезная работа за единицу времени оказалась заметно ниже, чем можно было ожидать от двух параллельных процессов на одном железе.
Дальше подключили nvidia-smi dmon, чтобы смотреть загрузку и служебные счётчики построчно во времени, а не разовым снимком:
nvidia-smi dmon -s pucvmet -d 1
В выводе не было ничего похожего на явную аномалию — ни скачков температуры, ни просадок по частоте, ни ошибок ECC. Логи обоих приложений тоже были чистыми: ни таймаутов на своей стороне, ни исключений вроде нехватки памяти, ни повторных попыток запросов. Оба процесса просто медленнее делали свою работу — как будто каждый получил меньше вычислительной мощности карты, чем должен был при честном разделении пополам.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Арендовать серверГипотезы, которые не подтвердились
Прежде чем разбираться в механике исполнения кода на GPU, проверили более простые и частые причины замедления — по очереди, чтобы не гадать, а последовательно закрывать версии.
Не хватает видеопамяти. Первая и самая частая причина конфликта нескольких процессов на одной карте — но в логах не было ни одной ошибки нехватки памяти (CUDA out of memory), а nvidia-smi стабильно показывал свободный запас в несколько гигабайт даже в пике. Версию закрыли быстро — с памятью подробнее и как её лимитировать между сервисами разобрано в статье про то, как раздать одну GPU нескольким сервисам.
Троттлинг по температуре или питанию. Карта под нагрузкой греется, и если она упирается в температурный или мощностной лимит, драйвер сам снижает частоты — это реальная и частая причина падения производительности под совместной нагрузкой. Проверили подробный отчёт по производительности:
nvidia-smi -q -d PERFORMANCE
В поле Clocks Throttle Reasons все флаги, кроме Idle, были в состоянии Not Active — то есть карта не снижала частоты ни по температуре, ни по лимиту мощности, ни по внешнему сигналу. Температура держалась в рабочем диапазоне, частоты — на ожидаемом уровне для обоих процессов. Версию закрыли: троттлинга не было.
Упор в шину PCIe или в память хоста. Если один из процессов активно гоняет данные между CPU и GPU — например, стримит большие батчи с диска, — теоретически можно упереться в пропускную способность PCIe раньше, чем в вычисления самой карты. Проверили топологию и загрузку шины:
nvidia-smi topo -m
nvidia-smi dmon -s t -d 1
Карта сидела на ожидаемом поколении и ширине линии PCIe, а счётчики загрузки шины были далеки от насыщения — оба процесса читали свои данные преимущественно из памяти самой карты, а не гоняли их туда-обратно с хоста в реальном времени. Версию тоже закрыли.
Конкуренция за CPU-ядра или сеть. На всякий случай проверили htop и сетевые счётчики — оба процесса использовали CPU умеренно, свободных ядер хватало, входящий и исходящий трафик были далеки от лимитов интерфейса. Здесь узкого места тоже не оказалось.
После того как отпали память, троттлинг, шина и хост-ресурсы, оставалось единственное необъяснённое место — сама механика того, как GPU физически исполняет инструкции двух независимых процессов одновременно.
Настоящая причина: контексты CUDA делят карту по очереди, а не параллельно
Здесь и оказалась суть проблемы, которая на первый взгляд выглядит нелогично: GPU без специальной настройки не выполняет код двух разных процессов по-настоящему параллельно, даже если у неё формально достаточно вычислительных блоков на обоих. У каждого процесса, который открывает своё соединение с картой, появляется собственный CUDA-контекст — а аппаратный планировщик карты умеет активно исполнять ядра только одного контекста в конкретный момент времени. Второй контекст ждёт своей очереди, а карта переключается между ними — это называется time-slicing, разделение по времени, а не по вычислительным блокам.
Само по себе переключение контекстов не бесплатно: у каждого переключения есть фиксированные накладные расходы на сохранение состояния одного контекста и восстановление другого, которые не зависят от того, насколько маленькое задание было запущено. И вот здесь кроется третий множитель, которого не должно было быть при честном делении пополам.
Инференс языковой модели — это не одна большая вычислительная операция, а по сути непрерывный поток из множества мелких ядер: генерация каждого следующего токена требует отдельных проходов через слои модели, и каждый такой проход — это десятки-сотни отдельных запусков на GPU. Когда таких мелких запусков много и они чередуются между двумя процессами, накладные расходы на переключение контекста начинают происходить настолько часто, что перестают быть незаметной мелочью на общем фоне — они начинают откусывать заметную долю времени, которое иначе ушло бы на полезные вычисления. Именно поэтому вместо ожидаемого падения «вдвое» (честная конкуренция за одну и ту же вычислительную мощность) получилось падение сильнее — «лишняя» просадка была платой за сами переключения, а не за нехватку вычислений как таковых.
Чтобы убедиться, что дело именно в этом, а не в очередной непроверенной гипотезе, поставили контролируемый эксперимент: прогнали одинаковый набор запросов через каждый процесс по отдельности, замерили время; затем прогнали оба процесса одновременно в обычном режиме — просадка оказалась заметно больше пропорциональной; затем повторили тот же параллельный прогон, но с включённым NVIDIA MPS (Multi-Process Service) — сервисом, который умеет объединять запросы разных процессов в общий контекст на аппаратном уровне вместо честного переключения между отдельными контекстами. С MPS суммарная просадка стала гораздо ближе к ожидаемой «примерно пополам» — лишний множитель практически исчез. Это и стало финальным подтверждением: контекст-свитчинг без MPS — реальная причина, а не побочный эффект.
Как включили MPS и что изменили в инфраструктуре
NVIDIA MPS запускается как отдельный демон, через который процессы отправляют работу на карту вместо прямого обращения к драйверу каждый сам по себе. Базовый запуск на Linux выглядит так:
# Каталоги для управляющего pipe и логов MPS
export CUDA_MPS_PIPE_DIRECTORY=/tmp/nvidia-mps
export CUDA_MPS_LOG_DIRECTORY=/tmp/nvidia-log
mkdir -p $CUDA_MPS_PIPE_DIRECTORY $CUDA_MPS_LOG_DIRECTORY
# Запуск control-демона от имени пользователя с доступом к карте
nvidia-cuda-mps-control -d
# Проверить, что демон поднялся
ps -ef | grep mps
После этого оба процесса — инференс-API и фоновый пересчёт — просто запускаются как обычно, с теми же переменными окружения CUDA_MPS_PIPE_DIRECTORY, указывающими на тот же MPS-демон. Никаких изменений в самом коде приложений не потребовалось — MPS работает прозрачно на уровне драйвера.
Отдельно настроили честность между процессами, чтобы фоновая задача не могла в моменты пиковой активности отжать себе непропорционально много вычислений у боевого API — ограничили ей долю активных потоков:
# Ограничить фоновому процессу не более 40% вычислительных ресурсов карты через MPS
export CUDA_MPS_ACTIVE_THREAD_PERCENTAGE=40
Кроме включения MPS, приняли ещё три решения по итогам разбора:
- Завели правило, что ручной запуск тяжёлых фоновых задач в рабочие часы требует предварительной проверки загрузки карты — простая договорённость, которая не стоила ничего, но убрала сам триггер инцидента.
- Добавили в дашборд мониторинга отдельную панель с числом активных CUDA-процессов на карте (
nvidia-smi --query-compute-apps=pid,process_name --format=csv), чтобы видеть факт совместного использования карты сразу, а не постфактум по жалобам на задержки. - Пересмотрели список кандидатов на MPS по умолчанию — не для всех пар нагрузок это правильное решение: MPS выигрывает там, где ядра мелкие и частые (как в нашем случае с инференсом), но почти не даёт эффекта, если один из процессов и так использует карту редкими, но крупными и долгими ядрами — там переключений контекста и так немного, и с ними можно смириться без дополнительной настройки.
Полезно посмотреть на проблему и шире: если карта регулярно упирается в подобные эффекты при росте числа параллельных клиентов, стоит заранее прикинуть, сколько пользователей выдержит одна видеокарта на вашем железе и профиле нагрузки, а также свериться с тем, почему batching запросов экономит GPU — во многих случаях объединение мелких запросов в общие батчи внутри одного процесса снимает саму необходимость держать на карте несколько конкурирующих контекстов.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Арендовать серверНужны сами нейросети для контента?
Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.
Частые вопросы
В чём принципиальная разница между обычным запуском нескольких процессов и MPS?
Без MPS каждый процесс получает собственный CUDA-контекст, и карта аппаратно переключается между ними по очереди с накладными расходами на каждое переключение. MPS объединяет запросы нескольких процессов в общий контекст на уровне демона, снимая большую часть этих накладных расходов и позволяя ядрам разных клиентов исполняться с меньшим числом переключений.
MIG (аппаратная нарезка карты) решает ту же проблему?
Частично и по-другому. MIG жёстко делит карту на изолированные аппаратные доли с гарантированной производительностью каждой — это надёжнее с точки зрения предсказуемости, но доступно не на всех картах (в основном на старших дата-центровых моделях) и снижает суммарную пиковую производительность карты. MPS доступен шире, но не даёт таких же жёстких гарантий изоляции между процессами.
Как понять прямо сейчас, работают ли на моей карте несколько процессов без MPS?
Запустите nvidia-smi --query-compute-apps=pid,process_name,used_memory --format=csv — если в списке больше одной строки и MPS-демон не поднят (ps -ef | grep mps ничего не находит), карта делит время между процессами в режиме по умолчанию.
MPS может сделать хуже, чем без него?
Да, в редких случаях: если один из клиентов ведёт себя недобросовестно (агрессивно занимает вычисления) и не ограничен по доле активных потоков, он может ещё сильнее задавить соседей, потому что накладные расходы на переключение контекстов исчезли, а сам дисбаланс распределения ресурсов остался. Поэтому вместе с включением MPS стоит сразу настраивать лимиты через CUDA_MPS_ACTIVE_THREAD_PERCENTAGE.
Нужно ли включать MPS, если на карте всегда только один процесс?
Нет, смысла нет — MPS решает проблему именно конкуренции нескольких контекстов за одну карту, для одного процесса он ничего не меняет.
Обсудить статью, задать вопрос или начать новую тему
Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.
Перейти в сообщество →