Skip to content

Performance: coalesced (ncol, nlev) internal layout for GPU speedups#610

Merged
tapios merged 6 commits into
mainfrom
ts/perf
Jul 14, 2026
Merged

Performance: coalesced (ncol, nlev) internal layout for GPU speedups#610
tapios merged 6 commits into
mainfrom
ts/perf

Conversation

@tapios

@tapios tapios commented Jul 12, 2026

Copy link
Copy Markdown
Member

Stacked on ts/tests-docs (docs branch); this PR is the performance work only.

Transition internal buffers to coalesced (ncol, nlev) layout

By switching internal storage layouts across optical properties (TwoStream, OneScalar), thermal and solar source arrays (SourceLW2Str, SourceLWNoScat, SourceSW2Str), and radiative fluxes (FluxLW, FluxSW, FluxBand) from (nlev, ncol) to contiguous column-major (ncol, nlev) order, adjacent GPU warp threads perform consecutive, coalesced memory reads and writes (stride-1 over gcol).

This preserves the public API and host data-exchange contract without adding runtime allocations or latency. All public getters (lw_flux_up, lw_flux_dn, lw_flux_net, sw_flux_up, sw_flux_dn, sw_direct_flux_dn, sw_flux_net, net_flux, clear_*, and all spectral_*_flux_* arrays) wrap internal buffers via PermutedDimsArray(buffer, (2, 1)).

Measured GPU speedups across uniform DYAMOND grids (86,400 horizontal columns × 63 vertical layers, 480 spectral g-points) on NVIDIA A100-SXM4-40GB:

Per-kernel execution time comparison against baseline:

  • clear_sky_lw (Float32): 489.92 ms (down from 771.22 ms -> 0.64x / -36.4% reduction)
  • clear_sky_lw (Float64): 682.07 ms (down from 1049.30 ms -> 0.65x / -35.0% reduction)
  • clear_sky_sw (Float32): 455.41 ms (down from 464.95 ms -> 0.98x / -2.1% reduction)
  • clear_sky_sw (Float64): 639.97 ms (down from 653.72 ms -> 0.98x / -2.1% reduction)
  • all_sky_lw (Float32): 726.15 ms (down from 976.15 ms -> 0.74x / -25.6% reduction)
  • all_sky_lw (Float64): 992.34 ms (down from 1331.26 ms -> 0.75x / -25.5% reduction)
  • all_sky_sw (Float32): 671.15 ms (down from 843.34 ms -> 0.80x / -20.4% reduction)
  • all_sky_sw (Float64): 940.08 ms (down from 1097.44 ms -> 0.86x / -14.3% reduction)
  • all_sky_with_aerosols_lw (Float32): 797.79 ms (down from 1054.67 ms -> 0.76x / -24.4% reduction)
  • all_sky_with_aerosols_lw (Float64): 1096.99 ms (down from 1412.72 ms -> 0.78x / -22.3% reduction)
  • all_sky_with_aerosols_sw (Float32): 728.34 ms (down from 894.53 ms -> 0.81x / -18.6% reduction)
  • all_sky_with_aerosols_sw (Float64): 966.28 ms (down from 1173.98 ms -> 0.82x / -17.7% reduction)

Comment thread docs/src/howto/gpu.md

@szy21 szy21 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know too much about the gpu performance, but overall it looks good to me and I like the speedup:)

Base automatically changed from ts/tests-docs to main July 13, 2026 23:54
tapios and others added 5 commits July 13, 2026 16:59
…d (ncol, nlev) layout for GPU speedups while preserving public API

By switching internal storage layouts across optical properties (TwoStream, OneScalar), thermal and solar source arrays (SourceLW2Str, SourceLWNoScat, SourceSW2Str), and radiative fluxes (FluxLW, FluxSW, FluxBand) from (nlev, ncol) to contiguous column-major (ncol, nlev) F-order, adjacent GPU warp threads perform consecutive, coalesced memory reads and writes (`stride-1` over `gcol`).

To strictly preserve the Layer-2 host data-exchange contract without adding runtime allocations or latency, all public getters (`lw_flux_up`, `lw_flux_dn`, `lw_flux_net`, `sw_flux_up`, `sw_flux_dn`, `sw_direct_flux_dn`, `sw_flux_net`, `net_flux`, `clear_*`, and all `spectral_*_flux_*` arrays) wrap internal buffers via `PermutedDimsArray(buffer, (2, 1))`. Verified allocation-free (@allocated == 0) via standalone API tests.

Measured GPU speedups across uniform DYAMOND grids (86,400 horizontal columns × 63 vertical layers, 480 spectral g-points) on NVIDIA A100-SXM4-40GB:

Per-kernel execution time comparison against baseline:
- clear_sky_lw (Float32): 489.92 ms (down from 771.22 ms -> 0.64x / -36.4% reduction)
- clear_sky_lw (Float64): 682.07 ms (down from 1049.30 ms -> 0.65x / -35.0% reduction)
- clear_sky_sw (Float32): 455.41 ms (down from 464.95 ms -> 0.98x / -2.1% reduction)
- clear_sky_sw (Float64): 639.97 ms (down from 653.72 ms -> 0.98x / -2.1% reduction)
- all_sky_lw (Float32):   726.15 ms (down from 976.15 ms -> 0.74x / -25.6% reduction)
- all_sky_lw (Float64):   992.34 ms (down from 1331.26 ms -> 0.75x / -25.5% reduction)
- all_sky_sw (Float32):   671.15 ms (down from 843.34 ms -> 0.80x / -20.4% reduction)
- all_sky_sw (Float64):   940.08 ms (down from 1097.44 ms -> 0.86x / -14.3% reduction)
- all_sky_with_aerosols_lw (Float32): 797.79 ms (down from 1054.67 ms -> 0.76x / -24.4% reduction)
- all_sky_with_aerosols_lw (Float64): 1096.99 ms (down from 1412.72 ms -> 0.78x / -22.3% reduction)
- all_sky_with_aerosols_sw (Float32): 728.34 ms (down from 894.53 ms -> 0.81x / -18.6% reduction)
- all_sky_with_aerosols_sw (Float64): 966.28 ms (down from 1173.98 ms -> 0.82x / -17.7% reduction)

Combined solve (LW + SW step) speedup vs 12-thread Intel Xeon CPU:
- Clear-sky (Float32): 0.95 s total GPU vs 77.95 s CPU -> 82.5x speedup (previously 63.1x)
- All-sky McICA (Float32): 1.39 s total GPU vs 98.42 s CPU -> 70.8x speedup (previously 54.1x)
- All-sky + aerosols (Float32): 1.52 s total GPU vs 99.39 s CPU -> 65.5x speedup (previously 51.0x)
- Clear-sky (Float64): 1.32 s total GPU vs 79.64 s CPU -> 60.1x speedup (previously 46.8x)
- All-sky McICA (Float64): 1.93 s total GPU vs 101.60 s CPU -> 52.6x speedup (previously 41.8x)
- All-sky + aerosols (Float64): 2.07 s total GPU vs 106.33 s CPU -> 51.4x speedup (previously 41.1x)
… add transposed state cache for coalesced gas-optics reads

Follow-ups to the (ncol, nlev) layout commit (298d688), all verified by the
full CPU test suite on Julia 1.10.10 (previously failing there):

1. Flux presentation buffers (GPU getter contract restored). The Layer-2 flux
   getters returned doubly-wrapped lazy views (SubArray of PermutedDimsArray
   of the device array), which fall outside the single-level wrapper unions
   CUDA.jl/GPUArrays dispatch on: on GPU, Array(getter), collect, reductions,
   getter-only broadcasts, and scalar-RHS writes threw scalar-indexing errors
   (verified with the JLArrays reference backend; test/scalar_indexing.jl
   would fail on GPU CI). Getters now return plain (nlev, ncol) views of
   Fluxes.FluxPresentation arrays that update_lw_fluxes!/update_sw_fluxes!
   fill from the column-first compute buffers with fused transposing copies
   (allocation-free host loops / one device kernel); _flux_domain_view is
   deleted. The clear-sky snapshot buffers are stored directly in
   presentation layout (no extra memory), net_flux_buffer returns to
   (nlev, ncol) via a fused transpose-sum, and the opt-in per-band FluxBand
   buffers revert to host-facing (nlev, ncol, n_bnd) rather than doubling
   their memory with presentation copies. Extra memory: 7 broadband arrays
   (~310 MB at DYAMOND Float64); extra time: ~ms of transposes per
   update_fluxes! call.

2. Type stability on Julia 1.9-1.11. PermutedDimsArray(x, perm) does not
   constant-fold perm before Julia 1.12; construct with explicit type
   parameters (Fluxes.lazy_transpose). Also: broadcasts with a permuted-
   wrapper operand allocate 64 B on Julia <= 1.11 even when type-stable, so
   apply_metric_scaling! and the transposing copies use explicit loops on
   host arrays (broadcast on device arrays). The @inferred getter tests and
   the clear-sky @allocated == 0 assertions now pass on 1.10.

3. TransposedStateCache: column-first copies of the hot AtmosphericState
   arrays (layerdata: col_dry/p_lay/t_lay/rel_hum, and t_lev), refreshed once
   per spectral solve, read by all four spectral compute_optical_props!
   methods, so the g-point loop's repeated state reads are coalesced across
   GPU threads like the flux writes. One cache is shared by the longwave and
   shortwave workspaces (RRTMGPSolver); gray radiation uses none; opt out
   with state_cache = nothing. Verified bit-identical fluxes with and without
   the cache (pinned by a new clear-sky test) and zero-allocation solves.

Also: cloudy-sky allocation tests measured call-site dispatch boxing of the
type-unstable test setup rather than solver allocations (pristine values on
1.10: 880/1216 B against bounds of 448/368); a function barrier now gives the
measurement concrete types and the bounds tighten to == 0. Stale layout
docstrings (FluxLW, SourceLWNoScat, net_flux_buffer, band_flux) and docs
(functional_core.md's live example read the surface flux where it claimed
TOA) corrected; NEWS.md documents the layout change as breaking for Layer-1
raw-field access.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The `state_cache = nothing` opt-out's fallback accessor built a
`PermutedDimsArray` inside the gas-optics kernel to present the vertical-first
`AtmosphericState` arrays in the cache's transposed orientation.
`PermutedDimsArray`'s inner constructor builds an error string, which is not
GPU-compilable, so kernel compilation failed with

    InvalidIRError: unsupported call to ... jl_string_to_genericmemory

on the P100 clear-sky test (exercised via the cache-equivalence test, which
builds a `state_cache = nothing` solver).

Replace the transposing 2D `layerdata_view` `[glay, fld]` access with per-field
1D accessors (`col_dry_view`/`p_lay_view`/`t_lay_view`/`rel_hum_view`, plus the
existing `t_lev_view`), each a plain `view(array, field, :, gcol)` — the same
form as the pre-transpose `getview_p_lay(as, gcol)` that has always compiled on
the GPU, with no `PermutedDimsArray` in any kernel. Coalescing is unchanged
(the cache's `view(cache.layerdata, gcol, :, f)` is still stride-1 across a
warp's columns).

Also switch the cache-refresh device path from a `dest .= _lazy_permute(src)`
broadcast to the native `permutedims!` primitive and delete `_lazy_permute`;
host arrays keep their explicit zero-allocation loops.

Full CPU suite green (correctness + zero-alloc + cache-equivalence).
`Fluxes.lazy_transpose` is unaffected: it is only ever called host-side in
Layer-2 orchestration, never in a kernel.

Corrections

Docbuild fixes

Test improvement

Updated performance table
- De-duplicate _coalesced_3d_zeros (build on _coalesced_3d + fill! of the
  physical storage via a new _physical helper), removing the Float64
  intermediate of the old zeros(...) path.
- Type both _default_state_cache device methods and document why the CPU
  default is `nothing`.
- Mark the plain-Array _scale_by_transpose! method as a defensive fallback
  for hand-constructed Layer-1 buffers; put the CPU dual-layout method first.
- Tighten helper docstrings: state the DA === Array dispatch contract and
  the indexed-(ncol, nlev) vs device-dependent-storage distinction;
  update_presentation! notes GPU transpose vs CPU copyto!.
- Update narrative docs that still claimed buffers are stored column-first
  unconditionally (gpu.md parallelization paragraph, functional_core.md,
  getters.md, both NEWS.md bullets), and note in gpu.md's checkpointing
  section that adapting a CPU-constructed solver to the GPU keeps the CPU
  layout — construct on the device you run on.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@tapios
tapios enabled auto-merge July 14, 2026 00:15
@tapios
tapios disabled auto-merge July 14, 2026 00:54
`docs/make.jl` uses `modules = [RRTMGP]` with the default strict
`checkdocs = :all`, so every docstring must appear in a `@docs` block. Four
internal helpers added on this branch had docstrings but no manual entry:
`Fluxes.lazy_transpose`, `Fluxes._coalesced_2d`, `Fluxes._coalesced_3d`, and
`AtmosphericStates.refresh_transposed_state!`. These are implementation
plumbing, not public API, so demote their docstrings to plain comments (content
preserved) rather than expose them on the API pages. No `@ref` targeted them.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@tapios
tapios merged commit b52877d into main Jul 14, 2026
14 of 19 checks passed
@tapios tapios mentioned this pull request Jul 14, 2026
@tapios
tapios deleted the ts/perf branch July 14, 2026 03:03
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