Нужна помощь в понимании скорости передачи ядра на графическом процессоре (numba, cupy, cuda)

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

Я использую Cupy и Numba. В первый раз, когда я выполняю вызов функции, которая использует версию numy для графического процессора Cupy, это довольно медленно. Но второй раз это быстро.

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

Например, если я умножу два массива Cupy, которые уже спрятаны на GPU, я мог бы написать C= A*B

В какой-то момент перегрузка Cupy при * умножении должна быть закодирована на графическом процессоре, и для этого автоматически потребуются также циклы, которые разбивают его на блоки и потоки. Предположительно, этот код представляет собой ядро, которое передается в графический процессор. Я предполагаю, что в следующий раз, когда я позвоню C * D, GPU больше не нужно учить, что означает *, и поэтому все будет быстро.

Но в какой-то момент я бы предположил, что GPU необходимо очистить старый код, чтобы * или другие неиспользуемые в этот момент операции могли быть сброшены из памяти, и так позже, когда снова будет вызван вызов A * B, произойдет штраф за вовремя перекомпилировать его на GPU.

Или я так себе представляю. Если я прав, как я узнаю, когда эти ядра останутся или исчезнут?

Если я ошибаюсь, и это не так, как это работает, или есть какой-то другой медленный шаг (я предполагаю, что данные уже переносятся в массивы на GPU), тогда что это за медленный шаг и как все организовано так, чтобы платить это как можно меньше?

Я стараюсь избегать написания явных ядер управления потоками numba, как это делается в cuda++, но просто использую стандартные декораторы numba @njit, @vectorize, @stencil. Точно так же в Cupy я хочу просто работать на уровне обалденного синтаксиса, а не погружаться в управление потоками.

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

1 ответ

У меня пока нет полного ответа на это. Но до сих пор самая большая подсказка, которую я получил, пришла от чтения недокументированной на данный момент функции @cupy.fuse(), которая делает ее более понятной, чем документы @numba.jit, где оплачиваются затраты на запуск ядра. Я еще не нашел связь с контекстами, как рекомендовано @talonmies.

см. https://gist.github.com/unnonouno/877f314870d1e3a2f3f45d84de78d56c

Ключевой пример это

c = cupy.arange(4)
#@cupy.fuse()
def foo(x):
    return x+x+x+x+x

foo(.) будет в три раза медленнее с @cupy.fuse(), потому что каждый "+" включает загрузку ядра и освобождение ядра. Fusion объединяет все дополнения в одно ядро, так что выпуски и запуск платные. Для матриц размером менее 1 миллиона на типичном графическом процессоре 2018 add() настолько быстр, что время запуска и бесплатное время доминируют.

Я бы хотел найти документацию по @fuse. Например, разворачивает ли он внутренние функции, как @jit. Могу ли я добиться этого, складывая @jit и @fuse?

Тем не менее, я все еще нахожусь в неведении относительно того, когда расходы оплачиваются в нумбе.

Другие вопросы по тегам