Нужна помощь в понимании скорости передачи ядра на графическом процессоре (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?
Тем не менее, я все еще нахожусь в неведении относительно того, когда расходы оплачиваются в нумбе.