TornadoVM’s hybrid API is intentionally open: any vendor-optimized native GPU library can be plugged in as a library provider without touching a single line of the core runtime. The extension point is a standardDocumentation 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.
java.util.ServiceLoader Service Provider Interface (SPI) — your module declares what it provides, the runtime discovers it automatically, and library tasks in a TaskGraph dispatch through it just like the built-in cuBLAS, cuDNN, cuFFT, cuSPARSE, and CUTLASS providers. This page walks through the complete four-step process and documents every SPI type, contract, and layout detail you need to build a production-quality provider.
Architecture Overview
When aTaskGraph reaches a .libraryTask(...) node at execution time, the TornadoVM interpreter:
- Resolves each reference argument to the raw device pointer of its TornadoVM-managed buffer (skipping the 24-byte array header).
- Looks up the registered
TornadoLibraryProviderwhoselibraryName()matches the descriptor’sgetLibraryName()string. - Calls
prepare()(before CUDA Graph capture) to let the provider allocate any shape-dependent plans or workspaces. - Calls
dispatch()at launch time, passing aLibraryInvocationwith all arguments already resolved.
Library tasks ride the same CUDA stream as surrounding JIT kernels. There is no host synchronization between JIT tasks and native calls — everything executes in-order on one stream, which is also what makes CUDA Graph capture possible.
Step 1 — The Factory: Build a LibraryTaskDescriptor
The factory is a plain Java class with static methods, one per supported operation. Each method constructs aLibraryTaskDescriptor that describes the call — what library handles it, what function to invoke, what the arguments are, and their access modes.
.withLibrary()
Sets the provider ID string. Must match exactly what
TornadoLibraryProvider.libraryName() returns — this is the lookup key..withFunction()
Names the operation. Passed verbatim to
dispatch(String functionName, ...), so a single provider can multiplex many operations..withParameters()
The argument array in the same order as the factory method parameters. Reference types (
FloatArray, HalfFloatArray, …) are resolved to device pointers; boxed primitives are passed through unchanged..withAccess()
The
Access[] array drives TornadoVM’s data-flow analysis: which buffers to transfer to/from the device and when. Length must equal withParameters() array length.TaskGraph exactly like a built-in provider:
Step 2 — The Provider: Implement TornadoLibraryProvider
The provider is the bridge between the TornadoVM runtime and your native library. It manages a per-(device, executionPlan) context object and performs the actual dispatch.
The prepare() Contract
Key SPI Types Reference
TornadoLibraryProvider
TornadoLibraryProvider
The top-level interface your module implements. Registered via
ServiceLoader. Methods:| Method | Called when |
|---|---|
libraryName() | At dispatch-lookup time to match the descriptor’s library ID |
canHandle(device) | During plan construction to skip unsupported devices |
createContext(device, planId) | Once per (device, TornadoExecutionPlan) pair |
prepare(descriptor, context) | Before every launch region, before CUDA Graph capture |
dispatch(functionName, invocation) | At LAUNCH bytecode execution for each library task |
destroyContext(context) | When the execution plan is closed / garbage-collected |
LibraryContext
LibraryContext
A marker interface — implement it with your own class carrying the native handle, CUDA stream reference, plan cache, and any pre-allocated workspace. The runtime stores one instance per
(device, executionPlanId) pair and passes it back to prepare() and dispatch() on every invocation.LibraryInvocation
LibraryInvocation
The per-call payload delivered to
dispatch():| Method | Returns |
|---|---|
getArg(i) | Original Java argument at index i (boxed primitive or host-side array object) |
getDevicePointer(i) | Raw GPU device pointer for reference argument i (past the 24-byte header) |
isReference(i) | true if argument i is a TornadoVM off-heap array |
getContext() | The LibraryContext returned by createContext() |
getTuning() | Opaque object from LibraryTaskDescriptor.withTuning(...), or null |
isCapturing() | true during CUDA Graph capture — avoid allocations if true |
TornadoNativeStreamSupport
TornadoNativeStreamSupport
Implemented by the CUDA backend device. Use it in Pass
canHandle() to restrict your provider to CUDA, and in createContext() to obtain the native stream:stream to your library’s setStream equivalent (e.g., cublasSetStream, cudnnSetStream) so all native calls run in-order with the surrounding JIT kernels.Step 3 — Registration
Registration is two lines in two files. Both must reference the same fully-qualified provider class name.module-info.java (in your module’s src/main/java/):
META-INF/services/uk.ac.manchester.tornado.runtime.library.spi.TornadoLibraryProvider (in src/main/resources/):
Both the
provides directive in module-info.java and the META-INF/services file are required. The JPMS module system uses the provides clause; ServiceLoader in unnamed-module contexts uses the META-INF file as a fallback.Step 4 — Native Binding: CMake Module
The Java side is complete — now wire up the native.so. Add a tornado-drivers/mylib-jni/ directory following the pattern of an existing JNI module:
- Host library (like cudnn-jni)
- Device-code library (like cutlass-jni)
Use this layout when your library is a pre-built
.so (cuDNN, cuBLAS, etc.) that you link against:tornado-drivers/pom.xml— add a<module>mylib-jni</module>entry under thecuda-backendprofile.- Root
pom.xml— add the module to thecuda-backendprofile’s<modules>list. tornado-assembly/assembly.xml— include the compiled.soin the distribution archive.tornado.pylauncher — addtornado.mylibto--add-moduleswhen the CUDA backend is present.
Reference: Data Layout, Types, and Alignment
Getting data layout wrong produces silently incorrect numerical results, which is harder to debug than a crash. Use this table before writing a single line of native code.Row-major providers
CUTLASS and cuSPARSE treat matrices as row-major — the same layout TornadoVM
FloatArray and HalfFloatArray use natively. No transposition is needed; pass arrays directly.Column-major providers
cuBLAS is column-major by convention. For row-major TornadoVM inputs either pass the transpose operation flag (e.g.,
CUBLAS_OP_T for SGEMV) or swap the operand order (e.g., swap A and B for SGEMM).TornadoVM Array Types
| Java type | Precision | Notes |
|---|---|---|
FloatArray | FP32 | Standard single-precision; use for cuBLAS SGEMM / SGEMV, CUTLASS SIMT |
HalfFloatArray | FP16 | Elements via new HalfFloat(float) / .get(i).getFloat32(); use for tensor-core paths |
DoubleArray | FP64 | Use for cuFFT Z2Z double-precision transforms |
Device Pointer Arithmetic
getDevicePointer(i) it already adds this offset — your dispatch() receives the pointer to the first data element and should pass it directly to the native API.
Alignment: All TornadoVM device buffers are guaranteed to be 8-byte aligned after the header. For FP16 CUTLASS kernels (which use 4-half vector loads), k and n must therefore be multiples of 4 — the factory should validate this and throw with a clear message for non-conforming shapes.
Existing Providers as Reference Implementations
The six built-in providers cover the full range of implementation patterns. Study the one closest to your use-case before starting:tornado-cublas/
Dense FP32/FP16/TF32 GEMM and GEMV. Shows column-major transpose handling,
beta != 0 READ_WRITE access, and the batched-strided variant.tornado-cudnn/
Convolution, pooling, activations, softmax, and SDPA. The most complex
prepare() implementation — per-shape cuDNN convolution descriptor cache + workspace allocation.tornado-cufft/
Per-
(n, batch) plan cache, real↔complex (R2C/C2R) and 2D transforms. Clean example of minimal provider state.tornado-cutlass/
Device-code compilation via CMake
FetchContent, fused epilogues (bias + ReLU/GELU), and FP16 alignment validation in the factory.tornado-cusparse/
CSR sparse matrix products. Shows pre-allocated fixed-size workspace in
prepare() and the 8 MiB workspace CUDA Graph constraint.tornado-cufft/ (FP64)
Z2Z double-precision FFT. Minimal delta from the FP32 provider — good template for adding a new precision variant to an existing library.