Call .backward() on anything but a bare Dense layer in TenfloweRS 0.1.x, and PyTorch-style training either raised or silently computed the wrong gradient — GroupNorm’s backward pass was wrong by up to 760% for non-uniform gamma, and the loss curve gave you no way to tell.
Today we released TenfloweRS 0.2.0 — a complete rewrite of the Python-facing, PyTorch-style implicit autograd system in tenflowers-ffi. Every layer type now has real, tape-backed .backward()/.grad() support, all 9 optimizers perform genuine gradient-based parameter updates through a new optimizer_bridge module, and a sweep of gradient-correctness bugs across tenflowers-autograd and tenflowers-core — wrong Softmax/LogSoftmax backward, hardcoded-zero BatchNorm eval gradients, a GroupNorm/LayerNorm gamma-before-reduction bug, stubbed Slice/Gather backward, a row-major stride bug, and two tape-registry lifecycle bugs — are fixed and pinned down with finite-difference gradient-check tests.
TenfloweRS is the pure-Rust answer to TensorFlow. The tensor engine, the autodiff tape, the neural layers, the dataset pipeline, and the GPU backend are all Rust — no C runtime, no CUDA-C kernels, and no Python interpreter required at inference time (Python is opt-in, through a PyO3 FFI crate that stays publish = false and ships via maturin develop rather than crates.io). It compiles to a single static binary and runs everywhere the SciRS2/NumRS2 stack beneath it already runs.
Why TenfloweRS 0.2.0 is a game changer
Hand-rolled autograd systems fail in a specific, insidious way:
- A
backward()call that runs to completion and returns a tensor of the right shape is not proof the gradient is correct — a wrong-but-plausible gradient just makes training slower or worse, with nothing in the logs pointing at the cause - It’s easy to gradient-test the one reference architecture (a single Dense layer,
Sequential, MSE loss) and never notice that every other layer, loss, or optimizer either raises or quietly computes something else - An optimizer’s
.step()can “work” in the sense that it doesn’t crash, while never actually reading.grad()and writing an update back to the parameter
TenfloweRS 0.2.0 ends all of that.
- Every layer type is genuinely wired into the tape:
Dense,Conv1D/Conv2D/Conv3D+ pooling,Embedding/EmbeddingBag,BatchNorm1d/LayerNorm/GroupNorm/InstanceNorm1d,MultiheadAttention,TransformerEncoderLayer/TransformerDecoderLayer, andLSTM/GRU/RNN(plus their single-step cells) — previously only a minimal Dense/Sequential/MSE path had working gradients. - All 9 optimizers do real work:
SGD,Adam,RMSprop,AdamW,AdaBelief,RAdam,Nadam,AdaGrad,AdaDeltaall perform genuine gradient-based parameter updates via the newoptimizer_bridgemodule, instead of the previous no-op/partialstep(). - Every loss function is genuinely backward-connected to the tape (
neural::losses, substantially rewritten). - Proven convergence, not just “didn’t crash”: three new end-to-end tests in
tests/test_training_convergence.pyassert loss actually decreases across real training steps for a Dense layer, a 3-layer Sequential MLP, and Conv2D. - A correctness sweep fixed six distinct gradient bugs: wrong Softmax/LogSoftmax backward; BatchNorm eval-mode
grad_gamma/grad_betahardcoded to zero; the GroupNorm/LayerNorm gamma-before-reduction bug (up to 760% relative error in GroupNorm); stubbed Slice/Gather backward; a row-major-vs-Fortran-order stride bug inslice_with_stride; and two tape-registry lifecycle bugs that could silently drop a parameter’s gradient. - Two remaining gaps are documented, not hidden: Conv1D/2D/3D and pooling tape recording only fires for unit dilation,
groups==1, and no explicit padding;EmbeddingBag’smode="max"isn’t tape-wired yet (sum/meanare).
Technical Deep Dive: what actually changed under the hood
tenflowers-ffi— the per-layer backward wiring lives here:Dense, the conv/pooling family,Embedding/EmbeddingBag, all four normalization layers,MultiheadAttention, both Transformer layer types, and the full RNN family each participate for real in the tape, alongside a rewrittenneural::lossesand the newoptimizer_bridgemodule that reads.grad()and writes updates back to parameters for all 9 optimizers.attention,conv_layers,recurrent,transformer, andextended_optimizerswere each split intomod.rs+ a dedicatedtests.rsgaining its own gradient-check suite;PyTensor::slice/transpose/reshapenow call the samerecord_and_link_unaryhook used by every other tracked op. 337 Rust tests plus 55 pytest and 13integration_test.pyPython tests back this crate, including the three new end-to-endtests/test_training_convergence.pycases.tenflowers-autograd— home of the correctness fixes: Softmax/LogSoftmax backward now recomputes the forward output on the tape and applies the correct Jacobian-vector product; BatchNorm’s eval-modegrad_gamma/grad_betaare derived from running statistics instead of hardcoded to zero, with a fixed 3-D (NCL) shape case; LayerNorm and GroupNorm both had the same gamma-before-reduction bug — gamma applied to the final reduced result instead of folded in before the per-axis reduction sums, silently wrong for non-uniform gamma and measured up to 760% relative error in GroupNorm. A new dedicated Conv1D backward (ops/convolution_ops/conv1d.rs) and six new gradient-check test suites (conv1d_gradient_test,conv3d_gradient_test,group_instance_norm_gradient_check,normalization_gradient_check,slice_concat_stack_split_gather_gradient_test,activation_gaps_gradient_test) pin all of it against finite-difference/closed-form references. 575 tests, 5 skipped.tenflowers-core— a row-major-vs-Fortran-order stride bug inslice_with_stride(ops::manipulation::indexing) was silently producing wrong elements for any non-square, non-1-D sliced array, caught by an LSTM/GRU gate-slicing finite-difference test — exactly the kind of bug a real neural workload finds and a synthetic one doesn’t. Also fixed: afallbackmodule global that lived in anunsafe static mutsynchronized only for its first write, now a safeOnceLock<RwLock<FallbackConfig>>; a missing#[cfg(feature = "parallel")]gate inhistogram.rsthat broke--no-default-features --features stdbuilds (the Miri workflow); and improved WASMSharedArrayBuffer/SIMD-capability detection.- Workspace hygiene —
implicit_autograd.rsand sixtenflowers-neuralmodules (attention,conv_layers,recurrent,transformer,normalization,optimizers) were split from single files intomod.rs+tests.rssubmodule directories, keeping every file under COOLJAPAN’s 2000-line refactor policy while the new gradient-check suites landed alongside the code they verify.
Getting Started
Add the meta-crate:
cargo add tenflowers
Ordinary eager tensor math plus explicit autograd, in Rust:
use tenflowers_core::{Tensor, Device, Context};
use tenflowers_autograd::GradientTape;
// Create a context for eager execution
let ctx = Context::new()?;
let a = Tensor::<f32>::ones(&[2, 3]);
let b = Tensor::<f32>::from_vec(vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0], &[2, 3])?;
let c = a.add(&b)?;
let d = c.matmul(&b.transpose()?)?;
// watch() wraps a Tensor in a TrackedTensor; pow() is tensor-tensor, and
// gradient() takes slices of TrackedTensor for both targets and sources.
let tape = GradientTape::new();
let x = tape.watch(Tensor::<f32>::from_vec(vec![1.0, 2.0, 3.0], &[3])?);
let y = x.pow(&x)?;
let grads = tape.gradient(&[y], &[x])?;
0.2.0’s headline feature, though, is on the Python side — a real, tape-backed training loop in the same shape PyTorch users already know:
pip install maturin
maturin develop --release
import numpy as np
import tenflowers as tf
X = np.random.uniform(-1.0, 1.0, size=(16, 4)).astype(np.float32)
y = np.random.uniform(-1.0, 1.0, size=(16, 1)).astype(np.float32)
x_tensor = tf.tensor_from_numpy(X)
y_tensor = tf.tensor_from_numpy(y)
layer = tf.PyDense(4, 1, activation=None)
for param in layer.parameters():
param.set_requires_grad(True)
optimizer = tf.SGD(learning_rate=0.1)
for step in range(50):
y_pred = layer.forward(x_tensor)
loss = tf.mse_loss(y_pred, y_tensor)
loss.backward()
optimizer.step(layer)
optimizer.zero_grad(layer)
print(f"final loss: {float(tf.tensor_to_numpy(loss)[()]):.6f}")
Every layer type listed above, and every one of the 9 optimizers, now follows this same .backward() / optimizer.step() / optimizer.zero_grad() pattern — see crates/tenflowers-ffi/tests/test_training_convergence.py for the full end-to-end tests that assert the loss curve actually goes down.
What’s New in 0.2.0
- Every layer type gets real backward support:
Dense,Conv1D/Conv2D/Conv3D+ pooling,Embedding/EmbeddingBag,BatchNorm1d/LayerNorm/GroupNorm/InstanceNorm1d,MultiheadAttention,TransformerEncoderLayer/TransformerDecoderLayer, andLSTM/GRU/RNN(plus their single-step cells) — previously only a minimal Dense/Sequential/MSE path worked end-to-end. - All 9 optimizers do real work now:
SGD,Adam,RMSprop,AdamW,AdaBelief,RAdam,Nadam,AdaGrad,AdaDeltaall perform genuine gradient-based parameter updates through the newoptimizer_bridgemodule instead of a no-op/partialstep(). - Every loss function is genuinely backward-connected to the tape (
neural::losses, substantially rewritten), andPyTensor::slice/transpose/reshapeall correctly record themselves onto the implicit autograd tape now. - Proven convergence, not just “doesn’t crash”: three new end-to-end tests in
tests/test_training_convergence.pyassert loss actually decreases across real training steps for a Dense layer, a 3-layer Sequential MLP, and Conv2D. - A gradient-correctness sweep fixed wrong Softmax/LogSoftmax backward, BatchNorm eval-mode gradients hardcoded to zero, a GroupNorm/LayerNorm gamma-before-reduction bug (up to 760% relative error), stubbed Slice/Gather backward, a row-major stride bug in
slice_with_stride, and two tape-registry lifecycle bugs that could silently drop a parameter’s gradient. - Two gaps are documented, not hidden: Conv1D/2D/3D and pooling tape recording only fires for unit dilation,
groups==1, and no explicit padding;EmbeddingBag’smode="max"isn’t tape-wired yet (sum/meanare). - Workspace hygiene: six
tenflowers-neuralmodules plusimplicit_autograd.rssplit intomod.rs+tests.rsdirectories, keeping every file under the 2000-line refactor policy. - Security: RUSTSEC-2026-0204 (
crossbeam-epoch) is resolved this release; 2 known transitive advisories remain tracked (instantviahdf5,pasteviarav1e/parquet/metal), neither directly exploitable.
Tips
- Don’t trust a
.backward()call just because it ran. Use the new gradient-check test suites as your template —conv1d_gradient_test,group_instance_norm_gradient_check, andslice_concat_stack_split_gather_gradient_testintenflowers-autograd, or the standalonegradient_parityfinite-difference checker from Python — before you trust a custom layer’s gradient in production. - Re-verify any model trained with non-uniform-gamma GroupNorm or LayerNorm on 0.1.x. The gamma-before-reduction bug measured up to 760% relative error in GroupNorm and never raised an error — a silently-wrong gradient may already be baked into a checkpoint.
- Know the two remaining tape gaps before you rely on them. Conv1D/2D/3D and
MaxPool2D/AvgPool2Donly record gradients for unit dilation,groups==1, and no explicit padding — other configurations still forward correctly but skip gradient recording, documented directly on the struct inneural/conv_layers/mod.rs. Trainer::fit()in pure Rust still doesn’t callbackward()/optimizer.step(). For real gradient-based training in Rust today, compose your own loop withtenflowers_autograd::GradientTape(see Getting Started above); the Python FFI path is what 0.2.0 actually rewired for genuine end-to-end updates.- The Python pattern has one explicit step PyTorch users can forget: call
param.set_requires_grad(True)on every leaf returned bylayer.parameters()before your first.forward()— the implicit tape only tracks tensors that ask to be tracked. - Use
tests/test_training_convergence.pyas your own regression template. Asserting the loss value after N steps is strictly stronger than asserting.backward()didn’t raise — it’s the difference this release is built around.
This is the foundation
TenfloweRS leans on the rest of the COOLJAPAN ecosystem for everything below the tensor engine: NumRS2 for n-dimensional arrays, the SciRS2 stack for the scientific core and LAPACK-backed linear algebra, OxiBLAS (oxiblas-ndarray, behind the blas-oxiblas feature) for pure-Rust BLAS, OxiFFT for FFT ops, and Oxicode for serialization. It stands alongside ToRSh, TrustformeRS, and sklears in the COOLJAPAN machine-learning stack.
Repository: https://github.com/cool-japan/tenflowers
Star the repo if a hand-rolled autograd engine that gradient-checks itself against finite differences, layer by layer, is something you want to see more of. The era of trusting a .backward() call just because it didn’t crash is over. Pure Rust ML is here — fast, safe, and sovereign.
— KitaSan at COOLJAPAN OÜ July 13, 2026