SameDiff
Automatic differentiation framework in ND4J — define-and-run computation graphs, comparison with MultiLayerNetwork and ComputationGraph
SameDiff is the automatic differentiation (autograd) framework built into ND4J. It lets you define mathematical computation graphs in Java, execute them against real data, and compute gradients automatically — without writing any backpropagation code by hand.
What SameDiff Is
At its core, SameDiff represents a computation as a directed acyclic graph (DAG) where:
Nodes are variables (
SDVariableinstances) holding arrays of numbers.Edges are operations that consume one or more input variables and produce an output variable.
When you write code like:
SameDiff sd = SameDiff.create();
SDVariable x = sd.placeHolder("x", DataType.FLOAT, -1, 784);
SDVariable w = sd.var("w", DataType.FLOAT, 784, 10);
SDVariable b = sd.var("b", DataType.FLOAT, 10);
SDVariable logits = x.mmul(w).add(b);
SDVariable output = sd.nn.softmax("output", logits);you are defining the graph, not executing it. No numeric computation happens yet. The graph is a blueprint that SameDiff stores internally. Execution happens separately when you call output(), exec(), or fit().
This approach is called define-and-run (as opposed to the eager evaluation model where each line immediately computes a result).
Automatic Gradient Computation
The major payoff of building a computation graph is that SameDiff can traverse it in reverse to compute gradients with respect to any variable automatically. When training, SameDiff:
Runs the forward pass (evaluates all nodes in topological order).
Computes the scalar loss value.
Runs the backward pass (applies the chain rule through each op in reverse order).
Updates trainable
VARIABLE-type parameters using the configured optimizer.
You never implement backward() methods. The gradients for every built-in operation are pre-registered in the framework.
Key Classes
SameDiff
The graph container. Holds all variables, ops, and training configuration. Create one with SameDiff.create().
SDVariable
A node in the graph. Wraps an INDArray (when values are available) and knows its position in the graph.
TrainingConfig
Bundles the optimizer, loss variable name, data-type mappings, and listener list for a training run.
History
Returned by fit(); records loss and metric values epoch by epoch.
InferenceSession
Low-level execution engine; usually used indirectly via sd.output().
When to Use SameDiff vs MultiLayerNetwork / ComputationGraph
DL4J provides three ways to build neural networks. Choose based on your needs:
MultiLayerNetwork
Use when your network is a simple sequential stack of layers. It is the easiest API:
Best for: standard feedforward networks, CNNs with a single input/output, beginners.
ComputationGraph
Use when your network has multiple inputs, multiple outputs, skip connections, or branching paths (e.g. encoder-decoder, Siamese networks). Still configuration-driven but more flexible than MultiLayerNetwork.
Best for: complex topologies that can still be described with DL4J's built-in layer types.
SameDiff
Use when you need:
Custom operations or loss functions that have no counterpart in the DL4J layer catalogue.
Research and experimentation where you want full symbolic control over every operation.
Fine-grained weight sharing or unusual parameter tying.
Importing and fine-tuning TensorFlow/ONNX models — the model import pipeline internally produces SameDiff graphs.
SameDiff is more verbose than the DL4J layer APIs but gives you complete flexibility over every computation in your model.
Building a Simple Neural Net in SameDiff
Here is a complete minimal example of a one-hidden-layer network for MNIST classification, from graph definition through to a training loop.
Step 1: Define the graph
Step 2: Configure training
Step 3: Train
Step 4: Run inference
How the Graph Executes
When sd.output() is called, SameDiff internally uses an InferenceSession that:
Resolves which nodes need to be computed in order to produce the requested output variables.
Determines a valid topological execution order.
Evaluates each op in that order, passing intermediate results through the graph.
Returns the values of the requested output nodes.
Only the ops necessary to compute the requested outputs are evaluated — unreachable subgraphs are skipped.
Graph Inspection
SameDiff provides several utilities for inspecting the graph you have built:
Thread Safety and Multiple Graphs
Each SameDiff instance is a self-contained graph. You can have multiple SameDiff instances in the same JVM, but variables from one instance cannot be mixed with variables from another. All SDVariable objects carry a reference back to their owning SameDiff.
SameDiff instances are not thread-safe for concurrent mutation. For inference in a multi-threaded server environment, either synchronise access or keep a pool of separate SameDiff instances loaded from the same saved file.
User-Defined Functions / Custom Ops (ADR 0023)
SameDiff lets you define your own operations — called User-Defined Functions (UDFs) — that participate in the graph the same way built-in ops do. UDFs are automatically saved and loaded with the graph, support gradient computation for training, and integrate with DSP compilation.
When to Use UDFs
Use a UDF when you need an operation that has no counterpart in the built-in op catalogue, you want a custom backward pass for training, or you want to wrap a third-party kernel or hardware-specific routine as a first-class graph node.
Annotating and Registering a UDF
Annotate your class with @UserDefinedOp. The annotation scanner discovers all annotated classes on the classpath and registers them with the op registry automatically, so they are available when a saved graph is reloaded.
Using a UDF in a Graph
Pass an instantiated UDF to sd.doUdf(). It returns SDVariable outputs wired into the graph exactly like any built-in op.
Optional Properties
If your UDF has configuration parameters (e.g., a threshold, kernel size), expose them via propertiesForFunction() and setPropertiesForFunction(). These are serialized alongside the graph structure:
Graph Tracing with Nd4j.graphScope()
Nd4j.graphScope()GraphScope lets you write ordinary eager ND4J code and have it automatically traced into a SameDiff graph, compiled into a DSP plan, and executed in a single optimized pass — with plan caching so the compilation cost is paid only once.
This is useful when you have a performance-critical compute kernel written using standard Nd4j.* calls and want it to benefit from DSP's CUDA graph capture and graph optimization without rewriting it in the SameDiff define-and-run style.
How It Works
Nd4j.graphScope()returns aGraphScopeinstance.Calling
scope.begin()starts tracing on the current thread.Every
Nd4j.exec()call intercepted during tracing records aLazyINDArrayproxy instead of executing immediately. The proxy carries the inferred output shape and data type so it can be passed to subsequent ops.scope.end()compiles the traced ops into aDynamicShapePlan, executes it, and materializes allLazyINDArrayproxies with real values.scope.close()(called automatically bytry-with-resources) cleans up resources.
Basic Usage
During tracing, mm is a LazyINDArray — it knows its shape but contains no data. After scope.end(), it is a fully materialized INDArray backed by real GPU/CPU memory.
Reusable Compiled Functions with Nd4j.compile()
Nd4j.compile()For a function you want to call many times (e.g., a custom layer forward pass), use Nd4j.compile() to get a CompiledGraphFunction. The first call traces and compiles; subsequent calls with the same op sequence replay the cached DSP plan.
Key Classes
GraphScope
Trace-compile-replay coordinator. Use via Nd4j.graphScope().
LazyINDArray
Proxy returned during tracing. Carries shape/dtype metadata; data access before scope.end() throws. Delegates to the real array after materialization.
CompiledGraphFunction
Wraps a GraphFunction lambda with a persistent GraphScope and DSP plan cache. Use via Nd4j.compile().
Limitations
Nested
GraphScopetraces on the same thread are not supported.Operations involving Java-side conditional logic (e.g.,
if (someArray.getDouble(0) > 0.5)) cannot be captured as graph nodes — only ND4J op calls are intercepted.If DSP compilation fails (e.g., due to control flow),
GraphScopefalls back to standard SameDiff interpreted execution automatically.
Next Steps
Variables — learn about
SDVariabletypes (VARIABLE,CONSTANT,PLACEHOLDER,ARRAY), data types, and type conversion.Operations — explore the op namespaces:
sd.math,sd.nn,sd.cnn,sd.rnn,sd.loss,sd.random.Training — configure
TrainingConfig, runfit(), and track progress withHistory.Execution and Inference — understand
sd.output(), placeholder binding, and batch inference.Serialization — save and load graphs with
sd.save()andSameDiff.load().
Last updated
Was this helpful?