Skip to content

Commit 5687c82

Browse files
dennisYatuninclaude
andcommitted
Fix GPU incompatibilities and remaining unmigrated code
Applies the fixes for the CI failures on the previous commit: - Mark a dynamic Nh with nothing instead of missing, since missing cannot appear in the type parameters of any GPU kernel argument (GPUCompiler mangles kernel names by comparing type parameters with ==, and the three-valued logic of missing makes those comparisons non-boolean) - Switch to the new parent array's DataScope in rebuild whenever it is not a subscope of the original scope, so that moving data between devices (with Adapt, gather, or an array conversion) updates its scope; slices keep their scopes, which can be narrower than what their types imply - Capture kwargs as NamedTuples in CUDA kernel closures, since the Pairs structure stores its names in a Tuple of Symbols, which is not a bitstype - Run loops sequentially when they are nested in multithreaded loops located outside of ClimaCore, distinguishing external threads from the pool's own worker tasks through each task's local storage - Replace closures passed to sum in DSS code with accumulation loops; the closures box variables assigned in multiple branches and pull in an empty-collection error path, neither of which can compile in GPU kernels (on CPUs, the error path's runtime dispatch gets flagged by JET) - Avoid indexing with colons in GPU kernels, which allocates copies, and fix two missing exclamation marks in calls to auto_launch! - Migrate ClimaCorePlots to the new DataLayouts API, fixing the plotting slice loop bounds that filled buffers with NaNs, and update the column examples to plot vectors instead of multidimensional parent arrays - Update the SEM benchmark utilities, the CUDA threadblock partition test, and the copyto! benchmark for the new API; wrap a bare tuple broadcast in the deformation flow example (bare multi-element tuples broadcast as collections along the first dimension, matching Base's semantics) - Compare reductions against a pairwise mapreduce over linear positions in unit tests, since the SIMD blocks in Base's optimized methods for dense arrays can reassociate values differently on different CPUs; loosen the JET failure count in opt_spaces, which is 122 on Julia 1.10 All 174 single-rank CPU-only steps from the Buildkite pipeline pass locally with these changes, including all examples and performance scripts. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 3f5dc1c commit 5687c82

26 files changed

Lines changed: 267 additions & 227 deletions

examples/column/ekman.jl

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -131,8 +131,8 @@ dir = "ekman"
131131
path = joinpath(@__DIR__, "output", dir)
132132
mkpath(path)
133133

134-
z_centers = parent(Fields.coordinate_field(cspace))
135-
z_faces = parent(Fields.coordinate_field(fspace))
134+
z_centers = vec(parent(Fields.coordinate_field(cspace)))
135+
z_faces = vec(parent(Fields.coordinate_field(fspace)))
136136

137137
function ekman_plot(u; title = "", size = (1024, 600))
138138
u_ref =
@@ -146,7 +146,7 @@ function ekman_plot(u; title = "", size = (1024, 600))
146146
xlabel = "u",
147147
label = "Ref",
148148
)
149-
sub_plt1 = Plots.plot!(sub_plt1, parent(u.Yc.u), z_centers, label = "Comp")
149+
sub_plt1 = Plots.plot!(sub_plt1, vec(parent(u.Yc.u)), z_centers, label = "Comp")
150150

151151
v_ref =
152152
vg .+
@@ -159,7 +159,7 @@ function ekman_plot(u; title = "", size = (1024, 600))
159159
xlabel = "v",
160160
label = "Ref",
161161
)
162-
sub_plt2 = Plots.plot!(sub_plt2, parent(u.Yc.v), z_centers, label = "Comp")
162+
sub_plt2 = Plots.plot!(sub_plt2, vec(parent(u.Yc.v)), z_centers, label = "Comp")
163163

164164
return Plots.plot(
165165
sub_plt1,

examples/column/hydrostatic.jl

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -147,37 +147,37 @@ dir = "hydrostatic"
147147
path = joinpath(@__DIR__, "output", dir)
148148
mkpath(path)
149149

150-
z_centers = parent(Fields.coordinate_field(cspace))
151-
z_faces = parent(Fields.coordinate_field(fspace))
150+
z_centers = vec(parent(Fields.coordinate_field(cspace)))
151+
z_faces = vec(parent(Fields.coordinate_field(fspace)))
152152

153153
function hydrostatic_plot(u; title = "", size = (1024, 600))
154154
sub_plt1 = Plots.plot(
155-
parent(Y_init.ρ),
155+
vec(parent(Y_init.ρ)),
156156
z_centers,
157157
marker = :circle,
158158
xlabel = "ρ",
159159
label = "T=0",
160160
)
161-
sub_plt1 = Plots.plot!(sub_plt1, parent(u.Yc.ρ), z_centers, label = "T")
161+
sub_plt1 = Plots.plot!(sub_plt1, vec(parent(u.Yc.ρ)), z_centers, label = "T")
162162

163163
sub_plt2 = Plots.plot(
164-
parent(w_init),
164+
vec(parent(w_init)),
165165
z_faces,
166166
marker = :circle,
167167
xlim = (-0.2, 0.2),
168168
xlabel = "ω",
169169
label = "T=0",
170170
)
171-
sub_plt2 = Plots.plot!(sub_plt2, parent(u.w), z_faces, label = "T")
171+
sub_plt2 = Plots.plot!(sub_plt2, vec(parent(u.w)), z_faces, label = "T")
172172

173173
sub_plt3 = Plots.plot(
174-
parent(Y_init.ρθ),
174+
vec(parent(Y_init.ρθ)),
175175
z_centers,
176176
marker = :circle,
177177
xlabel = "ρθ",
178178
label = "T=0",
179179
)
180-
sub_plt3 = Plots.plot!(sub_plt3, parent(u.Yc.ρθ), z_centers, label = "T")
180+
sub_plt3 = Plots.plot!(sub_plt3, vec(parent(u.Yc.ρθ)), z_centers, label = "T")
181181

182182
return Plots.plot(
183183
sub_plt1,

examples/column/hydrostatic_discrete.jl

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -159,37 +159,37 @@ dir = "hydrostatic_discretely_balanced"
159159
path = joinpath(@__DIR__, "output", dir)
160160
mkpath(path)
161161

162-
z_centers = parent(Fields.coordinate_field(cspace))
163-
z_faces = parent(Fields.coordinate_field(fspace))
162+
z_centers = vec(parent(Fields.coordinate_field(cspace)))
163+
z_faces = vec(parent(Fields.coordinate_field(fspace)))
164164

165165
function hydrostatic_plot(u; title = "", size = (1024, 600))
166166
sub_plt1 = Plots.plot(
167-
parent(Y_init.ρ),
167+
vec(parent(Y_init.ρ)),
168168
z_centers,
169169
marker = :circle,
170170
xlabel = "ρ",
171171
label = "T=0",
172172
)
173-
sub_plt1 = Plots.plot!(sub_plt1, parent(u.Yc.ρ), z_centers, label = "T")
173+
sub_plt1 = Plots.plot!(sub_plt1, vec(parent(u.Yc.ρ)), z_centers, label = "T")
174174

175175
sub_plt2 = Plots.plot(
176-
parent(w_init),
176+
vec(parent(w_init)),
177177
z_faces,
178178
marker = :circle,
179179
xlim = (-1e-10, 1e-10),
180180
xlabel = "ω",
181181
label = "T=0",
182182
)
183-
sub_plt2 = Plots.plot!(sub_plt2, parent(u.w), z_faces, label = "T")
183+
sub_plt2 = Plots.plot!(sub_plt2, vec(parent(u.w)), z_faces, label = "T")
184184

185185
sub_plt3 = Plots.plot(
186-
parent(Y_init.ρθ),
186+
vec(parent(Y_init.ρθ)),
187187
z_centers,
188188
marker = :circle,
189189
xlabel = "ρθ",
190190
label = "T=0",
191191
)
192-
sub_plt3 = Plots.plot!(sub_plt3, parent(u.Yc.ρθ), z_centers, label = "T")
192+
sub_plt3 = Plots.plot!(sub_plt3, vec(parent(u.Yc.ρθ)), z_centers, label = "T")
193193

194194
return Plots.plot(
195195
sub_plt1,

examples/column/hydrostatic_ekman.jl

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -193,8 +193,8 @@ dir = "hydrostatic_ekman"
193193
path = joinpath(@__DIR__, "output", dir)
194194
mkpath(path)
195195

196-
z_centers = parent(Fields.coordinate_field(cspace))
197-
z_faces = parent(Fields.coordinate_field(fspace))
196+
z_centers = vec(parent(Fields.coordinate_field(cspace)))
197+
z_faces = vec(parent(Fields.coordinate_field(fspace)))
198198

199199
function ekman_plot(u; title = "", size = (1024, 600))
200200
u_ref =
@@ -211,7 +211,7 @@ function ekman_plot(u; title = "", size = (1024, 600))
211211
# get u component of uv vector
212212
sub_plt1 = Plots.plot!(
213213
sub_plt1,
214-
parent(u.Yc.uv.components.data.:1),
214+
vec(parent(u.Yc.uv.components.data.:1)),
215215
z_centers,
216216
label = "Comp",
217217
)
@@ -230,7 +230,7 @@ function ekman_plot(u; title = "", size = (1024, 600))
230230
# get v component of uv vector
231231
sub_plt2 = Plots.plot!(
232232
sub_plt2,
233-
parent(u.Yc.uv.components.data.:2),
233+
vec(parent(u.Yc.uv.components.data.:2)),
234234
z_centers,
235235
label = "Comp",
236236
)

examples/hybrid/sphere/deformation_flow.jl

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -299,7 +299,9 @@ end
299299
# Roughness measured as deviation from mean (TODO: use a low-pass filter instead)
300300
function tracer_roughnesses(sol)
301301
final_q = sol.u[end].c.ρq ./ sol.u[end].c.ρ
302-
return mean(abs.(final_q .- mean(final_q)))
302+
# Wrap the mean in a Tuple so that it is broadcast like a single value (as
303+
# if it were in a Ref), rather than as a collection of separate values.
304+
return mean(abs.(final_q .- (mean(final_q),)))
303305
end
304306

305307
function tracer_ranges(sol)

ext/cuda/loops.jl

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
function DataLayouts.foreach_slice(::ThisHost, op::O, f::F, args...; kwargs...) where {O, F}
2-
kernel(args...) = DataLayouts.foreach_slice(ThisKernel(), op, f, args...; kwargs...)
2+
# Capture the kwargs as a NamedTuple, whose names are type parameters. The
3+
# Pairs structure of kwargs stores its names in a Tuple of Symbols, which
4+
# cannot be passed to a kernel because Symbols are not bitstypes.
5+
nt_kwargs = values(kwargs)
6+
kernel(args...) = DataLayouts.foreach_slice(ThisKernel(), op, f, args...; nt_kwargs...)
37
if DataLayouts.slice_subscope(ThisKernel(), op, args...) == ThisBlock()
48
max_slice_points = maximum(Base.Fix1(DataLayouts.inferred_slice_length, op), args)
59
threads = min(threads_via_occupancy(kernel, args), max_slice_points)
@@ -17,8 +21,9 @@ is_first_thread_in(scope) = isone(DataLayouts.thread_rank(scope))
1721

1822
# Reduce each block's values, then reduce the results in a single-block kernel.
1923
function DataLayouts.reduce_points(::ThisHost, op::O, arg; kwargs...) where {O}
24+
nt_kwargs = values(kwargs)
2025
function kernel(results, arg)
21-
result = DataLayouts.reduce_points(ThisBlock(), op, arg; kwargs...)
26+
result = DataLayouts.reduce_points(ThisBlock(), op, arg; nt_kwargs...)
2227
if is_first_thread_in(ThisBlock())
2328
@inbounds results[DataLayouts.partition_rank(ThisKernel())] = result
2429
end

ext/cuda/scopes.jl

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,14 +25,13 @@ DataLayouts.DataScope(::Type{<:CUDA.CuDeviceArray{<:Any, <:Any, A}}) where {A} =
2525
"""
2626
ThisHost()
2727
28-
[`DataScope`](@ref) that represents the host device for a GPU. This scope can be
28+
[`DataScope`](@ref) that represents the host device for a GPU. This scope is
2929
assigned to any [`DataLayout`](@ref) backed by a `CuArray`, and it is replaced
30-
with its device-side analogue [`ThisKernel`](@ref) by `Adapt.jl`. Aside from
31-
array allocations, other standard `DataScope` operations are not supported.
30+
with its device-side analogue [`ThisKernel`](@ref) through `Adapt.jl`. Aside
31+
from array allocations, other standard `DataScope` operations are not supported.
3232
"""
3333
struct ThisHost <: DataLayouts.DataScope end
3434

35-
Adapt.adapt_structure(::CUDA.KernelAdaptor, ::ThisHost) = ThisKernel()
3635
DataLayouts.num_threads(::ThisHost) = throw(ArgumentError("Cannot get num_threads on host"))
3736
DataLayouts.thread_rank(::ThisHost) = throw(ArgumentError("Cannot get thread_rank on host"))
3837
DataLayouts.scoped_array(::ThisHost, ::Type{T}, dims) where {T} =
@@ -41,8 +40,7 @@ DataLayouts.scoped_array(::ThisHost, ::Type{T}, dims) where {T} =
4140
"""
4241
ThisKernel()
4342
44-
[`DataScope`](@ref) that represents all available threads on a GPU. This scope
45-
can only be assigned to a [`DataLayout`](@ref) through `Adapt.jl`. Operations
43+
[`DataScope`](@ref) that represents all available threads on a GPU. Operations
4644
that require synchronizations or array allocations are not supported.
4745
"""
4846
struct ThisKernel <: DataLayouts.DataScope end

ext/cuda/topologies_dss.jl

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -127,10 +127,18 @@ 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-
sum_data = sum(first_offset:last_offset) do offset
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.
135+
(h, vert) = local_vertices[first_offset]
136+
p = Topologies.perimeter_vertex_node_index(vert)
137+
sum_data = perimeter_data[v, p, 1, h]
138+
for offset in (first_offset + 1):last_offset
131139
(h, vert) = local_vertices[offset]
132140
p = Topologies.perimeter_vertex_node_index(vert)
133-
perimeter_data[v, p, 1, h]
141+
sum_data += perimeter_data[v, p, 1, h]
134142
end
135143
for offset in first_offset:last_offset
136144
(h, vert) = local_vertices[offset]
@@ -164,7 +172,7 @@ function Topologies.dss_local_ghost!(
164172
nghostvertices = length(ghost_vertex_offset) - 1
165173
nitems = Nv * nghostvertices
166174
iszero(nitems) && return nothing
167-
auto_launch(
175+
auto_launch!(
168176
(perimeter_data, ghost_vertices, ghost_vertex_offset);
169177
dss_config(nitems)...,
170178
) do perimeter_data, ghost_vertices, ghost_vertex_offset
@@ -173,10 +181,15 @@ function Topologies.dss_local_ghost!(
173181
(v, vertex_index) = CartesianIndices((Nv, nghostvertices))[gidx].I
174182
first_offset = ghost_vertex_offset[vertex_index]
175183
last_offset = ghost_vertex_offset[vertex_index + 1] - 1
176-
sum_data = sum(first_offset:last_offset) do offset
184+
# Accumulate in a loop instead of calling sum with a closure, since
185+
# the empty-collection error path of sum cannot be compiled in a
186+
# GPU kernel.
187+
sum_data = zero(eltype(perimeter_data))
188+
for offset in first_offset:last_offset
177189
(isghost, h, vert) = ghost_vertices[offset]
190+
isghost && continue
178191
p = Topologies.perimeter_vertex_node_index(vert)
179-
isghost ? zero(eltype(perimeter_data)) : perimeter_data[v, p, 1, h]
192+
sum_data += perimeter_data[v, p, 1, h]
180193
end
181194
for offset in first_offset:last_offset
182195
(isghost, h, vert) = ghost_vertices[offset]
@@ -199,7 +212,7 @@ function Topologies.dss_ghost!(
199212
nghostvertices = length(ghost_vertex_offset) - 1
200213
nitems = Nv * nghostvertices
201214
iszero(nitems) && return nothing
202-
auto_launch(
215+
auto_launch!(
203216
(perimeter_data, ghost_vertices, ghost_vertex_offset, repr_ghost_vertex);
204217
dss_config(nitems)...,
205218
) do perimeter_data, ghost_vertices, ghost_vertex_offset, repr_ghost_vertex
@@ -238,7 +251,8 @@ function Topologies.fill_send_buffer!(
238251
gidx = DataLayouts.thread_rank(ThisKernel())
239252
@inbounds if gidx <= nitems
240253
(v, send_index) = CartesianIndices((Nv, nsend))[gidx].I
241-
(h, p) = send_buf_idx[send_index, :]
254+
# Avoid indexing with a colon, which would allocate in the kernel.
255+
(h, p) = (send_buf_idx[send_index, 1], send_buf_idx[send_index, 2])
242256
item = perimeter_data[v, p, 1, h]
243257
buffer_index = v + (send_index - 1) * Nv * Nf
244258
DataLayouts.set_struct!(send_data, item, buffer_index, Val(1))
@@ -265,7 +279,8 @@ function Topologies.load_from_recv_buffer!(
265279
@inbounds if gidx <= nitems
266280
T = eltype(perimeter_data)
267281
(v, recv_index) = CartesianIndices((Nv, nrecv))[gidx].I
268-
(h, p) = recv_buf_idx[recv_index, :]
282+
# Avoid indexing with a colon, which would allocate in the kernel.
283+
(h, p) = (recv_buf_idx[recv_index, 1], recv_buf_idx[recv_index, 2])
269284
buffer_index = v + (recv_index - 1) * Nv * Nf
270285
item_view = DataLayouts.view_struct(recv_data, T, buffer_index, Val(1))
271286
parent_view = parent(view(perimeter_data, v, p, 1, h))

lib/ClimaCorePlots/src/ClimaCorePlots.jl

Lines changed: 12 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -78,10 +78,9 @@ RecipesBase.@recipe function f(space::Spaces.RectilinearSpectralElementSpace2D;)
7878
n2 = Meshes.nelements(mesh.intervalmesh2)
7979

8080
coord_field = Fields.coordinate_field(space)
81-
x1coord = vec(parent(coord_field)[:, :, 1, :])
82-
x2coord = vec(parent(coord_field)[:, :, 2, :])
83-
8481
coord_symbols = propertynames(coord_field)
82+
x1coord = vec(parent(getproperty(coord_field, coord_symbols[1])))
83+
x2coord = vec(parent(getproperty(coord_field, coord_symbols[2])))
8584

8685
seriestype := :scatter
8786
title --> "$n1 × $n2 $quad_name{$dof} element space"
@@ -110,8 +109,8 @@ RecipesBase.@recipe function f(space::Spaces.ExtrudedFiniteDifferenceSpace)
110109
dof = Quadratures.degrees_of_freedom(quad)
111110

112111
coord_symbols = propertynames(coord_field)
113-
hcoord = vec(parent(coord_field)[:, :, 1, :])
114-
vcoord = vec(parent(coord_field)[:, :, 2, :])
112+
hcoord = vec(parent(getproperty(coord_field, coord_symbols[1])))
113+
vcoord = vec(parent(getproperty(coord_field, coord_symbols[2])))
115114

116115
stagger = space.staggering isa Spaces.CellCenter ? :center : :face
117116

@@ -308,7 +307,7 @@ function _slice_along(field, coord)
308307
hcoord_data = Spaces.local_geometry_data(hspace).coordinates
309308
hdata = ClimaCore.slab(hcoord_data, hidx)
310309
hnode_idx = 1
311-
for i in axes(hdata)[axis]
310+
for i in axes(hdata)[axis + 1] # axes(hdata) is (V, I, J, H), so I is axis 2
312311
pt = axis == 1 ? hdata[1, i, 1, 1] : hdata[1, 1, i, 1]
313312
axis_value = Geometry.component(pt, axis)
314313
coord_value = Geometry.component(coord, 1)
@@ -353,7 +352,7 @@ function _slice_along(field, coord)
353352
ijslab = ClimaCore.slab(field_data, v, hidx)
354353
islab = ClimaCore.slab(ortho_data, v, i)
355354
# copy the nodal data
356-
for ni in 1:size(islab)[1]
355+
for ni in 1:size(islab, 2) # size(islab) is (Nv, Ni, Nj, Nh)
357356
islab[ni] =
358357
axis == 1 ? ijslab[1, hnode_idx, ni, 1] : ijslab[1, ni, hnode_idx, 1]
359358
end
@@ -427,14 +426,11 @@ function _unfolded_pannel_matrix(field, interpolate)
427426
panels = [fill(NaN, (panel_size * dof, panel_size * dof)) for _ in 1:6]
428427

429428
field_data = Fields.field_values(field)
430-
fdim = DataLayouts.field_dim(DataLayouts.singleton(field_data))
431-
interpolated_data_type = if fdim == ndims(field_data)
432-
DataLayouts.VIJHF
433-
else
434-
DataLayouts.VIJFH
435-
end
436429
interpolated_data =
437-
interpolated_data_type{FT, interpolate}(Array{FT}, nelem)
430+
DataLayouts.VIJFH{FT, 1, interpolate, interpolate, nothing}(
431+
Array{FT},
432+
nelem,
433+
)
438434

439435
Operators.tensor_product!(interpolated_data, field_data, Imat)
440436

@@ -447,7 +443,8 @@ function _unfolded_pannel_matrix(field, interpolate)
447443
x2_nodal_range = (dof * (ex2 - 1) + 1):(dof * ex2)
448444
# transpose the data as our plotting axis order is
449445
# reverse nodal element order (x1 axis varies fastest)
450-
data_element = permutedims(parent(interpolated_data)[:, :, 1, lidx])
446+
data_element =
447+
permutedims([interpolated_data[1, i, j, lidx] for i in 1:dof, j in 1:dof])
451448
panel_data[x2_nodal_range, x1_nodal_range] = data_element
452449
end
453450

0 commit comments

Comments
 (0)