Python joblib - Запуск параллельного кода в параллельном коде - PullRequest
1 голос
/ 17 июня 2020

Я работаю над проектом, который я намерен сделать более эффективным, используя параллельную функцию с joblib с общей памятью.

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

Мне было интересно, можно ли это сделать в Python / joblib.

Изменить: 2020-06-19

Как уже упоминал другой пользователь, я должен пояснить, что в моем коде я хотел распараллелить. По сути, у меня есть массив 3D numpy, представляющий некоторое физическое пространство, которое я заполняю большим количеством усеченных гауссианов (влияющих только на конечное количество элементов). Не было обнаружено, что полная векторизация особенно ускоряет код из-за того, что узким местом является доступ к памяти, и я хотел попробовать распараллеливание, так как я перебираю все ith -гауссовские центры и добавляю его вклад в общую поле. (Эти циклы будут в некоторой степени разделять переменные). Параметр c исследование общей эффективности проекта в целом в отношении неуказанного метри c. Таким образом, эти циклы будут полностью независимыми.

Измененная выдержка из внутреннего l oop размещена здесь . К сожалению, похоже, что это не увеличивает производительность, а в случае, когда я не разбиваю список гауссовых центров на два массива для каждого ядра, это еще хуже, и в настоящее время я исследую это.

import numpy as np
import time
from joblib import Parallel, delayed, parallel_backend
from extra_fns import *

time.perf_counter()
nj = 2
set_par = True
split_var = True

# define 3d grid
nd = 3
nx = 250
ny = 250
nz = 250
x = np.linspace(0, 1, nx)
y = np.linspace(0, 1, ny)
z = np.linspace(0, 1, nz)

# positions of gaussians in space
pgrid = np.linspace(0.05, 0.95 , 20)
Xp, Yp, Zp = np.meshgrid(pgrid,pgrid,pgrid)
xp = Xp.ravel()
yp = Yp.ravel()
zp = Zp.ravel()
Np = np.size(xp)
s = np.ones(Np) # intensity of each gaussian
# compact gaussian representation
sigma = x[1]-x[0]
max_dist = sigma*(-2*np.log(10e-3))

# 3D domain: 
I = np.zeros((ny, nx, nz))
dx = x[1] - x[0]
dy = y[1] - y[0]
dz = z[1] - z[0]


dix = np.ceil(max_dist/dx)
diy = np.ceil(max_dist/dy)
diz = np.ceil(max_dist/dz)

def run_test(set_par, split_var, xp, yp, zp, s):
    def add_loc_gaussian(i):
        ix = round((xp[i] - x[0]) / dx)
        iy = round((yp[i] - y[0]) / dy)
        iz = round((zp[i] - z[0]) / dz)
        iix = np.arange(max(0, ix - dix), min(nx, ix + dix), 1, dtype=int)
        iiy = np.arange(max(0, iy - diy), min(ny, iy + diy), 1, dtype=int)
        iiz = np.arange(max(0, iz - diz), min(nz, iz + diz), 1, dtype=int)
        ddx = dx * iix - xp[i]
        ddy = dy * iiy - yp[i]
        ddz = dz * iiz - zp[i]
        gx = np.exp(-1 / (2 * sigma ** 2) * ddx ** 2)
        gy = np.exp(-1 / (2 * sigma ** 2) * ddy ** 2)
        gz = np.exp(-1 / (2 * sigma ** 2) * ddz ** 2)
        gx = gx[np.newaxis,:, np.newaxis]
        gy = gy[:,np.newaxis, np.newaxis]
        gz = gz[np.newaxis, np.newaxis, :]
        I[np.ix_(iiy, iix, iiz)] += s[i] * gy*gx*gz

    if set_par and split_var: # case 1
        mp = int(Np/nj) # hard code this test fn for two cores
        xp_list = [xp[:mp],xp[mp:]]
        yp_list = [yp[:mp],yp[mp:]]
        zp_list = [zp[:mp],zp[mp:]]
        sp_list = [s[:mp],s[mp:]]

        def core_loop(j):
            xpt = xp_list[j]
            ypt = yp_list[j]
            zpt = zp_list[j]
            spt = sp_list[j]

            def add_loc_gaussian_s(i):
                ix = round((xpt[i] - x[0]) / dx)
                iy = round((ypt[i] - y[0]) / dy)
                iz = round((zpt[i] - z[0]) / dz)
                iix = np.arange(max(0, ix - dix), min(nx, ix + dix), 1, dtype=int)
                iiy = np.arange(max(0, iy - diy), min(ny, iy + diy), 1, dtype=int)
                iiz = np.arange(max(0, iz - diz), min(nz, iz + diz), 1, dtype=int)
                ddx = dx * iix - xpt[i]
                ddy = dy * iiy - ypt[i]
                ddz = dz * iiz - zpt[i]
                gx = np.exp(-1 / (2 * sigma ** 2) * ddx ** 2)
                gy = np.exp(-1 / (2 * sigma ** 2) * ddy ** 2)
                gz = np.exp(-1 / (2 * sigma ** 2) * ddz ** 2)
                gx = gx[np.newaxis, :, np.newaxis]
                gy = gy[:, np.newaxis, np.newaxis]
                gz = gz[np.newaxis, np.newaxis, :]
                I[np.ix_(iiy, iix, iiz)] += spt[i] * gy * gx * gz

            for i in range(np.size(xpt)):
                add_loc_gaussian_s(i)

        Parallel(n_jobs=2, require='sharedmem')(delayed(core_loop)(i) for i in range(2))

    elif set_par: # case 2
        Parallel(n_jobs=nj, require='sharedmem')(delayed(add_loc_gaussian)(i) for i in range(Np))

    else: # case 3
        for i in range(0,Np):
            add_loc_gaussian(i)

run_test(set_par, split_var, xp, yp, zp, s)
print("Time taken: {} s".format(time.perf_counter()))

1 Ответ

1 голос
/ 18 июня 2020

"... выполнимо в Python / joblib ..."

Нет проблем с концептуальным замыслом, еще ...

"... Я намерен сделать более эффективным ..."

это самая сложная часть истории.


Почему?

Микрооперация процессора NOP (ничего не делать ) занимает ~ 0.1 [ns] в 2020 / 2H.

Микрооперации ЦП занимают примерно ~ 0.3 [ns] ADD/SUB, ~ 10 [ns] DIV в 2020 / 2H.

ЦП может иметь более одного ядра, а архитектуры CIS C могут управлять парой аппаратных потоков на каждом из таких ядер ЦП.

enter image description here

ЦП может развиваться, будет развиваться, но не будет делать никаких магических c чехарда "прыжков" за пределы реальности ограничений, введенных в игру l aws физикой. Никогда.

Планировщик O / S может запланировать CPU для чередования гораздо большего количества программных потоков (потоков исполнения кода), поскольку это чередование выполнения кода генерирует для нас, медленное, с визуальным отображением коры головного мозга примерно 25 Гц. понимание выборки, использование не более чем одного (голосового) или двуручного «устройства» ввода, иллюзия многозадачной операционной системы, но всей такой работы достаточно (нет гарантий для работы не в реальном времени (HRT) systems) помещены в несколько пар потоков CPU-core.

CPU может обеспечить наиболее эффективную обработку, если вычислительные задачи не чередуются сильно. Чем меньше, тем лучше.

ЦП будет в такой "компактной" оркестровке рабочего потока оставаться в пределах примерно ~ 0.3 ~ 10 [ns] на uop (аппаратная машинная инструкция ЦП) и будет лучше вычислять, если не будет никуда обращаться за данными но в свои собственные аппаратные регистры (выборка данных из кэша L1 «стоит» ~ 0.5 [ns], тогда как L2 ~ 8x дороже, L3 ~ 40x дороже а ОЗУ может go где угодно от ~70 .. 3++ [ns] для выборки данных). Таким образом, выполнение чередующихся процессов сопряжено с большими накладными расходами только для восстановления данных, многократно предварительно извлеченных из дорогостоящей ОЗУ, в менее дорогие кэш-память L3, L2 и L1 (просто возмещая затраты в размере ~ 300 ~ 350 [ns] каждый раз часть данных должна быть получена повторно, поскольку выполнение чередующегося процесса не сохраняет предварительно выбранные данные после того, как планировщик удалил этот поток из ядра процессора, чтобы освободить пространство-время для выполнения другого потока в очереди планировщика).

ЦП может делать все возможное, если не дожидается данных из ОЗУ (каналы памяти и узкие места ввода-вывода известны HP C -efficiency / Враги из-за нехватки ресурсов процессора ).


Этих аппаратных накладных расходов недостаточно «достаточно» , вам придется заплатить намного больше:

Python / joblib.Parallel()delayed() конструктор тривиально набирать, не так для точной настройки производительности для достижения максимальной эффективности.

Использование значения по умолчанию njobs (или любой простой ручной настройки) может и чаще всего снижает фактическая эффективность способа обработки в пределах производительности CPU-оборудования.

Существуют ненулевые дополнительные затраты , которые порождены joblib процессами должен заплатить. Во всех случаях они оплачивают дополнительные затраты на аппаратное обеспечение ЦП за каждый элемент данных, повторно извлеченный из ОЗУ в (теперь планировщик O / S повторно выбран) кэш L3 / L2 / L1 ядра ЦП (эти ~ 3++ [ns] сотни наносекунд), плюс он "разделяет" слабо приоритетную долю времени выполнения кода ядра ЦП (подробные сведения о настройках максимальной производительности / эффективности см. в параметризации O / S и свойствах планировщика)
и
последнее, но не менее важное
существуют огромные (в масштабе ~ hundreds of [us] if not [ms]) дополнительные затраты на создание экземпляра процесса, параметры подписи вызовов между процессами ' передача (считайте затраты SER / DES (часто pickle.dumps() / pickle.loads()) на преобразование данных параметров + Процесс-2-Процесс обмена сжатыми данными ... (время, время, время ...) ... , обратная передача данных результатов процесса (если есть), то есть снова SER / DES-конвейер + P2P-коммуникация ... (время, время, время ... ) ... плюс дополнительные затраты на завершение процесса.

Выполнение всего этого в любом месте, близком к пределу максимальной производительности аппаратного обеспечения ЦП, - это всегда сложно , больше в расслабленной и разнообразной экосистеме, где Python -GIL-lock ограниченное выполнение кода + joblib.Parallel() -процессы + Cython-ised модули (где вам не нужно комфортно контролировать / настраивать фактическое количество их порожденных под -процессы, не так ли?) сосуществуют, и это уже «эффективность» -tuning wild mix разрешается работать внутри обычного, ориентированного на пользователя MMI уровня COTS O / S.


Если все вышеперечисленное было легко "проглотить" , дополнительные затраты на совместное использование будут следующими:

Пока существуют протоколы с общими переменными, я примет все необходимые меры, чтобы не платить огромную надстройку для выполнения кода n затраты на их «использование».

Кто будет платить огромные расходы, чтобы «арендовать» Rolls-Royce только для ленивой поездки в школу в 9:00 утра и для возвращения иногда ближе к вечеру? выполнимая, но чрезвычайно дорогая «стратегия» . Есть надежные способы избежать общих переменных, и нулевое совместное использование является обязательным для любого программного обеспечения HP C, которое стремится к максимальной производительности с учетом эффективности.


«Я работаю над проектом ...»

Оплата счета только после XY-[man*months] затраченных усилий может быть слишком поздно:

Проанализировать обработку -стратегия и все фактические надстройки априори стоят решения.

Поздние сюрпризы - самые дорогие.

Даже оригинал, накладные расходы- Наивность и атомарность работы, игнорирующая закон Амдала , показывает, что существует главный предел - закон убывающей отдачи, который вы никогда не сможете обойти. И это были дополнительные затраты без учета optimisti c модели.

enter image description here

Реальность работает против вашей воли, чтобы получить улучшенная производительность. Тем более, что дополнительные затраты на макросопи c (связанные с многопроцессорной обработкой) возьмут (и очень скоро станут) доминирующее положение. Добавление протоколов обмена данными с совместно используемыми переменными снижает эффективность на много порядков (не только ~2 затраты на задержку добавляются для повторной выборки из кэша / ОЗУ, но и затраты на межпроцессную повторную синхронизацию «блокируют» свободный поток наиболее эффективной обработки на базе ядра ЦП, поскольку зависимости от других процессов, не связанных с ядром ЦП, вызывают состояния блокировки, похожие на барьеры, когда состояние совместно используемой переменной проверяется / повторно распространяется, чтобы поддерживать общесистемную согласованность ... ценой потери времени и эффективности превратится в крушение havo c ... только для простоты синтаксического сахара Python разделяемой переменной "комфорта" .


Возможно, вы захотите прочитать подробнее об этом,
возможно, с примерами кода:

Если вас интересует дальнейшее чтение joblib & изменено - Закон Амдала влияет на , не стесняйтесь погружаться. Дьявол скрыт в деталях. Как всегда.

...