[autodiff] different results in debug vs release and forward vs reverse mode
Nobody has claimed this yet.
- Dominant language
- Rust
- Stars
- 119k
- Forks
- 16.1k
- PR merge metrics
- PR metrics pending
Description
Hello, I identified a series of bugs that seem somewhat related. They all involve iterative loops inside the function to differentiate. All examples below produce the expected results if built in release mode. If built in debug mode, they either not compile, or they produce wrong results.
Build error using autodiff_reverse
Example code
#![feature(autodiff)]
use std::autodiff::autodiff_reverse;
use approx::assert_abs_diff_eq;
#[autodiff_reverse(f_rev, 2, Duplicated, Const, Duplicated)]
fn f(x: &[f64; 3], args: &[f64; 3], y: &mut [f64; 2]) {
y[0] = x.iter().map(|i| args[0] * i.powi(2)).sum();
y[1] = x
.windows(2)
.map(|w| (args[1] - w[0]).powi(2) + args[2] * (w[1] - w[0].powi(2)).powi(2))
.sum();
// for i in 0..3 {
// y[0] += args[0] * x[i].powi(2);
// }
// for i in 0..2 {
// y[1] += (args[1] - x[i]).powi(2) + args[2] * (x[i + 1] - x[i].powi(2)).powi(2);
// }
}
fn assert_abs_diff_eq<const N: usize>(x: &[f64; N], y: &[f64; N], tol: f64) {
for i in 0..N {
assert_abs_diff_eq!(x[i], y[i], epsilon = tol);
}
}
fn main() {
let x = [3.0, 5.0, 7.0];
let args = [2.0, 1.0, 100.0];
let mut vjp = ([0.0; 3], [0.0; 3]);
let mut y = [0.0; 2];
let mut dy = ([1.0, 0.0], [0.0, 1.0]);
f_rev(
&x, &mut vjp.0, &mut vjp.1, &args, &mut y, &mut dy.0, &mut dy.1,
);
assert_abs_diff_eq::<2>(&y, &[166.0, 34020.0], 1e-15);
assert_abs_diff_eq::<3>(&vjp.0, &[12.0, 20.0, 28.0], 1e-15);
assert_abs_diff_eq::<3>(&vjp.1, &[4804.0, 35208.0, -3600.0], 1e-15);
}
This example builds and produces the expected results in release mode. In debug mode it cannot compile:
error: library/core/src/iter/adapters/map.rs:128:9: in function preprocess__RINvXs0_NtNtNtCsfXmiG9HCApf_4core4iter8adapters3mapINtB6_3MapINtNtNtBc_5slice4iter7WindowsdENCNvCsabDTJ5vn3Pt_14vector_reverse1fs_0ENtNtNtBa_6traits8iterator8Iterator4folddNCINvXs26_NtB2a_5accumdNtB2U_3Sum3sumBN_E0EB1w_ double (ptr, double): Enzyme: Cannot deduce type of copy call void @llvm.memcpy.p0.p0.i64(ptr align 8 %3, ptr align 8 %0, i64 24, i1 false) #164, !dbg !178
<analysis>
ptr %0: {[-1]:Pointer, [-1,24]:Pointer}, intvals: {}
double %1: {[-1]:Float@double}, intvals: {}
%3 = tail call noalias nonnull dereferenceable(24) dereferenceable_or_null(24) ptr @malloc(i64 24), !enzyme_fromstack !157: {[-1]:Pointer}, intvals: {}
%4 = getelementptr inbounds i8, ptr %0, i64 24, !dbg !179: {[-1]:Pointer, [-1,0]:Pointer}, intvals: {}
%6 = call double @_RINvYINtNtNtCsfXmiG9HCApf_4core5slice4iter7WindowsdENtNtNtNtBa_4iter6traits8iterator8Iterator4folddNCINvNtNtBU_8adapters3map8map_foldRSdddNCNvCsabDTJ5vn3Pt_14vector_reverse1fs_0NCINvXs26_NtBS_5accumdNtB2Z_3Sum3sumINtB1E_3MapB3_B2c_EE0E0EB2g_(ptr align 8 %3, double %1, ptr align 8 %5) #165, !dbg !181: {[-1]:Float@double}, intvals: {}
%5 = load ptr, ptr %4, align 8, !dbg !179: {[-1]:Pointer}, intvals: {}
</analysis>
similar error with the commented out implementation of f
error: library/core/src/iter/range.rs:858:6: in function preprocess__RNvXs4_NtNtCsfXmiG9HCApf_4core4iter5rangeINtNtNtB9_3ops5range5RangejENtNtNtB7_6traits8iterator8Iterator4nextCsabDTJ5vn3Pt_14vector_reverse { i64, i64 } (ptr): Enzyme: Cannot deduce type of insertvalue ins %6 = insertvalue { i64, i64 } %5, i64 %4, 1, !dbg !123 size: 8 TT: {}
<analysis>
{ i64, i64 } poison: {[-1]:Anything}, intvals: {}
%4 = extractvalue { i64, i64 } %2, 1, !dbg !122: {}, intvals: {}
%3 = extractvalue { i64, i64 } %2, 0, !dbg !122: {}, intvals: {}
%2 = call { i64, i64 } @_RNvXs3_NtNtCsfXmiG9HCApf_4core4iter5rangeINtNtNtB9_3ops5range5RangejENtB5_17RangeIteratorImpl9spec_nextCsabDTJ5vn3Pt_14vector_reverse(ptr align 8 %0) #160, !dbg !122: {}, intvals: {}
ptr %0: {[-1]:Pointer, [-1,-1]:Integer}, intvals: {}
%6 = insertvalue { i64, i64 } %5, i64 %4, 1, !dbg !123: {}, intvals: {}
%5 = insertvalue { i64, i64 } poison, i64 %3, 0, !dbg !123: {[8]:Anything, [9]:Anything, [10]:Anything, [11]:Anything, [12]:Anything, [13]:Anything, [14]:Anything, [15]:Anything}, intvals: {}
</analysis>
Wrong output using autodiff_forward
Same example as above
#![feature(autodiff)]
use std::autodiff::autodiff_forward;
use approx::assert_abs_diff_eq;
#[autodiff_forward(f_fwd, 3, Dual, Const, Dual)]
fn f(x: &[f64; 3], args: &[f64; 3], y: &mut [f64; 2]) {
y[0] = x.iter().map(|i| args[0] * i.powi(2)).sum();
y[1] = x
.windows(2)
.map(|w| (args[1] - w[0]).powi(2) + args[2] * (w[1] - w[0].powi(2)).powi(2))
.sum();
}
fn assert_abs_diff_eq<const N: usize>(x: &[f64; N], y: &[f64; N], tol: f64) {
for i in 0..N {
assert_abs_diff_eq!(x[i], y[i], epsilon = tol);
}
}
fn main() {
let x = [3.0, 5.0, 7.0];
let args = [2.0, 1.0, 100.0];
let mut dx = ([1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]);
let mut y = [0.0; 2];
let mut jvp = ([0.0; 2], [0.0; 2], [0.0; 2]);
f_fwd(
&x, &mut dx.0, &mut dx.1, &mut dx.2, &args, &mut y, &mut jvp.0, &mut jvp.1, &mut jvp.2,
);
assert_abs_diff_eq::<2>(&y, &[166.0, 34020.0], 1e-15);
assert_abs_diff_eq::<2>(&jvp.0, &[12.0, 4804.0], 1e-15);
assert_abs_diff_eq::<2>(&jvp.1, &[20.0, 35208.0], 1e-15);
assert_abs_diff_eq::<2>(&jvp.2, &[28.0, -3600.0], 1e-15);
}
Produces the expected results in release mode. In debug mode it still compiles, but the assertions fail
assert_abs_diff_eq!(x[i], y[i], epsilon = tol)
left = 178.0
right = 12.0
Function with iterative while loop
In reverse mode, this example produces the expected results in both debug and release mode. In forward mode, it produces wrong results if built in debug mode.
#![feature(autodiff)]
use approx::assert_abs_diff_eq;
use std::autodiff::{autodiff_forward, autodiff_reverse};
use std::f64::consts::FRAC_PI_2;
fn newton_raphson(
f: fn(f64, &[f64]) -> f64,
df: fn(f64, &[f64]) -> f64,
args: &[f64],
x0: f64,
tol: f64,
max_iter: u32,
) -> f64 {
let mut xk = x0;
let mut dx = tol * 2.0;
let mut k = 0;
while dx.abs() > tol && k < max_iter {
dx = f(xk, args) / df(xk, args);
xk = xk - dx;
k += 1;
}
xk
}
fn kepler(ea: f64, args: &[f64]) -> f64 {
ea - args[0] * ea.sin() - args[1]
}
fn kepler_prime(ea: f64, args: &[f64]) -> f64 {
1.0 - args[0] * ea.cos()
}
#[autodiff_forward(solve_fwd_vec, 2, Dualv, Const, Const, Dual)]
#[autodiff_forward(solve_fwd, 2, Dual, Const, Const, Dual)]
#[autodiff_reverse(solve_rev, Duplicated, Const, Const, Active)]
fn solve(x: &[f64], dt: f64, mu: f64) -> f64 {
let a = x[0];
let e = x[1];
let ma = (mu / a.powi(3)).sqrt() * dt;
newton_raphson(kepler, kepler_prime, &[e, ma], ma, 1e-14, 50)
}
fn main() {
let (a, e, dt, mu) = (1.0, 0.5, FRAC_PI_2, 1.0);
let x = [a, e];
let mut vjp = [0.0; 2];
let ea = solve_rev(&x, &mut vjp, dt, mu, 1.0);
assert_abs_diff_eq!(ea, 2.0209799380897704, epsilon = 1e-14);
assert_abs_diff_eq!(vjp[0], -1.9351686842196312, epsilon = 1e-14);
assert_abs_diff_eq!(vjp[1], 7.3948159233291877e-1, epsilon = 1e-14);
let sol_fwd = solve_fwd(&x, &[1.0, 0.0], &[0.0, 1.0], dt, mu);
let sol_fwd_vec = solve_fwd_vec(&x, &[1.0, 0.0, 0.0, 1.0], dt, mu);
// expected: [2.0209799380897704, -1.9351686842196314, 0.7394815923329188]
// actual debug mode: [1.5707963267948966, -3.20502085334912, 0.0]
println!("{:?}", sol_fwd);
println!("{:?}", sol_fwd_vec);
}
Array reference vs slice
With Dualv it is not possible to have array references in the function arguments, as the size of the primal differs from that of the perturbations. E.g. modifying the signature of the function above into
fn solve(x: &[f64; 2], dt: f64, mu: f64) -> f64
result in a compile time error
58 | let sol_fwd_vec = solve_fwd_vec(&x, &[1.0, 0.0, 0.0, 1.0], dt, mu);
| ------------- ^^^^^^^^^^^^^^^^^^^^^ expected an array with a size of 2, found one with a size of 4
| |
| arguments to this function are incorrect
However, building with rustflags = ["-Z", "autodiff=Enable,PrintSteps"] I noticed that slices produce intermediate panic_bounds_check like
tail call fastcc void @_RNvNtCsfXmiG9HCApf_4core9panicking18panic_bounds_check(i64 noundef 0, i64 noundef 0, ptr noalias noundef readonly align 8 captures(address, read_provenance) dereferenceable(24) @anon.14d454a466a7566c13b3e439a1393f62.16) #99
unreachable
which seem to not affect the final output. These occur in both debug and release mode.
Meta
rustc --version --verbose:
rustc 1.95.0-nightly (d7daac06d 2026-02-13)
binary: rustc
commit-hash: d7daac06d87e1252d10eaa44960164faac46beff
commit-date: 2026-02-13
host: aarch64-apple-darwin
release: 1.95.0-nightly
LLVM version: 22.1.0
built to support autodiff as described in the rustc dev book.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with the standalone reproductions for autodiff_forward and autodiff_reverse, building each in debug and release modes and comparing the reported outputs and compiler errors. Consult the rustc autodiff development documentation and inspect the generated analysis around the reported iterator and loop entry points. Done means the reproductions compile where expected and produce the documented derivatives consistently across modes.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- rust
- Domain
- compilers
- Issue type
- Bug
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 35/100