Overview
Architecture, INDArray interface, memory model, data types, and backend system for the ND4J tensor library
ND4J is the tensor computation library for the JVM. Every numerical operation in the DL4J ecosystem — from simple element-wise arithmetic to distributed GPU training — is executed through ND4J. This page explains what ND4J is, how its memory model works, how the backend system is structured, and how it relates to SameDiff and the rest of the DL4J stack.
What ND4J Is
ND4J stands for N-Dimensional Arrays for Java. It is a scientific computing library designed for production JVM applications, serving the same role that NumPy serves in the Python ecosystem, but with first-class support for:
Off-heap memory and direct interaction with native BLAS libraries (OpenBLAS, oneMKL, cuBLAS)
Hardware acceleration on x86 (AVX2/AVX512), ARM (AArch64), PowerPC (PPC64LE), and NVIDIA GPUs via CUDA
A pluggable backend architecture that lets you swap from CPU to GPU by changing a single Maven/Gradle dependency
Per-array data types rather than a single global precision setting
ND4J is not a neural network library. It provides the tensor substrate on which DL4J and SameDiff are built. You can use ND4J independently for any scientific computing task that benefits from fast native math on the JVM.
Key Design Goals
Production-grade: ND4J is designed for deployment, not just experimentation. The off-heap memory model, workspace-based memory reuse, and native backend are all oriented toward throughput and stability in long-running JVM processes.
API stability: The INDArray Java interface is the stable contract. Backend implementations may change; the interface does not.
Backend transparency: The same Java code runs on CPU or GPU. No conditional logic, no separate code paths.
The INDArray Interface
org.nd4j.linalg.api.ndarray.INDArray is the central abstraction. It represents an N-dimensional array — a tensor — with a numeric DataType, a shape, and data stored off-heap.
You never instantiate INDArray directly. All creation goes through static factory methods on org.nd4j.linalg.factory.Nd4j:
Core Properties
Every INDArray has four defining characteristics:
Rank — the number of dimensions. A scalar has rank 0, a vector rank 1, a matrix rank 2, a batch of images rank 4. There is no upper limit on rank.
Shape — a long[] giving the size of each dimension. A 3x4x5 array has shape [3, 4, 5]. Shape determines which indices are valid.
Length — the total number of elements, equal to the product of the shape dimensions. Shape [3, 4, 5] gives length 60.
Stride — a long[] giving the distance in the underlying flat buffer between adjacent elements along each dimension. Stride is the mechanism that makes views, transposes, and non-contiguous slices possible without copying data.
Shape Introspection
The Nd4j Factory Class
Nd4j Factory ClassNd4j is the entry point for all array creation and for many utility operations. Its most frequently used methods:
Memory Layout: Off-Heap via JavaCPP
Off-Heap Storage
INDArray data is not stored on the JVM heap. It lives in native memory managed by JavaCPP, outside the reach of the garbage collector. The JVM-side INDArray object holds only a small Java pointer; the actual tensor data is in a DataBuffer backed by a direct ByteBuffer or a CUDA memory region.
This design has three significant consequences:
Interoperability with native libraries. OpenBLAS, oneMKL, and cuBLAS all accept raw memory pointers. Off-heap storage means data can be passed to these libraries with zero copy. Matrix multiplications and convolutions call directly into optimised BLAS routines.
No 2^31 element limit. Java arrays (float[], double[]) are indexed by int, capping their size at about 2.1 billion elements. Off-heap buffers use long indexing and have no such limit. A single INDArray can hold tens of billions of elements.
Reduced GC pressure. Large tensors do not participate in garbage collection cycles. This matters for training jobs where GC pauses can be a significant source of latency.
The tradeoff: you must configure both JVM heap memory and off-heap memory explicitly. See the Memory and Workspaces page for JVM launch flags.
C Order and F Order
ND4J supports two physical memory layouts:
C order (row-major) — the default. For a 2D matrix, elements within the same row are contiguous in memory. For a shape [rows, cols] matrix with C order, the strides are [cols, 1]. Moving from element [i, j] to [i, j+1] costs 1 position in the buffer; moving from [i, j] to [i+1, j] costs cols positions. This matches NumPy's default and the layout of C arrays.
F order (column-major) — Fortran order. For a 2D matrix, elements within the same column are contiguous. For a shape [rows, cols] matrix with F order, the strides are [1, rows]. Some BLAS routines return F-order results internally.
Concretely, for a 3x3 matrix:
You can inspect and control ordering:
For most users, the default C order is the right choice. Mixed-order arrays work correctly in all ND4J operations — order is an implementation detail about how data is laid out in memory, not a semantic constraint on what operations are valid.
Strides in Depth
Strides explain exactly how multi-dimensional index tuples map to positions in a flat buffer. For an array with shape [d0, d1, ..., dN] and strides [s0, s1, ..., sN], the buffer offset for element at index [i0, i1, ..., iN] is:
For a contiguous C-order array of shape [3, 4, 5], the strides are [20, 5, 1]. To reach element [1, 2, 3], the offset is 1*20 + 2*5 + 3*1 = 33. This is precisely what makes views efficient: a transposed array or a row slice just changes the strides and/or offset into the same underlying buffer, with no data copy.
Views vs. Copies
Understanding the difference between views and copies is essential for writing correct, efficient ND4J code.
What Is a View?
A view is an INDArray that shares the same underlying DataBuffer as another array. The view may have a different shape, different strides, or a different starting offset into the same buffer, but any modification to the view's data is visible in the original array, and vice versa.
Many common operations return views rather than copies:
getRow(int)— row slicegetColumn(int)— column slicetranspose()— dimension reorderingreshape(long...)— shape change (when possible)get(NDArrayIndex...)— sub-array access
Transpose Is a View
transpose() returns a view with swapped strides — no data is moved:
If you need an independent transposed copy: bigMat.transpose().dup().
Reshape: Views When Possible
reshape returns a view when the array is contiguous in memory, and a copy otherwise:
After a transpose(), the array is no longer contiguous, so reshape on a transposed array will produce a copy:
Making Explicit Copies
Use dup() when you need an independent array with the same values:
You can also request a specific memory order in the copy:
In-Place vs. Out-of-Place Operations
ND4J follows a naming convention that matters especially when working with views:
Methods ending in
i(addi,muli,subi,divi) are in-place: they modify the receiver and return it. The returned object is the same Java instance.Methods without the
isuffix (add,mul,sub,div) are out-of-place: they allocate a new array, leave the receiver unchanged, and return the new array.
Be careful with in-place operations on views. Calling addi on a view modifies the original array's data. This is often what you want (for example, matrix.getRow(0).addi(1.0) to increment the first row in place), but it can also introduce subtle bugs if you forget a variable is a view.
Data Types
Every INDArray has a data type represented by the org.nd4j.linalg.api.buffer.DataType enum. In M2.1, data types are per-array — different arrays in the same JVM can have different types simultaneously.
Available Types
Floating point:
DOUBLE
64
IEEE 754 double precision
FLOAT
32
IEEE 754 single precision — default
FLOAT16
16
Half precision (alias: HALF)
BFLOAT16
16
Brain float — wider exponent range than FLOAT16
Signed integer:
INT64
64
LONG
INT32
32
INT
INT16
16
SHORT
INT8
8
BYTE
Unsigned integer:
UINT64
64
—
UINT32
32
—
UINT16
16
—
UINT8
8
UBYTE
Other: BOOL, UTF8
Migration from Earlier Releases
Prior to M2.1, ND4J used DataBuffer.Type and a single global type setting:
Replace all occurrences of DataBuffer.Type with DataType when migrating.
Default Data Type and Global Configuration
The default type for newly created arrays is FLOAT. To change the default at application startup:
Call this once before any array creation. All subsequent Nd4j.zeros(...), Nd4j.rand(...), etc. calls that do not specify a type will use the new default.
Creating Typed Arrays
For full coverage of type semantics, casting rules, and best practices for mixed-precision workflows, see the Data Types page.
Creating NDArrays: Reference
Zeros, Ones, and Scalar Fill
From Java Arrays
Random Arrays
Sequences and Structured Arrays
From Other NDArrays
Getting and Setting Values
Individual Elements
Iterating element by element is slow. Prefer vectorised operations whenever possible.
Rows and Columns
Sub-Arrays with NDArrayIndex
NDArrayIndex provides flexible sub-array access for arbitrary dimensionality:
NDArrayIndex.interval, NDArrayIndex.point, and NDArrayIndex.all return views. Use .dup() on the result if you need a copy.
Key Operations
Scalar Operations
Add, subtract, multiply, divide every element by a constant:
Element-Wise Operations
Reductions
Reductions can run over the entire array or along specific dimensions:
Linear Algebra
Element-Wise Transforms
Reshape, Flatten, and Permute
Architecture: The Backend System
Overview
ND4J uses a Service Provider Interface (SPI) to decouple the Java API from the native implementation. The INDArray interface and Nd4j factory class are defined in the nd4j-api module and carry no native code. The actual computation is provided by a backend JAR that is discovered at runtime via java.util.ServiceLoader.
Two production backends ship with M2.1:
nd4j-native
org.nd4j:nd4j-native
CPU (x86, ARM, PPC)
nd4j-cuda
org.nd4j:nd4j-cuda-12.x
NVIDIA GPU via CUDA
Exactly one backend should be on the classpath at runtime.
nd4j-native (CPU Backend)
nd4j-native links against libnd4j, the C++ kernel library, via JavaCPP. It supports:
x86_64 with AVX2 acceleration (default)
x86_64 with AVX512 acceleration (via the
avx512classifier)AArch64 (ARM 64-bit)
PPC64LE (IBM Power)
The native platform binaries are bundled in classifier JARs. If you let Maven/Gradle resolve the platform automatically, the right native binary is pulled for your OS and CPU.
nd4j-cuda (GPU Backend)
nd4j-cuda links against libnd4j compiled for CUDA and uses cuBLAS and cuDNN for accelerated operations. Requirements:
NVIDIA GPU with CUDA Compute Capability 3.5 or higher
CUDA toolkit installed and matching the artifact version (
12.xfor M2.1)
From a Java code perspective, switching from CPU to GPU is a dependency swap only — no source changes required. All Nd4j.* calls, all INDArray operations, and all DL4J/SameDiff code work identically on both backends.
SPI Mechanism
At startup, Nd4j calls ServiceLoader.load(NDArrayFactory.class) to discover the backend. The factory loaded from the classpath determines the concrete INDArray implementation and the native dispatch layer. If no factory is found, Nd4j throws a RuntimeException immediately.
You can query the active backend at runtime:
libnd4j (C++ Layer)
Below the Java backend lies libnd4j — the C++ kernel library. It provides all the compute kernels: element-wise ops, BLAS calls, convolutions, reductions, and RNG. libnd4j is compiled separately for each target platform and bundled inside the classifier JARs. It is not a user-facing API; you do not need to interact with it directly.
Its existence matters for two scenarios:
Native crash diagnosis. If ND4J throws a
java.lang.UnsatisfiedLinkErroror the JVM crashes with a native stack trace, libnd4j is involved. Check that the classifier JAR for your OS/CPU/CUDA version is on the classpath.Building from source. If you need to add a custom kernel or support a new hardware target, libnd4j is where you write the C++ code.
Workspaces and Memory Management
Workspaces are ND4J's mechanism for reusing native memory allocations across iterations of a processing loop. Rather than allocating and deallocating off-heap memory on each training step, a workspace pre-allocates a memory block and recycles it.
For a complete treatment see the Memory and Workspaces page. The summary:
Important: arrays allocated inside a workspace are invalid after the workspace closes. Use INDArray.detach() to move an array out of a workspace into regular heap-managed off-heap memory when you need to retain it:
Serialization
ND4J supports saving and loading INDArrays in binary, text, and NumPy-compatible formats.
The nd4j-serde module also provides Jackson, Kryo, and Aeron serializers for integration with common Java serialization frameworks.
Relationship to SameDiff
SameDiff is ND4J's automatic differentiation framework. It lives in the nd4j-api module alongside INDArray and shares the same backend infrastructure. The key distinction:
Execution
Immediate — operations run when called
Deferred — operations build a graph, execution triggered separately
Gradients
Not automatic — you implement backprop manually
Automatic — call sd.execBackwards()
Use case
Data preprocessing, feature engineering, one-off computations
Neural network training, optimisation loops
Primary type
INDArray
SDVariable (wraps INDArray at execution time)
SameDiff operates on the same backends and produces INDArray results when executed:
DL4J's MultiLayerNetwork and ComputationGraph are built on SameDiff internally as of M2.1. Custom layers and loss functions can be written in either the INDArray eager API or the SameDiff graph API.
For a full treatment of SameDiff, including defining custom operations, exporting to ONNX/TensorFlow SavedModel, and mixed-precision training, see the SameDiff section.
Capability Map
The following table maps common tasks to the relevant ND4J API and links to more detailed pages in this section.
Math operations
INDArray.*, Transforms.*, Nd4j.getExecutioner()
Operations (forthcoming)
Linear algebra
mmul, transpose, InvertMatrix
Operations (forthcoming)
Quick Reference
Creating Arrays
Shape and Type
Indexing
Operations
Serialization
Last updated
Was this helpful?