Open-source project
raskr/rust-autograd avatar
raskr/rust-autograd

rust-autograd: Reverse-Mode Differentiation on ndarray Without a Framework

Tensors and differentiable operations (like TensorFlow) in Rust

503 stars39 forksRustMIT

At a glance

What is it?
rust-autograd is a small Rust crate that adds differentiable tensors and a computation graph on top of ndarray. It suits people who want gradients in Rust, not a full training framework, and its BLAS feature choices matter more than its API surface suggests.
Who is it for?
Adopt rust-autograd if you already build on ndarray and want gradients and a graph without pulling in a Python runtime or a large framework. Do not adopt it if you need ready-made layers, optimizers beyond what the README shows, or a stable release line, since no releases were retrieved.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 106 days ago.
What is it written in?
Mainly Rust, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on September 15, 2026, and from our analysis. They are not legal advice.

DEEP OPEN-SOURCE ANALYSIS

The gap rust-autograd fills between ndarray and a full framework

ndarray gives you n-dimensional arrays and the arithmetic that goes with them. It does not give you gradients. If you want to train anything in Rust, you either write the backward pass by hand or you take a dependency that brings a whole training stack with it. rust-autograd sits in the middle. The README describes it as tensors and differentiable operations backed by ndarray, which is the whole pitch: keep ndarray as the array type, add a graph and reverse-mode differentiation on top.

The audience is narrow but real. It is for Rust programmers who have a numerical workload where a derivative is needed, and who do not want to leave the language to get it. The README's own framing is explicit that the crate offers low-level features inspired by TensorFlow and Theano to train neural networks, so the intent is framework-shaped, but the primitives stay low level. There are no layer abstractions in the material shown. You assemble matmul, bias add, cross entropy and a gradient call yourself, which is exactly what the MNIST example does.

How the graph, placeholders and evaluator actually fit together

The core object is a Context. The run function takes a closure over a mutable Context, and everything you build lives inside that closure. Tensors are created from the context: ctx.placeholder("x", &[]) makes an input node, ctx.variable("w") reads a named variable, and operations like matmul, reduce_mean and sparse_softmax_cross_entropy build new nodes from existing ones.

Gradients come from grad. You pass a slice of target tensors and a slice of variables to differentiate with respect to, and it returns a slice of gradient tensors. The README example for z = 2x^2 + 3y + 1 asks for dz/dy first, evaluates it, and gets 3. That evaluation happens with no feed because y never needed a value for that partial. Then it asks for dz/dx, which does need x, so the example uses ctx.evaluator().push(gx).feed(x, feed.view()).run() and reads index zero. The distinction matters: eval works when nothing is missing, and the evaluator gives you explicit control over feeds when something is.

The example then differentiates the gradient itself. grad(&[gx], &[x]) produces a second derivative, and the README shows it evaluating to 4. That is reverse-mode differentiation applied to a graph that already contains a gradient node, which is the normal consequence of building gradients as ordinary graph nodes rather than as a separate pass.

Variables, namespaces and the optimizer registration step

The neural network example introduces a second layer of state. A VariableEnvironment holds named variables. You call env.name("w").set(...) with an initial value, and the README uses ArrayRng::<f32>::default() with glorot_uniform for the weight matrix and zeros for the bias. The environment is where the parameters live between steps, separate from the graph that reads them.

Optimizers are registered against that environment. The Adam example constructs Adam::default("my_adam", env.default_namespace().current_var_ids(), &mut env), which ties the optimizer instance to the current set of variable ids in the default namespace. That is a design decision worth noticing: the optimizer is bound to a namespace and an id list at construction time, so adding variables after the optimizer exists is not obviously supported by the material shown. If you plan to grow a model incrementally, that ordering is something to check against the docs.

The training loop then runs inside env.run, rebuilds the graph each iteration from placeholders, computes mean_loss, calls grad over the loss with respect to w and b, and would call adam.update with the variables, gradients, context and a Feeder. The Feeder lines in the README are commented out, which is a small but honest signal that the example is illustrative rather than a finished script.

Getting it running and the BLAS choice you cannot skip

The dependency line in the README is autograd = {"<version>", features = ["blas", "<blas-implementation-choice>"]}. The version placeholder is literal in the README, so the actual number has to come from crates.io rather than from this page. The second placeholder is not optional in practice: the README lists three accepted values. accelerate is macOS only. intel-mkl is for Intel or AMD CPUs only and, per the README, includes Vector Mathematics ops. openblas is the third. These names come from blas-src, which the README links to for the full set.

The README states plainly that if you use basic linalg operations, especially matrix multiplications, the blas feature would be important to speed them up. Read that as a warning about the default path. Without it, matmul runs through whatever ndarray does on its own. The feature selection also drags in a native BLAS library, so a build that works on one machine can fail on another if the corresponding implementation is not present. That is a packaging concern as much as a performance one.

Once the dependency resolves, the entry points are ag::run for a graph context and ag::VariableEnvironment::new for parameters. There is no separate binary or CLI in the material; this is a library you call from your own code.

Where the low-level design costs you

The README claims that computation graphs require only a bare minimum of heap allocations, so overhead is small even for complex networks. That is a design claim, not a benchmark, and the only performance figure anywhere in the material is a comment in the MNIST example reading 0.11 sec/epoch on a 2.7GHz Intel Core i5. That number is attached to an example, on unspecified hardware and an unspecified dataset split, and it should not be treated as a general measurement. Nothing in the supplied material lets you predict how the crate behaves on a larger model.

The real limitation is the abstraction level. There are no layers, no loss registry, no training loop helper. You write the forward pass from tensor ops, you call grad, you feed the optimizer. The README shows hooks and a map method for applying ndarray operations directly to a tensor, which is useful when an op is missing, but it also means the crate expects you to drop to ndarray when the op set runs out. For someone porting an existing ndarray pipeline that is an advantage. For someone expecting a PyTorch-like module system it is a mismatch, and the README does not pretend otherwise.

A second constraint is the placeholder-and-feed model. The example comments out the Feeder construction, so the exact call shape for feeding batches is not demonstrated end to end in the README. You will be reading docs.rs for that.

How this differs from tch-rs and the burn approach

The most direct alternative in Rust is tch-rs, which binds libtorch rather than building on ndarray. The difference is not speed, it is where the tensor type comes from. With tch-rs your arrays are torch tensors and you inherit the libtorch operator set and its module system. With rust-autograd your arrays stay ndarray, and the operator set is what the crate and your own map calls provide. If your code already passes ndarray arrays around, rust-autograd avoids a conversion boundary that tch-rs would introduce. If you want a large prebuilt operator library, that boundary is the price of getting it.

Burn takes a third position: it defines its own tensor abstraction with a backend trait, so you can swap CPU and GPU backends behind the same API. rust-autograd does not have a backend abstraction in the material shown. It has ndarray plus an optional BLAS implementation chosen at compile time. That is simpler and less flexible. Choosing between them comes down to whether you want a backend trait to design against or a concrete ndarray type to pass around.

Maintenance, licensing and what the repository state tells you

The crate is MIT licensed, which is permissive and compatible with commercial use, though the usual caveat applies: this is a description of the licence identifier, not legal advice, and you should read the LICENSE file and your own obligations. The repository is not archived and the last push is dated 2026-06-02, so the project is not abandoned as of that date. The README does not carry a maintenance statement or a compatibility policy, and no releases were retrieved, which means there is no published version history to reason about upgrade cost from.

That absence is the practical upgrade risk. Without releases, you track the master branch or a crates.io version, and a change to the graph API or the optimizer construction signature would be a breaking change you find at compile time. The build badge in the README points at a GitHub Actions workflow, so there is CI, but CI passing does not tell you about API stability. If you depend on this crate, pinning the exact version in Cargo.toml is the only concrete protection the material supports.

On features, the BLAS selection is per-platform, so a workspace that builds on macOS with accelerate needs a different feature line on Linux. That is a real cost for cross-platform projects and it is decided at the manifest level, not at runtime.

Editorial conclusion

Adopt rust-autograd if you already build on ndarray and want gradients and a graph without pulling in a Python runtime or a large framework. Do not adopt it if you need ready-made layers, optimizers beyond what the README shows, or a stable release line, since no releases were retrieved. Before committing, verify the BLAS feature combination for your platform (accelerate on macOS, intel-mkl on Intel or AMD CPUs, openblas elsewhere), confirm the current crate version on crates.io rather than the placeholder in the README, and check that the examples directory covers the operation you need.

Official sources

  1. Issues
  2. License: MIT
  3. raskr/rust-autograd on GitHub
  4. README
Community notes

Community notes