For the complete documentation index, see llms.txt. This page is also available as Markdown.

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 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 slice

  • getColumn(int) — column slice

  • transpose() — dimension reordering

  • reshape(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 i suffix (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:

Type
Bits
Notes

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:

Type
Bits
Alias (deprecated)

INT64

64

LONG

INT32

32

INT

INT16

16

SHORT

INT8

8

BYTE

Unsigned integer:

Type
Bits
Alias (deprecated)

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:

Backend
Maven artifact
Target hardware

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 avx512 classifier)

  • 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.x for 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:

  1. Native crash diagnosis. If ND4J throws a java.lang.UnsatisfiedLinkError or 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.

  2. 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:

INDArray (eager)
SameDiff (graph)

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.

Task
API entry point
Detail page

Array creation and destruction

Nd4j.* factory methods

Data types and casting

DataType enum, arr.castTo()

Element access and slicing

INDArrayIndex, getRow, get

Math operations

INDArray.*, Transforms.*, Nd4j.getExecutioner()

Operations (forthcoming)

Linear algebra

mmul, transpose, InvertMatrix

Operations (forthcoming)

Off-heap memory

JavaCPP, DataBuffer

Workspace-based memory reuse

Nd4j.getWorkspaceManager()

Backend selection (CPU/GPU)

Maven/Gradle dependency

Automatic differentiation

SameDiff, SDVariable


Quick Reference

Creating Arrays

Shape and Type

Indexing

Operations

Serialization

Last updated

Was this helpful?