COOLJAPAN
← All posts

SciRS2 0.6.5 Released — An Ignore-Audit Found the Backward Pass Was Mostly Dead Code

SciRS2 0.6.5 is a defect-hunting release: a workspace-wide audit of every #[ignore]d test found that scirs2-autograd's live backward pass silently identity-passed 223 of 281 differentiable ops instead of computing real gradients, plus real fixes across linalg, stats, graph, io, spatial, ndimage, and special. Pure Rust, Apache-2.0.

release scirs2 rust scientific-computing autograd machine-learning gradients testing

A ~670-line if/else chain matched op names as strings. It recognized 58 of 281. Every op it didn’t recognize returned the input gradient unchanged — no error, no warning, no debug_assert — and the test suite was green the whole time.

Today we released SciRS2 0.6.5 — a defect-hunting cycle rather than a features cycle. It started as a routine cleanup: audit every #[ignore]d test in the workspace (132 of them, roughly a third with no reason attached) and either fix what they were skipping or document why. Followed all the way to ground, that audit surfaced real bugs across autograd, linalg, neural, stats, graph, io, integrate, series, spatial, ndimage, and special — the single biggest of which is that scirs2-autograd’s live backward pass was, for the majority of operators, not actually differentiating anything.

SciRS2 stays what it has always been: no C, no Fortran, no CUDA Toolkit, no system BLAS — a single static binary (or WASM module) that compiles and runs everywhere. 0.6.5 makes sure that when you ask it for a gradient, you get the real one.

Why SciRS2 0.6.5 is a game changer

A gradient that’s silently wrong is worse than one that crashes:

SciRS2 0.6.5 ends all of that:

Technical Deep Dive: the trait method nobody called

The core issue is a class of bug that’s easy to introduce and hard to catch: two independent code paths that are supposed to agree, where only one of them is ever exercised at runtime.

// gradient.rs (simplified) — the path that actually ran:
fn compute_grad_for_input(op_name: &str, gy: Tensor, ...) -> Option<Tensor> {
    if op_name == "Sqrt" {
        // correct: gy * 0.5 / sqrt(x)
        Some(gy * T::scalar_mul(T::inv_sqrt(x), 0.5))
    } else if op_name == "Neg" {
        Some(T::neg(gy))
    }
    // ... ~60 more arms ...
    else {
        // silently wrong for the other 220+ ops
        Some(gy)
    }
}

// op.rs — the path with the real implementation, never called from here:
impl Op for SqrtOp {
    fn grad(&self, gy: Tensor, ctx: &mut GradContext) {
        // correct, tested in isolation, just never invoked by gradient.rs
        ctx.append_input_grad(0, Some(gy * ...));
    }
}

Every Op implementation’s own grad() method was correct — each was written and tested against its individual forward pass. The bug was entirely in the wiring: the live dispatcher never called it. Fixing this meant changing compute_grad_for_input to consult op.grad() as its fallback instead of Some(gy), keeping the hand-written override table only for the handful of ops (higher-order derivatives, ops needing extra context) that genuinely need special-casing. The finite-difference harness that verifies this is now permanent, and it deliberately uses a non-uniform cotangent for exactly the reason the old tests couldn’t catch this in the first place.

Getting Started

The custom-gradient API is a good way to see the fix directly — scale_gradient is a gradient-reversal-style layer that scales the backward gradient by a constant without touching the forward value. Before 0.6.5 this silently did nothing; now it works:

cargo add scirs2-autograd
use scirs2_autograd as ag;
use ag::tensor_ops::{self as T, scale_gradient};
use scirs2_core::ndarray::{ArrayD, IxDyn};

fn main() {
    ag::run(|g| {
        let x = T::variable(
            ArrayD::from_shape_vec(IxDyn(&[4]), vec![0.5, 1.5, -2.0, 3.25]).expect("x"),
            g,
        );

        // Forward value is unaffected; the backward gradient through `y` is
        // multiplied by -2.0 -- a textbook gradient-reversal layer.
        let y = scale_gradient(T::square(x), -2.0, g);
        let loss = T::sum_all(T::scalar_mul(y, 2.5));

        let grads = T::grad(&[loss], &[x]);
        let gx = grads[0].eval(g).expect("gradient eval");

        // True gradient of sum(2.5 * x^2) is 5*x; scale_gradient(-2.0) then
        // flips and doubles it. Before 0.6.5 this printed the un-reversed 5*x.
        println!("{:?}", gx); // [-5.0, -15.0, 20.0, -32.5]
    });
}

What’s New in 0.6.5

Fixed

Added

Changed

See CHANGELOG.md [0.6.5] for the complete list.

Tips

  1. If your autograd-based training loop uses custom_op, scale_gradient, selective_stop_gradient, or detach, re-check results computed before 0.6.5. These previously ignored your backward closure entirely; anything relying on gradient reversal, gradient scaling, or selective stop-gradients was silently training on the wrong signal.
  2. When you write a gradient test, use a non-uniform cotangent. An all-ones cotangent makes broadcast/permutation-shaped bugs (transpose, gather, reduce_sum over an axis) invisible, because permuting or broadcasting a constant is a no-op whether or not your VJP logic is correct.
  3. When a dispatcher has two ways to compute the same thing — a fast-path override table and a “real” per-case implementation — assert that the fallback path is actually reachable. A trait method that’s correct but never called is functionally the same as a bug.
  4. If you use scirs2-linalg’s schur/eig on non-symmetric matrices with closely-spaced eigenvalues, re-run affected pipelines. The new engine actually checks for QR convergence instead of running a fixed iteration count.
  5. If you run one-sided Kolmogorov-Smirnov tests via scirs2-stats, re-check any “Rejected”/“Not rejected” conclusion from before 0.6.5 — the p-value formula was backwards.
  6. #[ignore] a test only with a reason prefix (requires-gpu:, requires-env:, slow:, bench:, not-implemented:) if you’re contributing to SciRS2 — the new ignore_audit lint enforces this, and it exists precisely because a bare #[ignore] is where this release’s headline bug was hiding.

This is the foundation

SciRS2 0.6.5 is the sovereign scientific-computing layer of the COOLJAPAN ecosystem, and a release that fixes the actual gradient computation matters most for everything that trains on top of it:

Repository: https://github.com/cool-japan/scirs

Star the repo if you’d rather a silently-identity gradient get caught by a finite-difference harness with a non-uniform cotangent than by a model that quietly never learns.

The era of “the tests are green, so the gradients must be right” is over. Pure Rust scientific computing is here — fast, safe, and now provably differentiating what it claims to.

KitaSan at COOLJAPAN OÜ July 31, 2026

↑ Back to all posts