python joblib & random walk - выполнение планирования [CONCURRENT] -процесса

1

Вот мой код python-3.6 для имитации одномерного отраженного случайного блуждания, используя модуль joblib для одновременного создания 400 реализаций среди K работников на кластерной машине Linux.

Однако я отмечаю, что время выполнения для K=3 хуже, чем для K=1, и что время выполнения для K=5 еще хуже!

Может ли кто-нибудь увидеть способ улучшить мое использование joblib?

from math import sqrt
import numpy as np
import joblib as jl
import os

K = int(os.environ['SLURM_CPUS_PER_TASK'])

def f(j):
    N = 10**6
    p = 1/3
    np.random.seed(None)
    X = 2*np.random.binomial(1,p,N)-1   # X = 1 with probability p
    s = 0                               # X =-1 with probability 1-p
    m = 0 
    for t in range(0,N):
        s = max(0,s+X[t])
        m = max(m,s)
    return m

pool = jl.Parallel(n_jobs=K)
W = np.asarray(pool(jl.delayed(f)(j) for j in range(0,400)))
W      
Теги:
performance
parallel-processing
joblib
parallelism-amdahl

2 ответа

3

способ улучшить мое использование joblib?

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

Тонкая joblib предварительной загрузки и размера партии joblib только начинает иметь смысл только после того, как joblib код получит производительность.

Некоторые усилия на этом, как показано ниже, показали core- ускорение ~ 8x для достижения еще pure- [SERIAL] времени выполнения ~ 217,000 [us] за одну ~ 1,640,000 [us] прогулку (вместо ~ 1,640,000 [us] за пункт, как сообщалось выше).

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

Это имеет смысл тогда и только тогда, когда:

  • если это возможно, чтобы избежать голодания процессора и
  • если не придется платить чрезмерные надбавки за распределенное задание -scheduling.

Возможно, длинная, но важная история о том, где производительность сохраняется или теряется
УПРАВЛЯЮЩЕЕ РЕЗЮМЕ

Может быть, вряд ли
лучшая награда доктору Джин Джин AMDAHL аргумент:

Внутренняя структура указанной выше задачи сильно [SERIAL]:

  • генерация случайных чисел является главным образом процессом pure- [SERIAL], благодаря PRNG-дизайну
  • Итератор 1E6 поверх 1D-вектора предварительно вычисленных шагов пьяного матроса pure- [SERIAL]

Да, "внешний" -свойник работы (400 повторений одного и того же процесса) может быть легко преобразован в "просто" - [CONCURRENT] (а не true- [PARALLEL], даже если профессора и гуру-гуру попробуйте сказать вам) планирование процесса, но дополнительные затраты на это ухудшаются, чем линейно добавляются во время выполнения, и учитывая, что часть [SERIAL] не была переработана по производительности, сеть -effect таких усилий может легко разрушить первоначальные благие намерения (QED выше, так как опубликованные времена выполнения выросли, начиная с 10:52, для K == 1, до ~ 13 минут даже для небольшого количества K).


Краткое тестирование показало, что вся эта задача может быть выполнена после использования стандартных инструментов python в режиме pure- [SERIAL] под < 1.45 [s] (вместо сообщенных ~ 12-13 минут) даже на довольно каменном, устаревшее архаичное настольное устройство (некоторый эффект в кеш-памяти был возможен, но скорее как случайная сторона -effect, чем преднамеренный HPC-мотивированный код-рефакторинг для производительности HPC-кластера):

u@amd64FX:~$ lstopo --of ascii
+-----------------------------------------------------------------+
| Machine (7969MB)                                                |
|                                                                 |
| +------------------------------------------------------------+  |
| | Package P#0                                                |  |
| |                                                            |  |
| | +--------------------------------------------------------+ |  |
| | | L3 (8192KB)                                            | |  |
| | +--------------------------------------------------------+ |  |
| |                                                            |  |
| | +--------------------------+  +--------------------------+ |  |
| | | L2 (2048KB)              |  | L2 (2048KB)              | |  |
| | +--------------------------+  +--------------------------+ |  |
| |                                                            |  |
| | +--------------------------+  +--------------------------+ |  |
| | | L1i (64KB)               |  | L1i (64KB)               | |  |
| | +--------------------------+  +--------------------------+ |  |
| |                                                            |  |
| | +------------++------------+  +------------++------------+ |  |
| | | L1d (16KB) || L1d (16KB) |  | L1d (16KB) || L1d (16KB) | |  |
| | +------------++------------+  +------------++------------+ |  |
| |                                                            |  |
| | +------------++------------+  +------------++------------+ |  |
| | | Core P#0   || Core P#1   |  | Core P#2   || Core P#3   | |  |
| | |            ||            |  |            ||            | |  |
| | | +--------+ || +--------+ |  | +--------+ || +--------+ | |  |
| | | | PU P#0 | || | PU P#1 | |  | | PU P#2 | || | PU P#3 | | |  |
| | | +--------+ || +--------+ |  | +--------+ || +--------+ | |  |
| | +------------++------------+  +------------++------------+ |  |
| +------------------------------------------------------------+  |
|                                                                 |
+-----------------------------------------------------------------+
+-----------------------------------------------------------------+
| Host: amd64FX                                                   |
| Date: Fri 15 Jun 2018 07:08:44 AM                               |
+-----------------------------------------------------------------+

< 1.45 [s]?
Зачем? Как? Вот и вся история о... (Благодаря усилиям HPC это могло бы быть еще более успешным в течение 1 [s])


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

Шаг №1:
Восстановите код, чтобы сделать что-то полезное.

учитывая, что код как есть, вообще нет никакой случайной ходьбы.

Зачем?

>>> [ op( np.random.binomial( 1, 1 /3,  1E9 ) ) for op in ( sum, min, max, len ) ]
[0, 0, 0, 1000000000]

Так,
код as-is производит довольно дорогой список a priori известных констант. Никакой случайности вообще. Проклятое округление питона целочисленного деления. :o)

>>> [ op( np.random.binomial( 1, 1./3., 1E9 ) ) for op in ( sum, min, max, len ) ]
[333338430, 0, 1, 1000000000]

Так что это исправлено.


Шаг № 2:
Понимать накладные расходы (и лучше всего любые скрытые блокировщики производительности)

Любая попытка создать экземпляр распределенного процесса (для каждого из инструкций K -amount joblib -spawned, вызывающих multiprocessing с подпроцессом, а не с потоком, backend) заставляет вас оплачивать стоимость. Всегда...

При условии,
ваш код будет получать дополнительный код [SERIAL] -add-on, который должен быть запущен, прежде чем какой-либо ... еще может быть только теоретический... ( 1/n_jobs ) split -effect.

Присмотритесь к "полезной" работе:

def f( j ):                                         # T0
    #pass;   np.random.seed( None )                 #  +      ~ 250 [us]
    prnGEN = np.random.RandomState()                #  +      ~ 230 [us]
    # = 2 * np.random.binomial( 1, 1./3., 1E6 ) - 1 #  +  ~ 465,000 [us]
    X =        prnGEN.binomial( 1, 1./3., 1E6 )     #  +  ~ 393,000
    X*= 2                                           #  +    ~ 2.940
    X-= 1                                           #  +    ~ 2.940                                  
    s = 0; m = 0                                    #  +        ~ 3 [us]
    for t in range( 0, int( 1E6 ) ):                #     ( py3+ does not allocate range() but works as an xrange()-generator
        s = max( 0, s + X[t] )                      #  +       ~ 15 [us] cache-line friendly consecutive { hit | miss }-rulez here, heavily ...
        m = max( m, s )                             #  +       ~  5 [us]
    return  m                                       # = ~ 2,150,000 [us] @  i5/2.67 GHz
#                                                   # = ~ 1,002,250 [us] @ amd/3.6  GHz

Для такого рода workpackage наилучшие ускорители для демонстрационных целей будут очевидны из неинтерпретированных, не содержащих GIL-потоков, потоковых и multiprocessing.Pool пакетов. -spawned Cython-ised code-packages, cdef -ed с директивой nogil ,Может ожидать такое выполнение кода примерно около = ~ 217,000 [us] за один pure- [SERIAL] Random Walk с шагом 1E6, когда 1E6 использовать пул узлов для выполнения кода с некоторой настройкой предварительной нагрузки, чтобы не дать им голодать.Тем не менее, все предупреждения преждевременной оптимизации являются обоснованными и действительными в этом упрощенном контексте, и для достижения результата профессионального уровня необходимо использовать надлежащую инженерную практику.

Некоторые инструменты могут помочь вам увидеть, до уровня сборки, сколько фактически добавленных инструкций, любым соответствующим элементом синтаксиса-конструктора языка высокого уровня (или параллелизмом/параллелизацией #pragma masquerades), чтобы "обонять" эти надстройки, расходы, которые будут выплачиваться во время окончательного выполнения кода:

Изображение 174551

Учитывая эти дополнительные затраты на обработку, выполняется "маленький" - (тонкий) -amount "внутри" "всего" -concurrently (остерегайтесь, а не автоматически [PARALLEL] -scheduling), они добавляют - Затраты могут заставлять вас платить больше, чем вы могли бы получить по расколу.

Блокаторы:

Любая добавочная связь/синхронизация может еще больше разрушить теоретический ускорение потока-кода-исполнения. Замки, исключение GIL, если не использовать поточно-бэкенд, семафоры, сокеты, совместное использование и др., Являются обычными блокаторами.

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


Следующий шаг?

Подробнее об Amdahl Law, лучшей в современной версии с повторной формулировкой, где в режиме "Накладные расходы" добавлены как накладные расходы + накладные расходы, так и обработка атомарности для практической оценки реалистичных ограничений ускорения

Далее: измерьте свои чистые затраты времени на ваш код, и вы косвенным образом добавляете дополнительные затраты на установку + накладные расходы на завершение работы в вашей системе in-vivo.

def f( j ):
    ts = time.time()
    #------------------------------------------------------<clock>-ed SECTION
    N = 10**6
    p = 1./3.
    np.random.seed( None )                    # RandomState coordination ...
    X = 2 * np.random.binomial( 1, p, N ) - 1 # X = 1 with probability p
    s = 0                                     # X =-1 with probability 1-p
    m = 0 
    for t in range( 0, N ):
        s = max( 0, s + X[t] )
        m = max( m, s )
    #------------------------------------------------------<clock>-ed SECTION
    return ( m, time.time() - ts )            # tuple

Для учебных занятий в классе я успешно распараллеливал свой случайный код прохода, используя специальные модули внутри R, Matlab, Julia & Stata. ("Успешно", я имею в виду, что в изобилии ясно, что 20 рабочих выполняют по меньшей мере в 10 раз больше работы, чем один работник за тот же промежуток времени.) Является ли такое внутреннее распараллеливание невозможным в Python?

Ну, последнее замечание, похоже, вызывает какие-то неудобства для людей, которые пытались помочь, и кто привел причины, почему опубликованный код, как есть, работает так, как было замечено. Это был не наш выбор, чтобы определить стратегию обработки именно таким образом, не так ли?

Так,
снова.
Учитывая первоначальное решение было
используйте инструмент python-3.6 + joblib.Parallel() + joblib.delayed(), просто Alea Iacta Est...

Что могло бы работать (как указано) для {R | MATLAB | MATLAB | Julia | Юлия | Stata } simply does not mean that it will work the same way in GIL-stepped, the less joblib.Parallel() -spawned eco-systems. Stata} просто не означает, что он будет работать одинаково в GIL-ступенчатой, тем меньше joblib.Parallel() -spawned.

Первая стоимость будет ВСЕГДА платить за joblib.Parallel() -spawned задание - это стоимость перестройки целой, 1:1 копии текущего состояния интерпретатора python. Учитывая, что текущее состояние содержит больше объектов-экземпляров, чем урезанный MCVE-код (как было продемонстрировано для MCVE-скрипта, как показано в @rth), весь многократно воспроизводимый образ памяти сначала необходимо скопировать + перевезти + реконструировать на все узлы распределенной обработки в соответствии с управляемым SLURM кластером-следствием, что все затраты на добавочное (непроизводительное) накладное время. Если вы сомневаетесь, добавьте несколько nump-массивов размера GB в состояние интерпретатора python и поместите измеренные временные метки для соответствующих вычислений продолжительности в первую и последнюю ячейки массива и, наконец, return ( m, aFatArray ). Общее время выполнения будет скачкообразно увеличиваться, так как исходная копия 1:1 и возвращаемый путь должны будут перемещать гораздо больший объем данных там и обратно (опять же, для получения подробной информации о связанных с добавлением дополнительных расходах, размещенные во многих местах здесь, включая шаблоны для систематического бенчмаркинга соответствующих дополнительных затрат).

Именно это и послужило основанием для того, чтобы рекомендовать вид O/P действительно измерять эффективный объем вычислительного времени ("полезная" часть элементарного аргумента стоимости/выгоды), что и дешево, чтобы попасть в тривиализованный эксперимент, который будет показать масштаб, сумму и фактическую долю полезной работы внутри "удаленной" -executed эффективной вычислительной полезной нагрузки (см. предложенную выше модификацию кода, которая возвращает значения так, чтобы W[:][1] расскажут о фактических "сетевых" затратах на вычисление " полезной работы ", затраченных на время эффективной работы компьютера, после того, как они наконец-то появились и активированы в соответствующую "удаленную" систему управления кодом (s) (здесь, в виде бинарных полномасштабных реплик исходного joblib.Parallel() -spawned исходного интерпретатора python), тогда как поток времени между началом и концом выполнения основного кода показывает фактические затраты - здесь единовременно затраченное время, т.е. включая все "удаленные" -процессы-экземпляры (-ы) + все соответствующие рабочие пакеты (CN014) + все "удаленные" -процессы-завершения.


Последнее замечание для тех,
которые не тратят время на чтение
о проблемах, связанных с источником случайности:

Любая хорошая практика скорее избежит логики блокировки, скрытой за "общей случайностью". Лучше использовать индивидуально настроенные источники PRNG. Если вы заинтересованы или нуждаетесь в сертифицируемой надежности PRNG, не стесняйтесь читать об этом в обсуждении здесь

2

@user3666197 написал очень хороший ответ о накладных расходах, с большим количеством выделенного жирным шрифтом;) Однако я хочу обратить ваше внимание на то, что при запуске кода с K = 1 вы выполняете только одно случайное блуждание. С K = 3 или 5 вы одновременно выполняете 3 или 5 случайных блужданий (кажется). Поэтому вам нужно умножить время выполнения K = 1 на 3 или 5, чтобы получить время выполнения, которое требуется для выполнения той же самой работы. Я предполагаю, что эта продолжительность будет намного больше, чем вы.

Ну, чтобы дать полезный ответ, а не только примечание (OP в комментариях). Кажется, что multiprocessing модуль - лучший выбор. Здесь ваш код

from math import sqrt
import numpy as np

from multiprocessing import Pool
import os

K = int(os.environ['NTASK'])


def f(j):
    N = 10**6
    p = 1./3.
    np.random.seed(None)
    X = 2*np.random.binomial(1,p,N)-1   # X = 1 with probability p
    s = 0                               # X =-1 with probability 1-p
    m = 0 
    for t in range(0,N):
        s = max(0,s+X[t])
        m = max(m,s)
    return m

pool = Pool(processes=K) 
print pool.map(f, xrange(40))

и производительность

$ time NTASK=1 python stof.py                                                                                                
[21, 19, 17, 17, 18, 16, 17, 17, 19, 19, 17, 16, 18, 16, 19, 22, 20, 18, 16, 17, 17, 16, 18, 18, 17, 17, 19, 17, 19, 19, 16, 16, 18, 17, 18, 18, 19, 20, 16, 19]

real    0m30.367s
user    0m30.064s
sys     0m 0.420s

$ time NTASK=2 python stof.py                                                                                                
[18, 16, 16, 17, 19, 17, 21, 18, 19, 21, 17, 16, 15, 25, 19, 16, 20, 17, 15, 19, 17, 16, 20, 17, 16, 16, 16, 16, 17, 23, 17, 16, 17, 17, 19, 16, 17, 16, 19, 18]

real    0m13.428s
user    0m26.184s
sys     0m 0.348s

$ time NTASK=3 python stof.py 
[18, 17, 16, 19, 17, 18, 20, 17, 21, 16, 16, 16, 16, 17, 22, 18, 17, 15, 17, 19, 18, 16, 15, 16, 16, 24, 20, 16, 16, 16, 22, 19, 17, 18, 18, 16, 16, 19, 17, 18]

real    0m11.946s
user    0m29.424s
sys     0m 0.308s

$ time NTASK=4 python stof.py
[16, 19, 17, 16, 19, 17, 17, 16, 18, 22, 16, 21, 16, 18, 15, 16, 20, 17, 22, 17, 16, 17, 17, 20, 22, 21, 17, 17, 16, 17, 19, 16, 19, 16, 16, 18, 25, 21, 19, 18]

real    0m 8.206s
user    0m26.580s
sys     0m 0.360s
  • 0
    Отрицательный, сэр. В любом случае, def -ed процесс петля всегда точно 1E6 раз «внутри» любой из joblib не -spawned 400 -walks, независимо от того , как они получили , наконец , распределялись между K == { 1, 3, 5, ... } пути выполнения кода. Плюс, конечно, Oveheads имеют значение здесь :o)
  • 0
    @ user3666197, конечно, я с тобой полностью согласен! Затраты, а в случае OP и время распространения шлама имеют значение. Но смешно думать, что, когда мы распределяем процессы, мы ожидаем, что за это же время будет выполнено только умножение работы. Насколько я помню, в Библии параллельного программирования DBPP есть очень приятное замечание об этом предположении.
Показать ещё 8 комментариев

Ещё вопросы

Сообщество Overcoder
Наверх
Меню