Skip to content

Commit ee00ebe

Browse files
dennisYatuninclaude
andcommitted
Treat view slices as single points and vectorize unmasked point loops
Two launch-machinery fixes, verified locally with the device-free compilation checker, DataLayouts and Fields unit tests (single- and multi-threaded, bounds checks on), and interleaved benchmarks: - num_slice_points(view, arg) = 1: a view slice contains one point by definition, so no slice needs to be constructed at launch time. The generic method throws for FusedMultiBroadcast launches (point-sliced fused broadcasts mix 0-dimensional DataF dest slices with dimension-preserving Broadcasted slices, so their inferred_size values never agree), and it turned the slice size from a compile-time constant into runtime slice construction executed several times per GPU launch on the host and once per thread inside every kernel (slice_subscope: 16.7-33.3 ns and 18 typed statements down to a constant-folded 4.1 ns and 3 statements). - Point loops with no mask now use @simd and inline f at the call site. LLVM cannot vectorize across the carry branches of a flattened CartesianIndices iteration, and the inliner gives up on point closures over large broadcast expressions, materializing every argument slice into a function call at each point; either alone leaves pointwise loops scalar (0 vector instructions vs 512 for the equivalent plain array broadcast). GPU subscopes iterate Generators, which do not support @simd and keep a plain loop with the inlined call. Pointwise broadcasts drop from 0.298 to 0.158 ns/point and fill! from 0.303 to 0.138 ns/point; baroclinic-wave step! recovers from 1.37x of main to 1.05x, and 100 bubble_2d steps from 1.73x to ~1.2x. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 8398477 commit ee00ebe

1 file changed

Lines changed: 48 additions & 12 deletions

File tree

src/DataLayouts/loops.jl

Lines changed: 48 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,11 @@ end
1818
@generated invalid_mask_string(::M, ::O) where {M, O} =
1919
"$M cannot be applied to $(O.instance) slices"
2020

21+
# A view slice contains one point by definition, so its size is known without
22+
# constructing a slice. The generic method cannot handle point slices of fused
23+
# broadcasts, whose 0-dimensional DataF args and dimension-preserving
24+
# Broadcasted args have inconsistent values of inferred_size.
25+
@inline num_slice_points(::typeof(view), arg) = 1
2126
@inline function num_slice_points(op::O, arg) where {O}
2227
first_slice = op(arg, Tuple(first(each_slice_index(op, arg)))...)
2328
has_inferred_size(first_slice) && return prod(inferred_size(first_slice))
@@ -64,24 +69,55 @@ also be used to skip over a particular subset of slices.
6469
isone(num_threads(scope)) ? foreach_slice(ThisThread(), op, f, args...; mask) :
6570
parallelize_over(scope) do
6671
subscope = slice_subscope(scope, op, args...)
67-
for index in subscope_slice_indices(subscope, scope, mask, op, args...)
68-
slices = unrolled_map(args) do arg
69-
Base.@_inline_meta # Slices must be constructed without closures.
70-
@inbounds reassign(op(arg, Tuple(index)...), subscope)
71-
end
72-
f(slices...)
73-
end
72+
indices = subscope_slice_indices(subscope, scope, mask, op, args...)
73+
slice_loop(indices, subscope, op, f, mask, args...)
7474
end
7575

7676
# Reassign each slice to ThisThread so nested loops in f dispatch here statically: the
7777
# runtime thread-count check, though cheap, blocks removal of the parallelized closures.
7878
@inline foreach_slice(::ThisThread, op::O, f::F, args...; mask) where {O, F} =
79-
for index in subscope_slice_indices(ThisThread(), ThisThread(), mask, op, args...)
80-
slices = unrolled_map(args) do arg
81-
Base.@_inline_meta # Slices must be constructed without closures.
82-
@inbounds reassign(op(arg, Tuple(index)...), ThisThread())
79+
slice_loop(
80+
subscope_slice_indices(ThisThread(), ThisThread(), mask, op, args...),
81+
ThisThread(),
82+
op,
83+
f,
84+
mask,
85+
args...,
86+
)
87+
88+
# Unmasked point loops need @simd and an inlined call to f in order to vectorize: LLVM
89+
# cannot vectorize across the carry branches of a flattened CartesianIndices iteration
90+
# unless @simd splits it into an outer loop and a unit-stride inner loop, and Julia's
91+
# inliner gives up on point closures over large broadcast expressions, which forces
92+
# every argument slice to be materialized and passed to a function call at each point.
93+
# Without these, pointwise loops are several times slower than plain array broadcasts.
94+
# Masked index iterators do not support @simd, and slice operators do too much work per
95+
# slice for vectorization to apply to them.
96+
@inline function slice_loop(indices, subscope, op::O, f::F, mask, args...) where {O, F}
97+
if op === view && mask isa NoMask
98+
point_loop(indices, subscope, op, f, args...)
99+
else
100+
for index in indices
101+
f(slices_at(op, subscope, index, args...)...)
83102
end
84-
f(slices...)
103+
end
104+
end
105+
106+
# @simd only supports indexable iterators; GPU subscopes iterate Generators over strided
107+
# ranges, so their point loops just inline f (each GPU thread gets at most a few points,
108+
# and there is nothing for LLVM to vectorize across them).
109+
@inline point_loop(indices::AbstractArray, subscope, op::O, f::F, args...) where {O, F} =
110+
@simd for index in indices
111+
@inline f(slices_at(op, subscope, index, args...)...)
112+
end
113+
@inline point_loop(indices, subscope, op::O, f::F, args...) where {O, F} =
114+
for index in indices
115+
@inline f(slices_at(op, subscope, index, args...)...)
116+
end
117+
@inline slices_at(op::O, subscope, index, args...) where {O} =
118+
unrolled_map(args) do arg
119+
Base.@_inline_meta # Slices must be constructed without closures.
120+
@inbounds reassign(op(arg, Tuple(index)...), subscope)
85121
end
86122

87123
"""

0 commit comments

Comments
 (0)