Skip to content

Take is_inplaceable_destination seriously - #577

Merged
mcabbott merged 5 commits into
JuliaDiff:mainfrom
mcabbott:safe_destination
Aug 28, 2022
Merged

Take is_inplaceable_destination seriously#577
mcabbott merged 5 commits into
JuliaDiff:mainfrom
mcabbott:safe_destination

Conversation

@mcabbott

@mcabbottmcabbott commented Aug 25, 2022

Copy link
Copy Markdown
Member

To be useful, this function should return true only when it is certain that the array can be written into. A false false costs one allocation, but a false true will lead to an error. At present, it guesses wrong for wrappers like ReadOnlyArrays.

Commit c3bf4b0 is just fixing tests to pass on 1.9, could be pulled out.

@codecov-commenter

codecov-commenter commented Aug 25, 2022

Copy link
Copy Markdown

Codecov Report

Merging #577 (3d4acc8) into main (fbb4936) will increase coverage by 0.03%.
The diff coverage is 100.00%.

@@ Coverage Diff @@## main #577 +/- ##
==========================================
+ Coverage 93.15% 93.18% +0.03% 
==========================================
Files 15 15 Lines 891 895 +4 ==========================================
+ Hits 830 834 +4 
Misses 61 61 
Impacted FilesCoverage Δ
src/ChainRulesCore.jl100.00% <ø> (ø)
src/accumulation.jl97.22% <100.00%> (ø)
src/projection.jl97.34% <0.00%> (+0.04%)⬆️

Help us with your feedback. Take ten seconds to tell us how you rate us. Have a feature suggestion? Share it here.

Comment threadsrc/accumulation.jl Outdated
Comment on lines +55 to +56
is_inplaceable_destination(::DenseArray)= true
is_inplaceable_destination(::DenseArray{<:Integer}) = false

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The intention was to allow for CuArrays. But perhaps this is unsafe, as:

julia> Base.Experimental.Const([1,2]) isa DenseArray
true
julia> subtypes(DenseArray)
13-element Vector{Any}:
Array
Base.CodeUnits
Base.Experimental.Const
CUDA.Const
CUDA.CuDeviceArray
GPUArrays.AbstractDeviceArray
GPUArraysCore.AbstractGPUArray
Metal.Const
Metal.MtlLargerDeviceArray
MtlDeviceArray
Random.UnsafeView
SharedArrays.SharedArray
SparseArrays.CHOLMOD.Dense
Suggested change
is_inplaceable_destination(::DenseArray)=true
is_inplaceable_destination(::DenseArray{<:Integer}) =false
is_inplaceable_destination(::Array)=true
is_inplaceable_destination(::Array{<:Integer}) =false

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or perhaps:

Suggested change
is_inplaceable_destination(::DenseArray)=true
is_inplaceable_destination(::DenseArray{<:Integer}) =false
is_inplaceable_destination(x::DenseArray)=ismutable(x)
is_inplaceable_destination(x::DenseArray{<:Integer}) =false

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think to be truly safe we much only accept Array
Since anyone can subtype DenseArray, and overload setindex however they want

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A pity to miss CuArrays. (Although previous version also marks them non-writable.) But agree that if the goal is always to fail on the safe side, then is has to be Array.

@mzgubicmzgubic left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As far as I understand this PR will make some things work, and other things silently slower, because it will make them not inplaceable. Do we know how big the performance drop will be, and how widespread it will be? I imagine it should be fine as long as we cover most commonly used types here?

Comment threadsrc/accumulation.jl Outdated
Co-authored-by: Miha Zgubic <mzgubic@users.noreply.github.com>
@mcabbott

Copy link
Copy Markdown
MemberAuthor

The cost is like copy(x). I don't know how widely used this is.

I think its real intended use is for InplaceabeThunk which, as far as I know, is not in use at all, since + always un-thunks. Are there any known users of that?

The goal is more to make this function safe to use in code which may meet unknown types. For this it needs to fail to the side which doesn't error, that's much more important than guessing correctly more often. Some discussion of such use here: FluxML/Optimisers.jl#99

Instead of changing this function, it could be deprecated & replaced.

@mcabbott

Copy link
Copy Markdown
MemberAuthor

There is also a question of whether it treats eltypes and structure correctly. The present one is documented to allow Diagonal(x) .+= Diagonal(y), with the update having the same structure. A two-argument ok_to_add!(x, y) could check for you, but that won't help InplaceabeThunk... xref #393 I guess.

The PR makes it return false on integers arrays, as you can't safely write a gradient into such a thing. But it won't stop you trying to write a complex array into a real one.

@mcabbottmcabbott added the inplace accumulation for things relating to inplace accumulation of gradients label Aug 25, 2022
@oxinabox

Copy link
Copy Markdown
Member

You know if you made this PR from a branch of this repo, rather than from your fork, you could just click accept on all the nagging from the autoformatter?

@oxinabox

Copy link
Copy Markdown
Member

I think its real intended use is for InplaceabeThunk which, as far as I know, is not in use at all, since + always un-thunks. Are there any known users of that?

The intended use is for in add!!.
Which Nabla does use for accumulation.
But Noone uses the latest release of Nabla (its a bit fragile TBH).

I think this will have no performance impacts on anything real

Comment threadsrc/accumulation.jl Outdated
Comment threadsrc/accumulation.jl
Comment on lines +62 to +64
alpha = is_inplaceable_destination(parent(x))
beta = x.indices isa Tuple{Vararg{ Union{Integer, Base.Slice, UnitRange}}}
return alpha && beta

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it clearer to do

Suggested change
alpha =is_inplaceable_destination(parent(x))
beta = x.indices isa Tuple{Vararg{ Union{Integer, Base.Slice, UnitRange}}}
return alpha && beta
is_inplaceable_destination(parent(x)) ||return falsse
return x.indices isa Tuple{Vararg{Union{Integer,Base.Slice,UnitRange}}}

I will trust your judgement

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had a few variants. Maybe I don't like that this seems to imply you ought to check one before the other, but in fact it's just dealing with line length.

Comment threadsrc/accumulation.jl
@eval is_inplaceable_destination(x::Base.$T) = is_inplaceable_destination(parent(x))
end
for T in [:Adjoint, :Transpose, :Diagonal, :UpperTriangular, :LowerTriangular]
@eval is_inplaceable_destination(x::LinearAlgebra.$T) = is_inplaceable_destination(parent(x))

@oxinaboxoxinaboxAug 26, 2022

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

a bit sad that this won't catch NamedDimsArrays or KeyedArrays for free anymore.
But those need to overload ProjectTo anyway so its fine to make them do this

That wrapper types in Base is most of what we need to deal with

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Besides having something you have to opt into, the other approach I can think of would be to ask type inference. But... this doesn't seem like the package's style:

julia> Core.Compiler._return_type(setindex!, typeof(([1,2,3.0], 4.0, 2)))
Vector{Float64} (alias for Array{Float64, 1})
julia> Core.Compiler._return_type(setindex!, typeof((SA[1,2,3.0], 4.0, 2)))
Union{}
julia> Core.Compiler._return_type(setindex!, typeof((1:3.0, 4.0, 2)))
Union{}
julia> which(setindex!, typeof((SA[1,2,3.0], 4.0, 2))) # their own error
setindex!(a::StaticArray, value, i::Int64)
@ StaticArrays ~/.julia/packages/StaticArrays/6QFsp/src/indexing.jl:3
julia> which(setindex!, typeof((1:3.0, 4.0, 2))) # not useful
setindex!(A::AbstractArray, v, I...)
@ Base abstractarray.jl:1374

Comment threadsrc/accumulation.jl

@oxinaboxoxinabox left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sensible reallly.
Only handle that which we know about.

Address remaining comments and then merge when happy

Comment threadsrc/accumulation.jl
Co-authored-by: Frames Catherine White <oxinabox@ucc.asn.au>
Comment on lines 111 to 117
# TODO: uncomment this once https://github.com/JuliaLang/julia/issues/35516
if VERSION >= v"1.8-"
@test haskey(Tangent{Tuple{Float64}}(2.0), 1) == true
else
@test_broken haskey(Tangent{Tuple{Float64}}(2.0), 1) == true
end
# if VERSION >= v"1.8-"
# @test haskey(Tangent{Tuple{Float64}}(2.0), 1) == true
# else
# @test_broken haskey(Tangent{Tuple{Float64}}(2.0), 1) == true
# end
@test_broken hasproperty(Tangent{Tuple{Float64}}(2.0), 2) == false

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is unrelated, but was needed to make tests pass on 1.9. I don't know why it says "uncomment" but now that things are commented, it may make sense.

@test_throws MethodError reverse(Tangent{Foo}(; x=1.0, y=2.0))
if VERSION < v"1.9-"
# can't reverse a named tuple or a dict
@test_throws MethodError reverse(Tangent{Foo}(; x=1.0, y=2.0))

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Likewise needed for 1.9

This tests that you can't reverse a Tangent, but do we care that you can't? We know that it's meaningless, like the order of fields in a struct. But if it used to be an error and now works, why is that a failure?

Comment threadsrc/accumulation.jl Outdated
Comment threadsrc/accumulation.jl Outdated
Comment threadsrc/accumulation.jl Outdated
@mcabbott
mcabbott merged commit aba2fcb into JuliaDiff:mainAug 28, 2022
@mcabbott
mcabbott deleted the safe_destination branch August 28, 2022 06:18
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

inplace accumulationfor things relating to inplace accumulation of gradients

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@mcabbott@codecov-commenter@oxinabox@mzgubic