Низкая производительность при одновременном вызове cudaMalloc с 2 GPU.

У меня есть приложение, в котором я распределяю вычислительную нагрузку между графическими процессорами в системе пользователя. По сути, для каждого графического процессора существует поток ЦП, который инициирует интервал обработки графического процессора при периодическом запуске основным потоком приложения.

Рассмотрим следующее изображение (созданное с помощью инструмента профилирования NVIDIA CUDA) в качестве примера интервала обработки GPU — здесь приложение использует один GPU.

введите здесь описание изображения

Как видите, большая часть времени обработки графического процессора потребляется двумя операциями сортировки, и я использую для этого библиотеку Thrust (thrust::sort_by_key). Кроме того, похоже, что Thrust::sort_by_key вызывает несколько cudaMalloc под капотом, прежде чем он начнет фактическую сортировку.

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

введите здесь описание изображения

В идеальном мире вы ожидаете, что интервал обработки 2 GPU будет ровно вдвое меньше, чем у одного GPU (поскольку каждый GPU выполняет половину работы). Как вы можете видеть, это частично не так, потому что cudaMallocs, кажется, занимают больше времени, когда они вызываются одновременно (иногда в 2-3 раза дольше) из-за какой-то проблемы с конкуренцией. Я не понимаю, почему это должно быть так, потому что пространство выделения памяти для двух графических процессоров полностью независимо, поэтому не должно быть общесистемной блокировки на cudaMalloc - блокировка для каждого графического процессора была бы более разумной.

Чтобы доказать свою гипотезу о том, что проблема связана с одновременными вызовами cudaMalloc, я создал до смешного простую программу с двумя потоками ЦП (для каждого графического процессора), каждый из которых несколько раз вызывает cudaMalloc. Сначала я запустил эту программу, чтобы отдельные потоки не вызывали cudaMalloc одновременно:

введите здесь описание изображения

Вы видите, что на выделение уходит ~ 175 микросекунд. Затем я запустил программу с одновременным вызовом потоков cudaMalloc:

введите здесь описание изображения

Здесь каждый вызов занимал ~538 микросекунд или в 3 раза больше, чем в предыдущем случае! Излишне говорить, что это сильно замедляет работу моего приложения, и понятно, что проблема только усугубится при использовании более двух графических процессоров.

Я заметил такое поведение в Linux и Windows. В Linux я использую драйвер Nvidia версии 319.60, а в Windows — версию 327.23. Я использую набор инструментов CUDA 5.5.

Возможная причина: в этих тестах я использую GTX 690. Эта карта, по сути, представляет собой 2 графических процессора типа 680, размещенных в одном блоке. Это единственная установка с несколькими графическими процессорами, которую я запускал, поэтому, возможно, проблема с cudaMalloc как-то связана с некоторой аппаратной зависимостью между двумя графическими процессорами 690?


person rmccabe3701    schedule 05.10.2013    source источник
comment
Обычной рекомендацией для высокопроизводительного кода является удаление операций malloc из любых циклов производительности. Я понимаю, что это не тривиальный вопрос, так как вы используете тягу. Существуют высокопроизводительные библиотеки сортировки, которые могут заменить строгую sort_by_key, что позволит вам заранее выполнять распределения и повторно использовать их для операций сортировки. CUB, b40c и MGPU — все это возможные варианты.   -  person Robert Crovella    schedule 05.10.2013
comment
Да, я изучил CUB и b40c (на сайте b40c говорится, что проект устарел). Прежде чем приступить к работе по устранению тяги, хотелось бы увидеть несколько сравнительных графиков между библиотеками. Не могли бы вы указать мне некоторые цифры производительности? Какую библиотеку посоветуете? ... Кажется, что тяга не очень высокая производительность, например, я уже заменил кучу вызовов тяги:: уменьшить и уменьшить_by_key с моими собственными ядрами - это сократило время обработки вдвое. Без шуток.   -  person rmccabe3701    schedule 05.10.2013
comment
Тяга на самом деле основана на конкретном варианте b40c (или раньше). Для эквивалентных тестовых случаев в моем тестировании не было большой разницы между b40c и MGPU. В одном тесте, который я провел, я сортировал только около 22 битов 32-битного значения. У MGPU была шкала, которую я мог поворачивать только для 22-битной сортировки, и при этом я наблюдал примерно 40-процентное ускорение по сравнению с тягой. Я не использовал CUB много. Если вы просмотрите эти ссылки, вы можете найти некоторые данные о производительности. Например, некоторые данные производительности MGPU здесь   -  person Robert Crovella    schedule 05.10.2013
comment
В случае, если это неясно, я предлагаю эти альтернативные библиотеки не потому, что они имеют более высокую производительность сортировки, чем тяга (хотя они могут быть, я не уверен, каковы будут результаты в вашем тестовом примере), а что они позволяют вариант разделение временных выделений данных, которые делает тяга, чтобы вы могли обрабатывать их один раз, заранее.   -  person Robert Crovella    schedule 05.10.2013


Ответы (2)


Я предварю это заявлением об отказе от ответственности: я не знаком с внутренностями драйвера NVIDIA, так что это несколько спекулятивно.

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

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

  • Вы можете сократить до нуля накладные расходы системных вызовов на выделение памяти, написав свой собственный распределитель памяти для устройства, который работает с блоком памяти, выделенным во время инициализации. Это позволит избавиться от всех накладных расходов на системные вызовы в каждом sort_by_key, но усилия по написанию собственного диспетчера пользовательской памяти нетривиальны. С другой стороны, он оставляет остальную часть вашего кода тяги нетронутой.
  • Вы можете переключиться на альтернативную библиотеку сортировки и самостоятельно управлять выделением временной памяти. Если вы делаете все распределение на этапе инициализации, стоимость одноразовых выделений памяти может быть амортизирована почти до нуля в течение жизни каждого потока.

В кодах линейной алгебры на основе CUBLAS с несколькими графическими процессорами, которые я написал, я объединил обе идеи и написал автономный диспетчер памяти устройства пользовательского пространства, который работает с единовременно выделенным пулом памяти устройства. Я обнаружил, что удаление всех накладных расходов на выделение памяти промежуточным устройствам дало полезное ускорение. В вашем варианте использования может помочь аналогичная стратегия.

person talonmies    schedule 05.10.2013

Подведем итоги проблемы и дадим возможное решение:

Конфликт cudaMalloc, вероятно, возникает из-за конкуренции на уровне драйвера (возможно, из-за необходимости переключения контекстов устройства, как предполагают talonmies), и можно было бы избежать этой дополнительной задержки в критических секциях производительности с помощью cudaMalloc-ing и временных буферов заранее.

Похоже, мне, вероятно, нужно реорганизовать мой код, чтобы я не вызывал под капотом какую-либо подпрограмму сортировки, которая вызывает cudaMalloc (в моем случае это Thrust::sort_by_key). В этом отношении многообещающей выглядит библиотека CUB. В качестве бонуса CUB также предоставляет пользователю параметр потока CUDA, что также может способствовать повышению производительности.

Подробнее об этом см. CUB (CUDA UnBound), эквивалентный функции Thrust::gather. переход от тяги к КУБ.

ОБНОВЛЕНИЕ:

Я отказался от вызовов push::sort_by_key в пользу cub::DeviceRadixSort::SortPairs.
Это сократило время обработки каждого интервала на миллисекунды. Кроме того, проблема конкуренции за несколько GPU была решена сама собой — разгрузка на 2 GPU сокращает время обработки почти на 50%, как и ожидалось.

person rmccabe3701    schedule 06.10.2013
comment
Было бы хорошо, если бы вы могли пройтись по этому и вашим старым вопросам CUDA и принять некоторые ответы, если вы считаете это уместным. Это убирает их из списка без ответа (мы активно стараемся, чтобы он был как можно короче), и это облегчает другим пользователям поиск с помощью поиска, если вы это сделаете. Спасибо. - person talonmies; 06.10.2013
comment
Ой, извините, я думал, что когда за ответ голосуют, он принимается. Я вернулся и принял кучу ответов на свои старые вопросы. Еще раз извините, я все еще немного новичок на этом сайте. - person rmccabe3701; 06.10.2013