Skip to content

Commit 24041ff

Browse files
mbaumantimholyvtjnashajkeller34
authored andcommitted
Customizable lazy fused broadcasting in pure Julia
This patch represents the combined efforts of four individuals, over 60 commits, and an iterated design over (at least) three pull requests that spanned nearly an entire year (closes #22063, #23692, #25377 by superceding them). This introduces a pure Julia data structure that represents a fused broadcast expression. For example, the expression `2 .* (x .+ 1)` lowers to: ```julia julia> Meta.@lower 2 .* (x .+ 1) :($(Expr(:thunk, CodeInfo(:(begin Core.SSAValue(0) = (Base.getproperty)(Base.Broadcast, :materialize) Core.SSAValue(1) = (Base.getproperty)(Base.Broadcast, :make) Core.SSAValue(2) = (Base.getproperty)(Base.Broadcast, :make) Core.SSAValue(3) = (Core.SSAValue(2))(+, x, 1) Core.SSAValue(4) = (Core.SSAValue(1))(*, 2, Core.SSAValue(3)) Core.SSAValue(5) = (Core.SSAValue(0))(Core.SSAValue(4)) return Core.SSAValue(5) end))))) ``` Or, slightly more readably as: ```julia using .Broadcast: materialize, make materialize(make(*, 2, make(+, x, 1))) ``` The `Broadcast.make` function serves two purposes. Its primary purpose is to construct the `Broadcast.Broadcasted` objects that hold onto the function, the tuple of arguments (potentially including nested `Broadcasted` arguments), and sometimes a set of `axes` to include knowledge of the outer shape. The secondary purpose, however, is to allow an "out" for objects that _don't_ want to participate in fusion. For example, if `x` is a range in the above `2 .* (x .+ 1)` expression, it needn't allocate an array and operate elementwise — it can just compute and return a new range. Thus custom structures are able to specialize `Broadcast.make(f, args...)` just as they'd specialize on `f` normally to return an immediate result. `Broadcast.materialize` is identity for everything _except_ `Broadcasted` objects for which it allocates an appropriate result and computes the broadcast. It does two things: it `initialize`s the outermost `Broadcasted` object to compute its axes and then `copy`s it. Similarly, an in-place fused broadcast like `y .= 2 .* (x .+ 1)` uses the exact same expression tree to compute the right-hand side of the expression as above, and then uses `materialize!(y, make(*, 2, make(+, x, 1)))` to `instantiate` the `Broadcasted` expression tree and then `copyto!` it into the given destination. All-together, this forms a complete API for custom types to extend and customize the behavior of broadcast (fixes #22060). It uses the existing `BroadcastStyle`s throughout to simplify dispatch on many arguments: * Custom types can opt-out of broadcast fusion by specializing `Broadcast.make(f, args...)` or `Broadcast.make(::BroadcastStyle, f, args...)`. * The `Broadcasted` object computes and stores the type of the combined `BroadcastStyle` of its arguments as its first type parameter, allowing for easy dispatch and specialization. * Custom Broadcast storage is still allocated via `broadcast_similar`, however instead of passing just a function as a first argument, the entire `Broadcasted` object is passed as a final argument. This potentially allows for much more runtime specialization dependent upon the exact expression given. * Custom broadcast implmentations for a `CustomStyle` are defined by specializing `copy(bc::Broadcasted{CustomStyle})` or `copyto!(dest::AbstractArray, bc::Broadcasted{CustomStyle})`. * Fallback broadcast specializations for a given output object of type `Dest` (for the `DefaultArrayStyle` or another such style that hasn't implemented assignments into such an object) are defined by specializing `copyto(dest::Dest, bc::Broadcasted{Nothing})`. As it fully supports range broadcasting, this now deprecates `(1:5) + 2` to `.+`, just as had been done for all `AbstractArray`s in general. As a first-mover proof of concept, LinearAlgebra uses this new system to improve broadcasting over structured arrays. Before, broadcasting over a structured matrix would result in a sparse array. Now, broadcasting over a structured matrix will _either_ return an appropriately structured matrix _or_ a dense array. This does incur a type instability (in the form of a discriminated union) in some situations, but thanks to type-based introspection of the `Broadcasted` wrapper commonly used functions can be special cased to be type stable. For example: ```julia julia> f(d) = round.(Int, d) f (generic function with 1 method) julia> @inferred f(Diagonal(rand(3))) 3×3 Diagonal{Int64,Array{Int64,1}}: 0 ⋅ ⋅ ⋅ 0 ⋅ ⋅ ⋅ 1 julia> @inferred Diagonal(rand(3)) .* 3 ERROR: return type Diagonal{Float64,Array{Float64,1}} does not match inferred return type Union{Array{Float64,2}, Diagonal{Float64,Array{Float64,1}}} Stacktrace: [1] error(::String) at ./error.jl:33 [2] top-level scope julia> @inferred Diagonal(1:4) .+ Bidiagonal(rand(4), rand(3), 'U') .* Tridiagonal(1:3, 1:4, 1:3) 4×4 Tridiagonal{Float64,Array{Float64,1}}: 1.30771 0.838589 ⋅ ⋅ 0.0 3.89109 0.0459757 ⋅ ⋅ 0.0 4.48033 2.51508 ⋅ ⋅ 0.0 6.23739 ``` In addition to the issues referenced above, it fixes: * Fixes #19313, #22053, #23445, and #24586: Literals are no longer treated specially in a fused broadcast; they're just arguments in a `Broadcasted` object like everything else. * Fixes #21094: Since broadcasting is now represented by a pure Julia datastructure it can be created within `@generated` functions and serialized. * Fixes #26097: The fallback destination-array specialization method of `copyto!` is specifically implemented as `Broadcasted{Nothing}` and will not be confused by `nothing` arguments. * Fixes the broadcast-specific element of #25499: The default base broadcast implementation no longer depends upon `Base._return_type` to allocate its array (except in the empty or concretely-type cases). Note that the sparse implementation (#19595) is still dependent upon inference and is _not_ fixed. * Fixes #25340: Functions are treated like normal values just like arguments and only evaluated once. * Fixes #22255, and is performant with 12+ fused broadcasts. Okay, that one was fixed on master already, but this fixes it now, too. * Fixes #25521. * The performance of this patch has been thoroughly tested through its iterative development process in #25377. There remain [two classes of performance regressions](#25377) that Nanosoldier flagged. * #25691: Propagation of constant literals sill lose their constant-ness upon going through the broadcast machinery. I believe quite a large number of functions would need to be marked as `@pure` to support this -- including functions that are intended to be specialized. (For bookkeeping, this is the squashed version of the [teh-jn/lazydotfuse](#25377) branch as of a1d4e7e. Squashed and separated out to make it easier to review and commit) Co-authored-by: Tim Holy <[email protected]> Co-authored-by: Jameson Nash <[email protected]> Co-authored-by: Andrew Keller <[email protected]>
1 parent 73e39b3 commit 24041ff

29 files changed

+1407
-864
lines changed

NEWS.md

+8-5
Original file line numberDiff line numberDiff line change
@@ -388,11 +388,6 @@ This section lists changes that do not have deprecation warnings.
388388
Its return value has been removed. Use the `process_running` function
389389
to determine if a process has already exited.
390390

391-
* Broadcasting has been redesigned with an extensible public interface. The new API is
392-
documented at https://docs.julialang.org/en/latest/manual/interfaces/#Interfaces-1.
393-
`AbstractArray` types that specialized broadcasting using the old internal API will
394-
need to switch to the new API. ([#20740])
395-
396391
* The logging system has been redesigned - `info` and `warn` are deprecated
397392
and replaced with the logging macros `@info`, `@warn`, `@debug` and
398393
`@error`. The `logging` function is also deprecated and replaced with
@@ -418,6 +413,14 @@ This section lists changes that do not have deprecation warnings.
418413
* `findn(x::AbstractArray)` has been deprecated in favor of `findall(!iszero, x)`, which
419414
now returns cartesian indices for multidimensional arrays (see below, [#25532]).
420415

416+
* Broadcasting operations are no longer fused into a single operation by Julia's parser.
417+
Instead, a lazy `Broadcasted` wrapper is created, and the parser will call
418+
`copy(bc::Broadcasted)` or `copyto!(dest, bc::Broadcasted)`
419+
to evaluate the wrapper. Consequently, package authors generally need to specialize
420+
`copy` and `copyto!` methods rather than `broadcast` and `broadcast!`.
421+
See the [Interfaces chapter](https://docs.julialang.org/en/latest/manual/interfaces/#Interfaces-1)
422+
for more information.
423+
421424
* `find` has been renamed to `findall`. `findall`, `findfirst`, `findlast`, `findnext`
422425
now take and/or return the same type of indices as `keys`/`pairs` for `AbstractArray`,
423426
`AbstractDict`, `AbstractString`, `Tuple` and `NamedTuple` objects ([#24774], [#25545]).

base/bitarray.jl

-40
Original file line numberDiff line numberDiff line change
@@ -1097,19 +1097,6 @@ function (-)(B::BitArray)
10971097
end
10981098
broadcast(::typeof(sign), B::BitArray) = copy(B)
10991099

1100-
function broadcast(::typeof(~), B::BitArray)
1101-
C = similar(B)
1102-
Bc = B.chunks
1103-
if !isempty(Bc)
1104-
Cc = C.chunks
1105-
for i = 1:length(Bc)
1106-
Cc[i] = ~Bc[i]
1107-
end
1108-
Cc[end] &= _msk_end(B)
1109-
end
1110-
return C
1111-
end
1112-
11131100
"""
11141101
flipbits!(B::BitArray{N}) -> BitArray{N}
11151102
@@ -1166,33 +1153,6 @@ end
11661153
(/)(B::BitArray, x::Number) = (/)(Array(B), x)
11671154
(/)(x::Number, B::BitArray) = (/)(x, Array(B))
11681155

1169-
# broadcast specializations for &, |, and xor/⊻
1170-
broadcast(::typeof(&), B::BitArray, x::Bool) = x ? copy(B) : falses(size(B))
1171-
broadcast(::typeof(&), x::Bool, B::BitArray) = broadcast(&, B, x)
1172-
broadcast(::typeof(|), B::BitArray, x::Bool) = x ? trues(size(B)) : copy(B)
1173-
broadcast(::typeof(|), x::Bool, B::BitArray) = broadcast(|, B, x)
1174-
broadcast(::typeof(xor), B::BitArray, x::Bool) = x ? .~B : copy(B)
1175-
broadcast(::typeof(xor), x::Bool, B::BitArray) = broadcast(xor, B, x)
1176-
for f in (:&, :|, :xor)
1177-
@eval begin
1178-
function broadcast(::typeof($f), A::BitArray, B::BitArray)
1179-
F = BitArray(undef, promote_shape(size(A),size(B))...)
1180-
Fc = F.chunks
1181-
Ac = A.chunks
1182-
Bc = B.chunks
1183-
(isempty(Ac) || isempty(Bc)) && return F
1184-
for i = 1:length(Fc)
1185-
Fc[i] = ($f)(Ac[i], Bc[i])
1186-
end
1187-
Fc[end] &= _msk_end(F)
1188-
return F
1189-
end
1190-
broadcast(::typeof($f), A::DenseArray{Bool}, B::BitArray) = broadcast($f, BitArray(A), B)
1191-
broadcast(::typeof($f), B::BitArray, A::DenseArray{Bool}) = broadcast($f, B, BitArray(A))
1192-
end
1193-
end
1194-
1195-
11961156
## promotion to complex ##
11971157

11981158
# TODO?

0 commit comments

Comments
 (0)