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.