cuDNN: Deep Learning Primitives in a Java TaskGraph
Use NVIDIA cuDNN for convolutions, activations, pooling, and fused FP16 flash attention as TornadoVM library tasks — up to 499× faster than JIT kernels.
Use this file to discover all available pages before exploring further.
TornadoVM’s cuDNN integration exposes NVIDIA’s deep-learning primitive library through the familiar TaskGraph API. Convolution layers, activation functions, pooling operations, and fused scaled-dot-product attention (flash attention) become library tasks that share TornadoVM-managed device buffers with adjacent JIT kernels on one CUDA stream. You can build a complete CNN forward pass — convolution, bias addition, activation, and pooling — as a single TaskGraph where each primitive runs with cuDNN’s highly-tuned implementation and the intermediate feature maps never leave the GPU.
Prerequisite: cuDNN 9 must be installed separately. It is not bundled with the CUDA Toolkit.
Activations, pooling, and convolution all use FP32 FloatArray with NCHW layout: batch dimension (N), then channels (C), then spatial height (H), then width (W). TornadoVM flat arrays map directly to this layout — element (n, c, h, w) is at index n*C*H*W + c*H*W + h*W + w.
FP16 / BHSD (Flash Attention)
The sdpaForward factory uses FP16 HalfFloatArray with BHSD layout: batch (B), heads (H), sequence length (S), head dimension (D). The head dimension d must be a multiple of 8 and no larger than 256. Requires an Ampere or newer GPU.
The following example builds a complete CNN block — convolution, JIT bias addition, ReLU activation, and max pooling — as a single TaskGraph. All intermediate feature maps stay on the device.
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.cudnn.CuDnn;// Input: 8 images, 64 channels, 56×56 spatialint n = 8, c = 64, h = 56, w = 56;// Filter: 64 output channels, 3×3 kernelint k = 64, r = 3, s = 3, pad = 1, stride = 1;int outH = (h + 2*pad - r) / stride + 1; // = 56 with pad=1, stride=1int outW = (w + 2*pad - s) / stride + 1; // = 56FloatArray input = new FloatArray(n * c * h * w);FloatArray filter = new FloatArray(k * c * r * s);FloatArray convOut = new FloatArray(n * k * outH * outW);FloatArray bias = new FloatArray(k);FloatArray reluOut = new FloatArray(n * k * outH * outW);FloatArray pooled = new FloatArray(n * k * (outH/2) * (outW/2));// ... fill input, filter, bias ...TaskGraph graph = new TaskGraph("cnn") .transferToDevice(DataTransferMode.EVERY_EXECUTION, input, filter, bias) .libraryTask("conv", CuDnn::cudnnConv2d, input, filter, convOut, n, c, h, w, k, r, s, pad, stride) .task("bias", Layers::addBias, convOut, bias) // JIT kernel .libraryTask("relu", CuDnn::cudnnRelu, convOut, reluOut, n * k * outH * outW) .libraryTask("pool", CuDnn::cudnnMaxPool2d, reluOut, pooled, n, k, outH, outW, 2, 2) .transferToHost(DataTransferMode.EVERY_EXECUTION, pooled);try (TornadoExecutionPlan plan = new TornadoExecutionPlan(graph.snapshot())) { plan.execute();}
The sdpaForward factory implements fused scaled-dot-product attention (flash attention) via the cuDNN graph API. It takes query, key, and value tensors in BHSD layout and produces the attention output in the same layout.
import uk.ac.manchester.tornado.api.types.arrays.HalfFloatArray;// Shape: 4 batches, 16 heads, 1024 query tokens, 128 head dimint b = 4, heads = 16, sQ = 1024, sKv = 1024, d = 128;float scale = 1.0f / (float) Math.sqrt(d);boolean causal = true;HalfFloatArray q = new HalfFloatArray(b * heads * sQ * d);HalfFloatArray k = new HalfFloatArray(b * heads * sKv * d);HalfFloatArray v = new HalfFloatArray(b * heads * sKv * d);HalfFloatArray o = new HalfFloatArray(b * heads * sQ * d);// ... fill q, k, v ...TaskGraph graph = new TaskGraph("sdpa") .transferToDevice(DataTransferMode.EVERY_EXECUTION, q, k, v) .libraryTask("sdpa", CuDnn::sdpaForward, q, k, v, o, b, heads, sQ, sKv, d, scale, causal) .transferToHost(DataTransferMode.EVERY_EXECUTION, o);try (TornadoExecutionPlan plan = new TornadoExecutionPlan(graph.snapshot())) { plan.execute();}
sdpaForward requires the head dimension d to be a multiple of 8 and no larger than 256. It also requires an Ampere or newer GPU (compute capability 8.0+). On older hardware the task will report UNSUPPORTED.
Convolution descriptors, algorithm selection, and the grow-only workspace are created once per shape in the prepare() hook, before CUDA Graph capture begins. This makes all cuDNN library tasks CUDA Graph compatible.
try (TornadoExecutionPlan plan = new TornadoExecutionPlan(graph.snapshot())) { plan.withCUDAGraph(); // iteration 0 captures; subsequent iterations replay for (int i = 0; i < 100; i++) { plan.execute(); }}
cuDNN library tasks can be freely mixed with JIT-compiled tasks in the same TaskGraph. The JIT bias task in the CNN example above writes back to the convOut buffer on the device; the subsequent cudnnRelu call reads that modified buffer directly from device memory — the CUDA stream guarantees ordering.
# Full cuDNN test suite (auto-skips without CUDA backend / libtornado-cudnn)tornado-test -V uk.ac.manchester.tornado.unittests.cudnn.TestCuDnn# CUDA Graph capture test for Conv2dtornado-test -V uk.ac.manchester.tornado.unittests.cudnn.TestCuDnn#testConv2dWithCudaGraph
Results from BenchmarkSdpa are cross-validated against the FP32 JIT attention kernel on the same FP16-rounded inputs, confirming numerical correctness before reporting throughput numbers.
The current cuDNN bindings use the cuDNN legacy compute API (deprecated since cuDNN 9.0, but still fully functional — validated on cuDNN 9.23). The ROADMAP tracks migration to the graph API for additional operations:
FP16/BF16 Tensor Core convolution
cudnnFindConvolutionForwardAlgorithm autotuning (fixed IMPLICIT_PRECOMP_GEMM algorithm is used today)