Masks
Motivation
ClimaCore spaces, SpectralElementSpace2D
s in particular, support masks, where users can set horizontal nodal locations where operations are skipped.
This is especially helpful for the land model, where they may have degrees of freedom over the ocean, but do not want to evaluate expressions in regions where data is missing.
Masks in ClimaCore offer a solution to this by, ahead of time prescribing regions to skip. This helps both with the ergonomics, as well as performance.
User interface
There are two user-facing parts for ClimaCore masks:
- set the
enable_mask = true
keyword in the space constructor (when available), which is currently any constructor that returns/contains aSpectralElementSpace2D
. - use
set_mask!
to set where the mask istrue
(where compute should occur) andfalse
(where compute should be skipped)
Here is an example
using ClimaComms
ClimaComms.@import_required_backends
import ClimaCore: Spaces, Fields
using ClimaCore.CommonSpaces
using Test
FT = Float64
ᶜspace = ExtrudedCubedSphereSpace(FT;
z_elem = 10,
z_min = 0,
z_max = 1,
radius = 10,
h_elem = 10,
n_quad_points = 4,
staggering = CellCenter(),
enable_mask = true,
)
# How to set the mask
Spaces.set_mask!(ᶜspace) do coords
coords.lat > 0.5
end
# Or
mask = Fields.Field(FT, ᶜspace)
mask .= map(cf -> cf.lat > 0.5 ? 0.0 : 1.0, Fields.coordinate_field(mask))
Spaces.set_mask!(ᶜspace, mask)
Finally, operations over fields will be skipped where mask == 0
, and applied where mask == 1
:
@. f = 1 # only applied where the mask is equal to 1
Example script
Here is a more complex script where the mask is used:
using ClimaComms
ClimaComms.@import_required_backends
import ClimaCore: Spaces, Fields, DataLayouts, Geometry, Operators
using ClimaCore.CommonSpaces
using Test
FT = Float64
ᶜspace = ExtrudedCubedSphereSpace(FT;
z_elem = 10,
z_min = 0,
z_max = 1,
radius = 10,
h_elem = 10,
n_quad_points = 4,
staggering = CellCenter(),
enable_mask = true,
)
ᶠspace = Spaces.face_space(ᶜspace)
ᶠcoords = Fields.coordinate_field(ᶠspace)
# How to set the mask
Spaces.set_mask!(ᶜspace) do coords
coords.lat > 0.5
end
# We also support the syntax `Spaces.set_mask!(::AbstractSpace, ::Field)`
# We can check the mask directly: (internals, only for demonstrative purposes)
mask = Spaces.get_mask(ᶜspace)
@test count(parent(mask.is_active)) == 4640
@test length(parent(mask.is_active)) == 9600
# Let's skip operations that use fill!
ᶜf = zeros(ᶜspace) # ignores mask
@. ᶜf = 1 # tests fill! # abides by mask
# Let's show that 4640 columns were impacted:
@test count(x->x==1, parent(ᶜf)) == 4640 * Spaces.nlevels(axes(ᶜf))
@test length(parent(ᶜf)) == 9600 * Spaces.nlevels(axes(ᶜf))
# Let's skip operations that use copyto!
ᶜz = Fields.coordinate_field(ᶜspace).z
ᶜf = zeros(ᶜspace)
@. ᶜf = 1 + 0 * ᶜz # tests copyto!
# Let's again show that 4640 columns were impacted:
@test count(x->x==1, parent(ᶜf)) == 4640 * Spaces.nlevels(axes(ᶜf))
@test length(parent(ᶜf)) == 9600 * Spaces.nlevels(axes(ᶜf))
# Let's skip operations in FiniteDifference operators
ᶠf = zeros(ᶠspace)
c = Fields.Field(FT, ᶜspace)
div = Operators.DivergenceF2C()
foo(f, cf) = cf.lat > 0.5 ? zero(f) : sqrt(-1) # results in NaN in masked out regions
@. c = div(Geometry.WVector(foo(ᶠf, ᶠcoords)))
# Check that this field should never yield NaNs
@test count(isnan, parent(c)) == 0
# Doing the same thing with a space without a mask will yield NaNs:
ᶜspace_no_mask = ExtrudedCubedSphereSpace(FT;
z_elem = 10,
z_min = 0,
z_max = 1,
radius = 10,
h_elem = 10,
n_quad_points = 4,
staggering = CellCenter(),
)
ᶠspace_no_mask = Spaces.face_space(ᶜspace_no_mask)
ᶠcoords_no_mask = Fields.coordinate_field(ᶠspace_no_mask)
c_no_mask = Fields.Field(FT, ᶜspace_no_mask)
ᶠf_no_mask = Fields.Field(FT, ᶠspace_no_mask)
@. c_no_mask = div(Geometry.WVector(foo(ᶠf_no_mask, ᶠcoords_no_mask)))
@test count(isnan, parent(c_no_mask)) == 49600
Supported operations and caveats
Currently, masked operations are only supported for Fields
(and not DataLayouts
) with SpectralElementSpace2D
s. We do not yet have support for masked SpectralElement1DSpace
s, and we will likely never offer masked operation support for DataLayouts
, as they do not have the space, and can therefore not use the mask.
In addition, some operations with masked fields skip masked regions (i.e., mask-aware), and other operations execute everywhere (i.e., mask-unaware), effectively ignoring the mask. Here is a list of operations of mask-aware and mask-unaware:
DataLayout
operations (Fields.field_values(f) = 1
) mask-unaware (will likely never be mask-aware).fill!
(@. f = 1
) mask-aware- point-wise
copyto!
(@. f = 1 + z
) mask-aware - stencil
copyto!
(@. ᶜf = 1 + DivergenceF2C()(Geometry.WVector(ᶠf))
) mask-aware (vertical derivatives and interpolations interpolations) - spectral element operations
copyto!
(@. f = 1 + Operators.Divergence()(f)
), whereOperators.Divergence
carries out a divergence operation in horizontal directions. mask-unaware - fieldvector operations
copyto!
(@. Y += 1
) mask-unaware - reductions:
sum
(mask-unaware, warning is thrown)extrema
(mask-unaware, warning is thrown)min
(mask-unaware, warning is thrown)max
(mask-unaware, warning is thrown)
- field constructors (
copy
,Fields.Field
,ones
,zeros
) are mask-unaware. This was a design implementation detail, users should not generally depend on the results wheremask == 0
, in case this is changed in the future. - internal array operations (
fill!(parent(field), 0)
) mask-unaware.
Developer docs
In order to support masks, we define their types in DataLayouts
, since we need access to them from within kernels in DataLayouts
. We could have made an API and kept them completely orthogonal, but that would have been a bit more complicated, also, it was convenient to make the masks themselves data layouts, so it seemed most natural for them to live there.
We have a couple types:
- abstract
AbstractMask
for subtyping masks and use for generic interface methods NoMask
(the default), which is a lazy object that should effectively result in a no-op, without any loss of runtime performanceIJHMask
currently the only supported horizontal mask, which containsis_active
(defined inset_mask!
),N
(the number of active columns), and maps containing indices to thei, j, h
locations whereis_active
is true. The maps are defined inset_mask_maps!
, allows us to launch cuda kernels to only target the active columns, and threads are not wasted on non-existent columns. The logic to handle this is relatively thin, and extends our currentext/cuda/datalayouts_threadblock.jl
api (viamasked_partition
andmasked_universal_index
).
An important note is that when we set the mask maps for active columns, the order that they are assigned can be permuted without impacting correctness, but this could have a big impact on performance on the gpu. We should investigate this.