Uh oh!
There was an error while loading. Please reload this page.
Do not seed structural zeros - #739
Conversation
| @testset "LowerTriangular and UpperTriangular" begin | ||
| M = rand(3, 3) | ||
| for T in (LowerTriangular, UpperTriangular) | ||
| @test ForwardDiff.gradient(sum, T(randn(3, 3))) == T(ones(3, 3)) |
There was a problem hiding this comment.
Unfortunately this fails if I increase the size of the matrix above the chunk size:
julia>@testset"LowerTriangular and UpperTriangular"begin
M =rand(3, 3)
for T in (LowerTriangular, UpperTriangular)
@test ForwardDiff.gradient(sum, T(randn(3, 3))) ==T(ones(3, 3))
@test ForwardDiff.gradient(x ->dot(M, x), T(randn(3, 3))) ==T(M)
endend
Test Summary:| Pass Total Time
LowerTriangular and UpperTriangular |441.0s
Test.DefaultTestSet("LowerTriangular and UpperTriangular", Any[], 4, false, false, true, 1.74360103628159e9, 1.743601037319704e9, false, "REPL[8]")
julia>@testset"LowerTriangular and UpperTriangular"begin
M =rand(10, 10)
for T in (LowerTriangular, UpperTriangular)
@test ForwardDiff.gradient(sum, T(randn(10, 10))) ==T(ones(10, 10))
@test ForwardDiff.gradient(x ->dot(M, x), T(randn(10, 10))) ==T(M)
endend
LowerTriangular and UpperTriangular: Error During Test at REPL[9]:4
Test threw exception
Expression: ForwardDiff.gradient(sum, T(randn(10, 10))) ==T(ones(10, 10))
ArgumentError: cannot set index in the upper triangular part (1, 2) of an LowerTriangular matrix to a nonzero value (Dual{ForwardDiff.Tag{typeof(sum), Float64}}(0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,1.0,0.0))
Stacktrace:
[1] throw_nonzeroerror(T::Type, x::Any, i::Int64, j::Int64)
@ LinearAlgebra ~/.julia/juliaup/julia-1.11.4+0.aarch64.apple.darwin14/share/julia/stdlib/v1.11/LinearAlgebra/src/triangular.jl:295
[2] setindex!
@ ~/.julia/juliaup/julia-1.11.4+0.aarch64.apple.darwin14/share/julia/stdlib/v1.11/LinearAlgebra/src/triangular.jl:326 [inlined]
[3] _unsafe_setindex!
@ ./reshapedarray.jl:297 [inlined]
[4] setindex!
@ ./reshapedarray.jl:286 [inlined]
[5] setindex!
@ ./subarray.jl:372 [inlined]
[6] macro expansion
@ ./broadcast.jl:973 [inlined]
[7] macro expansion
@ ./simdloop.jl:77 [inlined]
[8] copyto!
@ ./broadcast.jl:972 [inlined]
[9] copyto!
@ ./broadcast.jl:925 [inlined]
[10] materialize!
@ ./broadcast.jl:883 [inlined]
[11] materialize!
@ ./broadcast.jl:880 [inlined]
[12] seed!(duals::LowerTriangular{ForwardDiff.Dual{ForwardDiff.Tag{typeof(sum), Float64}, Float64, 12}, Matrix{ForwardDiff.Dual{ForwardDiff.Tag{typeof(sum), Float64}, Float64, 12}}}, x::LowerTriangular{Float64, Matrix{Float64}}, index::Int64, seeds::NTuple{12, ForwardDiff.Partials{12, Float64}}, chunksize::Int64)
@ ForwardDiff ~/.julia/packages/ForwardDiff/L0kjR/src/apiutils.jl:69
[13] seed!
@ ~/.julia/packages/ForwardDiff/L0kjR/src/apiutils.jl:66 [inlined]
[14] chunk_mode_gradient(f::typeof(sum), x::LowerTriangular{Float64, Matrix{Float64}}, cfg::ForwardDiff.GradientConfig{ForwardDiff.Tag{typeof(sum), Float64}, Float64, 12, LowerTriangular{ForwardDiff.Dual{ForwardDiff.Tag{typeof(sum), Float64}, Float64, 12}, Matrix{ForwardDiff.Dual{ForwardDiff.Tag{typeof(sum), Float64}, Float64, 12}}}})There was a problem hiding this comment.
Another catch is: This PR doesn't (yet) fix the fact that still for every element of the matrices seeds are generated - even though (IMO) we should limit it to the structurally non-zero entries.
There was a problem hiding this comment.
I have forgotten how all this code works... but 5-arg seed! implies the iteration is happening elsewhere?
The other thing worth checking would be GradientConfig and DiffResult things from https://juliadiff.org/ForwardDiff.jl/stable/user/advanced/ as I'm not sure what paths they take.
There was a problem hiding this comment.
Yeah, seed! (and extract_gradient_chunk!) with > 3 arguments are called if gradients are computed in multiple chunks:
ForwardDiff.jl/src/gradient.jl
Lines 118 to 142 in fd2d4a9
There was a problem hiding this comment.
Those look good. Some chance this is flawed copy-pasting at my end, but for chunk mode I see strange effects:
julia> ForwardDiff.gradient(x ->begin@show(eltype(x)); sum(x) end, UpperTriangular(rand(5, 5)))
eltype(x) = ForwardDiff.Dual{ForwardDiff.Tag{var"#27#28", Float64}, Float64, 8}
eltype(x) = ForwardDiff.Dual{ForwardDiff.Tag{var"#27#28", Float64}, Float64, 8}
eltype(x) = ForwardDiff.Dual{ForwardDiff.Tag{var"#27#28", Float64}, Float64, 8}
eltype(x) = ForwardDiff.Dual{ForwardDiff.Tag{var"#27#28", Float64}, Float64, 8}
5×5 UpperTriangular{Float64, Matrix{Float64}}:1.01.01.01.01.0⋅1.01.01.01.0⋅⋅1.01.01.0⋅⋅⋅1.01.0⋅⋅⋅⋅1.0
julia>sum(ans)
15.0
julia> ForwardDiff.gradient(x ->begin@show(eltype(x)); sum(x) end, Diagonal(rand(11))); # okeltype(x) = ForwardDiff.Dual{ForwardDiff.Tag{var"#21#22", Float64}, Float64, 11}
julia> ForwardDiff.gradient(x ->begin@show(eltype(x)); sum(x) end, Diagonal(rand(13))); # weirdeltype(x) = ForwardDiff.Dual{ForwardDiff.Tag{var"#23#24", Float64}, Float64, 7}
eltype(x) = ForwardDiff.Dual{ForwardDiff.Tag{var"#23#24", Float64}, Float64, 7}
eltype(x) = ForwardDiff.Dual{ForwardDiff.Tag{var"#23#24", Float64}, Float64, 7}
[....>15 more]There was a problem hiding this comment.
Hmm, strange. I get
julia> ForwardDiff.gradient(x ->begin@show(eltype(x)); sum(x) end, UpperTriangular(rand(5, 5)))
eltype(x) = ForwardDiff.Dual{ForwardDiff.Tag{var"#40#41", Float64}, Float64, 8}
eltype(x) = ForwardDiff.Dual{ForwardDiff.Tag{var"#40#41", Float64}, Float64, 8}
5×5 UpperTriangular{Float64, Matrix{Float64}}:1.01.01.01.01.0⋅1.01.01.01.0⋅⋅1.01.01.0⋅⋅⋅1.01.0⋅⋅⋅⋅1.0
julia>sum(ans)
15.0
julia> ForwardDiff.gradient(x ->begin@show(eltype(x)); sum(x) end, Diagonal(rand(11)));
eltype(x) = ForwardDiff.Dual{ForwardDiff.Tag{var"#42#43", Float64}, Float64, 11}
julia> ForwardDiff.gradient(x ->begin@show(eltype(x)); sum(x) end, Diagonal(rand(13)));
eltype(x) = ForwardDiff.Dual{ForwardDiff.Tag{var"#44#45", Float64}, Float64, 7}
eltype(x) = ForwardDiff.Dual{ForwardDiff.Tag{var"#44#45", Float64}, Float64, 7}which seems expected given
Lines 10 to 13 in fd2d4a9
There was a problem hiding this comment.
The only test failures are the ones already present on the master branch (possibly related to some recent changes of acosh or NaNMath.acosh?).
There was a problem hiding this comment.
My weird results above must have been my mistake, lazy to check out the branch...
We could add tests of how many executions are used in chunk mode (not sure whether there are any such now) but not essential.
LowerTriangular and UpperTriangular input… init Keeps the O(n^2/N) fix from the previous commits, but revises the API around it. Rename `unseed!` to `seed_zero_partials!`. Of the 14 call sites the rename touched, 10 are initializing a work buffer that was never seeded -- `cfg.duals` is raw `similar` memory, and `ydual` in the `f!` paths never carried a perturbation -- and only 4 are clearing a chunk. `unseed!` names the minority case and asserts a prior state that usually does not exist; `seed_zero_partials!` names what the function does (write `x`'s values, zero the perturbations), which holds at every site. Give the windowed method an explicit `count = N` rather than an implicit `take(..., N)`, mirroring the `chunksize = N` argument that `seed!(duals, x, index, seeds, chunksize)` already has, so the seeding and zeroing paths take the same window arguments. Factor the shared loop into `_seed_zero_partials!`. The two public methods differed only in the index iterator they pass. Initialize the chunk-mode work buffer as a disjoint partition: seed chunk 1, then zero only the untouched tail (`N + 1`, `xlen - N`), instead of clearing the whole array and overwriting the first N elements. `xlen > N` always holds in chunk mode, since `chunksize(cfg) == structural_length(x)` routes to vector mode, so the two windows are disjoint and together cover `1:xlen` -- the buffer is still fully written before the first `f` call, which is what makes `cfg` reuse safe. This also lets the four `work_array_definition` quotes in jacobian.jl drop to just unpacking `cfg.duals`, removing a line duplicated 4x, and gives `count` an in-package caller. Tests. Add test/SeedTest.jl: the windowed clear is only ever called on a chunk that was just seeded, so clearing too much is harmless and no test written against the public API can tell a bounded implementation from an unbounded one -- which is why this regression survived since JuliaDiff#739. The new tests pin the window directly, and were validated by mutation: restoring the unbounded sweep fails 4 of them. Expected structural index sets are written out by hand and `structural_eachindex` is pinned to them once, so a bug in that iterator cannot hide inside the assertions that depend on it; `values_match` compares over `eachindex(x)` for the same reason. Extend the allocation test with the 4-arg form, where `count` is a runtime value and so catches an inference regression at the `_seed_zero_partials!` boundary that the `count`-defaulting forms could hide. Consolidate the second BigFloat block in test/JacobianTest.jl into a loop over the position of the unassigned entry. Existing coverage only ever placed it in the last chunk, which is never cleared, leaving the `Base._unsetindex!` branch of the windowed path unreached; `hole = 5` puts it in a middle chunk. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… the config Since #739 only the structurally non-zero entries of an input are seeded, but extraction was not updated to match, so the derivatives were written to positions taken from the result container instead of from `x`. `extract_gradient!`/`extract_gradient_chunk!` now take the positions of `x`. Entries that receive no derivative are zeroed, which is their derivative, and the sweep does that once up front: those entries -- the ones belonging to the structural zeros of `x` -- belong to no chunk in particular. The `DiffResult` method splits on mutability, since an immutable result cannot be written to entry by entry, and it dispatches on `DiffResult` rather than `MutableDiffResult`, because a `StaticArray` gradient buffer makes the result immutable even when the buffer itself can be written to entry by entry, as an `MVector` can. Fixes#838, where a dense result got the derivatives at linear positions `1:structural_length(x)` and a `DiffResults.GradientResult` threw, and with it the mis-scattered gradient of `hessian!(::DiffResult, ...)`. The Jacobian is indexed by the linear indices of `x`: column `j` holds `∂f(x)[i]/∂x[j]`, as documented, with hard zeros in the columns of the structural zeros. Its allocations therefore use `length(x)` rather than `structural_length(x)`, which is what `reshape_jacobian` expected all along, so chunk mode stops throwing. Fixes#839. This changes the shape of the result for structured inputs: `jacobian` gains the zero columns and `hessian` inherits both conventions through `jacobian(∇f, x)`, becoming `length(x) x length(x)` with hard-zero rows and columns instead of mixing linear and structural indices. That also makes `hessian!(DiffResults.HessianResult(x), ...)` work. For a `Diagonal` the result now scales with `length(x)`, so differentiating with respect to the diagonal vector is the better choice there. `reshape_jacobian` keeps the short-circuit `extract_jacobian!` was given in #797, with an explicit size check in place of the one `reshape` performed on the way past: since 1.11 `reshape` can no longer return its argument, so every chunk-mode `jacobian!` allocated an `Array` wrapper. Both modes now reject a wrongly shaped matrix result with the same error. Also drops the `map!` that `vector_mode_jacobian(f!, ...)` ran before `extract_jacobian!`, which reads only `ydual`, and that the `map!` after it repeats. Seeding, meanwhile, took the positions to seed from the config's work buffer while extraction took them from `x`, so reusing a config with an input of the same length and a different structure seeded one set of positions and extracted another -- and left the buffer entries outside the input's structure uninitialized for the target function to read. Fixes#842. `structural_eachindex` and `structural_columns` are replaced by `structural_indices`, which returns the linear indices of the seeded entries in seeding order, and each config stores one per work buffer it owns, built from the buffer rather than from `x`. Every API entry then calls `checkstructure` next to `checktag`: it compares `structural_kind`, which is O(1) and a compile-time constant, so it can run unconditionally, and no size or count comparison could replace it -- `LowerTriangular(n, n)` and `UpperTriangular(n, n)` agree on `size` and on `structural_length` alike. Storing the positions also removes the `Iterators.drop` walk that re-traversed them from the front to reach each chunk, three times per gradient chunk. That walk was 35-49% of `gradient!` for an `UpperTriangular` input, and dense inputs paid it too, `Iterators.drop` having no range specialization: `gradient!` is now 1.4-1.9x faster and `jacobian!` 1.2-1.5x, dense and structured alike. Because the positions are linear indices, the Jacobian's structured and dense extraction paths collapse into the one fused broadcast the dense path already used, and neither chunk extractor takes `x` any more. The unassigned-entry branch of seeding is fixed with them, the second half of `CartesianIndex` method at all and whose `AbstractArray` fallback for a linear index recurses forever. `adjoint`, `transpose`, `PermutedDimsArray` and `view` inputs work now, their buffers being plain `Array`s, and the three wrappers `similar` preserves raise an `ArgumentError` naming the entry instead of a `MethodError` or a `StackOverflowError`, since only an `Array` can hold an unassigned entry at all. The new config type parameter is not shimmed: code that pins the parameter count of these types has to be updated. A config for an offset-indexed input now throws at construction rather than at the first `gradient`/`jacobian` call. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… the config Since #739 only the structurally non-zero entries of an input are seeded, but extraction was not updated to match, so the derivatives were written to positions taken from the result container instead of from `x`. `extract_gradient!`/`extract_gradient_chunk!` now take the positions of `x`. Entries that receive no derivative are zeroed, which is their derivative, and the sweep does that once up front: those entries -- the ones belonging to the structural zeros of `x` -- belong to no chunk in particular. The `DiffResult` method splits on mutability, since an immutable result cannot be written to entry by entry, and it dispatches on `DiffResult` rather than `MutableDiffResult`, because a `StaticArray` gradient buffer makes the result immutable even when the buffer itself can be written to entry by entry, as an `MVector` can. Fixes#838, where a dense result got the derivatives at linear positions `1:structural_length(x)` and a `DiffResults.GradientResult` threw, and with it the mis-scattered gradient of `hessian!(::DiffResult, ...)`. The Jacobian is indexed by the linear indices of `x`: column `j` holds `∂f(x)[i]/∂x[j]`, as documented, with hard zeros in the columns of the structural zeros. Its allocations therefore use `length(x)` rather than `structural_length(x)`, which is what `reshape_jacobian` expected all along, so chunk mode stops throwing. Fixes#839. This changes the shape of the result for structured inputs: `jacobian` gains the zero columns and `hessian` inherits both conventions through `jacobian(∇f, x)`, becoming `length(x) x length(x)` with hard-zero rows and columns instead of mixing linear and structural indices. That also makes `hessian!(DiffResults.HessianResult(x), ...)` work. For a `Diagonal` the result now scales with `length(x)`, so differentiating with respect to the diagonal vector is the better choice there. `reshape_jacobian` keeps the short-circuit `extract_jacobian!` was given in #797, with an explicit size check in place of the one `reshape` performed on the way past: since 1.12 `reshape` can no longer return its argument, so every chunk-mode `jacobian!` allocated an `Array` wrapper. Both modes now reject a wrongly shaped matrix result with the same error. Also drops the `map!` that `vector_mode_jacobian(f!, ...)` ran before `extract_jacobian!`, which reads only `ydual`, and that the `map!` after it repeats. Seeding, meanwhile, took the positions to seed from the config's work buffer while extraction took them from `x`, so reusing a config with an input of the same length and a different structure seeded one set of positions and extracted another -- and left the buffer entries outside the input's structure uninitialized for the target function to read. Fixes#842. `structural_eachindex` and `structural_columns` are replaced by `structural_indices`, which returns the linear indices of the seeded entries in seeding order, and each config stores one per work buffer it owns, built from the buffer rather than from `x`. Every API entry then calls `checkstructure` next to `checktag`: it compares `structural_kind`, which is O(1) and a compile-time constant, so it can run unconditionally, and no size or count comparison could replace it -- `LowerTriangular(n, n)` and `UpperTriangular(n, n)` agree on `size` and on `structural_length` alike. Storing the positions also removes the `Iterators.drop` walk that re-traversed them from the front to reach each chunk, three times per gradient chunk. That walk was 35-49% of `gradient!` for an `UpperTriangular` input, and dense inputs paid it too, `Iterators.drop` having no range specialization: `gradient!` is now 1.4-1.9x faster and `jacobian!` 1.2-1.5x, dense and structured alike. Because the positions are linear indices, the Jacobian's structured and dense extraction paths collapse into the one fused broadcast the dense path already used, and neither chunk extractor takes `x` any more. The unassigned-entry branch of seeding is fixed with them, the second half of #842: it called `Base._unsetindex!(duals, idx)`, for which Base has no `CartesianIndex` method at all and whose `AbstractArray` fallback for a linear index recurses forever. `adjoint`, `transpose`, `PermutedDimsArray` and `view` inputs work now, their buffers being plain `Array`s, and the three wrappers `similar` preserves raise an `ArgumentError` naming the entry instead of a `MethodError` or a `StackOverflowError`, since only an `Array` can hold an unassigned entry at all. The new config type parameter is not shimmed: code that pins the parameter count of these types has to be updated. A config for an offset-indexed input now throws at construction rather than at the first `gradient`/`jacobian` call. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Fixes#738.
Since I had made #695 I felt somewhat responsible for #738. Defining
seed!andextract_gradient!for triangular matrices seems to fix the example.Probably this should b extended to additional matrix types.