Подтвердить что ты не робот

Joblib Параллельный множественный процессор медленнее, чем одиночный

Я только начал использовать модуль Joblib, и я пытаюсь понять, как работает функция Parallel. Ниже приведен пример того, где распараллеливание приводит к более длительному времени автономной работы, но я не понимаю, почему. Мое время работы на 1 процессорное время составляло 51 сек против 217 секунд на 2 процессора.

Мое предположение заключалось в том, что запуск цикла в параллель будет копировать списки a и b для каждого процессора. Затем отправьте item_n на один процессор и item_n + 1 на другой процессор, выполните функцию, а затем запишите результаты обратно в список (по порядку). Затем возьмите следующие 2 элемента и так далее. Я, очевидно, что-то пропустил.

Это плохой пример или использование joblib? Я просто неправильно структурировал код?

Вот пример:

import numpy as np
from matplotlib.path import Path
from joblib import Parallel, delayed

## Create pairs of points for line segments
a = zip(np.random.rand(5000,2),np.random.rand(5000,2))

b = zip(np.random.rand(300,2),np.random.rand(300,2))

## Check if one line segment contains another. 
def check_paths(path, paths):
    for other_path in paths:
        res='no cross'
        chck = Path(other_path)
        if chck.contains_path(path)==1:
            res= 'cross'
            break
    return res

res = Parallel(n_jobs=2) (delayed(check_paths) (Path(points), a) for points in b)
4b9b3361

Ответ 1

Вкратце: я не могу воспроизвести вашу проблему. Если вы находитесь в Windows, вы должны использовать защитник для вашего основного цикла: документация joblib.Parallel. Единственная проблема, которую я вижу, - это много накладных расходов на копирование данных, но ваши цифры кажутся нереалистичными из-за этого.

В долгу, вот мои тайминги с вашим кодом:

На моем i7 3770k (4 ядра, 8 потоков) я получаю следующие результаты для разных n_jobs:

For-loop: Finished in 33.8521318436 sec
n_jobs=1: Finished in 33.5527760983 sec
n_jobs=2: Finished in 18.9543449879 sec
n_jobs=3: Finished in 13.4856410027 sec
n_jobs=4: Finished in 15.0832719803 sec
n_jobs=5: Finished in 14.7227740288 sec
n_jobs=6: Finished in 15.6106669903 sec

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

Вы должны заметить, что аргументы для каждой отдельной записи цикла копируются в процесс, выполняющий ее. Это означает, что вы копируете a для каждого элемента в b. Это неэффективно. Поэтому вместо этого вы получите доступ к глобальному a. (Parallel будет разветвлять процесс, копируя все глобальные переменные для вновь созданных процессов, поэтому a доступен). Это дает мне следующий код (с настройкой времени и основного цикла, так как документация joblib рекомендует:

import numpy as np
from matplotlib.path import Path
from joblib import Parallel, delayed
import time
import sys

## Check if one line segment contains another. 

def check_paths(path):
    for other_path in a:
        res='no cross'
        chck = Path(other_path)
        if chck.contains_path(path)==1:
            res= 'cross'
            break
    return res

if __name__ == '__main__':
    ## Create pairs of points for line segments
    a = zip(np.random.rand(5000,2),np.random.rand(5000,2))
    b = zip(np.random.rand(300,2),np.random.rand(300,2))

    now = time.time()
    if len(sys.argv) >= 2:
        res = Parallel(n_jobs=int(sys.argv[1])) (delayed(check_paths) (Path(points)) for points in b)
    else:
        res = [check_paths(Path(points)) for points in b]
    print "Finished in", time.time()-now , "sec"

Результаты синхронизации:

 n_jobs=1: Finished in 34.2845709324 sec
 n_jobs=2: Finished in 16.6254048347 sec
 n_jobs=3: Finished in 11.219119072 sec
 n_jobs=4: Finished in 8.61683392525 sec
 n_jobs=5: Finished in 8.51907801628 sec
 n_jobs=6: Finished in 8.21842098236 sec
 n_jobs=7: Finished in 8.21816396713 sec
 n_jobs=8: Finished in 7.81841087341 sec

Насыщенность теперь немного перемещена в n_jobs=4, которая является ожидаемым значением.

check_paths выполняет несколько избыточных вычислений, которые можно легко устранить. Во-первых, для всех элементов в other_paths=a строка Path(...) выполняется в каждом вызове. Предсчитайте это. Во-вторых, строка res='no cross' записывается в каждом повороте цикла, хотя она может меняться только один раз (с последующим перерывом и возвратом). Переместите линию перед контуром. Затем код выглядит следующим образом:

import numpy as np
from matplotlib.path import Path
from joblib import Parallel, delayed
import time
import sys

## Check if one line segment contains another. 

def check_paths(path):
    #global a
    #print(path, a[:10])
    res='no cross'
    for other_path in a:
        if other_path.contains_path(path)==1:
            res= 'cross'
            break
    return res

if __name__ == '__main__':
    ## Create pairs of points for line segments
    a = zip(np.random.rand(5000,2),np.random.rand(5000,2))
    a = [Path(x) for x in a]

    b = zip(np.random.rand(300,2),np.random.rand(300,2))

    now = time.time()
    if len(sys.argv) >= 2:
        res = Parallel(n_jobs=int(sys.argv[1])) (delayed(check_paths) (Path(points)) for points in b)
    else:
        res = [check_paths(Path(points)) for points in b]
    print "Finished in", time.time()-now , "sec"

с таймингами:

n_jobs=1: Finished in 5.33742594719 sec
n_jobs=2: Finished in 2.70858597755 sec
n_jobs=3: Finished in 1.80810618401 sec
n_jobs=4: Finished in 1.40814709663 sec
n_jobs=5: Finished in 1.50854086876 sec
n_jobs=6: Finished in 1.50901818275 sec
n_jobs=7: Finished in 1.51030707359 sec
n_jobs=8: Finished in 1.51062297821 sec

Сторона node на вашем коде, хотя я не выполнял ее цели, поскольку это не было связано с вашим вопросом, contains_path вернет только True if this path completely contains the given path. (см. документация). Поэтому ваша функция будет в основном всегда возвращать no cross с учетом случайного ввода.

Ответ 2

В дополнение к вышеприведенному ответу и для будущей ссылки в этом вопросе есть два аспекта, и недавние разработки joblib помогают в обоих.

Накладные расходы на параллельный пул. Проблема заключается в том, что создание параллельного пула является дорогостоящим. Здесь было особенно дорого, так как код, не защищенный " main", запускался в каждом задании при создании объекта Parallel. В последнем joblib (бета-версии) Parallel может использоваться как контекстный менеджер, чтобы ограничить количество времени создания пула и, следовательно, влияние этих накладных расходов.

Диспетчерские накладные расходы: важно иметь в виду, что отправка элемента цикла for имеет накладные расходы (намного больше, чем повторение цикла for без параллелизма). Таким образом, если эти отдельные вычислительные элементы будут очень быстрыми, эти издержки будут доминировать над вычислением. В последнем joblib joblib будет отслеживать время выполнения каждой работы и начинать группировать их, если они очень быстрые. Это сильно ограничивает влияние накладных расходов на отправку в большинстве случаев (см.