Сервер 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 Думает, Что Он Занят
-
Танго Prefill/Decode: Фреймворки вроде vLLM, SGLang и TGI пытаются объединять префиллинг (обработку ввода) и декодирование (генерацию вывода) на одном и том же железе. Когда префиллинг требует экспоненциально больше вычислений, чем декодирование — что часто бывает при работе с длинными контекстами — один запрос с длинным контекстом становится пробкой для всех остальных, более коротких. GPU остаётся на 100%
SM_ACTIVE, потому что префилльные ядра занимают все шейдерные ядра. Тем временем, задержка декодирования для ожидающих запросов растёт до бесконечности. -
Пробки в Распределённом Обучении: Представьте операцию
all-reduceна 4 GPU. Если один GPU отстаёт, остальные ждут. Эти ожидающие GPU показывают 100% утилизацию, потому что ядро, оркестрирующее ожидание, само по себе является ядром. Общая пропускная способность определяется самым медленным узлом, а не эффективными. -
Тупик Загрузчика Данных: PyTorch
DataLoader, при выполнении перестановки индексов в основном процессе, может стать однопоточным узким местом. GPU послушно запускает одно и то же ядроforwardснова и снова, в то время как запуск следующего пакета блокируется вызовомcudaStreamSync. Ядро кричит, но следующая задача застряла на подъездной дорожке. -
Хаос CPU-Ядер: Движок vLLM работает в однопоточном режиме. Контекстный переключатель ОС — работа соседнего ядра, назойливое прерывание, плохо управляемая cgroup — может приостановить вызов
cudaLaunchKernel. Мы видели, как P99cudaLaunchKernelдостигал 13.1 мс (огромный скачок от типичного P50 в 16.7 мкс), и всё это из-за сбоев планировщика. GPU продолжает выполнять то ядро, которое было активно до остановки, создавая иллюзию нормальной утилизации. -
Мельница Пропускной Способности Памяти: Ядро, которое перегружает систему данными быстрее, чем 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-нагрузки становятся всё более сложными, способность видеть дальше простого счётчика утилизации станет не просто полезной, а абсолютно необходимой.