Skip to content

experiment: claude ported everything - #82

Draft
JorgeG94 wants to merge 46 commits into
MOM6-GPU:dev/gpufrom
JorgeG94:benchmark_ALE_retribution
Draft

experiment: claude ported everything #82
JorgeG94 wants to merge 46 commits into
MOM6-GPU:dev/gpufrom
JorgeG94:benchmark_ALE_retribution

Conversation

@JorgeG94

Copy link
Copy Markdown

No description provided.

edoyango and others added 30 commits May 30, 2026 17:13
- Block all local arrays in find_N2 with an nj j-dimension; move the
  find_N2 call before the inner OMP j-loop in set_diffusivity and add
  an internal j-loop inside find_N2
- Make h_bot/k_bot non-optional in find_N2
- Add find_rho_bottom generic interface in MOM_interface_heights,
  renaming the existing routine to find_rho_bottom_1d and adding
  find_rho_bottom_2d that accepts blocked arrays (nj j-dimension);
  move find_rho_bottom call outside find_N2's j-loop
- Replace each outer j-loop in find_N2 and find_rho_bottom_2d with
  individual do j / do i nested loops placed next to each i-loop

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…_to_Kd

Add njblock j-dimension to local and dummy arrays in calculate_bkgnd_mixing
and find_TKE_to_Kd, move j-loops into both routines, and decouple inner
loops to individual j-loops in the caller. Also block TKE_to_Kd, maxTKE,
and kb arrays in set_diffusivity.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Compute dz once unconditionally in set_diffusivity and pass it as a
required argument to find_N2, find_TKE_to_Kd, and add_LOTW_BBL_diffusivity,
removing the redundant per-call thickness_to_dz invocations in each routine.
set_BBL_TKE retains its own thickness_to_dz since it is called externally.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…P loop

Give each branch its own j-loop outside the OMP parallel region, with
the int_tides diagnostics kept inside that loop alongside the call.
Remove stray print statements.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add nj j-block dimension to local arrays (htot, rho_htot, gh_sum_top,
Rho_top, TKE, I2decay, do_i), promote all dummy array args to blocked,
replace scalar j with jstart/jend/nj, and fission the outer j-loop into
three separate ji/jki loop nests. Move the call before the OMP region
and pass full blocked arrays.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Moves add_LOTW_BBL_diffusivity, VBF%Kd_BBL copy, limit_dissipation,
Kd_add, Kd_int/Kd_lay copy, Kd_Work, and Kd_Work_added blocks out of
the OMP parallel do into individual do j=jstart,jend loops. Removes
the now-empty OMP parallel do directive and its enclosing j-loop.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Record each clock's name as cpu_clock_id() registers it, then open an NVTX
range in cpu_clock_begin() and close it in cpu_clock_end(). Every existing
cpu_clock_id() name becomes a named range in an nsys timeline with no
call-site changes.

The range is opened before, and closed after, the mpp clock so that it
encloses it. Begin and end test the same condition, so starts and ends stay
balanced for handles that were never named or that fall outside the table.

The code is guarded behind -DMOM_USE_NVTX, which is undefined by default:
without it the preprocessed source is byte-identical to before and no extra
library is linked. Profiling builds need -DMOM_USE_NVTX and -cudalib=nvtx.

Applied to both the FMS1 and FMS2 infra shims, which are byte-identical to
each other. This also covers the halo and message-passing clocks, which
MOM_domain_infra begins and ends through this module directly rather than
through the MOM_cpu_clock framework wrapper.

Based on Edward Yang's ae67665 ("add nvtx markers to clocks", branch
benchmark_ALE_nvtx_clocks), which introduced the name-table approach and the
range nesting; this adds the build guard so non-NVHPC builds are unaffected.

Co-authored-by: Edward Yang <edward.yang@anu.edu.au>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
First increment of the MOM_kappa_shear GPU port. Converts the two
thickness-to-vertex interpolation loops in Calc_kappa_shear_vertex
(both branches of vertex_shear_OBC_bug) from !$OMP parallel do to
do concurrent, offloading them to the device.

Residency:
- h is host-authoritative in the (host-only) diabatic stack and is
  mutated on host during diabatic (applyBoundaryFluxesInOut), so the
  globally-resident device copy (CS%h) is refreshed with update to(h)
  before the kernels; a plain map(to:) does not refresh a present
  object.
- G%mask2dCu/Cv/T are already device-resident (initialize_MOM).
- h_at_u/h_at_v are device workspace (map alloc), copied back with
  update from for the still-host per-column solver below. enter/exit
  data are mirrored and refcount-neutral (release, not delete).

The per-column solver (kappa_shear_column/find_kappa_tke) stays on
host for now; later increments move it to the device, at which point
the update from(h_at_u,h_at_v) round-trip is removed.

Verification:
- benchmark_ALE ocean.stats bit-identical to the dev/gpu reference (np=1).
- nsys --stats=true confirms both do concurrent loops run on the GPU
  (calc_kappa_shear_vertex_{550,553}_gpu, 12 launches each).
- Reviewed; no code defect found. The >=2-rank bitwise run remains the
  mandatory pre-merge gate and is still pending (single-GPU node).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Groundwork for whole-column GPU kernels (kappa_shear next) that must
compute density derivatives from INSIDE a device region, where the
polymorphic EOS%type%... dispatch is not usable.

- Expose the existing this-free _loc elementals
  calculate_density_derivs_elem_{Roquet_rho,buggy_Wright}_loc as public
  and !$omp declare target.
- Add calculate_density_derivs_elem_loc(form_of_EOS, T, S, pressure,
  drho_dT, drho_dS): a declare-target subroutine that select-case's on
  the integer form and calls the matching _loc kernel, returning mks
  derivatives. Unit rescaling and any `scale` factor remain the
  caller's responsibility (as in calculate_density_derivs_1d). Forms
  without a device _loc kernel hit the default branch; a device-using
  module must FATAL at init on a GPU build before reaching it.

This is the reusable, form-general enabler chosen over a Roquet-scoped
inline; it is aligned with doc 06 6.4's select-case direction but adds
only the device-callable point path (not the full polymorphism removal).

No functional change: nothing calls the dispatcher yet.

Verification: benchmark_ALE ocean.stats bit-identical to the dev/gpu
reference (np=1). Dispatcher is exercised (and Fable-reviewed) in the
follow-up that wires kappa_shear to it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…r on GPU builds

Wires kappa_shear_column's Boussinesq density-derivative computation to the
device-callable path added in 47ac931, so the column solver can later run
inside a target region (the polymorphic calculate_density_derivs interface is
not device-callable).

- MOM_EOS: add get_EOS_form_and_scaling(EOS, form, kg_m3_to_R, C_to_degC,
  S_to_ppt, RL2_T2_to_Pa) so callers can reproduce, host-side, the unit
  rescaling that calculate_density_derivs_1d applies (EOS_type components are
  private).
- kappa_shear_column: under #ifdef __NVCOMPILER_OPENMP_GPU, replace the
  calculate_density_derivs array call with an inline K=2..nzc loop calling
  calculate_density_derivs_elem_loc + the exact scale math of
  calculate_density_derivs_1d(dom=(/2,nzc/), scale=-g_R0). Non-GPU builds keep
  the original polymorphic call verbatim for all EOS forms. On GPU builds only
  ROQUET_RHO and WRIGHT have device _loc kernels; others FATAL.

Verification: benchmark_ALE (ROQUET_RHO, Boussinesq) ocean.stats bit-identical
to the dev/gpu reference (np=1) with the GPU path active on host. Reviewed;
bitwise equivalence confirmed operation-by-operation, verdict ship.

Follow-ups required by the offload step (increment 3), per review: hoist
get_EOS_form_and_scaling + the form FATAL out of kappa_shear_column to the
driver (both are host-only and cannot run in a device region), passing the
form/scaling in; and FATAL on non-Boussinesq on GPU builds (that branch still
uses polymorphic EOS calls). Untested-by-benchmark but code-identical: the
non-unity unit-rescale branch and the WRIGHT dispatcher case.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…d prep)

Prerequisite for offloading the column solver (increment 3): get_EOS_form_and_scaling
and MOM_error are host-only and cannot run inside a device region, so move them out
of kappa_shear_column up into both drivers (Calculate_kappa_shear and
Calc_kappa_shear_vertex).

- Resolve eos_form + the four unit-scaling factors once per driver call (guarded by
  use_temperature), and thread them into kappa_shear_column as new intent(in) args.
- Move the "unsupported EOS form on GPU" FATAL to the drivers, and add a FATAL on
  non-Boussinesq on GPU builds (that branch still uses polymorphic EOS calls).
- Add the five hoisted scalars to the shared() clause of both drivers' !$OMP
  parallel do (they are loop-invariant and read-only in the region; default(private)
  would leave them uninitialized inside the loop -- caught by the bitwise check).

No functional change: same values, computed once per call instead of once per column.

Verification: benchmark_ALE (vertex driver, ROQUET_RHO, Boussinesq) ocean.stats
bit-identical to the dev/gpu reference (np=1). Reviewed; verdict ship, bitwise
safety confirmed by inspection for both drivers. Not runtime-exercised by the
benchmark (inspection-only): the h-point driver, the non-unity unit-scaling branch.

Notes for the offload step: the driver FATAL guards execution not compilation, so
declare target on kappa_shear_column will require #ifdef-excluding the
non-Boussinesq branch; CS (pointer dummy) and tv (pointer-laden) must be
demoted/pre-extracted before declare target.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Prepares the per-column solver for offload: the three workers
(kappa_shear_column, find_kappa_tke, calculate_projected_state) are now
!$omp declare target, so the driver column loop can run inside a target
region in the next step. Device code is generated for all three
(confirmed by -Minfo). Execution is still on the host (the column loop is
still !$OMP parallel do), so this is bitwise-neutral.

To make them device-compilable:
- Demote CS from `pointer` to `type(Kappa_shear_CS), intent(in)` in
  kappa_shear_column and find_kappa_tke (the type is all scalars bar a
  diag pointer that the column math never touches).
- Hoist use_temperature to a kappa_shear_column argument (resolved in the
  drivers) so the device path no longer reads the tv%T pointer via
  associated(); tv is then untouched on GPU builds.
- #ifndef __NVCOMPILER_OPENMP_GPU around the non-Boussinesq branch's
  polymorphic calculate_specific_vol_derivs/calculate_density calls so
  they are excluded from device compilation (unreachable on GPU anyway --
  the driver FATALs on non-Boussinesq).

Notably the 26 early exit/return sites in the two solvers compile for the
device without error on NVHPC 26.5 (the 25.11 exit-under-parallel
miscompile appears fixed); their runtime correctness on device is gated
by the bitwise check in the offload step, not pre-emptive rewrites.

Verification: benchmark_ALE ocean.stats bit-identical to the dev/gpu
reference (np=1). Next step flips the driver !$OMP parallel do to a
target teams loop and adds residency; the solver then runs on the GPU.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Promote the per-J 2-D vertex slabs (u_2d..rho_2d) in Calc_kappa_shear_vertex
to 3-D arrays (u_slab..rho_slab) computed in device do-concurrent passes ahead
of the column loop. The loop bodies are verbatim except that I_hwt is inlined
as a reciprocal multiply and the use_temperature branch is hoisted so each
pass is purely elementwise; answers are bitwise-identical (verified np=1
against repro_ocean.stats on benchmark_ALE, V100/NVHPC 26.5).

The key residency fix relative to the earlier reverted attempt: u_in/v_in and
T_in/S_in (tv%T, tv%S) are already device-resident (mapped in MOM.F90), so a
map(to:) on them does not copy - and the diabatic stack modifies T/S on the
host before set_diffusivity, leaving the device copies stale. They now get an
explicit "target update to" refresh, which resolves the NaN-diffusivity crash
the first attempt hit. dz_3d is a fresh local each call, so its map(to:)
does copy. h_at_u/h_at_v are now consumed on the device by the slab passes,
so increment 1's copy-back is dropped; the slabs are copied back for the
(still host) per-column solver loop, which reads them as X_slab(I,J,k).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Convert Calc_kappa_shear_vertex's per-column loop from a host OMP parallel do
over J into an "omp target teams loop collapse(2)" over (J,I), calling the
declare-target kappa_shear_column on the device. Bitwise-identical (np=1,
benchmark_ALE vs repro_ocean.stats, V100/NVHPC 26.5), which also demonstrates
that the solver's 26 early exit/return sites are runtime-correct on 26.5.

Restructuring, all bitwise-neutral:
- The per-J staging slices kappa_2d/tke_2d become device-resident 3-D staging
  arrays kappa_3d/tke_3d (map(alloc:)), written per column and consumed by new
  elementwise do-concurrent write-back passes for kappa_vertex/tke_io/kv_io.
- The surface pressure is interpolated to vertices on the host ahead of the
  region (surface_pres_2d, verbatim expressions), keeping the possibly-null
  p_surf pointer and the CS%psurf_bug branch out of device code.
- The h_vert fill stays on the host (it is only consumed by the host
  tracer-point averaging when VS_ThicknessMean=true), fed by a guarded
  update from(h_slab).
- All per-column scratch is private at the loop; loop-invariant scalars are
  firstprivate; CS gets a per-call shallow map(to:) in the driver (all scalars
  plus a diag pointer that device code never dereferences). kappa_vertex,
  tke_io and kv_io are mapped to: (not alloc:) so host-set halo/initial values
  survive the full-array update from. diag_N2/S2_init/mean are mapped after
  their conditional host zeroing and copied back under one guard matching
  their consumers (post_data/Bchksum).

Known limitation, to be fixed next: the solver's automatic arrays (sized nzc
or SZK_(GV)) are device-heap allocated, so runs currently need
NVCOMPILER_ACC_CUDA_HEAPSIZE=1073741824 (the default heap is too small:
"DEVICE FORTRAN AUTO ALLOCATION FAILED"). Fixed-size locals will remove this.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… allocs)

The vertex driver's per-column private scratch and the column routines'
automatic locals are sized by GV%ke/nzc, which is not a compile-time constant,
so nvfortran allocated them from the device heap on every column call
(NVFORTRAN-W-0155). That exhausted the default device heap ("DEVICE FORTRAN
AUTO ALLOCATION FAILED" unless NVCOMPILER_ACC_CUDA_HEAPSIZE was raised) and
serialized columns on the device allocator (~2x total model slowdown).

In GPU builds (#ifdef __NVCOMPILER_OPENMP_GPU) these arrays now use a
compile-time ceiling, the new module parameter GPU_nk_max=128, so each device
thread gets stack ("local memory") arrays; kappa_shear_init fatals if
GV%ke > GPU_nk_max when the scheme is active. CPU builds keep the exact-size
declarations. No code queries these arrays' sizes and all accesses are
indexed to nzc/nz, so the enlargement is bitwise-neutral: verified np=1
against repro_ocean.stats at the default device heap, wall time back to the
pre-offload baseline (98s -> 45s), and the W-0155 warnings are gone.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…residency

Review of the column-offload series found one latent halo bug: kv_io's actual
argument (visc%Kv_shear_Bu) is already persistently device-resident, mapped in
set_visc_init BEFORE the restart-reproducibility pass_var halo update, so this
routine's map(to: kv_io) copies nothing and the full-array "update from" wrote
stale pre-pass_var device halos over the corrected host halos - silently
resurrecting the restart non-reproducibility in non-symmetric mode that the
pass_var was added to fix. Invisible to a cold-start np=1 run. Fixed with an
explicit "update to(tke_io, kv_io)" after the maps (tke_io defensively, so the
routine does not depend on it staying unmapped elsewhere).

Also corrects the increment-2 residency comment and drops the redundant
"update to(T_in, S_in)": the actuals are the caller's convection-filtered
T_f/S_f, fresh host locals each call, so their map(to:) already copies (they
are not tv%T/tv%S as the comment claimed). This also establishes that the
earlier reverted slab attempt's NaN was not T/S staleness; the operative fix
was restructuring the interpolation (hoisting the use_temperature branch out
of the do concurrent). And the GPU_nk_max FATAL message now notes it covers
the shared tracer-point column routines too.

Bit-identical at np=1 (benchmark_ALE vs repro_ocean.stats, V100/NVHPC 26.5).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Utheri Wagura <Utheri.Wagura@noaa.gov>
Groundwork for offloading the finite-volume pressure-gradient density
integrals (int_density_dz_generic_plm next), which must evaluate the
equation of state from INSIDE a device region, where the polymorphic
EOS%type%density_elem / density_anomaly_elem dispatch is not usable.

- Expose the existing this-free _loc elementals as public and
  !$omp declare target: density_elem_Roquet_rho_loc and
  density_anomaly_elem_Roquet_rho_loc (Roquet_rho), and
  density_elem_buggy_Wright_loc (Wright). buggy_Wright has no anomaly
  _loc kernel, so its anomaly branch stays unsupported here.
- Add calculate_density_elem_loc(form_of_EOS, T, S, pressure,
  use_rho_ref, rho_ref): a declare-target function that select-case's on
  the integer form and returns, in mks units, either in-situ density or
  (when use_rho_ref) the anomaly relative to rho_ref, by forwarding to
  the matching _loc kernel. It is the density analogue of
  calculate_density_derivs_elem_loc (47ac931) and, like it, leaves unit
  rescaling (EOS%*_to_* factors) and any `scale` factor to the caller,
  exactly as calculate_density_elem does. Combinations without a
  device-callable kernel -- an unsupported form, or the anomaly branch of
  buggy_Wright -- return 0; a device-using module must FATAL at init on a
  GPU build before reaching such a combination.

Bitwise-safety: this is purely additive. Nothing calls the dispatcher
yet, and the exposed elementals are the identical functions the
polymorphic host path already wraps (density_anomaly_elem_Roquet_rho =>
density_anomaly_elem_Roquet_rho_loc, etc.), so no existing arithmetic
moves or changes.

Scope note (reviewer-directed): no new Wright kernels are added here.
benchmark_ALE (ROQUET_RHO) cannot exercise a new Wright anomaly kernel,
and untested numerics is scope creep; parity can come later with a config
that exercises it.

Verification: benchmark_ALE (ROQUET_RHO, Boussinesq, PLM) ocean.stats
bit-identical to the dev/gpu reference (np=1); DEBUG=True redundancy gate
clean (0 'redundant'). "(Ocean pressure force)" clock unchanged at
~20.8 s/24 calls, as expected for an additive change.

Co-Authored-By: Claude Opus <noreply@anthropic.com>
…fload prep)

Host-side bitwise-safe refactor of the finite-volume PGF density integrals,
preparing int_density_dz_generic_plm for offload (increment C next): replace
the polymorphic array calculate_density calls -- which are not callable from a
device region -- with an element-wise path built on the device-callable
dispatcher calculate_density_elem_loc (327f88470). Still host-executed here;
directives come in C.

- MOM_EOS: get_EOS_form_and_scaling gains an optional R_to_kg_m3 out-arg
  (backward-compatible; kappa_shear's two callers are unaffected). Needed to
  reproduce calculate_density_1d's unity fast-path test and non-unity rescale
  exactly.
- int_density_dz_generic_plm: resolve eos_form + the five unit-scaling factors
  once on the host (the accessor and MOM_error are not device-callable), and
  precompute eos_unity mirroring calculate_density_1d's no-rescale test. Under
  #ifdef __NVCOMPILER_OPENMP_GPU, replace the three non-Stanley calculate_density
  array calls (phase 1 vertical r5; phase 2 x-integral r15; phase 3 y-integral
  r15) with element loops calling calculate_density_elem_loc, reproducing
  calculate_density_1d(..., EOSdom_*[, rho_ref]) bit-for-bit: same _loc kernel
  per point, same rescale math (Ta=C_to_degC*T, ..., rho*=kg_m3_to_R,
  rho_ref->R_to_kg_m3*rho_ref). The eos_unity branch is hoisted outside the
  point loop so the loop bodies are branch-free (offload-ready; trap 3). Non-GPU
  builds keep the original array call verbatim in #else. The Stanley calls, and
  the int_density_dz_generic_ppm / _pcm siblings, are untouched.
- On GPU builds, FATAL host-side (before any device region) when the non-Stanley
  path hits an unsupported EOS form / anomaly mode: the dispatcher covers only
  ROQUET_RHO (density and anomaly) and buggy_Wright (density only).

Bitwise-safety: (i,n)/(i,n15) loops cover exactly the EOSdom_h5 / EOSdom_q15 /
EOSdom_h15 index ranges the array calls work on; per-point density values are
order-independent, so the loop order does not matter; the _loc kernels are the
identical functions the polymorphic path already wraps. Multiplying by an
unscaled factor of exactly 1.0 is bit-exact, so the non-unity branch matches
calculate_density_1d's guarded rho_scale multiply.

Verification: benchmark_ALE (ROQUET_RHO, Boussinesq, PLM) ocean.stats
bit-identical to the dev/gpu reference (np=1); the #ifdef GPU element path is the
executed arithmetic on the GPU build. DEBUG=True redundancy gate clean
(0 'redundant'). "(Ocean pressure force)" clock ~20.4 s/24 calls, unchanged (host
execution; directives land in C). Not runtime-exercised by the benchmark
(inspection-only, code-identical to the array path): the Stanley branch, the
use_rho_ref=.false. (inaccurate-form) branch, and the non-unity unit-scaling
branch.

Co-Authored-By: Claude Opus <noreply@anthropic.com>
First quadrature-phase offload of the finite-volume PGF density integrals.
The phase-1 vertical-integral loop (-> dpa, intz_dpa) now runs in a
!$omp target teams loop collapse(2) over (j,i) when the configuration is
device-supported; the host loop is unchanged for every other case.

- New host-resolved gate offload_phase1 = (.not.use_stanley_eos) .and.
  eos_unity .and. use_rho_ref, set only on GPU builds (#ifdef). When false
  (CPU build, Stanley, non-unity units, or inaccurate-form) the original
  host loop runs verbatim -- no code duplicated (single else block).
- Device kernel: per-(i,j) size-5 quadrature locals T5l/S5l/p5l/r5l
  (compile-time sized, so no device auto-allocation) reproduce the host
  point-for-point, call the declare-target dispatcher calculate_density_elem_loc,
  and write dpa(i,j)/intz_dpa(i,j) via Boole's rule. dpa/intz_dpa are
  independent across (i,j).
- Residency: e, dpa, intz_dpa are already device-resident (caller maps);
  the region carries no map() clauses and relies on presence. z0pres (a
  host-computed local) is mapped to/released around the region. The layer
  edge reconstructions T_t/T_b/S_t/S_b are mapped to() once per call by the
  caller (PressureForce_FV_Bouss) before the k-loop and released after
  (guarded to the plm/ppm path); they are read-only and identical for all k.
- After the region, target update from(dpa) (and intz_dpa when present)
  refreshes the host: phases 2 & 3 (still host) read dpa, and the caller's
  kept "update to(dpa,...)" would otherwise overwrite the device values with
  a stale host copy. This intermediate copy-back is removed once phases 2/3
  also run on device (increment D) and the caller round-trip is deleted (E).

Bitwise-safety: the size-5 locals hold the identical per-point values as the
old i*5+n slices; rho_anom/dpa/intz_dpa are computed independently per (i,j)
from those points, so collapsing the (j,i) loops reorders no FP op; the _loc
kernel and Boole coefficients are unchanged. The device path is entered only
for the unscaled, accurate, non-Stanley configuration; all others keep the
verbatim host arithmetic.

Sole caller: PressureForce_FV_Bouss (audited: only call site of
int_density_dz_generic_plm), where all no-clause arrays are device-present,
so the compiler's implicit map(tofrom:) are runtime no-ops.

Verification: benchmark_ALE (ROQUET_RHO, Boussinesq, PLM) ocean.stats
bit-identical to the dev/gpu reference (np=1); log tmp_local_artifacts/incC_verify.log.
Execution proof: NVCOMPILER_ACC_NOTIFY=1 shows the F1L639 kernel launching
(tmp_local_artifacts/notify_incC.log). Compile proof: -Minfo reports "Generating
NVIDIA GPU code / Loop parallelized across teams collapse(2)" at the teams loop.
DEBUG=True redundancy gate clean, 0 'redundant'
(tmp_local_artifacts/debug_incC.log). "(Ocean pressure force)" clock 19.49 s/24
(was 20.40), consistent with phase 1 on device; the ~2-3 s target awaits D+E
(round-trip removal). Not benchmark-exercised (inspection-only): Stanley,
inaccurate-form, and non-unity-scaling paths (host, unchanged).

Co-Authored-By: Claude Opus <noreply@anthropic.com>
…grals) to GPU

Second and third quadrature-phase offload of the finite-volume PGF density
integrals. The x-integral (-> intx_dpa) and y-integral (-> inty_dpa) loops now
each run in a !$omp target teams loop collapse(2) when the configuration is
device-supported; the host loops are unchanged for every other case.

- New host-resolved gates offload_phase2/offload_phase3 = offload_phase1's
  condition, kept as separate flags so phase 3 can be forced off for x-only
  failure bisection.
- Device kernels: per-cell size-15 q-point locals T15l/S15l/p15l/r15l and
  dz_x_l/dz_y_l (compile-time sized) reproduce the host loops point-for-point,
  call the declare-target dispatcher calculate_density_elem_loc for all 15
  points, and write intx_dpa(I,j)/inty_dpa(i,J) via Boole's rule. Both the
  hWght>0 mass-weighting branch and its else branch are carried verbatim from
  the host path (they compute the same set of private corner scalars, so there
  is no divergent conditional array write). intx_dpa/inty_dpa are independent
  across cells.
- Residency: dpa (written on the device by phase 1) is now read by phases 2/3
  on the device with no host round-trip. z0pres is mapped once to bracket all
  three phases (its enter-data moved above phase 1, release moved after phase 3).
  e/dpa/intx_dpa/inty_dpa are already device-resident; T_t/T_b/S_t/S_b are
  mapped by the caller (unchanged from the phase-1 increment).
- Phase 1's interim "update from(dpa,intz_dpa)" is replaced by a single
  consolidated "update from(dpa, intz_dpa, intx_dpa, inty_dpa)" after phase 3,
  so the caller's kept "update to(dpa,...)" still sees current host values. That
  consolidated copy-back and the caller round-trip are removed in increment E.

Bitwise-safety: the size-15 locals hold the identical per-point values as the
old i*15+(m-2)*5 slices; the pressure recurrence over n and the Boole quadrature
are copied verbatim and run serially within each cell's thread; intx_dpa/inty_dpa
are computed independently per cell, so collapsing the (j,I)/(J,i) loops reorders
no FP op. The device path is entered only for the unscaled, accurate, non-Stanley
configuration; all others keep the verbatim host arithmetic (host else-paths are
byte-identical to the prior increment, verified by diff).

Verification: benchmark_ALE (ROQUET_RHO, Boussinesq, PLM) ocean.stats
bit-identical to the dev/gpu reference (np=1); log tmp_local_artifacts/incD_verify.log.
Compile proof: -Minfo generates GPU kernels F1L750 (x) and F1L946 (y), each
"Loop parallelized across teams collapse(2)". DEBUG=True redundancy gate clean,
0 'redundant' (tmp_local_artifacts/debug_incD.log). "(Ocean pressure force)" clock
9.66 s/24 (was 19.49), halved by moving the horizontal integrals on-device; the
remaining host round-trip is removed in increment E. Not benchmark-exercised
(inspection-only): Stanley, inaccurate-form, and non-unity-scaling host paths.

Co-Authored-By: Claude Opus <noreply@anthropic.com>
…increment E, caller side)

Removes the caller half of the int_density_dz_generic_plm device round-trip:
the four integral outputs (dpa, intz_dpa, intx_dpa, inty_dpa) are now consumed
entirely in the caller's own device regions (interface-pressure accumulation and
the PFu/PFv loops), so the host no longer pushes them to the device before use.

- New host-resolved gate idz_on_device, set only on GPU builds (#ifdef
  __NVCOMPILER_OPENMP_GPU), that mirrors int_density_dz_generic_plm's internal
  offload_phase1 condition EXACTLY on the generic_plm call path: use_EOS .and.
  use_ALE .and. Recon in {1,3} .and. .not.use_stanley_pgf .and.
  .not.use_inaccurate_pgf_rho_anom .and. the four EOS unit-rescale factors == 1
  (queried via get_EOS_form_and_scaling). When true, the routine leaves its
  outputs device-resident; when false (CPU build, Recon scheme 2, Stanley,
  inaccurate-form, non-unity units) the host round-trip is kept verbatim.
- Drop the per-call "defensive" update to(dpa,intx_dpa,inty_dpa,intz_dpa) when
  idz_on_device: the generic_plm kernels already wrote them on the device, so the
  copy would clobber them with a stale host copy. Kept (guarded) for the host path.
- Narrow the full update from(e): e is built on the device, and under
  idz_on_device the blanket ALE pull is skipped. The pull is retained for its
  genuine host consumers -- host-run int_density, the correction/reset-intxpa
  reference-interface searches, the MassWt diagnostics, and (this is the
  stale-host fix) the SAL/tides "new answers" blocks that read e(:,:,1) and
  read-modify e(:,:,K) on the host.
- Add a guarded update from(intx_dpa,inty_dpa) at routine exit for the SAL/tides
  horizontal-gradient diagnostics (the only host readers of those two arrays),
  guarded to idz_on_device .and. .not.bq_sal_tides .and. (calculate_SAL .or. tides).

Verification: benchmark_ALE (ROQUET_RHO, Boussinesq, PLM) ocean.stats bit-identical
to the dev/gpu reference (np=1); log tmp_local_artifacts/incE_fix_verify.log. The
SAL/tides and correction/reset guard terms are false in benchmark_ALE, so this is
inspection-only for those configs; "(Ocean pressure force)" clock ~2.3 s/24 (was
~20). Note the SAL/tides-new-answers GPU write-back (host e/pa mods pushed back to
the device) is a separate, pre-existing gap -- idz_on_device is the fully-GPU,
non-SAL/tides PGF path. Remaining gates: np>=2 multi-GPU verify (1 V100 here) and a
plain-CPU compile of the #else branches.

Co-Authored-By: Claude Opus <noreply@anthropic.com>
…ncrement E)

Removes the routine half of the device round-trip. Phases 1-3 already compute
dpa/intz_dpa/intx_dpa/inty_dpa on the device; this drops the consolidated
device->host copy-back at the end of the routine. The sole caller
(PressureForce_FV_Bouss) now consumes all four outputs in its own device regions
(prior commit), so no host refresh is needed. Only the per-call z0pres scratch
release is kept.

Also rewrites the offload_phase2/offload_phase3 comment: they must track
offload_phase1 now that the round-trip is gone. Decoupling a phase would leave its
host-computed integral stranded on the host while the caller (which mirrors this
gate as idz_on_device and no longer copies back) reads a stale device copy. The
"settable independently for x-only failure bisection" affordance no longer holds.

Bitwise-safety: no arithmetic changes; only OpenMP data-movement directives are
removed. On CPU builds (offload_phase1/2/3 all false) the removed blocks were
already skipped, so host builds are unaffected.

Verification: benchmark_ALE (ROQUET_RHO, Boussinesq, PLM) ocean.stats bit-identical
to the dev/gpu reference (np=1); log tmp_local_artifacts/incE_fix_verify.log.

Co-Authored-By: Claude Opus <noreply@anthropic.com>
… the GPU

Mirrors the Calc_kappa_shear_vertex offload for the tracer-point driver used when
VERTEX_SHEAR=False (the plain benchmark case). All the shared machinery was already
in place -- the three column workers are declare target, the density-derivs element
dispatcher covers WRIGHT and ROQUET_RHO, eos_form/scaling are hoisted host-side in
this driver too, and GPU_nk_max sizes the per-column scratch -- so this is only the
final loop-flip.

- Hoist thickness_to_dz out of the j-loop to a whole-domain dz_3d (halo_size=0), and
  precompute surface_pres_2d from the (possibly-null) p_surf pointer on the host,
  ahead of the device region.
- Flip the column loop from !$OMP parallel do (host) to !$omp target teams loop
  collapse(2) over (j,i). The per-J 2-D slabs (h_2d/u_2d/...) are gone: the device
  loop reads the mapped 3-D inputs (h, u_in, v_in, tv%T, tv%S, dz_3d) directly -- the
  h-point driver has no vertex interpolation, so no slab staging is needed. Results
  stage in device-only kappa_3d/tke_3d (was the per-J kappa_2d/tke_2d); a masked
  do concurrent then writes kappa_io/tke_io/kv_io.
- Per-column private scratch (Idz/h_lay/.../kappa/tke/.../kc/kf) is GPU_nk_max-sized
  under #ifdef __NVCOMPILER_OPENMP_GPU (compile-time constant -> device stack, not the
  serialized device-heap path); SZK_(GV) on CPU builds. Bitwise-safe (no size()/bounds
  intrinsics on these in the routine).
- Residency: h host-authoritative -> map(to:)+update to; u_in/v_in (set_diffusivity's
  u_h/v_h) and dz_3d are fresh host locals -> map(to:); CS shallow map(to:); outputs
  map(to:)+update to (preserve host halos) then update from; diag_* mapped after their
  conditional host zeroing, copy-back guarded by the same predicate. tv%T/tv%S are read
  directly on the device here (this path passes raw tv, no full_convection filtering,
  unlike the vertex driver's fresh T_f/S_f): the T/S data is persistently mapped via
  MOM's CS%tv but on a different descriptor, so this routine's tv dummy is explicitly
  mapped map(to: tv, tv%T, tv%S) + update to (the MOM_set_viscosity idiom) so the device
  can resolve tv%T/tv%S -- update to alone does not attach the descriptor.

Verification: benchmark (VERTEX_SHEAR=False, EQN_OF_STATE=WRIGHT, Boussinesq) ocean.stats
bit-identical to the dev/gpu reference (np=1); log tmp_local_artifacts/kshpoint_verify2.log.
Execution proof: NVCOMPILER_ACC_NOTIFY shows the device kernel calculate_kappa_shear
line 328 (target teams loop) launching grid=64800 (360x180 columns) once per call, plus
the write-back do concurrent at line 469. "(Ocean kappa_shear)" ~0.78 s/6 hits (the
benchmark kappa_shear workload is small; the kernel is teams-only block=(1,1,1), the same
occupancy headroom the vertex driver carries). Untested-but-code-identical: the non-unity
unit-rescale branch and the non-temperature (rho) branch. Deferred pre-merge gate:
>=2-GPU / np>=2 verify (this node has 1 V100).

Co-Authored-By: Claude Opus <noreply@anthropic.com>
Co-authored-by: Utheri Wagura <Utheri.Wagura@noaa.gov>
…ix W-0155)

The two !$omp target teams loop regions in tracer_epipycnal_ML_diff had per-column
private arrays declared dimension(SZK_(GV)) / dimension(SZK_(GV)*2), i.e. sized by
the runtime GV%ke.  NVFORTRAN cannot place a runtime-sized private array in local
(stack) memory (NVFORTRAN-W-0155) and spills it to slow device global memory, with a
"results may be incorrect" warning.

Fix mirrors the kappa_shear GPU_nk_max pattern (KNOWLEDGE row 21): under
#ifdef __NVCOMPILER_OPENMP_GPU the per-column scratch (h_demand_L/R, h_used_L/R,
h_supply_frac_L/R real, and kbs_Lp/Rp integer + left_set/right_set logical, the
last four dimension GPU_nk_max*2) is given the compile-time-constant bound
GPU_nk_max=128; CPU builds keep the exact SZK_(GV) sizes.  A GPU-only FATAL guard in
tracer_epipycnal_ML_diff catches GV%ke > GPU_nk_max instead of silently corrupting.

Bitwise-safe: no size()/ubound()/lbound() on any of these arrays (loops are bound by
num_srt(i,j) <= nz), so enlarging the declared extent changes no arithmetic.

Pre-existing issue (not from the pressure-force/kappa stack); surfaced by a clean
rebuild.  Verification: benchmark_ALE (which exercises "Ocean epipycnal diffuse
tracer") ocean.stats bit-identical to the dev/gpu reference (np=1); the ten W-0155
lines are gone from the build log.  Deferred: >=2-GPU verify (1 V100 here).

Co-Authored-By: Claude Opus <noreply@anthropic.com>
…the ZSTAR regrid

Verbatim refactor (no OpenMP directives, no behaviour change) that prepares the ALE
ZSTAR regrid for GPU offload.  The per-column regrid kernels dereference the regridding
control structure on entry, and regridding_CS holds a nested zlike_CS (a pointer) whose
coordinateResolution is allocatable -- exactly the nested-derived-type-with-allocatable
shape that is fragile to map onto the device.  Rather than map it, the scalar/array
parameters are hoisted so the kernels take them explicitly.

- coord_zlike: new build_zstar_column_loc(nk, min_thick, coordinateResolution, ...) holds
  the former build_zstar_column body verbatim; build_zstar_column becomes a thin wrapper
  passing CS%nk/CS%min_thickness/CS%coordinateResolution.  _loc is public.
- MOM_regridding: new filtered_grid_motion_loc(nk_tgt, filt_shallow, filt_deep,
  old_grid_wt, ...) and adjust_interface_motion_loc(nk_tgt, min_thick, ...) hold the
  former bodies verbatim (CS%nk -> nk_tgt, CS%depth_of_time_filter_* -> filt_*,
  CS%old_grid_weight -> old_grid_wt, CS%min_thickness -> min_thick); the CS-taking
  filtered_grid_motion / adjust_interface_motion become thin wrappers.

No call site changes anywhere: every existing caller (build_{zstar,sigma,rho,...}_grid,
MOM_diag_remap) keeps using the CS-taking wrappers unchanged.  This is the first of two
commits; the device directives + residency come next (GPU-gated).

Bitwise-safety: the _loc bodies are the originals with CS%x textually replaced by the
matching argument; the wrappers pass exactly those CS fields, so the arithmetic and the
values are identical.  Verification: benchmark_ALE ocean.stats bit-identical to the
dev/gpu reference (np=1) -- CPU-path gate (the _loc kernels still run on the host; no
directives yet).  log tmp_local_artifacts/ale_refactor_verify3.log.

Co-Authored-By: Claude Opus <noreply@anthropic.com>
JorgeG94 and others added 16 commits July 20, 2026 10:15
Second of two commits (the first extracted the device-callable _loc kernels).  This offloads
build_zstar_grid's per-column loop to the device for the ZSTAR coordinate (the mode benchmark_ALE
uses), building on the _loc refactor.

- build_zstar_column_loc / filtered_grid_motion_loc / adjust_interface_motion_loc are now
  !$omp declare target.  The "should never happen" grid sign/tangling/negative-thickness
  MOM_error + write + stop asserts, and the debug-only blocks (debug=.false.) and their z_act
  scratch, are #ifndef __NVCOMPILER_OPENMP_GPU (kept on the host, compiled out of device builds).
  filtered_grid_motion_loc's massless-column early return is expressed as an if-guard
  (if (.not. massless)) since device loops cannot return early.
- build_zstar_column_loc's zScale is now a required positional arg (z_scale) ahead of the two
  optional shelf args, so the device call sites need no keyword arguments (nvfortran rejects
  keyword-form args inside a target region).
- build_zstar_grid: the !$OMP parallel do over (j,i) becomes a !$omp target teams loop collapse(2);
  the land-mask `cycle` is an if-guard; the regridding_CS scalars + coordinateResolution are
  hoisted into plain locals on the host (the private component is fetched via the new
  get_zlike_coord_res accessor) so the device loop never dereferences the nested (pointer +
  allocatable) control structure; per-column zNew/zOld are fixed-size (GPU_nk_max=128, module
  parameter) under #ifdef with a FATAL guard when CS%nk/GV%ke exceed it.
- Residency: h is host-authoritative in the (host) ALE stack -> update to(h); nom_depth_H and the
  hoisted coordRes -> map(to:); dzInterface is device-written and copied back (update from) for the
  subsequent host remap that reads it (ALE_remap_set_h_vel_via_dz, remap_OBC).  calc_h_new_by_dz and
  the rest of ALE stay on the host for now.

Verification: benchmark_ALE (ZSTAR, PLM, ROQUET_RHO, Boussinesq) ocean.stats bit-identical to the
dev/gpu reference (np=1); log tmp_local_artifacts/ale_port_verify3.log.  Execution proof:
NVCOMPILER_ACC_NOTIFY shows the device kernel build_zstar_grid line 1728 launching grid=2432 per
call (teams-only block=(1,1,1); the _loc kernels are inlined).  Untested-but-code-identical: the
ice-shelf (frac_shelf_h) branch and the RHO/SIGMA/HYCOM coordinate builders (host, unchanged).
Deferred pre-merge gate: >=2-GPU / np>=2 verify (this node has 1 V100).

Co-Authored-By: Claude Opus <noreply@anthropic.com>
First device increment of the set_diffusivity port, on Ed's j-blocked groundwork.  find_N2 is the
dominant, self-contained, EOS-bearing piece of the active benchmark_ALE path (find_TKE_to_Kd and
set_density_ratios are never called there and stay host).

- EOS: the per-interface calculate_density_derivs (ROQUET_RHO) is evaluated on the device through
  the declare-target calculate_density_derivs_elem_loc dispatcher.  eos_form + unit scaling are
  resolved once on the host (get_EOS_form_and_scaling, with a GPU-only FATAL if the form is not
  ROQUET_RHO/WRIGHT); the scaling is always applied, so the unscaled (unity) case reproduces the
  former array call bit-for-bit.  This is the kappa_shear increment-2b pattern.
- Loop forms: the pressure recurrence (serial down/up-K) + the two bottom-boundary-layer
  do k=nz,2,-1 loops become per-column !$omp target teams loop collapse(2) over (j,i) with a serial
  k inside; the former cross-column do_any/exit early-out is dropped -- each device thread runs its
  own k loop and terminates via its do_i flag, bit-for-bit equivalent.  The elementwise N2_lay/N2_int,
  dRho_int init/copy and dz_BBL_avg loops become do concurrent.
- Residency: the full-domain EOS inputs (T_f/S_f and the tv descriptor) are mapped ONCE by the caller
  (set_diffusivity) around the whole jstart loop, and tv%T/tv%S are refreshed there (update to) since
  they are the diabatic-mutated persistently-mapped fields; h is already persistently device-resident.
  find_N2 itself maps only the per-block scratch (dz/pres/h_amp + the 3-D block arrays); doing the
  full-domain maps per block would re-copy them on every j-row (njblock=1) and dominate the runtime.
  dRho_int/N2_int/N2_lay/N2_bot (find_N2 outputs) and pres/dz_BBL_avg (read by the host find_rho_bottom
  that follows) are update from before the per-block region closes.  find_rho_bottom stays on the host.

Verification: benchmark_ALE ocean.stats bit-identical to the dev/gpu reference (np=1); logs
tmp_local_artifacts/setdiff_findN2_verify.log (+ ..._perf_verify.log for the residency fix).  Execution
proof: NVCOMPILER_ACC_NOTIFY shows the device kernels find_n2 lines ~1284 (pres+EOS), ~1333 and ~1368
(bottom-BL) launching per block.  Timing: "(Ocean set_diffusivity)" 3.4 s/24 (host baseline 3.2 s;
find_N2-on-GPU is ~break-even at this size because the bkgnd/assembly/add_LOTW_BBL tail is still host,
so find_N2 pays a host<->device boundary each call and the njblock=1 kernels are teams-only/low-occupancy;
the win materialises once the surrounding path is on-device).  Remaining set_diffusivity tail left on host
by design.  Deferred: >=2-GPU verify (1 V100 here).

Co-Authored-By: Claude Opus <noreply@anthropic.com>
…le (increment 1)

First increment of the CPU-preserving ePBL GPU port (the ePBL half of the mislabeled "(Ocean vert
remap)" region).  Bitwise-neutral enablers: !$omp declare target the pure per-column kernels so
ePBL_column can call them once it (and the driver loop) are offloaded in later increments.  The same
source serves the host and device paths.

- MOM_energetic_PBL.F90: module-scope declare target on the pure scalar helpers exp_decay_TKE_adjust,
  find_PE_chg, find_PE_chg_orig, find_Kd_from_PE_chg, find_mstar, mstar_Langmuir (no host-sinks, no
  runtime-sized array locals -> nothing to gate).
- MOM_wave_interface.F90: declare target on get_Langmuir_Number, get_StokesSL_LiFoxKemper,
  ust_2_u10_coare3p5 (the LF17 statistical-wave Langmuir path that benchmark_ALE uses:
  USE_LA_LI2016=True, USE_WAVES=False).  get_Langmuir_Number is #ifdef-split: on GPU builds only the
  LF17 branch (call get_StokesSL_LiFoxKemper) is compiled -- the wave-model branches (TESTPROF/
  SURFBANDS/DHH85, their US_H/VS_H(SZK) + allocatable StkBand scratch, the Get_SL_Average_* calls, the
  Null-method + misalignment MOM_errors) are host-only.  This reproduces the host LF17 result bit-for-bit
  (Dpt_LASL is unused on the LF17 path; LA_Misalignment defaults false).  ust_2_u10_coare3p5's vonKar<0
  config MOM_error is #ifndef-gated off device.

TODO before merge: a GPU build currently compiles the LF17-only get_Langmuir_Number for all callers;
add a host-side FATAL guard for GPU builds with a non-LF17 WaveMethod or LA_Misalignment (benign for
benchmark_ALE, which is LF17/no-misalignment).

Verification: benchmark_ALE ocean.stats bit-identical to the dev/gpu reference (np=1); log
tmp_local_artifacts/epbl_inc1_verify.log.  This device-compiles the full ePBL+wave kernel set (the key
de-risking) while being bitwise-neutral (device versions unused until ePBL_column + the driver loop are
offloaded).  Remaining: increment 2 = declare target ePBL_column + fixed-size its SZK locals; increment
3 = flip the energetic_PBL driver loop to a target region + residency.  Deferred: >=2-GPU verify.

Co-Authored-By: Claude Opus <noreply@anthropic.com>
…ment 2)

Second increment of the CPU-preserving ePBL GPU port.  ePBL_column (the ~1061-line iterative
single-column solver: outer MLD iteration + down-K TKE budget with an inner Newton solve) is now
!$omp declare target, so it device-compiles with its declare-target helper kernels (find_mstar,
find_PE_chg*, get_Langmuir_Number, ...) from increment 1.  Still bitwise-neutral: ePBL_column is
host-called until the driver loop is offloaded (increment 3); on GPU builds its host version uses
the #ifdef paths below, which are bit-identical for benchmark_ALE.

- GPU_nk_max=128 module parameter; ePBL_column's per-column private automatic arrays (pres_Z/hb_hs,
  the dT_to_dColHt.../hp_a.../Sh_b block, MixLen_shape/h_dz_int/Kddt_h, and mech_TKE_k/conv_PErel_k/
  nstar_k/dT_expect/dS_expect/num_itts) are given compile-time-constant sizes under
  #ifdef __NVCOMPILER_OPENMP_GPU (KNOWLEDGE row 21), keeping SZK_(GV) on CPU builds.  The dummy args
  and the dimension(20) iteration arrays are already fine.  (ePBL_BBL_column's analogous locals are
  fixed-sized too via the shared decls; it is dead in benchmark_ALE, not declare target.)
- The equation-discovery (eqdisc) mixing-length/velocity calls (kappa_eqdisc/get_eqdisc_v0/v0h) are
  #ifndef-gated off device (dead in benchmark_ALE: EPBL_EQD_* absent -> .false.); the transLay
  mixing-length + v0_scale=0 paths run, bit-identical.
- FATAL guards in energetic_PBL_init on GPU builds: GV%ke > GPU_nk_max, and any eqdisc* enabled.

TODO before merge (carried from increment 1): host-side guard for GPU builds with a non-LF17
WaveMethod / LA_Misalignment (get_Langmuir_Number compiles the LF17-only body on GPU).

Verification: benchmark_ALE ocean.stats bit-identical to the dev/gpu reference (np=1); log
tmp_local_artifacts/epbl_inc2_verify.log.  ePBL_column device-compiles cleanly (the key de-risking).
Remaining: increment 3 = flip the energetic_PBL driver loop to a target teams loop + one driver-level
data region (residency) so the solver runs on the device.  Deferred: >=2-GPU verify.

Co-Authored-By: Claude Opus <noreply@anthropic.com>
Flip the energetic_PBL driver (do j / do i over ocean columns calling the
now-declare-target ePBL_column) to a single `!$omp target teams loop
collapse(2)` with one driver-level data region. Columns are read directly
from the 3-d inputs (2-d staging slabs and the per-j Kd_2d staging removed,
bit-identical); thickness_to_dz is hoisted whole-domain (dz_3d); the ~14
per-column scratch arrays are teams-loop private + fixed-size under
GPU_nk_max. Residency: map(to:) the inputs + CS + tv/tv%T/tv%S (with
update-to for the map-to-present trap) + Waves; map(from:) Kd_int / ML_depth
/ BBL_depth. Forcing (fluxes%ustar) and the MLD first guess are hoisted to
plain host arrays so the device never touches a fluxes% pointer member or a
CS allocatable. Dead-in-benchmark_ALE branches (BBL mixing, options_diff,
stochastics, per-column diagnostics, report_avg_its) are #ifndef'd off the
device path and backed by runtime FATAL guards so an unsupported config
fails loudly rather than silently wrong. CPU path is preserved verbatim
under #else.

Device-callable Langmuir chain (MOM_wave_interface): the LF17 path
dereferenced the Waves *pointer* on-device, which nvfortran will not attach
under -gpu=mem:separate. Replace it with a pointer-free
`wave_LF17_params` bundle (allocatable-free, maps cleanly) populated
host-side by set_wave_LF17_params and threaded through
get_Langmuir_Number_LF17 -> get_StokesSL_LiFoxKemper -> ust_2_u10_coare3p5.
get_Langmuir_Number_LF17 uses the in-routine `!$omp declare target` block
form (the module name-list form silently failed to emit its device symbol).
get_Langmuir_Number is restored to its full CPU form (no longer
declare-target).

STATUS: ePBL executes correctly on the device (no illegal-address; T/S/mass
bit-identical to displayed precision). NOT YET bitwise: the energy
diagnostic differs at ~1e-13, the documented host-vs-device x**(1/n)
transcendental floor (KNOWLEDGE row 5) from the 4 `(...)**C1_3` cube-roots
in the ePBL_column vstar budget and the Langmuir `LA**(-1.33)` power.
Closing it needs cuberoot() for the **C1_3 sites + an explicit-form rewrite
of the arbitrary power, applied to the one CPU+GPU source, plus a
regenerated golden (the fix changes the CPU answer). Deferred by decision.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ispatcher (increment 1)

Offload prep for the thickness_diffuse_full GPU port. Swap the three live
calculate_density_derivs generic array calls (the u-point slopes :933, the
v-point slopes :1246, and the surface u block :1545) for the element-wise
device-callable dispatcher calculate_density_derivs_elem_loc, folded into
the existing fill loops so the evaluated range matches the array call
exactly. get_EOS_form_and_scaling resolves the form + unit factors once
(host); the scaling is always applied (unity-exact for the unscaled case),
reproducing the former array calls bit-for-bit. Adds the same
GPU-build FATAL guard as find_N2 for EOS forms without a device kernel.
This mirrors the find_N2/kappa EOS-dispatcher pattern; the calculate_density
_second_derivs (Stanley) path is left untouched (dead in benchmark_ALE).

No device directives execute yet (elem_loc runs on the host here) -- this is
prep so the u/v slope loops can be offloaded next without a polymorphic EOS
call on the device.

The GPU port of this file follows the structure of Ed Yang's
edoyango/port/thickness_diffuse branch (the (i,j,k)-blocking and directive
placement) as a reference; his branch is not merged (it is stale and
overlaps our density/EOS work) but informs the approach.

BITWISE: neutral -- benchmark_ALE np=1 ocean.stats is byte-identical to the
pre-increment (ePBL-device) baseline (tmp_local_artifacts/td_baseline_ocean
.stats). The only residual vs the canonical golden repro_ocean.stats is the
separately-deferred ePBL x**(1/n) transcendental floor (~1e-13 in energy),
which this commit does not touch.

Co-authored-by: Edward Yang <edward.yang@anu.edu.au>
…d prep)

Promote the ten per-column slope-scratch arrays of thickness_diffuse_full
from 2-D (i,K) to 3-D (i,j,K) so the u/v slope+flux loops can later be
fully collapsed onto the device with the scratch device-resident across the
slope->flux passes: drdi_u/drdj_v, drdkDe_u/_v, dzN2_u/_v, Sfn_unlim_u/_v,
slope2_Ratio_u/_v (v-arrays carry the SZJB_ symmetric-memory j-extent).
Declarations + every index site updated (~50 sites across the u-block and
v-block). c2_dz_u/_v are left 2-D (FGNV path, dead in benchmark_ALE, host-only).

No directives yet: this is a pure storage transform, computation unchanged.
The promoted arrays remain in the CPU !$OMP parallel do private() clauses
transitionally (per-thread copies, still bit-for-bit; the do-j loop writes
disjoint j-slices) -- the target-teams-loop offload (increment 2b) replaces
those clauses.

BITWISE: neutral -- benchmark_ALE np=1 ocean.stats byte-identical to the
pre-increment ePBL-device baseline (tmp_local_artifacts/td_baseline_ocean.stats,
diff empty). Only residual vs the canonical golden is the separately-deferred
ePBL transcendental floor.

The GPU offload of this file follows the structure of Ed Yang's
edoyango/port/thickness_diffuse (3-D scratch promotion + full collapse) as a
reference.

Co-authored-by: Edward Yang <edward.yang@anu.edu.au>
Offload the whole thickness_diffuse_full compute (setup + u/v slope +
streamfunction + flux + MEKE work) to the device, one target-data region,
building on the 3-D scratch of increment 2a and the elem_loc EOS of
increment 1.

- CPU-neutral refactor first (verified bitwise-neutral before any directive):
  fuse the per-K EOS-fill do-I loop into the slope do-I loop (scalarizing the
  per-I EOS temporaries) so the slope nest is a tight do j / do K / do I;
  split the outer do-j into slope and flux j-loops; swap the flux loops to
  do I / serial do K (per-column uhtot/vhtot recurrence order preserved);
  move the now-3-D scratch out of the OMP private() clauses to shared.
- Loop forms (all `#ifdef __NVCOMPILER_OPENMP_GPU`, original !$OMP parallel do
  kept under #else so CPU threading is retained): slope loops ->
  `target teams loop collapse(3)` over (j,K,I) with per-iteration scalars
  private; the uhtot/vhtot flux recurrence + the h_avail_rsum setup running
  sum -> collapse(2) + serial do K (KNOWLEDGE 4.1 branch 5); the remaining
  elementwise/init/assembly loops -> collapse(2). 16 device kernels generate,
  no W-0155.
- Dead-in-benchmark_ALE paths #ifndef'd off the device with host FATAL guards:
  use_stanley (second derivs), FGNV streamfn + streamfn_solver + c2_dz,
  skeb_use_gm/STOCH, non-Boussinesq tv%SpV_avg, and the slope/sfn diagnostics.
  The layer-1 v-point EOS array call is converted to the elem_loc dispatcher.
- Residency: one target enter/exit data region; map(to:) h (+update to), e, dz,
  T, S, Kh_u/v, int_slope_u/v, CS, slope_x/y (present-guarded) and the
  host-zeroed Slope_/hN2 PE arrays; map(alloc:) pres, dz-scratch, h_avail*,
  uhtot/vhtot, Work_u/v, uhD/vhD and the 3-D slope scratch; map(from:) uhD/vhD.
  All exit maps use `release` (never `delete`).
- Pointer/allocatable hoist (the map-to-present / Waves-pointer-trap class):
  MEKE%GM_src, CS%GMwork and tv%p_surf hoisted to plain local arrays
  (copied in, written/read on device, copied back) so no allocatable
  component of a pointer/derived type is dereferenced on the device.

Two fixes found via the crash -> compute-sanitizer -> fix loop: (1) tv%p_surf
IS associated in benchmark_ALE, so hoist it rather than FATAL; (2) an illegal
address that surfaced downstream in set_viscous_BBL (pbv attach) was traced
(sanitizer + a revert-to-2a control build) to this region -- fixed by making
the whole region pure OpenMP target (no `do concurrent` inside the target-data
region) and by using map(release:) instead of map(delete:) on exit
(KNOWLEDGE 8: a per-call delete can zero an outer/persistent mapping refcount).

Note tv/tv%T/tv%S are deliberately NOT mapped here: on device the density
inputs are the host-filled local arrays T/S from vert_fill_TS (mapped to:
fresh), so tv%T/tv%S are never read on device.

BITWISE: neutral -- benchmark_ALE np=1 ocean.stats byte-identical to the
pre-increment ePBL-device baseline (tmp_local_artifacts/td_baseline_ocean.stats,
diff empty), verified on a clean from-scratch rebuild. Device execution
confirmed (16 kernels; full 24-step run, no CUDA fault; compute-sanitizer
memcheck clean). Only residual vs the canonical golden is the separately-
deferred ePBL x**(1/n) transcendental floor.

Structure follows Ed Yang's edoyango/port/thickness_diffuse (3-D scratch +
full collapse); our EOS goes through the elem_loc dispatcher rather than his
EOS-3d stack.

Co-authored-by: Edward Yang <edward.yang@anu.edu.au>
…(TODO for Marshall)

Comment-only. ePBL is the one offloaded routine on this branch that is not
yet GPU/CPU bit-for-bit: the device run differs from host at ~1e-13 in the
energy diagnostic (T/S/mass bit-identical) solely from device-vs-host
libm/libdevice divergence in transcendental ops in its on-device kernels,
which have no deterministic drop-in (unlike the x**(1/3) cube roots, already
handled via cuberoot() on the answer_date>=20240101 path).

Add a consolidated, greppable `TODO(gpu-bitwise-repro)` audit at the module
level enumerating the live (benchmark_ALE) transcendental sites for the real
matched-transcendental fix (being done by Marshall -- not duplicated here):
find_PE_chg exp() (core, likely dominant), exp_decay_TKE_adjust exp(),
find_mstar mstar_N log() (OM4 Ekman), mstar_Langmuir **(-1.33) (arbitrary
power), and the MixLen pow(x,1.0) (trivial identity stopgap noted but not
applied). MKE_src exp() is inert (MKE_TO_TKE_EFFIC=0). No numerical change,
so the ePBL-device baseline and the no-new-diff offload gate are unaffected;
an nvfortran-CPU reference build is staged at ocean_only/cpu_build for the
GPU==CPU one-source verification once Marshall's fix lands.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… (TODO for Marshall)

Comment-only. The LF17 Langmuir chain (get_StokesSL_LiFoxKemper /
one_minus_exp_x / ust_2_u10_coare3p5) runs on-device when called from
ePBL_column and contributes device-vs-host transcendental divergence to the
~1e-13 ePBL energy floor via LA -> mstar_LT. Add a greppable
TODO(gpu-bitwise-repro) enumerating the live sites (exp in one_minus_exp_x;
erfc in get_StokesSL r5 large-root branch; atan(1.0) for PI, usually folded;
sqrt is exact) for Marshall's matched-transcendental pass, cross-referencing
the ePBL audit. No numerical change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…llable exp()

The intrinsic exp() lowers to host libm on the CPU and CUDA libdevice on the
GPU, whose last-bit rounding differs, so any do concurrent / omp target kernel
that calls exp() is not bit-for-bit CPU==GPU -- the dominant source of the
~1e-13 ePBL energy-budget divergence on feat/port-ePBL (see the
TODO(gpu-bitwise-repro) audit in MOM_energetic_PBL.F90).

exp_reprod avoids the library call: Cody-Waite range reduction (x = k*ln2 + r
with a two-part ln2 so k*ln2_hi is near-exact), a degree-12 Horner polynomial
for exp(r) whose reciprocal-factorial coefficients are compile-time
constant-folded (identical host/device), and scale(.,k) for 2**k. Every op is
+ - * / (plus nint/scale, exact), all bit-identical host vs device under
-Mnofma -- so the result is reproducible by construction, the same way
cuberoot/nth_root replace x**(1/n). declare target; compiles to device code
(symbol confirmed); scale/nint verified device-callable.

Accuracy: max relative error 3.1e-16 (~1.4 ULP) vs the intrinsic over
x in [-70, 20]; unit test Test_exp_reprod added (tol 1e-14). Like cuberoot it
is NOT bit-identical to intrinsic exp, so adopting it in a kernel changes
answers and needs a golden regeneration. Nothing calls it yet (bitwise-neutral
to commit); offered as the exp building block for the transcendental-repro
pass. Log/erfc/arbitrary-pow still need the same treatment.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…llable log()

Companion to exp_reprod: a device-callable natural log built from only +,-,*,/
(plus the exact exponent/fraction bit intrinsics), so it is bit-identical host
vs device under -Mnofma. x = m*2**k via exponent/fraction; m reduced to
[sqrt(1/2), sqrt(2)); log(m) = 2*(s + s^3/3 + ... ), s=(m-1)/(m+1), |s|<=0.172,
as a degree-21 Horner poly in s^2 with compile-folded reciprocal-odd coeffs;
then + k*ln2. Max relative error 3.7e-16 (~1.7 ULP) vs intrinsic log over
x in [1e-30, 1e30]; unit test Test_log_reprod added (tol 1e-13). declare
target; device code + symbol confirmed.

Together exp_reprod + log_reprod make arbitrary real powers reproducible via
x**y = exp_reprod(y*log_reprod(x)) -- covering the ePBL Langmuir
Convect_Langmuir_Number**(-1.33) and the MixLen **1.0. Bitwise-neutral commit
(nothing calls it yet). Building block for the transcendental-repro pass;
erfc and site wiring + golden regen still to come.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…allable erfc()

Third companion to exp_reprod/log_reprod, for x >= 0. Built from +,-,*,/ and
exp_reprod only, so bit-identical host vs device under -Mnofma. Two ranges:
x < 1.5 uses the erf Maclaurin series (48 fixed terms), erfc = 1 - erf; x >= 1.5
uses the incomplete-gamma continued fraction erfc = (x/sqrt(pi))*exp(-x^2)*
Q-CF(1/2,x^2) via a FIXED-iteration (60) modified-Lentz recurrence (no early
exit -> device-safe). Max relative error 5.8e-14 vs the intrinsic over
x in (0.01,26] (worst near x~24 where erfc~1e-263 is physically zero; ~1e-15
across the physically relevant range). declare target; device code + symbol
confirmed. Unit test Test_erfc_reprod added (tol 1e-12).

This is the last transcendental in the ePBL/wave on-device path: get_StokesSL
r5 calls erfc() (root_2kz>1e-3 branch), which was the sole remaining GPU-vs-CPU
floor after exp/log/pow were wired to reprod kernels (~8.5e-20 residual).
Bitwise-neutral commit (nothing calls it yet).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ls (GPU==CPU bitwise)

Replace every device-executed intrinsic transcendental in the ePBL + LF17
Langmuir on-device path with the bit-reproducible kernels exp_reprod /
log_reprod / erfc_reprod (committed just prior), so the whole path is built
from +,-,*,/ (plus exact bit intrinsics) and is therefore bit-identical host
vs device under -Mnofma. Only the transcendental symbol is swapped at each
site; all other arithmetic is preserved character-for-character.

MOM_energetic_PBL.F90: exp_decay_TKE_adjust + ePBL_column TKE-decay exp() ->
exp_reprod; find_mstar mstar_N log() -> log_reprod; mstar_Langmuir
Convect_Langmuir_Number**LT_enhance_exp -> exp_reprod(y*log_reprod(x)); MixLen
**1.0 -> an ==1.0 identity fast-path. MOM_wave_interface.F90: one_minus_exp_x
exp() -> exp_reprod; get_StokesSL r5 erfc() -> erfc_reprod; ust_2_u10_coare3p5
COARE-Cd log() -> log_reprod; PI = 4*atan(1) -> a literal (kills device atan).
Dead branches (answer_date<2023/2024, RH18, BBL, Stanley) and the inert
MKE_src exp (x MKE_TO_TKE_EFFIC=0) left as-is; sqrt is IEEE-exact host==device.

RESULT: benchmark_ALE np=1 GPU build == a same-source nvfortran-CPU build
(ocean_only/cpu_build, GPU-offload flags stripped) BIT-FOR-BIT
(diff ocean.stats empty) -- ePBL is now fully GPU/CPU reproducible, closing
the ~1e-13 energy-diagnostic floor that had made the branch non-bitwise since
the ePBL offload. The reproducible kernels change answers slightly vs the
intrinsics (like cuberoot vs x**(1/3)), so the canonical golden needs
regenerating to this reference. These prototype kernels feed / may be
superseded by Marshall's transcendental-repro pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…d kernels, GPU==CPU)

Comment-only: flip the two TODO(gpu-bitwise-repro) audit blocks to RESOLVED now
that the transcendentals are wired to exp_reprod/log_reprod/erfc_reprod and the
GPU build reproduces a same-source nvfortran-CPU build bit-for-bit. Kept the
site lists as a record; noted the prototype kernels may be superseded by
Marshall's pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants