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 TornadoVM Hybrid API is the bridge between Java-authored GPU kernels and NVIDIA’s hand-tuned native libraries. A single TaskGraph can interleave @Parallel or KernelContext tasks with calls into cuBLAS, cuFFT, cuDNN, cuSPARSE, cuBLASLt, and CUTLASS — all sharing the same TornadoVM-managed device buffers on one CUDA stream. There are no manual cudaMemcpy calls between the JIT and native sides, no host synchronization points, and no separate memory management to maintain. Every library call becomes a library task: a first-class citizen in the TaskGraph that flows through the same ALLOC/TRANSFER/LAUNCH bytecodes as any other task.
The Hybrid API requires the CUDA backend. Build with make BACKEND=cuda and source setvars.sh. Library tasks on OpenCL or Metal backends are silently reported as UNSUPPORTED.

Core Concepts

Library Tasks

A library task is a SchedulableTask without a JIT sketch. Its argument access descriptors (READ_ONLY, WRITE_ONLY, READ_WRITE) come from the provider factory, so the data-flow graph tracks transfers automatically — just like any other task.

Shared Device Buffers

JIT kernels and library calls operate on the same TornadoVM buffer objects. Data produced by a Java kernel stays on the GPU and feeds directly into the native library call — no host round-trips.

Same CUDA Stream

Every provider binds its native handle to the backend’s CUDA stream (via cublasSetStream, cudnnSetStream, etc.). JIT kernels, transfers, and library calls all execute in order on one stream — no manual synchronization.

The prepare() Hook

Shape-dependent allocations (cuFFT plans, cuDNN descriptors, cuBLAS workspaces) happen in a prepare() hook called before CUDA Graph capture. This makes library tasks capture-safe — dispatch() allocates nothing.

The libraryTask Method

The only new API surface is .libraryTask(id, factory, args...) — a sibling of .task(...) on TaskGraph. There are 20 overloads accepting 1–20 typed arguments. The second argument is always a method reference to a provider factory that returns a LibraryTaskDescriptor.
import uk.ac.manchester.tornado.api.*;
import uk.ac.manchester.tornado.api.enums.DataTransferMode;
import uk.ac.manchester.tornado.api.types.arrays.FloatArray;
import uk.ac.manchester.tornado.cublas.CuBlas;
import uk.ac.manchester.tornado.cublas.enums.CuBlasOperation;

TaskGraph graph = new TaskGraph("hybrid")
    .transferToDevice(DataTransferMode.EVERY_EXECUTION, matrix, vector)
    .task("pre",  MyKernels::preprocess, matrix)                        // JIT kernel
    .libraryTask("sgemv", CuBlas::cublasSgemv,                          // native cuBLAS
            CuBlasOperation.CUBLAS_OP_T.operation(),
            m, n, 1.0f, matrix, lda, vector, 1, 0.0f, output, 1)
    .task("post", MyKernels::postprocess, output)                       // JIT kernel
    .transferToHost(DataTransferMode.EVERY_EXECUTION, output);

try (TornadoExecutionPlan plan = new TornadoExecutionPlan(graph.snapshot())) {
    plan.execute();
}
The library task’s position in the chain determines ordering. Buffers written by a preceding JIT task are guaranteed to be visible to the native call — the CUDA stream serializes everything.

Provider Catalog

FP32, TF32, FP16, and BF16 GEMV and GEMM operations. cuBLAS uses column-major storage; for row-major TornadoVM arrays either pass the transpose op (SGEMV) or swap operands (SGEMM).
FactoryOperation
CuBlas::cublasSgemvy = α·op(A)·x + β·y
CuBlas::cublasSgemmC = α·op(A)·op(B) + β·C
CuBlas::cublasSgemmTF32SGEMM on TF32 Tensor Cores
CuBlas::cublasGemmExFP16FP16 inputs, FP16 output, FP32 Tensor Core accumulation
CuBlas::cublasGemmExFP16FP32FP16 inputs, FP32 output, FP32 Tensor Core accumulation
CuBlas::cublasGemmExBF16BF16 inputs and output, FP32 accumulation
CuBlas::cublasSgemmStridedBatchedBatched SGEMM over flat arrays
FP32, FP16, and FP8 matmul with fused bias and activation epilogues via the cuBLASLt API. Plans (descriptors + heuristic-selected algorithm) are created once per problem shape and cached with a 32 MiB device workspace.
FactoryEpilogueNotes
CuBlasLt::ltMatmulFP32NoneFP32 matmul with plan caching
CuBlasLt::ltMatmulFP16NoneFP16 matmul, FP32 Tensor Core accumulation
CuBlasLt::ltMatmulFP8NoneE4M3 operands, FP16 output, TN layout, ld must be multiple of 16 B
CuBlasLt::ltMatmulBiasFP16BIASC = op(A)·op(B) + bias, fused
CuBlasLt::ltMatmulGeluBiasFP16GELU_BIASC = GELU(op(A)·op(B) + bias), tanh approximation, fully fused
1D and 2D FFT transforms with automatic plan caching per (n, batch) pair.
FactoryTransform
CuFft::cufftForwardC2C / cufftInverseC2C1D FP32 complex-to-complex
CuFft::cufftForwardR2C / cufftInverseC2R1D real ↔ complex (Hermitian)
CuFft::cufftForwardZ2Z / cufftInverseZ2Z1D FP64 complex-to-complex
CuFft::cufftForward2dC2C / cufftInverse2dC2C2D FP32 complex-to-complex
FP32/NCHW activations, pooling, and convolution, plus fused FP16 flash attention via the cuDNN graph API.
FactoryOperation
CuDnn::cudnnSoftmaxPer-row numerically stable softmax
CuDnn::cudnnRelu / cudnnSigmoid / cudnnTanhElement-wise activations
CuDnn::cudnnMaxPool2d2D max pooling
CuDnn::cudnnConv2d2D cross-correlation convolution
CuDnn::sdpaForwardFused scaled-dot-product attention (FP16)
FP32 sparse-matrix products over CSR format (32-bit, zero-based indices).
FactoryOperation
Cusparse::cusparseSpMVy = A·x, CSR sparse-dense
Cusparse::cusparseSpMMC = A·B, sparse-dense, row-major output
FP32 SIMT and FP16 Tensor Core GEMM with fused epilogues. Row-major natively — no operand-swap needed.
FactoryOperation
Cutlass::cutlassSgemmFP32 SIMT GEMM
Cutlass::cutlassHgemmFP16 Tensor Core GEMM
Cutlass::cutlassGemmBiasReluFused relu(A·B + bias)
Cutlass::cutlassGemmBiasGeluFused gelu(A·B + bias)

Composition Patterns

The canonical “sandwich” pattern: a JIT kernel preprocesses data, a library call does the heavy compute, and a second JIT kernel post-processes the result — all on the same device buffers with no host round-trip.
TaskGraph graph = new TaskGraph("sandwich")
    .transferToDevice(DataTransferMode.EVERY_EXECUTION, matrix, vector)
    .task("preprocess",  MyKernels::preprocess, matrix)
    .libraryTask("sgemv", CuBlas::cublasSgemv,
            CuBlasOperation.CUBLAS_OP_T.operation(),
            m, n, 1.0f, matrix, lda, vector, 1, 0.0f, output, 1)
    .task("postprocess", MyKernels::activate, output)
    .transferToHost(DataTransferMode.EVERY_EXECUTION, output);

Profiling Library Tasks

Library tasks are profiled through the same mechanism as JIT tasks. Enable the console profiler with the --enableProfiler console flag — each library task reports TASK_KERNEL_TIME (host-timed, bounded by CUDA stream markers) alongside BACKEND, DEVICE, and METHOD.
tornado --enableProfiler console \
  -m tornado.cublas/uk.ac.manchester.tornado.cublas.tests.BenchmarkSgemm 2048 50

Writing Your Own Provider

Adding a new library requires no changes to the TornadoVM core runtime. Providers are discovered via java.util.ServiceLoader. Follow these four steps, mirroring the tornado-cublas module as the reference implementation.
1

Create a Factory class

Build a LibraryTaskDescriptor that declares the library name, function name, parameter list, and per-argument access modes.
public final class MyLib {
    public static final String LIBRARY_NAME = "vendor/mylib";

    public static LibraryTaskDescriptor myOp(int n, FloatArray in, FloatArray out) {
        Access[] access = { Access.READ_ONLY, Access.READ_ONLY, Access.WRITE_ONLY };
        return new LibraryTaskDescriptor()
            .withLibrary(LIBRARY_NAME)
            .withFunction("myOp")
            .withParameters(new Object[] { n, in, out })
            .withAccess(access);
    }
}
2

Implement TornadoLibraryProvider

Create a context per (device, planId), bind the native handle to the CUDA stream, implement prepare() for shape-dependent allocations (idempotent), and dispatch() for the actual native call.
public final class MyProvider implements TornadoLibraryProvider {
    public String libraryName() { return MyLib.LIBRARY_NAME; }

    public boolean canHandle(TornadoXPUDevice device) {
        return device instanceof TornadoNativeStreamSupport;
    }

    public LibraryContext createContext(TornadoXPUDevice device, long planId) {
        long stream = ((TornadoNativeStreamSupport) device).getNativeStream(planId);
        return new MyContext(MyNativeLib.createHandle(), stream);
    }

    public void prepare(LibraryTaskDescriptor d, LibraryContext ctx) {
        // Idempotent: create/cache per-shape plans + workspace BEFORE capture
    }

    public void dispatch(String fn, LibraryInvocation call) {
        long dIn  = call.getDevicePointer(1);   // reference args → raw device pointers
        long dOut = call.getDevicePointer(2);
        int  n    = (int) call.getArg(0);        // scalars → boxed values
        MyNativeLib.myOp(((MyContext) call.getContext()).handle, n, dIn, dOut);
    }

    public void destroyContext(LibraryContext ctx) { /* free handle, plans, workspace */ }
}
3

Register the provider in module-info.java

Declare the service provision in the module descriptor and the corresponding META-INF/services file.
open module tornado.mylib {
    requires transitive tornado.api;
    requires tornado.runtime;
    exports vendor.mylib;
    provides uk.ac.manchester.tornado.runtime.library.spi.TornadoLibraryProvider
        with vendor.mylib.provider.MyProvider;
}
Also add vendor.mylib.provider.MyProvider to: src/main/resources/META-INF/services/uk.ac.manchester.tornado.runtime.library.spi.TornadoLibraryProvider
4

Add a JNI native module

Create a tornado-drivers/mylib-jni CMake module (see cudnn-jni for a host library, cutlass-jni for device-code compilation) under the cuda-backend Maven profile. Wire it into the root pom.xml, tornado-assembly, and tornado.py (--add-modules).The native module is self-guarding: if its shared library is missing at build time, the .so is skipped, and the provider reports UNSUPPORTED at runtime instead of failing the build.

Troubleshooting

Library tasks only work on the CUDA backend. If you see UNSUPPORTED results, verify the active backend with tornado --devices and ensure you built with make BACKEND=cuda.
SymptomCause & Fix
Task reports UNSUPPORTEDDefault device is not CUDA, or the native .so / vendor library is missing. Build with make BACKEND=cuda; install the required library.
UnsatisfiedLinkError: libtornado-<x>Native module was skipped at build time. Set the corresponding *_ROOT environment variable and rebuild.
Wrong result from cuBLASColumn-major mismatch — use transpose op for SGEMV, or swap operands for SGEMM.
CUTLASS FP16 rejects a shapek or n not a multiple of 4 (8-byte alignment constraint). Pad dimensions or switch to cutlassSgemm (FP32, no constraint).
CUDA_ERROR_LAUNCH_FAILED after a Tensor Core callKernel built for the wrong SM. Rebuild with CUDA_ARCH=<your compute capability>.

Build Requirements

ProviderExtra DependencyHow to Install
cuBLAS, cuBLASLt, cuFFT, cuSPARSEBundled with the CUDA ToolkitNothing extra required
cuDNNlibcudnn9 (separate package)apt install libcudnn9-cuda-12 libcudnn9-dev-cuda-12
CUTLASSHeader-only, fetched by CMakeAutomatic via FetchContent (v3.5.1); requires CUDA 12+

Build docs developers (and LLMs) love