Паралелізація циклу for-циклу в Python


35

Чи є в Python інструменти, схожі на парфор Матлаба? Я знайшов цю нитку , але це чотири роки. Я подумав, що, можливо, хтось тут може мати останній досвід.

Ось приклад типу речі, яку я хотів би паралелізувати:

X = np.random.normal(size=(10, 3))
F = np.zeros((10, ))
for i in range(10):
    F[i] = my_function(X[i,:])

де my_functionприймає ndarrayрозмір (1,3)і повертає скаляр.

Принаймні, я хотів би використовувати декілька ядер одночасно --- як parfor. Іншими словами, припустімо загальну систему пам'яті з 8 до 16 ядрами.


Багато результатів у Google. Вони здаються досить простими: blog.dominodatalab.com/simple-parallelization quora.com/What-is-the-Python-equivalent-of-MATLABs-parfor
Дуг Ліпінський,

Дякую, @ doug-lipinski. Ці приклади, як і інші, які я знайшов під час гуглінгу, мають деякі тривіальні обчислення на основі індексу ітерації. І вони завжди стверджують, що код "неймовірно простий". Мій приклад визначає масиви (виділяє пам'ять) поза фор-циклом. Я добре це роблю по-іншому; саме так я це роблю в Matlab. Хитра частина, яка, здається, перебирає ці приклади, - це отримання частини заданого масиву до функції всередині циклу.
Павло Григорович Костянтин

Відповіді:


19

Джобліб робить те, що ти хочеш. Основна схема використання:

from joblib import Parallel, delayed

def myfun(arg):
     do_stuff
     return result

results = Parallel(n_jobs=-1, verbose=verbosity_level, backend="threading")(
             map(delayed(myfun), arg_instances))

де arg_instancesперелік значень, для яких myfunобчислюється паралельно. Основним обмеженням є те, що myfunмає бути функція вершини. backendПараметр може бути або "threading"або "multiprocessing".

Ви можете передавати паралельні функції додаткові загальні параметри. Тіло myfunтакож може посилатися на ініціалізовані глобальні змінні, значення яких будуть доступні дітям.

Аргументи та результати можуть бути майже будь-якими з програмою потоків потоків, але результати повинні бути послідовними для багатопроцесорного бекенду.


Dask також пропонує подібний функціонал. Це може бути кращим, якщо ви працюєте з основними даними або ви намагаєтеся паралелізувати більш складні обчислення.


Я бачу нульову вартість для використання акумулятора, включаючи багатообробну обробку. Я б сказав, що joblib використовує його під кришкою.
Ксав’є Комбелле

1
Слід зазначити, що joblib не є магією, threadingбекенд страждає від вузького місця GIL, а multiprocessingбекенд приносить великі накладні витрати через серіалізацію всіх параметрів та повернених значень. Дивіться цю відповідь на деталі низького рівня паралельної обробки в Python.
Якуб Клінковський

Я не можу знайти комбінацію складності функції та кількості ітерацій, для яких joblib був би швидшим, ніж цикл for. Для мене він має таку ж швидкість, якщо n_jobs = 1, і набагато повільніше у всіх інших випадках
Aleksejs Fomins

@AleksejsFomins Паралелізм на основі ниток не допоможе коду, який не випускає GIL, але значна кількість дійсно, зокрема наукові дані чи числові бібліотеки. Інакше вам потрібна мути-обробка, Jobli підтримує обоє. Модуль багатопроцесорної обробки тепер також має паралель map, яку можна використовувати безпосередньо. Також якщо ви використовуєте mkl, складений numpy, він автоматично паралелізує векторизовані операції, не роблячи нічого. Число в Ananconda умовно включено mkl. Однак універсального рішення немає. Джобліб дуже суєтний, у 2015 році було менше отік
Даніель Малер

Дякую за вашу пораду. Я пам’ятаю, як намагався переробляти багатопроцесові раніше і навіть писати кілька дописів, тому що він не масштабував, як я очікував. Можливо, я мусив би поглянути ще один погляд
Алексей Фомінс

9

Що ви шукаєте, це Numba , який може автоматично паралелізувати цикл for. З їх документації

from numba import jit, prange

@jit
def parallel_sum(A):
    sum = 0.0
    for i in prange(A.shape[0]):
        sum += A[i]

    return sum

8

Не припускаючи щось особливе щодо my_functionвибору, multiprocessing.Pool().map()є гарною здогадкою для паралелізації таких простих циклів. joblib, dask, mpiОбчислення або numbaяк запропоновано в інших відповідях виглядає не приносить ніякої користі для таких випадків використання і додавати непотрібні залежності (підсумовувати їх надлишкова). Використання нарізки, запропонованої в іншій відповіді, навряд чи буде хорошим рішенням, оскільки ви повинні бути близькими до GIL-взаємодії вашого коду, або ваш код повинен робити в основному введення / виведення.

Це numbaмогло б бути гарною ідеєю для прискорення послідовного чистого коду python, але я вважаю, що це поза сферою питання.

import multiprocessing
import numpy as np

if __name__ == "__main__":
   #the previous line is necessary under windows to not execute 
   # main module on each child under windows

   X = np.random.normal(size=(10, 3))
   F = np.zeros((10, ))

   pool = multiprocessing.Pool(processes=16)
   # if number of processes is not specified, it uses the number of core
   F[:] = pool.map(my_function, (X[i,:] for i in range(10)) )

Однак є деякі застереження (але це не повинно впливати на більшість застосувань):

  • під Windows немає підтримки fork, тому інтерпретатор з основним модулем запускається при запуску кожної дитини, тому він може мати накладні витрати (адже це причина для if __name__ == "__main__"
  • Аргументи та результати функції my_function мариновані та відібрані, це може бути занадто великим накладним покриттям. Дивіться цю відповідь, щоб зменшити її https://stackoverflow.com/a/37072511/128629 . Це також робить непридатні об'єкти непридатними
  • my_functionне повинно залежати від спільних станів, таких як спілкування з глобальними змінними, оскільки стани не поділяються між процесом. чисті функції (функції в математичних сенсах) є прикладом функцій, які не поділяють стани

6

Моє враження parfor полягає в тому, що MATLAB інкапсулює деталі реалізації, тому він може використовувати як паралелізм спільної пам’яті (який саме ви хочете), так і паралелізм розподіленої пам’яті (якщо ви використовуєте розподілений обчислювальний сервер MATLAB ).

Якщо ви хочете паралелізму спільної пам’яті, і ви виконуєте якусь паралельну цикл завдань, багатопроцесорний стандартний бібліотечний пакет - це, мабуть, те, що ви хочете, можливо, з приємним фронтальним словом , як joblib , як згадується у публікації Дуга. Стандартна бібліотека не збирається згасати, і вона підтримується, тому це низький ризик.

Існують і інші варіанти, як-от паралельні можливості Parallel Python та IPython . Швидкий погляд на Паралельний Питон змушує мене думати, що це ближче до духу парфору, оскільки бібліотека інкапсулює деталі для розподіленого корпусу, але вартість цього полягає в тому, що ви повинні прийняти їх екосистему. Вартість використання IPython аналогічна; ви повинні прийняти спосіб здійснення IPython, який може вам і не бути вартим.

Якщо ви дбаєте про розподілену пам'ять, рекомендую mpi4py . Lisandro Dalcin чудово справляється, і mpi4py використовується в обгортках PETSc Python, тому я не думаю, що він скоро пройде. Як і багатопроцесорний, це низький (ер) рівень інтерфейсу для паралелізму, ніж парфор, але той, який, ймовірно, може тривати деякий час.


Дякую, @Geoff. Чи маєте ви досвід роботи з цими бібліотеками? Можливо, я спробую використовувати mpi4py на спільній машині пам'яті / багатоядерному процесорі.
Павло Григорович Костянтин

@PaulGConstantine Я успішно використовував mpi4py; це досить безболісно, ​​якщо ви знайомі з MPI. Я не використовував мультипроцесори, але рекомендував це колегам, які сказали, що це добре працює для них. Я також використовував IPython, але не паралелізм, тому я не можу говорити про те, наскільки добре він працює.
Джефф Оксберрі

1
Арон має чудовий підручник з mpi4py, який він підготував до курсу PyHPC на веб-сайті Supercomputing: github.com/pyHPC/pyhpc-tutorial
Метт Кнеплі

4

Перш ніж шукати інструмент "чорний ящик", який можна використовувати для паралельного виконання "загальних" функцій пітона, я б запропонував проаналізувати, як my_function()можна паралелізувати вручну.

По-перше, порівняйте час виконання з накладними петлями my_function(v)python for: [C] forПетлі Python досить повільні, тому час, витрачений на них, my_function()може бути незначним.

>>> timeit.timeit('pass', number=1000000)
0.01692986488342285
>>> timeit.timeit('for i in range(10): pass', number=1000000)
0.47521495819091797
>>> timeit.timeit('for i in xrange(10): pass', number=1000000)
0.42337894439697266

По-друге, перевірте, чи існує проста проста векторна реалізація my_function(v), яка не вимагає циклів:F[:] = my_vector_function(X)

(Ці два перші пункти досить тривіальні, вибачте, якщо я згадав їх тут лише для повноти.)

В- третіх , і найголовніше, по крайней мере , для реалізації CPython, щоб перевірити , є чи my_functionпроводить більшу частину часу , це всередині або зовні від глобальної блокування інтерпретатора або GIL . Якщо час проводиться поза GIL, слід використовувати threadingстандартний модуль бібліотеки . ( Ось приклад). До речі, можна було б написати my_function()як розширення C, щоб випустити GIL.

Нарешті, якщо my_function()GIL не випускає, можна використовувати multiprocessingмодуль .

Посилання: Документи Python про паралельне виконання та введення numpy / scipy при паралельній обробці .


2

Можна спробувати Юлію. Він досить близький до Python і має безліч конструкцій MATLAB. Переклад тут:

F = @parallel (vcat) for i in 1:10
    my_function(randn(3))
end

Це робить і випадкові числа паралельними і просто об'єднує результати в кінці під час зменшення. Для цього використовується багатопроцесорна робота (тому потрібно addprocs(N)додати процеси перед використанням, і це також працює на декількох вузлах HPC, як показано в цій публікації блогу ).

Ви також можете використовувати pmapзамість цього:

F = pmap((i)->my_function(randn(3)),1:10)

Якщо ви хочете паралелізм потоку, ви можете використовувати Threads.@threads(хоча переконайтеся, що алгоритм є безпечним для потоку). Перш ніж відкрити Джулію, встановіть змінну середовища JULIA_NUM_THREADS, тоді це:

Ftmp = [Float64[] for i in Threads.nthreads()]
Threads.@threads for i in 1:10
    push!(Ftmp[Threads.threadid()],my_function(randn(3)))
end
F = vcat(Ftmp...)

Тут я створюю окремий масив для кожного потоку, щоб таким чином вони не стикалися при додаванні до масиву, а потім просто об'єднували масиви згодом. Нитка ниток є досить новою, тому зараз є просто використання потоків, але я впевнений, що потокові скорочення та карти будуть додані так само, як це було для багатопроцесорної обробки.


0

я рекомендую використовувати паралельні та затримані функції бібліотеки joblib, використовуючи модуль "tempfile" для створення загальної пам'яті temp для величезних масивів, приклади та використання можна знайти тут https://pythonhosted.org/joblib/parallel.html

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.