TornadoVM – Boosting the Concurrency
TornadoVM is an open-source framework that extends the Java Virtual Machine (JVM) to support hardware accelerators such as Graphics Processing Units (GPUs), Field-Programmable Gate Arrays (FPGAs), and multi-core central processing units (CPUs). This allows developers to accelerate their Java programs on heterogeneous hardware without needing to rewrite their code in low-level languages such as CUDA or OpenCL.
Here’s an overview of TornadoVM’s key aspects and features:
Motivation and Background
The growth in heterogeneous computing, driven by the increasing demand for performance in fields like machine learning, data processing, and scientific computing, has led to the development of specialised hardware such as GPUs and FPGAs. These devices offer significant performance improvements over traditional CPUs for parallel workloads.
However, programming for these accelerators traditionally requires low-level programming models like CUDA or OpenCL, which are less developer-friendly than higher-level languages like Java. TornadoVM addresses this challenge by allowing developers to use the Java programming language while transparently offloading computations to hardware accelerators.
The JVM has been traditionally optimised for multi-core CPUs, but its design is unsuited for exploiting massively parallel architectures like GPUs. TornadoVM fills this gap by introducing a runtime and compilation infrastructure that allows the JVM to leverage these devices.
How TornadoVM Works
TornadoVM provides an intermediate layer between Java applications and the underlying hardware. It intercepts certain portions of the Java code, such as loops and parallelizable methods, and compiles them to run on GPUs, FPGAs, or multi-core CPUs.
The TornadoVM runtime is divided into three main components:
Bytecode Analyzer: TornadoVM analyses the Java bytecode to identify portions of the code that can be accelerated. This typically includes loops, data-parallel operations, and compute-intensive methods.
JIT Compiler: TornadoVM uses a Just-In-Time (JIT) compiler to translate the selected portions of the bytecode into OpenCL or PTX (NVIDIA’s parallel thread execution format) code that can run on GPUs or FPGAs. The compiler also optimises the code for the specific hardware target.
Task Scheduling and Execution: Once compiled, TornadoVM schedules the execution tasks on the available hardware. It manages data transfers between the host (CPU) and the accelerators and ensures the results are correctly integrated into the running Java program.
Key Features
Java API: TornadoVM provides a simple Java API that allows developers to annotate the code they want to accelerate. This can include compute-intensive methods or loops suitable for parallel execution.
Automatic Offloading: TornadoVM automatically detects which portions of the code can be offloaded to the hardware accelerators. The developer doesn’t need to manage low-level details such as memory transfers or kernel execution.
Multi-backend Support: TornadoVM supports multiple backends, including OpenCL, PTX (for NVIDIA GPUs), and SPIR-V. This enables it to target various hardware platforms, from different GPU vendors (NVIDIA, AMD, Intel) to FPGAs.
Data Management: TornadoVM handles data transfers between the main memory (used by the CPU) and the memory of the accelerators (such as the GPU memory). It uses a memory management system to minimise unnecessary data transfers, reduce overhead, and improve performance.
Heterogeneous Task Scheduling: TornadoVM can execute multiple tasks across different devices in parallel. For example, it can execute one part of the code on a CPU and another part on a GPU, enabling efficient use of all available hardware resources.
Use Cases
TornadoVM is particularly suited for applications that can benefit from parallel execution. Some of the most common use cases include:
Machine Learning: Many machine learning algorithms, especially those based on matrix operations (like neural networks), can be accelerated on GPUs or FPGAs using TornadoVM.
Big Data Processing: Frameworks like Apache Spark can be integrated with TornadoVM to accelerate data processing pipelines. By offloading certain operations to GPUs, it can significantly reduce the time required to process large datasets.
Scientific Computing: Many scientific applications involve complex mathematical computations that can benefit from parallel execution on hardware accelerators. TornadoVM makes it possible to run these applications on GPUs without rewriting the code in CUDA or OpenCL.
Financial Modeling: Algorithms used in financial simulations, such as Monte Carlo simulations or options pricing, often involve parallelizable computations that can be offloaded to hardware accelerators using TornadoVM.
Performance and Benchmarks
TornadoVM has been shown to improve performance significantly for various applications. For example, benchmarks have demonstrated that TornadoVM can speed up certain machine learning algorithms by 10x to 100x when running on GPUs, compared to the same code running on a CPU.
The performance gains are particularly significant for compute-bound applications that involve a large number of parallel operations. However, not all applications will benefit from TornadoVM. Applications that are I/O-bound or involve a lot of sequential processing may not see significant speedups.
Limitations and Challenges
Despite its benefits, TornadoVM has some limitations:
Device Availability: TornadoVM requires the presence of a hardware accelerator (such as a GPU or FPGA) to provide performance improvements. If no such device is available, the code will fall back to running on the CPU.
Limited Scope: TornadoVM is most effective for applications that involve parallelizable computations. Applications with complex control flow or those that are heavily reliant on I/O may not see significant performance improvements.
Hardware-Specific Tuning: While TornadoVM abstracts many of the details of hardware acceleration, developers may still need to fine-tune their code for specific hardware platforms to achieve optimal performance.
Integration with Java Ecosystem
One of the key strengths of TornadoVM is its seamless integration with the existing Java ecosystem. It works with the standard Java Development Kit (JDK) and can be integrated with popular Java frameworks such as:
Apache Spark: TornadoVM can be used to accelerate Spark workloads by offloading certain operations to hardware accelerators. This can significantly reduce the time required to process large datasets.
JVM-based Languages: TornadoVM can be used with any JVM-based language, including Scala, Kotlin, and Groovy. This makes it a versatile solution for developers working in various JVM languages.
GraalVM: TornadoVM can also be used in conjunction with GraalVM, an advanced JVM that includes an optimising compiler and support for polyglot programming. GraalVM can further enhance TornadoVM’s performance by applying additional optimizations at runtime.
Future Directions
TornadoVM is an actively developed project, and its developers are working on several features to improve its functionality and performance. Some of the key areas of future development include:
Expanded Hardware Support: TornadoVM continually supports new hardware platforms, including newer generations of GPUs and FPGAs.
Improved Optimization: The TornadoVM team is working on improving the JIT compiler to generate more optimised code for hardware accelerators, leading to even more significant performance gains.
Automatic Parallelization: While TornadoVM already supports automatic offloading of specific code segments, there is ongoing work to automate further the process of identifying parallelizable code and offloading it to hardware accelerators.
Deep Integration with Machine Learning Frameworks: TornadoVM is exploring deeper integration with machine learning frameworks like TensorFlow and PyTorch to provide hardware acceleration for a wider range of machine learning tasks.
TornadoVM is a powerful tool that brings hardware acceleration to the Java ecosystem, allowing developers to harness the power of GPUs, FPGAs, and multi-core CPUs without having to leave the familiar world of the JVM. By simplifying the process of offloading computations to heterogeneous hardware, TornadoVM opens up new possibilities for accelerating a wide range of applications, from machine learning and big data processing to scientific computing and financial modelling. As the demand for high-performance computing continues to grow, TornadoVM is likely to play an increasingly important role in enabling Java developers to take full advantage of modern hardware platforms.
Let’s have an example
To give you a comprehensive example in Java using TornadoVM, we will look at simple matrix multiplication, a common parallelisable operation. The example will include a theoretical background on parallelism, how TornadoVM exploits it, and how the program is structured to benefit from GPU/FPGA acceleration.
Theoretical Background: Parallelism in Matrix Multiplication
Matrix multiplication is a good example of parallel execution because each element of the result matrix can be computed independently of the others. Consider two matrices, `A` and `B`, where:
– Matrix `A` is of size N x M,
– Matrix `B` is of size M x K.
The resulting matrix `C` will be of size N x K, where each element C[i][j] is the dot product of the i-th row of `A` and the j-th column of `B`:

This operation involves a large number of independent computations, meaning that all elements of `C` can be computed in parallel. This is an ideal scenario for hardware acceleration using GPUs or FPGAs, which can perform a massive number of operations simultaneously.
How TornadoVM Exploits Parallelism
TornadoVM can accelerate the matrix multiplication by identifying the parallelisable loops (i.e., loops where the computations are independent). It translates the loops into tasks that can be distributed across GPU cores. The key steps are:
Bytecode Analysis: TornadoVM inspects the Java bytecode to identify parallelisable sections, such as loops where each iteration performs independent operations.
JIT Compilation: The identified sections are compiled Just-In-Time (JIT) into code that runs on the accelerator (such as PTX for NVIDIA GPUs).
Task Execution: The compiled tasks are executed on the available hardware, while data is transferred between the host (CPU) and the accelerator as needed.
Example: Matrix Multiplication in Java with TornadoVM
Below is a Java code example that uses TornadoVM to perform matrix multiplication.
import uk.ac.manchester.tornado.api.TaskSchedule;
import uk.ac.manchester.tornado.api.TornadoVM;
import uk.ac.manchester.tornado.api.annotations.Parallel;
public class MatrixMultiplication {
// Matrix multiplication using TornadoVM
public static void multiplyMatrix(float[][] A, float[][] B, float[][] C, int N, int M, int K) {
// TornadoVM annotation to indicate parallelism
@Parallel
for (int i = 0; i < N; i++) {
for (int j = 0; j < K; j++) {
float sum = 0.0f;
for (int k = 0; k < M; k++) {
sum += A[i][k] * B[k][j];
}
C[i][j] = sum;
}
}
}
public static void main(String[] args) {
int N = 1024; // Number of rows in A
int M = 1024; // Number of columns in A (and rows in B)
int K = 1024; // Number of columns in B
// Initialize matrices
float[][] A = new float[N][M];
float[][] B = new float[M][K];
float[][] C = new float[N][K]; // Result matrix
// Fill matrices A and B with random values
for (int i = 0; i < N; i++) {
for (int j = 0; j < M; j++) {
A[i][j] = (float) Math.random();
}
}
for (int i = 0; i < M; i++) {
for (int j = 0; j < K; j++) {
B[i][j] = (float) Math.random();
}
}
// Create a task schedule for TornadoVM
TaskSchedule ts = new TaskSchedule("s0")
.task("t0", MatrixMultiplication::multiplyMatrix, A, B, C, N, M, K)
.mapAllTo(TornadoVM.getDefaultDevice());
// Execute the task on the GPU
ts.execute();
// Print out a few results for verification
System.out.println("Result (C[0][0]): " + C[0][0]);
System.out.println("Result (C[10][10]): " + C[10][10]);
}
}
Code Explanation
Matrix Initialization:
Matrices `A` and `B` are initialised with random floating-point values. The matrix `C` will store the result of the multiplication.
Matrix Multiplication Function:
The `multiplyMatrix` method multiplies matrix `A` by matrix `B` and stores the result in matrix `C`. The **@Parallel** annotation tells TornadoVM that this loop is parallelisable, meaning that each computation of `C[i][j]` can be executed independently across different threads or hardware cores.
Task Scheduling with TornadoVM:
The `TaskSchedule` object schedules a TornadoVM task, where the `multiplyMatrix` function is executed. `.mapAllTo(TornadoVM.getDefaultDevice())` specifies that the entire task will be mapped to the default TornadoVM device, which could be a GPU, FPGA, or multi-core CPU, depending on the available hardware. The `.execute()` method runs the task, performing the matrix multiplication on the selected hardware accelerator.
Performance Consideration:
For large matrix sizes (e.g., 1024×1024, as used in the example), the GPU can perform the multiplication much faster than a CPU by exploiting parallelism. TornadoVM handles the data transfer and task offloading, ensuring efficient execution on the accelerator.
Theoretical Discussion
Parallelism:
Each element C[i][j] is independent of all other elements in matrix multiplication, making the operation embarrassingly parallel. The GPU can execute these independent operations concurrently with its thousands of cores, leading to significant speedups over a sequential CPU-based implementation.
Data Transfer:
TornadoVM manages data transfers between the CPU (host) and the GPU (device). Before the computation starts, matrices A and B are copied to the GPU’s memory. After the computation, the resulting matrix `C` is transferred back to the CPU. TornadoVM optimised these transfers, ensuring that only the necessary data is copied and avoiding redundant transfers to minimise overhead.
Memory Coalescing:
Memory access patterns are important for performance on a GPU. TornadoVM attempts to optimise memory access through techniques like memory coalescing, where adjacent threads access adjacent memory locations, reducing the number of memory transactions and improving bandwidth utilisation.
Loop Parallelization:
The loop structure is a perfect candidate for parallelisation: TornadoVM splits the outer two loops (over `i’ and `j` indices) across different GPU threads. Each thread computes a single element of the result matrix `C`, while the innermost loop (over `k`) can be executed within each thread.
Just-in-Time Compilation:
TornadoVM’s JIT compiler generates GPU-specific code (such as PTX code for NVIDIA GPUs) based on the annotated Java bytecode. This process occurs at runtime, allowing TornadoVM to optimise the code for the specific hardware platform in use.
Performance Considerations
Using TornadoVM on a GPU or FPGA for matrix multiplication can yield significant performance improvements, especially for large matrices. For instance:
A CPU may take several seconds to multiply two large matrices (e.g., 1024×1024), whereas a GPU can perform the same operation in a fraction of the time by leveraging its thousands of cores. The larger the matrices, the more efficient TornadoVM becomes, as the overhead of transferring data between the CPU and the accelerator becomes less significant relative to the computation time.
This example illustrates how TornadoVM allows Java developers to exploit hardware accelerators like GPUs for computationally expensive tasks like matrix multiplication. By identifying parallelisable loops and offloading them to the accelerator, TornadoVM achieves substantial performance improvements while allowing developers to continue working in Java without needing lower-level languages like CUDA or OpenCL.
Happy Coding
Sven
Discover more from Sven Ruppert
Subscribe to get the latest posts sent to your email.