rust-lang / rust-lang/rust

[autodiff] different results in debug vs release and forward vs reverse mode

Open
#152,724 18 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

C-bug F-autodiff T-compiler
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

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.