Skip to content

Commit 686cf36

Browse files
dennisYatuninclaude
andcommitted
Shorten code comments and tighten reduction utilities
Long explanatory comments introduced during the DataLayouts rewrite are condensed (142 lines to 83) while keeping every load-bearing rationale. safe_mapreduce now asserts that collections are non-empty when init is not provided, PAIRWISE_BLOCKSIZE cites Base.pairwise_blocksize (which uses the same 1024 for Base's pairwise mapreduce), and running_in_external_threaded_loop reads the task storage once. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 6ade3a9 commit 686cf36

12 files changed

Lines changed: 95 additions & 149 deletions

File tree

ext/ClimaCoreCUDAExt.jl

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,11 +32,10 @@ include(joinpath("cuda", "matrix_fields_single_field_solve.jl"))
3232
include(joinpath("cuda", "matrix_fields_multiple_field_solve.jl"))
3333
include(joinpath("cuda", "operators_spectral_element.jl"))
3434

35-
# Disable the recursion limit for the device reduce_points, which recursively
36-
# reduces over warps and sub-warps while forwarding its keyword arguments. This
37-
# resembles unbounded recursion to the compiler, which otherwise widens and boxes
38-
# the arguments (requiring dynamic dispatch). The limit must also be lifted on the
39-
# keyword-argument body functions, since that is where the recursion occurs.
35+
# Lift the recursion limit for the device reduce_points, whose recursion over warps
36+
# and sub-warps forwards kwargs and looks unbounded to the compiler, which would widen
37+
# and box the arguments (requiring dynamic dispatch). The limit must also be lifted on
38+
# the keyword-argument body functions, since that is where the recursion occurs.
4039
@static if hasfield(Method, :recursion_relation)
4140
for method in methods(ClimaCore.DataLayouts.reduce_points)
4241
method.module === (@__MODULE__) || continue

ext/cuda/loops.jl

Lines changed: 10 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -31,12 +31,9 @@ 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-
# 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.
34+
# Launch at most one thread per point, so that every thread's strided range of
35+
# indices is nonempty. Threads without values would need warp-shuffle placeholders,
36+
# which reductions without neutral elements (like min) cannot generate.
4037
max_threads = threads_via_occupancy(kernel, (empty_results, arg))
4138
threads = min(length(arg), max_threads)
4239
blocks = max(fld(length(arg), threads), 1)
@@ -50,10 +47,8 @@ function DataLayouts.reduce_points(::ThisHost, op::O, arg; kwargs...) where {O}
5047
return CUDA.@allowscalar @inbounds results[1]
5148
end
5249

53-
# If given a warp or a partition of a warp, reduce each threads's values, then
54-
# reduce the results using warp shuffles. Otherwise, reduce each warp's values,
55-
# then reduce the results in the first warp. Limit warp shuffles to the active
56-
# threads in each warp, since the results from inactive threads are undefined.
50+
# Reduce a warp or sub-warp with warp shuffles, limited to active threads (inactive
51+
# results are undefined); otherwise reduce each warp, then combine in the first warp.
5752
DataLayouts.reduce_points(scope::ThisCooperativeGroup, op::O, arg; kwargs...) where {O} =
5853
if scope != ThisBlock() && DataLayouts.num_threads(scope) <= THREADS_PER_WARP
5954
thread_result =
@@ -87,10 +82,8 @@ DataLayouts.reduce_points(scope::ThisCooperativeGroup, op::O, arg; kwargs...) wh
8782
@generated num_reductions(::ThisSubBlock{N}) where {N} =
8883
8 * sizeof(N) - leading_zeros(N) - 1
8984

90-
# Use warp shuffles to perform binary tree reductions over the first num_values
91-
# threads in a warp or sub-warp. All active threads in the warp must execute the
92-
# shuffles, but values from threads with ranks above num_values are ignored. The
93-
# mask identifies active lanes of the warp, which correspond to its lowest bits.
85+
# Binary-tree warp-shuffle reduction over the first num_values threads: every active
86+
# lane (the mask's lowest bits) must shuffle, but ranks above num_values are ignored.
9487
function shuffle_reduce(scope, op::O, value, num_values) where {O}
9588
num_offsets = num_reductions(scope)
9689
num_inactive = THREADS_PER_WARP - num_active_threads(ThisWarp())
@@ -104,10 +97,8 @@ function shuffle_reduce(scope, op::O, value, num_values) where {O}
10497
return value
10598
end
10699

107-
# CUDA's warp shuffle intrinsics only support scalar values, so register how they
108-
# should shuffle the AutoBroadcaster-wrapped values produced by multi-component
109-
# reductions: shuffle each of the underlying values, recursing through any nested
110-
# AutoBroadcasters. shfl_recurse is CUDA's documented extension point for value
111-
# types that the shuffle intrinsics do not natively support.
100+
# CUDA's warp shuffle intrinsics only support scalar values; shfl_recurse is their
101+
# documented extension point, used here to recursively shuffle each value wrapped in
102+
# the AutoBroadcasters produced by multi-component reductions.
112103
CUDA.shfl_recurse(op::O, x::Utilities.AutoBroadcaster) where {O} =
113104
Utilities.AutoBroadcaster(UnrolledUtilities.unrolled_map(op, Utilities.unwrap(x)))

ext/cuda/scopes.jl

Lines changed: 7 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -115,13 +115,10 @@ function DataLayouts.scoped_static_array(scope::ThisSubBlock, ::Type{T}, dims) w
115115
return @inbounds view(array, ntuple(Returns(:), Val(length(dims)))..., subblock_index)
116116
end
117117

118-
# The last sub-block in a block may be only partially filled, so its active
119-
# thread count is computed from the total number of threads in the block. This
120-
# cannot be determined from CUDA.active_mask, which is not guaranteed to be
121-
# consistent across different lanes of the same warp (since the Volta
122-
# architecture introduced independent thread scheduling, the lanes of a warp are
123-
# no longer guaranteed to be synchronized when they poll their mask; see
124-
# https://stackoverflow.com/questions/54055195 for more details).
118+
# The last sub-block in a block may be only partially filled, so its active thread
119+
# count is computed from the block's total. CUDA.active_mask is unusable here: since
120+
# Volta's independent thread scheduling, it is not guaranteed to be consistent across
121+
# lanes of a warp (see https://stackoverflow.com/questions/54055195).
125122
num_active_threads(scope::ThisBlock) = DataLayouts.num_threads(ThisBlock())
126123
function num_active_threads(scope::ThisSubBlock)
127124
num_threads = DataLayouts.num_threads(scope)
@@ -130,11 +127,9 @@ function num_active_threads(scope::ThisSubBlock)
130127
return clamp(DataLayouts.num_threads(ThisBlock()) - subblock_offset, 0, num_threads)
131128
end
132129

133-
# Iterate over a reshape-free generator on GPUs, since the default view of a
134-
# strided range of multidimensional CartesianIndices builds a ReshapedArray
135-
# whose bounds-checking machinery cannot be compiled in a GPU kernel. One-
136-
# dimensional indices (from getindex slices) are left to the default view, which
137-
# is already a reshape-free range that supports the indexing used by reductions.
130+
# A strided view of multidimensional CartesianIndices is a ReshapedArray whose bounds
131+
# checking cannot be compiled in a GPU kernel, so iterate a reshape-free generator here.
132+
# One-dimensional indices (from getindex slices) keep the default view, an indexable range.
138133
Base.@propagate_inbounds DataLayouts.subscope_index_view(
139134
::Union{ThisKernel, ThisCooperativeGroup},
140135
indices::CartesianIndices,

ext/cuda/topologies_dss.jl

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -127,11 +127,9 @@ function Topologies.dss_local!(
127127
(v, vertex_index) = CartesianIndices((Nv, nlocalvertices))[gidx].I
128128
first_offset = local_vertex_offset[vertex_index]
129129
last_offset = local_vertex_offset[vertex_index + 1] - 1
130-
# Accumulate in a loop instead of calling sum with a closure; the
131-
# closure would box the variable v (which is also assigned in the
132-
# other branch of this conditional), and the empty-collection error
133-
# path of sum cannot be compiled in a GPU kernel. Every vertex has
134-
# at least one entry in local_vertices, so the loop is not empty.
130+
# Accumulate in a loop: sum with a closure would box v (also assigned in the
131+
# other branch), and sum's empty-collection error path cannot be compiled in
132+
# a GPU kernel. Every vertex has a local_vertices entry, so the loop is nonempty.
135133
(h, vert) = local_vertices[first_offset]
136134
p = Topologies.perimeter_vertex_node_index(vert)
137135
sum_data = perimeter_data[v, p, 1, h]

src/DataLayouts/DataLayouts.jl

Lines changed: 12 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -283,16 +283,12 @@ end
283283

284284
@inline Base.propertynames(data::DataLayout) = fieldnames(eltype(data))
285285

286-
# Wrap the field index in a Val as soon as it is available, so that all of the
287-
# lookups required to generate a field view are resolved through specialization
288-
# instead of constant propagation. The index or name of a field is a constant
289-
# at every getproperty call site, and converting it to a Val only requires one
290-
# level of constant propagation, which the compiler performs by default; in
291-
# contrast, propagating the constant through every fieldtype lookup can exhaust
292-
# the compiler's budget in expressions with several getproperty calls, which
293-
# results in runtime allocations from dynamic types. Use Base's fieldindex to
294-
# convert the field name into an index, since it is guaranteed to constant-fold
295-
# whenever the name can be statically inferred.
286+
# Wrap the field index in a Val as soon as it is available, so field-view lookups are
287+
# resolved through specialization: making the Val needs only one level of constant
288+
# propagation (done by default), whereas propagating the index through every fieldtype
289+
# lookup can exhaust the compiler's budget when an expression has several getproperty
290+
# calls, causing runtime allocations from dynamic types. Base's fieldindex is used
291+
# because it is guaranteed to constant-fold whenever the name is statically inferred.
296292
@inline function property_view(data, ::Val{i}) where {i}
297293
1 <= i <= fieldcount(eltype(data)) || throw(BoundsError(data, i))
298294
array =
@@ -315,10 +311,8 @@ end
315311
size(array) == array_size ? array :
316312
throw(DimensionMismatch("Array size is not consistent with layout type"))
317313

318-
# A dynamic Nh is marked with nothing instead of missing because missing cannot
319-
# appear in the type parameters of a GPU kernel argument; GPUCompiler mangles
320-
# kernel names by comparing each type parameter with ==, and the three-valued
321-
# logic of missing turns those comparisons into non-boolean conditions.
314+
# A dynamic Nh is marked with nothing rather than missing: GPUCompiler mangles kernel
315+
# names by comparing type parameters with ==, and missing's three-valued == is non-boolean.
322316
@inline check_Nh_dynamic(Nh_dynamic) =
323317
!isnothing(Nh_dynamic) ||
324318
throw(ArgumentError("Nh_dynamic must be specified to construct layout type"))
@@ -542,13 +536,10 @@ include("masks.jl")
542536
include("loops.jl")
543537
include("deprecated.jl")
544538

545-
# Disable the recursion limit for every method of Core.kwcall in this module,
546-
# and also for the recursive slice_subscope function. This is needed for keyword
547-
# argument functions like fill! and column_reduce! to be composed with each
548-
# other an arbitrary number of times, and for slice_subscope to recursively
549-
# partition a scope multiple times before terminating. The default limit
550-
# prevents the compiler from fully analyzing such nested calls, instead making
551-
# it use widened argument types that are boxed and require dynamic dispatch.
539+
# Lift the recursion limit for this module's Core.kwcall methods and the recursive
540+
# scope functions, so that keyword-argument functions like fill! and column_reduce!
541+
# can compose arbitrarily and slice_subscope can repeatedly partition a scope. The
542+
# default limit makes the compiler widen and box arguments, requiring dynamic dispatch.
552543
@static if hasfield(Method, :recursion_relation)
553544
for func in (Core.kwcall, slice_subscope, is_subscope), method in methods(func)
554545
method.module === (@__MODULE__) || continue

src/DataLayouts/broadcast.jl

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,9 @@ DataStyle(::Type{D}) where {D} = DataStyle{ndims(D), layout_type(D)}()
99

1010
Broadcast.BroadcastStyle(::Type{D}) where {D <: DataLayout} = DataStyle(D)
1111

12-
# When two styles share a typename but have different dimensionalities, Base's
13-
# fallback for AbstractArrayStyle evaluates typeof(style)(Val(N)) with the
14-
# larger dimensionality N. DataStyle cannot implement such a Val constructor
15-
# because it needs the layout type D, so it bypasses this fallback.
12+
# For styles with equal typenames but different dimensionalities, Base's fallback for
13+
# AbstractArrayStyle calls typeof(style)(Val(N)); DataStyle needs the layout type D, so
14+
# it cannot define that constructor and bypasses the fallback instead.
1615
Broadcast.BroadcastStyle(::DataStyle{<:Any, D1}, ::DataStyle{<:Any, D2}) where {D1, D2} =
1716
D1 == D2 || iszero(ndims(D2)) ? DataStyle(D1) :
1817
iszero(ndims(D1)) ? DataStyle(D2) : Broadcast.Unknown()
@@ -37,12 +36,9 @@ A [`DataStyle`](@ref) broadcast expression whose [`layout_type`](@ref) is `D`.
3736
"""
3837
const LazyDataLayout{D} = Broadcast.Broadcasted{<:DataStyle{<:Any, D}}
3938

40-
# Optimize axes(::LazyDataLayout) to use statically inferrable size information.
41-
# When the size is dynamic, combine the axes of the arguments without Base's
42-
# combine_axes, whose dimension-mismatch check has an error path that cannot be
43-
# compiled in GPU kernels; the arguments are assumed to be broadcast-compatible,
44-
# as they are in any valid broadcast, so each dimension takes the non-singleton
45-
# axis of the two being combined.
39+
# Optimize axes(::LazyDataLayout) with statically inferrable sizes. Dynamic sizes avoid
40+
# Base's combine_axes, whose dimension-mismatch error path cannot compile in GPU kernels:
41+
# assuming broadcast-compatible arguments, each dimension takes the non-singleton axis.
4642
@inline Broadcast._axes(bc::LazyDataLayout, ::Nothing) =
4743
has_inferred_size(bc) ? unrolled_map(Base.OneTo, inferred_size(bc)) :
4844
unrolled_reduce(broadcast_axes, unrolled_map(axes, bc.args))

src/DataLayouts/indexing.jl

Lines changed: 9 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,11 @@ Base.IndexStyle(bc::FusedMultiBroadcast) = IndexStyle(unrolled_map(first, bc.pai
99

1010
const IndexableData = Union{DataLayout, LazyDataLayout, FusedMultiBroadcast}
1111

12-
# Preserve linear indices when they are used to access broadcast arguments.
13-
# Base's fallback method for newindex reinterprets an integer index as a
14-
# CartesianIndex along the first dimension, which would cause a linear index to
15-
# silently read the wrong element from any argument with multiple dimensions.
16-
# Linear indices are only used when IndexStyle confirms that every layout with
17-
# a nonzero number of dimensions in a broadcast expression has the same shape,
18-
# so the only required index conversion is for 0-dimensional data, whose single
19-
# point is accessed by every index.
12+
# Preserve linear indices into broadcast arguments: Base's newindex fallback
13+
# reinterprets an integer as a CartesianIndex along the first dimension, silently
14+
# reading the wrong element from multidimensional arguments. IndexStyle only permits
15+
# linear indices when all nonzero-dimensional layouts share a shape, so only
16+
# 0-dimensional data (its single point read by every index) needs conversion.
2017
@inline Broadcast.newindex(arg::IndexableData, index::Integer) =
2118
iszero(ndims(arg)) ? CartesianIndex() : index
2219

@@ -92,13 +89,10 @@ end
9289
indices::Integer...,
9390
) = view(arg, CartesianIndex(index1, index2, indices...))
9491

95-
# Return a DataF instead of a layout with singleton dimensions, since a view of
96-
# a single point is 0-dimensional. This also avoids reshaping the 1-dimensional
97-
# view of the point's entries, which would require the construction of division
98-
# helpers that cannot be optimized into SIMD instructions in pointwise loops.
99-
# The inner DataF constructor is used because the view of the entries is
100-
# already shaped correctly; the checking constructor's runtime size comparison
101-
# would make the parent array type a Union of the checked and reshaped types.
92+
# A single-point view is 0-dimensional, so return a DataF instead of a layout with
93+
# singleton dimensions, avoiding a reshape of the 1-D entry view whose division helpers
94+
# block SIMD in pointwise loops. The inner DataF constructor is used since the checking
95+
# one's runtime size comparison would union the checked and reshaped parent array types.
10296
@propagate_inbounds function Base.view(data::DataLayout, index::PointIndex)
10397
is_invalid_linear(data, index) && return view(data, CartesianIndices(data)[index])
10498
array = view_struct(parent(data), eltype(data), index, Val(f_dim(data)))

0 commit comments

Comments
 (0)