Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Deprecate defaulting to scalar in broadcast (take 2) #26435

Merged
merged 3 commits into from Mar 22, 2018
Merged

Conversation

mbauman
Copy link
Sponsor Member

@mbauman mbauman commented Mar 13, 2018

This PR replaces #26212. The end result is the same, but I like this implementation much better.

This PR consists of two commits — both pass tests locally and I'd like to keep them separate. The first commit changes the default behavior of broadcast to collect its arguments instead of treating them as scalars. Broadcast now calls a special helper function, broadcastable, on each argument. It ensures that the arguments support indexing and have a shape, or otherwise have defined a custom BroadcastStyle. The second commit layers on top a deprecation mechanism that makes this — almost entirely — non-breaking.

I believe this to be a compromise that will fix #18618 once deprecations are removed. Note that I kept all the types that are currently tested to behave as scalars as specializing broadcastable to return something that supports both indexing and axes, or otherwise has its own BroadcastStyle and internal implementation.

This also changes the behavior of broadcast such that if it ever wants to return a 0-dimensional array, it'll just "unwrap" that 0-dimensional array and return the contents instead.

Reasons I like this approach better than #26212:

  • We're going to be requiring folks to explicitly wrap things in a Ref to make them broadcast as scalars. In RFC: Deprecate defaulting to scalar in broadcast #26212, adding a Ref to an otherwise scalar expression would result in a 0-dimensional array as a result. Yes, I generally have a very strong aversion to these kinds of auto-wrapping/unwrapping, but in this case I believe it is warranted and ends up with fewer special cases. 0-dimensional arrays and "scalar" types are treated identically as far as broadcast is concerned.
  • Having an explicit broadcastable psuedo-conversion method makes it easier for other code to "work-around" broadcast and know how it'll behave. Call broadcastable on a thing and then you'll be able to know what broadcast is going to do on the basis of its axes. No digging around in BroadcastStyles or other internals — this one function makes sure that iterables and everything else can just work. (This isn't quite true, yet, as Ref isn't fully conforming with the iterable and axes spec, but I'd like to implement that separately.)
  • It's so much simpler to not need to worry about Broadcast.Scalar() styles.

@mbauman mbauman added domain:arrays [a, r, r, a, y, s] kind:deprecation This change introduces or involves a deprecation broadcast labels Mar 13, 2018
@mbauman mbauman added this to the 1.0 milestone Mar 13, 2018
@stevengj
Copy link
Member

Does wrapping all scalars in a Ref affect performance?

@mbauman
Copy link
Sponsor Member Author

mbauman commented Mar 13, 2018

Numbers have both axes and indexing, so they don't need to be wrapped in a Ref. But even if you do wrap them in a ref, they're immediately unwrapped again (if all other arguments are 0-dim or similarly "scalar").

julia> f(a, b) = Ref(a) .+ Ref(b)
       g(a, b) = a .+ b
g (generic function with 1 method)

julia> @code_llvm f(1,2)

; Function f
; Location: REPL[7]:1
define i64 @julia_f_34293(i64, i64) {
top:
; Function broadcast; {
; Location: broadcast.jl:636
; Function broadcast; {
; Location: broadcast.jl:640
; Function +; {
; Location: int.jl:53
  %2 = add i64 %1, %0
;}}}
  ret i64 %2
}

julia> @code_llvm g(1,2)

; Function g
; Location: REPL[7]:2
define i64 @julia_g_34294(i64, i64) {
top:
; Function broadcast; {
; Location: broadcast.jl:228
; Function +; {
; Location: int.jl:53
  %2 = add i64 %1, %0
;}}
  ret i64 %2
}

Copy link
Sponsor Member

@vchuravy vchuravy left a comment

Choose a reason for hiding this comment

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

I like this direction. Automatically unwrapping Ref is technically a breaking change, but I don't see a clean way of deprecating this. Maybe add a deprecation warning to broadcastable(::Ref)?

"""
struct DefaultArrayStyle{N} <: AbstractArrayStyle{N} end
(::Type{<:DefaultArrayStyle})(::Val{N}) where N = DefaultArrayStyle{N}()
const DefaultVectorStyle = DefaultArrayStyle{1}
const DefaultMatrixStyle = DefaultArrayStyle{2}
BroadcastStyle(::Type{<:AbstractArray{T,N}}) where {T,N} = DefaultArrayStyle{N}()
BroadcastStyle(::Type{<:Ref}) = DefaultArrayStyle{0}()
BroadcastStyle(::Type{<:Union{Base.RefValue,Number}}) = DefaultArrayStyle{0}()
Copy link
Sponsor Member

Choose a reason for hiding this comment

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

This also changes RefArray (which is a good thing). Should there be a BroadcastStyle for RefArray?

Copy link
Sponsor Member

Choose a reason for hiding this comment

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

Why is that a good thing (I don't actually know what this return value means)? For context, RefArray is a pointer to precisely one element of an array. It also changes Ptr, if that matters?

Copy link
Sponsor Member Author

@mbauman mbauman Mar 13, 2018

Choose a reason for hiding this comment

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

All arguments' BroadcastStyles are combined with a promote-like mechanism in order to select a broadcasting implementation. For the most part, it's largely used as a kludge around the lack of a "at least one vararg is of type T" dispatch, but it's more complicated to deal with cases that would otherwise be ambiguities.

Yes, Ptr has neither axes or indexing defined, so it cannot be — by itself — considered like a 0-dimensional array. I don't think RefArray is common enough to add special broadcasting support for it.

Before:

julia> identity.(Ref([1,2,3],1))
0-dimensional Array{Int64,0}:
1

After (edit: now out of date):

julia> identity.(Ref([1,2,3],1))
┌ Warning: broadcast will default to iterating over its arguments in the future. Wrap arguments of
│ type `x::Base.RefArray{Int64,Array{Int64,1},Nothing}` with `Ref(x)` to ensure they broadcast as "scalar" elements.
│   caller = top-level scope
└ @ Core :0
Base.RefArray{Int64,Array{Int64,1},Nothing}([1, 2, 3], 1, nothing)

In the future, it will be an error because:

julia> collect(Ref([1,2,3],1))
ERROR: MethodError: no method matching length(::Base.RefArray{Int64,Array{Int64,1},Nothing})

Copy link
Sponsor Member

Choose a reason for hiding this comment

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

I don't think RefArray is common enough to add special broadcasting support for it.

However, RefValue is an implementation detail, and should never be mentioned explicitly either: RefArray is simply an alternative implementation of it. We know that length(::Ref) == 1 forall Ref subtypes, we've just hesitated to add that definition to the code until needed to keep the API small.

Copy link
Sponsor Member Author

Choose a reason for hiding this comment

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

This is largely a historical accident due to how I ended up here… and trying to work around Ptr. As it stands, I can easily remove all uses of RefValue. I just pushed a change.

New after:

julia> identity.(Ref([1,2,3],1))
1

if Base.Broadcast.BroadcastStyle(typeof(x)) isa Broadcast.Unknown
depwarn("""
broadcast will default to iterating over its arguments in the future. Wrap arguments of
type `x::$(typeof(x))` with `Ref(x)` to ensure they broadcast as "scalar" elements.
Copy link
Sponsor Member

Choose a reason for hiding this comment

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

Maybe add:

or define broadcastable(x::MyType) = Ref(x) for the conversion to be automatic.

Copy link
Sponsor Member Author

Choose a reason for hiding this comment

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

I'm guessing that the most common reader of this deprecation will be an end-user.

broadcastable(x::AbstractArray) = x
# In the future, default to collecting arguments. TODO: uncomment once deprecations are removed
# broadcastable(x) = BroadcastStyle(typeof(x)) isa Unknown ? collect(x) : x
# broadcastable(::Dict) = error("intentionally unimplemented to allow development in 1.x")
Copy link
Sponsor Member

Choose a reason for hiding this comment

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

Will this be a different PR? We should deprecate this behaviour in 0.7

Copy link
Sponsor Member Author

Choose a reason for hiding this comment

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

AbstractDict hits the fallback ::Any deprecation. Next to that deprecation is a task-item to un-comment these definitions, so when that happens it will go from a warning to an error.

@inline broadcast(f, A, Bs...) =
broadcast(f, combine_styles(A, Bs...), nothing, nothing, A, Bs...)
@inline function broadcast(f, A, Bs...)
A′ = broadcastable(A)
Copy link
Member

Choose a reason for hiding this comment

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

It's unfortunate that the prime looks so similar to the adjoint...

Copy link
Sponsor Member Author

@mbauman mbauman Mar 13, 2018

Choose a reason for hiding this comment

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

¯\_(ツ)_/¯

There's more than one of us using this style in base to represent roughly-the-same-value-but-maybe-slightly-different.

Copy link
Member

@stevengj stevengj Mar 14, 2018

Choose a reason for hiding this comment

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

You could also use , but I agree that the \prime style is pretty common.

broadcastable(x::AbstractArray) = x
# In the future, default to collecting arguments. TODO: uncomment once deprecations are removed
# broadcastable(x) = BroadcastStyle(typeof(x)) isa Unknown ? collect(x) : x
# broadcastable(::Dict) = error("intentionally unimplemented to allow development in 1.x")
Copy link
Member

Choose a reason for hiding this comment

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

::AbstractDict

Copy link
Member

Choose a reason for hiding this comment

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

Also we might consider if NamedTuple should maybe broadcast together with a AbstractDict{Symbol} rather than by iteration index.

@vchuravy
Copy link
Sponsor Member

@nanosoldier runbenchmarks(ALL, vs = ":master")

@mbauman
Copy link
Sponsor Member Author

mbauman commented Mar 13, 2018

Nanosoldier is going to fail without that commit I just pushed.

@vchuravy
Copy link
Sponsor Member

Well it didn't listen to me anyway...

@nanosoldier runbenchmarks(ALL, vs = ":master")

(Put I am just being overly cautious)

@@ -687,6 +687,21 @@ end
# After deprecation is removed, enable the @testset "indexing by Bool values" in test/arrayops.jl
# Also un-comment the new definition in base/indices.jl

# Broadcast no longer defaults to treating its arguments as scalar (#)
function Broadcast.broadcastable(x)
Copy link
Sponsor Member

Choose a reason for hiding this comment

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

should have @noinline to ensure depwarn tracking works

Copy link
Member

@nalimilan nalimilan left a comment

Choose a reason for hiding this comment

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

Thanks for your efforts to improve broadcast!

Cc: @timholy


Return either `x` or an object like `x` such that it supports `axes` and indexing.
"""
broadcastable(x::Union{Symbol,String,Type,Function,UndefInitializer,Nothing,RoundingMode}) = Ref(x)
Copy link
Member

Choose a reason for hiding this comment

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

I guess you can add Missing to this list.

@@ -457,13 +460,13 @@ as in `broadcast!(f, A, A, B)` to perform `A[:] = broadcast(f, A, B)`.
end

# Optimization for the all-Scalar case.
Copy link
Member

Choose a reason for hiding this comment

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

Is this still the "all scalar case"?

arguments and the scalars themselves in `As`. Singleton and missing dimensions
are expanded to match the extents of the other arguments by virtually repeating
the value. By default, only a limited number of types are considered scalars,
including `Number`s, `Type`s and `Function`s; all other arguments are iterated
Copy link
Member

Choose a reason for hiding this comment

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

Could give the full list (in particular strings).


- If all the arguments are scalars, it returns a scalar.
Copy link
Member

Choose a reason for hiding this comment

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

I imagine this rule still applies? Maybe worth keeping for clarity even if it's covered by more general rules below.

@@ -905,6 +905,11 @@ Deprecated or removed
* `map` on dictionaries previously operated on `key=>value` pairs. This behavior is deprecated,
and in the future `map` will operate only on values ([#5794]).

* Previously, broadcast defaulted to treating its arguments as scalars if they were not
Copy link
Member

Choose a reason for hiding this comment

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

It would be useful to add a sentence for package developers mentioning broadcastable, just like the deprecation does.


Return either `x` or an object like `x` such that it supports `axes` and indexing.
"""
broadcastable(x::Union{Symbol,AbstractString,Type,Function,UndefInitializer,Nothing,RoundingMode,Missing}) = Ref(x)
Copy link
Sponsor Member

Choose a reason for hiding this comment

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

Can remove Type here since it has a method below.

@KristofferC
Copy link
Sponsor Member

@nanosoldier runbenchmarks(ALL, vs = ":master")

@nanosoldier
Copy link
Collaborator

Something went wrong when running your job:

IOError(EOFError() during request(https://api.github.com/repos/JuliaLang/julia/statuses/fb59643d8cfa5011d66a4d09d8e38227b977a3e5))

cc @ararslan

broadcastable(x)

Return either `x` or an object like `x` such that it supports `axes` and indexing.
"""
Copy link
Member

Choose a reason for hiding this comment

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

Perhaps add an example?

# Examples
```jldoctest
julia> Broadcast.broadcastable([1, 2, 3])
3-element Array{Int64,1}:
 1
 2
 3

julia> Broadcast.broadcastable("Hello, world!")
Base.RefValue{String}("Hello, world!")
```

@@ -423,6 +423,23 @@ end
broadcastable(x)

Return either `x` or an object like `x` such that it supports `axes` and indexing.

If `x` supports iteration, the returned value should match [`collect(x)`](@ref).
Copy link
Member

Choose a reason for hiding this comment

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

This isn't true. e.g. collect(3) returns an Array{Int,0}, but broadcastable just returns 3.

Copy link
Sponsor Member Author

@mbauman mbauman Mar 15, 2018

Choose a reason for hiding this comment

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

Fair. How about I add with respect to axes and indexing?

Edit: or If `x` supports iteration, the returned value should have the same `axes` and indexing behaviors as [`collect(x)`](@ref)

@mbauman
Copy link
Sponsor Member Author

mbauman commented Mar 19, 2018

@nanosoldier runbenchmarks(ALL, vs = ":master")

@nanosoldier
Copy link
Collaborator

Your benchmark job has completed - possible performance regressions were detected. A full report can be found here. cc @ararslan

@StefanKarpinski
Copy link
Sponsor Member

That does not look too bad – all these regressions seem like they could be in the noise.

@stevengj
Copy link
Member

stevengj commented Mar 20, 2018

The slowdowns for tuple broadcasting seem like they might be real. They are testing:

round.(Int, (rand(n)...,))

for various n ≤ 10. Would be worth trying that manually to verify.

@mbauman
Copy link
Sponsor Member Author

mbauman commented Mar 20, 2018

Well that was a remarkably simple fix. Once CI passes again I'd like to merge.

@vchuravy
Copy link
Sponsor Member

While we wait on CI.
@nanosoldier runbenchmarks(ALL, vs = ":master")

@ararslan
Copy link
Member

While we wait on CI.

Except that Nanosoldier takes about 3 times longer than our longest running CI build. 😉

@ararslan
Copy link
Member

I had to restart the server. @nanosoldier runbenchmarks(ALL, vs=":master")

@nanosoldier
Copy link
Collaborator

Your benchmark job has completed - possible performance regressions were detected. A full report can be found here. cc @ararslan

@vchuravy
Copy link
Sponsor Member

There is still a regression for ["broadcast", "typeargs", "(\"tuple\", 10)"] and a deprecation warning in the Pkg3 tests

@mbauman
Copy link
Sponsor Member Author

mbauman commented Mar 21, 2018

I fought with that remaining allocation for quite some time with no success. It's the Ref{Type{Int}}(Int) that is getting heap-allocated in some situations, but it's not clear to me why. Everything downstream of the Ref is inlined and it doesn't escape, but I think things are just on the edge of what the optimizer can see. Nothing is obviously problematic to my eyes. My attempts at reducing this down to a MWE have failed — it works as I'd expect once I pull the relevant methods out of Base and into my own module. I'm guessing that it's because I've not copied the entire method tables.

Given that this regression is small and the optimizer is getting revamped, I'm inclined to merge.

Broadcast now calls a special helper function, `broadcastable`, on each argument. It ensures that the arguments support indexing and have a shape, or otherwise have defined a custom `BroadcastStyle`.
@StefanKarpinski
Copy link
Sponsor Member

Would be interesting to see if the new optimizer can eliminate that allocation.

@mbauman mbauman merged commit 274f0a2 into master Mar 22, 2018
@mbauman mbauman deleted the mb/broadcastable branch March 22, 2018 15:57
@fredrikekre
Copy link
Member

The manual section on the broadcasting interface still uses Broadcast.Scalar in a couple of places resulting in

WARNING: Broadcast.Scalar is deprecated, use DefaultArrayStyle{0} instead.
  likely near /home/fredrik/julia-master/doc/make.jl:149

when building the docs.

@fredrikekre
Copy link
Member

And

julia/base/broadcast.jl

Lines 593 to 596 in 7eb5d44

julia> broadcast(+, 1.0, (0, -2.0), Ref(1))
2-element Array{Float64,1}:
2.0
0.0
returns a tuple, but that seems to be intended.

julia> broadcast(+, 1.0, (0, -2.0), Ref(1))
(2.0, 0.0)

That example seem rather pointless now that Ref(1) and 1 behaves the same.

mbauman added a commit that referenced this pull request Mar 23, 2018
Amazing what a great exercise writing documentation is. This is a followup to #26435, and more clearly lays out the requirements for defining a custom (non-AbstractArray) type that can participate in broadcasting.  The biggest functional change here is that types that define their own BroadcastStyle must now also implement `broadcastable` to be a no-op.
@mbauman
Copy link
Sponsor Member Author

mbauman commented Mar 23, 2018

Thank you! See #26601.

mbauman added a commit that referenced this pull request Apr 4, 2018
* Make LinAlg fess up to using Base._return_type

* Simplify broadcastable a bit further and add docs

Amazing what a great exercise writing documentation is. This is a followup to #26435, and more clearly lays out the requirements for defining a custom (non-AbstractArray) type that can participate in broadcasting.  The biggest functional change here is that types that define their own BroadcastStyle must now also implement `broadcastable` to be a no-op.

* Trailing comma, doc fixes from review; more tests
@mbauman mbauman added the domain:broadcast Applying a function over a collection label Apr 24, 2018
@GiggleLiu
Copy link
Contributor

GiggleLiu commented Sep 26, 2018

Thanks for the design of Ref and very explicit broadcastable interface.

The only problem is defining broadcastable for every type is too expensive for developers. We should probably provide a default broadcastable function (like before).

If broadcastable is not defined for a type, then its instance will be broadcasted by Ref, this is OK. If the program instead throws an error like length is not defined (like now), this is less intuitive. It will make people less confidense to use broadcast operations.

@fredrikekre
Copy link
Member

There is a default, which is broadcastable(x) = x.

@GiggleLiu
Copy link
Contributor

GiggleLiu commented Sep 26, 2018

@fredrikekre
see the discussion here,

https://discourse.julialang.org/t/broadcasting-over-structs-in-1-0-what-am-i-supposed-to-do/13408

We have to implement the interface like

julia> Broadcast.broadcastable(m::M) = Ref(m)

to make a type work as a scalar.

A minimal working example:

julia> struct A end

julia> println.(A())
ERROR: MethodError: no method matching length(::A)
Closest candidates are:
  length(::Core.SimpleVector) at essentials.jl:571
  length(::Base.MethodList) at reflection.jl:728
  length(::Core.MethodTable) at reflection.jl:802
  ...
Stacktrace:
 [1] _similar_for(::UnitRange{Int64}, ::Type, ::A, ::Base.HasLength) at ./array.jl:532
 [2] _collect(::UnitRange{Int64}, ::A, ::Base.HasEltype, ::Base.HasLength) at ./array.jl:563
 [3] collect(::A) at ./array.jl:557
 [4] broadcastable(::A) at ./broadcast.jl:609
 [5] broadcasted(::Function, ::A) at ./broadcast.jl:1134
 [6] top-level scope at none:0

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
domain:arrays [a, r, r, a, y, s] domain:broadcast Applying a function over a collection kind:deprecation This change introduces or involves a deprecation
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Broadcast had one job (e.g. broadcasting over iterators and generator)