Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/Deepak-Sangle/TornadoVM/llms.txt

Use this file to discover all available pages before exploring further.

The TaskGraph is TornadoVM’s primary abstraction for describing GPU workloads. It acts as a directed acyclic graph (DAG) that captures which Java methods should run on an accelerator, which data needs to travel to the device before execution, and which results must be read back afterward. Crucially, creating a TaskGraph does not trigger any computation or data movement — it only records intent. Execution only happens through a TornadoExecutionPlan, giving you a clean separation between workload description and execution policy.

Core Concepts

TaskGraph

A mutable builder that records tasks and data-transfer declarations. Think of it as the “blueprint” for your GPU pipeline.

ImmutableTaskGraph

A frozen snapshot of a TaskGraph. Immutability guarantees thread safety and prevents accidental mutation between execution calls.

TornadoExecutionPlan

Wraps one or more ImmutableTaskGraph objects and provides the fluent API to select devices, enable profiling, and trigger execution.

TornadoExecutionResult

Returned by every .execute() call. Holds profiler data and execution metadata.

Step-by-Step: Building Your First Pipeline

1

Write a parallelisable Java method

Methods targeted for GPU acceleration are ordinary Java static methods. Annotate loop variables with @Parallel to hint at parallelism (see Annotations).
import uk.ac.manchester.tornado.api.annotations.Parallel;
import uk.ac.manchester.tornado.api.types.arrays.FloatArray;

public static void vectorAdd(FloatArray a, FloatArray b, FloatArray c) {
    for (@Parallel int i = 0; i < a.getSize(); i++) {
        c.set(i, a.get(i) + b.get(i));
    }
}
2

Create a TaskGraph

Construct a TaskGraph with a unique string name. This name becomes a prefix for all task IDs inside the graph (e.g. "graph.task").
import uk.ac.manchester.tornado.api.TaskGraph;
import uk.ac.manchester.tornado.api.enums.DataTransferMode;

TaskGraph tg = new TaskGraph("graph");
3

Declare device-bound data with transferToDevice

Tell TornadoVM which arrays must be on the accelerator before execution begins. Choose the appropriate DataTransferMode for your access pattern.
tg.transferToDevice(DataTransferMode.FIRST_EXECUTION, a, b);
4

Register tasks with .task()

Link Java methods as GPU tasks. The first argument is a unique task ID, the second is a method reference, and the remaining arguments mirror the method signature.
tg.task("t0", VectorOps::vectorAdd, a, b, c);
5

Declare results with transferToHost

Specify which arrays should be synchronised back to CPU memory after execution.
tg.transferToHost(DataTransferMode.EVERY_EXECUTION, c);
6

Snapshot → ImmutableTaskGraph

Call .snapshot() to freeze the graph. From this point the TaskGraph can continue to be mutated without affecting the immutable copy.
ImmutableTaskGraph itg = tg.snapshot();
7

Create and execute a TornadoExecutionPlan

Wrap the immutable graph in an execution plan and call .execute().
try (TornadoExecutionPlan plan = new TornadoExecutionPlan(itg)) {
    TornadoExecutionResult result = plan.execute();
}

DataTransferMode Reference

The DataTransferMode enum controls when TornadoVM actually moves data between host and device memory.
Data is copied on every call to .execute(). Use this for inputs that change between executions or for results you want back every time.
tg.transferToDevice(DataTransferMode.EVERY_EXECUTION, inputArray);
tg.transferToHost(DataTransferMode.EVERY_EXECUTION, outputArray);
The DataTransferMode only influences the runtime data movement — the TornadoVM JIT compiler still sees all arguments and optimises accordingly regardless of the mode selected.

Complete Working Example: Vector Addition

The following self-contained example demonstrates the entire lifecycle — allocation, task declaration, snapshotting, plan creation, execution, and verification.
import uk.ac.manchester.tornado.api.ImmutableTaskGraph;
import uk.ac.manchester.tornado.api.TaskGraph;
import uk.ac.manchester.tornado.api.TornadoExecutionPlan;
import uk.ac.manchester.tornado.api.TornadoExecutionResult;
import uk.ac.manchester.tornado.api.annotations.Parallel;
import uk.ac.manchester.tornado.api.enums.DataTransferMode;
import uk.ac.manchester.tornado.api.types.arrays.FloatArray;

public class VectorAddExample {

    // GPU-accelerated method — must be static
    public static void vectorAdd(FloatArray a, FloatArray b, FloatArray c) {
        for (@Parallel int i = 0; i < a.getSize(); i++) {
            c.set(i, a.get(i) + b.get(i));
        }
    }

    public static void main(String[] args) throws Exception {
        final int SIZE = 1_024;

        // 1. Allocate off-heap arrays (TornadoVM-managed memory)
        FloatArray a = new FloatArray(SIZE);
        FloatArray b = new FloatArray(SIZE);
        FloatArray c = new FloatArray(SIZE);

        a.init(1.0f);   // fill with 1.0
        b.init(2.0f);   // fill with 2.0
        c.clear();      // zero result buffer

        // 2. Build the TaskGraph
        TaskGraph tg = new TaskGraph("s0")
            .transferToDevice(DataTransferMode.FIRST_EXECUTION, a, b)
            .task("t0", VectorAddExample::vectorAdd, a, b, c)
            .transferToHost(DataTransferMode.EVERY_EXECUTION, c);

        // 3. Snapshot → ImmutableTaskGraph
        ImmutableTaskGraph itg = tg.snapshot();

        // 4. Execute inside try-with-resources for safe cleanup
        try (TornadoExecutionPlan plan = new TornadoExecutionPlan(itg)) {
            TornadoExecutionResult result = plan.execute();
            System.out.println("c[0] = " + c.get(0));  // 3.0
        }
    }
}

The .task() Method

The task() call accepts a method reference (or lambda) via TornadoVM’s TornadoFunctions interfaces. The framework supports methods with zero to twenty parameters.
Both styles are valid. Method references (Class::method) are preferred for named, reusable kernels. Lambdas are convenient for one-off inline kernels.
// Method reference (recommended)
tg.task("t0", MyKernels::processArray, data, size);

// Lambda (also valid)
tg.task("t0", (FloatArray d, Integer s) -> {
    for (@Parallel int i = 0; i < s; i++) {
        d.set(i, d.get(i) * 2.0f);
    }
}, data, size);
Every task within a single TaskGraph must have a unique ID. Registering two tasks with the same string ID throws a TornadoTaskRuntimeException at graph-building time.

Chaining Multiple Tasks

A single TaskGraph can hold any number of tasks. TornadoVM chains their execution on the device, eliminating redundant host↔device round-trips between tasks.
// Map step: element-wise addition
public static void map(IntArray a, IntArray b, IntArray c) {
    for (@Parallel int i = 0; i < a.getSize(); i++) {
        c.set(i, a.get(i) + b.get(i));
    }
}

// Reduce step: sum all elements
public static void reduce(IntArray c, @Reduce IntArray result) {
    result.set(0, 0);
    for (@Parallel int i = 0; i < c.getSize(); i++) {
        result.set(0, result.get(0) + c.get(i));
    }
}

// Wire both into one graph
TaskGraph tg = new TaskGraph("pipeline")
    .transferToDevice(DataTransferMode.EVERY_EXECUTION, a, b)
    .task("t0", Pipeline::map,    a, b, c)       // task 1
    .task("t1", Pipeline::reduce, c, result)     // task 2 — consumes t0's output
    .transferToHost(DataTransferMode.EVERY_EXECUTION, result);
The intermediate array c above does not need a transferToHost call — it lives entirely on the device and is consumed by t1 without touching the CPU.

TornadoExecutionPlan Fluent API

TornadoExecutionPlan is the runtime controller. Its methods all return this (or a decorated subtype), enabling fluent chaining.
MethodEffect
.withDevice(TornadoDevice)Run all graphs on a specific device
.withDevice(String, TornadoDevice)Target one named task to a specific device
.withGridScheduler(GridScheduler)Attach explicit thread grid (Kernel API)
.withProfiler(ProfilerMode)Enable profiler (SILENT or CONSOLE)
.withPreCompilation()JIT-compile all tasks without executing
.withWarmUpIterations(int)Run N warm-up rounds before timing
.withBatch(String)Enable batch processing (e.g. "512MB")
.withConcurrentDevices()Run independent tasks in parallel across devices
try (TornadoExecutionPlan plan = new TornadoExecutionPlan(itg)) {
    TornadoExecutionResult result = plan
        .withProfiler(ProfilerMode.SILENT)
        .withWarmUpIterations(5)
        .withDevice(TornadoExecutionPlan.getDevice(0, 0))
        .execute();

    // Query profiler data
    System.out.println(result.getProfilerResult().getDeviceKernelTime());
}

AutoCloseable and Resource Management

TornadoExecutionPlan implements AutoCloseable. Always wrap it in a try-with-resources block so that device memory, command queues, and compiled kernels are freed deterministically.
try (TornadoExecutionPlan plan = new TornadoExecutionPlan(itg)) {
    plan.execute();
    // device resources freed automatically on block exit
}
If you run a TornadoExecutionPlan repeatedly in a loop (e.g. a render loop or iterative solver), create it once outside the loop and call .execute() repeatedly. Recreating the plan each iteration forces recompilation.

Obtaining Profiler Results

When profiling is enabled, every TornadoExecutionResult carries timing and memory-transfer metrics.
TornadoExecutionResult result = plan
    .withProfiler(ProfilerMode.SILENT)
    .execute();

TornadoProfilerResult profiler = result.getProfilerResult();

// Kernel execution time in nanoseconds
long kernelNs = profiler.getDeviceKernelTime();

// Host-to-device copy time (device write time)
long h2dNs   = profiler.getDeviceWriteTime();

// Device-to-host copy time (device read time)
long d2hNs   = profiler.getDeviceReadTime();

System.out.printf("Kernel: %.3f ms%n", kernelNs / 1e6);
Profiler data is only populated when .withProfiler(...) has been called. Querying profiler results without enabling the profiler returns zero or undefined values.

Build docs developers (and LLMs) love