Как реализовать распараллеливающий данные (multiprocessing.Pool) декоратор в python? - PullRequest
0 голосов
/ 29 марта 2020

Описание проблемы

TL; DR

Как реализовать декораторы, не нарушающие процесс засолки ?

Цель

multiprocessing.Pool может использоваться для разделения данных и их распределения по процессам для распараллеливания данных данной функции. Я хотел бы использовать такой подход в декораторе для удобного параллелизации данных. Декоратор обычно будет выглядеть следующим образом:

from multiprocessing import Pool
from functools import partial, wraps

def deco_data_parallel(func):
    @wraps(func)
    def to_parallel(arg, **kwargs):
        part_func = partial(func, **kwargs)
        tot = 0
        with Pool() as p:
            for output in p.imap_unordered(part_func, arg):
                tot += output
        return tot
    return to_parallel

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

  • arg - это итерация, которая может быть разбита на куски
  • Аргументы исправления должны называться аргументами ключевых слов

Вот пример предполагаемого использования:

@deco_data_parallel
def compute(data, arg1, arg2):
    return data + arg1 + arg2

if __name__ == "__main__":
    # Dummy data
    data = [4]*100000

    # Fix arguments must be used as keyword arguments
    compute(data, arg1=1, arg2=2)

Ошибка

Функция, поданная на imap_unordered, должна быть выбираемой. Декоратор, кажется, нарушает возможность выбора исходной функции:

_pickle.PicklingError: Can't pickle <function compute at 0x1040137a0>: it's not the same object as __main__.compute    

Лучшее решение

Сначала я подумал, что проблема была @wraps: если декорированная функция идентична исходной функции тогда последний не может быть найден по бассейнам. Но оказывается, что декоратор @wraps не имеет никакого эффекта.

Благодаря этой замечательной записи я мог бы предложить следующее неоптимальное решение: создать новое Функциональный объект верхнего уровня с использованием декоратора явно следующим образом. Это частично нарушает удобство для пользователя и, следовательно, не удовлетворяет. Тем не менее, он выполняет ожидаемую цель.

# Beware: the names should not be the same
compute_ = deco_data_parallel(compute)

if __name__ == "__main__":
    ...
    compute_(data, arg1=1, arg2=2)

Вопросы

  • Как элегантно решить проблему выбора, чтобы пользователь мог просто украсить функцию, которая будет распараллелена?
  • Почему декоратор @functools.wraps не имеет никакого эффекта?
  • Что на самом деле означает ошибка it's not the same object as __main__.compute? Т.е. в каком именно смысле я нарушаю процесс травления?

Моя конфигурация

Macport Python 3.7.7 на OSX 10.14.6

Отказ от ответственности

Я довольно новичок в мире параллельных вычислений в python, а также в мире python декораторов. Ересь, скорее всего, произошла в этом посте, и я прошу прощения за это!

Это также мой второй вопрос по StackOverflow, приветствуются любые предложения по улучшению.



Подробно расследование

Убедившись, что это сработает, я попробовал несколько стратегий декорирования . Этот очень полный пост по вопросу декораторов был отличным руководством. Это сообщение в блоге дало мне некоторую надежду, что объектно-ориентированная стратегия украшения может заставить вещи работать: автор действительно утверждает, что она исправила его / ее проблему выбора.

Все следующие подходы имеют были протестированы, с @wraps и без него, и все они приводят к одной и той же _pickle.PicklingError ошибке. У меня появляется ощущение, что я испробовал все нехакерские возможности, которые python предлагает, и было бы очень приятно оказаться неправым!

Функциональный подход

Наиболее простой подход - тот, который я показал выше. Для декораторов с аргументами также можно использовать «фабрику декораторов». Давайте для примера рассмотрим количество процессоров.

def factory_data_parallel(nproc=4):
    def deco_data_parallel(func):
        @wraps(func)
        def to_parallel(arg, **kwargs):
            part_func = partial(func, **kwargs)
            tot = 0
            with Pool(nproc) as p:
                for output in p.imap_unordered(part_func, arg):
                    tot += output
            return tot
        return to_parallel
    return deco_data_parallel

# Usage: only with the argument (or parenthesis at least)
@factory_data_parallel(8)
def compute(data, arg1, arg2):
    ...

Гибридная форма, которую можно использовать в качестве простого декоратора или фабрики декораторов, будет реализована следующим образом:

def factorydeco_data_parallel(_func=None, *, nproc=4):
    def deco_data_parallel(func):
        ...

    if _func is None:
        return deco_data_parallel
    else:
        return deco_data_parallel(_func)

# Usage as a factory (with argument)
@factorydeco_data_parallel(8)
def compute(data, arg1, arg2):
    ...

# Usage as a simple decorator
@factorydeco_data_parallel
def other_compute(data, arg1, arg2):
    ...

Объектно-ориентированный подход

Насколько я понимаю, декоратором может быть любой вызываемый объект. Простой декоратор с использованием объекта может быть реализован следующим образом. Первая версия вызывается с круглыми скобками (явное создание объекта при оформлении), а вторая используется в качестве стандартного декоратора.

class Class_data_parallel(object):
    def __call__(self, func):
        self.orig_func = func

        @wraps(func)
        def to_parallel(arg, **kwargs):
            # Does it make a difference to use the argument func instead?
            part_func = partial(self.orig_func, **kwargs)
            tot = 0
            with Pool() as p:
                for output in p.imap_unordered(part_func, arg):
                    tot += output
            return tot

        return to_parallel

class Class_data_parallel_alt(object):
    def __init__(self, func):
        self.orig_func = func

    # PB: no way I'm aware of to use @wraps
    def __call__(self, arg, **kwargs):
        part_func = partial(self.orig_func, **kwargs)
        tot = 0
        with Pool() as p:
            for output in p.imap_unordered(part_func, arg):
                tot += output
        return tot

# Usage: with parenthesis
@Class_data_parallel()
def compute(data, arg1, arg2):
    ...

# Usage: without parenthesis
@Class_data_parallel_alt
def other_compute(data, arg1, arg2):
    ...

Очевидное расширение первого случая может позволить добавить некоторые параметры конструктору. Затем класс будет играть роль декоратора.

Еще немного мыслей

  • Как я уже говорил, @wraps был кандидатом как для причины, так и для решения проблемы. Использование этого или нет ничего не меняет
  • Использование parallel для обработки константных аргументов (т. Е. Констант в разных процессах, arg1 и arg2 в моих примерах) может быть проблемой, но я сомневаюсь. Я мог бы использовать аргумент initializer конструктора Pool().
  • A. Шерман и П. Ден Хартог достигли этой цели в своей параллельной модели DECO . Однако я не могу понять, как они преодолели мою проблему. Кажется, это доказывает, что то, что я хочу сделать, не является фундаментальным ограничением декораторов.

1 Ответ

0 голосов
/ 29 марта 2020

Вы пытаетесь заставить рабочих выполнить partial(func, **kwargs), где func - неокрашенная функция. Чтобы это работало, работники должны быть в состоянии найти func по модулю и квалифицированному имени, но это не так, как следует из названия. to_parallel обертка там вместо этого. Это обнаруживается, когда главный процесс пытается засечь func.

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

...