AI & Machine Learning

GPU Utilization: Counter vs. Cause

That comforting 97% GPU utilization number? It's a lie. Your GPU might be busy, but it's likely not doing anything useful.

График, показывающий временную шкалу с высокой утилизацией GPU (зелёный) и значительным падением пропускной способности токенов (красные провалы), иллюстрирующий это расхождение.

Key Takeaways

  • Метрика утилизации GPU от NVIDIA (например, из nvidia-smi) показывает время, когда на GPU выполнялось *любое* ядро, а не продуктивная работа, что приводит к ошибочной диагностике производительности.
  • Типичные сценарии сбоев, такие как несбалансированный префиллинг/декодирование, задержки в распределённом обучении, остановки ввода-вывода, конкуренция за ресурсы CPU и насыщение пропускной способности памяти, проявляются как высокая утилизация GPU при низкой пропускной способности.
  • Точное определение узких мест требует сопоставления низкоуровневых данных: вызовов CUDA API, событий драйвера, трассировок выполнения ядер и активности CPU на хосте, а не только агрегированной утилизации.

Сервер vLLM, согласно nvidia-smi, восемь минут подряд рапортует о 97% загрузке GPU. В то же время, пропускная способность по токенам стремительно падает. И оба утверждения, как ни парадоксально, верны. В этом и кроется цифровая алхимия. Повсеместный показатель GPU utilization от NVIDIA — это не мера продуктивной работы. Нет. Это простой счётчик цикла нагрузки. Он показывает, выполнялось ли что-то на GPU, но не то, стоило ли это что-то выполнять.

Мы столкнулись с этой абсурдностью, воспроизводя внутренний кейс с пиком задержки vLLM. Железо? TensorDock RTX 4090. ПО? vLLM 0.18.0, работающий с Qwen2.5-0.5B-Instruct. Восемь минут дашборд выглядел образцово. Nvidia-smi показывал колебания от 92% до 99%, в среднем стабильные 97%. Вентиляторы гудели, память держалась, энергопотребление — 320 Вт. Всё идёт по плану, верно?

Не совсем.

Виновником стал скромный на вид запрос: n_completions=8 в паре с logprobs=20. Эта конфигурация при каждом шаге декодирования порождала восемь отдельных последовательностей, каждая из которых требовала полнословарный softmax. Речь идёт о 150 000 токенов на каждое такое расширение. Каждая из этих громадин фактически ставила в заложники все остальные одновременно планируемые запросы на 9-11 секунд. GPU был занят, да, но он был занят обработкой невидимого пользователю мусора. Пропускная способность? Коллапсировала.

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

В собственной документации NVIDIA полезно трактуется GPU-Util как: “процент времени за последний период выборки, в течение которого один или несколько ядер выполнялись на GPU”. Рабочий цикл. Не более. Он не даёт никакой информации о том, эффективен ли поток, есть ли в нём узкое место, или же он активно мешает другим операциям. Это как хвастаться количеством часов, проведённых в спортзале, не упоминая, поднимали ли вы штангу или просто смотрели в потолок.

DCGM, более продвинутый набор инструментов NVIDIA, предлагает более детальную гранулярность с такими счётчиками, как SM_ACTIVE и MEM_COPY_UTIL. Они помогают, но лишь немного. Ядро, работающее на жалких 5% от своей пиковой производительности в течение 100 миллисекунд, всё равно зарегистрирует 100% SM_ACTIVE за этот интервал. Дашборд остаётся в неведении.

Мы разбирали этот паттерн на различных нагрузках. Высокая утилизация, падающая пропускная способность и дашборд, который мог бы быть и волшебным шаром. Общая нить? Первопричина лежит глубже.

Обычные Подозреваемые: Почему Ваш GPU Думает, Что Он Занят

  1. Танго Prefill/Decode: Фреймворки вроде vLLM, SGLang и TGI пытаются объединять префиллинг (обработку ввода) и декодирование (генерацию вывода) на одном и том же железе. Когда префиллинг требует экспоненциально больше вычислений, чем декодирование — что часто бывает при работе с длинными контекстами — один запрос с длинным контекстом становится пробкой для всех остальных, более коротких. GPU остаётся на 100% SM_ACTIVE, потому что префилльные ядра занимают все шейдерные ядра. Тем временем, задержка декодирования для ожидающих запросов растёт до бесконечности.

  2. Пробки в Распределённом Обучении: Представьте операцию all-reduce на 4 GPU. Если один GPU отстаёт, остальные ждут. Эти ожидающие GPU показывают 100% утилизацию, потому что ядро, оркестрирующее ожидание, само по себе является ядром. Общая пропускная способность определяется самым медленным узлом, а не эффективными.

  3. Тупик Загрузчика Данных: PyTorch DataLoader, при выполнении перестановки индексов в основном процессе, может стать однопоточным узким местом. GPU послушно запускает одно и то же ядро forward снова и снова, в то время как запуск следующего пакета блокируется вызовом cudaStreamSync. Ядро кричит, но следующая задача застряла на подъездной дорожке.

  4. Хаос CPU-Ядер: Движок vLLM работает в однопоточном режиме. Контекстный переключатель ОС — работа соседнего ядра, назойливое прерывание, плохо управляемая cgroup — может приостановить вызов cudaLaunchKernel. Мы видели, как P99 cudaLaunchKernel достигал 13.1 мс (огромный скачок от типичного P50 в 16.7 мкс), и всё это из-за сбоев планировщика. GPU продолжает выполнять то ядро, которое было активно до остановки, создавая иллюзию нормальной утилизации.

  5. Мельница Пропускной Способности Памяти: Ядро, которое перегружает систему данными быстрее, чем SM могут их обработать, отчитается о 100% SM_ACTIVE. Но реальное ограничение? Пропускная способность DRAM. Утилизация — это отвлекающий манёвр; узким местом является пропускная способность памяти.

Во всех этих сценариях симптом удручающе знаком: высокая утилизация, низкая пропускная способность. Причина же скрывается в нижележащих слоях.

Поиск Реального Узкого Места

Так как же снять эти слои? Забудьте об агрегированной утилизации. Задайте главный вопрос: “На что на самом деле ждал GPU, секунда за секундой?”

Ответ на этот вопрос требует сопоставления данных из нескольких источников на одном и том же хосте, синхронизированных по меткам времени:

  • Вызовы CUDA Runtime API: Отслеживайте такие события, как cudaLaunchKernel, cudaMemcpyAsync, cudaStreamSynchronize и cudaDeviceSynchronize с помощью uprobes на libcudart.so.
  • Вызовы CUDA Driver API: Отслеживайте cuLaunchKernel и связанные с ним операции на уровне драйвера, используя uprobes на libcuda.so.
  • Трассировки Выполнения Ядер: Погрузитесь в реально выполняющиеся ядра. Инструменты вроде CUPTI или NVIDIA Nsight могут предоставить детальные профили длительности ядра, загрузки и использования ресурсов внутри самого ядра.
  • Активность на Хосте: Не игнорируйте CPU. Мониторьте активность потоков CPU, контекстные переключения и системные вызовы, связанные с взаимодействием драйвера GPU.
  • Пропускная Способность Памяти: Напрямую измеряйте использование пропускной способности DRAM. Это часто предоставляется через DCGM или специфические инструменты профилирования.

Сплетая эти нити воедино, вы наконец сможете увидеть разницу между GPU, занятым продуктивными вычислениями, и тем, который просто крутит колеса — различие, которое 97% утилизации удобно скрывает.

Это не просто теоретическая проблема; это постоянная, разочаровывающая реальность в сфере высокопроизводительных вычислений. И по мере того, как AI-нагрузки становятся всё более сложными, способность видеть дальше простого счётчика утилизации станет не просто полезной, а абсолютно необходимой.


🧬 Похожие Материалы

Written by
Open Source Beat Editorial Team

Curated insights, explainers, and analysis from the editorial team.

Worth sharing?

Get the best Open Source stories of the week in your inbox — no noise, no spam.

Originally reported by Dev.to