From b3b35af2a361566df775670b74626e5aab722187 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20M=C3=BCller-Widmann?= Date: Fri, 16 Oct 2026 13:08:38 +0200 Subject: [PATCH 1/7] Require Julia 1.10 and remove pre-1.10 code paths Raise the minimum supported Julia version to 1.10 (the current LTS) and drop the version checks that are now always true or always false: - inline the LU/Cholesky pivot aliases now that `RowMaximum`/`NoPivot` always exist - unwrap the fused 3-arg `*` rules and the `\` array workarounds - remove the pre-1.7 `logabsdet(::UmfpackLU)` backport and with it the SuiteSparse dependency - drop the corresponding version gates and `isdefined` guards in tests CI now tests `lts` instead of 1.6. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/CI.yml | 4 +- Project.toml | 6 +- src/rulesets/Base/arraymath.jl | 142 +++++++++---------- src/rulesets/Base/base.jl | 4 - src/rulesets/Base/nondiff.jl | 14 +- src/rulesets/LinearAlgebra/factorization.jl | 15 +- src/rulesets/SparseArrays/sparsematrix.jl | 51 ------- test/rulesets/Base/array.jl | 21 +-- test/rulesets/Base/arraymath.jl | 43 +++--- test/rulesets/Base/base.jl | 5 +- test/rulesets/Base/broadcast.jl | 7 +- test/rulesets/Base/indexing.jl | 31 ++-- test/rulesets/LinearAlgebra/factorization.jl | 60 ++++---- test/rulesets/LinearAlgebra/symmetric.jl | 8 +- test/rulesets/Random/random.jl | 10 +- 15 files changed, 148 insertions(+), 273 deletions(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 16b076a17..5cce86e1e 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -13,7 +13,7 @@ jobs: fail-fast: false matrix: version: - - "1.6" # LTS + - "lts" # LTS - "1" # Latest Release os: - ubuntu-latest @@ -21,7 +21,7 @@ jobs: - x86 - x64 exclude: - - version: "1.6" + - version: "lts" arch: x86 steps: - uses: actions/checkout@v6 diff --git a/Project.toml b/Project.toml index d4e54cad2..f343d4056 100644 --- a/Project.toml +++ b/Project.toml @@ -1,6 +1,6 @@ name = "ChainRules" uuid = "082447d4-558c-5d27-93f4-14fc19e9eca2" -version = "1.73.1" +version = "1.74.0" [deps] Adapt = "79e6a3ab-5dfb-504d-930d-738a2a938a0e" @@ -16,7 +16,6 @@ SparseArrays = "2f01184e-e22b-5df5-ae63-d93ebab69eaf" SparseInverseSubset = "dc90abb0-5640-4711-901d-7e5b23a2fada" Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2" StructArrays = "09ab397b-f2b6-538f-b94a-2f83cf4a842a" -SuiteSparse = "4607b0f0-06f3-5cda-b6b1-a6196a1729e9" [compat] Adapt = "3.4.0, 4" @@ -37,8 +36,7 @@ SparseInverseSubset = "0.1" StaticArrays = "1.2" Statistics = "1" StructArrays = "0.6.11, 0.7" -SuiteSparse = "1" -julia = "1.6" +julia = "1.10" [extras] ChainRulesTestUtils = "cdddcdb0-9152-4a09-a978-84456f9df70a" diff --git a/src/rulesets/Base/arraymath.jl b/src/rulesets/Base/arraymath.jl index 765b55b48..69aa220ba 100644 --- a/src/rulesets/Base/arraymath.jl +++ b/src/rulesets/Base/arraymath.jl @@ -138,73 +138,69 @@ end ##### fused 3-argument * ##### -if VERSION > v"1.7.0-" +using LinearAlgebra: mat_mat_scalar, mat_vec_scalar, StridedMaybeAdjOrTransMat - @eval using LinearAlgebra: mat_mat_scalar, mat_vec_scalar, StridedMaybeAdjOrTransMat - - function rrule( - ::typeof(mat_mat_scalar), - A::StridedMaybeAdjOrTransMat{<:CommutativeMulNumber}, - B::StridedMaybeAdjOrTransMat{<:CommutativeMulNumber}, - γ::CommutativeMulNumber - ) - project_A = ProjectTo(A) - project_B = ProjectTo(B) - project_γ = ProjectTo(γ) - C = mat_mat_scalar(A, B, γ) - function mat_mat_scalar_back(Ȳraw) - Ȳ = unthunk(Ȳraw) - Athunk = InplaceableThunk( - dA -> mul!(dA, Ȳ, B', conj(γ), true), - @thunk(project_A(mat_mat_scalar(Ȳ, B', conj(γ)))), - ) - Bthunk = InplaceableThunk( - dB -> mul!(dB, A', Ȳ, conj(γ), true), - @thunk(project_B(mat_mat_scalar(A', Ȳ, conj(γ)))), - ) - γthunk = @thunk if iszero(γ) - # Could save A*B on the forward pass, but it's messy. - # This ought to be rare, should guarantee the same type: - project_γ(dot(mat_mat_scalar(A, B, oneunit(γ)), Ȳ) / one(γ)) - else - project_γ(dot(C, Ȳ) / conj(γ)) - end - return (NoTangent(), Athunk, Bthunk, γthunk) +function rrule( + ::typeof(mat_mat_scalar), + A::StridedMaybeAdjOrTransMat{<:CommutativeMulNumber}, + B::StridedMaybeAdjOrTransMat{<:CommutativeMulNumber}, + γ::CommutativeMulNumber +) + project_A = ProjectTo(A) + project_B = ProjectTo(B) + project_γ = ProjectTo(γ) + C = mat_mat_scalar(A, B, γ) + function mat_mat_scalar_back(Ȳraw) + Ȳ = unthunk(Ȳraw) + Athunk = InplaceableThunk( + dA -> mul!(dA, Ȳ, B', conj(γ), true), + @thunk(project_A(mat_mat_scalar(Ȳ, B', conj(γ)))), + ) + Bthunk = InplaceableThunk( + dB -> mul!(dB, A', Ȳ, conj(γ), true), + @thunk(project_B(mat_mat_scalar(A', Ȳ, conj(γ)))), + ) + γthunk = @thunk if iszero(γ) + # Could save A*B on the forward pass, but it's messy. + # This ought to be rare, should guarantee the same type: + project_γ(dot(mat_mat_scalar(A, B, oneunit(γ)), Ȳ) / one(γ)) + else + project_γ(dot(C, Ȳ) / conj(γ)) end - return C, mat_mat_scalar_back + return (NoTangent(), Athunk, Bthunk, γthunk) end + return C, mat_mat_scalar_back +end - function rrule( - ::typeof(mat_vec_scalar), - A::StridedMaybeAdjOrTransMat{<:CommutativeMulNumber}, - b::StridedVector{<:CommutativeMulNumber}, - γ::CommutativeMulNumber - ) - project_A = ProjectTo(A) - project_b = ProjectTo(b) - project_γ = ProjectTo(γ) - y = mat_vec_scalar(A, b, γ) - function mat_vec_scalar_back(dy_raw) - dy = unthunk(dy_raw) - Athunk = InplaceableThunk( - dA -> mul!(dA, dy, b', conj(γ), true), - @thunk(project_A(*(dy, b', conj(γ)))), - ) - Bthunk = InplaceableThunk( - db -> mul!(db, A', dy, conj(γ), true), - @thunk(project_b(*(A', dy, conj(γ)))), - ) - γthunk = @thunk if iszero(γ) - project_γ(dot(mat_vec_scalar(A, b, oneunit(γ)), dy)) - else - project_γ(dot(y, dy) / conj(γ)) - end - return (NoTangent(), Athunk, Bthunk, γthunk) +function rrule( + ::typeof(mat_vec_scalar), + A::StridedMaybeAdjOrTransMat{<:CommutativeMulNumber}, + b::StridedVector{<:CommutativeMulNumber}, + γ::CommutativeMulNumber +) + project_A = ProjectTo(A) + project_b = ProjectTo(b) + project_γ = ProjectTo(γ) + y = mat_vec_scalar(A, b, γ) + function mat_vec_scalar_back(dy_raw) + dy = unthunk(dy_raw) + Athunk = InplaceableThunk( + dA -> mul!(dA, dy, b', conj(γ), true), + @thunk(project_A(*(dy, b', conj(γ)))), + ) + Bthunk = InplaceableThunk( + db -> mul!(db, A', dy, conj(γ), true), + @thunk(project_b(*(A', dy, conj(γ)))), + ) + γthunk = @thunk if iszero(γ) + project_γ(dot(mat_vec_scalar(A, b, oneunit(γ)), dy)) + else + project_γ(dot(y, dy) / conj(γ)) end - return y, mat_vec_scalar_back + return (NoTangent(), Athunk, Bthunk, γthunk) end - -end # VERSION + return y, mat_vec_scalar_back +end ##### ##### `muladd` @@ -353,29 +349,23 @@ function rrule(::typeof(\), A::AbstractVecOrMat{<:Real}, B::AbstractVecOrMat{<:R Ȳ = unthunk(ȳ) Ȳf = Ȳ - @static if VERSION >= v"1.9" - # Need to ensure Ȳ is an array since since https://github.com/JuliaLang/julia/pull/44358 - if !isa(Ȳ, AbstractArray) - Ȳf = [Ȳ] - end + # Need to ensure Ȳ is an array since since https://github.com/JuliaLang/julia/pull/44358 + if !isa(Ȳ, AbstractArray) + Ȳf = [Ȳ] end Yf = Y - @static if VERSION >= v"1.9" - # Need to ensure Yf is an array since since https://github.com/JuliaLang/julia/pull/44358 - if !isa(Y, AbstractArray) - Yf = [Y] - end + # Need to ensure Yf is an array since since https://github.com/JuliaLang/julia/pull/44358 + if !isa(Y, AbstractArray) + Yf = [Y] end #@info "vars" typeof(Ȳ) typeof(Y) typeof(Yf) typeof(A) typeof(B) ∂A = @thunk begin B̄ = A' \ Ȳf Ā = -B̄ * Y' t = (B - A * Y) * B̄' - @static if VERSION >= v"1.9" - # Need to ensure t is an array since since https://github.com/JuliaLang/julia/pull/44358 - if !isa(t, AbstractArray) - t = [t] - end + # Need to ensure t is an array since since https://github.com/JuliaLang/julia/pull/44358 + if !isa(t, AbstractArray) + t = [t] end Ā = add!!(Ā, t / A') Ā = add!!(Ā, A' \ Yf * (Ȳ' - B̄'A)) diff --git a/src/rulesets/Base/base.jl b/src/rulesets/Base/base.jl index 6d0f0d8b2..357a3cd5e 100644 --- a/src/rulesets/Base/base.jl +++ b/src/rulesets/Base/base.jl @@ -281,10 +281,6 @@ function rrule(config::RuleConfig{>:HasReverseMode}, ::typeof(map), f::F, xs::Tu y = map(first, hobbits) num_xs = Val(length(xs)) paddings = map(x -> ntuple(Returns(NoTangent()), (length(x) - length_y)), xs) - @static if VERSION < v"1.10.0-DEV.1194" - all(isempty, paddings) || @error """map(f, xs::Tuple...) does not allow mismatched lengths in Julia <1.10! - But its `rrule` does.""" - end function map_pullback(dy_raw) dy = unthunk(dy_raw) # We want to call the pullbacks in `rrule_via_ad` in reverse sequence to the forward pass: diff --git a/src/rulesets/Base/nondiff.jl b/src/rulesets/Base/nondiff.jl index 184413a5e..7fcf13cd5 100644 --- a/src/rulesets/Base/nondiff.jl +++ b/src/rulesets/Base/nondiff.jl @@ -367,19 +367,7 @@ @non_differentiable readuntil(::IO, ::AbstractChar) @non_differentiable readuntil(::IO, ::AbstractString) @non_differentiable realpath(::AbstractString) -if isdefined(Base, :redirect_stdio) - @non_differentiable Base.redirect_stdio(::Any) -else - @non_differentiable redirect_stderr( - ::Union{IOStream, Base.LibuvStream, Base.DevNull, IOContext}, - ) - @non_differentiable redirect_stdin( - ::Union{IOStream, Base.LibuvStream, Base.DevNull, IOContext}, - ) - @non_differentiable redirect_stdout( - ::Union{IOStream, Base.LibuvStream, Base.DevNull, IOContext}, - ) -end +@non_differentiable Base.redirect_stdio(::Any) @non_differentiable relpath(::AbstractString, ::AbstractString) @non_differentiable relpath(::String) @non_differentiable repr(::Any) diff --git a/src/rulesets/LinearAlgebra/factorization.jl b/src/rulesets/LinearAlgebra/factorization.jl index b4913447c..adad40d00 100644 --- a/src/rulesets/LinearAlgebra/factorization.jl +++ b/src/rulesets/LinearAlgebra/factorization.jl @@ -15,26 +15,23 @@ using LinearAlgebra.BLAS: gemv, gemv!, gemm!, trsm!, axpy!, ger! # for derivations for wide and tall matrices, see # https://sethaxen.com/blog/2021/02/differentiating-the-lu-decomposition/ -const LU_RowMaximum = VERSION >= v"1.7.0-DEV.1188" ? RowMaximum : Val{true} -const LU_NoPivot = VERSION >= v"1.7.0-DEV.1188" ? NoPivot : Val{false} - -const CHOLESKY_NoPivot = VERSION >= v"1.8.0-rc1" ? Union{NoPivot, Val{false}} : Val{false} +const CHOLESKY_NoPivot = Union{NoPivot, Val{false}} # For fixed pivots, the factors are differentiable iff all pivots are nonzero, except # possibly the last pivot of square or wide A. `F.info` is the index of the first zero # pivot, negated for unpivoted factorizations since JuliaLang/julia#52957 function _lu_isdifferentiable(F::LU, pivot) m, n = size(F.factors) - info = pivot isa LU_NoPivot && VERSION >= v"1.11.0-DEV.1535" ? -F.info : F.info + info = pivot isa NoPivot && VERSION >= v"1.11.0-DEV.1535" ? -F.info : F.info return iszero(info) || (m <= n && info == m) end function frule( - (_, Ȧ), ::typeof(lu!), A::StridedMatrix, pivot::Union{LU_RowMaximum,LU_NoPivot}; kwargs... + (_, Ȧ), ::typeof(lu!), A::StridedMatrix, pivot::Union{RowMaximum,NoPivot}; kwargs... ) ΔA = unthunk(Ȧ) F = lu!(A, pivot; kwargs...) - ∂factors = pivot isa LU_RowMaximum ? ΔA[F.p, :] : ΔA + ∂factors = pivot isa RowMaximum ? ΔA[F.p, :] : ΔA m, n = size(∂factors) q = min(m, n) if !_lu_isdifferentiable(F, pivot) @@ -126,14 +123,14 @@ function _lu_pullback(ΔF::Tangent, m, n, eltypeA, pivot, F) ldiv!(L1', ∂A1) rdiv!(∂A, U') end - if pivot isa LU_RowMaximum + if pivot isa RowMaximum ∂A = ∂A[invperm(F.p), :] end return NoTangent(), ∂A, NoTangent() end _lu_pullback(ΔF::AbstractThunk, m, n, eltypeA, pivot, F) = _lu_pullback(unthunk(ΔF), m, n, eltypeA, pivot, F) function rrule( - ::typeof(lu), A::StridedMatrix, pivot::Union{LU_RowMaximum,LU_NoPivot}; kwargs... + ::typeof(lu), A::StridedMatrix, pivot::Union{RowMaximum,NoPivot}; kwargs... ) m, n = size(A) F = lu(A, pivot; kwargs...) diff --git a/src/rulesets/SparseArrays/sparsematrix.jl b/src/rulesets/SparseArrays/sparsematrix.jl index 12ae76f80..bba6040ed 100644 --- a/src/rulesets/SparseArrays/sparsematrix.jl +++ b/src/rulesets/SparseArrays/sparsematrix.jl @@ -53,57 +53,6 @@ end if Base.USE_GPL_LIBS # Don't define rrules for sparse determinants if we don't have CHOLMOD from SuiteSparse.jl using SparseInverseSubset - if VERSION < v"1.7" - #= - The method below for `logabsdet(F::UmfpackLU)` is required to calculate the (log) - determinants of sparse matrices, but was not defined prior to Julia v1.7. In order - for the rrules for the determinants of sparse matrices below to work, they need to be - able to compute the primals as well, so this import from the future is included. For - more recent versions of Julia, this definition lives in: - julia/stdlib/SuiteSparse/src/umfpack.jl - =# - using SuiteSparse.UMFPACK: UmfpackLU - - # compute the sign/parity of a permutation - function _signperm(p) - n = length(p) - result = 0 - todo = trues(n) - while any(todo) - k = findfirst(todo) - todo[k] = false - result += 1 # increment element count - j = p[k] - while j != k - result += 1 # increment element count - todo[j] = false - j = p[j] - end - result += 1 # increment cycle count - end - return ifelse(isodd(result), -1, 1) - end - - function LinearAlgebra.logabsdet(F::UmfpackLU{T, TI}) where {T<:Union{Float64,ComplexF64},TI<:Union{Int32, Int64}} - n = checksquare(F) - issuccess(F) || return log(zero(real(T))), zero(T) - U = F.U - Rs = F.Rs - p = F.p - q = F.q - s = _signperm(p)*_signperm(q)*one(real(T)) - P = one(T) - abs_det = zero(real(T)) - @inbounds for i in 1:n - dg_ii = U[i, i] / Rs[i] - P *= sign(dg_ii) - abs_det += log(abs(dg_ii)) - end - return abs_det, s * P - end - end - - function rrule(::typeof(logabsdet), x::SparseMatrixCSC) F = cholesky(x) L, D, U, P = SparseInverseSubset.get_ldup(F) diff --git a/test/rulesets/Base/array.jl b/test/rulesets/Base/array.jl index 5120b2677..dc6ef1355 100644 --- a/test/rulesets/Base/array.jl +++ b/test/rulesets/Base/array.jl @@ -140,11 +140,10 @@ end @gpu test_rrule(repeat, rand(4, ), 2) @gpu test_rrule(repeat, rand(4, 5), 2) @gpu test_rrule(repeat, rand(4, 5), 2, 3) - test_rrule(repeat, rand(1,2,3), 2,3,4; check_inferred=VERSION>v"1.6") - test_rrule(repeat, rand(0,2,3), 2,0,4; check_inferred=VERSION>v"1.6") - test_rrule(repeat, rand(1,1,1,1), 2,3,4,5; check_inferred=VERSION>v"1.6") - - # These need Julia 1.6 + test_rrule(repeat, rand(1,2,3), 2,3,4) + test_rrule(repeat, rand(0,2,3), 2,0,4) + test_rrule(repeat, rand(1,1,1,1), 2,3,4,5) + test_rrule(repeat, rand(4, 5); fkwargs = (inner=(2,4), outer=(1,1,1,3))) test_rrule(repeat, rand(1,2,3), 2,3) test_rrule(repeat, rand(1,2,3), 2,3,4,2) @@ -245,9 +244,7 @@ end test_rrule(cat, rand(), rand(2, 3); fkwargs=(dims=[1,2],)) test_rrule(cat, rand(1), rand(3, 2, 1); fkwargs=(dims=(1,2),), check_inferred=false) # infers Tuple{Zero, Vector{Float64}, Any} - if VERSION ≥ v"1.8" # Val(tuple) dims support was added in v1.8 - test_rrule(cat, randn(3,2,4), randn(3,2,4); fkwargs=(dims=Val((1,2)),)) #678 - end + test_rrule(cat, randn(3,2,4), randn(3,2,4); fkwargs=(dims=Val((1,2)),)) #678 test_rrule(cat, rand(2, 2), rand(2, 2)'; fkwargs=(dims=1,)) # inference on exotic array types @@ -456,18 +453,14 @@ end # multidimensional container ms = [rand(2,3) for _ in 1:4, _ in 1:5]; - if VERSION > v"1.9-" # this needs new eachslice, not yet in Compat - test_rrule(stack, ms, check_inferred=false) - end + test_rrule(stack, ms, check_inferred=false) test_rrule(stack, ms, fkwargs=(dims=1,), check_inferred=false) test_rrule(stack, ms, fkwargs=(dims=3,), check_inferred=false) # non-array inner objects ts = [Tuple(rand(3)) for _ in 1:4, _ in 1:2]; - if VERSION > v"1.9-" - test_rrule(stack, ts, check_inferred=false) - end + test_rrule(stack, ts, check_inferred=false) test_rrule(stack, ts, fkwargs=(dims=1,), check_inferred=false) test_rrule(stack, ts, fkwargs=(dims=2,), check_inferred=false) end diff --git a/test/rulesets/Base/arraymath.jl b/test/rulesets/Base/arraymath.jl index 8b8104f2b..e2ac73e49 100644 --- a/test/rulesets/Base/arraymath.jl +++ b/test/rulesets/Base/arraymath.jl @@ -1,13 +1,8 @@ @testset "arraymath.jl" begin @testset "inv(::Matrix{$T})" for T in (Float64, ComplexF64) B = generate_well_conditioned_matrix(T, 3) - if v"1.7" <= VERSION < v"1.9" - @gpu test_frule(inv, B) - @gpu test_rrule(inv, B) - else - @gpu_broken test_frule(inv, B) - @gpu_broken test_rrule(inv, B) - end + @gpu_broken test_frule(inv, B) + @gpu_broken test_rrule(inv, B) end @testset "*: $T" for T in (Float64, ComplexF64) @@ -133,28 +128,26 @@ end end - if VERSION > v"1.7.0-" - mat_mat_scalar = LinearAlgebra.mat_mat_scalar - mat_vec_scalar = LinearAlgebra.mat_vec_scalar + mat_mat_scalar = LinearAlgebra.mat_mat_scalar + mat_vec_scalar = LinearAlgebra.mat_vec_scalar - @testset "3-arg *, $T" for T in [Float64, ComplexF64] + @testset "3-arg *, $T" for T in [Float64, ComplexF64] - test_rrule(mat_mat_scalar, rand(T,4,4), rand(T,4,4), rand(T)) - test_rrule(mat_mat_scalar, rand(T,4,4), rand(T,4,4), 0.0) - test_rrule(mat_mat_scalar, rand(T,4,4)' ⊢ rand(T,4,4), rand(T,4,4), rand(T)) + test_rrule(mat_mat_scalar, rand(T,4,4), rand(T,4,4), rand(T)) + test_rrule(mat_mat_scalar, rand(T,4,4), rand(T,4,4), 0.0) + test_rrule(mat_mat_scalar, rand(T,4,4)' ⊢ rand(T,4,4), rand(T,4,4), rand(T)) - test_rrule(mat_vec_scalar, rand(T,4,4), rand(T,4), rand(T)) - test_rrule(mat_vec_scalar, rand(T,4,4), rand(T,4), 0.0) + test_rrule(mat_vec_scalar, rand(T,4,4), rand(T,4), rand(T)) + test_rrule(mat_vec_scalar, rand(T,4,4), rand(T,4), 0.0) - T == ComplexF64 && continue - # Test with γ of a wider type - A, B, b, γ = rand(3,3), rand(3,3), rand(3), rand() - dZ, dz = rand(3,3), rand(3) - unthunk(rrule(mat_mat_scalar, A, B, γ + 0im)[2](dZ)[4]) ≈ unthunk(rrule(mat_mat_scalar, A, B, γ)[2](dZ)[4]) - unthunk(rrule(mat_mat_scalar, A, B, 0 + 0im)[2](dZ)[4]) ≈ unthunk(rrule(mat_mat_scalar, A, B, 0)[2](dZ)[4]) - unthunk(rrule(mat_vec_scalar, A, b, γ + 0im)[2](dz)[4]) ≈ unthunk(rrule(mat_vec_scalar, A, b, γ)[2](dz)[4]) - end - end # VERSION + T == ComplexF64 && continue + # Test with γ of a wider type + A, B, b, γ = rand(3,3), rand(3,3), rand(3), rand() + dZ, dz = rand(3,3), rand(3) + unthunk(rrule(mat_mat_scalar, A, B, γ + 0im)[2](dZ)[4]) ≈ unthunk(rrule(mat_mat_scalar, A, B, γ)[2](dZ)[4]) + unthunk(rrule(mat_mat_scalar, A, B, 0 + 0im)[2](dZ)[4]) ≈ unthunk(rrule(mat_mat_scalar, A, B, 0)[2](dZ)[4]) + unthunk(rrule(mat_vec_scalar, A, b, γ + 0im)[2](dz)[4]) ≈ unthunk(rrule(mat_vec_scalar, A, b, γ)[2](dz)[4]) + end @testset "$f" for f in (/, \) @testset "Matrix" begin diff --git a/test/rulesets/Base/base.jl b/test/rulesets/Base/base.jl index a71e7f603..92a9cbdc6 100644 --- a/test/rulesets/Base/base.jl +++ b/test/rulesets/Base/base.jl @@ -265,10 +265,7 @@ end test_rrule(map, make_two_vec, (4.0, 5.0 + 6im), check_inferred=false) test_rrule(map, Multiplier(rand() + im), Tuple(rand(3)), check_inferred=false) - if VERSION >= v"1.10.0-DEV.1194" - # Mismatched lengths were not allowed before 1.10 - test_rrule(map, Multiplier(4.5), (6.7, 8.9), (0.1, 0.2, 0.3), check_inferred=false) - end + test_rrule(map, Multiplier(4.5), (6.7, 8.9), (0.1, 0.2, 0.3), check_inferred=false) end @testset "merge NamedTuple" begin diff --git a/test/rulesets/Base/broadcast.jl b/test/rulesets/Base/broadcast.jl index f486852a1..7cc6dc4a2 100644 --- a/test/rulesets/Base/broadcast.jl +++ b/test/rulesets/Base/broadcast.jl @@ -1,10 +1,7 @@ using Base.Broadcast: broadcasted -if VERSION < v"1.7" - Base.ndims(::Type{<:AbstractArray{<:Any,N}}) where {N} = N -end BS0 = Broadcast.BroadcastStyle(Float64) -BS1 = Broadcast.BroadcastStyle(Vector) # without ndims method, error on 1.6 +BS1 = Broadcast.BroadcastStyle(Vector) BS2 = Broadcast.BroadcastStyle(Matrix) BT1 = Broadcast.BroadcastStyle(Tuple) @@ -32,7 +29,7 @@ BT1 = Broadcast.BroadcastStyle(Tuple) test_rrule(copy∘broadcasted, BS2, atan, rand(3), rand(4)') test_rrule(copy∘broadcasted, BS1, atan, rand(3), rand()) test_rrule(copy∘broadcasted, BT1, atan, rand(3), Tuple(rand(1))) - test_rrule(copy∘broadcasted, BT1, atan, Tuple(rand(3)), Tuple(rand(3)), check_inferred = VERSION > v"1.7") + test_rrule(copy∘broadcasted, BT1, atan, Tuple(rand(3)), Tuple(rand(3))) # test_rrule(copy∘broadcasted, *, BS1, rand(3), Ref(rand())) # don't know what I was testing end diff --git a/test/rulesets/Base/indexing.jl b/test/rulesets/Base/indexing.jl index e0e17237c..68a09a500 100644 --- a/test/rulesets/Base/indexing.jl +++ b/test/rulesets/Base/indexing.jl @@ -217,25 +217,21 @@ end # DimensionMismatch("second dimension of A, 6, does not match length of x, 5") # Probably similar to https://github.com/JuliaDiff/ChainRulesTestUtils.jl/issues/234 (about Broadcasted not Generator) - # Inference on 1.6 sometimes fails, so don't enforce there. - test_rrule(collect ∘ eachrow, rand(5); check_inferred=(VERSION >= v"1.7")) - test_rrule(collect ∘ eachrow, rand(3, 4); check_inferred=(VERSION >= v"1.7")) + test_rrule(collect ∘ eachrow, rand(5)) + test_rrule(collect ∘ eachrow, rand(3, 4)) - test_rrule(collect ∘ eachcol, rand(3, 4); check_inferred=(VERSION >= v"1.7")) + test_rrule(collect ∘ eachcol, rand(3, 4)) @test_skip test_rrule(collect ∘ eachcol, Diagonal(rand(5))) # works locally! - if VERSION >= v"1.7" - # On 1.6, ComposedFunction doesn't take keywords. Only affects this testing strategy, not real use. - test_rrule(collect ∘ eachslice, rand(3, 4, 5); fkwargs=(; dims=3)) - test_rrule(collect ∘ eachslice, rand(3, 4, 5); fkwargs=(; dims=(2,))) - - test_rrule( - collect ∘ eachslice, - FooTwoField.(rand(3, 4, 5), rand(3, 4, 5)); - check_inferred=false, - fkwargs=(; dims=3), - ) - end + test_rrule(collect ∘ eachslice, rand(3, 4, 5); fkwargs=(; dims=3)) + test_rrule(collect ∘ eachslice, rand(3, 4, 5); fkwargs=(; dims=(2,))) + + test_rrule( + collect ∘ eachslice, + FooTwoField.(rand(3, 4, 5), rand(3, 4, 5)); + check_inferred=false, + fkwargs=(; dims=3), + ) # Make sure pulling back an array that mixes some AbstractZeros in works right _, back = rrule(eachcol, rand(3, 4)) @@ -258,8 +254,7 @@ end ChainRules.∇eachslice, [rand(2, 3) for _ in 1:4], rand(2, 3, 4), - Val(3); - check_inferred=(VERSION >= v"1.7"), + Val(3), ) # eachslice: Make sure pulling back an array of thunks unthunks them and does not return all zeros. diff --git a/test/rulesets/LinearAlgebra/factorization.jl b/test/rulesets/LinearAlgebra/factorization.jl index a108dd919..88ad50496 100644 --- a/test/rulesets/LinearAlgebra/factorization.jl +++ b/test/rulesets/LinearAlgebra/factorization.jl @@ -15,16 +15,6 @@ function FiniteDifferences.to_vec(C::Cholesky) return C_vec, cholesky_from_vec end -function FiniteDifferences.to_vec(x::Val) - Val_from_vec(v) = x - return Bool[], Val_from_vec -end - -const LU_ROW_MAXIMUM = VERSION >= v"1.7.0-DEV.1188" ? RowMaximum() : Val(true) -const LU_NO_PIVOT = VERSION >= v"1.7.0-DEV.1188" ? NoPivot() : Val(false) - -const CHOLESKY_NO_PIVOT = VERSION >= v"1.8.0-rc1" ? NoPivot() : Val(false) - # well-conditioned random n×n matrix with elements of type `T` for testing `eigen` function rand_eigen(T::Type, n::Int) # uniform distribution over `(-1, 1)` / `(-1, 1)^2` @@ -63,13 +53,13 @@ end @testset "lu! frule" begin @testset "lu!(A::Matrix{$T}, $pivot) for size(A)=($m, $n)" for T in (Float64, ComplexF64), - pivot in (LU_ROW_MAXIMUM, LU_NO_PIVOT), + pivot in (RowMaximum(), NoPivot()), m in (7, 10, 13) test_frule(lu!, randn(T, m, n), pivot ⊢ NoTangent()) end @testset "zero last pivot: lu!(A, $pivot) for size(A)=($m, $n)" for pivot in ( - LU_ROW_MAXIMUM, LU_NO_PIVOT + RowMaximum(), NoPivot() ), m in (7, 10, 13) @@ -87,10 +77,10 @@ end Asingular = zeros(n, n) ΔAsingular = rand_tangent(Asingular) @test_throws SingularException frule( - (ZeroTangent(), copy(ΔAsingular)), lu!, copy(Asingular), LU_ROW_MAXIMUM + (ZeroTangent(), copy(ΔAsingular)), lu!, copy(Asingular), RowMaximum() ) _, ∂F = frule( - (ZeroTangent(), ΔAsingular), lu!, Asingular, LU_ROW_MAXIMUM; check=false + (ZeroTangent(), ΔAsingular), lu!, Asingular, RowMaximum(); check=false ) @test all(isnan, ∂F.factors) # not differentiable end @@ -98,13 +88,13 @@ end @testset "lu rrule" begin @testset "lu(A::Matrix{$T}, $pivot) for size(A)=($m, $n)" for T in (Float64, ComplexF64), - pivot in (LU_ROW_MAXIMUM, LU_NO_PIVOT), + pivot in (RowMaximum(), NoPivot()), m in (7, 10, 13) test_rrule(lu, randn(T, m, n), pivot ⊢ NoTangent()) end @testset "zero last pivot: lu(A, $pivot) for size(A)=($m, $n)" for pivot in ( - LU_ROW_MAXIMUM, LU_NO_PIVOT + RowMaximum(), NoPivot() ), m in (7, 10, 13) @@ -120,10 +110,10 @@ end end @testset "check=false passed to primal function" begin Asingular = zeros(n, n) - F = lu(Asingular, LU_ROW_MAXIMUM; check=false) + F = lu(Asingular, RowMaximum(); check=false) ΔF = Tangent{typeof(F)}(; factors=rand_tangent(F.factors)) - @test_throws SingularException rrule(lu, Asingular, LU_ROW_MAXIMUM) - _, back = rrule(lu, Asingular, LU_ROW_MAXIMUM; check=false) + @test_throws SingularException rrule(lu, Asingular, RowMaximum()) + _, back = rrule(lu, Asingular, RowMaximum(); check=false) _, ∂A, _ = back(ΔF) @test all(isnan, ∂A) # not differentiable end @@ -141,8 +131,8 @@ end end @testset "matrix inverse using LU" begin @testset "inv!(lu(::LU{$T,<:StridedMatrix}))" for T in (Float64,ComplexF64) - test_frule(LinearAlgebra.inv!, lu(randn(T, n, n), LU_ROW_MAXIMUM)) - test_rrule(inv, lu(randn(T, n, n), LU_ROW_MAXIMUM)) + test_frule(LinearAlgebra.inv!, lu(randn(T, n, n), RowMaximum())) + test_rrule(inv, lu(randn(T, n, n), RowMaximum())) end end end @@ -482,7 +472,7 @@ end @testset "Diagonal" begin @testset "Diagonal{<:Real}" begin - test_rrule(cholesky, Diagonal([0.3, 0.2, 0.5, 0.6, 0.9]), CHOLESKY_NO_PIVOT) + test_rrule(cholesky, Diagonal([0.3, 0.2, 0.5, 0.6, 0.9]), NoPivot()) end @testset "Diagonal{<:Complex}" begin # finite differences in general will produce matrices with non-real @@ -491,18 +481,18 @@ end D = Diagonal([0.3 + 0im, 0.2, 0.5, 0.6, 0.9]) C = cholesky(D) test_rrule( - cholesky, D, CHOLESKY_NO_PIVOT; + cholesky, D, NoPivot(); output_tangent=Tangent{typeof(C)}(factors=complex(randn(5, 5))), fkwargs=(; check=false), ) end @testset "check has correct default and passed to primal" begin - @test_throws Exception rrule(cholesky, Diagonal(-rand(5)), CHOLESKY_NO_PIVOT) - rrule(cholesky, Diagonal(-rand(5)), CHOLESKY_NO_PIVOT; check=false) + @test_throws Exception rrule(cholesky, Diagonal(-rand(5)), NoPivot()) + rrule(cholesky, Diagonal(-rand(5)), NoPivot(); check=false) end @testset "failed factorization" begin A = Diagonal(vcat(rand(4), -rand(4), rand(4))) - test_rrule(cholesky, A, CHOLESKY_NO_PIVOT; fkwargs=(; check=false)) + test_rrule(cholesky, A, NoPivot(); fkwargs=(; check=false)) end end @@ -510,7 +500,7 @@ end @testset "Matrix{$T}" for T in (Float64, ComplexF64) X = generate_well_conditioned_matrix(T, 10) V = generate_well_conditioned_matrix(T, 10) - F, dX_pullback = rrule(cholesky, X, CHOLESKY_NO_PIVOT) + F, dX_pullback = rrule(cholesky, X, NoPivot()) @testset "uplo=$p, cotangent eltype=$T" for p in [:U, :L], S in unique([T, complex(T)]) Y, dF_pullback = rrule(getproperty, F, p) Ȳ = randn(S, size(Y)) @@ -535,8 +525,8 @@ end @testset "check has correct default and passed to primal" begin # this will almost certainly be a non-PD matrix X = Matrix(Symmetric(randn(10, 10))) - @test_throws Exception rrule(cholesky, X, CHOLESKY_NO_PIVOT) - rrule(cholesky, X, CHOLESKY_NO_PIVOT; check=false) # just check it doesn't throw + @test_throws Exception rrule(cholesky, X, NoPivot()) + rrule(cholesky, X, NoPivot(); check=false) # just check it doesn't throw end end @@ -544,13 +534,13 @@ end # (cholesky ∘ Symmetric)(::StridedMatrix) are equal. @testset "Symmetric" begin X = generate_well_conditioned_matrix(10) - F, dX_pullback = rrule(cholesky, X, CHOLESKY_NO_PIVOT) + F, dX_pullback = rrule(cholesky, X, NoPivot()) ΔU = randn(size(X)) ΔF = Tangent{typeof(F)}(; factors=ΔU) @testset for uplo in (:L, :U) X_symmetric, sym_back = rrule(Symmetric, X, uplo) - C, chol_back_sym = rrule(cholesky, X_symmetric, CHOLESKY_NO_PIVOT) + C, chol_back_sym = rrule(cholesky, X_symmetric, NoPivot()) ΔC = Tangent{typeof(C)}(; factors=(uplo === :U ? ΔU : ΔU')) ΔX_symmetric = chol_back_sym(ΔC)[2] @@ -567,13 +557,13 @@ end @testset "Hermitian" begin @testset "Hermitian{$T}" for T in (Float64, ComplexF64) X = generate_well_conditioned_matrix(T, 10) - F, dX_pullback = rrule(cholesky, X, CHOLESKY_NO_PIVOT) + F, dX_pullback = rrule(cholesky, X, NoPivot()) ΔU = randn(T, size(X)) ΔF = Tangent{typeof(F)}(; factors=ΔU) @testset for uplo in (:L, :U) X_hermitian, herm_back = rrule(Hermitian, X, uplo) - C, chol_back_herm = rrule(cholesky, X_hermitian, CHOLESKY_NO_PIVOT) + C, chol_back_herm = rrule(cholesky, X_hermitian, NoPivot()) ΔC = Tangent{typeof(C)}(; factors=(uplo === :U ? ΔU : ΔU')) ΔX_hermitian = chol_back_herm(ΔC)[2] @@ -587,8 +577,8 @@ end @testset "check has correct default and passed to primal" begin # this will almost certainly be a non-PD matrix X = Hermitian(randn(10, 10)) - @test_throws Exception rrule(cholesky, X, CHOLESKY_NO_PIVOT) - rrule(cholesky, X, CHOLESKY_NO_PIVOT; check=false) + @test_throws Exception rrule(cholesky, X, NoPivot()) + rrule(cholesky, X, NoPivot(); check=false) end end diff --git a/test/rulesets/LinearAlgebra/symmetric.jl b/test/rulesets/LinearAlgebra/symmetric.jl index d824556b6..240013996 100644 --- a/test/rulesets/LinearAlgebra/symmetric.jl +++ b/test/rulesets/LinearAlgebra/symmetric.jl @@ -8,10 +8,6 @@ test_frule(SymHerm, rand(T, 3, 3), uplo) end @testset "rrule" begin - # on old versions of julia this combination doesn't infer but we don't care as - # it infers fine on modern versions. - check_inferred = !(VERSION < v"1.5" && T <: ComplexF64 && SymHerm <: Hermitian) - @testset "back(::$MT)" for MT in (Matrix, LowerTriangular, UpperTriangular) x = randn(T, 3, 3) ΔΩ = MT(randn(T, 3, 3)) @@ -22,7 +18,7 @@ # so we need to test this more carefully below check_inferred=false, ) - if check_inferred && false # ChainRulesCore #407 + if false # ChainRulesCore #407 @maybe_inferred (function (SymHerm, x, ΔΩ, ::Val) return rrule(SymHerm, x, uplo)[2](ΔΩ) end)(SymHerm, x, ΔΩ, Val(uplo)) @@ -36,7 +32,7 @@ check_inferred=false, output_tangent = ΔΩ, ) - if check_inferred && false # ChainRulesCore #407 + if false # ChainRulesCore #407 @maybe_inferred (function (SymHerm, x, ΔΩ, ::Val) return rrule(SymHerm, x, uplo)[2](ΔΩ) end)(SymHerm, x, ΔΩ, Val(uplo)) diff --git a/test/rulesets/Random/random.jl b/test/rulesets/Random/random.jl index 4f1ccd3a8..00326819f 100644 --- a/test/rulesets/Random/random.jl +++ b/test/rulesets/Random/random.jl @@ -6,8 +6,7 @@ end Random.rand(d::NormalDistribution) = d.μ + d.σ*randn() @testset "Random" begin - rng_types = [MersenneTwister] - isdefined(Random, :Xoshiro) && push!(rng_types, getfield(Random, :Xoshiro)) + rng_types = [MersenneTwister, Xoshiro] @testset "$Rng" for Rng in rng_types @testset "no args" begin @@ -40,11 +39,8 @@ Random.rand(d::NormalDistribution) = d.μ + d.σ*randn() ((Float32,(2,2)), Matrix{<:Float32}), ((2,2), Matrix{<:Float64}), ] - if isdefined(Random, :Xoshiro) - Xoshiro = getfield(Random, :Xoshiro) - push!(non_differentiables, ((Xoshiro(123),), Float64)) - push!(non_differentiables, ((Xoshiro(123),2,2), Matrix{<:Float64})) - end + push!(non_differentiables, ((Xoshiro(123),), Float64)) + push!(non_differentiables, ((Xoshiro(123),2,2), Matrix{<:Float64})) for (args, xType) in non_differentiables x, dΩ = frule((ZeroTangent(), randn(args...)), rand, args...) From 975e3fa327ae8de91382498b7580b55004bb8302 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20M=C3=BCller-Widmann?= Date: Fri, 16 Oct 2026 14:17:11 +0200 Subject: [PATCH 2/7] Run tests with ParallelTestRunner instead of JuliaInterpreter Replace the JuliaInterpreter-based test setup, which interpreted every top-level `@testset` via an `include` mapexpr hack and a hand-maintained `compiled_modules` list, with plain compiled tests orchestrated by ParallelTestRunner.jl. Each test file now runs in its own sandbox module on a pool of worker processes. - Every test file loads the packages it uses itself. - The shared helpers (`Multiplier`, `@gpu`, ...) move into a `ChainRulesTestHelpers` module in `test/testutils.jl`, which is loaded once per worker together with the `Adapt` piracy for the mock GPU array. - `test_method_tables()` moves into `test_helpers.jl`. - `rulesets/Core/core.jl` is now discovered and run (it was never included before); its scalar `ifelse` rrule test does not infer because the pullback's return type depends on the condition's value. Tests are now selected by path prefix, e.g. `Pkg.test("ChainRules"; test_args=["rulesets/LinearAlgebra"])`. Co-Authored-By: Claude Opus 5.5 --- Project.toml | 6 +- test/rulesets/Base/CoreLogging.jl | 2 + test/rulesets/Base/array.jl | 2 + test/rulesets/Base/arraymath.jl | 2 + test/rulesets/Base/base.jl | 2 + test/rulesets/Base/broadcast.jl | 1 + test/rulesets/Base/evalpoly.jl | 1 + test/rulesets/Base/fastmath_able.jl | 19 +- test/rulesets/Base/indexing.jl | 2 + test/rulesets/Base/mapreduce.jl | 2 + test/rulesets/Base/sort.jl | 2 + test/rulesets/Core/core.jl | 5 +- test/rulesets/LinearAlgebra/blas.jl | 3 + test/rulesets/LinearAlgebra/dense.jl | 2 + test/rulesets/LinearAlgebra/factorization.jl | 3 + test/rulesets/LinearAlgebra/lapack.jl | 2 + test/rulesets/LinearAlgebra/matfun.jl | 2 + test/rulesets/LinearAlgebra/norm.jl | 2 + test/rulesets/LinearAlgebra/structured.jl | 3 + test/rulesets/LinearAlgebra/symmetric.jl | 3 + test/rulesets/LinearAlgebra/uniformscaling.jl | 2 + test/rulesets/Random/random.jl | 2 + test/rulesets/SparseArrays/sparsematrix.jl | 2 + test/rulesets/Statistics/statistics.jl | 2 + test/runtests.jl | 105 ++------- test/test_helpers.jl | 214 +---------------- test/testutils.jl | 222 ++++++++++++++++++ test/unzipped.jl | 2 +- 28 files changed, 302 insertions(+), 315 deletions(-) create mode 100644 test/testutils.jl diff --git a/Project.toml b/Project.toml index f343d4056..2444ff59a 100644 --- a/Project.toml +++ b/Project.toml @@ -27,8 +27,8 @@ FiniteDifferences = "0.12.20" GPUArraysCore = "0.1.0, 0.2" IrrationalConstants = "0.1.1, 0.2" JLArrays = "0.1, 0.4" -JuliaInterpreter = "0.9, 0.12" LinearAlgebra = "1" +ParallelTestRunner = "2" Random = "1" RealDot = "0.1" SparseArrays = "1" @@ -42,10 +42,10 @@ julia = "1.10" ChainRulesTestUtils = "cdddcdb0-9152-4a09-a978-84456f9df70a" FiniteDifferences = "26cc04aa-876d-5657-8c51-4c34ba976000" JLArrays = "27aeb0d3-9eb9-45fb-866b-73c2ecf80fcb" -JuliaInterpreter = "aa1ae85d-cabe-5617-a682-6adf51b2e16a" +ParallelTestRunner = "d3525ed8-44d0-4b2c-a655-542cee43accc" Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" StaticArrays = "90137ffa-7385-5640-81b9-e52037218182" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" [targets] -test = ["ChainRulesTestUtils", "FiniteDifferences", "JLArrays", "JuliaInterpreter", "Random", "StaticArrays", "Test"] +test = ["ChainRulesTestUtils", "FiniteDifferences", "JLArrays", "ParallelTestRunner", "Random", "StaticArrays", "Test"] diff --git a/test/rulesets/Base/CoreLogging.jl b/test/rulesets/Base/CoreLogging.jl index 28c0b74a8..5f97502e6 100644 --- a/test/rulesets/Base/CoreLogging.jl +++ b/test/rulesets/Base/CoreLogging.jl @@ -1,3 +1,5 @@ +using ChainRules, ChainRulesTestUtils, Test + # For the CoreLogging submodule of Base. (not to be confused with the Logging stdlib) @testset "CoreLogging.jl" begin @testset "with_logger" begin diff --git a/test/rulesets/Base/array.jl b/test/rulesets/Base/array.jl index dc6ef1355..5744bf169 100644 --- a/test/rulesets/Base/array.jl +++ b/test/rulesets/Base/array.jl @@ -1,3 +1,5 @@ +using ChainRules, ChainRulesCore, ChainRulesTestUtils, FiniteDifferences, JLArrays, LinearAlgebra, SparseArrays, StaticArrays, Test + @testset "Array constructors" begin @testset "undef" begin # We can't use test_rrule here (as it's currently implemented) because the elements of diff --git a/test/rulesets/Base/arraymath.jl b/test/rulesets/Base/arraymath.jl index e2ac73e49..978e1964f 100644 --- a/test/rulesets/Base/arraymath.jl +++ b/test/rulesets/Base/arraymath.jl @@ -1,3 +1,5 @@ +using ChainRules, ChainRulesCore, ChainRulesTestUtils, LinearAlgebra, Test + @testset "arraymath.jl" begin @testset "inv(::Matrix{$T})" for T in (Float64, ComplexF64) B = generate_well_conditioned_matrix(T, 3) diff --git a/test/rulesets/Base/base.jl b/test/rulesets/Base/base.jl index 92a9cbdc6..98cfed334 100644 --- a/test/rulesets/Base/base.jl +++ b/test/rulesets/Base/base.jl @@ -1,3 +1,5 @@ +using ChainRules, ChainRulesCore, ChainRulesTestUtils, FiniteDifferences, Test + mutable struct MDemo x::Float64 end diff --git a/test/rulesets/Base/broadcast.jl b/test/rulesets/Base/broadcast.jl index 7cc6dc4a2..ea9bac8d0 100644 --- a/test/rulesets/Base/broadcast.jl +++ b/test/rulesets/Base/broadcast.jl @@ -1,3 +1,4 @@ +using ChainRules, ChainRulesCore, ChainRulesTestUtils, Random, Test using Base.Broadcast: broadcasted BS0 = Broadcast.BroadcastStyle(Float64) diff --git a/test/rulesets/Base/evalpoly.jl b/test/rulesets/Base/evalpoly.jl index 25f0202ed..ea9ce6d44 100644 --- a/test/rulesets/Base/evalpoly.jl +++ b/test/rulesets/Base/evalpoly.jl @@ -1,3 +1,4 @@ +using ChainRules, ChainRulesTestUtils, Test @testset "evalpoly" begin # test fallbacks for when code generation fails diff --git a/test/rulesets/Base/fastmath_able.jl b/test/rulesets/Base/fastmath_able.jl index 45ad33cc3..9d7851e23 100644 --- a/test/rulesets/Base/fastmath_able.jl +++ b/test/rulesets/Base/fastmath_able.jl @@ -1,3 +1,5 @@ +using ChainRules, ChainRulesCore, ChainRulesTestUtils, FiniteDifferences, Test + # Add tests to the quote for functions with FastMath variants. function jacobian_via_frule(f,z) du_dx, dv_dx = reim(frule((ZeroTangent(), 1),f,z)[2]) @@ -286,13 +288,10 @@ const FASTABLE_AST = quote end # Now we generate tests for fast and nonfast versions -@eval @interpret (function() - @testset "fastmath_able Base functions" begin - $FASTABLE_AST - end - - - @testset "fastmath_able FastMath functions" begin - $(Base.FastMath.make_fastmath(FASTABLE_AST)) - end -end)() +@eval @testset "fastmath_able Base functions" begin + $FASTABLE_AST +end + +@eval @testset "fastmath_able FastMath functions" begin + $(Base.FastMath.make_fastmath(FASTABLE_AST)) +end diff --git a/test/rulesets/Base/indexing.jl b/test/rulesets/Base/indexing.jl index 68a09a500..d9a67d444 100644 --- a/test/rulesets/Base/indexing.jl +++ b/test/rulesets/Base/indexing.jl @@ -1,3 +1,5 @@ +using ChainRules, ChainRulesCore, ChainRulesTestUtils, GPUArraysCore, JLArrays, LinearAlgebra, Test + struct FooTwoField x::Float64 y::Float64 diff --git a/test/rulesets/Base/mapreduce.jl b/test/rulesets/Base/mapreduce.jl index 89f41c933..e2f71225d 100644 --- a/test/rulesets/Base/mapreduce.jl +++ b/test/rulesets/Base/mapreduce.jl @@ -1,3 +1,5 @@ +using ChainRules, ChainRulesCore, ChainRulesTestUtils, LinearAlgebra, StaticArrays, Test + # for sum(xs, weights) (#522) Base.sum(xs::AbstractArray, weights::AbstractArray) = dot(xs, weights) struct SumRuleConfig <: RuleConfig{Union{HasReverseMode}} end diff --git a/test/rulesets/Base/sort.jl b/test/rulesets/Base/sort.jl index 052045d1e..d571a0256 100644 --- a/test/rulesets/Base/sort.jl +++ b/test/rulesets/Base/sort.jl @@ -1,3 +1,5 @@ +using ChainRules, ChainRulesCore, ChainRulesTestUtils, LinearAlgebra, Test + @testset "sort.jl" begin @testset "sort" begin a = rand(10) diff --git a/test/rulesets/Core/core.jl b/test/rulesets/Core/core.jl index 2a9d829bb..213bd3d6e 100644 --- a/test/rulesets/Core/core.jl +++ b/test/rulesets/Core/core.jl @@ -1,10 +1,13 @@ +using ChainRules, ChainRulesTestUtils, Test + @testset "typeassert" begin test_rrule(typeassert, 1.1, Float64) test_frule(typeassert, 1.1, Float64) end @testset "ifelse" begin - test_rrule(ifelse, true, 1.1, 2.0) + # The pullback's return type depends on the runtime value of the condition + test_rrule(ifelse, true, 1.1, 2.0; check_inferred=false) test_frule(ifelse, false, 1.1, 2.0) test_rrule(ifelse, true, [1.1], [2.0]; check_inferred=false) diff --git a/test/rulesets/LinearAlgebra/blas.jl b/test/rulesets/LinearAlgebra/blas.jl index cee656715..76fa14f5d 100644 --- a/test/rulesets/LinearAlgebra/blas.jl +++ b/test/rulesets/LinearAlgebra/blas.jl @@ -1,3 +1,6 @@ +using ChainRules, ChainRulesTestUtils, LinearAlgebra, Test +using LinearAlgebra.BLAS + @testset "BLAS" begin @testset "dot" begin @testset "all entries" begin diff --git a/test/rulesets/LinearAlgebra/dense.jl b/test/rulesets/LinearAlgebra/dense.jl index 5f5efa8d2..1a4312666 100644 --- a/test/rulesets/LinearAlgebra/dense.jl +++ b/test/rulesets/LinearAlgebra/dense.jl @@ -1,3 +1,5 @@ +using ChainRules, ChainRulesCore, ChainRulesTestUtils, LinearAlgebra, Test + @testset "dense LinearAlgebra" begin @testset "dot" begin @testset "Vector{$T}" for T in (Float64, ComplexF64) diff --git a/test/rulesets/LinearAlgebra/factorization.jl b/test/rulesets/LinearAlgebra/factorization.jl index 88ad50496..761ae77cf 100644 --- a/test/rulesets/LinearAlgebra/factorization.jl +++ b/test/rulesets/LinearAlgebra/factorization.jl @@ -1,3 +1,6 @@ +using ChainRules, ChainRulesCore, ChainRulesTestUtils, FiniteDifferences, LinearAlgebra, Random, Test +using ChainRulesTestUtils: _fdm + # TODO: move this to FiniteDifferences function FiniteDifferences.to_vec(X::LU) x_vec, back = to_vec(Matrix(X.factors)) diff --git a/test/rulesets/LinearAlgebra/lapack.jl b/test/rulesets/LinearAlgebra/lapack.jl index cdb259694..769b7ad8d 100644 --- a/test/rulesets/LinearAlgebra/lapack.jl +++ b/test/rulesets/LinearAlgebra/lapack.jl @@ -1,3 +1,5 @@ +using ChainRules, ChainRulesTestUtils, LinearAlgebra, Test + @testset "LAPACK" begin @testset "trsyl!" begin @testset "T=$T, m=$m, n=$n, transa='$transa', transb='$transb', isgn=$isgn" for diff --git a/test/rulesets/LinearAlgebra/matfun.jl b/test/rulesets/LinearAlgebra/matfun.jl index 308d87ddc..2fca36304 100644 --- a/test/rulesets/LinearAlgebra/matfun.jl +++ b/test/rulesets/LinearAlgebra/matfun.jl @@ -1,3 +1,5 @@ +using ChainRules, ChainRulesCore, ChainRulesTestUtils, LinearAlgebra, Random, Test + @testset "matrix functions" begin @testset "LinearAlgebra.exp!(A::Matrix) frule" begin n = 10 diff --git a/test/rulesets/LinearAlgebra/norm.jl b/test/rulesets/LinearAlgebra/norm.jl index 2b6306966..09d17d1f3 100644 --- a/test/rulesets/LinearAlgebra/norm.jl +++ b/test/rulesets/LinearAlgebra/norm.jl @@ -1,3 +1,5 @@ +using ChainRules, ChainRulesCore, ChainRulesTestUtils, LinearAlgebra, Test + @testset "norm functions" begin # First test the un-exported functions which norm(A,p) calls diff --git a/test/rulesets/LinearAlgebra/structured.jl b/test/rulesets/LinearAlgebra/structured.jl index db7148f06..364a67032 100644 --- a/test/rulesets/LinearAlgebra/structured.jl +++ b/test/rulesets/LinearAlgebra/structured.jl @@ -1,3 +1,6 @@ +using ChainRules, ChainRulesCore, ChainRulesTestUtils, FiniteDifferences, LinearAlgebra, Test +using ChainRulesTestUtils: _fdm + # `Adjoint(::Adjoint)` and `Transpose(::Transpose)` throw since JuliaLang/LinearAlgebra.jl#1739 const DOUBLE_WRAP_ALLOWED = try Adjoint(Adjoint([1.0])) isa Adjoint diff --git a/test/rulesets/LinearAlgebra/symmetric.jl b/test/rulesets/LinearAlgebra/symmetric.jl index 240013996..5e379af82 100644 --- a/test/rulesets/LinearAlgebra/symmetric.jl +++ b/test/rulesets/LinearAlgebra/symmetric.jl @@ -1,3 +1,6 @@ +using ChainRules, ChainRulesCore, ChainRulesTestUtils, FiniteDifferences, LinearAlgebra, Test +using ChainRulesTestUtils: _fdm + @testset "Symmetric/Hermitian rules" begin @testset "$(SymHerm)(::AbstractMatrix{$T}, :$(uplo))" for SymHerm in (Symmetric, Hermitian), diff --git a/test/rulesets/LinearAlgebra/uniformscaling.jl b/test/rulesets/LinearAlgebra/uniformscaling.jl index 4b6d87fc0..6ca4a7117 100644 --- a/test/rulesets/LinearAlgebra/uniformscaling.jl +++ b/test/rulesets/LinearAlgebra/uniformscaling.jl @@ -1,3 +1,5 @@ +using ChainRules, ChainRulesTestUtils, LinearAlgebra, Test + @testset "UniformScaling rules" begin @testset "constructor" begin diff --git a/test/rulesets/Random/random.jl b/test/rulesets/Random/random.jl index 00326819f..6fdbbb762 100644 --- a/test/rulesets/Random/random.jl +++ b/test/rulesets/Random/random.jl @@ -1,3 +1,5 @@ +using ChainRules, ChainRulesCore, Random, Test + # Simple Distributions like object for testing purposes struct NormalDistribution μ diff --git a/test/rulesets/SparseArrays/sparsematrix.jl b/test/rulesets/SparseArrays/sparsematrix.jl index ea0cf5199..05bbeac17 100644 --- a/test/rulesets/SparseArrays/sparsematrix.jl +++ b/test/rulesets/SparseArrays/sparsematrix.jl @@ -1,3 +1,5 @@ +using ChainRules, ChainRulesCore, ChainRulesTestUtils, FiniteDifferences, LinearAlgebra, Random, SparseArrays, Test +using ChainRulesTestUtils: _fdm @testset "sparse(I, J, V, m, n, +)" begin m, n = 3, 5 diff --git a/test/rulesets/Statistics/statistics.jl b/test/rulesets/Statistics/statistics.jl index 657542d69..37d2499fb 100644 --- a/test/rulesets/Statistics/statistics.jl +++ b/test/rulesets/Statistics/statistics.jl @@ -1,3 +1,5 @@ +using ChainRules, ChainRulesCore, ChainRulesTestUtils, LinearAlgebra, Statistics, Test + struct DummyWeights <: AbstractVector{Float64} # DummyType that looks like StatsBase's Weights types end diff --git a/test/runtests.jl b/test/runtests.jl index 1df03e998..e2e2ae15b 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,98 +1,21 @@ -using Test, ChainRulesCore, ChainRulesTestUtils - -@nospecialize - -using Adapt -using Base.Broadcast: broadcastable using ChainRules -using ChainRules: stack -using ChainRulesCore -using ChainRulesTestUtils -using ChainRulesTestUtils: rand_tangent, _fdm -using FiniteDifferences -using GPUArraysCore -using JLArrays -using LinearAlgebra -using LinearAlgebra.BLAS -using LinearAlgebra: dot -using Random -using SparseArrays -using StaticArrays -using Statistics -using Test -using JuliaInterpreter - -union!(JuliaInterpreter.compiled_modules, Any[Base, Base.Broadcast, LinearAlgebra, Random, StaticArrays, Statistics]) - -# JuliaInterpreter (0.12) does not support Julia 1.14 yet -const INTERPRET_TESTSETS = VERSION < v"1.14.0-DEV" +using ParallelTestRunner -Random.seed!(1) # Set seed that all testsets should reset to. - -function include_test(path) - if isempty(ARGS) || any(occursin(a, path) for a in ARGS) - println("Testing $path:") # print so TravisCI doesn't timeout due to no output - @time Base.include(@__MODULE__(), path) do ex - INTERPRET_TESTSETS && - Meta.isexpr(ex, :macrocall) && - ex.args[1] == Symbol("@testset") || return ex - return :(@interpret (() -> $ex)()) # interpret testsets using JuliaInterpreter - end - else - # If you provide ARGS like so, then it runs only matching testsets: - # Pkg.test("ChainRules", test_args = ["index", "LinearAlgebra"]) - println("(Not testing $path)") - end +# Shared test helpers are defined once per worker process. This includes some piracy +# (`Adapt` methods for the mock GPU array), which should only be evaluated once. +const init_worker_code = quote + include($(joinpath(@__DIR__, "testutils.jl"))) end -if isempty(ARGS) - println("Testing ChainRules.jl") -else - println("Testing ChainRules.jl with test_args = ", ARGS) +# Each test file runs in its own sandbox module and loads the packages it needs itself. +const init_code = quote + using Main.ChainRulesTestHelpers end -@testset "ChainRules" begin # One overall @testset ensures it keeps going after failures - include("test_helpers.jl") # This can't be skipped - println() - - test_method_tables() # Check the global method tables are consistent - - # Each file puts all tests inside one or more @testset blocks - include_test("rulesets/Base/CoreLogging.jl") - include_test("rulesets/Base/base.jl") - include_test("rulesets/Base/fastmath_able.jl") - include_test("rulesets/Base/evalpoly.jl") - include_test("rulesets/Base/array.jl") - include_test("rulesets/Base/arraymath.jl") - include_test("rulesets/Base/indexing.jl") - include_test("rulesets/Base/mapreduce.jl") - include_test("rulesets/Base/sort.jl") - include_test("rulesets/Base/broadcast.jl") - - include_test("unzipped.jl") # used primarily for broadcast +# Every file under `test/` (except this one) is a test, named by its path, e.g. "rulesets/Base/array". +# Select tests via `Pkg.test("ChainRules"; test_args=["rulesets/LinearAlgebra"])`, +# exclude them with a leading `!`, and list all with `test_args=["--list"]`. +const testsuite = find_tests(@__DIR__) +delete!(testsuite, "testutils") - println() - - include_test("rulesets/Statistics/statistics.jl") - - println() - - include_test("rulesets/LinearAlgebra/dense.jl") - include_test("rulesets/LinearAlgebra/norm.jl") - include_test("rulesets/LinearAlgebra/matfun.jl") - include_test("rulesets/LinearAlgebra/structured.jl") - include_test("rulesets/LinearAlgebra/symmetric.jl") - include_test("rulesets/LinearAlgebra/factorization.jl") - include_test("rulesets/LinearAlgebra/blas.jl") - include_test("rulesets/LinearAlgebra/lapack.jl") - include_test("rulesets/LinearAlgebra/uniformscaling.jl") - - println() - - include_test("rulesets/SparseArrays/sparsematrix.jl") - - println() - - include_test("rulesets/Random/random.jl") - println() -end +runtests(ChainRules, ARGS; testsuite, init_code, init_worker_code) diff --git a/test/test_helpers.jl b/test/test_helpers.jl index 2aebaccdc..06611fb4c 100644 --- a/test/test_helpers.jl +++ b/test/test_helpers.jl @@ -1,212 +1,4 @@ -using JLArrays # provides a fake GPU array -JLArrays.allowscalar(false) - -using Adapt -jl32(xs) = adapt(JLArray{Float32}, xs) # this works much like `CUDA.cu` -# This defines the behaviour of `adapt(JLArray{Float32}, xs)` on arrays. -# This is piracy, but only while running these tests... could avoid by defining a struct. -Adapt.adapt_storage(::Type{<:JLArray{Float32}}, xs::AbstractArray{<:Complex}) = convert(JLArray{ComplexF32}, xs) -Adapt.adapt_storage(::Type{<:JLArray{Float32}}, x::Float64) = Float32(x) -Adapt.adapt_storage(::Type{<:JLArray{Float32}}, x::ComplexF64) = ComplexF32(x) -Adapt.adapt(T::Type{<:JLArray{Float32}}, x::AbstractThunk) = adapt(T, unthunk(x)) - -f32(xs) = adapt(Array{Float32}, xs) -# This maps things back to the CPU. Or, applied to CPU arrays, changes to Float32 to match GPU calculation. -Adapt.adapt_storage(::Type{<:Array{Float32}}, xs::AbstractArray{<:Complex}) = convert(Array{ComplexF32}, xs) -Adapt.adapt_storage(::Type{<:Array{Float32}}, x::Float64) = Float32(x) -Adapt.adapt_storage(::Type{<:Array{Float32}}, x::ComplexF64) = ComplexF32(x) -Adapt.adapt(T::Type{<:Array{Float32}}, x::AbstractThunk) = adapt(T, unthunk(x)) - - -""" - @gpu test_rrule(sum, rand(3)) - @gpu test_frule((0, rand(3)), sum, rand(3)) - -After running the test shown, this converts all arrays to `GPUArray`s, -and checks that the rule accepts this, and produces the same result. -Uses the mock GPU array from JLArrays.jl, even if you have a real GPU available. -Does not understand all features of `test_rrule`, in particular `⊢`. - - @gpu rrule(sum, rand(3)) - @gpu frule((0, rand(3)), sum, rand(3)) - -Used directly on a rule, this compares a CPU to a GPU run, without -checking correctness. Both runs will convert numbers to `Float32` first. -""" -macro gpu(ex) - _gpu_macro(ex, false, __source__) -end -macro gpu_broken(ex) - _gpu_macro(ex, true, __source__) -end -function _gpu_macro(ex, broken::Bool, __source__) - Meta.isexpr(ex, :call) && ex.args[1] in (:test_rrule, :test_frule, :rrule, :frule) || - error("@gpu doesn't understand this input") - # `@test` requires a Symbol, not a function (Julia ≥ 1.13.0-DEV.300) - ex2 = if Meta.isexpr(ex.args[2], :parameters) - :(_gpu_test($(ex.args[2]), $(ex.args[1]), $(ex.args[3:end]...))) - else - :(_gpu_test($(ex.args[1]), $(ex.args[2:end]...))) - end - return if broken - Expr(:block, ex, Expr(:macrocall, Symbol("@test_broken"), __source__, ex2)) |> esc - else - Expr(:block, ex, Expr(:macrocall, Symbol("@test"), __source__, ex2)) |> esc - # NB this @test should report a line number in your tests, not here. - end -end - -function _gpu_test(::typeof(test_rrule), xs...; fkwargs = (;), kw...) - _gpu_test(rrule, xs...; fkwargs...) -end - -function _gpu_test(::typeof(test_frule), xs...; fkwargs = (;), kw...) - _gpu_test(frule, xs...; fkwargs...) -end - -function _gpu_test(::typeof(rrule), xs...; kw...) - y, bk = rrule(f32(xs)...; kw...) - - eltype(y) in (Float64, ComplexF64) && return false # test for accidental Float64 promotion - - y1 = one.(y) # crude way to get input sensitivity - dxs = unthunk.(bk(y1)) - - gpu_y, gpu_bk = rrule(jl32(xs)...; kw...) - gpu_dxs = adapt(Array, unthunk.(gpu_bk(one.(gpu_y)))) - - agree = map(gpu_dxs, dxs) do a, b - a isa AbstractZero && return b isa AbstractZero - isapprox(a, b) # compare on CPU to avoid error from e.g. `isapprox(jl(x), jl(permutedims(x))')` - end - return all(agree) - # NB this does not contain @test, it just returns true/false. Then it can be used with `@test_broken` without going mad. -end - -function _gpu_test(::typeof(frule), xdots, f::Function, xs...; kw...) - y = frule(f32(xdots), f, f32(xs)...; kw...) - @test eltype(y[2]) ∉ (Float64, ComplexF64) - gpu_y = frule(jl32(xdots), f, jl32(xs)...; kw...) - ChainRulesTestUtils.test_approx(gpu_y, jl32(y)) - true -end -function _gpu_test(::typeof(frule), f::Function, xs...; kw...) - xdots = (NoTangent(), xs...) # a pretty crude way to generate xdots for you - _gpu_test(frule, xdots, f, xs...; kw...) -end -function _gpu_test(::typeof(frule), f::Function, g::Function, xs...; kw...) # solves an ambiguity - xdots = (NoTangent(), NoTangent(), xs...) - _gpu_test(frule, xdots, f, g, xs...; kw...) -end - -const CFG = ChainRulesTestUtils.TestConfig() - -""" - Multiplier(x) - -Stores a fixed `x` and multiplies by it, especially for testing -the gradient of higher order functions with respect to `x`. -``` -julia> map(Multiplier(pi), [1 10 100 1000]) -1×4 Matrix{Float64}: - 3.14159 31.4159 314.159 3141.59 - -julia> map(Multiplier(2), [1 10 100], [-1 3 -7]) # two arguments -1×3 Matrix{Int64}: - -2 60 -1400 - -julia> map(Multiplier([1 2; 3 4]), ([5, 6], [7, 8])) # x isa Matrix -([17, 39], [23, 53]) -``` -""" -struct Multiplier{T} - x::T -end -(m::Multiplier)(y) = m.x * y -(m::Multiplier)(y, z) = (m.x * y) * z - -function ChainRulesCore.rrule(m::Multiplier, y) - Multiplier_pullback(dΩ) = (Tangent{typeof(m)}(; x = dΩ * y'), m.x' * dΩ) - return m(y), Multiplier_pullback -end -function ChainRulesCore.rrule(m::Multiplier, y, z) - Multiplier_pullback_3(dΩ) = ( - Tangent{typeof(m)}(; x = dΩ * (y * z)'), - m.x' * dΩ * z', - (m.x * y)' * dΩ, - ) - return m(y, z), Multiplier_pullback_3 -end - -""" - Divider(x) - -Stores a fixed `x` and divides by it, then squares the result. - -Especially for testing the gradient of higher order functions with respect to `x`. -``` -julia> map(Divider(2), [1 2 3 4 10]) -1×5 Matrix{Float64}: - 0.25 1.0 2.25 4.0 25.0 -``` -""" -struct Divider{T<:Real} - x::T -end -(d::Divider)(y::Real) = (y / d.x)^2 - -function ChainRulesCore.rrule(d::Divider, y::Real) - Divider_pullback(dΩ) = (Tangent{typeof(d)}(; x = -2 * dΩ * y^2 / d.x^3), 2 * dΩ * y / d.x^2) - return d(y), Divider_pullback -end - -""" - Counter() - -Multiplies its input by number that increments on each call, -for testing execution order. Has a gradient `rrule` which -similarly increases by 10 each call, this is *not* the true gradient! -``` -julia> map(Counter(), [1 1 1 10 10 10]) -1×6 Matrix{Int64}: - 1 2 3 40 50 60 -``` -""" -mutable struct Counter - n::Int - Counter(n=0) = new(n) -end -Base.:(==)(a::Counter, b::Counter) = (a.n == b.n) -(c::Counter)(x) = x * (c.n += 1) -(c::Counter)(x, y) = (x + y) * (c.n += 1) - -function ChainRulesCore.rrule(c::Counter, x) - # False gradient, again just to record sequence of calls: - Counter_back(dΩ) = (NoTangent(), dΩ * (c.n += 10)) - return c(x), Counter_back -end -function ChainRulesCore.rrule(c::Counter, x, y) - Counter_back_2(dΩ) = (NoTangent(), dΩ * (c.n += 10), dΩ * (c.n += 10)) - return c(x, y), Counter_back_2 -end - -# NoRules - has no rules defined -struct NoRules; end - -"A function that outputs a vector from a scalar for testing" -make_two_vec(x) = [x, x] -function ChainRulesCore.rrule(::typeof(make_two_vec), x) - make_two_vec_pullback(ȳ) = (NoTangent(), sum(ȳ)) - return make_two_vec(x), make_two_vec_pullback -end - -"A version of `*` with only an `frule` defined" -fstar(A, B) = A * B -ChainRulesCore.frule((_, ΔA, ΔB), ::typeof(fstar), A, B) = A * B, muladd(ΔA, B, A * ΔB) - -"A version of `log` with only an `frule` defined" -flog(x::Number) = log(x) -ChainRulesCore.frule((_, Δx), ::typeof(flog), x::Number) = log(x), inv(x) * Δx +using ChainRules, ChainRulesCore, ChainRulesTestUtils, Test @testset "test_helpers.jl" begin @@ -253,4 +45,8 @@ ChainRulesCore.frule((_, Δx), ::typeof(flog), x::Number) = log(x), inv(x) * Δx (t[1].name in [:rrule, Symbol("rrule##kw")]) end end + + @testset "method tables" begin + test_method_tables() # Check the global method tables are consistent + end end diff --git a/test/testutils.jl b/test/testutils.jl new file mode 100644 index 000000000..de4fd76a1 --- /dev/null +++ b/test/testutils.jl @@ -0,0 +1,222 @@ +# Helpers shared by the test files. This module is loaded once per test worker +# (see `init_worker_code` in runtests.jl) and brought into each test via `using`. +module ChainRulesTestHelpers + +using Adapt +using ChainRulesCore +using ChainRulesTestUtils +using JLArrays # provides a fake GPU array +using Test + +export jl32, f32, @gpu, @gpu_broken, _gpu_test # `@gpu` expands to a call of `_gpu_test` by name +export CFG, Multiplier, Divider, Counter, NoRules, make_two_vec, fstar, flog + +JLArrays.allowscalar(false) + +jl32(xs) = adapt(JLArray{Float32}, xs) # this works much like `CUDA.cu` +# This defines the behaviour of `adapt(JLArray{Float32}, xs)` on arrays. +# This is piracy, but only while running these tests... could avoid by defining a struct. +Adapt.adapt_storage(::Type{<:JLArray{Float32}}, xs::AbstractArray{<:Complex}) = convert(JLArray{ComplexF32}, xs) +Adapt.adapt_storage(::Type{<:JLArray{Float32}}, x::Float64) = Float32(x) +Adapt.adapt_storage(::Type{<:JLArray{Float32}}, x::ComplexF64) = ComplexF32(x) +Adapt.adapt(T::Type{<:JLArray{Float32}}, x::AbstractThunk) = adapt(T, unthunk(x)) + +f32(xs) = adapt(Array{Float32}, xs) +# This maps things back to the CPU. Or, applied to CPU arrays, changes to Float32 to match GPU calculation. +Adapt.adapt_storage(::Type{<:Array{Float32}}, xs::AbstractArray{<:Complex}) = convert(Array{ComplexF32}, xs) +Adapt.adapt_storage(::Type{<:Array{Float32}}, x::Float64) = Float32(x) +Adapt.adapt_storage(::Type{<:Array{Float32}}, x::ComplexF64) = ComplexF32(x) +Adapt.adapt(T::Type{<:Array{Float32}}, x::AbstractThunk) = adapt(T, unthunk(x)) + + +""" + @gpu test_rrule(sum, rand(3)) + @gpu test_frule((0, rand(3)), sum, rand(3)) + +After running the test shown, this converts all arrays to `GPUArray`s, +and checks that the rule accepts this, and produces the same result. +Uses the mock GPU array from JLArrays.jl, even if you have a real GPU available. +Does not understand all features of `test_rrule`, in particular `⊢`. + + @gpu rrule(sum, rand(3)) + @gpu frule((0, rand(3)), sum, rand(3)) + +Used directly on a rule, this compares a CPU to a GPU run, without +checking correctness. Both runs will convert numbers to `Float32` first. +""" +macro gpu(ex) + _gpu_macro(ex, false, __source__) +end +macro gpu_broken(ex) + _gpu_macro(ex, true, __source__) +end +function _gpu_macro(ex, broken::Bool, __source__) + Meta.isexpr(ex, :call) && ex.args[1] in (:test_rrule, :test_frule, :rrule, :frule) || + error("@gpu doesn't understand this input") + # `@test` requires a Symbol, not a function (Julia ≥ 1.13.0-DEV.300) + ex2 = if Meta.isexpr(ex.args[2], :parameters) + :(_gpu_test($(ex.args[2]), $(ex.args[1]), $(ex.args[3:end]...))) + else + :(_gpu_test($(ex.args[1]), $(ex.args[2:end]...))) + end + return if broken + Expr(:block, ex, Expr(:macrocall, Symbol("@test_broken"), __source__, ex2)) |> esc + else + Expr(:block, ex, Expr(:macrocall, Symbol("@test"), __source__, ex2)) |> esc + # NB this @test should report a line number in your tests, not here. + end +end + +function _gpu_test(::typeof(test_rrule), xs...; fkwargs = (;), kw...) + _gpu_test(rrule, xs...; fkwargs...) +end + +function _gpu_test(::typeof(test_frule), xs...; fkwargs = (;), kw...) + _gpu_test(frule, xs...; fkwargs...) +end + +function _gpu_test(::typeof(rrule), xs...; kw...) + y, bk = rrule(f32(xs)...; kw...) + + eltype(y) in (Float64, ComplexF64) && return false # test for accidental Float64 promotion + + y1 = one.(y) # crude way to get input sensitivity + dxs = unthunk.(bk(y1)) + + gpu_y, gpu_bk = rrule(jl32(xs)...; kw...) + gpu_dxs = adapt(Array, unthunk.(gpu_bk(one.(gpu_y)))) + + agree = map(gpu_dxs, dxs) do a, b + a isa AbstractZero && return b isa AbstractZero + isapprox(a, b) # compare on CPU to avoid error from e.g. `isapprox(jl(x), jl(permutedims(x))')` + end + return all(agree) + # NB this does not contain @test, it just returns true/false. Then it can be used with `@test_broken` without going mad. +end + +function _gpu_test(::typeof(frule), xdots, f::Function, xs...; kw...) + y = frule(f32(xdots), f, f32(xs)...; kw...) + @test eltype(y[2]) ∉ (Float64, ComplexF64) + gpu_y = frule(jl32(xdots), f, jl32(xs)...; kw...) + ChainRulesTestUtils.test_approx(gpu_y, jl32(y)) + true +end +function _gpu_test(::typeof(frule), f::Function, xs...; kw...) + xdots = (NoTangent(), xs...) # a pretty crude way to generate xdots for you + _gpu_test(frule, xdots, f, xs...; kw...) +end +function _gpu_test(::typeof(frule), f::Function, g::Function, xs...; kw...) # solves an ambiguity + xdots = (NoTangent(), NoTangent(), xs...) + _gpu_test(frule, xdots, f, g, xs...; kw...) +end + +const CFG = ChainRulesTestUtils.TestConfig() + +""" + Multiplier(x) + +Stores a fixed `x` and multiplies by it, especially for testing +the gradient of higher order functions with respect to `x`. +``` +julia> map(Multiplier(pi), [1 10 100 1000]) +1×4 Matrix{Float64}: + 3.14159 31.4159 314.159 3141.59 + +julia> map(Multiplier(2), [1 10 100], [-1 3 -7]) # two arguments +1×3 Matrix{Int64}: + -2 60 -1400 + +julia> map(Multiplier([1 2; 3 4]), ([5, 6], [7, 8])) # x isa Matrix +([17, 39], [23, 53]) +``` +""" +struct Multiplier{T} + x::T +end +(m::Multiplier)(y) = m.x * y +(m::Multiplier)(y, z) = (m.x * y) * z + +function ChainRulesCore.rrule(m::Multiplier, y) + Multiplier_pullback(dΩ) = (Tangent{typeof(m)}(; x = dΩ * y'), m.x' * dΩ) + return m(y), Multiplier_pullback +end +function ChainRulesCore.rrule(m::Multiplier, y, z) + Multiplier_pullback_3(dΩ) = ( + Tangent{typeof(m)}(; x = dΩ * (y * z)'), + m.x' * dΩ * z', + (m.x * y)' * dΩ, + ) + return m(y, z), Multiplier_pullback_3 +end + +""" + Divider(x) + +Stores a fixed `x` and divides by it, then squares the result. + +Especially for testing the gradient of higher order functions with respect to `x`. +``` +julia> map(Divider(2), [1 2 3 4 10]) +1×5 Matrix{Float64}: + 0.25 1.0 2.25 4.0 25.0 +``` +""" +struct Divider{T<:Real} + x::T +end +(d::Divider)(y::Real) = (y / d.x)^2 + +function ChainRulesCore.rrule(d::Divider, y::Real) + Divider_pullback(dΩ) = (Tangent{typeof(d)}(; x = -2 * dΩ * y^2 / d.x^3), 2 * dΩ * y / d.x^2) + return d(y), Divider_pullback +end + +""" + Counter() + +Multiplies its input by number that increments on each call, +for testing execution order. Has a gradient `rrule` which +similarly increases by 10 each call, this is *not* the true gradient! +``` +julia> map(Counter(), [1 1 1 10 10 10]) +1×6 Matrix{Int64}: + 1 2 3 40 50 60 +``` +""" +mutable struct Counter + n::Int + Counter(n=0) = new(n) +end +Base.:(==)(a::Counter, b::Counter) = (a.n == b.n) +(c::Counter)(x) = x * (c.n += 1) +(c::Counter)(x, y) = (x + y) * (c.n += 1) + +function ChainRulesCore.rrule(c::Counter, x) + # False gradient, again just to record sequence of calls: + Counter_back(dΩ) = (NoTangent(), dΩ * (c.n += 10)) + return c(x), Counter_back +end +function ChainRulesCore.rrule(c::Counter, x, y) + Counter_back_2(dΩ) = (NoTangent(), dΩ * (c.n += 10), dΩ * (c.n += 10)) + return c(x, y), Counter_back_2 +end + +# NoRules - has no rules defined +struct NoRules; end + +"A function that outputs a vector from a scalar for testing" +make_two_vec(x) = [x, x] +function ChainRulesCore.rrule(::typeof(make_two_vec), x) + make_two_vec_pullback(ȳ) = (NoTangent(), sum(ȳ)) + return make_two_vec(x), make_two_vec_pullback +end + +"A version of `*` with only an `frule` defined" +fstar(A, B) = A * B +ChainRulesCore.frule((_, ΔA, ΔB), ::typeof(fstar), A, B) = A * B, muladd(ΔA, B, A * ΔB) + +"A version of `log` with only an `frule` defined" +flog(x::Number) = log(x) +ChainRulesCore.frule((_, Δx), ::typeof(flog), x::Number) = log(x), inv(x) * Δx + +end # module diff --git a/test/unzipped.jl b/test/unzipped.jl index 4215f3a6e..6bc508f3f 100644 --- a/test/unzipped.jl +++ b/test/unzipped.jl @@ -1,4 +1,4 @@ - +using ChainRules, ChainRulesCore, JLArrays, Test using ChainRules: unzip_broadcast, unzip #, unzip_map @testset "unzipped.jl" begin From 60adaca04c68c1b022344105a026efa5b1ab2cce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20M=C3=BCller-Widmann?= Date: Fri, 16 Oct 2026 16:13:06 +0200 Subject: [PATCH 3/7] Apply format suggestions Format the new `test/testutils.jl` and the lines changed in this branch with JuliaFormatter (BlueStyle). Co-Authored-By: Claude Opus 5.5 --- src/rulesets/Base/arraymath.jl | 10 ++-- src/rulesets/LinearAlgebra/factorization.jl | 10 ++-- src/rulesets/SparseArrays/sparsematrix.jl | 2 +- test/rulesets/Base/array.jl | 24 ++++++--- test/rulesets/Base/arraymath.jl | 23 +++++---- test/rulesets/Base/indexing.jl | 10 ++-- test/rulesets/LinearAlgebra/factorization.jl | 7 ++- test/rulesets/LinearAlgebra/structured.jl | 3 +- test/rulesets/LinearAlgebra/symmetric.jl | 3 +- test/rulesets/Random/random.jl | 2 +- test/rulesets/SparseArrays/sparsematrix.jl | 9 +++- test/testutils.jl | 53 ++++++++++---------- 12 files changed, 87 insertions(+), 69 deletions(-) diff --git a/src/rulesets/Base/arraymath.jl b/src/rulesets/Base/arraymath.jl index 69aa220ba..c92817207 100644 --- a/src/rulesets/Base/arraymath.jl +++ b/src/rulesets/Base/arraymath.jl @@ -144,7 +144,7 @@ function rrule( ::typeof(mat_mat_scalar), A::StridedMaybeAdjOrTransMat{<:CommutativeMulNumber}, B::StridedMaybeAdjOrTransMat{<:CommutativeMulNumber}, - γ::CommutativeMulNumber + γ::CommutativeMulNumber, ) project_A = ProjectTo(A) project_B = ProjectTo(B) @@ -176,7 +176,7 @@ function rrule( ::typeof(mat_vec_scalar), A::StridedMaybeAdjOrTransMat{<:CommutativeMulNumber}, b::StridedVector{<:CommutativeMulNumber}, - γ::CommutativeMulNumber + γ::CommutativeMulNumber, ) project_A = ProjectTo(A) project_b = ProjectTo(b) @@ -185,12 +185,10 @@ function rrule( function mat_vec_scalar_back(dy_raw) dy = unthunk(dy_raw) Athunk = InplaceableThunk( - dA -> mul!(dA, dy, b', conj(γ), true), - @thunk(project_A(*(dy, b', conj(γ)))), + dA -> mul!(dA, dy, b', conj(γ), true), @thunk(project_A(*(dy, b', conj(γ)))) ) Bthunk = InplaceableThunk( - db -> mul!(db, A', dy, conj(γ), true), - @thunk(project_b(*(A', dy, conj(γ)))), + db -> mul!(db, A', dy, conj(γ), true), @thunk(project_b(*(A', dy, conj(γ)))) ) γthunk = @thunk if iszero(γ) project_γ(dot(mat_vec_scalar(A, b, oneunit(γ)), dy)) diff --git a/src/rulesets/LinearAlgebra/factorization.jl b/src/rulesets/LinearAlgebra/factorization.jl index adad40d00..e04bd0e21 100644 --- a/src/rulesets/LinearAlgebra/factorization.jl +++ b/src/rulesets/LinearAlgebra/factorization.jl @@ -15,7 +15,7 @@ using LinearAlgebra.BLAS: gemv, gemv!, gemm!, trsm!, axpy!, ger! # for derivations for wide and tall matrices, see # https://sethaxen.com/blog/2021/02/differentiating-the-lu-decomposition/ -const CHOLESKY_NoPivot = Union{NoPivot, Val{false}} +const CHOLESKY_NoPivot = Union{NoPivot,Val{false}} # For fixed pivots, the factors are differentiable iff all pivots are nonzero, except # possibly the last pivot of square or wide A. `F.info` is the index of the first zero @@ -128,10 +128,10 @@ function _lu_pullback(ΔF::Tangent, m, n, eltypeA, pivot, F) end return NoTangent(), ∂A, NoTangent() end -_lu_pullback(ΔF::AbstractThunk, m, n, eltypeA, pivot, F) = _lu_pullback(unthunk(ΔF), m, n, eltypeA, pivot, F) -function rrule( - ::typeof(lu), A::StridedMatrix, pivot::Union{RowMaximum,NoPivot}; kwargs... -) +function _lu_pullback(ΔF::AbstractThunk, m, n, eltypeA, pivot, F) + return _lu_pullback(unthunk(ΔF), m, n, eltypeA, pivot, F) +end +function rrule(::typeof(lu), A::StridedMatrix, pivot::Union{RowMaximum,NoPivot}; kwargs...) m, n = size(A) F = lu(A, pivot; kwargs...) lu_pullback(ȳ) = _lu_pullback(ȳ, m, n, eltype(A), pivot, F) diff --git a/src/rulesets/SparseArrays/sparsematrix.jl b/src/rulesets/SparseArrays/sparsematrix.jl index bba6040ed..b912a0891 100644 --- a/src/rulesets/SparseArrays/sparsematrix.jl +++ b/src/rulesets/SparseArrays/sparsematrix.jl @@ -69,7 +69,7 @@ if Base.USE_GPL_LIBS # Don't define rrules for sparse determinants if we don't h end return Ω, logabsdet_pullback end - + function rrule(::typeof(logdet), x::SparseMatrixCSC) Ω = logdet(x) function logdet_pullback(ΔΩ) diff --git a/test/rulesets/Base/array.jl b/test/rulesets/Base/array.jl index 5744bf169..0fb2844a3 100644 --- a/test/rulesets/Base/array.jl +++ b/test/rulesets/Base/array.jl @@ -1,4 +1,12 @@ -using ChainRules, ChainRulesCore, ChainRulesTestUtils, FiniteDifferences, JLArrays, LinearAlgebra, SparseArrays, StaticArrays, Test +using ChainRules, + ChainRulesCore, + ChainRulesTestUtils, + FiniteDifferences, + JLArrays, + LinearAlgebra, + SparseArrays, + StaticArrays, + Test @testset "Array constructors" begin @testset "undef" begin @@ -142,9 +150,9 @@ end @gpu test_rrule(repeat, rand(4, ), 2) @gpu test_rrule(repeat, rand(4, 5), 2) @gpu test_rrule(repeat, rand(4, 5), 2, 3) - test_rrule(repeat, rand(1,2,3), 2,3,4) - test_rrule(repeat, rand(0,2,3), 2,0,4) - test_rrule(repeat, rand(1,1,1,1), 2,3,4,5) + test_rrule(repeat, rand(1, 2, 3), 2, 3, 4) + test_rrule(repeat, rand(0, 2, 3), 2, 0, 4) + test_rrule(repeat, rand(1, 1, 1, 1), 2, 3, 4, 5) test_rrule(repeat, rand(4, 5); fkwargs = (inner=(2,4), outer=(1,1,1,3))) test_rrule(repeat, rand(1,2,3), 2,3) @@ -243,10 +251,10 @@ end # reverse @gpu test_rrule(cat, rand(2, 4), rand(1, 4); fkwargs=(dims=1,)) @gpu test_rrule(cat, rand(2, 4), rand(2); fkwargs=(dims=Val(2),)) - test_rrule(cat, rand(), rand(2, 3); fkwargs=(dims=[1,2],)) - test_rrule(cat, rand(1), rand(3, 2, 1); fkwargs=(dims=(1,2),), check_inferred=false) # infers Tuple{Zero, Vector{Float64}, Any} - - test_rrule(cat, randn(3,2,4), randn(3,2,4); fkwargs=(dims=Val((1,2)),)) #678 + test_rrule(cat, rand(), rand(2, 3); fkwargs=(dims=[1, 2],)) + test_rrule(cat, rand(1), rand(3, 2, 1); fkwargs=(dims=(1, 2),), check_inferred=false) # infers Tuple{Zero, Vector{Float64}, Any} + + test_rrule(cat, randn(3, 2, 4), randn(3, 2, 4); fkwargs=(dims=Val((1, 2)),)) #678 test_rrule(cat, rand(2, 2), rand(2, 2)'; fkwargs=(dims=1,)) # inference on exotic array types diff --git a/test/rulesets/Base/arraymath.jl b/test/rulesets/Base/arraymath.jl index 978e1964f..d761e5cf0 100644 --- a/test/rulesets/Base/arraymath.jl +++ b/test/rulesets/Base/arraymath.jl @@ -135,20 +135,23 @@ using ChainRules, ChainRulesCore, ChainRulesTestUtils, LinearAlgebra, Test @testset "3-arg *, $T" for T in [Float64, ComplexF64] - test_rrule(mat_mat_scalar, rand(T,4,4), rand(T,4,4), rand(T)) - test_rrule(mat_mat_scalar, rand(T,4,4), rand(T,4,4), 0.0) - test_rrule(mat_mat_scalar, rand(T,4,4)' ⊢ rand(T,4,4), rand(T,4,4), rand(T)) + test_rrule(mat_mat_scalar, rand(T, 4, 4), rand(T, 4, 4), rand(T)) + test_rrule(mat_mat_scalar, rand(T, 4, 4), rand(T, 4, 4), 0.0) + test_rrule(mat_mat_scalar, rand(T, 4, 4)' ⊢ rand(T, 4, 4), rand(T, 4, 4), rand(T)) - test_rrule(mat_vec_scalar, rand(T,4,4), rand(T,4), rand(T)) - test_rrule(mat_vec_scalar, rand(T,4,4), rand(T,4), 0.0) + test_rrule(mat_vec_scalar, rand(T, 4, 4), rand(T, 4), rand(T)) + test_rrule(mat_vec_scalar, rand(T, 4, 4), rand(T, 4), 0.0) T == ComplexF64 && continue # Test with γ of a wider type - A, B, b, γ = rand(3,3), rand(3,3), rand(3), rand() - dZ, dz = rand(3,3), rand(3) - unthunk(rrule(mat_mat_scalar, A, B, γ + 0im)[2](dZ)[4]) ≈ unthunk(rrule(mat_mat_scalar, A, B, γ)[2](dZ)[4]) - unthunk(rrule(mat_mat_scalar, A, B, 0 + 0im)[2](dZ)[4]) ≈ unthunk(rrule(mat_mat_scalar, A, B, 0)[2](dZ)[4]) - unthunk(rrule(mat_vec_scalar, A, b, γ + 0im)[2](dz)[4]) ≈ unthunk(rrule(mat_vec_scalar, A, b, γ)[2](dz)[4]) + A, B, b, γ = rand(3, 3), rand(3, 3), rand(3), rand() + dZ, dz = rand(3, 3), rand(3) + unthunk(rrule(mat_mat_scalar, A, B, γ + 0im)[2](dZ)[4]) ≈ + unthunk(rrule(mat_mat_scalar, A, B, γ)[2](dZ)[4]) + unthunk(rrule(mat_mat_scalar, A, B, 0 + 0im)[2](dZ)[4]) ≈ + unthunk(rrule(mat_mat_scalar, A, B, 0)[2](dZ)[4]) + unthunk(rrule(mat_vec_scalar, A, b, γ + 0im)[2](dz)[4]) ≈ + unthunk(rrule(mat_vec_scalar, A, b, γ)[2](dz)[4]) end @testset "$f" for f in (/, \) diff --git a/test/rulesets/Base/indexing.jl b/test/rulesets/Base/indexing.jl index d9a67d444..a38dfc4d4 100644 --- a/test/rulesets/Base/indexing.jl +++ b/test/rulesets/Base/indexing.jl @@ -1,4 +1,5 @@ -using ChainRules, ChainRulesCore, ChainRulesTestUtils, GPUArraysCore, JLArrays, LinearAlgebra, Test +using ChainRules, + ChainRulesCore, ChainRulesTestUtils, GPUArraysCore, JLArrays, LinearAlgebra, Test struct FooTwoField x::Float64 @@ -252,12 +253,7 @@ end # Second derivative rule test_rrule(ChainRules.∇eachslice, [rand(4) for _ in 1:3], rand(3, 4), Val(1)) test_rrule(ChainRules.∇eachslice, [rand(3) for _ in 1:4], rand(3, 4), Val(2)) - test_rrule( - ChainRules.∇eachslice, - [rand(2, 3) for _ in 1:4], - rand(2, 3, 4), - Val(3), - ) + test_rrule(ChainRules.∇eachslice, [rand(2, 3) for _ in 1:4], rand(2, 3, 4), Val(3)) # eachslice: Make sure pulling back an array of thunks unthunks them and does not return all zeros. x = ones(Float32, 3) diff --git a/test/rulesets/LinearAlgebra/factorization.jl b/test/rulesets/LinearAlgebra/factorization.jl index 761ae77cf..f5cc8be7e 100644 --- a/test/rulesets/LinearAlgebra/factorization.jl +++ b/test/rulesets/LinearAlgebra/factorization.jl @@ -1,4 +1,5 @@ -using ChainRules, ChainRulesCore, ChainRulesTestUtils, FiniteDifferences, LinearAlgebra, Random, Test +using ChainRules, + ChainRulesCore, ChainRulesTestUtils, FiniteDifferences, LinearAlgebra, Random, Test using ChainRulesTestUtils: _fdm # TODO: move this to FiniteDifferences @@ -484,7 +485,9 @@ end D = Diagonal([0.3 + 0im, 0.2, 0.5, 0.6, 0.9]) C = cholesky(D) test_rrule( - cholesky, D, NoPivot(); + cholesky, + D, + NoPivot(); output_tangent=Tangent{typeof(C)}(factors=complex(randn(5, 5))), fkwargs=(; check=false), ) diff --git a/test/rulesets/LinearAlgebra/structured.jl b/test/rulesets/LinearAlgebra/structured.jl index 364a67032..d0f259ac0 100644 --- a/test/rulesets/LinearAlgebra/structured.jl +++ b/test/rulesets/LinearAlgebra/structured.jl @@ -1,4 +1,5 @@ -using ChainRules, ChainRulesCore, ChainRulesTestUtils, FiniteDifferences, LinearAlgebra, Test +using ChainRules, + ChainRulesCore, ChainRulesTestUtils, FiniteDifferences, LinearAlgebra, Test using ChainRulesTestUtils: _fdm # `Adjoint(::Adjoint)` and `Transpose(::Transpose)` throw since JuliaLang/LinearAlgebra.jl#1739 diff --git a/test/rulesets/LinearAlgebra/symmetric.jl b/test/rulesets/LinearAlgebra/symmetric.jl index 5e379af82..be87b11f7 100644 --- a/test/rulesets/LinearAlgebra/symmetric.jl +++ b/test/rulesets/LinearAlgebra/symmetric.jl @@ -1,4 +1,5 @@ -using ChainRules, ChainRulesCore, ChainRulesTestUtils, FiniteDifferences, LinearAlgebra, Test +using ChainRules, + ChainRulesCore, ChainRulesTestUtils, FiniteDifferences, LinearAlgebra, Test using ChainRulesTestUtils: _fdm @testset "Symmetric/Hermitian rules" begin diff --git a/test/rulesets/Random/random.jl b/test/rulesets/Random/random.jl index 6fdbbb762..6915a3f86 100644 --- a/test/rulesets/Random/random.jl +++ b/test/rulesets/Random/random.jl @@ -42,7 +42,7 @@ Random.rand(d::NormalDistribution) = d.μ + d.σ*randn() ((2,2), Matrix{<:Float64}), ] push!(non_differentiables, ((Xoshiro(123),), Float64)) - push!(non_differentiables, ((Xoshiro(123),2,2), Matrix{<:Float64})) + push!(non_differentiables, ((Xoshiro(123), 2, 2), Matrix{<:Float64})) for (args, xType) in non_differentiables x, dΩ = frule((ZeroTangent(), randn(args...)), rand, args...) diff --git a/test/rulesets/SparseArrays/sparsematrix.jl b/test/rulesets/SparseArrays/sparsematrix.jl index 05bbeac17..0ad948c58 100644 --- a/test/rulesets/SparseArrays/sparsematrix.jl +++ b/test/rulesets/SparseArrays/sparsematrix.jl @@ -1,4 +1,11 @@ -using ChainRules, ChainRulesCore, ChainRulesTestUtils, FiniteDifferences, LinearAlgebra, Random, SparseArrays, Test +using ChainRules, + ChainRulesCore, + ChainRulesTestUtils, + FiniteDifferences, + LinearAlgebra, + Random, + SparseArrays, + Test using ChainRulesTestUtils: _fdm @testset "sparse(I, J, V, m, n, +)" begin diff --git a/test/testutils.jl b/test/testutils.jl index de4fd76a1..8d7fda112 100644 --- a/test/testutils.jl +++ b/test/testutils.jl @@ -16,18 +16,21 @@ JLArrays.allowscalar(false) jl32(xs) = adapt(JLArray{Float32}, xs) # this works much like `CUDA.cu` # This defines the behaviour of `adapt(JLArray{Float32}, xs)` on arrays. # This is piracy, but only while running these tests... could avoid by defining a struct. -Adapt.adapt_storage(::Type{<:JLArray{Float32}}, xs::AbstractArray{<:Complex}) = convert(JLArray{ComplexF32}, xs) +function Adapt.adapt_storage(::Type{<:JLArray{Float32}}, xs::AbstractArray{<:Complex}) + return convert(JLArray{ComplexF32}, xs) +end Adapt.adapt_storage(::Type{<:JLArray{Float32}}, x::Float64) = Float32(x) Adapt.adapt_storage(::Type{<:JLArray{Float32}}, x::ComplexF64) = ComplexF32(x) Adapt.adapt(T::Type{<:JLArray{Float32}}, x::AbstractThunk) = adapt(T, unthunk(x)) f32(xs) = adapt(Array{Float32}, xs) # This maps things back to the CPU. Or, applied to CPU arrays, changes to Float32 to match GPU calculation. -Adapt.adapt_storage(::Type{<:Array{Float32}}, xs::AbstractArray{<:Complex}) = convert(Array{ComplexF32}, xs) +function Adapt.adapt_storage(::Type{<:Array{Float32}}, xs::AbstractArray{<:Complex}) + return convert(Array{ComplexF32}, xs) +end Adapt.adapt_storage(::Type{<:Array{Float32}}, x::Float64) = Float32(x) Adapt.adapt_storage(::Type{<:Array{Float32}}, x::ComplexF64) = ComplexF32(x) Adapt.adapt(T::Type{<:Array{Float32}}, x::AbstractThunk) = adapt(T, unthunk(x)) - """ @gpu test_rrule(sum, rand(3)) @@ -45,14 +48,14 @@ Used directly on a rule, this compares a CPU to a GPU run, without checking correctness. Both runs will convert numbers to `Float32` first. """ macro gpu(ex) - _gpu_macro(ex, false, __source__) + return _gpu_macro(ex, false, __source__) end macro gpu_broken(ex) - _gpu_macro(ex, true, __source__) -end + return _gpu_macro(ex, true, __source__) +end function _gpu_macro(ex, broken::Bool, __source__) Meta.isexpr(ex, :call) && ex.args[1] in (:test_rrule, :test_frule, :rrule, :frule) || - error("@gpu doesn't understand this input") + error("@gpu doesn't understand this input") # `@test` requires a Symbol, not a function (Julia ≥ 1.13.0-DEV.300) ex2 = if Meta.isexpr(ex.args[2], :parameters) :(_gpu_test($(ex.args[2]), $(ex.args[1]), $(ex.args[3:end]...))) @@ -60,19 +63,19 @@ function _gpu_macro(ex, broken::Bool, __source__) :(_gpu_test($(ex.args[1]), $(ex.args[2:end]...))) end return if broken - Expr(:block, ex, Expr(:macrocall, Symbol("@test_broken"), __source__, ex2)) |> esc + esc(Expr(:block, ex, Expr(:macrocall, Symbol("@test_broken"), __source__, ex2))) else - Expr(:block, ex, Expr(:macrocall, Symbol("@test"), __source__, ex2)) |> esc + esc(Expr(:block, ex, Expr(:macrocall, Symbol("@test"), __source__, ex2))) # NB this @test should report a line number in your tests, not here. end end -function _gpu_test(::typeof(test_rrule), xs...; fkwargs = (;), kw...) - _gpu_test(rrule, xs...; fkwargs...) +function _gpu_test(::typeof(test_rrule), xs...; fkwargs=(;), kw...) + return _gpu_test(rrule, xs...; fkwargs...) end -function _gpu_test(::typeof(test_frule), xs...; fkwargs = (;), kw...) - _gpu_test(frule, xs...; fkwargs...) +function _gpu_test(::typeof(test_frule), xs...; fkwargs=(;), kw...) + return _gpu_test(frule, xs...; fkwargs...) end function _gpu_test(::typeof(rrule), xs...; kw...) @@ -88,7 +91,7 @@ function _gpu_test(::typeof(rrule), xs...; kw...) agree = map(gpu_dxs, dxs) do a, b a isa AbstractZero && return b isa AbstractZero - isapprox(a, b) # compare on CPU to avoid error from e.g. `isapprox(jl(x), jl(permutedims(x))')` + return isapprox(a, b) # compare on CPU to avoid error from e.g. `isapprox(jl(x), jl(permutedims(x))')` end return all(agree) # NB this does not contain @test, it just returns true/false. Then it can be used with `@test_broken` without going mad. @@ -99,15 +102,15 @@ function _gpu_test(::typeof(frule), xdots, f::Function, xs...; kw...) @test eltype(y[2]) ∉ (Float64, ComplexF64) gpu_y = frule(jl32(xdots), f, jl32(xs)...; kw...) ChainRulesTestUtils.test_approx(gpu_y, jl32(y)) - true + return true end function _gpu_test(::typeof(frule), f::Function, xs...; kw...) xdots = (NoTangent(), xs...) # a pretty crude way to generate xdots for you - _gpu_test(frule, xdots, f, xs...; kw...) + return _gpu_test(frule, xdots, f, xs...; kw...) end function _gpu_test(::typeof(frule), f::Function, g::Function, xs...; kw...) # solves an ambiguity xdots = (NoTangent(), NoTangent(), xs...) - _gpu_test(frule, xdots, f, g, xs...; kw...) + return _gpu_test(frule, xdots, f, g, xs...; kw...) end const CFG = ChainRulesTestUtils.TestConfig() @@ -137,15 +140,12 @@ end (m::Multiplier)(y, z) = (m.x * y) * z function ChainRulesCore.rrule(m::Multiplier, y) - Multiplier_pullback(dΩ) = (Tangent{typeof(m)}(; x = dΩ * y'), m.x' * dΩ) + Multiplier_pullback(dΩ) = (Tangent{typeof(m)}(; x=dΩ * y'), m.x' * dΩ) return m(y), Multiplier_pullback end function ChainRulesCore.rrule(m::Multiplier, y, z) - Multiplier_pullback_3(dΩ) = ( - Tangent{typeof(m)}(; x = dΩ * (y * z)'), - m.x' * dΩ * z', - (m.x * y)' * dΩ, - ) + Multiplier_pullback_3(dΩ) = + (Tangent{typeof(m)}(; x=dΩ * (y * z)'), m.x' * dΩ * z', (m.x * y)' * dΩ) return m(y, z), Multiplier_pullback_3 end @@ -167,7 +167,8 @@ end (d::Divider)(y::Real) = (y / d.x)^2 function ChainRulesCore.rrule(d::Divider, y::Real) - Divider_pullback(dΩ) = (Tangent{typeof(d)}(; x = -2 * dΩ * y^2 / d.x^3), 2 * dΩ * y / d.x^2) + Divider_pullback(dΩ) = + (Tangent{typeof(d)}(; x=-2 * dΩ * y^2 / d.x^3), 2 * dΩ * y / d.x^2) return d(y), Divider_pullback end @@ -188,7 +189,7 @@ mutable struct Counter Counter(n=0) = new(n) end Base.:(==)(a::Counter, b::Counter) = (a.n == b.n) -(c::Counter)(x) = x * (c.n += 1) +(c::Counter)(x) = x * (c.n += 1) (c::Counter)(x, y) = (x + y) * (c.n += 1) function ChainRulesCore.rrule(c::Counter, x) @@ -202,7 +203,7 @@ function ChainRulesCore.rrule(c::Counter, x, y) end # NoRules - has no rules defined -struct NoRules; end +struct NoRules end "A function that outputs a vector from a scalar for testing" make_two_vec(x) = [x, x] From 322c1a82d2e76e396c4799ab0a771bf783d156f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20M=C3=BCller-Widmann?= Date: Fri, 16 Oct 2026 16:13:13 +0200 Subject: [PATCH 4/7] Upload coverage to Codecov Process coverage and upload it to Codecov in the CI and nightly workflows, as in ChainRulesCore.jl. This replaces the commented-out Coveralls/Codecov steps in CI and the Coveralls upload in the nightly workflow. Update the README badges: show Codecov coverage, point the CI badge to `main`, and drop the obsolete Travis badge. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/CI.yml | 26 ++++++-------------------- .github/workflows/JuliaNightly.yml | 19 ++++--------------- README.md | 5 ++--- 3 files changed, 12 insertions(+), 38 deletions(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 5cce86e1e..9fe53a14e 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -32,23 +32,9 @@ jobs: - uses: julia-actions/cache@v3 - uses: julia-actions/julia-buildpkg@v1 - uses: julia-actions/julia-runtest@v1 -# - uses: julia-actions/julia-processcoverage@v1 -# - uses: codecov/codecov-action@v1 -# with: -# file: lcov.info -# - uses: coverallsapp/github-action@master -# with: -# github-token: ${{ secrets.GITHUB_TOKEN }} -# flag-name: Julia ${{ matrix.version }} - ${{ matrix.os }} - ${{ matrix.arch }} -# parallel: true -# path-to-lcov: lcov.info -# -# finish: -# name: Coveralls Finished -# needs: test -# runs-on: ubuntu-latest -# steps: -# - uses: coverallsapp/github-action@master -# with: -# github-token: ${{ secrets.GITHUB_TOKEN }} -# parallel-finished: true + - uses: julia-actions/julia-processcoverage@v1 + - uses: codecov/codecov-action@v7 + with: + files: lcov.info + token: ${{ secrets.CODECOV_TOKEN }} + fail_ci_if_error: true diff --git a/.github/workflows/JuliaNightly.yml b/.github/workflows/JuliaNightly.yml index b425ab3b9..6f8d5279a 100644 --- a/.github/workflows/JuliaNightly.yml +++ b/.github/workflows/JuliaNightly.yml @@ -32,19 +32,8 @@ jobs: - uses: julia-actions/julia-buildpkg@v1 - uses: julia-actions/julia-runtest@v1 - uses: julia-actions/julia-processcoverage@v1 - - uses: coverallsapp/github-action@master + - uses: codecov/codecov-action@v7 with: - github-token: ${{ secrets.GITHUB_TOKEN }} - flag-name: Julia ${{ matrix.version }} - ${{ matrix.os }} - ${{ matrix.arch }} - parallel: true - path-to-lcov: lcov.info - - finish: - name: Coveralls Finished - needs: test - runs-on: ubuntu-latest - steps: - - uses: coverallsapp/github-action@master - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - parallel-finished: true + files: lcov.info + token: ${{ secrets.CODECOV_TOKEN }} + fail_ci_if_error: true diff --git a/README.md b/README.md index 093144f7a..056c0cb0a 100644 --- a/README.md +++ b/README.md @@ -2,9 +2,8 @@ # ChainRules -[![CI](https://github.com/JuliaDiff/ChainRules.jl/workflows/CI/badge.svg?branch=master)](https://github.com/JuliaDiff/ChainRules.jl/actions?query=workflow%3ACI) -[![Travis](https://travis-ci.org/JuliaDiff/ChainRules.jl.svg?branch=master)](https://travis-ci.org/JuliaDiff/ChainRules.jl) -[![Coveralls](https://coveralls.io/repos/github/JuliaDiff/ChainRules.jl/badge.svg?branch=master)](https://coveralls.io/github/JuliaDiff/ChainRules.jl?branch=master) +[![CI](https://github.com/JuliaDiff/ChainRules.jl/actions/workflows/CI.yml/badge.svg?branch=main)](https://github.com/JuliaDiff/ChainRules.jl/actions/workflows/CI.yml?query=branch%3Amain) +[![Coverage](https://codecov.io/gh/JuliaDiff/ChainRules.jl/branch/main/graph/badge.svg)](https://codecov.io/gh/JuliaDiff/ChainRules.jl) [![PkgEval](https://juliaci.github.io/NanosoldierReports/pkgeval_badges/C/ChainRules.svg)](https://juliaci.github.io/NanosoldierReports/pkgeval_badges/report.html) [![Code Style: Blue](https://img.shields.io/badge/code%20style-blue-4495d1.svg)](https://github.com/invenia/BlueStyle) [![ColPrac: Contributor's Guide on Collaborative Practices for Community Packages](https://img.shields.io/badge/ColPrac-Contributor's%20Guide-blueviolet)](https://github.com/SciML/ColPrac) From 5f1b2e2e213c30d575bd20b35ceac4e83c1f8b07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20M=C3=BCller-Widmann?= Date: Fri, 16 Oct 2026 21:07:59 +0200 Subject: [PATCH 5/7] =?UTF-8?q?Check=20depwarn=20rules=20and=20wider-?= =?UTF-8?q?=CE=B3=203-arg=20`*`=20gradients=20in=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - The `Base.depwarn` frule/rrule tests call `depwarn`, which emitted a deprecation warning into the test output; check it with `@test_deprecated` instead. - The comparisons of 3-arg `*` gradients for γ of a wider type were not wrapped in `@test`, so they were never checked. Co-Authored-By: Claude Opus 5.5 --- test/rulesets/Base/arraymath.jl | 12 ++++++------ test/rulesets/Base/base.jl | 6 ++++-- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/test/rulesets/Base/arraymath.jl b/test/rulesets/Base/arraymath.jl index d761e5cf0..eedc8b0f9 100644 --- a/test/rulesets/Base/arraymath.jl +++ b/test/rulesets/Base/arraymath.jl @@ -146,12 +146,12 @@ using ChainRules, ChainRulesCore, ChainRulesTestUtils, LinearAlgebra, Test # Test with γ of a wider type A, B, b, γ = rand(3, 3), rand(3, 3), rand(3), rand() dZ, dz = rand(3, 3), rand(3) - unthunk(rrule(mat_mat_scalar, A, B, γ + 0im)[2](dZ)[4]) ≈ - unthunk(rrule(mat_mat_scalar, A, B, γ)[2](dZ)[4]) - unthunk(rrule(mat_mat_scalar, A, B, 0 + 0im)[2](dZ)[4]) ≈ - unthunk(rrule(mat_mat_scalar, A, B, 0)[2](dZ)[4]) - unthunk(rrule(mat_vec_scalar, A, b, γ + 0im)[2](dz)[4]) ≈ - unthunk(rrule(mat_vec_scalar, A, b, γ)[2](dz)[4]) + @test unthunk(rrule(mat_mat_scalar, A, B, γ + 0im)[2](dZ)[4]) ≈ + unthunk(rrule(mat_mat_scalar, A, B, γ)[2](dZ)[4]) + @test unthunk(rrule(mat_mat_scalar, A, B, 0 + 0im)[2](dZ)[4]) ≈ + unthunk(rrule(mat_mat_scalar, A, B, 0)[2](dZ)[4]) + @test unthunk(rrule(mat_vec_scalar, A, b, γ + 0im)[2](dz)[4]) ≈ + unthunk(rrule(mat_vec_scalar, A, b, γ)[2](dz)[4]) end @testset "$f" for f in (/, \) diff --git a/test/rulesets/Base/base.jl b/test/rulesets/Base/base.jl index 98cfed334..e9d0fa3ef 100644 --- a/test/rulesets/Base/base.jl +++ b/test/rulesets/Base/base.jl @@ -222,8 +222,10 @@ end end @testset "Logging" begin - @test frule((NoTangent(), NoTangent(), NoTangent()), Base.depwarn, "message", :f) !== nothing - @test rrule(Base.depwarn, "message", :f) !== nothing + @test (@test_deprecated "message" frule( + (NoTangent(), NoTangent(), NoTangent()), Base.depwarn, "message", :f + )) !== nothing + @test (@test_deprecated "message" rrule(Base.depwarn, "message", :f)) !== nothing end if isdefined(Threads, :_spawn_set_thrpool) From 2e4cb285e5dc0f618c2f9ef14280bd8dd14bc4c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20M=C3=BCller-Widmann?= Date: Fri, 16 Oct 2026 21:09:55 +0200 Subject: [PATCH 6/7] Set workflow concurrency and cache permissions - Cancel superseded runs of the CI, nightly and integration test workflows on pull requests with `concurrency`, as in ChainRulesCore.jl, and remove the `Cancel` workflow that did this with `styfle/cancel-workflow-action`. - Grant `actions: write` (and only `contents: read` otherwise) in the workflows that use `julia-actions/cache`, so it can delete its old caches without relying on the repository's default token permissions. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/CI.yml | 11 +++++++++++ .github/workflows/Cancel.yml | 19 ------------------- .github/workflows/IntegrationTest.yml | 6 ++++++ .github/workflows/JuliaNightly.yml | 11 +++++++++++ 4 files changed, 28 insertions(+), 19 deletions(-) delete mode 100644 .github/workflows/Cancel.yml diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 9fe53a14e..e880367d8 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -5,6 +5,17 @@ on: tags: [v*] pull_request: +concurrency: + # Skip intermediate builds: always. + # Cancel intermediate builds: only if it is a pull request build. + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ startsWith(github.ref, 'refs/pull/') }} + +# needed to allow julia-actions/cache to delete old caches that it has created +permissions: + actions: write + contents: read + jobs: test: name: Julia ${{ matrix.version }} - ${{ matrix.os }} - ${{ matrix.arch }} diff --git a/.github/workflows/Cancel.yml b/.github/workflows/Cancel.yml deleted file mode 100644 index 911f53ce2..000000000 --- a/.github/workflows/Cancel.yml +++ /dev/null @@ -1,19 +0,0 @@ -name: Cancel - -on: - workflow_run: - workflows: - - "CI" - - "IntegrationTest" - - "JuliaNightly" - types: - - requested - -jobs: - cancel: - runs-on: ubuntu-latest - steps: - - uses: styfle/cancel-workflow-action@0.13.0 - with: - all_but_latest: true - workflow_id: ${{ github.event.workflow.id }} diff --git a/.github/workflows/IntegrationTest.yml b/.github/workflows/IntegrationTest.yml index c10a5ab71..c95c6df6a 100644 --- a/.github/workflows/IntegrationTest.yml +++ b/.github/workflows/IntegrationTest.yml @@ -5,6 +5,12 @@ on: tags: [v*] pull_request: +concurrency: + # Skip intermediate builds: always. + # Cancel intermediate builds: only if it is a pull request build. + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ startsWith(github.ref, 'refs/pull/') }} + jobs: test: name: ${{ matrix.package.repo }} diff --git a/.github/workflows/JuliaNightly.yml b/.github/workflows/JuliaNightly.yml index 6f8d5279a..b21799a57 100644 --- a/.github/workflows/JuliaNightly.yml +++ b/.github/workflows/JuliaNightly.yml @@ -8,6 +8,17 @@ on: schedule: - cron: "0 0 * * *" +concurrency: + # Skip intermediate builds: always. + # Cancel intermediate builds: only if it is a pull request build. + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ startsWith(github.ref, 'refs/pull/') }} + +# needed to allow julia-actions/cache to delete old caches that it has created +permissions: + actions: write + contents: read + jobs: test: name: ${{ matrix.os }} - ${{ matrix.arch }} From 20f8453b4f40f7604e9a365dd4d3d579157158ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20M=C3=BCller-Widmann?= Date: Sat, 17 Oct 2026 01:37:40 +0200 Subject: [PATCH 7/7] Skip non-finite power gradients in FastMath tests FastMath functions assume no NaN or Inf, so these results are undefined. Since LLVM 23 (JuliaLang/julia#63626) they are folded to arbitrary values, which broke the compiled FastMath POWERGRADS tests on nightly. Co-Authored-By: Claude Opus 5.5 --- test/rulesets/Base/fastmath_able.jl | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/test/rulesets/Base/fastmath_able.jl b/test/rulesets/Base/fastmath_able.jl index 9d7851e23..c411811a2 100644 --- a/test/rulesets/Base/fastmath_able.jl +++ b/test/rulesets/Base/fastmath_able.jl @@ -204,6 +204,10 @@ const FASTABLE_AST = quote (0.0, -1.5) => (-Inf, NaN), ] + # Non-finite results of FastMath functions are undefined + fastmath = (^) !== Base.:^ + isequal_or_undef(a, b) = (fastmath && !Base.isfinite(a)) || isequal(a, b) + @testset "$x ^ $p" for ((x,p), (∂x, ∂p)) in POWERGRADS if x isa Integer && p isa Integer && p < 0 @test_throws DomainError x^p @@ -213,27 +217,27 @@ const FASTABLE_AST = quote # Forward y_fwd = frule((1,1,1), ^, x, p)[1] - @test isequal(y, y_fwd) + @test isequal_or_undef(y, y_fwd) ∂x_fwd = frule((0,1,0), ^, x, p)[2] ∂p_fwd = frule((0,0,1), ^, x, p)[2] - @test isequal(∂x, ∂x_fwd) + @test isequal_or_undef(∂x, ∂x_fwd) if x===0.0 && p===0.5 @test_broken isequal(∂p, ∂p_fwd) else - @test isequal(∂p, ∂p_fwd) + @test isequal_or_undef(∂p, ∂p_fwd) end ∂x_fwd = frule((0,1,ZeroTangent()), ^, x, p)[2] # easier, strong zero - @test isequal(∂x, ∂x_fwd) + @test isequal_or_undef(∂x, ∂x_fwd) # Reverse y_rev = rrule(^, x, p)[1] - @test isequal(y, y_rev) + @test isequal_or_undef(y, y_rev) ∂x_rev, ∂p_rev = unthunk.(rrule(^, x, p)[2](1))[2:3] - @test isequal(∂x, ∂x_rev) - @test isequal(∂p, ∂p_rev) + @test isequal_or_undef(∂x, ∂x_rev) + @test isequal_or_undef(∂p, ∂p_rev) end end