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.

TornadoVM’s cuBLAS integration brings NVIDIA’s production-grade dense linear algebra library into the Java TaskGraph model. Rather than calling cuBLAS through a separate JNI layer with manually managed device memory, you express GEMM and GEMV operations as library tasks — first-class TaskGraph nodes that share TornadoVM-managed device buffers with your JIT-compiled kernels on one CUDA stream. The cuBLASLt variant extends this with fused epilogues (BIAS, GELU_BIAS) that collapse a GEMM plus a separate activation kernel into a single highly-optimized dispatch, which is especially valuable in the launch-bound regime of LLM token generation.
Prerequisite: CUDA Toolkit with cuBLAS (libcublas). cuBLAS ships inside the CUDA Toolkit — no separate package is needed. Build TornadoVM with make BACKEND=cuda.

cuBLAS Factory Methods

All factories are static methods on uk.ac.manchester.tornado.cublas.CuBlas. Pass them as the second argument to taskGraph.libraryTask(id, factory, args...).
FactorycuBLAS FunctionSemantics
cublasSgemv(op, m, n, α, A, lda, x, incx, β, y, incy)cublasSgemvy = α·op(A)·x + β·y
cublasSgemm(opA, opB, m, n, k, α, A, lda, B, ldb, β, C, ldc)cublasSgemmC = α·op(A)·op(B) + β·C
cublasSgemmTF32(...)cublasSgemm + TF32 math modeSame as SGEMM, executed on TF32 Tensor Cores (~1e-4 rel. error)
cublasGemmExFP16(...)cublasGemmExFP16 inputs, FP16 output, FP32 Tensor Core accumulation
cublasGemmExFP16FP32(...)cublasGemmExFP16 inputs, FP32 output, FP32 Tensor Core accumulation
cublasGemmExBF16(...)cublasGemmExBF16 inputs and output, FP32 accumulation (BFloat16Array)
cublasSgemmStridedBatched(...)cublasSgemmStridedBatchedC[i] = α·op(A[i])·op(B[i]) + β·C[i] over a flat-array batch

cuBLASLt Factory Methods

Fused-epilogue GEMM is available through uk.ac.manchester.tornado.cublas.CuBlasLt. Plans (descriptors + heuristic-selected algorithm) are created once per problem shape and cached with a 32 MiB device workspace.
FactoryEpilogueNotes
ltMatmulFP32(opA, opB, m, n, k, α, A, lda, B, ldb, β, C, ldc)NoneFP32 matmul with plan caching
ltMatmulFP16(...)NoneFP16 matmul, FP32 Tensor Core accumulation
ltMatmulFP8(...)NoneE4M3 operands, FP16 output, TN layout, ld must be multiple of 16 B
ltMatmulBiasFP16(..., bias)BIASC = op(A)·op(B) + bias, fused
ltMatmulGeluBiasFP16(..., bias)GELU_BIASC = GELU(op(A)·op(B) + bias), tanh approximation, fully fused

Column-Major Layout

cuBLAS is column-major. TornadoVM arrays are row-major. The standard tricks used throughout the tests:
A row-major weight matrix W of shape (d × n) is the column-major matrix (n × d) with lda = n. Compute y = W·x using the transpose op:
// W is stored row-major as (d rows × n cols)
// cuBLAS sees it as column-major (n × d) — pass CUBLAS_OP_T and swap m/n
taskGraph.libraryTask("sgemv", CuBlas::cublasSgemv,
        CuBlasOperation.CUBLAS_OP_T.operation(),
        n,           // rows of op(A) = cols of the row-major W
        d,           // cols of op(A) = rows of the row-major W
        1.0f,
        W, n,        // lda = n (leading dimension of the row-major layout)
        x, 1,
        0.0f, y, 1);
When β != 0, cuBLAS reads the output operand before writing. The factory marks it READ_WRITE automatically. Include it in transferToDevice if its initial values come from the host (see TestCuBlasSgemvBeta).

Complete SGEMM Example

The following example runs a complete matrix multiply through a cuBLAS library task inside a TaskGraph, cross-validates against a sequential Java result, and measures throughput.
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;

int size = 2048;
FloatArray matrixA = new FloatArray(size * size);
FloatArray matrixB = new FloatArray(size * size);
FloatArray output  = new FloatArray(size * size);

// ... fill matrixA and matrixB with data ...

// Row-major C = A·B using the operand-swap trick
TaskGraph graph = new TaskGraph("sgemm")
    .transferToDevice(DataTransferMode.FIRST_EXECUTION, matrixA, matrixB)
    .libraryTask("sgemm", CuBlas::cublasSgemm,
            CuBlasOperation.CUBLAS_OP_N.operation(),
            CuBlasOperation.CUBLAS_OP_N.operation(),
            size, size, size,
            1.0f, matrixB, size, matrixA, size,
            0.0f, output,  size)
    .transferToHost(DataTransferMode.EVERY_EXECUTION, output);

try (TornadoExecutionPlan plan = new TornadoExecutionPlan(graph.snapshot())) {
    plan.execute();
}
Run the built-in benchmark (RTX 4090 reference numbers below):
# args: [size] [iterations]
tornado -m tornado.cublas/uk.ac.manchester.tornado.cublas.tests.BenchmarkSgemm 2048 50

Performance Reference

All numbers below are from an NVIDIA GeForce RTX 4090 with CUDA 12.6.

SGEMM Throughput

SizeJIT KernelcuBLAS FP32cuBLAS TF32cuBLAS FP16
10243.9 TFLOP/s26.1 TFLOP/s28.2 TFLOP/s44.1 TFLOP/s
20486.0 TFLOP/s48.3 TFLOP/s62.8 TFLOP/s120.6 TFLOP/s
40965.8 TFLOP/s57.0 TFLOP/s81.1 TFLOP/s160.6 TFLOP/s
160.6 TFLOP/s is the RTX 4090’s peak FP16 Tensor Core throughput — reached from Java by changing only the factory name.

Fused Epilogue Speedup (cuBLASLt)

BenchmarkLtFusedMlp: fused GELU_BIAS vs. unfused GemmEx FP16 + JIT kernel.
SizeUnfusedFusedSpeedup
102417.5 TFLOP/s29.4 TFLOP/s1.68×
204873.8 TFLOP/s98.3 TFLOP/s1.33×
4096132.1 TFLOP/s147.3 TFLOP/s1.11×
Fusion pays most at small/medium sizes — the launch-bound regime of LLM decoding.

TF32 and FP16 Tensor Cores

TornadoVM exposes Tensor Core acceleration through named factory variants — no code restructuring required.
TF32 uses the same FloatArray inputs as FP32 but routes through Tensor Cores via the CUBLAS_TF32_TENSOR_OP_MATH math mode. Expect ~1e-4 relative error compared to FP32.
// Switch from FP32 to TF32 by changing only the factory name
taskGraph.libraryTask("sgemm_tf32", CuBlas::cublasSgemmTF32,
        CuBlasOperation.CUBLAS_OP_N.operation(),
        CuBlasOperation.CUBLAS_OP_N.operation(),
        n, m, k, 1.0f, matrixB, n, matrixA, k, 0.0f, output, n);

Strided Batched GEMM

One call launches a whole batch of equally-shaped GEMMs stored as contiguous slices in flat arrays.
int batchCount = 32;
long stride = (long) size * size;   // elements between consecutive matrices in the flat array

taskGraph.libraryTask("batched", CuBlas::cublasSgemmStridedBatched,
        CuBlasOperation.CUBLAS_OP_N.operation(),
        CuBlasOperation.CUBLAS_OP_N.operation(),
        size, size, size,
        1.0f,
        matrixB, size, stride,
        matrixA, size, stride,
        0.0f,
        matrixC, size, stride,
        batchCount);

Unit Tests and Runnable Examples

# Full cuBLAS test suite (auto-skips without CUDA backend)
tornado-test -V uk.ac.manchester.tornado.unittests.cublas.TestCuBlas

# cuBLASLt fused epilogue tests
tornado-test -V uk.ac.manchester.tornado.unittests.cublas.TestCuBlasLt
TestCuBlas#testSharedBufferAcrossTaskGraphs demonstrates the cross-graph shared-buffer pattern: a JIT task graph produces the matrix on the device, and a second graph consumes it with a cuBLAS call — the data never returns to the host between the two graphs.

Build docs developers (and LLMs) love