High-water-mark storage for the mesh-sized arrays of the data classes (AMR Stage 6b).
Why this exists
Adaptive mesh refinement changes the element count almost every epoch, and the adaptive loop
responded by freeing and re-initializing every mesh-sized field: seven in the model plus six
in the geometry. Profiling one MI300X showed that cycle to be the single largest component of
an adaptation - larger than the solution transfer and larger than geometry regeneration - and
that the cost is NOT the device allocator (hipMalloc plus hipFree together are only ~8% of an
adaptation) but the host-side work that accompanies a fresh allocation: allocating, zeroing
~140 MB of arrays per epoch at modest resolution, and then uploading those zeros to the
device only for the solution transfer to overwrite them.
How it avoids the stride problem
A naive high-water-mark scheme allocates the element dimension larger than the logical
element count. That does not work here: the device kernels take nElem as BOTH the launch
bound and the array stride (see SC_2D_INDEX in src/gpu/SELF_GPU_Macros.h), the variable index
is the slowest-varying dimension so a logical element prefix is not a contiguous prefix of
the buffer, HDF5 hyperslabs are taken from shape(), and several routines assert on nElem
equality.
Instead, each array is backed by a rank-1 pool that is grown monotonically, and the array
itself is a pointer remapped onto the leading part of that pool at the EXACT logical shape.
nElem therefore keeps meaning the logical element count everywhere, every stride, bound,
shape() and equality assertion stays correct with no change, and no padding is ever computed
over or transferred. Only the pool is oversized, and nothing indexes the pool directly.
The device side needs no shape at all - a device pointer is an address - so a device buffer is
reused whenever the bytes required fit the bytes already allocated.
The pools are POINTER rather than ALLOCATABLE components deliberately: a derived-type
component cannot carry the TARGET attribute, and pointing at a component of an object that is
not itself a target is not conforming. An allocated pointer, by contrast, is always a valid
target, so the public arrays can be remapped onto it safely regardless of how the owning
object was declared.
Growth policy
poolGrowth is the over-allocation factor applied when a pool must grow. 1.0 reproduces exact
sizing, i.e. the pre-6b behaviour, and is the escape hatch if capacity reuse is ever
suspected of misbehaving: set it to 1.0 and every Resize reallocates exactly as Init used to.
Above 1.0, a monotonically growing element count (the usual case while a wavefront expands)
stops reallocating almost immediately.
This mirrors the amortized-capacity pattern already used for the quadtree forest in
EnsureCapacity_QuadTreeMesh2D (src/SELF_QuadTreeMesh_2D.f90).
Nodes of different colours represent the following:
Solid arrows point from a submodule to the (sub)module which it is
descended from. Dashed arrows point from a module or program unit to
modules which it uses. Where possible, edges connecting nodes are given different colours to make them easier to distinguish in large graphs.
Nodes of different colours represent the following:
Solid arrows point from a submodule to the (sub)module which it is
descended from. Dashed arrows point from a module or program unit to
modules which it uses. Where possible, edges connecting nodes are given different colours to make them easier to distinguish in large graphs.
Guarantee that pool has at least needed elements, growing it by poolGrowth when it must
reallocate. Existing contents are NOT preserved: every caller re-establishes the array
contents after resizing (the solution through the AMR transfer, the geometry through
GenerateFromMesh, everything else by being written before it is read), so copying the old
data would be wasted bandwidth.