Inside the distributed scheduler

Dask · Expert · 15 min

When a large job stalls or behaves oddly, you need a mental model of how the scheduler places tasks and moves data.

A distributed cluster has three roles. The client in your Python session builds graphs and sends them over. The scheduler is a single process that tracks every task's state and decides where it runs. Workers execute tasks in thread pools, store results in memory, and fetch dependencies directly from each other. A nanny process supervises each worker and restarts it if it dies or exceeds memory limits.

Each task moves through states on the scheduler: released, waiting (dependencies not ready), processing (assigned to a worker), memory (result held by a worker), or erred. Workers keep their own matching state machine.

Placement decisions:

  • Data locality: a task usually goes to the worker that already holds most of its input bytes, estimating transfer cost from data sizes and bandwidth.
  • Queuing: root tasks such as file reads are queued on the scheduler and released as workers free up, which keeps memory steady.
  • Work stealing: idle workers take queued tasks from busy ones when the compute saved exceeds the transfer cost.

Resilience: if a worker dies, the scheduler recomputes lost results from the graph's lineage. A task that keeps killing workers is marked as erred after allowed-failures attempts (default 3), with a KilledWorker error. The usual cause is a task that uses too much memory.

The scheduler is single-threaded Python, so very high task rates, roughly thousands per second, make it the bottleneck. That's another reason to keep tasks coarse.

Example

from dask.distributed import Client

if __name__ == "__main__":
    with Client(n_workers=2, threads_per_worker=1, processes=False) as client:
        a = client.submit(lambda: list(range(100_000)), key="big-data")
        b = client.submit(len, a, key="needs-big")     # goes where 'big-data' lives
        print(b.result())                               # 100000

        who_has = client.who_has([a, b])
        print(who_has["big-data"] == who_has["needs-big"])   # True (data locality)

        info = client.scheduler_info()
        print(len(info["workers"]))                     # 2
        states = client.run_on_scheduler(
            lambda dask_scheduler: dask_scheduler.tasks["needs-big"].state)
        print(states)                                   # memory

Remember

  • Client builds graphs; one scheduler tracks task states; workers compute and share data
  • Placement favours data locality, with queuing and work stealing to balance load
  • Lost results are recomputed; repeated crashes surface as KilledWorker

Watch out for

  • Seeing KilledWorker and retrying blindly instead of shrinking the memory used per task.

Check yourself

  1. A task repeatedly causes its worker to be restarted. What does the scheduler eventually report?

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

Official docs: distributed.dask.org/en/stable/scheduling-state.html