From 6ed678b122a51f9b48fc301b2800a611f67bfb3d Mon Sep 17 00:00:00 2001 From: ChrisRackauckas-Claude Date: Sat, 8 Aug 2026 08:42:45 -0400 Subject: [PATCH] Stop the cache-less JVP from overwriting the caller's x finite_difference_jvp! built its internal cache with the non-allocating JVPCache(x, fx, fdtype) constructor, so cache.x1 === x and the in-place perturbation overwrote the caller's input. With fdtype = Val(:central) and f_in supplied it also overwrote f_in and returned half the correct JVP, because the restored x1 put the second evaluation back at x. The cache-less path now perturbs a copy of x and never writes into f_in. Co-Authored-By: Chris Rackauckas --- Project.toml | 2 +- src/jvp.jl | 23 +++++++++++++++++------ test/cache_reuse_tests.jl | 23 +++++++++++++++++++++++ 3 files changed, 41 insertions(+), 7 deletions(-) diff --git a/Project.toml b/Project.toml index 9ecc2ee..c03ed66 100644 --- a/Project.toml +++ b/Project.toml @@ -1,6 +1,6 @@ name = "FiniteDiff" uuid = "6a86dc24-6348-571c-b903-95158fe2bd41" -version = "2.33.0" +version = "2.33.1" [deps] ArrayInterface = "4fba245c-0d91-5ea0-9b3e-6abc04ee57a9" diff --git a/src/jvp.jl b/src/jvp.jl index d194bea..014cdd0 100644 --- a/src/jvp.jl +++ b/src/jvp.jl @@ -229,6 +229,11 @@ end absstep = relstep) Cache-less. + +Neither `x` nor `f_in` is modified: the internally-built cache perturbs a copy of `x`, +and only reads `f_in`. Use the cached method with a `JVPCache` built by the +non-allocating `JVPCache(x1, fx1, fdtype)` constructor to opt into perturbing arrays +you own. """ function finite_difference_jvp!(jvp, f, @@ -238,12 +243,18 @@ function finite_difference_jvp!(jvp, f_in = nothing; relstep = default_relstep(fdtype, eltype(x)), absstep = relstep) - if !isnothing(f_in) - cache = JVPCache(x, f_in, fdtype) - elseif fdtype == Val(:forward) - fx = zero(x) - f(fx, x) - cache = JVPCache(x, fx, fdtype) + if fdtype == Val(:forward) + if isnothing(f_in) + fx = zero(x) + f(fx, x) + else + fx = f_in + end + cache = JVPCache(copy(x), fx, fdtype) + elseif !isnothing(f_in) + # f_in is unusable for a non-forward difference, but it does pin down the + # size of the function output, which `JVPCache(x, fdtype)` has to guess. + cache = JVPCache(copy(x), zero(f_in), fdtype) else cache = JVPCache(x, fdtype) end diff --git a/test/cache_reuse_tests.jl b/test/cache_reuse_tests.jl index 53a0807..791b6af 100644 --- a/test/cache_reuse_tests.jl +++ b/test/cache_reuse_tests.jl @@ -159,4 +159,27 @@ end end end +# Aliasing x into the internally-built cache also broke :central-with-f_in numerically: +# the second evaluation landed back on x instead of x + ϵv, halving the answer. +@testset "cache-less JVP does not mutate the caller's x" begin + sq!(y, x) = (y[1] = x[1]^2 + x[2]; y[2] = x[1] * x[2]; y) + x_ref = [1.0, 2.0] + v = [1.0, 0.5] + jvp_ref = [2x_ref[1] 1.0; x_ref[2] x_ref[1]] * v + f_in = sq!(zeros(2), x_ref) + + cases = ((Val(:forward), nothing), (Val(:forward), f_in), + (Val(:central), nothing), (Val(:central), f_in)) + + @testset "$(fdtype), f_in=$(fin0 !== nothing)" for (fdtype, fin0) in cases + x = copy(x_ref) + fin = fin0 === nothing ? nothing : copy(fin0) + jvp = zeros(2) + FiniteDiff.finite_difference_jvp!(jvp, sq!, x, v, fdtype, fin) + @test x == x_ref + fin === nothing || @test fin == f_in + @test jvp≈jvp_ref atol=1e-6 + end +end + end # outer testset