dask.delayed: parallelize custom code

Dask · Essentials · 12 min

Many real workloads aren't DataFrames or arrays, and delayed parallelizes them with a few decorators.

dask.delayed turns a function call into a lazy task. Calling a delayed function doesn't run it; it returns a Delayed object representing the future result. Passing Delayed objects into other delayed calls links them into a graph.

@dask.delayed
def load(path): ...
parts = [load(p) for p in paths]
total = dask.delayed(sum)(parts)
total.compute()

Dask sees that all load calls are independent and runs them in parallel, then runs sum when they're done. You write a normal-looking loop; Dask extracts the parallelism.

Guidelines:

  • Decorate the function, then call it; don't call the function and wrap the result (delayed(f(x)) runs f immediately).
  • Each task should do a meaningful amount of work, at least around 100 ms. Each task costs scheduler overhead, so a million tiny tasks is slower than a plain loop.
  • Don't mutate inputs or rely on global state inside tasks; they may run in another process or machine.
  • Collect everything with one dask.compute(*objs) call.
  • Use total.visualize() to draw the graph (requires graphviz).

Delayed objects support attribute access, indexing and arithmetic, which become tasks too. Combine them with collections via dd.from_delayed or da.from_delayed.

Example

import threading
import time
import dask

threads_used = set()

@dask.delayed
def fetch(i):
    threads_used.add(threading.current_thread().name)
    time.sleep(0.5)            # pretend this is slow I/O
    return i * 10

@dask.delayed
def combine(values):
    return sum(values)

parts = [fetch(i) for i in range(4)]   # nothing runs yet
total = combine(parts)

print(total.compute(num_workers=4))    # 60
print(len(threads_used) > 1)
# True: the fetches ran on several threads at the same time

Remember

  • @dask.delayed makes calls lazy; passing Delayed objects builds a graph
  • Independent calls run in parallel when you compute()
  • Keep tasks coarse (around 100 ms or more) and side-effect free

Watch out for

  • Writing delayed(f(x)), which calls f immediately instead of delayed(f)(x).

Check yourself

  1. What is wrong with dask.delayed(process(data))?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: docs.dask.org/en/stable/delayed.html