Как распараллелить этот цикл Python for при использовании Numba

я использую распределение Anaconda Python вместе с Numba, и я написал следующую функцию Python, которая умножает разреженную матрицу A (хранится в формате CSR) плотным вектором x:

@jit
def csrMult( x, Adata, Aindices, Aindptr, Ashape ):

    numRowsA = Ashape[0]
    Ax       = numpy.zeros( numRowsA )

    for i in range( numRowsA ):
        Ax_i = 0.0
        for dataIdx in range( Aindptr[i], Aindptr[i+1] ):

            j     = Aindices[dataIdx]
            Ax_i +=    Adata[dataIdx] * x[j]

        Ax[i] = Ax_i

    return Ax 

здесь A большой scipy разреженные матрицы,

>>> A.shape
( 56469, 39279 )
#                  having ~ 142,258,302 nonzero entries (so about 6.4% )
>>> type( A[0,0] )
dtype( 'float32' )

и x это numpy массив. Вот фрагмент кода, который вызывает выше функция:

x       = numpy.random.randn( A.shape[1] )
Ax      = A.dot( x )   
AxCheck = csrMult( x, A.data, A.indices, A.indptr, A.shape )

уведомления @jit-декоратор, который говорит Numba сделать компиляцию точно в срок для csrMult().

в моих экспериментах моя функция csrMult() о в два раза быстрее как scipy .dot() метод. Это довольно впечатляющий результат для Numba.

однако MATLAB все еще выполняет это умножение матрицы-вектора о 6 раз быстрее!--33--> чем csrMult(). Я считаю, что это потому, что MATLAB использует многопоточность при выполнении разреженного умножения матрицы-вектора.


вопрос:

как я могу распараллелить внешний for-цикл при использовании Numba?

у Numba был prange() функция, которая упростила распараллеливание смущающе параллельных for-петли. К сожалению, Numba больше не имеет prange() [на самом деле, это ложь, см. редактирования ниже]. Итак, каков правильный способ распараллеливания этого for-петли, что Numba это

2 ответов


Спасибо за ваши обновления quant, Даниэль.N [TB]; N > 10 и их немногочисленные accompanions, поэтому некоторые фрагменты опыта могут быть полезны для дальнейших просмотров.

предупреждение: Не ожидайте ужин бесплатно!--41-->

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

экономика является проблемой номер один. Закон Амдала, как она была первоначально сформулирована ген Amdahl, не принимать во внимание расходы [PAR]-процессы-сетапы + [PAR] - процессы-финализации и прекращения, которые действительно должны быть оплачены в каждом реальном мире реализация.

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

не стесняйтесь читать больше сообщений о накладных расходах-строгая формулировка закона Amdahl, если вы хотите лучше понять эту тему и предварительно рассчитают фактический "минимум"-подзадача-"в размере", для которых суммы-[PAR] - накладные расходы будут по крайней мере оправданы из реальных инструментов для введения параллельного разделения на подзадачи на N_trully_[PAR]_processes ( ни каких "просто"-[CONCURRENT], но правда-[PARALLEL] -- эти пути не равны ).


питон может получить дозу стероидов для увеличенного представления:

Python-отличная эко-система прототипирования, тогда как numba, numpy и другие скомпилированные расширения помогают значительно повысить производительность, чем родной, Gil-stepped python (co-)-обработка обычно обеспечивает.

здесь, вы пытаетесь применить numba.jit() организовать работу почти - бесплатно, просто его автоматизированный jit()-time lexical-analyser ( который вы бросаете свой код), который должен "понимать" вашу глобальную цель (что сделать ), а также предложить некоторые векторизации-трюки (как лучше соберите кучу CPU-инструкций для максимальной эффективности такого кода-выполнения).

это звучит легко, но это не так.

команда Трэвиса Олифанта сделала огромный прогресс!--37-- > on numba инструменты, но давайте будем реалистичными и справедливыми, чтобы не ожидать, что любая форма автоматизированного волшебства будет реализована внутри .jit() - lexer + code-analysis, при попытке преобразовать код и собрать более эффективный поток машинных инструкций для реализации цели задачи высокого уровня.

@guvectorize? Здесь? Серьезно?

из-за [PSPACE] калибровка, вы можете сразу забыть спросить numba как-то эффективно "набивать" GPU-движок данными, объем памяти, который находится позади размеров GPU-GDDR ( не говоря уже о слишком "мелких" размерах GPU-ядра для такой математически "крошечной" обработки, чтобы просто умножить, потенциально в [PAR], но и позже в сумме [SEQ] ).

(Re-)-загрузка GPU с данными занимает кучу времени. Если вы заплатили за это, задержки памяти в GPU не очень дружелюбны для"крошечной" экономики GPU-ядер-ваш GPU-SMX-код-выполнение будет придется платить ~ 350-700 [ns] просто за номером (скорее всего, автоматически не выровнен для лучшего коалесцированного повторного использования SM-cache в следующих шагах, и вы можете заметить, что вы никогда, позвольте мне повторить это, никогда не повторно использовать одну ячейку матрицы вообще, поэтому кэширование per-se не доставит ничего под этими 350~700 [ns] в ячейку матрицы ), а смарт чисто numpy-векторизованный код может обрабатывать матрично-векторное произведение менее чем 1 [ns] в клетку на даже самом большом [PSPACE]-следы.

это критерий для сравнения.

( профилирование лучше показать здесь жесткие факты, но принцип хорошо известен заранее, без проверки того, как переместить несколько TB данных на GPU-fabric, чтобы понять это самостоятельно. )


худшая из плохих новостей:

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


перечень решаемых проблем:

  • всегда лучше предварительно выделить вектор Ax = np.zeros_like( A[:,0] ) и передайте его как другой параметр в numba.jit()-скомпилированные части кода, чтобы избежать повторной оплаты дополнительных [PTIME,PSPACE]-затраты на создание (снова) новых распределений памяти ( тем более, если вектор подозревается в использовании внутри внешне организованного итеративного процесса оптимизации )
  • всегда лучше указать ( сузить универсальность, ради результирующей производительности кода)
    по крайней мере numba.jit( "f8[:]( f4[:], f4[:,:], ... )" ) - вызов директив интерфейса
  • всегда рассмотреть все numba.jit() - доступные параметры и их соответствующие значения по умолчанию (может изменить версию на версию) для вашей конкретной ситуации (отключение GIL и лучшее согласование целей с numba + аппаратные возможности всегда помогут в численно интенсивных частях код )

@jit(   signature = [    numba.float32( numba.float32, numba.int32 ),                                   #          # [_v41] @decorator with a list of calling-signatures
                         numba.float64( numba.float64, numba.int64 )                                    #
                         ],    #__________________ a list of signatures for prepared alternative code-paths, to avoid a deferred lazy-compilation if undefined
        nopython = False,      #__________________ forces the function to be compiled in nopython mode. If not possible, compilation will raise an error.
        nogil    = False,      #__________________ tries to release the global interpreter lock inside the compiled function. The GIL will only be released if Numba can compile the function in nopython mode, otherwise a compilation warning will be printed.
        cache    = False,      #__________________ enables a file-based cache to shorten compilation times when the function was already compiled in a previous invocation. The cache is maintained in the __pycache__ subdirectory of the directory containing the source file.
        forceobj = False,      #__________________ forces the function to be compiled in object mode. Since object mode is slower than nopython mode, this is mostly useful for testing purposes.
        locals   = {}          #__________________ a mapping of local variable names to Numba Types.
        ) #____________________# [_v41] ZERO <____ TEST *ALL* CALLED sub-func()-s to @.jit() too >>>>>>>>>>>>>>>>>>>>> [DONE]
 def r...(...):
      ...

Numba был обновлен и prange() работает теперь! (я отвечаю на свой собственный вопрос.)

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

давно (более 20 релизов!), Numba используется для поддержки идиома для записи параллели для циклов называется prange(). После майора рефакторинг базы кода в 2014 году эту функцию пришлось удалить, но это была одна из наиболее часто запрашиваемых функций Numba с тех пор. После Intel разработчики распараллелили массив выражения, они поняли, что возвращение prange будет достаточно легко

используя Numba версии 0.36.1, я могу распараллелить мою смущающе параллельную for-петли, используя следующий простой код:

@numba.jit(nopython=True, parallel=True)
def csrMult_parallel(x,Adata,Aindices,Aindptr,Ashape): 

    numRowsA = Ashape[0]    
    Ax = np.zeros(numRowsA)

    for i in numba.prange(numRowsA):
        Ax_i = 0.0        
        for dataIdx in range(Aindptr[i],Aindptr[i+1]):

            j = Aindices[dataIdx]
            Ax_i += Adata[dataIdx]*x[j]

        Ax[i] = Ax_i            

    return Ax

в моих экспериментах, распараллеливание for-loop заставил функцию выполняться примерно в восемь раз быстрее, чем версия, которую я опубликовал в начале моего вопроса, которая уже использовала Numba, но которая не была распараллелена. Более того, в моих экспериментах распараллеленная версия примерно в 5 раз быстрее команды Ax = A.dot(x) который использует разреженную функцию умножения матрицы-вектора scipy. Numba разгромил составляющей и у меня, наконец, есть Python разреженная матрица-векторная процедура умножения, которая так же быстро, как MATLAB.