From 3e832c1d4c8590f5c4f9e75a21671782fe43477b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20M=C3=BCller-Widmann?= Date: Thu, 22 Oct 2026 20:26:01 +0200 Subject: [PATCH 1/5] Run tests with ParallelTestRunner Each test file now runs in its own sandbox module on a pool of worker processes, so the per-file `module` wrappers are dropped. The #218 multithreaded recording test moves to `test/ThreadsTests.jl` and gets a dedicated 4-thread worker via `test_worker`; all other workers stay single-threaded, so `JULIA_NUM_THREADS` is no longer set in CI. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/ci.yml | 2 - test/ChainRulesTests.jl | 4 - test/MacrosTests.jl | 4 - test/PrecisionTests.jl | 4 - test/Project.toml | 2 + test/TapeTests.jl | 4 - test/ThreadsTests.jl | 32 ++++++++ test/TrackedTests.jl | 4 - test/api/ConfigTests.jl | 4 - test/api/GradientTests.jl | 32 -------- test/api/HessianTests.jl | 4 - test/api/JacobianTests.jl | 4 - test/compat/CompatTests.jl | 4 - test/derivatives/BroadcastTests.jl | 4 - test/derivatives/ElementwiseTests.jl | 4 - test/derivatives/LinAlgTests.jl | 4 - test/derivatives/ScalarTests.jl | 4 - test/runtests.jl | 111 ++++----------------------- 18 files changed, 51 insertions(+), 180 deletions(-) create mode 100644 test/ThreadsTests.jl diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 90022731..1ad1ff41 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -48,8 +48,6 @@ jobs: - uses: julia-actions/cache@v3 - uses: julia-actions/julia-buildpkg@v1 - uses: julia-actions/julia-runtest@v1 - env: - JULIA_NUM_THREADS: 4 - uses: julia-actions/julia-processcoverage@v1 - uses: codecov/codecov-action@v7 with: diff --git a/test/ChainRulesTests.jl b/test/ChainRulesTests.jl index 7433d78b..1bfc7b3a 100644 --- a/test/ChainRulesTests.jl +++ b/test/ChainRulesTests.jl @@ -1,5 +1,3 @@ -module ChainRulesTest - using LinearAlgebra using ChainRulesCore using DiffResults @@ -283,5 +281,3 @@ module IsolatedModuleForTestingScoping end # end of SubModule end # end of IsolatedModuleForTestingScoping - -end diff --git a/test/MacrosTests.jl b/test/MacrosTests.jl index 6e4ad358..082f3946 100644 --- a/test/MacrosTests.jl +++ b/test/MacrosTests.jl @@ -1,5 +1,3 @@ -module MacrosTests - using ReverseDiff, ForwardDiff, Test, StaticArrays using ForwardDiff: Dual, Partials, partials @@ -344,5 +342,3 @@ using ReverseDiff: @grad, TrackedReal, TrackedVector, TrackedMatrix, TrackedArra @test g1 == g2 @test custom_grad_called end - -end # module diff --git a/test/PrecisionTests.jl b/test/PrecisionTests.jl index 6ba039a3..9bcec747 100644 --- a/test/PrecisionTests.jl +++ b/test/PrecisionTests.jl @@ -1,5 +1,3 @@ -module PrecisionTests - using ReverseDiff, Test, LinearAlgebra, Statistics include("utils.jl") @@ -53,5 +51,3 @@ for (n, d) in ((1.0e-200, 1.0e-200), (1.0e200, 1.0e200)) @test ReverseDiff.gradient(f, [d]) ≈ [∂] rtol = 8 * eps(Float64) end end - -end # module diff --git a/test/Project.toml b/test/Project.toml index 80006f85..45c1dff8 100644 --- a/test/Project.toml +++ b/test/Project.toml @@ -9,6 +9,7 @@ IrrationalConstants = "92d709cd-6900-40b7-9082-c6be49f344b6" LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" LogExpFunctions = "2ab3a3ac-af41-5b50-aa03-7779005ae688" NaNMath = "77ba4419-2d1f-58cd-9bb1-8ffee604a2e3" +ParallelTestRunner = "d3525ed8-44d0-4b2c-a655-542cee43accc" Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" ReverseDiff = "37e2e3b7-166d-5795-8a7a-e32c996b4267" SpecialFunctions = "276daf66-3868-5448-9aa4-cd146d93841b" @@ -27,6 +28,7 @@ IrrationalConstants = "0.2" LinearAlgebra = "1" LogExpFunctions = "1" NaNMath = "1" +ParallelTestRunner = "2" Random = "1" SpecialFunctions = "2" StaticArrays = "1.6.4" diff --git a/test/TapeTests.jl b/test/TapeTests.jl index d22d1fa5..00b7e1cf 100644 --- a/test/TapeTests.jl +++ b/test/TapeTests.jl @@ -1,5 +1,3 @@ -module TapeTests - using ReverseDiff, Test using ReverseDiff: SpecialInstruction, ScalarInstruction, NULL_TAPE @@ -62,5 +60,3 @@ for Instr in (SpecialInstruction, ScalarInstruction) @test length(NULL_TAPE) == 0 @test repr(MIME"text/plain"(), NULL_TAPE) == "0-element InstructionTape" end - -end # module diff --git a/test/ThreadsTests.jl b/test/ThreadsTests.jl new file mode 100644 index 00000000..7a77358c --- /dev/null +++ b/test/ThreadsTests.jl @@ -0,0 +1,32 @@ +using ReverseDiff, Test + +# Run on a multithreaded worker, see `test_worker` in runtests.jl +@test Threads.nthreads() > 1 + +@testset "recording from multiple threads (#218)" begin + function g(x, i) + acc = zero(eltype(x)) + for _ in 1:100 + acc += (i * x[1] - x[2])^2 + end + return acc + sum(abs2, i .* x) + end + function f(x) + s = Vector{Any}(undef, 64) + @sync for i in eachindex(s) + Threads.@spawn s[i] = g(x, i) + end + return sum(s) + end + f_serial(x) = sum(i -> g(x, i), 1:64) + + x = [1.0, 2.0] + ∇f = ReverseDiff.gradient(f_serial, x) + for _ in 1:20 + @test ReverseDiff.gradient(f, x) ≈ ∇f + end + + # tape replay + tape = ReverseDiff.compile(ReverseDiff.GradientTape(f, x)) + @test ReverseDiff.gradient!(similar(x), tape, x) ≈ ∇f +end diff --git a/test/TrackedTests.jl b/test/TrackedTests.jl index 863df5fe..5a209e19 100644 --- a/test/TrackedTests.jl +++ b/test/TrackedTests.jl @@ -1,5 +1,3 @@ -module TrackedTests - using ReverseDiff, Test using ReverseDiff: TrackedReal, TrackedArray @@ -897,5 +895,3 @@ end @test occursin(r"^TrackedReal<\w{3}>\(1\.0, 0\.0, ---, ---\)$", repr(TrackedReal(1.0, 0.0))) ta = TrackedArray([1.0, 2.0], [0.0, 3.0], InstructionTape()) @test occursin(r"^TrackedReal<\w{3}>\(2\.0, 3\.0, \w{3}, 2, \w{3}\)$", repr(ta[2])) - -end diff --git a/test/api/ConfigTests.jl b/test/api/ConfigTests.jl index d5e4e507..5c6971b2 100644 --- a/test/api/ConfigTests.jl +++ b/test/api/ConfigTests.jl @@ -1,5 +1,3 @@ -module ConfigTests - using ReverseDiff, Test include(joinpath(dirname(@__FILE__), "../utils.jl")) @@ -75,5 +73,3 @@ cfg = HessianConfig(DiffResults.HessianResult(y), x, gtp, jtp) # These two act weird: work locally but fail in test mode #@test issimilar(cfg.gradient_config, GradientConfig(track(x, Int), gtp)) #@test issimilar(cfg.jacobian_config, JacobianConfig(y, x, jtp)) - -end # module diff --git a/test/api/GradientTests.jl b/test/api/GradientTests.jl index e101ab7f..8aa67c70 100644 --- a/test/api/GradientTests.jl +++ b/test/api/GradientTests.jl @@ -1,5 +1,3 @@ -module GradientTests - using DiffTests, ForwardDiff, ReverseDiff, StaticArrays, Test, LinearAlgebra include(joinpath(dirname(@__FILE__), "../utils.jl")) @@ -489,33 +487,3 @@ end # a `Float64` buffer throws instead of zeroing the gradient @test_throws ArgumentError ReverseDiff.gradient(W -> loss(zeros(3, 5), W, H), W) end - -@testset "recording from multiple threads (#218)" begin - function g(x, i) - acc = zero(eltype(x)) - for _ in 1:100 - acc += (i * x[1] - x[2])^2 - end - return acc + sum(abs2, i .* x) - end - function f(x) - s = Vector{Any}(undef, 64) - @sync for i in eachindex(s) - Threads.@spawn s[i] = g(x, i) - end - return sum(s) - end - f_serial(x) = sum(i -> g(x, i), 1:64) - - x = [1.0, 2.0] - ∇f = ReverseDiff.gradient(f_serial, x) - for _ in 1:20 - @test ReverseDiff.gradient(f, x) ≈ ∇f - end - - # tape replay - tape = ReverseDiff.compile(ReverseDiff.GradientTape(f, x)) - @test ReverseDiff.gradient!(similar(x), tape, x) ≈ ∇f -end - -end # module diff --git a/test/api/HessianTests.jl b/test/api/HessianTests.jl index 73d5ffdb..de4d8708 100644 --- a/test/api/HessianTests.jl +++ b/test/api/HessianTests.jl @@ -1,5 +1,3 @@ -module HessianTests - using DiffTests, ForwardDiff, ReverseDiff, StaticArrays, Test include(joinpath(dirname(@__FILE__), "../utils.jl")) @@ -130,5 +128,3 @@ f251(x) = sum(abs2, x) @test DiffResults.hessian(result) == hess end end - -end # module diff --git a/test/api/JacobianTests.jl b/test/api/JacobianTests.jl index 8c09f88f..13e6e349 100644 --- a/test/api/JacobianTests.jl +++ b/test/api/JacobianTests.jl @@ -1,5 +1,3 @@ -module JacobianTests - using DiffTests, ForwardDiff, ReverseDiff, StaticArrays, Test include(joinpath(dirname(@__FILE__), "../utils.jl")) @@ -348,5 +346,3 @@ end # an element that is not a number has no single derivative to seed @test_throws ArgumentError ReverseDiff.jacobian(x -> [x, x], [1.0, 2.0]) end - -end # module diff --git a/test/compat/CompatTests.jl b/test/compat/CompatTests.jl index 3b84eb47..1b0e1021 100644 --- a/test/compat/CompatTests.jl +++ b/test/compat/CompatTests.jl @@ -1,5 +1,3 @@ -module CompatTests - using FillArrays, ReverseDiff, Test @test ReverseDiff.gradient(fill(2.0, 3)) do x @@ -9,5 +7,3 @@ end == fill(4.0, 3) @test ReverseDiff.gradient(fill(2.0, 3)) do x sum(abs2.(x .- (1:3))) end == [2, 0, -2] - -end diff --git a/test/derivatives/BroadcastTests.jl b/test/derivatives/BroadcastTests.jl index 3f3e1e13..705aec7c 100644 --- a/test/derivatives/BroadcastTests.jl +++ b/test/derivatives/BroadcastTests.jl @@ -1,5 +1,3 @@ -module BroadcastTests - using ReverseDiff using ForwardDiff @@ -686,5 +684,3 @@ end @test ReverseDiff.gradient!(tape3, x) == 2 .* x @test ReverseDiff.gradient!(ReverseDiff.compile(tape3), x) == 2 .* x end - -end # module diff --git a/test/derivatives/ElementwiseTests.jl b/test/derivatives/ElementwiseTests.jl index 54486273..c62c7bf2 100644 --- a/test/derivatives/ElementwiseTests.jl +++ b/test/derivatives/ElementwiseTests.jl @@ -1,5 +1,3 @@ -module ElementwiseTests - using ReverseDiff using DiffRules @@ -548,5 +546,3 @@ end end @test isempty(take_recorded!(tp)) end - -end # module diff --git a/test/derivatives/LinAlgTests.jl b/test/derivatives/LinAlgTests.jl index 6a627263..a8b311b2 100644 --- a/test/derivatives/LinAlgTests.jl +++ b/test/derivatives/LinAlgTests.jl @@ -1,5 +1,3 @@ -module LinAlgTests - using ReverseDiff, ForwardDiff, Test, LinearAlgebra, StaticArrays if get(ENV, "DOWNGRADE_TEST", "false")::String != "true" @@ -416,5 +414,3 @@ end test_approx(ReverseDiff.jacobian!(tp, x), J) end end - -end # module diff --git a/test/derivatives/ScalarTests.jl b/test/derivatives/ScalarTests.jl index f1e35aa2..ce639cab 100644 --- a/test/derivatives/ScalarTests.jl +++ b/test/derivatives/ScalarTests.jl @@ -1,5 +1,3 @@ -module ScalarTests - using ReverseDiff using DiffRules @@ -179,5 +177,3 @@ for f in ReverseDiff.SKIPPED_BINARY_SCALAR_FUNCS test_println("SKIPPED_BINARY_SCALAR_FUNCS", f) test_skip(eval(f), a, b, tp) end - -end # module diff --git a/test/runtests.jl b/test/runtests.jl index abc1e2ca..6dba4a50 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,96 +1,19 @@ -using Test - -test_println(kind, f, pad = " ") = println(pad, "testing $(kind): `$(f)`...") - -@testset "ReverseDiff" begin - - @testset "TapeTests" begin - println("running TapeTests...") - t = @elapsed include("TapeTests.jl") - println("done (took $t seconds).") - end - - @testset "TrackedTests" begin - println("running TrackedTests...") - t = @elapsed include("TrackedTests.jl") - println("done (took $t seconds).") - end - - @testset "MacrosTests" begin - println("running MacrosTests...") - t = @elapsed include("MacrosTests.jl") - println("done (took $t seconds).") - end - - @testset "ChainRulesTests" begin - println("running ChainRulesTests...") - t = @elapsed include("ChainRulesTests.jl") - println("done (took $t seconds).") - end - - @testset "ScalarTests" begin - println("running ScalarTests...") - t = @elapsed include("derivatives/ScalarTests.jl") - println("done (took $t seconds).") - end - - @testset "LinAlgTests" begin - println("running LinAlgTests...") - t = @elapsed include("derivatives/LinAlgTests.jl") - println("done (took $t seconds).") - end - - @testset "PrecisionTests" begin - println("running PrecisionTests...") - t = @elapsed include("PrecisionTests.jl") - println("done (took $t seconds).") - end - - @testset "ElementWiseTests" begin - println("running ElementwiseTests...") - t = @elapsed include("derivatives/ElementwiseTests.jl") - println("done (took $t seconds).") - end - - @testset "BroadcastTests" begin - println("running BroadcastTests...") - t = @elapsed include("derivatives/BroadcastTests.jl") - println("done (took $t seconds).") - end - - @testset "ArrayFunctionTests" begin - println("running ArrayFunctionTests...") - t = @elapsed include("derivatives/ArrayFunctionTests.jl") - println("done (took $t seconds).") - end - - @testset "GradientTests" begin - println("running GradientTests...") - t = @elapsed include("api/GradientTests.jl") - println("done (took $t seconds).") - end - - @testset "JacobianTests" begin - println("running JacobianTests...") - t = @elapsed include("api/JacobianTests.jl") - println("done (took $t seconds).") - end - - @testset "HessianTests" begin - println("running HessianTests...") - t = @elapsed include("api/HessianTests.jl") - println("done (took $t seconds).") - end - - @testset "ConfigTests" begin - println("running ConfigTests...") - t = @elapsed include("api/ConfigTests.jl") - println("done (took $t seconds).") - end - - @testset "CompatTests" begin - println("running CompatTests...") - t = @elapsed include("compat/CompatTests.jl") - println("done (took $t seconds).") +using ReverseDiff +using ParallelTestRunner + +# Every file under `test/` except this one and `utils.jl` is a test, named by its path, +# e.g. "api/GradientTests". Select tests via `Pkg.test("ReverseDiff"; test_args=["api/GradientTests"])`, +# exclude them with a leading `!`, and list all with `test_args=["--list"]`. +const testsuite = find_tests(@__DIR__) +delete!(testsuite, "utils") + +# Workers are single-threaded by default +function test_worker(name) + if name == "ThreadsTests" + return addworker(; exeflags = ["--threads=4"]) + else + return nothing end end + +runtests(ReverseDiff, ARGS; testsuite, test_worker) From efa117a40577c016fd5ca0984c35d8d74a987a2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20M=C3=BCller-Widmann?= Date: Thu, 22 Oct 2026 23:59:11 +0200 Subject: [PATCH 2/5] Fix CI with ParallelTestRunner ParallelTestRunner loads Statistics on its workers, so StatisticsExt is always loaded before LinAlgTests runs. Drop the check that it isn't, and the DOWNGRADE_TEST variable that only existed for it. Require ParallelTestRunner 2.0.1, which fixes the paths of nested tests returned by find_tests. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/Downgrade.yml | 3 --- test/Project.toml | 2 +- test/derivatives/LinAlgTests.jl | 5 ----- 3 files changed, 1 insertion(+), 9 deletions(-) diff --git a/.github/workflows/Downgrade.yml b/.github/workflows/Downgrade.yml index 9ccbd196..4ae78874 100644 --- a/.github/workflows/Downgrade.yml +++ b/.github/workflows/Downgrade.yml @@ -45,9 +45,6 @@ jobs: coverage: true allow_reresolve: false force_latest_compatible_version: false - env: - # `julia-downgrade-compat` promotes weakdeps to deps, so extensions always load - DOWNGRADE_TEST: "true" - uses: julia-actions/julia-processcoverage@v1 - uses: codecov/codecov-action@v7 with: diff --git a/test/Project.toml b/test/Project.toml index 45c1dff8..23b91319 100644 --- a/test/Project.toml +++ b/test/Project.toml @@ -28,7 +28,7 @@ IrrationalConstants = "0.2" LinearAlgebra = "1" LogExpFunctions = "1" NaNMath = "1" -ParallelTestRunner = "2" +ParallelTestRunner = "2.0.1" Random = "1" SpecialFunctions = "2" StaticArrays = "1.6.4" diff --git a/test/derivatives/LinAlgTests.jl b/test/derivatives/LinAlgTests.jl index a8b311b2..656817e3 100644 --- a/test/derivatives/LinAlgTests.jl +++ b/test/derivatives/LinAlgTests.jl @@ -1,9 +1,4 @@ using ReverseDiff, ForwardDiff, Test, LinearAlgebra, StaticArrays - -if get(ENV, "DOWNGRADE_TEST", "false")::String != "true" - @test Base.get_extension(ReverseDiff, :StatisticsExt) === nothing -end - using Statistics @test Base.get_extension(ReverseDiff, :StatisticsExt) !== nothing From 30c61316bb35f500f1dc5ea113a3b55a1f7eba25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20M=C3=BCller-Widmann?= Date: Fri, 23 Oct 2026 10:26:46 +0200 Subject: [PATCH 3/5] Split the slowest test files for better parallelism ElementwiseTests and JacobianTests dominated the wall time of the parallel test run. Move the binary DiffRules tests into ElementwiseBinaryTests and the nested Jacobian tests into NestedJacobianTests, with the shared elementwise helpers in derivatives/elementwise_utils.jl. Co-Authored-By: Claude Opus 5.5 --- test/api/JacobianTests.jl | 51 --- test/api/NestedJacobianTests.jl | 54 +++ test/derivatives/ElementwiseBinaryTests.jl | 24 ++ test/derivatives/ElementwiseTests.jl | 436 +-------------------- test/derivatives/elementwise_utils.jl | 420 ++++++++++++++++++++ test/runtests.jl | 3 +- 6 files changed, 502 insertions(+), 486 deletions(-) create mode 100644 test/api/NestedJacobianTests.jl create mode 100644 test/derivatives/ElementwiseBinaryTests.jl create mode 100644 test/derivatives/elementwise_utils.jl diff --git a/test/api/JacobianTests.jl b/test/api/JacobianTests.jl index 13e6e349..2d4927e7 100644 --- a/test/api/JacobianTests.jl +++ b/test/api/JacobianTests.jl @@ -267,57 +267,6 @@ for f in DiffTests.BINARY_MATRIX_TO_MATRIX_FUNCS test_binary_jacobian(f, rand(5, 5), rand(5, 5)) end -for f in (DiffTests.ARRAY_TO_ARRAY_FUNCS..., DiffTests.MATRIX_TO_MATRIX_FUNCS...) - test_println("ARRAY_TO_ARRAY_FUNCS + MATRIX_TO_MATRIX_FUNCS", f) - - x = rand(5, 5) - test = ForwardDiff.jacobian(y -> ForwardDiff.jacobian(f, y), x) - - # without JacobianTape - - J = ReverseDiff.jacobian(y -> ReverseDiff.jacobian(f, y), x) - test_approx(J, test) - - # with JacobianTape - - tp = ReverseDiff.JacobianTape(y -> ReverseDiff.jacobian(f, y), rand(eltype(x), size(x))) - J = ReverseDiff.jacobian!(tp, x) - test_approx(J, test) -end - -for f in DiffTests.BINARY_MATRIX_TO_MATRIX_FUNCS - test_println("BINARY_MATRIX_TO_MATRIX_FUNCS", f) - - a, b = rand(5, 5), rand(5, 5) - - test_val = f(a, b) - test_a = ForwardDiff.jacobian(y -> ForwardDiff.jacobian(x -> f(x, b), y), a) - test_b = ForwardDiff.jacobian(y -> ForwardDiff.jacobian(x -> f(a, x), y), b) - - # without JacobianTape - - Ja = ReverseDiff.jacobian(y -> ReverseDiff.jacobian(x -> f(x, b), y), a) - Jb = ReverseDiff.jacobian(y -> ReverseDiff.jacobian(x -> f(a, x), y), b) - test_approx(Ja, test_a) - test_approx(Jb, test_b) - - # with JacobianTape - - ra = ReverseDiff.JacobianTape(y -> ReverseDiff.jacobian(x -> f(x, b), y), rand(eltype(a), size(a))) - rb = ReverseDiff.JacobianTape(y -> ReverseDiff.jacobian(x -> f(a, x), y), rand(eltype(b), size(b))) - Ja = ReverseDiff.jacobian!(ra, a) - Jb = ReverseDiff.jacobian!(rb, b) - test_approx(Ja, test_a) - test_approx(Jb, test_b) - - # The below will fail until support for the Jacobian of - # functions with multiple output arrays is implemented - - # Ja, Jb = ReverseDiff.jacobian((x, y) -> ReverseDiff.jacobian(f, (x, y)), (a, b)) - # test_approx(Ja test_a) - # test_approx(Jb test_b) -end - ############################################################################################ f251(x) = x .^ 2 diff --git a/test/api/NestedJacobianTests.jl b/test/api/NestedJacobianTests.jl new file mode 100644 index 00000000..536d0bdc --- /dev/null +++ b/test/api/NestedJacobianTests.jl @@ -0,0 +1,54 @@ +using DiffTests, ForwardDiff, ReverseDiff, Test + +include(joinpath(dirname(@__FILE__), "../utils.jl")) + +for f in (DiffTests.ARRAY_TO_ARRAY_FUNCS..., DiffTests.MATRIX_TO_MATRIX_FUNCS...) + test_println("ARRAY_TO_ARRAY_FUNCS + MATRIX_TO_MATRIX_FUNCS", f) + + x = rand(5, 5) + test = ForwardDiff.jacobian(y -> ForwardDiff.jacobian(f, y), x) + + # without JacobianTape + + J = ReverseDiff.jacobian(y -> ReverseDiff.jacobian(f, y), x) + test_approx(J, test) + + # with JacobianTape + + tp = ReverseDiff.JacobianTape(y -> ReverseDiff.jacobian(f, y), rand(eltype(x), size(x))) + J = ReverseDiff.jacobian!(tp, x) + test_approx(J, test) +end + +for f in DiffTests.BINARY_MATRIX_TO_MATRIX_FUNCS + test_println("BINARY_MATRIX_TO_MATRIX_FUNCS", f) + + a, b = rand(5, 5), rand(5, 5) + + test_val = f(a, b) + test_a = ForwardDiff.jacobian(y -> ForwardDiff.jacobian(x -> f(x, b), y), a) + test_b = ForwardDiff.jacobian(y -> ForwardDiff.jacobian(x -> f(a, x), y), b) + + # without JacobianTape + + Ja = ReverseDiff.jacobian(y -> ReverseDiff.jacobian(x -> f(x, b), y), a) + Jb = ReverseDiff.jacobian(y -> ReverseDiff.jacobian(x -> f(a, x), y), b) + test_approx(Ja, test_a) + test_approx(Jb, test_b) + + # with JacobianTape + + ra = ReverseDiff.JacobianTape(y -> ReverseDiff.jacobian(x -> f(x, b), y), rand(eltype(a), size(a))) + rb = ReverseDiff.JacobianTape(y -> ReverseDiff.jacobian(x -> f(a, x), y), rand(eltype(b), size(b))) + Ja = ReverseDiff.jacobian!(ra, a) + Jb = ReverseDiff.jacobian!(rb, b) + test_approx(Ja, test_a) + test_approx(Jb, test_b) + + # The below will fail until support for the Jacobian of + # functions with multiple output arrays is implemented + + # Ja, Jb = ReverseDiff.jacobian((x, y) -> ReverseDiff.jacobian(f, (x, y)), (a, b)) + # test_approx(Ja test_a) + # test_approx(Jb test_b) +end diff --git a/test/derivatives/ElementwiseBinaryTests.jl b/test/derivatives/ElementwiseBinaryTests.jl new file mode 100644 index 00000000..0b5f3e97 --- /dev/null +++ b/test/derivatives/ElementwiseBinaryTests.jl @@ -0,0 +1,24 @@ +include("elementwise_utils.jl") + +for (M, fsym, arity) in DiffRules.diffrules(; filter_modules = nothing) + # ensure that all rules can be tested + if !(isdefined(@__MODULE__, M) && isdefined(getfield(@__MODULE__, M), fsym)) + error("$M.$fsym is not available") + end + (M, fsym) in ReverseDiff.SKIPPED_DIFFRULES && continue + if arity == 2 + in(fsym, SKIPPED_BINARY_SCALAR_TESTS) && continue + f = eval(:($M.$fsym)) + test_println("forward-mode binary scalar functions", f) + test_map(f, f, x, y, tp) + test_map(f, f, a, b, tp) + test_broadcast(f, f, x, y, tp) + test_broadcast(f, f, a, b, tp) + test_broadcast(f, f, x, a, tp) + test_broadcast(f, f, a, x, tp) + test_broadcast(f, f, n, x, tp) + test_broadcast(f, f, x, n, tp) + test_broadcast(f, f, n, a, tp) + test_broadcast(f, f, a, n, tp) + end +end diff --git a/test/derivatives/ElementwiseTests.jl b/test/derivatives/ElementwiseTests.jl index c62c7bf2..ebf0803a 100644 --- a/test/derivatives/ElementwiseTests.jl +++ b/test/derivatives/ElementwiseTests.jl @@ -1,423 +1,4 @@ -using ReverseDiff - -using DiffRules -using DiffTests -using ForwardDiff -using LogExpFunctions -using NaNMath -using SpecialFunctions - -using Test - -include(joinpath(dirname(@__FILE__), "../utils.jl")) - -x, y = rand(3, 3), rand(3, 3) -a, b = rand(3), rand(3) -n = rand() -tp = InstructionTape() -offset = 0.00001 - -function test_elementwise(f, fopt, x, tp) - xt = track(copy(x), tp) - - ######################################## - - y = map(f, x) - - # record - yt = map(fopt, xt) - @test yt == y - ReverseDiff.finish!(tp) - @test length(tp) == 1 - - # reverse - out = similar(y, (length(x), length(x))) - ReverseDiff.seeded_reverse_pass!(out, yt, xt, tp) - test_approx(out, ForwardDiff.jacobian(z -> map(f, z), x); nans = true) - - # forward - x2 = x .- offset - ReverseDiff.value!(xt, x2) - ReverseDiff.forward_pass!(tp) - @test yt == map(f, x2) - ReverseDiff.value!(xt, x) - - empty!(tp) - - ######################################## - - y = broadcast(f, x) - - # record - yt = broadcast(fopt, xt) - @test yt == y - # a constant function has no derivative and is left off the tape - tracked = f !== DiffTests.num2num_4 - @test (yt isa ReverseDiff.TrackedArray) == tracked - ReverseDiff.finish!(tp) - @test length(tp) == (tracked ? 1 : 0) - - # reverse - out = similar(y, (length(x), length(x))) - ReverseDiff.seeded_reverse_pass!(out, yt, xt, tp) - test_approx(out, ForwardDiff.jacobian(z -> broadcast(f, z), x); nans = true) - - # forward, which cannot reach an untracked result (#313) - if tracked - x2 = x .- offset - ReverseDiff.value!(xt, x2) - ReverseDiff.forward_pass!(tp) - @test yt == broadcast(f, x2) - ReverseDiff.value!(xt, x) - end - - empty!(tp) - return nothing -end - -function test_map(f, fopt, a, b, tp) - at, bt = track(copy(a), tp), track(copy(b), tp) - c = map(f, a, b) - - ######################################## - - # record - ct = map(fopt, at, b) - @test ct == c - ReverseDiff.finish!(tp) - @test length(tp) == 1 - - # reverse - out = similar(c, (length(c), length(a))) - ReverseDiff.seeded_reverse_pass!(out, ct, at, tp) - test_approx(out, ForwardDiff.jacobian(x -> map(f, x, b), a); nans = true) - - # forward - a2 = a .- offset - ReverseDiff.value!(at, a2) - ReverseDiff.forward_pass!(tp) - @test ct == map(f, a2, b) - ReverseDiff.value!(at, a) - - empty!(tp) - - ######################################## - - # record - ct = map(fopt, a, bt) - @test ct == c - ReverseDiff.finish!(tp) - @test length(tp) == 1 - - # reverse - out = similar(c, (length(c), length(b))) - ReverseDiff.seeded_reverse_pass!(out, ct, bt, tp) - test_approx(out, ForwardDiff.jacobian(x -> map(f, a, x), b); nans = true) - - # forward - b2 = b .- offset - ReverseDiff.value!(bt, b2) - ReverseDiff.forward_pass!(tp) - @test ct == map(f, a, b2) - ReverseDiff.value!(bt, b) - - empty!(tp) - - ######################################## - - # record - ct = map(fopt, at, bt) - @test ct == c - ReverseDiff.finish!(tp) - @test length(tp) == 1 - - # reverse - out_a = similar(c, (length(c), length(a))) - out_b = similar(c, (length(c), length(b))) - ReverseDiff.seeded_reverse_pass!(out_a, ct, at, tp) - ReverseDiff.seeded_reverse_pass!(out_b, ct, bt, tp) - jac = let a = a, b = b, f = f - ForwardDiff.jacobian(vcat(vec(a), vec(b))) do x - map(f, reshape(x[1:length(a)], size(a)), reshape(x[(length(a) + 1):end], size(b))) - end - end - test_approx(out_a, jac[:, 1:length(a)]; nans = true) - test_approx(out_b, jac[:, (length(a) + 1):end]; nans = true) - # forward - a2, b2 = a .- offset, b .- offset - ReverseDiff.value!(at, a2) - ReverseDiff.value!(bt, b2) - ReverseDiff.forward_pass!(tp) - @test ct == map(f, a2, b2) - ReverseDiff.value!(at, a) - ReverseDiff.value!(bt, b) - - empty!(tp) - return nothing -end - -function test_broadcast(f, fopt, a::AbstractArray, b::AbstractArray, tp, builtin::Bool = false) - at, bt = track(copy(a), tp), track(copy(b), tp) - - if builtin - g = fopt - else - g = (x, y) -> broadcast(fopt, x, y) - end - - c = g(a, b) - - ######################################## - - # record - ct = g(at, b) - @test ct == c - ReverseDiff.finish!(tp) - @test length(tp) == 1 - - # reverse - out = similar(c, (length(c), length(a))) - ReverseDiff.seeded_reverse_pass!(out, ct, at, tp) - test_approx(out, ForwardDiff.jacobian(x -> g(x, b), a); nans = true) - - # forward - a2 = a .- offset - ReverseDiff.value!(at, a2) - ReverseDiff.forward_pass!(tp) - @test ct == g(a2, b) - ReverseDiff.value!(at, a) - - empty!(tp) - - ######################################## - - # record - ct = g(a, bt) - @test ct == c - ReverseDiff.finish!(tp) - @test length(tp) == 1 - - # reverse - out = similar(c, (length(c), length(b))) - ReverseDiff.seeded_reverse_pass!(out, ct, bt, tp) - test_approx(out, ForwardDiff.jacobian(x -> g(a, x), b); nans = true) - - # forward - b2 = b .- offset - ReverseDiff.value!(bt, b2) - ReverseDiff.forward_pass!(tp) - @test ct == g(a, b2) - ReverseDiff.value!(bt, b) - - empty!(tp) - - ######################################## - - # record - ct = g(at, bt) - @test ct == c - ReverseDiff.finish!(tp) - @test length(tp) == 1 - - # reverse - out_a = similar(c, (length(c), length(a))) - out_b = similar(c, (length(c), length(b))) - ReverseDiff.seeded_reverse_pass!(out_a, ct, at, tp) - ReverseDiff.seeded_reverse_pass!(out_b, ct, bt, tp) - jac = let a = a, b = b, g = g - ForwardDiff.jacobian(vcat(vec(a), vec(b))) do x - g(reshape(x[1:length(a)], size(a)), reshape(x[(length(a) + 1):end], size(b))) - end - end - test_approx(out_a, jac[:, 1:length(a)]; nans = true) - test_approx(out_b, jac[:, (length(a) + 1):end]; nans = true) - - # forward - a2, b2 = a .- offset, b .- offset - ReverseDiff.value!(at, a2) - ReverseDiff.value!(bt, b2) - ReverseDiff.forward_pass!(tp) - @test ct == g(a2, b2) - ReverseDiff.value!(at, a) - ReverseDiff.value!(bt, b) - - empty!(tp) - return nothing -end - -function test_broadcast(f, fopt, n::Number, x::AbstractArray, tp, builtin::Bool = false) - nt, xt = track(copy(n), tp), track(copy(x), tp) - - if builtin - g = fopt - else - g = (x, y) -> broadcast(fopt, x, y) - end - - y = g(n, x) - - ######################################## - - # record - yt = g(nt, x) - @test yt == y - ReverseDiff.finish!(tp) - @test length(tp) == 1 - - # reverse - out = similar(y) - ReverseDiff.seeded_reverse_pass!(out, yt, nt, tp) - test_approx(out, ForwardDiff.derivative(z -> g(z, x), n); nans = true) - - # forward - n2 = n + offset - ReverseDiff.value!(nt, n2) - ReverseDiff.forward_pass!(tp) - @test yt == g(n2, x) - ReverseDiff.value!(nt, n) - - empty!(tp) - - ######################################## - - # record - yt = g(n, xt) - @test yt == y - ReverseDiff.finish!(tp) - @test length(tp) == 1 - - # reverse - out = similar(y, (length(y), length(x))) - ReverseDiff.seeded_reverse_pass!(out, yt, xt, tp) - test_approx(out, ForwardDiff.jacobian(z -> g(n, z), x); nans = true) - - # forward - x2 = x .- offset - ReverseDiff.value!(xt, x2) - ReverseDiff.forward_pass!(tp) - @test yt == g(n, x2) - ReverseDiff.value!(xt, x) - - empty!(tp) - - ######################################## - - # record - yt = g(nt, xt) - @test yt == y - ReverseDiff.finish!(tp) - @test length(tp) == 1 - - # reverse - out_n = similar(y) - out_x = similar(y, (length(y), length(x))) - ReverseDiff.seeded_reverse_pass!(out_n, yt, nt, tp) - ReverseDiff.seeded_reverse_pass!(out_x, yt, xt, tp) - jac = let x = x, g = g - ForwardDiff.jacobian(z -> g(z[1], reshape(z[2:end], size(x))), vcat(n, vec(x))) - end - test_approx(out_n, reshape(jac[:, 1], size(y)); nans = true) - test_approx(out_x, jac[:, 2:end]; nans = true) - - # forward - n2, x2 = n + offset, x .- offset - ReverseDiff.value!(nt, n2) - ReverseDiff.value!(xt, x2) - ReverseDiff.forward_pass!(tp) - @test yt == g(n2, x2) - ReverseDiff.value!(nt, n) - ReverseDiff.value!(xt, x) - - empty!(tp) - return nothing -end - -function test_broadcast(f, fopt, x::AbstractArray, n::Number, tp, builtin::Bool = false) - xt, nt = track(copy(x), tp), track(copy(n), tp) - - if builtin - g = fopt - else - g = (x, y) -> broadcast(fopt, x, y) - end - - y = g(x, n) - - ######################################## - - # record - yt = g(x, nt) - @test yt == y - ReverseDiff.finish!(tp) - @test length(tp) == 1 - - # reverse - out = similar(y) - ReverseDiff.seeded_reverse_pass!(out, yt, nt, tp) - test_approx(out, ForwardDiff.derivative(z -> g(x, z), n); nans = true) - - # forward - n2 = n + offset - ReverseDiff.value!(nt, n2) - ReverseDiff.forward_pass!(tp) - @test yt == g(x, n2) - ReverseDiff.value!(nt, n) - - empty!(tp) - - ######################################## - - # record - yt = g(xt, n) - @test yt == y - ReverseDiff.finish!(tp) - @test length(tp) == 1 - - # reverse - out = similar(y, (length(y), length(x))) - ReverseDiff.seeded_reverse_pass!(out, yt, xt, tp) - test_approx(out, ForwardDiff.jacobian(z -> g(z, n), x); nans = true) - - # forward - x2 = x .- offset - ReverseDiff.value!(xt, x2) - ReverseDiff.forward_pass!(tp) - @test yt == g(x2, n) - ReverseDiff.value!(xt, x) - - empty!(tp) - - ######################################## - - # record - yt = g(xt, nt) - @test yt == y - ReverseDiff.finish!(tp) - @test length(tp) == 1 - - # reverse - out_n = similar(y) - out_x = similar(y, (length(y), length(x))) - ReverseDiff.seeded_reverse_pass!(out_n, yt, nt, tp) - ReverseDiff.seeded_reverse_pass!(out_x, yt, xt, tp) - jac = let x = x, g = g - ForwardDiff.jacobian(z -> g(reshape(z[1:(end - 1)], size(x)), z[end]), vcat(vec(x), n)) - end - test_approx(out_x, jac[:, 1:(end - 1)]; nans = true) - test_approx(out_n, reshape(jac[:, end], size(y)); nans = true) - - # forward - x2, n2 = x .- offset, n + offset - ReverseDiff.value!(xt, x2) - ReverseDiff.value!(nt, n2) - ReverseDiff.forward_pass!(tp) - @test yt == g(x2, n2) - ReverseDiff.value!(xt, x) - ReverseDiff.value!(nt, n) - - empty!(tp) - return nothing -end +include("elementwise_utils.jl") for f in DiffTests.NUMBER_TO_NUMBER_FUNCS test_println("DiffTests.NUMBER_TO_NUMBER_FUNCS", f) @@ -436,21 +17,8 @@ for (M, fsym, arity) in DiffRules.diffrules(; filter_modules = nothing) test_println("forward-mode unary scalar functions", f) test_elementwise(f, f, modify_input(fsym, x), tp) test_elementwise(f, f, modify_input(fsym, a), tp) - elseif arity == 2 - in(fsym, SKIPPED_BINARY_SCALAR_TESTS) && continue - f = eval(:($M.$fsym)) - test_println("forward-mode binary scalar functions", f) - test_map(f, f, x, y, tp) - test_map(f, f, a, b, tp) - test_broadcast(f, f, x, y, tp) - test_broadcast(f, f, a, b, tp) - test_broadcast(f, f, x, a, tp) - test_broadcast(f, f, a, x, tp) - test_broadcast(f, f, n, x, tp) - test_broadcast(f, f, x, n, tp) - test_broadcast(f, f, n, a, tp) - test_broadcast(f, f, a, n, tp) end + # binary rules are tested in ElementwiseBinaryTests.jl end for f in DiffTests.BINARY_BROADCAST_OPS diff --git a/test/derivatives/elementwise_utils.jl b/test/derivatives/elementwise_utils.jl new file mode 100644 index 00000000..b6f25d72 --- /dev/null +++ b/test/derivatives/elementwise_utils.jl @@ -0,0 +1,420 @@ +using ReverseDiff + +using DiffRules +using DiffTests +using ForwardDiff +using LogExpFunctions +using NaNMath +using SpecialFunctions + +using Test + +include(joinpath(dirname(@__FILE__), "../utils.jl")) + +x, y = rand(3, 3), rand(3, 3) +a, b = rand(3), rand(3) +n = rand() +tp = InstructionTape() +offset = 0.00001 + +function test_elementwise(f, fopt, x, tp) + xt = track(copy(x), tp) + + ######################################## + + y = map(f, x) + + # record + yt = map(fopt, xt) + @test yt == y + ReverseDiff.finish!(tp) + @test length(tp) == 1 + + # reverse + out = similar(y, (length(x), length(x))) + ReverseDiff.seeded_reverse_pass!(out, yt, xt, tp) + test_approx(out, ForwardDiff.jacobian(z -> map(f, z), x); nans = true) + + # forward + x2 = x .- offset + ReverseDiff.value!(xt, x2) + ReverseDiff.forward_pass!(tp) + @test yt == map(f, x2) + ReverseDiff.value!(xt, x) + + empty!(tp) + + ######################################## + + y = broadcast(f, x) + + # record + yt = broadcast(fopt, xt) + @test yt == y + # a constant function has no derivative and is left off the tape + tracked = f !== DiffTests.num2num_4 + @test (yt isa ReverseDiff.TrackedArray) == tracked + ReverseDiff.finish!(tp) + @test length(tp) == (tracked ? 1 : 0) + + # reverse + out = similar(y, (length(x), length(x))) + ReverseDiff.seeded_reverse_pass!(out, yt, xt, tp) + test_approx(out, ForwardDiff.jacobian(z -> broadcast(f, z), x); nans = true) + + # forward, which cannot reach an untracked result (#313) + if tracked + x2 = x .- offset + ReverseDiff.value!(xt, x2) + ReverseDiff.forward_pass!(tp) + @test yt == broadcast(f, x2) + ReverseDiff.value!(xt, x) + end + + empty!(tp) + return nothing +end + +function test_map(f, fopt, a, b, tp) + at, bt = track(copy(a), tp), track(copy(b), tp) + c = map(f, a, b) + + ######################################## + + # record + ct = map(fopt, at, b) + @test ct == c + ReverseDiff.finish!(tp) + @test length(tp) == 1 + + # reverse + out = similar(c, (length(c), length(a))) + ReverseDiff.seeded_reverse_pass!(out, ct, at, tp) + test_approx(out, ForwardDiff.jacobian(x -> map(f, x, b), a); nans = true) + + # forward + a2 = a .- offset + ReverseDiff.value!(at, a2) + ReverseDiff.forward_pass!(tp) + @test ct == map(f, a2, b) + ReverseDiff.value!(at, a) + + empty!(tp) + + ######################################## + + # record + ct = map(fopt, a, bt) + @test ct == c + ReverseDiff.finish!(tp) + @test length(tp) == 1 + + # reverse + out = similar(c, (length(c), length(b))) + ReverseDiff.seeded_reverse_pass!(out, ct, bt, tp) + test_approx(out, ForwardDiff.jacobian(x -> map(f, a, x), b); nans = true) + + # forward + b2 = b .- offset + ReverseDiff.value!(bt, b2) + ReverseDiff.forward_pass!(tp) + @test ct == map(f, a, b2) + ReverseDiff.value!(bt, b) + + empty!(tp) + + ######################################## + + # record + ct = map(fopt, at, bt) + @test ct == c + ReverseDiff.finish!(tp) + @test length(tp) == 1 + + # reverse + out_a = similar(c, (length(c), length(a))) + out_b = similar(c, (length(c), length(b))) + ReverseDiff.seeded_reverse_pass!(out_a, ct, at, tp) + ReverseDiff.seeded_reverse_pass!(out_b, ct, bt, tp) + jac = let a = a, b = b, f = f + ForwardDiff.jacobian(vcat(vec(a), vec(b))) do x + map(f, reshape(x[1:length(a)], size(a)), reshape(x[(length(a) + 1):end], size(b))) + end + end + test_approx(out_a, jac[:, 1:length(a)]; nans = true) + test_approx(out_b, jac[:, (length(a) + 1):end]; nans = true) + # forward + a2, b2 = a .- offset, b .- offset + ReverseDiff.value!(at, a2) + ReverseDiff.value!(bt, b2) + ReverseDiff.forward_pass!(tp) + @test ct == map(f, a2, b2) + ReverseDiff.value!(at, a) + ReverseDiff.value!(bt, b) + + empty!(tp) + return nothing +end + +function test_broadcast(f, fopt, a::AbstractArray, b::AbstractArray, tp, builtin::Bool = false) + at, bt = track(copy(a), tp), track(copy(b), tp) + + if builtin + g = fopt + else + g = (x, y) -> broadcast(fopt, x, y) + end + + c = g(a, b) + + ######################################## + + # record + ct = g(at, b) + @test ct == c + ReverseDiff.finish!(tp) + @test length(tp) == 1 + + # reverse + out = similar(c, (length(c), length(a))) + ReverseDiff.seeded_reverse_pass!(out, ct, at, tp) + test_approx(out, ForwardDiff.jacobian(x -> g(x, b), a); nans = true) + + # forward + a2 = a .- offset + ReverseDiff.value!(at, a2) + ReverseDiff.forward_pass!(tp) + @test ct == g(a2, b) + ReverseDiff.value!(at, a) + + empty!(tp) + + ######################################## + + # record + ct = g(a, bt) + @test ct == c + ReverseDiff.finish!(tp) + @test length(tp) == 1 + + # reverse + out = similar(c, (length(c), length(b))) + ReverseDiff.seeded_reverse_pass!(out, ct, bt, tp) + test_approx(out, ForwardDiff.jacobian(x -> g(a, x), b); nans = true) + + # forward + b2 = b .- offset + ReverseDiff.value!(bt, b2) + ReverseDiff.forward_pass!(tp) + @test ct == g(a, b2) + ReverseDiff.value!(bt, b) + + empty!(tp) + + ######################################## + + # record + ct = g(at, bt) + @test ct == c + ReverseDiff.finish!(tp) + @test length(tp) == 1 + + # reverse + out_a = similar(c, (length(c), length(a))) + out_b = similar(c, (length(c), length(b))) + ReverseDiff.seeded_reverse_pass!(out_a, ct, at, tp) + ReverseDiff.seeded_reverse_pass!(out_b, ct, bt, tp) + jac = let a = a, b = b, g = g + ForwardDiff.jacobian(vcat(vec(a), vec(b))) do x + g(reshape(x[1:length(a)], size(a)), reshape(x[(length(a) + 1):end], size(b))) + end + end + test_approx(out_a, jac[:, 1:length(a)]; nans = true) + test_approx(out_b, jac[:, (length(a) + 1):end]; nans = true) + + # forward + a2, b2 = a .- offset, b .- offset + ReverseDiff.value!(at, a2) + ReverseDiff.value!(bt, b2) + ReverseDiff.forward_pass!(tp) + @test ct == g(a2, b2) + ReverseDiff.value!(at, a) + ReverseDiff.value!(bt, b) + + empty!(tp) + return nothing +end + +function test_broadcast(f, fopt, n::Number, x::AbstractArray, tp, builtin::Bool = false) + nt, xt = track(copy(n), tp), track(copy(x), tp) + + if builtin + g = fopt + else + g = (x, y) -> broadcast(fopt, x, y) + end + + y = g(n, x) + + ######################################## + + # record + yt = g(nt, x) + @test yt == y + ReverseDiff.finish!(tp) + @test length(tp) == 1 + + # reverse + out = similar(y) + ReverseDiff.seeded_reverse_pass!(out, yt, nt, tp) + test_approx(out, ForwardDiff.derivative(z -> g(z, x), n); nans = true) + + # forward + n2 = n + offset + ReverseDiff.value!(nt, n2) + ReverseDiff.forward_pass!(tp) + @test yt == g(n2, x) + ReverseDiff.value!(nt, n) + + empty!(tp) + + ######################################## + + # record + yt = g(n, xt) + @test yt == y + ReverseDiff.finish!(tp) + @test length(tp) == 1 + + # reverse + out = similar(y, (length(y), length(x))) + ReverseDiff.seeded_reverse_pass!(out, yt, xt, tp) + test_approx(out, ForwardDiff.jacobian(z -> g(n, z), x); nans = true) + + # forward + x2 = x .- offset + ReverseDiff.value!(xt, x2) + ReverseDiff.forward_pass!(tp) + @test yt == g(n, x2) + ReverseDiff.value!(xt, x) + + empty!(tp) + + ######################################## + + # record + yt = g(nt, xt) + @test yt == y + ReverseDiff.finish!(tp) + @test length(tp) == 1 + + # reverse + out_n = similar(y) + out_x = similar(y, (length(y), length(x))) + ReverseDiff.seeded_reverse_pass!(out_n, yt, nt, tp) + ReverseDiff.seeded_reverse_pass!(out_x, yt, xt, tp) + jac = let x = x, g = g + ForwardDiff.jacobian(z -> g(z[1], reshape(z[2:end], size(x))), vcat(n, vec(x))) + end + test_approx(out_n, reshape(jac[:, 1], size(y)); nans = true) + test_approx(out_x, jac[:, 2:end]; nans = true) + + # forward + n2, x2 = n + offset, x .- offset + ReverseDiff.value!(nt, n2) + ReverseDiff.value!(xt, x2) + ReverseDiff.forward_pass!(tp) + @test yt == g(n2, x2) + ReverseDiff.value!(nt, n) + ReverseDiff.value!(xt, x) + + empty!(tp) + return nothing +end + +function test_broadcast(f, fopt, x::AbstractArray, n::Number, tp, builtin::Bool = false) + xt, nt = track(copy(x), tp), track(copy(n), tp) + + if builtin + g = fopt + else + g = (x, y) -> broadcast(fopt, x, y) + end + + y = g(x, n) + + ######################################## + + # record + yt = g(x, nt) + @test yt == y + ReverseDiff.finish!(tp) + @test length(tp) == 1 + + # reverse + out = similar(y) + ReverseDiff.seeded_reverse_pass!(out, yt, nt, tp) + test_approx(out, ForwardDiff.derivative(z -> g(x, z), n); nans = true) + + # forward + n2 = n + offset + ReverseDiff.value!(nt, n2) + ReverseDiff.forward_pass!(tp) + @test yt == g(x, n2) + ReverseDiff.value!(nt, n) + + empty!(tp) + + ######################################## + + # record + yt = g(xt, n) + @test yt == y + ReverseDiff.finish!(tp) + @test length(tp) == 1 + + # reverse + out = similar(y, (length(y), length(x))) + ReverseDiff.seeded_reverse_pass!(out, yt, xt, tp) + test_approx(out, ForwardDiff.jacobian(z -> g(z, n), x); nans = true) + + # forward + x2 = x .- offset + ReverseDiff.value!(xt, x2) + ReverseDiff.forward_pass!(tp) + @test yt == g(x2, n) + ReverseDiff.value!(xt, x) + + empty!(tp) + + ######################################## + + # record + yt = g(xt, nt) + @test yt == y + ReverseDiff.finish!(tp) + @test length(tp) == 1 + + # reverse + out_n = similar(y) + out_x = similar(y, (length(y), length(x))) + ReverseDiff.seeded_reverse_pass!(out_n, yt, nt, tp) + ReverseDiff.seeded_reverse_pass!(out_x, yt, xt, tp) + jac = let x = x, g = g + ForwardDiff.jacobian(z -> g(reshape(z[1:(end - 1)], size(x)), z[end]), vcat(vec(x), n)) + end + test_approx(out_x, jac[:, 1:(end - 1)]; nans = true) + test_approx(out_n, reshape(jac[:, end], size(y)); nans = true) + + # forward + x2, n2 = x .- offset, n + offset + ReverseDiff.value!(xt, x2) + ReverseDiff.value!(nt, n2) + ReverseDiff.forward_pass!(tp) + @test yt == g(x2, n2) + ReverseDiff.value!(xt, x) + ReverseDiff.value!(nt, n) + + empty!(tp) + return nothing +end diff --git a/test/runtests.jl b/test/runtests.jl index 6dba4a50..f4b636e1 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,11 +1,12 @@ using ReverseDiff using ParallelTestRunner -# Every file under `test/` except this one and `utils.jl` is a test, named by its path, +# Every file under `test/` except this one and the shared helpers is a test, named by its path, # e.g. "api/GradientTests". Select tests via `Pkg.test("ReverseDiff"; test_args=["api/GradientTests"])`, # exclude them with a leading `!`, and list all with `test_args=["--list"]`. const testsuite = find_tests(@__DIR__) delete!(testsuite, "utils") +delete!(testsuite, "derivatives/elementwise_utils") # Workers are single-threaded by default function test_worker(name) From d30f2feb19af5027086fef72cbeabbdf73e49fd3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20M=C3=BCller-Widmann?= Date: Fri, 23 Oct 2026 16:10:38 +0200 Subject: [PATCH 4/5] Split the binary elementwise tests in two and silence soft-scope warnings ElementwiseBinaryTests took about 960 s on its own worker and set the wall time of the whole run, while the other workers finished after about 750 s. Each half now takes every other binary rule. Co-Authored-By: Claude Opus 5.5 --- test/TrackedTests.jl | 1 + test/derivatives/ElementwiseBinaryTests.jl | 24 ------------------ test/derivatives/ElementwiseBinaryTests1.jl | 5 ++++ test/derivatives/ElementwiseBinaryTests2.jl | 5 ++++ test/derivatives/ElementwiseTests.jl | 2 +- test/derivatives/elementwise_utils.jl | 28 +++++++++++++++++++++ 6 files changed, 40 insertions(+), 25 deletions(-) delete mode 100644 test/derivatives/ElementwiseBinaryTests.jl create mode 100644 test/derivatives/ElementwiseBinaryTests1.jl create mode 100644 test/derivatives/ElementwiseBinaryTests2.jl diff --git a/test/TrackedTests.jl b/test/TrackedTests.jl index 5a209e19..97be9f9f 100644 --- a/test/TrackedTests.jl +++ b/test/TrackedTests.jl @@ -650,6 +650,7 @@ instr = first(recorded) @test instr.cache === nothing for T in (UInt, Int) + local ta_sub, idx, recorded, instr ta_sub = ta[:, T(1):T(2)] idx = ReverseDiff.index_iterable(axes(ta), (:, T(1):T(2))) @test collect(idx) == [(i, j) for i in 1:3, j in 1:2] diff --git a/test/derivatives/ElementwiseBinaryTests.jl b/test/derivatives/ElementwiseBinaryTests.jl deleted file mode 100644 index 0b5f3e97..00000000 --- a/test/derivatives/ElementwiseBinaryTests.jl +++ /dev/null @@ -1,24 +0,0 @@ -include("elementwise_utils.jl") - -for (M, fsym, arity) in DiffRules.diffrules(; filter_modules = nothing) - # ensure that all rules can be tested - if !(isdefined(@__MODULE__, M) && isdefined(getfield(@__MODULE__, M), fsym)) - error("$M.$fsym is not available") - end - (M, fsym) in ReverseDiff.SKIPPED_DIFFRULES && continue - if arity == 2 - in(fsym, SKIPPED_BINARY_SCALAR_TESTS) && continue - f = eval(:($M.$fsym)) - test_println("forward-mode binary scalar functions", f) - test_map(f, f, x, y, tp) - test_map(f, f, a, b, tp) - test_broadcast(f, f, x, y, tp) - test_broadcast(f, f, a, b, tp) - test_broadcast(f, f, x, a, tp) - test_broadcast(f, f, a, x, tp) - test_broadcast(f, f, n, x, tp) - test_broadcast(f, f, x, n, tp) - test_broadcast(f, f, n, a, tp) - test_broadcast(f, f, a, n, tp) - end -end diff --git a/test/derivatives/ElementwiseBinaryTests1.jl b/test/derivatives/ElementwiseBinaryTests1.jl new file mode 100644 index 00000000..013eda58 --- /dev/null +++ b/test/derivatives/ElementwiseBinaryTests1.jl @@ -0,0 +1,5 @@ +include("elementwise_utils.jl") + +for (M, fsym) in binary_diffrules()[1:2:end] + test_binary(M, fsym) +end diff --git a/test/derivatives/ElementwiseBinaryTests2.jl b/test/derivatives/ElementwiseBinaryTests2.jl new file mode 100644 index 00000000..9d485e89 --- /dev/null +++ b/test/derivatives/ElementwiseBinaryTests2.jl @@ -0,0 +1,5 @@ +include("elementwise_utils.jl") + +for (M, fsym) in binary_diffrules()[2:2:end] + test_binary(M, fsym) +end diff --git a/test/derivatives/ElementwiseTests.jl b/test/derivatives/ElementwiseTests.jl index ebf0803a..97f9f4c9 100644 --- a/test/derivatives/ElementwiseTests.jl +++ b/test/derivatives/ElementwiseTests.jl @@ -18,7 +18,7 @@ for (M, fsym, arity) in DiffRules.diffrules(; filter_modules = nothing) test_elementwise(f, f, modify_input(fsym, x), tp) test_elementwise(f, f, modify_input(fsym, a), tp) end - # binary rules are tested in ElementwiseBinaryTests.jl + # binary rules are tested in ElementwiseBinaryTests1.jl and ElementwiseBinaryTests2.jl end for f in DiffTests.BINARY_BROADCAST_OPS diff --git a/test/derivatives/elementwise_utils.jl b/test/derivatives/elementwise_utils.jl index b6f25d72..a7ad22a4 100644 --- a/test/derivatives/elementwise_utils.jl +++ b/test/derivatives/elementwise_utils.jl @@ -418,3 +418,31 @@ function test_broadcast(f, fopt, x::AbstractArray, n::Number, tp, builtin::Bool empty!(tp) return nothing end + +# Tested binary rules, sorted so that ElementwiseBinaryTests1.jl and +# ElementwiseBinaryTests2.jl can each take every other one +function binary_diffrules() + rules = Tuple{Symbol, Symbol}[] + for (M, fsym, arity) in DiffRules.diffrules(; filter_modules = nothing) + if arity == 2 && !((M, fsym) in ReverseDiff.SKIPPED_DIFFRULES) && !(fsym in SKIPPED_BINARY_SCALAR_TESTS) + push!(rules, (M, fsym)) + end + end + return sort!(rules) +end + +function test_binary(M, fsym) + f = eval(:($M.$fsym)) + test_println("forward-mode binary scalar functions", f) + test_map(f, f, x, y, tp) + test_map(f, f, a, b, tp) + test_broadcast(f, f, x, y, tp) + test_broadcast(f, f, a, b, tp) + test_broadcast(f, f, x, a, tp) + test_broadcast(f, f, a, x, tp) + test_broadcast(f, f, n, x, tp) + test_broadcast(f, f, x, n, tp) + test_broadcast(f, f, n, a, tp) + test_broadcast(f, f, a, n, tp) + return nothing +end From 26179caff3eb65c2b7558214ef691b2d15e4c0c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20M=C3=BCller-Widmann?= Date: Sat, 24 Oct 2026 00:17:44 +0200 Subject: [PATCH 5/5] Apply batched suggestions from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: David Müller-Widmann --- test/api/NestedJacobianTests.jl | 2 +- test/derivatives/elementwise_utils.jl | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/test/api/NestedJacobianTests.jl b/test/api/NestedJacobianTests.jl index 536d0bdc..3153187c 100644 --- a/test/api/NestedJacobianTests.jl +++ b/test/api/NestedJacobianTests.jl @@ -1,6 +1,6 @@ using DiffTests, ForwardDiff, ReverseDiff, Test -include(joinpath(dirname(@__FILE__), "../utils.jl")) +include("../utils.jl") for f in (DiffTests.ARRAY_TO_ARRAY_FUNCS..., DiffTests.MATRIX_TO_MATRIX_FUNCS...) test_println("ARRAY_TO_ARRAY_FUNCS + MATRIX_TO_MATRIX_FUNCS", f) diff --git a/test/derivatives/elementwise_utils.jl b/test/derivatives/elementwise_utils.jl index a7ad22a4..44159a9c 100644 --- a/test/derivatives/elementwise_utils.jl +++ b/test/derivatives/elementwise_utils.jl @@ -9,7 +9,7 @@ using SpecialFunctions using Test -include(joinpath(dirname(@__FILE__), "../utils.jl")) +include("../utils.jl") x, y = rand(3, 3), rand(3, 3) a, b = rand(3), rand(3)