Skip to content

Commit 6474619

Browse files
imreddyTejaclaude
andcommitted
Fix eager FD kernel bugs and replace op-specific isa checks with dispatch
Bug fixes (each reproduced on GPU before the fix): - ext/cuda/operators_finite_difference.jl: read `mask.N[1]` under `CUDA.@allowscalar`. It is a one-element device array, so the masked eager path threw "Scalar indexing is disallowed" on its first call. Matches the three existing read sites in data_layouts_threadblock.jl. - src/Operators/finitedifference.jl: restore `boundary_width(::DivergenceF2C, ::Extrapolate) = 1`. Without it the boundary centers fall in the interior window, so `op_matrix_first_row`/`_last_row` for `Extrapolate` were unreachable and the condition was silently ignored -- `divf2c_extrapolate(u)` returned the plain interior stencil at the boundary. `test_op_matrix(DivergenceF2C, Extrapolate, ...)` could not catch this because both the matrix and operator paths degraded identically. - ext/cuda/operators_fd_eager.jl: size the dynamic shared memory from the actual projected operand type (`cached_operand_type`) instead of the `AbstractTensor ? sizeof(eltype)*9 : sizeof` heuristic. `project_row2_for_mul` projects tensor leaves nested inside a `BandMatrixRow`, which the heuristic did not see: a `QuaddiagonalMatrixRow{Covariant1Vector{Float64}}` was budgeted 32 bytes and written as 96, crashing the kernel with ERROR_ILLEGAL_ADDRESS. The new version mirrors `project_row2_for_mul`'s type logic, so it cannot drift from what the kernel writes. - .buildkite/pipeline.yml: drop the stray `soft_fail: true` from the `agents:` block of unit_matrix_field_broadcasting_gpu_non_scalar_3. It is a step attribute, not an agent tag; there it became an agent-tag query that would leave the step unscheduled. The correct one three lines above stays. Op-specific `isa` checks in generic machinery, now handled by dispatch: - `should_call_left_boundary`/`should_call_right_boundary` no longer special-case `SetBoundaryOperator` + `NullBoundaryCondition`. Instead `boundary_width(::SetBoundaryOperator, ...)` returns 0 by default and 1 for the four value-fixing conditions, matching the idiom already used for GradientF2C/DivergenceF2C. This also fixes what the guard could not reach: `left_interior_window_idx`/`right_interior_window_idx` bypass `should_call_*`, so one-sided SetBoundaryOperators were needlessly widening the boundary window by a level. `should_call_right_boundary` now also uses the `boundary_condition` local it already binds. - The `op_matrix.op isa Union{GradientF2C, DivergenceF2C} && bc isa SetValue` branch inside the generic `FDOperatorMatrix` `stencil_*_boundary` methods becomes a more specific method on `FDOperatorMatrix{<:InputFixingFDOperator}`. - The `op isa SetBoundaryOperator` early return in the generic `OneArgFDOperator` StencilBroadcasted constructor becomes its own constructor method; the gradient/divergence adjoint ternaries become `adjoint_matrix_arg`/`adjoint_matrix_result`. The generic constructor now has no `isa` checks. - Drop `split_bcs = op isa WeightedInterpolateC2F` from the `TwoArgFDOperator` constructor. Every other two-argument operator's conditions are linear, so `output_bcs` is already empty and `op_with_matrix_bcs` already returns the op unchanged; the flag was both a hardcoded op name and redundant. `detect_ambiguities` is empty for both Operators and MatrixFields. Verified on CPU and GPU: MatrixFields/operator_matrices.jl (CPU 275+41, GPU 207+25), finitedifference/unit_column.jl (25 each), and matrix_fields_broadcasting/test_non_scalar_2.jl (GPU 2) all pass. test_non_scalar_3.jl remains red, but with a pre-existing InvalidIRError over `AutoBroadcaster` row entries that reproduces with these changes stashed, so its soft_fail is left in place. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BLMKMmk7JezBvagh3AybPQ
1 parent bd961ea commit 6474619

5 files changed

Lines changed: 169 additions & 77 deletions

File tree

.buildkite/pipeline.yml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1376,7 +1376,6 @@ steps:
13761376
agents:
13771377
slurm_gpus: 1
13781378
slurm_mem: 10GB
1379-
soft_fail: true
13801379

13811380
- label: "Unit: matrix field broadcasting (GPU)"
13821381
key: unit_matrix_field_broadcasting_gpu_non_scalar_4

ext/cuda/operators_fd_eager.jl

Lines changed: 39 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -38,22 +38,53 @@ fit and can always be cached.
3838
max_eager_shmem_per_thread(x) = 0
3939
max_eager_shmem_per_thread(bc::Union{Broadcasted, StencilBroadcasted}) =
4040
_max_eager_shmem_over_args(bc.args)
41-
function max_eager_shmem_per_thread(
41+
max_eager_shmem_per_thread(
4242
bc::StencilBroadcasted{S, <:MultiplyColumnwiseBandMatrixField},
43-
) where {S}
44-
raw_arg_type = unsafe_eltype(bc.args[2])
45-
# tensors may be projected to contain more components
46-
# this is hacky and will sometimes overestimate the size of the projected row
47-
modified_arg_size = raw_arg_type <: AbstractTensor ? sizeof(eltype(raw_arg_type)) * 9 : sizeof(raw_arg_type)
48-
max(modified_arg_size, _max_eager_shmem_over_args(bc.args))
49-
end
43+
) where {S} =
44+
max(sizeof(cached_operand_type(bc)), _max_eager_shmem_over_args(bc.args))
5045

5146
_max_eager_shmem_over_args(::Tuple{}) = 0
5247
_max_eager_shmem_over_args(args::Tuple) = max(
5348
max_eager_shmem_per_thread(first(args)),
5449
_max_eager_shmem_over_args(Base.tail(args)),
5550
)
5651

52+
"""
53+
cached_operand_type(bc)
54+
55+
The type that `calc_level_val` writes into shared memory for the multiplication `bc`,
56+
i.e. `typeof(project_row2_for_mul(mat1_row, mat2_row, hidx, mat2_space))`.
57+
58+
The size cannot be read off the second operand directly, because
59+
`project_row2_for_mul` projects every tensor leaf of that operand onto the axis dual
60+
to the first operand's entries, which changes its size in either direction: a
61+
`Covariant1Vector` widens to a `Contravariant123Vector`, while a `Covariant12Vector`
62+
narrows to a `Contravariant1Vector`. For a matrix-matrix product those leaves are also
63+
nested inside a `BandMatrixRow`, so no property of the operand's outermost type
64+
bounds the projected size. Mirror `project_row2_for_mul`'s type-level logic instead
65+
and infer the projected type, so the buffer is always big enough for what the kernel
66+
writes into it.
67+
"""
68+
function cached_operand_type(bc)
69+
mat1_type = unsafe_eltype(bc.args[1i32])
70+
mat2_type = unsafe_eltype(bc.args[2i32])
71+
mat1_et = mat1_type <: BandMatrixRow ? eltype(mat1_type) : mat1_type
72+
project_onto =
73+
ClimaCore.Geometry.recursively_find_dual_axes_for_projection(mat1_et)
74+
isnothing(project_onto) && return mat2_type
75+
lg_type = Spaces.local_geometry_type(typeof(axes(bc.args[2i32])))
76+
projected_type = ClimaCore.Utilities.return_type(
77+
recursively_project,
78+
Tuple{Tuple{typeof(project_onto), lg_type}, mat2_type},
79+
)
80+
isconcretetype(projected_type) || error(
81+
"Unable to size the eager finite difference kernel's shared memory: \
82+
inference gave the non-concrete type $projected_type for the \
83+
projection of a $mat2_type operand onto $project_onto",
84+
)
85+
return projected_type
86+
end
87+
5788

5889
ClimaCore.Utilities.unsafe_eltype(::CUDA.CuRefType{T}) where {T} = T
5990

ext/cuda/operators_finite_difference.jl

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,11 @@ function Base.copyto!(
9494
eager_shmem_per_thread = max_eager_shmem_per_thread(bc′)
9595
if !high_resolution
9696
# 32 <= n_face_levels * Ni <= 256
97-
n_columns = mask isa NoMask ? Ni * Nj * Nh : mask.N[1]
97+
# mask.N holds the active column count in a one-element device array;
98+
# reading it on the host needs @allowscalar.
99+
n_columns =
100+
mask isa NoMask ? Ni * Nj * Nh :
101+
CUDA.@allowscalar(mask.N[1])
98102
# 108 is the number of SMs in an A100. TODO: get this value from CUDA.jl to better optimize for different GPUs
99103
threads_dim_y = n_columns > 256 * 108 ? div(256, n_face_levels) : 1
100104
block_dim_x = div(n_columns, threads_dim_y, RoundUp)

src/MatrixFields/operator_matrices.jl

Lines changed: 110 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,47 @@ apply_boundary_operator(::Type{Style}, op, arg, axes, work) where {Style} =
215215
)
216216
end
217217

218+
# A gradient operator matrix has vector entries and a divergence operator matrix has
219+
# covector entries, so for the plain `*` of the matrix multiply to produce a result of
220+
# the right rank, a gradient needs an adjoint on its argument and a divergence needs
221+
# one on its result.
222+
adjoint_matrix_arg(op, arg) = arg
223+
adjoint_matrix_arg(::Operators.GradientOperator, arg) =
224+
Base.Broadcast.broadcasted(adjoint, arg)
225+
adjoint_matrix_result(op, result) = result
226+
adjoint_matrix_result(::Operators.DivergenceOperator, result) =
227+
Base.Broadcast.broadcasted(adjoint, result)
228+
229+
# Builds an ordinary StencilBroadcasted, without rewriting `op` into a matrix multiply.
230+
unconverted_stencil_broadcasted(
231+
::Type{Style},
232+
op,
233+
args::Args,
234+
axes,
235+
work::Work,
236+
) where {Style, Args, Work} = Operators.StencilBroadcasted{
237+
Style,
238+
typeof(op),
239+
Args,
240+
typeof(axes),
241+
Work,
242+
}(
243+
op,
244+
args,
245+
axes,
246+
work,
247+
)
248+
249+
# A SetBoundaryOperator has no operator matrix: it is what the conversions below use to
250+
# reapply the boundary conditions they strip out, so it is built verbatim.
251+
Operators.StencilBroadcasted{Style}(
252+
op::Operators.SetBoundaryOperator,
253+
args::Args,
254+
axes::Spaces.AbstractSpace,
255+
work::Work = nothing,
256+
) where {Style, Args, Work} =
257+
unconverted_stencil_broadcasted(Style, op, args, axes, work)
258+
218259
# Converts a broadcast over a one-argument operator, `op(arg)`, into the
219260
# equivalent operator matrix expression, `op_matrix() * arg`. Boundary conditions
220261
# that modify the operator's input or output are stripped from the matrix and
@@ -227,20 +268,6 @@ function Operators.StencilBroadcasted{Style}(
227268
axes::Spaces.AbstractSpace,
228269
work::Work = nothing,
229270
) where {Style, Args, Work}
230-
# SetBoundaryOperator is not represented by an operator matrix.
231-
op isa Operators.SetBoundaryOperator && return Operators.StencilBroadcasted{
232-
Style,
233-
typeof(op),
234-
Args,
235-
typeof(axes),
236-
Work,
237-
}(
238-
op,
239-
args,
240-
axes,
241-
work,
242-
)
243-
244271
op_matrix = Base.Broadcast.broadcasted(
245272
FDOperatorMatrix(op_with_matrix_bcs(op)),
246273
Fields.local_geometry_field(operator_input_space(op, axes)),
@@ -256,14 +283,10 @@ function Operators.StencilBroadcasted{Style}(
256283
Base.axes(args[1]),
257284
nothing,
258285
)
259-
arg =
260-
op isa Operators.GradientOperator ?
261-
Base.Broadcast.broadcasted(adjoint, arg) : arg
286+
arg = adjoint_matrix_arg(op, arg)
262287

263288
result = multiply_matrix_broadcasted(Style, op_matrix, arg, axes, work)
264-
result =
265-
op isa Operators.DivergenceOperator ?
266-
Base.Broadcast.broadcasted(adjoint, result) : result
289+
result = adjoint_matrix_result(op, result)
267290

268291
bcs_out = output_bcs(op)
269292
return isempty(bcs_out) ? result :
@@ -278,23 +301,24 @@ end
278301

279302

280303
# Converts a broadcast over a two-argument operator, `op(weight, arg)`, into the
281-
# equivalent operator matrix expression, `op_matrix(weight) * arg`. Only
282-
# WeightedInterpolateC2F has boundary conditions (SetValue) that modify its
283-
# output; when present, they are stripped from the matrix and reapplied to the
284-
# result with a SetBoundaryOperator.
304+
# equivalent operator matrix expression, `op_matrix(weight) * arg`. As for one-argument
305+
# operators, boundary conditions that modify the output are stripped from the matrix and
306+
# reapplied to the result with a SetBoundaryOperator. In practice only
307+
# WeightedInterpolateC2F has such conditions (SetValue); every other two-argument
308+
# operator's conditions are linear, so `output_bcs` is empty for them and both
309+
# `op_with_matrix_bcs` and this function leave them untouched.
285310
function Operators.StencilBroadcasted{Style}(
286311
op::TwoArgFDOperator,
287312
args::Args,
288313
axes::Spaces.AbstractSpace,
289314
work::Work = nothing,
290315
) where {Style, Args, Work}
291-
split_bcs = op isa Operators.WeightedInterpolateC2F
292-
matrix_op = split_bcs ? op_with_matrix_bcs(op) : op
293-
op_matrix = Base.Broadcast.broadcasted(FDOperatorMatrix(matrix_op), args[1])
316+
op_matrix =
317+
Base.Broadcast.broadcasted(FDOperatorMatrix(op_with_matrix_bcs(op)), args[1])
294318

295319
result = multiply_matrix_broadcasted(Style, op_matrix, args[2], axes, work)
296320

297-
bcs_out = split_bcs ? output_bcs(op) : (;)
321+
bcs_out = output_bcs(op)
298322
return isempty(bcs_out) ? result :
299323
apply_boundary_operator(
300324
Style,
@@ -527,53 +551,78 @@ Operators.stencil_right_boundary(
527551
# constant contributed by the boundary value is zeroed out (see `has_affine_bc`).
528552
# For every operator except GradientF2C/DivergenceF2C, a value-fixing condition
529553
# prescribes the *output* at the boundary as a pure constant, so its linear part is
530-
# zero and the boundary row is empty (`rzero`). GradientF2C/DivergenceF2C with a
531-
# SetValue are the exception (as with `modifies_input`): the condition fixes an
532-
# *input* value, and the near-boundary output still depends linearly on the
533-
# adjacent interior input, so the row is the genuine boundary stencil (with the
534-
# fixed input's coefficient dropped) rather than zero.
535-
Base.@propagate_inbounds function Operators.stencil_left_boundary(
554+
# zero and the boundary row is empty (`rzero`).
555+
const ValueFixingBoundaryCondition = Union{
556+
Operators.SetValue,
557+
Operators.SetGradient,
558+
Operators.SetDivergence,
559+
Operators.SetCurl,
560+
}
561+
# The operators whose SetValue fixes an *input* rather than an output, and so keep a
562+
# genuine boundary stencil in the matrix (see `modifies_input`).
563+
const InputFixingFDOperator =
564+
Union{Operators.GradientF2C, Operators.DivergenceF2C}
565+
566+
Base.@propagate_inbounds Operators.stencil_left_boundary(
536567
op_matrix::FDOperatorMatrix,
537-
bc::Union{
538-
Operators.SetValue,
539-
Operators.SetGradient,
540-
Operators.SetDivergence,
541-
Operators.SetCurl,
542-
},
568+
::ValueFixingBoundaryCondition,
569+
space,
570+
idx,
571+
hidx,
572+
args...,
573+
) = rzero(Operators.return_eltype(op_matrix, args...))
574+
# Mirror of stencil_left_boundary above, for the right boundary.
575+
Base.@propagate_inbounds Operators.stencil_right_boundary(
576+
op_matrix::FDOperatorMatrix,
577+
::ValueFixingBoundaryCondition,
578+
space,
579+
idx,
580+
hidx,
581+
args...,
582+
) = rzero(Operators.return_eltype(op_matrix, args...))
583+
584+
# GradientF2C/DivergenceF2C with a SetValue are the exception (as with
585+
# `modifies_input`): the condition fixes an *input* value, and the near-boundary output
586+
# still depends linearly on the adjacent interior input, so the row is the genuine
587+
# boundary stencil (with the fixed input's coefficient dropped) rather than zero. The
588+
# last of `args` is the local geometry field that `op_matrix` was given, which the
589+
# underlying operator's row functions do not take.
590+
Base.@propagate_inbounds function Operators.stencil_left_boundary(
591+
op_matrix::FDOperatorMatrix{<:InputFixingFDOperator},
592+
bc::Operators.SetValue,
543593
space,
544594
idx,
545595
hidx,
546596
args...,
547597
)
548-
if op_matrix.op isa Union{Operators.GradientF2C, Operators.DivergenceF2C} &&
549-
bc isa Operators.SetValue
550-
args′ = args[1:(end - 1)]
551-
row = op_matrix_first_row(op_matrix.op, bc, space, idx, hidx, args′...)
552-
return convert(Operators.return_eltype(op_matrix, args...), row)
553-
end
554-
rzero(Operators.return_eltype(op_matrix, args...))
598+
row = op_matrix_first_row(
599+
op_matrix.op,
600+
bc,
601+
space,
602+
idx,
603+
hidx,
604+
args[1:(end - 1)]...,
605+
)
606+
return convert(Operators.return_eltype(op_matrix, args...), row)
555607
end
556608
# Mirror of stencil_left_boundary above, for the right boundary.
557609
Base.@propagate_inbounds function Operators.stencil_right_boundary(
558-
op_matrix::FDOperatorMatrix,
559-
bc::Union{
560-
Operators.SetValue,
561-
Operators.SetGradient,
562-
Operators.SetDivergence,
563-
Operators.SetCurl,
564-
},
610+
op_matrix::FDOperatorMatrix{<:InputFixingFDOperator},
611+
bc::Operators.SetValue,
565612
space,
566613
idx,
567614
hidx,
568615
args...,
569616
)
570-
if op_matrix.op isa Union{Operators.GradientF2C, Operators.DivergenceF2C} &&
571-
bc isa Operators.SetValue
572-
args′ = args[1:(end - 1)]
573-
row = op_matrix_last_row(op_matrix.op, bc, space, idx, hidx, args′...)
574-
return convert(Operators.return_eltype(op_matrix, args...), row)
575-
end
576-
rzero(Operators.return_eltype(op_matrix, args...))
617+
row = op_matrix_last_row(
618+
op_matrix.op,
619+
bc,
620+
space,
621+
idx,
622+
hidx,
623+
args[1:(end - 1)]...,
624+
)
625+
return convert(Operators.return_eltype(op_matrix, args...), row)
577626
end
578627

579628
################################################################################

src/Operators/finitedifference.jl

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1660,7 +1660,16 @@ Base.@propagate_inbounds stencil_interior(
16601660
arg,
16611661
) = getidx(space, arg, idx, hidx)
16621662

1663-
boundary_width(::SetBoundaryOperator, ::AbstractBoundaryCondition) = 1
1663+
# A side with no boundary condition has no boundary row: the operator is the
1664+
# identity there. Returning 0 (rather than gating on NullBoundaryCondition in the
1665+
# generic windowing code) keeps one-sided SetBoundaryOperators out of the boundary
1666+
# window entirely, so they never reach the NaN-producing NullBoundaryCondition
1667+
# stencil methods.
1668+
boundary_width(::SetBoundaryOperator, ::AbstractBoundaryCondition) = 0
1669+
boundary_width(
1670+
::SetBoundaryOperator,
1671+
::Union{SetValue, SetGradient, SetCurl, SetDivergence},
1672+
) = 1
16641673
Base.@propagate_inbounds function stencil_left_boundary(
16651674
::SetBoundaryOperator,
16661675
bc::Union{SetValue, SetGradient, SetCurl, SetDivergence},
@@ -1949,6 +1958,10 @@ stencil_interior_width(::DivergenceF2C, arg) = ((-half, half),)
19491958
boundary_width(::DivergenceF2C, ::AbstractBoundaryCondition) = 0
19501959
boundary_width(::DivergenceF2C, ::SetValue) = 1
19511960
boundary_width(::DivergenceF2C, ::SetDivergence) = 1
1961+
# Without this, `left_interior_idx`/`right_interior_idx` place the boundary
1962+
# centers in the interior window, so `op_matrix_first_row`/`op_matrix_last_row`
1963+
# for `Extrapolate` are never reached and the condition is silently ignored.
1964+
boundary_width(::DivergenceF2C, ::Extrapolate) = 1
19521965

19531966
# Extend `adapt_structure` for all boundary conditions containing a `val` field.
19541967
function Adapt.adapt_structure(to, bc::AbstractBoundaryCondition)
@@ -2209,8 +2222,6 @@ end
22092222
Topologies.isperiodic(space) && return false
22102223
loc = left_boundary_window(space)
22112224
boundary_condition = Operators.get_boundary(op, loc)
2212-
op isa SetBoundaryOperator && boundary_condition isa NullBoundaryCondition &&
2213-
return false
22142225
return idx < Operators.left_interior_idx(
22152226
space,
22162227
op,
@@ -2223,12 +2234,10 @@ end
22232234
Topologies.isperiodic(space) && return false
22242235
loc = right_boundary_window(space)
22252236
boundary_condition = Operators.get_boundary(op, loc)
2226-
op isa SetBoundaryOperator && boundary_condition isa NullBoundaryCondition &&
2227-
return false
22282237
return idx > Operators.right_interior_idx(
22292238
space,
22302239
op,
2231-
Operators.get_boundary(op, loc),
2240+
boundary_condition,
22322241
args...,
22332242
)
22342243
end

0 commit comments

Comments
 (0)