Почему иногда мне нужно дважды вызывать compute () для отложенных функций dask?

Я работаю с отложенными функциями dask и знакомлюсь с тем, что можно и чего нельзя делать при использовании декоратора @dask.delayed для функций. Я понял, что иногда мне нужно дважды позвонить compute(), чтобы получить результат, несмотря на то, что я думал, что следую лучшим практикам. т.е. не вызывать отложенную функцию dask внутри другой отложенной функции dask.

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

@dask.delayed
def add(a, b):
    return  a + b

def inc(a):
    return add(a, 1)

@dask.delayed
def foo(x):
    return inc(x)

x = foo(3)
x.compute()
class Add():
    def __init__(self, a, b):
        self.a = a
        self.b = b

    @dask.delayed
    def calc(self):
        return self.a+self.b

a = dask.delayed(1)
b = dask.delayed(2)
add = Add(a, b)
add.calc().compute()

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

Во втором примере мне снова придется позвонить add.calc().compute().compute(), чтобы получить фактический результат. В этом случае self.a и self.b являются просто отложенными атрибутами, и нигде нет вложенных отложенных функций.

Может ли кто-нибудь помочь мне понять, почему мне нужно дважды звонить compute() в этих двух случаях? Или, что еще лучше, может кто-нибудь вкратце объяснить общее «правило» при использовании отложенных функций dask? Я прочитал документацию, и там не так уж много можно найти.

Обновление: @malbert указал, что в примерах требуется дважды вызвать compute(), потому что в отложенной функции задействованы отложенные результаты, и поэтому он считается «вызовом отложенной функции в другой отложенной функции». Но почему что-то вроде следующего требует только одного вызова compute()?

@dask.delayed
def add(a,b):
    return a+b

a = dask.delayed(1)
b = dask.delayed(2)
c = add(a,b)
c.compute()

В этом примере a и b также являются отложенными результатами, и они используются в отложенной функции. Моя случайная догадка заключается в том, что на самом деле имеет значение, где задержанный результат находится в отложенной функции? Возможно, это нормально, только если они будут переданы в качестве аргументов?


person Yilie Ma    schedule 09.07.2019    source источник


Ответы (1)


Я думаю, что ключ кроется в более точном понимании того, что делает dask.delayed.

Учитывать

my_delayed_function = dask.delayed(my_function)

При использовании в качестве декоратора на my_function, dask.delayed возвращает функцию my_delayed_function, которая задерживает выполнение my_function. Когда my_delayed_function вызывается с аргументом

delayed_result = my_delayed_function(arg)

это возвращает объект, который содержит всю необходимую информацию о выполнении my_function с аргументом arg.

Звонок

result = delayed_result.compute()

запускает выполнение функции.

Теперь эффект от использования операторов, таких как +, для двух отложенных результатов заключается в том, что возвращается новый отложенный результат, который объединяет два выполнения, содержащиеся во входных данных. Вызов compute для этого объекта запускает этот пакет выполнений.


Все идет нормально. Теперь в вашем первом примере foo вызывает inc, который вызывает отложенную функцию, которая возвращает отложенный результат. Следовательно, вычисление foo делает именно это и возвращает этот отложенный результат. Вызов compute для этого отложенного результата (вашего «второго» вычисления) затем запускает его вычисление.

Во втором примере a и b - это отложенные результаты. Добавление двух отложенных результатов с использованием + возвращает отложенный результат объединения выполнения _21 _, _ 22_ и их сложения. Теперь, поскольку calc является отложенной функцией, она возвращает отложенный результат при получении отложенного результата. Поэтому снова его вычисление вернет отложенный объект.

В обоих случаях вы не совсем следовали передовым методам. Конкретно точка

Избегайте отложенного вызова в отложенных функциях

поскольку в вашем первом примере отложенный add вызывается в inc, который вызывается в foo. Поэтому вы звоните с задержкой в ​​течение foo с задержкой. Во втором примере отложенный calc работает с отложенным a и b, поэтому вы снова вызываете отложенный вызов в отложенной функции.

В вашем вопросе вы говорите

Но я считаю, что inc не является отложенной функцией и, следовательно, не противоречит правилу не вызывать отложенную функцию в другой отложенной функции?

Я подозреваю, что вы могли неправильно понять «отложенный вызов в отложенных функциях». Это относится ко всему, что происходит внутри функции, и поэтому является ее частью: inc включает вызов отложенного add, поэтому отложенный вызов вызывается в foo.

Дополнение после обновления вопроса: передача отложенных аргументов в отложенную функцию объединяет отложенное выполнение в новый отложенный результат. Это отличается от «отложенного вызова в рамках отложенной функции» и является частью предполагаемого варианта использования. На самом деле я также не нашел четкого объяснения этого в документации, но одна точка входа может быть this: unpack_collections используется для обработки отложенных аргументов. Даже если это должно оставаться в некоторой степени неясным, следование передовым практикам (интерпретируемым таким образом) должно обеспечить воспроизводимое поведение в отношении вывода compute().

Следующие коды приводят к тому, что придерживаются «Избегать отложенного вызова в функциях с задержкой» и возвращают результат после одного вызова compute:

Первый пример:

#@dask.delayed
def add(a, b):
    return  a + b

def inc(a):
    return add(a, 1)

@dask.delayed
def foo(x):
    return inc(x)

x = foo(3)
x.compute()

Второй пример:

class Add():
    def __init__(self, a, b):
        self.a = a
        self.b = b

    #@dask.delayed
    def calc(self):
        return self.a+self.b

a = dask.delayed(1)
b = dask.delayed(2)
add = Add(a, b)
add.calc().compute()
person malbert    schedule 10.07.2019
comment
Большое спасибо за ваше объяснение. Однако есть кое-что, что я не могу полностью усвоить. См. Мой обновленный вопрос. Спасибо! - person Yilie Ma; 11.07.2019
comment
Вы добавили отличный пример. Проверить мой ответ править - person malbert; 11.07.2019