Skip to content

Commit caabfdd

Browse files
dennisYatuninclaude
andcommitted
Fix GPU reductions, kernel-side slice lengths, and mask device placement
Fixes for the CI failures on build 7043, all in code that can only run with a GPU: - Compute slice lengths from shape parameters instead of inferring the types of slices; the return_type of a slice operator cannot be obtained from within a GPU kernel, and the previous approach also failed on the host for slices of broadcasted expressions and for single-point views of expressions with mixtures of parent array types - Generate the butterfly shuffle offsets in warp reductions from a static thread count, since tuples with runtime lengths cannot be constructed in GPU kernels - Launch at most one reduction thread per point, so that every thread has at least one value to reduce; threads without values would need placeholders for warp shuffles, but reductions without neutral elements (like minimum) cannot generate placeholders - Use the active mask instead of the full mask for warp shuffles, so that partial warps can also execute them, and exclude the lanes of partial warps from reduction results - Construct each grid's mask from its device-side local geometry, so that masks of GPU spaces are not stored in CPU arrays - Qualify the CUDA intrinsics and import slab in the GPU DataLayouts test Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 5687c82 commit caabfdd

4 files changed

Lines changed: 58 additions & 20 deletions

File tree

ext/cuda/loops.jl

Lines changed: 37 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,15 @@ function DataLayouts.reduce_points(::ThisHost, op::O, arg; kwargs...) where {O}
3131
end
3232
T = return_type(op, NTuple{2, eltype(arg)})
3333
empty_results = DataLayouts.scoped_array(ThisHost(), T, 0)
34-
(; threads, blocks) = config_via_occupancy(kernel, length(arg), (empty_results, arg))
34+
# Launch at most one thread per point, so that every thread has at least
35+
# one value to reduce (indices are divided among threads through strided
36+
# ranges, which are only guaranteed to be nonempty when the total thread
37+
# count does not exceed the number of points). Threads without values would
38+
# need placeholders for the warp shuffles, but reduction operations without
39+
# neutral elements (like the min in minimum) cannot generate placeholders.
40+
max_threads = threads_via_occupancy(kernel, (empty_results, arg))
41+
threads = min(length(arg), max_threads)
42+
blocks = max(fld(length(arg), threads), 1)
3543
num_results = min(max_resident_blocks(threads), blocks)
3644
results = similar(empty_results, num_results)
3745
@cuda always_inline = true threads = threads blocks = num_results kernel(results, arg)
@@ -69,29 +77,46 @@ function DataLayouts.reduce_points(::ThisBlock, op::O, arg; kwargs...) where {O}
6977
return @inbounds results[1]
7078
end
7179

72-
DataLayouts.reduce_points(scope::ThisSubBlock, op::O, arg; kwargs...) where {O} =
73-
DataLayouts.num_threads(scope) <= THREADS_PER_WARP ?
74-
shuffle_reduce(scope, op, DataLayouts.reduce_points(ThisThread(), op, arg; kwargs...)) :
80+
DataLayouts.reduce_points(scope::ThisSubBlock{N}, op::O, arg; kwargs...) where {N, O} =
81+
N <= THREADS_PER_WARP ?
82+
shuffle_reduce(
83+
scope,
84+
op,
85+
DataLayouts.reduce_points(ThisThread(), op, arg; kwargs...),
86+
# Partial warps at the ends of partial blocks have fewer than N threads.
87+
min(N, count_ones(CUDA.active_mask())),
88+
) :
7589
DataLayouts.reduce_points(
7690
ThisWarp(),
7791
op,
7892
DataLayouts.reassign(arg, ThisWarp());
7993
kwargs...,
8094
)
8195

96+
# Generate the tuple of butterfly shuffle offsets (n ÷ 2, n ÷ 4, ..., 1) from a
97+
# static thread count, since a tuple whose length depends on a runtime value
98+
# cannot be constructed in a GPU kernel.
99+
@generated shuffle_offsets(::Val{n}) where {n} =
100+
ntuple(Base.Fix1(>>, n), 8 * sizeof(n) - leading_zeros(n) - 1)
101+
82102
# Use warp shuffles to perform binary tree reductions over the first
83103
# num_threads values in a scope. Every thread in the scope must execute the
84104
# shuffles, but values from threads with ranks above num_threads are ignored.
85-
function shuffle_reduce(scope, op::O, value, num_threads = THREADS_PER_WARP) where {O}
86-
DataLayouts.is_subscope(scope, ThisWarp()) ||
87-
throw(ArgumentError(DataLayouts.invalid_subscope_string(scope, ThisWarp())))
88-
num_threads <= THREADS_PER_WARP ||
105+
# The active mask is used instead of the full mask so that partial warps can
106+
# also execute the shuffles (values from lanes outside the mask are undefined,
107+
# but such lanes always have ranks above num_threads).
108+
function shuffle_reduce(
109+
scope::ThisSubBlock{n},
110+
op::O,
111+
value,
112+
num_threads = THREADS_PER_WARP,
113+
) where {n, O}
114+
n <= THREADS_PER_WARP ||
89115
throw(ArgumentError("Number of threads is too large for warp shuffle"))
90-
n = DataLayouts.num_threads(scope)
116+
mask = CUDA.active_mask()
91117
rank = DataLayouts.thread_rank(scope)
92-
log2_n = 8 * sizeof(n) - Base.ctlz_int(n) - 1
93-
for offset in ntuple(Base.Fix1(>>, n), Val(log2_n)) # n ÷ 2, n ÷ 4, ..., 1
94-
shuffled_value = CUDA.shfl_xor_sync(CUDA.FULL_MASK, value, offset)
118+
for offset in shuffle_offsets(Val(n))
119+
shuffled_value = CUDA.shfl_xor_sync(mask, value, offset)
95120
if rank <= num_threads && xor(rank - 1, offset) + 1 <= num_threads
96121
value = op(value, shuffled_value)
97122
end

src/DataLayouts/loops.jl

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

21+
# Compute slice lengths directly from shape parameters instead of inferring the
22+
# types of slices, since the return_type of a slice operator cannot be obtained
23+
# from within a GPU kernel.
24+
@inline slice_size_params(::typeof(view), arg) = ()
25+
@inline slice_size_params(::typeof(column), arg) = (vijh_params(arg).Nv,)
26+
@inline slice_size_params(::typeof(slab), arg) =
27+
(vijh_params(arg).Ni, vijh_params(arg).Nj)
28+
@inline slice_size_params(::typeof(level), arg) =
29+
(vijh_params(arg).Ni, vijh_params(arg).Nj, vijh_params(arg).Nh)
2130
@inline function inferred_slice_length(op::O, arg) where {O}
22-
index = one(eltype(each_slice_index(op, arg)))
23-
slice_type = return_type(op, typeof((arg, Tuple(index)...)))
24-
has_inferred_size(slice_type) && return prod(inferred_size(slice_type))
25-
throw(ArgumentError("Size of view from slice operator must be inferrable"))
31+
params = slice_size_params(op, arg)
32+
params isa Tuple{Vararg{Integer}} && return prod(params; init = 1)
33+
throw(ArgumentError("Size of slice from slice operator must be inferrable"))
2634
end
2735

2836
"""

src/Grids/spectralelement.jl

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -249,7 +249,6 @@ function _SpectralElementGrid2D(
249249
LG = Geometry.LocalGeometryType(CoordType2D, FT, AIdx)
250250

251251
local_geometry = VIJH{LG, 1, Nq, Nq, nothing}(Array{FT}, Nh)
252-
mask = enable_mask ? DataLayouts.IJHMask(local_geometry) : DataLayouts.NoMask()
253252

254253
_, quad_weights = Quadratures.quadrature_points(FT, quadrature_style)
255254
_, high_order_quad_weights =
@@ -424,6 +423,11 @@ function _SpectralElementGrid2D(
424423
end
425424

426425
device_local_geometry = DataLayouts.rebuild(local_geometry, DA)
426+
# Construct the mask from the device-side geometry, so that its data is
427+
# stored on the same device as the rest of the grid.
428+
mask =
429+
enable_mask ? DataLayouts.IJHMask(device_local_geometry) :
430+
DataLayouts.NoMask()
427431
return SpectralElementGrid2D(
428432
topology,
429433
quadrature_style,

test/DataLayouts/cuda.jl

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
using Test
22
import ClimaComms
3+
import ClimaCore: slab
34
import ClimaCore.DataLayouts: VIJFH
45
ClimaComms.@import_required_backends
56

67
function knl_copy!(dest, src)
7-
i = threadIdx().x
8-
j = threadIdx().y
9-
h = blockIdx().x
8+
i = CUDA.threadIdx().x
9+
j = CUDA.threadIdx().y
10+
h = CUDA.blockIdx().x
1011
p_dest = slab(dest, h)
1112
p_src = slab(src, h)
1213
@inbounds p_dest[1, i, j, 1] = p_src[1, i, j, 1]

0 commit comments

Comments
 (0)