Numba Python - как эффективно использовать параллелизм? - PullRequest
0 голосов
/ 10 января 2020

Я пытался использовать Numba для ускорения вычислений с большим массивом. Я измерял скорость вычислений в GFLOPS, и она неизменно далеко не соответствует моим ожиданиям в отношении моего ЦП.

Мой процессор i9-9900k, который согласно тестам float32 должен поддерживать более 200 GFLOPS. В моих тестах я никогда не превышал 50 GFLOPS. Это работает на всех 8 ядрах.

На одном ядре я получаю около 17 GFLOPS, что (я считаю) составляет 50% от теоретической производительности. Я не уверен, что это невозможно, но проблема в том, что он плохо распространяется на многоядерные системы.

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

Вот пример кода с несколькими попытками написания быстрых функций. Операция, которую я тестирую, умножает массив на float32, а затем суммирует весь массив, то есть операция MA C.

Как получить лучшие результаты?

import os
# os.environ["NUMBA_ENABLE_AVX"] = "1"
import numpy as np
import timeit
from timeit import default_timer as timer
import numba
# numba.config.NUMBA_ENABLE_AVX = 1
# numba.config.LOOP_VECTORIZE = 1
# numba.config.DUMP_ASSEMBLY = 1
from numba import float32, float64
from numba import jit, njit, prange
from numba import vectorize
from numba import cuda

lengthY = 16 # 2D array Y axis
lengthX = 2**16 # X axis
totalops = lengthY * lengthX * 2 # MAC operation has 2 operations
iters = 100
doParallel = True


@njit(fastmath=True, parallel=doParallel)
def MAC_numpy(testarray):
    output = (float)(0.0)
    multconst = (float)(.99)
    output = np.sum(np.multiply(testarray, multconst))
    return output


@njit(fastmath=True, parallel=doParallel)
def MAC_01(testarray):
    lengthX = testarray.shape[1]
    lengthY = testarray.shape[0]
    output = (float)(0.0)
    multconst = (float)(.99)
    for y in prange(lengthY):
        for x in prange(lengthX):
            output += multconst*testarray[y,x]
    return output


@njit(fastmath=True, parallel=doParallel)
def MAC_04(testarray):
    lengthX = testarray.shape[1]
    lengthY = testarray.shape[0]
    output = (float)(0.0)
    multconst = (float)(.99)
    for y in prange(lengthY):
        for x in prange(int(lengthX/4)):
            xn = x*4
            output += multconst*testarray[y,xn] + multconst*testarray[y,xn+1] + multconst*testarray[y,xn+2] + multconst*testarray[y,xn+3]
    return output



# ======================================= TESTS =======================================

testarray = np.random.rand(lengthY, lengthX)

# ==== MAC_numpy ====
time = 1000
for n in range(iters):
    start = timer()
    output = MAC_numpy(testarray)
    end = timer()
    if((end-start) < time): #get shortest time
        time = end-start
print("\nMAC_numpy")
print("output = %f" % (output))
print(type(output))
print("fastest time = %16.10f us" % (time*10**6))
print("Compute Rate = %f GFLOPS" % ((totalops/time)/10**9))

# ==== MAC_01 ====
time = 1000
lengthX = testarray.shape[1]
lengthY = testarray.shape[0]
for n in range(iters):
    start = timer()
    output = MAC_01(testarray)
    end = timer()
    if((end-start) < time): #get shortest time
        time = end-start
print("\nMAC_01")
print("output = %f" % (output))
print(type(output))
print("fastest time = %16.10f us" % (time*10**6))
print("Compute Rate = %f GFLOPS" % ((totalops/time)/10**9))

# ==== MAC_04 ====
time = 1000
for n in range(iters):
    start = timer()
    output = MAC_04(testarray)
    end = timer()
    if((end-start) < time): #get shortest time
        time = end-start
print("\nMAC_04")
print("output = %f" % (output))
print(type(output))
print("fastest time = %16.10f us" % (time*10**6))
print("Compute Rate = %f GFLOPS" % ((totalops/time)/10**9))

1 Ответ

0 голосов
/ 10 января 2020

Q : Как мне получить лучше результаты?

1 st : Узнайте, как избежать бесполезной работы - вы можете прямо исключить ПОЛОВИНУ FLOP-ов , не говоря уже о половине всей ОЗУ -Указаны входы / выходы, каждый из которых стоит +100~350 [ns] за обратную запись

Из-за распределенного характера MUL и ADD ( a.C + b.C ) == ( a + b ).C, лучше сначала np.sum( A ) и только после этого MUL сумма по константе (с плавающей точкой).

#utput = np.sum(np.multiply(testarray, multconst)) # AWFULLY INEFFICIENT
output = np.sum(            testarray)*multconst #######################

2 и : Узнайте, как лучше выровнять данные по порядок обработки (повторное использование строки кэша дает вам ~100x быстрее повторное использование предварительно извлеченных данных. Не выравнивая векторизованный код по этим уже предварительно извлеченным побочным эффектам данных, просто позвольте вашему коду заплатить много время задержки доступа к ОЗУ, вместо умного повторного использования уже оплаченных блоков данных. Разработка рабочих блоков, выровненных по этому принципу, означает еще несколько SLOC, b но награды стоят того - кто получает ~100x более быстрые ЦП + ОЗУ бесплатно и прямо сейчас или о ~100x ускорении бесплатно, только если не писать плохо или наивно разработанные итераторы циклов?

3 rd : Узнайте, как эффективно использовать векторизованные (ориентированные на блоки) операции внутри numpy или numba кодовых блоков и избегать нажатия клавиши numba, чтобы тратить время на автоматический анализ вызова -подпись (вы платите дополнительное время за этот автоанализ для каждого вызова, в то время как вы разработали код и точно знали, какие типы данных будут там go, так зачем каждый раз платить за дополнительное время для автоматического анализа блок нумба называется ???)

4 th : Узнайте, где расширенный Закон Амдала , имея все соответствующие дополнения о затратах и ​​обработке, заложенных в игру, поддерживает ваш wi sh, чтобы получить ускорения, чтобы никогда не платить больше, чем вы получите (по крайней мере, оправдать дополнительные расходы ...) - оплачивая дополнительные расходы за не получить y вознаграждение возможно, но не оказывает положительного влияния на производительность вашего кода (скорее наоборот)

5 th : Узнайте, когда и как встроенные строки создаются вручную ) может сохранить ваш код, если шаги 1-4 хорошо изучены и регулярно выполняются с надлежащим мастерством (использование популярных COTS-фреймворков - это хорошо, однако они могут дать результаты после нескольких дней работы, в то время как специально разработанное вручную интеллектуальное решение ассемблерный код смог получить те же результаты примерно за 12 минут (!), а не за несколько дней без каких-либо трюков с GPU / CPU и т. д. c - да, это быстрее - просто не делая ни одного шага больше, чем требовалось для числовая обработка больших матричных данных)


...