Стрелка утилизации на мониторе уверенно стоит у 100%, а задача идет втрое медленнее, чем должна. Такие цифры регулярно встречаются на серверах с ускорителями, и причина почти всегда одна: стандартная метрика показывает занятость устройства по времени, но не то, как работа разложена по его внутренним блокам. Профилирование распределения вычислительной нагрузки отвечает на более тонкий вопрос: сколько мультипроцессоров действительно занято, равномерно ли ложится сетка потоков на чип, конкурируют ли процессы и потоки за одни и те же вычислительные блоки. В статье разобраны инструменты Linux, которые дают честную картину: nvidia-smi с поколоночным мониторингом, профилировщик Nsight Compute, трассировщик Nsight Systems, служба MPS, аппаратное разбиение MIG и сборщик метрик DCGM. Каждый раздел снабжен командами и числовыми примерами, чтобы выводы можно было проверить на своей машине за несколько минут.

Распределение блоков по мультипроцессорам и обманчивость стопроцентной утилизации

Чип ускорителя собран из десятков мультипроцессоров SM, каждый со своими вычислительными ядрами, регистрами и разделяемой памятью. У популярной серверной модели A100 таких блоков 108, на каждый приходится до 2048 одновременно живущих потоков, то есть 64 варпа по 32 нити. Планировщик оборудования раздает блоки запущенной сетки по свободным мультипроцессорам в момент старта ядра, и от количества блоков и их размера зависит вся картина загрузки. Метрика utilization.gpu устроена проще, чем кажется: она показывает долю времени, когда на устройстве выполнялся хотя бы один kernel, и ничего не сообщает о числе занятых SM. Ядро с единственным блоком подвесит стрелку на 100%, хотя реально трудится меньше одного процента площади чипа: один блок на 108 мультипроцессоров это примерно 0.9%.

Второй классический эффект это хвост сетки. Допустим, ядро содержит 109 блоков одинаковой длительности, а на каждом SM свободен один слот. Чип возьмет 108 блоков первой волной, оставшийся блок пойдет второй волной, общее время удвоится, а средняя загрузка упадет примерно до 50%. Лечение прямое: выравнивать число блоков до кратного количеству мультипроцессоров или дробить работу мельче, чтобы планировщику было чем заполнить паузы. Отсюда интерес к метрике achieved occupancy, показывающей среднюю долю живущих на SM варпов.

Быстрый замер нагрузки средствами nvidia-smi и наблюдение за процессами

Первый взгляд стоит делать дешевыми средствами, без запуска профилировщиков. Утилита nvidia-smi печатает выбранные поля с заданным интервалом и не требует ничего, кроме драйвера:

nvidia-smi --query-gpu=name,utilization.gpu,utilization.memory,power.draw,clocks.sm --format=csv -l 1

Поле utilization.gpu здесь снова про время, а не про площадь, поэтому рядом полезно держать частоту вычислительных ядер clocks.sm: просевшие частоты сразу объясняют, отчего проценты не конвертируются в скорость. Следующий уровень детализации дает режим dmon с колонками по подсистемам и таблица pmon с разбивкой по процессам:

nvidia-smi dmon -s pucvmet -d 1
nvidia-smi pmon -d 1

В dmon колонка sm показывает среднюю занятость вычислительного движка за интервал, mclk и pclk раскрывают частоты памяти и ядер, а ключ -s pucvmet включает питание, утилизацию, частоты, нарушения лимитов, память и ошибки. Утилита pmon идет дальше и раскладывает нагрузку по процессам: строки с pid, процентами sm и mem и именем команды показывают, кто именно держит ускоритель. Если два процесса пульсируют по очереди, это верный признак поочередного разделения контекстов, о котором подробнее пойдет речь в разделе про службу MPS.

Нагляднее всего видеть колонки вживую. Ниже типовой вывод dmon для чипа под сплошным вычислением:

# gpu  sm  mem  enc  dec  mclk  pclk  power  temp
# 0    97   6    0    0   1595  1410   252    63

Колонка sm около 97 при пустых кодировщиках enc и dec говорит о чисто вычислительной задаче, а мощность 252 Вт рядом с лимитом подсказывает, до упора ли выбран запас. В системах с несколькими ускорителями еще одну карту дает вывод nvidia-smi topo -m: таблица показывает родство каждого GPU с процессорами и связи NVLink, и по ней задачи распределяют так, чтобы соседние по шине ускорители не собирали всю работу в одну точку.

Заполнение мультипроцессоров в Nsight Compute и хвостовой эффект сетки

Nsight Compute показывает внутренности одного ядра: сколько варпов реально жило на мультипроцессорах, где узкое место и что мешает заполнению чипа. Запуск сводится к одной строке, отчет сохраняется рядом с исполняемым файлом:

ncu --set detailed -o wld_report ./matmul
ncu --import wld_report.ncu-rep

В разделе SpeedOfLight интересны два числа: sm__throughput.avg.pct_of_peak_sustained_elapsed как доля пикового темпа вычислений и sm__warps_active.avg.pct_of_peak_sustained_active, она же achieved occupancy. Разбор занятости удобно вести руками, опираясь на лимиты конкретного чипа. Возьмем A100 и ядро с блоками по 256 потоков и 32 КБ разделяемой памяти на блок. По памяти на мультипроцессор помещается 163 КБ, то есть 5 блоков; по регистрам при 32 регистрах на поток вышло бы 8, поскольку на SM доступно 65536 регистров. Ограничивает разделяемая память: 5 блоков по 256 потоков дают 1280 из 2048 возможных, это 62.5% теоретической занятости. Снижение расхода до 24 КБ поднимает предел до 6 блоков и занятость до 75%, и нередко это единственное изменение, после которого загрузка SM в отчете вырастает на десятки процентов.

Хвост сетки в отчете виден по числу волн: при 109 блоках и 108 SM вторая волна почти пустая, и выравнивание размера сетки до 108 или 216 блоков возвращает потерянные проценты без единой правки кода ядра.

Потоки CUDA и параллельная работа ядер внутри одного процесса

Внутри одного процесса распределением управляют потоки CUDA. По умолчанию все операции идут в нулевой поток и выстраиваются в очередь, а операции из потоков с явными дескрипторами выполняются одновременно, если ресурса хватает. Типовая схема это перекрытие передачи данных и вычислений через асинхронные вызовы cudaMemcpyAsync и запуск ядра с указанием потока. Проверить, что перекрытие действительно произошло, удобно трассировщиком Nsight Systems:

nsys profile -t cuda,nvtx -o wld_trace ./pipeline
nsys stats --report cuda_gpu_kern_sum wld_trace.nsys-rep

Временная шкала в отчете сразу показывает, шли ли ядра из разных потоков вплотную или с паузами. Арифметика простая: передача блока за 6 мс и ядро за 10 мс последовательно дают 16 мс, а при перекрытии около 11 мс, экономия почти треть без изменений самого кода ядра. Отчет cuda_gpu_kern_sum сортирует ядра по суммарному времени и честно называет главный источник нагрузки. Диапазоны NVTX вокруг этапов конвейера, расставленные вызовами nvtxRangePush и nvtxRangePop, превращают шкалу в читаемую карту этапов, где сразу видно, какой участок конвейера держит остальные в ожидании.

Отдельный нюанс касается нулевого потока: программы, собранные с ключом --default-stream per-thread, получают собственный нулевой поток на каждый поток процесса, и скрытая синхронизация между задачами исчезает. Для очередей с разной важностью есть приоритеты через cudaStreamCreateWithPriority, но они лишь подсказывают планировщику порядок выбора.

Совместное использование GPU процессами через службу MPS

Когда на один ускоритель претендуют несколько процессов, драйвер по умолчанию чередует контексты: каждый процесс на свой отрезок времени получает весь чип, а в моменты переключения оборудование простаивает. Служба MPS убирает это чередование и сводит контексты клиентов в один, позволяя ядрам из разных процессов выполняться на мультипроцессорах одновременно. Управляет запуском служба nvidia-cuda-mps-control, рабочим каналом служит каталог с пайпами, серверный процесс nvidia-cuda-mps-server стартует автоматически при первом клиенте:

export CUDA_MPS_PIPE_DIRECTORY=/tmp/nvidia-mps
export CUDA_MPS_LOG_DIRECTORY=/tmp/nvidia-mps-log
nvidia-cuda-mps-control -d

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

CUDA_MPS_ACTIVE_THREAD_PERCENTAGE=50 ./trainer

На чипах поколений Volta и новее на каждом SM 64 слота варпов, лимит 50% оставляет процессу 32 и освобождает половину площади чипа для соседа. Контроль такой же, как раньше: pmon показывает оба процесса с ненулевыми процентами одновременно, а суммарная колонка sm в dmon поднимается выше, чем при поочередной работе. Разница особенно заметна на коротких ядрах, где само переключение контекстов съедало заметную долю времени.

Список активных серверов печатает управляющая служба по запросу через стандартный ввод:

echo get_server_list | nvidia-cuda-mps-control

Журналы службы лежат в каталоге из переменной CUDA_MPS_LOG_DIRECTORY, там же ищут причины отказов клиентов.

Аппаратное разделение на инстансы через MIG и метрики DCGM

Виртуального разделения времени иногда мало, нужен физически гарантированный кусок. Режим MIG делит ускоритель на инстансы с собственными SM, кэшем и полосой памяти и поддержан на серверных чипах последних поколений. Работа начинается со списка доступных профилей, затем профили включаются и получают идентификаторы:

nvidia-smi mig -lgip
nvidia-smi mig -cgi 3,4 -C
nvidia-smi mig -lgi

Номера в команде создания берутся из вывода первой команды, каждая строка профиля там сопровождается идентификатором. Профили отличаются числом мультипроцессоров и объемом памяти: нарезка 1g отдает инстансу 14 SM, 2g это 28, 3g это 42, а старшая 7g забирает 98 SM из 108, при этом на одном чипе помещается до семи инстансов. Чем мельче нарезка, тем жестче изоляция и меньше вычислительного запаса у каждой задачи. Замер по инстансам удобнее всего собирать через DCGM, который видит каждый инстанс как отдельную сущность и отдает профильные метрики с высоким разрешением:

dcgmi discovery -l
dcgmi dmon -e 1001,1002,1003,1005 -d 1000 -c 60

Расшифровка полей: 1001 показывает долю времени активного вычислительного движка, 1002 долю тактов, когда активны SM, 1003 заполнение мультипроцессоров варпами, 1005 активность памяти DRAM. Классическая картина плохого распределения это 100% по движку при SM_ACTIVE около 20% и заполнении варпами около 15%: чип численно занят, физически спит. Для долгих наблюдений те же поля отдает экспортер dcgm-exporter в стандартные системы мониторинга, и график за сутки скажет о поведении больше, чем десяток точечных прогонов.

Энергопотребление и температура как косвенные признаки неравномерной нагрузки

Равномерно загруженный чип стабильно берет мощность и держит частоты, а рваная нагрузка видна по скачкам потребления и просевшим частотам после троттлинга:

nvidia-smi --query-gpu=power.draw,power.limit,temperature.gpu --format=csv -l 1
nvidia-smi -q -d PERFORMANCE

Смысловая проверка делается через энергию. Ядро за 12 мс при 250 Вт расходует 3 Дж. После выравнивания распределения то же ядро идет 8 мс при 240 Вт, то есть 1.92 Дж, минус 36% на тот же результат. Масштаб проверить легко: прогон на протяжении 10 минут при средней мощности 250 Вт потратит 150000 Дж, или около 0.042 кВт·ч, и статистика за сутки по таким прогонам сразу покажет, работает ли оборудование ночами вхолостую. Энергия на единицу работы честнее голых процентов и хорошо ловит мелкие паузы, которые не видны на секундной шкале. Раздел PERFORMANCE подсказывает, когда колебания нагрузки путают с перегревом: при срабатывании лимитов частоты падают первыми, и рост времени выполнения объясняется охлаждением, а не распределением.

Четыре признака того, что нагрузка разложена по чипу плохо, видны даже без глубокого анализа:
1. утилизация по nvidia-smi держится около 100%, а средняя занятость SM в Nsight Compute не поднимается выше 40%;
2. число блоков сетки не кратно количеству мультипроцессоров, и в конце ядра остается короткая волна из пары блоков;
3. на временной шкале Nsight Systems между ядрами видны провалы, а копирования не перекрываются с вычислениями;
4. суммарная занятость SM при двух процессах не растет после запуска службы MPS.

Дисциплина замеров здесь не менее важна, чем сами инструменты. Меняется одна переменная за раз, до и после фиксируются частоты, температура и режим охлаждения, иначе прирост спишется на случайный поток воздуха из стойки. Такой порядок превращает профилирование распределения нагрузки из разового расследования в постоянную практику, и ускоритель перестает простаивать за счет владельца.