Dista
Mojo functions, across a cluster
Dista is for developers who have written fast Mojo and now need more than one machine. A task is an ordinary Mojo function. The program that calls it runs unchanged on a laptop, where Dista starts its own workers, and on a Kubernetes cluster, where every node runs the same small image. There is no Python in the path and no second language to learn.
from dista import Runtime, Task
def square(x: Int) -> Int:
return x * x
comptime Square = Task[Int, Int, square]
def main() raises:
var rt = Runtime[Square](workers=4)
var r = rt.remote[Square](7) # an ObjectRef[Int]
print(rt.get(r)) # 49 Results that are large stay on the worker that computed them. A task on the same host maps them from shared memory, and one elsewhere fetches them from that worker directly, without going through the head. A task can take other tasks' results as its input, so a pipeline is a graph of calls rather than a sequence of round trips to the driver.
Performance so far
12 s a pass, against 28.7 s for Rust and 38.6 s for Ray
The same eight queries over 1.4 billion taxi trips in an Iceberg table in S3, on three worker nodes, with the answers checked against each other. A day earlier Dista took 56.7 s on the same run; rewriting how it reads from object storage made it the fastest of the three. The performance page has every query, the before-and-after timeline, the earlier runs where Dista lost, and how to reproduce each number.
Median pass of the eight queries, 3 × AWS c7g.4xlarge (16 Graviton3 vCPUs, 1 November 2026. Lower is better. One run on one day: the comparison frameworks varied about 10% between runs.
Status
Early, and measured as it goes
Dista has passed its fourth phase's gate: the taxi queries on a Kubernetes cluster stay exact with a worker killed mid-run, and a worker stopped the way Kubernetes stops one drains without anything running twice. Before that came tasks, results, a head that restarts, and actors that survive their worker or the head restarting. What it doesn't do yet is listed here rather than left to be found out.
Works today
- Tasks: a top-level Mojo function, declared once, called with rt.remote; Task2 and Task3 for two or three arguments
- Results that stay on the worker that computed them, mapped from shared memory on the same host or fetched from that worker directly
- Tasks on other tasks' results: one ObjectRef, or a list gathered into a Seq
- Placement by data location: a task goes to the node holding most of its input, when one does
- Tasks whose worker dies run again on another, up to DISTA_TASK_RETRIES times
- Draining: a worker that Kubernetes stops finishes its tasks, and its results and its actor move to other workers, so nothing fails or runs twice
- Spilling: past DISTA_STORE_BYTES, a worker's results go to disk, and readers find them there
- Backpressure: with too much queued, rt.remote waits for the workers to catch up
- Arrow record batches as task arguments and results
- Freeing: reference counts at the head, rt.release
- A head that restarts: workers rejoin with what they hold, drivers resume where they were
- Actors: stateful structs on a worker of their own, calls in order, handles passed to tasks and other actors
- Actors that survive their worker or the head restarting, restored from checkpoints in a directory or S3
- Shuffles: several results from one task, and a task taking a value beside other tasks' results; a shuffle benchmark against Ray
- A task timeline in Chrome's trace format, the driver's own work included
- Prometheus metrics on the head and every worker
- The dista command: one head for several programs; dista status, timeline and stop
- One container image for every role, 32 MB compressed, on Kubernetes
- A published package: pixi shelf add dista-mojo
Not yet
- Refs nested inside a struct or list in a task's argument, which is a compile error for now (deferred, issue 68)
- Rebuilding results lost with a worker, as Ray's lineage does: the job fails, naming them, and orca runs it again (deferred, issue 69)
- A running job asking for more workers: orca sizes each run's cluster (deferred, issue 76)
A longer write-up, with the design and the measurements behind it, follows with the second benchmark, which spends its time in user code and a shuffle.