Free — no signup required

Core Concept: Tasks (Remote Functions)

2 min read

The @ray.remote Decorator

The simplest building block in Ray is the remote function, also called a Task. You annotate a normal Python function with @ray.remote, and Ray can now schedule and execute that function anywhere in the cluster.

Normal Python (sequential):

def square(x):
    return x * x

results = [square(i) for i in range(4)]
# Runs sequentially on 1 CPU core. Each call blocks until complete.
# Output: [0, 1, 4, 9]

Ray Python (parallel):

import ray
ray.init()  # Connect to or start a Ray cluster

@ray.remote
def square(x):
    return x * x

# Launch 4 tasks in parallel across the cluster.
# Each .remote() call returns IMMEDIATELY — it does not block.
futures = [square.remote(i) for i in range(4)]

# Block here until all 4 results are ready.
results = ray.get(futures)
# Output: [0, 1, 4, 9]

The only code changes are: add @ray.remote, call with .remote(), and retrieve with ray.get().

Futures and the Object Store

When you call square.remote(i), Ray does not return the result. It returns an ObjectRef — a unique identifier (essentially a pointer) that refers to a future result somewhere in the cluster. This is the same concept as a Future or Promise in other languages.

The task is placed in Ray's distributed scheduler queue. The scheduler finds an available worker with the required resources (e.g., 1 CPU) and sends the task there. Meanwhile, your driver script continues executing — it does not wait.

ray.get(futures) is the synchronization point. It blocks until all referenced objects are materialized and returns their values.

You can also pass ObjectRefs directly into other remote calls:

a_ref = square.remote(3)   # Returns ObjectRef, not 9
b_ref = square.remote(4)   # Returns ObjectRef, not 16

# Ray automatically resolves a_ref and b_ref before running add
@ray.remote
def add(x, y):
    return x + y

result_ref = add.remote(a_ref, b_ref)
print(ray.get(result_ref))  # 25

This dependency chaining lets Ray build a task graph and execute it efficiently without you manually managing synchronization.

Resource Constraints on Tasks

By default, each task requests 1 CPU. You can override this to request GPUs or fractional CPUs:

@ray.remote(num_gpus=1)
def train_on_gpu(model):
    # This task will only be scheduled on a node with a free GPU
    ...

@ray.remote(num_cpus=0.5)
def lightweight_task(data):
    # Two of these can share a single CPU core
    ...

This resource model is how Ray prevents oversubscription — it won't schedule more tasks than a node can handle.

This is one of 18 chapters

Every chapter is free — Kubernetes, Terraform, SRE, distributed systems, and more — with fast daily review built in.

Sign up free