gpuwm-hex 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- gpuwm_hex-0.1.0.dist-info/METADATA +675 -0
- gpuwm_hex-0.1.0.dist-info/RECORD +96 -0
- gpuwm_hex-0.1.0.dist-info/WHEEL +5 -0
- gpuwm_hex-0.1.0.dist-info/entry_points.txt +2 -0
- gpuwm_hex-0.1.0.dist-info/licenses/LICENSE +202 -0
- gpuwm_hex-0.1.0.dist-info/licenses/NOTICE +112 -0
- gpuwm_hex-0.1.0.dist-info/top_level.txt +1 -0
- mpas_port/__init__.py +64 -0
- mpas_port/__main__.py +14 -0
- mpas_port/acoustic.py +515 -0
- mpas_port/acoustic_v841.py +540 -0
- mpas_port/cli.py +204 -0
- mpas_port/config.py +574 -0
- mpas_port/config_lts.py +182 -0
- mpas_port/config_v841.py +511 -0
- mpas_port/cuda_acoustic.py +799 -0
- mpas_port/cuda_acoustic_lts.py +572 -0
- mpas_port/cuda_acoustic_v841.py +685 -0
- mpas_port/cuda_arwen_physics_v841.py +1593 -0
- mpas_port/cuda_backend/__init__.py +60 -0
- mpas_port/cuda_backend/compile_contract.py +414 -0
- mpas_port/cuda_backend/containers.py +867 -0
- mpas_port/cuda_backend/recovery.py +414 -0
- mpas_port/cuda_backend/runtime.py +423 -0
- mpas_port/cuda_driver.py +4642 -0
- mpas_port/cuda_driver_lts.py +698 -0
- mpas_port/cuda_dualrun.py +2042 -0
- mpas_port/cuda_dynamics_v841.py +582 -0
- mpas_port/cuda_fp32.py +229 -0
- mpas_port/cuda_ftz.py +4559 -0
- mpas_port/cuda_ftz_v841.py +1440 -0
- mpas_port/cuda_gwdo_v841.py +1659 -0
- mpas_port/cuda_horizontal.py +1659 -0
- mpas_port/cuda_horizontal_v841.py +877 -0
- mpas_port/cuda_physics_prep_v841.py +2447 -0
- mpas_port/cuda_physics_v841.py +1500 -0
- mpas_port/cuda_transport.py +1280 -0
- mpas_port/cuda_transport_v841.py +881 -0
- mpas_port/cuda_v841.py +198 -0
- mpas_port/damping_v841.py +112 -0
- mpas_port/diagnostics.py +316 -0
- mpas_port/doctor.py +503 -0
- mpas_port/driver.py +2887 -0
- mpas_port/dynamics.py +432 -0
- mpas_port/dynamics_v841.py +185 -0
- mpas_port/engines.py +420 -0
- mpas_port/errors.py +30 -0
- mpas_port/horizontal.py +395 -0
- mpas_port/init_door.py +696 -0
- mpas_port/initialization.py +2175 -0
- mpas_port/integration.py +327 -0
- mpas_port/integration_v841.py +28 -0
- mpas_port/lts_v841.py +360 -0
- mpas_port/mesh.py +1399 -0
- mpas_port/mixing.py +929 -0
- mpas_port/mixing_v841.py +562 -0
- mpas_port/nomix_oracle.py +515 -0
- mpas_port/obs_referee/__init__.py +19 -0
- mpas_port/obs_referee/__main__.py +3 -0
- mpas_port/obs_referee/acquire.py +220 -0
- mpas_port/obs_referee/align.py +296 -0
- mpas_port/obs_referee/bootstrap.py +148 -0
- mpas_port/obs_referee/bundle.py +681 -0
- mpas_port/obs_referee/canonical.py +182 -0
- mpas_port/obs_referee/cli.py +221 -0
- mpas_port/obs_referee/errors.py +27 -0
- mpas_port/obs_referee/manifest.py +607 -0
- mpas_port/obs_referee/metrics.py +512 -0
- mpas_port/obs_referee/report.py +232 -0
- mpas_port/obs_referee/runner.py +436 -0
- mpas_port/obs_referee/scorecard.py +268 -0
- mpas_port/obs_referee/treatment.py +195 -0
- mpas_port/offcentering_v841.py +173 -0
- mpas_port/oracle.py +289 -0
- mpas_port/output.py +859 -0
- mpas_port/partition_assets_v841.py +569 -0
- mpas_port/partition_device_scheduler_v841.py +672 -0
- mpas_port/partition_executor_v841.py +515 -0
- mpas_port/partition_local_mesh_v841.py +507 -0
- mpas_port/partition_net_v841.py +257 -0
- mpas_port/partition_state_v841.py +148 -0
- mpas_port/physics_seam.py +1763 -0
- mpas_port/physics_seam_v841.py +672 -0
- mpas_port/regrid.py +908 -0
- mpas_port/render_door.py +649 -0
- mpas_port/rust_renderer.py +2099 -0
- mpas_port/state.py +72 -0
- mpas_port/step_oracle.py +362 -0
- mpas_port/terrain.py +794 -0
- mpas_port/timestep_admission.py +197 -0
- mpas_port/tracer_oracle.py +326 -0
- mpas_port/transport.py +2070 -0
- mpas_port/vector.py +877 -0
- mpas_port/vertical.py +677 -0
- mpas_port/vertical_spec.py +770 -0
- mpas_port/wps_intermediate.py +521 -0
|
@@ -0,0 +1,675 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: gpuwm-hex
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: GPU global atmospheric model on unstructured Voronoi meshes: a CUDA port of the MPAS-A v8.4.1 dycore with ArWen physics, arbitrary mesh generation, and single-card mesh streaming for 12-16 GB GPUs.
|
|
5
|
+
License-Expression: Apache-2.0
|
|
6
|
+
Project-URL: Homepage, https://github.com/FahrenheitResearch/gpuwm-hex
|
|
7
|
+
Project-URL: Repository, https://github.com/FahrenheitResearch/gpuwm-hex
|
|
8
|
+
Keywords: atmospheric-model,MPAS,CUDA,GPU,weather,simulation,unstructured-mesh,voronoi,global-model
|
|
9
|
+
Classifier: Development Status :: 4 - Beta
|
|
10
|
+
Classifier: Environment :: GPU :: NVIDIA CUDA
|
|
11
|
+
Classifier: Intended Audience :: Science/Research
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
+
Classifier: Programming Language :: Rust
|
|
17
|
+
Classifier: Topic :: Scientific/Engineering :: Atmospheric Science
|
|
18
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
19
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
20
|
+
Requires-Python: >=3.11
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
License-File: LICENSE
|
|
23
|
+
License-File: NOTICE
|
|
24
|
+
Requires-Dist: numpy>=2.0
|
|
25
|
+
Requires-Dist: netCDF4>=1.6
|
|
26
|
+
Requires-Dist: scipy>=1.11
|
|
27
|
+
Requires-Dist: gpuwm>=2.5.3
|
|
28
|
+
Provides-Extra: gpu-cu12
|
|
29
|
+
Requires-Dist: cupy-cuda12x[ctk]>=14.0; extra == "gpu-cu12"
|
|
30
|
+
Provides-Extra: gpu-cu13
|
|
31
|
+
Requires-Dist: cupy-cuda13x[ctk]>=14.0; extra == "gpu-cu13"
|
|
32
|
+
Provides-Extra: gpu
|
|
33
|
+
Requires-Dist: gpuwm-hex[gpu-cu12]; extra == "gpu"
|
|
34
|
+
Provides-Extra: dev
|
|
35
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
36
|
+
Dynamic: license-file
|
|
37
|
+
|
|
38
|
+
# gpuwm-hex
|
|
39
|
+
|
|
40
|
+
A GPU-native global variable-resolution atmospheric model core, driven from the
|
|
41
|
+
ArWen engine.
|
|
42
|
+
|
|
43
|
+
The numerics are a port of MPAS-Atmosphere v8.4.1 to Python and CuPy, fully
|
|
44
|
+
device-resident: an unstructured global mesh with regional refinement, run on
|
|
45
|
+
one consumer CUDA card. The physics is not the port's own — every physics
|
|
46
|
+
column runs through the ArWen engine's column-batch seam, so gpuwm-hex and the
|
|
47
|
+
`gpuwm` distribution share one physics implementation rather than two that
|
|
48
|
+
drift.
|
|
49
|
+
|
|
50
|
+
*(MPAS-Atmosphere is a registered project of NCAR and LANL. This distribution
|
|
51
|
+
is an independent port and is not affiliated with or endorsed by them. The name
|
|
52
|
+
is used here only to say which model was ported.)*
|
|
53
|
+
|
|
54
|
+
**Version 0.1.0.** The cut shape for this line is: global variable-resolution
|
|
55
|
+
on one consumer GPU, deterministic, ArWen physics. Multi-GPU is built and
|
|
56
|
+
proven but not part of 0.1. The regional (limited-area) lane does not exist —
|
|
57
|
+
see *Limitations*.
|
|
58
|
+
|
|
59
|
+
**New here? Start with the [User Manual](docs/manual/index.md)** — a
|
|
60
|
+
plain-language introduction, a quickstart in which every command was run
|
|
61
|
+
before it was printed, task guides for each door, and a troubleshooting
|
|
62
|
+
index built from the doors' own refusals. This README is the capability
|
|
63
|
+
contract; the manual is how you walk it.
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
## What is proven
|
|
68
|
+
|
|
69
|
+
Measured, on an RTX 5090:
|
|
70
|
+
|
|
71
|
+
- 24 h global forecasts at 720/720 steps, about 3.07 s/step, **deterministic** —
|
|
72
|
+
two arms of the same run compared byte-for-byte and identical.
|
|
73
|
+
- 2-D Smagorinsky horizontal mixing, ported from the native Registry default and
|
|
74
|
+
**on by default**, with a full A/B pass.
|
|
75
|
+
- Two-node multi-GPU, bitwise partition-invariant against the single-GPU
|
|
76
|
+
reference (0.2 material, not shipped in 0.1).
|
|
77
|
+
- 702 product PNGs rendered through the Rust renderer.
|
|
78
|
+
- The init door reproduces 92/92 carried fields bit-identically against a native
|
|
79
|
+
golden init, and an init it produced started the port in a 5-step smoke.
|
|
80
|
+
- The render door produced 31/31 renderable products from a native history.
|
|
81
|
+
- Restart: checkpoint and restore with bitwise-identical continuation — in the
|
|
82
|
+
full-physics proof harness the restarted history file is byte-identical
|
|
83
|
+
(same SHA-256) to the uninterrupted run's
|
|
84
|
+
(`evidence/restart-step16-327/`).
|
|
85
|
+
- The engine-pin move onto the GF local-memory frame cut left the trajectory
|
|
86
|
+
untouched: 132/132 history variables and every per-step atmosphere
|
|
87
|
+
fingerprint byte-identical across 30 full-physics composite steps, against a
|
|
88
|
+
1-ULP flip instrument the comparator flagged
|
|
89
|
+
(`evidence/gf-pin-move-measured-20260824/`).
|
|
90
|
+
- Device footprint, measured at two meshes and both pins in one session:
|
|
91
|
+
**6,296.5 MiB fixed + 93,474 B/cell** at the current pin on this card; the
|
|
92
|
+
published 40,962-cell global mesh peaks at **9,948 MiB**.
|
|
93
|
+
|
|
94
|
+
## What is *not* claimed
|
|
95
|
+
|
|
96
|
+
It is not bit-identical to native MPAS and cannot be: native is single-precision
|
|
97
|
+
CPU, this is CUDA on sm_120. Over 24 h the two produce the same storms in the
|
|
98
|
+
same pattern with the same energy, with chaos-shaped divergence — and **three
|
|
99
|
+
bias-shaped differences that are declared divergences**. They are quantified
|
|
100
|
+
below, and [`docs/declared-divergences.md`](docs/declared-divergences.md)
|
|
101
|
+
carries the mechanism, the magnitude and the observational referee for each,
|
|
102
|
+
because a user deciding whether to trust a number needs them before the run,
|
|
103
|
+
not after.
|
|
104
|
+
|
|
105
|
+
**Physics parity against MPAS is not a goal of this project.** The port runs
|
|
106
|
+
ArWen's physics rather than MPAS's, so whole-model agreement with MPAS stopped
|
|
107
|
+
being reachable the moment that choice was made — which is why every remaining
|
|
108
|
+
difference below is physics-shaped. The **dynamical core stays pinned** to
|
|
109
|
+
native v8.4.1 and that pin is the correctness anchor. The physics is judged by
|
|
110
|
+
**obs-skill against MRMS and ASOS**, not by agreement with another model.
|
|
111
|
+
|
|
112
|
+
That changes the referee; it does not clear a finding. A bias that is wrong
|
|
113
|
+
against *observations* is still wrong, and the obs-skill comparison that would
|
|
114
|
+
judge these three has not been run yet.
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
## Requirements
|
|
119
|
+
|
|
120
|
+
| | |
|
|
121
|
+
| --- | --- |
|
|
122
|
+
| Python | 3.11 or newer |
|
|
123
|
+
| GPU | A CUDA device. Memory is set by mesh size and by the card. Measured 2026-08-24 on an RTX 5090 at the current engine pin: `footprint = 6,296.5 MiB + 93,474 B/cell`. The published 40,962-cell global mesh (x1.40962, about 120 km) peaked at **9,948 MiB — inside a 12 GiB card's budget**; the 163,842-cell mesh (x4.163842, about 24 km) peaked at **20,902 MiB**, and its run path still admits at a 24 GiB free-memory floor, so it remains a 32 GiB-card configuration until that floor is re-derived. The fixed term is a property of the card, not the mesh; both smaller parts previously measured carried smaller fixed terms. Receipts: `evidence/gf-pin-move-measured-20260824/`. |
|
|
124
|
+
| CUDA | CuPy matching your driver's CUDA major — there is no way for pip to detect it, so you choose (below). |
|
|
125
|
+
| Engine | The `gpuwm` distribution, 2.5.3 or newer: the physics seam, and the bundle that carries the four MPAS bridge binaries both doors drive. **Plus a `gpuwm` source checkout for the forecast lane** — see *The engine pin*. |
|
|
126
|
+
| Assets | A mesh grid file, a matching static file, and (for the init door) a vertical-grid declaration — normally a `--vertical-spec` JSON; a native-minted init file as a capsule is the compatibility mode. **gpuwm-hex ships none of these and has no fetch path for them.** See *Assets you must supply*. |
|
|
127
|
+
| Rust binaries | `rw_mpas_init` for the init door; `rw_mpas_convert` and `rw_wrfbatch` for the render door. They are built from the `gpuwm` source tree — see *Building the Rust engines*. |
|
|
128
|
+
|
|
129
|
+
### Install
|
|
130
|
+
|
|
131
|
+
```sh
|
|
132
|
+
pip install gpuwm-hex # then one of:
|
|
133
|
+
pip install "gpuwm-hex[gpu-cu12]" # driver reports CUDA 12.x
|
|
134
|
+
pip install "gpuwm-hex[gpu-cu13]" # driver reports CUDA 13.x
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Check which you need with `nvidia-smi` — the CUDA version in its header is the
|
|
138
|
+
driver's major. `pip install "gpuwm-hex[gpu]"` is an alias for `gpu-cu12`.
|
|
139
|
+
|
|
140
|
+
```sh
|
|
141
|
+
gpuwm-hex version # what is installed, and where
|
|
142
|
+
gpuwm-hex --help # the doors
|
|
143
|
+
gpuwm-hex doctor # what this install can actually reach
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
**Run `gpuwm-hex doctor` first.** A wheel for this project is deliberately
|
|
147
|
+
partial and says so: the Rust engines, the CUDA runtime and the mesh assets
|
|
148
|
+
cannot travel inside it. Doctor checks each of those estates for real and
|
|
149
|
+
prints, for every gap, the exact command that closes it on your platform.
|
|
150
|
+
`gpuwm-hex doctor --explain` prints the evidence and the whole pasteable
|
|
151
|
+
remedy block; `--json` emits the same findings as data. It exits 1 while any
|
|
152
|
+
required item is missing, so it works as a gate in a script.
|
|
153
|
+
|
|
154
|
+
### The import namespace
|
|
155
|
+
|
|
156
|
+
The distribution is `gpuwm-hex` and every command you type is `gpuwm-hex`. The
|
|
157
|
+
**import namespace is still `mpas_port`** — `import mpas_port`, not
|
|
158
|
+
`import gpuwm_hex`. That is a known inconsistency, stated rather than hidden.
|
|
159
|
+
|
|
160
|
+
It is not left alone out of laziness. Eleven of the seventeen modules that the
|
|
161
|
+
full-physics proof harness pins by SHA-256 carry the literal string
|
|
162
|
+
`mpas_port`, and in five of them it is a `module_key=` NVRTC compile-contract
|
|
163
|
+
identity rather than a comment — so editing them changes the kernel-cache
|
|
164
|
+
identity as well as breaking the pin, and the rename costs a re-proof on a
|
|
165
|
+
32 GiB card against the native authority. It is scheduled for a lane that can
|
|
166
|
+
pay that, not sneaked into a packaging change. Scripts that import the package
|
|
167
|
+
directly should expect the name to change in a later release.
|
|
168
|
+
|
|
169
|
+
*(The `hex` in the name is the hexagonal Voronoi mesh the model runs on.)*
|
|
170
|
+
|
|
171
|
+
### The engine pin
|
|
172
|
+
|
|
173
|
+
gpuwm-hex depends on `gpuwm>=2.5.3`. That floor is a coarse filter, not the
|
|
174
|
+
wall: the port pins the engine's physics seam by the SHA-256 of **sixteen
|
|
175
|
+
individual gpuwm source files**, and thirteen of those sixteen differ at
|
|
176
|
+
gpuwm's published 2.5.0 stamp and six at its 2.5.1 stamp — the GF seam-parity
|
|
177
|
+
work the port pins, and the GF frame cut on top of it, landed after those
|
|
178
|
+
cuts. A `>=2.5.0` floor would let pip resolve an install that the port then
|
|
179
|
+
refuses at launch, and no published version satisfies the sixteen-file
|
|
180
|
+
manifest on its own.
|
|
181
|
+
|
|
182
|
+
The floor sits at 2.5.3 rather than 2.5.1 for a **second and independent**
|
|
183
|
+
reason. The four MPAS bridge binaries — `rw_mpas_init`, `rw_mpas_convert`,
|
|
184
|
+
`rw_mpas_mesh`, `rw_mpas_static` — enter gpuwm's *bundle* at 2.5.3; their
|
|
185
|
+
rows landed the day after the 2.5.2 upload, so published 2.5.2 stages none of
|
|
186
|
+
them through `gpuwm fetch-bridges`. Both front doors drive those binaries, and
|
|
187
|
+
the gpuwm source tree does not publish, so a user who resolved onto a 2.5.2
|
|
188
|
+
engine could open **neither door** and would have no route to build what was
|
|
189
|
+
missing. That is a stranded install rather than a degraded one, and a
|
|
190
|
+
dependency floor is the only place pip can refuse it. gpuwm 2.5.3 publishes
|
|
191
|
+
before this distribution does, which is the one hard ordering constraint the
|
|
192
|
+
release carries.
|
|
193
|
+
|
|
194
|
+
**Installing `gpuwm` is necessary but not sufficient for the forecast lane.**
|
|
195
|
+
One of the sixteen pinned files is `docs/mpas-seam.md`, a repository document
|
|
196
|
+
that no wheel places in `site-packages`. The forecast lane therefore needs a
|
|
197
|
+
**gpuwm source checkout** at the pinned commit, passed as `--arwen-checkout`,
|
|
198
|
+
in addition to the installed distribution. This is stated plainly because the
|
|
199
|
+
alternative — discovering it as a `FileNotFoundError` deep in a launch — is the
|
|
200
|
+
trap.
|
|
201
|
+
|
|
202
|
+
**The two front doors do not need any of that.** `init` and `render` import no
|
|
203
|
+
`gpuwm` at all; they drive Rust binaries.
|
|
204
|
+
|
|
205
|
+
---
|
|
206
|
+
|
|
207
|
+
## Assets you must supply
|
|
208
|
+
|
|
209
|
+
There is **no fetch path in gpuwm-hex for any of these**. No `gpuwm-hex fetch`
|
|
210
|
+
command exists. If a document ever implies otherwise, it is wrong.
|
|
211
|
+
|
|
212
|
+
**1. A mesh grid file** (`x1.2562.grid.nc`, `x4.163842.grid.nc`, …). Meshes
|
|
213
|
+
come from the MPAS-Atmosphere project's published mesh downloads, from the
|
|
214
|
+
`MPAS-Tools` mesh generator, or from the engine's own `gpuwm mesh` door, which
|
|
215
|
+
generates an icosahedral Goldberg grid **and its matching static** for a named
|
|
216
|
+
refinement region, sized against the measured footprint of a named card.
|
|
217
|
+
gpuwm-hex reads and validates meshes and registers a new pair as table work;
|
|
218
|
+
the generator lives in `gpuwm`.
|
|
219
|
+
|
|
220
|
+
**2. A matching static file** (`*.static.nc`) carrying terrain, land use, soil
|
|
221
|
+
category and vegetation interpolated onto that mesh. Static files are produced
|
|
222
|
+
by native `init_atmosphere_model`, or by the engine's `rw_mpas_static` (the
|
|
223
|
+
same writer `gpuwm mesh` drives, against a WPS geographical dataset).
|
|
224
|
+
gpuwm-hex does not build static files itself. The grid and static must be the
|
|
225
|
+
same mesh — the doors cross-check `nCells` and refuse a mismatched pair by
|
|
226
|
+
name.
|
|
227
|
+
|
|
228
|
+
**3. For the init door: a vertical grid, one of two ways.** The Rust init
|
|
229
|
+
engine does not invent the vertical grid.
|
|
230
|
+
|
|
231
|
+
- **Native-free (the normal path):** pass `--vertical-spec` with a
|
|
232
|
+
`gpuwm-hex.vertical-spec/v1` JSON declaration; the door constructs the
|
|
233
|
+
v8.4.1 vertical contract itself and writes a durable vertical artifact with
|
|
234
|
+
its receipt. No native file is read. A new vertical configuration is a JSON
|
|
235
|
+
file, not a code branch. Measured against a native golden on 2026-08-24: the
|
|
236
|
+
mint is schema-complete (134/134 variables), the constructed vertical sits
|
|
237
|
+
within 3.9 mm of native `zgrid` with the per-field cost quantified, and the
|
|
238
|
+
minted init runs the dycore —
|
|
239
|
+
receipt `evidence/native-free-proof-20260824/` (see
|
|
240
|
+
[`evidence/EVIDENCE.md`](evidence/EVIDENCE.md)); boundary in
|
|
241
|
+
[`docs/native-free-init-admission.md`](docs/native-free-init-admission.md).
|
|
242
|
+
- **Native-capsule compatibility mode:** pass `--capsule`/`--reference` naming
|
|
243
|
+
a native-minted init-class file; the door reads `zgrid, zz, fzm, fzp, dzu,
|
|
244
|
+
rdzw, zb, zb3` and the smoothed terrain out of it and asserts the capsule's
|
|
245
|
+
`zgrid` bit-identical against the reference before trusting a single level.
|
|
246
|
+
|
|
247
|
+
The two modes are mutually exclusive and there is no hidden native read.
|
|
248
|
+
|
|
249
|
+
**4. Meteorological input**: a WPS intermediate file from `ungrib` (GFS, ERA5,
|
|
250
|
+
whatever you drive with).
|
|
251
|
+
|
|
252
|
+
### Getting the Rust engines
|
|
253
|
+
|
|
254
|
+
Both doors are orchestration only; the field data is handled entirely by Rust
|
|
255
|
+
binaries built from the `gpuwm` source tree. Nothing compiled ships inside this
|
|
256
|
+
wheel, so the binaries are staged onto your machine one of two ways.
|
|
257
|
+
|
|
258
|
+
**The short way.** `gpuwm` publishes prebuilt bundles, and one command stages
|
|
259
|
+
the whole set — `rw_mpas_mesh`, `rw_mpas_static`, `rw_mpas_init`,
|
|
260
|
+
`rw_mpas_convert` and `rw_wrfbatch`:
|
|
261
|
+
|
|
262
|
+
```sh
|
|
263
|
+
gpuwm fetch-bridges
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
It writes into `~/.gpuwm/bridges`, which gpuwm-hex reads directly. No
|
|
267
|
+
environment variables to set. This works wherever a bundle is published for
|
|
268
|
+
your platform; `gpuwm-hex doctor` tells you whether it worked.
|
|
269
|
+
|
|
270
|
+
**The build.** On a platform with no published bundle, build from a checkout:
|
|
271
|
+
|
|
272
|
+
```sh
|
|
273
|
+
cd <gpuwm checkout>/tools/rustwx
|
|
274
|
+
cargo build --release --offline --locked -p rw-mpas # rw_mpas_init, rw_mpas_convert
|
|
275
|
+
cargo build --release --offline --locked -p rw-wrfbatch # rw_wrfbatch
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
Then point the doors at what you built:
|
|
279
|
+
|
|
280
|
+
```sh
|
|
281
|
+
export GPUWM_HEX_RW_MPAS_INIT=<gpuwm>/tools/rustwx/target/release/rw_mpas_init
|
|
282
|
+
export GPUWM_HEX_RW_MPAS_CONVERT=<gpuwm>/tools/rustwx/target/release/rw_mpas_convert
|
|
283
|
+
export GPUWM_HEX_RW_WRFBATCH=<gpuwm>/tools/rustwx/target/release/rw_wrfbatch
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
#### The resolution ladder
|
|
287
|
+
|
|
288
|
+
Every door resolves every engine the same way, best first:
|
|
289
|
+
|
|
290
|
+
1. the door's own flag (`--engine`, `--convert-exe`, `--renderer-exe`);
|
|
291
|
+
2. this distribution's variable, then any older spelling it has carried —
|
|
292
|
+
`RW_MPAS_INIT`, `MPAS_PORT_RW_MPAS_CONVERT` and `MPAS_PORT_RW_WRFBATCH`
|
|
293
|
+
still work and always will, because a rename must never invalidate an
|
|
294
|
+
install line that already works;
|
|
295
|
+
3. `gpuwm`'s own variable (`GPUWM_RW_MPAS_INIT` and siblings) and its bridge
|
|
296
|
+
directories, which is where `gpuwm fetch-bridges` stages;
|
|
297
|
+
4. `PATH`.
|
|
298
|
+
|
|
299
|
+
A flag or an environment variable naming a missing file is a **hard error**,
|
|
300
|
+
never a fall through to the next rung — a ladder that silently skips a broken
|
|
301
|
+
setting runs the wrong engine build and reports success. When nothing is
|
|
302
|
+
found, the refusal names every rung it searched and both commands above.
|
|
303
|
+
|
|
304
|
+
#### What `gpuwm fetch-bridges` can and cannot supply today
|
|
305
|
+
|
|
306
|
+
Measured against the **published** `gpuwm` 2.5.2 wheel, not a checkout: its
|
|
307
|
+
bundle carries `rw_wrfbatch` and **not** `rw_mpas_init` or `rw_mpas_convert`.
|
|
308
|
+
On that engine `gpuwm fetch-bridges` gives you the renderer and nothing that
|
|
309
|
+
opens either MPAS door. That measurement is why the dependency floor is
|
|
310
|
+
`gpuwm>=2.5.3` and not lower: 2.5.3 is where the four MPAS bridge binaries
|
|
311
|
+
enter the bundle, so pip cannot resolve you onto an engine that strands both
|
|
312
|
+
doors. A conforming install therefore never sees the 2.5.2 shortfall; it is
|
|
313
|
+
recorded here because it is the reason the floor moved.
|
|
314
|
+
|
|
315
|
+
You are not asked to track that. `gpuwm-hex doctor` asks the gpuwm you
|
|
316
|
+
actually have which artifacts its bundle declares, and every refusal offers
|
|
317
|
+
the staging command only when it can really deliver the file — a remedy that
|
|
318
|
+
cannot work is worse than no remedy, because you run it, it succeeds, and the
|
|
319
|
+
door still refuses.
|
|
320
|
+
|
|
321
|
+
---
|
|
322
|
+
|
|
323
|
+
## Door 1: `gpuwm-hex init`
|
|
324
|
+
|
|
325
|
+
Build initial conditions without native Fortran `init_atmosphere_model`.
|
|
326
|
+
|
|
327
|
+
```sh
|
|
328
|
+
gpuwm-hex init \
|
|
329
|
+
--met WORK/MET:2025-03-14_12 \
|
|
330
|
+
--static assets/x4.163842.static.nc \
|
|
331
|
+
--grid assets/x4.163842.grid.nc \
|
|
332
|
+
--vertical-spec verification/vertical-specs/tc55-v1.json \
|
|
333
|
+
--out run/init.nc \
|
|
334
|
+
--start-time 2025-03-14_12:00:00 \
|
|
335
|
+
--nfglevels 38 --nfgsoillevels 4 \
|
|
336
|
+
--extrap-airtemp lapse-rate --use-spechumd no \
|
|
337
|
+
--theta-adv-order 3 --coef-3rd-order 0.25 \
|
|
338
|
+
--virtual-factor reproduce-fortran \
|
|
339
|
+
--deep-soil-moisture reproduce-fortran \
|
|
340
|
+
--landuse-table MODIFIED_IGBP_MODIS_NOAH \
|
|
341
|
+
--frac-seaice yes --tsk-seaice-threshold 100.0 \
|
|
342
|
+
--oned-underflow preserve
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
Writes `run/init.nc` and `run/init.nc.provenance.json` (SHA-256 of every input,
|
|
346
|
+
the engine binary, the argv, the engine's own receipt and the output), prints a
|
|
347
|
+
JSON summary, exits 0.
|
|
348
|
+
|
|
349
|
+
**Every physics switch is required and has no default.** That is deliberate:
|
|
350
|
+
each one changes the numbers in a file that opens cleanly and reads plausibly
|
|
351
|
+
either way, so a default would be a silent wrong answer. Each refusal prints the
|
|
352
|
+
native namelist key it corresponds to, so a captured `namelist.init_atmosphere`
|
|
353
|
+
transcribes without guessing. Full detail, including the sixteen named refusals:
|
|
354
|
+
[`docs/init-door.md`](docs/init-door.md).
|
|
355
|
+
|
|
356
|
+
What the door accepts is not a promise but a measured table:
|
|
357
|
+
[`docs/source-matrix.md`](docs/source-matrix.md) drives every source in the
|
|
358
|
+
RW-WPS registry with real bytes through intermediate, init and a
|
|
359
|
+
five-composite-step forecast, and records one verdict per source — a green
|
|
360
|
+
run with receipts, or the chain's refusal, verbatim.
|
|
361
|
+
|
|
362
|
+
`--vertical-spec` is the native-free path (see *Assets you must supply* and
|
|
363
|
+
[`docs/native-free-init-admission.md`](docs/native-free-init-admission.md));
|
|
364
|
+
`--capsule`/`--reference` with a native-minted init file is the compatibility
|
|
365
|
+
mode. The two are mutually exclusive.
|
|
366
|
+
|
|
367
|
+
## Door 2: `gpuwm-hex render`
|
|
368
|
+
|
|
369
|
+
History in, product PNGs out, entirely through the Rust path.
|
|
370
|
+
|
|
371
|
+
```sh
|
|
372
|
+
gpuwm-hex render \
|
|
373
|
+
--history history.2025-03-15_00.00.00.nc \
|
|
374
|
+
--mesh assets/x4.163842.grid.nc \
|
|
375
|
+
--out ./png \
|
|
376
|
+
--simulation-start 2025-03-14_12:00:00 \
|
|
377
|
+
--products all
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
`rw_mpas_convert` resamples each history frame onto a render window and
|
|
381
|
+
`rw_wrfbatch` draws the products. PNGs are filed at render time into
|
|
382
|
+
`<out>/<domain>/<product>/<valid-day>/`, never flat, with a
|
|
383
|
+
`render-manifest.json` beside the tree carrying engine digests, per-frame
|
|
384
|
+
results and the exact invocations. Scratch lives in a sibling of `--out`, never
|
|
385
|
+
inside it, and is deleted after a clean run.
|
|
386
|
+
|
|
387
|
+
There is no fallback plotter. If the Rust renderer is absent the door refuses by
|
|
388
|
+
name; it never draws a weather field in Python. Full detail:
|
|
389
|
+
[`docs/render-door.md`](docs/render-door.md).
|
|
390
|
+
|
|
391
|
+
## Two smaller doors
|
|
392
|
+
|
|
393
|
+
`gpuwm-hex mesh-check --grid <grid> --static <static>` validates a mesh pair
|
|
394
|
+
before anything expensive touches it, and refuses a defective pair by name.
|
|
395
|
+
`gpuwm-hex oracle-gate` replays the source-extracted Fortran fixtures against
|
|
396
|
+
a mesh. Both are listed by `gpuwm-hex --help`.
|
|
397
|
+
|
|
398
|
+
## The forecast lane
|
|
399
|
+
|
|
400
|
+
There is no console script for it in 0.1.0, and that is a deliberate omission
|
|
401
|
+
rather than an oversight. The driver exists and works
|
|
402
|
+
(`tools/run_cuda_v841_forecast.py` in the source checkout, an arbitrary-case
|
|
403
|
+
runner), but it is not a door a user can walk through: it needs a `gpuwm`
|
|
404
|
+
source checkout at a pinned commit, its mesh and static inputs are byte-pinned
|
|
405
|
+
authority files with no fetch path, and the proven x4.163842 mesh admits at a
|
|
406
|
+
24 GiB free-device-memory floor (measured peak 20,902 MiB; the registered
|
|
407
|
+
x1.40962 mesh peaked at 9,948 MiB). Putting a console script on that would be
|
|
408
|
+
a front door on a room with no floor. Running a forecast in 0.1.0 means
|
|
409
|
+
working from the source checkout, and the packaging verdict says so.
|
|
410
|
+
|
|
411
|
+
### Local time stepping, opt-in
|
|
412
|
+
|
|
413
|
+
On a variable-resolution mesh most columns are far coarser than the finest one,
|
|
414
|
+
and the acoustic sub-step is sized for the finest. `--local-timestep` lets a
|
|
415
|
+
coarse column take fewer, longer acoustic sub-steps, chosen from the grid
|
|
416
|
+
file's own `dcEdge`.
|
|
417
|
+
|
|
418
|
+
```bash
|
|
419
|
+
python tools/run_cuda_v841_forecast.py \
|
|
420
|
+
--grid x4.163842.grid.nc --static x4.163842.static.nc \
|
|
421
|
+
--init <init>.nc --hours 6 \
|
|
422
|
+
--cache-root <cache> --output <out> \
|
|
423
|
+
--local-timestep
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
`--local-timestep-rates` sets the ladder (default `1,3`, two classes) and
|
|
427
|
+
`--local-timestep-buffer-rings` the width of the finer-rate buffer around a
|
|
428
|
+
class boundary (default 1).
|
|
429
|
+
|
|
430
|
+
**It is off by default and that is deliberate.** Native MPAS-A v8.4.1 has no
|
|
431
|
+
local time stepping: `Registry.xml:64-68` offers SRK3 only, and `dt_dynamics`,
|
|
432
|
+
`rk_timestep`, `rk_sub_timestep` and `number_sub_steps` are scalars at
|
|
433
|
+
`mpas_atm_time_integration.F:2053-2092`. There is therefore no byte-identical
|
|
434
|
+
implementation and there never can be. A user who turns it on takes a declared
|
|
435
|
+
divergence from native; a user who does not gets the pinned arithmetic
|
|
436
|
+
unchanged, byte for byte. This is a performance feature, not a correctness
|
|
437
|
+
remedy, so opt-in is the correct shape for it.
|
|
438
|
+
|
|
439
|
+
Two things follow from the mesh rather than the flag:
|
|
440
|
+
|
|
441
|
+
- On a **quasi-uniform** mesh every column lands in one class, so the option is
|
|
442
|
+
inert and the run is bit-identical to a default run. Measured on x1.40962:
|
|
443
|
+
all 40,962 cells in class 0, zero interface edges, identity permutation, and
|
|
444
|
+
every history frame SHA-256-identical.
|
|
445
|
+
- The ladder is not free. A rate must divide every RK stage's acoustic
|
|
446
|
+
sub-step count, and the released `(1, 3, 6)` schedule admits `1` and `3`
|
|
447
|
+
only — `2` and `4` do not divide the RK2 stage's three sub-steps.
|
|
448
|
+
|
|
449
|
+
Class boundaries are refluxed, so mass and passive water vapour are conserved
|
|
450
|
+
to binary32 rounding rather than exactly; see *Local time stepping is
|
|
451
|
+
flux-conservative, not exactly conservative* under **Limitations**. A run with
|
|
452
|
+
the option on stays bit-reproducible run to run, so the dual-run byte
|
|
453
|
+
comparison that screens for memory corruption on cards without ECC still
|
|
454
|
+
works.
|
|
455
|
+
|
|
456
|
+
#### What it costs, measured
|
|
457
|
+
|
|
458
|
+
**On the published x4.163842 mesh the option does not pay, and the ceiling
|
|
459
|
+
says it cannot.** Dry lane, 163,842 columns, 55 levels, RTX 5070 Ti, one model
|
|
460
|
+
hour:
|
|
461
|
+
|
|
462
|
+
| | wall seconds per model step |
|
|
463
|
+
|---|---|
|
|
464
|
+
| default path | 1.268 |
|
|
465
|
+
| `--local-timestep` | 1.283 |
|
|
466
|
+
|
|
467
|
+
0.988x — about 1% slower. The reason is measurable rather than mysterious.
|
|
468
|
+
Re-timing the same arm at 12 acoustic sub-steps instead of 6 gives the cost of
|
|
469
|
+
one sub-step directly, and from it **the acoustic loop is 23.5% of a model
|
|
470
|
+
step**. This mesh admits a 23.0% acoustic saving, so the best a whole step
|
|
471
|
+
could do is **1.057x** before the option pays for its own bookkeeping, and the
|
|
472
|
+
bookkeeping is larger than that.
|
|
473
|
+
|
|
474
|
+
The ceiling is a property of the released schedule, not of this mesh. A rate
|
|
475
|
+
must divide every stage's sub-step count, so `(1, 3, 6)` admits only rate 3,
|
|
476
|
+
and a rate-3 column still runs 4 of the 10 sub-steps a fine column runs. Even
|
|
477
|
+
in the limit where **every** column is coarse the saving is 60% of the acoustic
|
|
478
|
+
loop, which at a 23.5% share is **1.16x for the whole step**. That number is
|
|
479
|
+
the honest cap on this feature as the port stands.
|
|
480
|
+
|
|
481
|
+
What would move it: a mesh with a much steeper resolution gradient (the
|
|
482
|
+
generated 15 km-in-136 km box mesh classes to a 49.7% acoustic saving, a 1.13x
|
|
483
|
+
ceiling), and an acoustic schedule whose sub-step counts admit a rate above 3.
|
|
484
|
+
|
|
485
|
+
#### The full-step form, measured to a no-go
|
|
486
|
+
|
|
487
|
+
The stronger form of this feature — every dycore kernel launched per rate
|
|
488
|
+
class, whole RK steps advancing at `rate * dt` — is not capped by the acoustic
|
|
489
|
+
share. Its arithmetic prize on real meshes is large: counting cell-steps under
|
|
490
|
+
a `(1,2,4)` ladder with one buffer ring, the published x4.163842 mesh admits
|
|
491
|
+
**1.47x** and the generated 15 km-in-136 km box mesh **2.73x**. Whether the
|
|
492
|
+
card can collect that prize is a question about launch cost: a kernel launched
|
|
493
|
+
over a class must cost proportionally less than a launch over the whole mesh,
|
|
494
|
+
and below the card's occupancy knee it does not.
|
|
495
|
+
|
|
496
|
+
`tools/probe_lts_fullstep_projection.py` measured it (RTX 5070 Ti, the port's
|
|
497
|
+
own pinned kernels and their landed index-list derivations, the real meshes'
|
|
498
|
+
own class index lists, `evidence/local-timestep/fullstep-projection.json`):
|
|
499
|
+
|
|
500
|
+
| mesh | cells | arithmetic prize | measured projection |
|
|
501
|
+
|---|---|---|---|
|
|
502
|
+
| x1.40962 uniform | 40,962 | 1.000x | 1.000x |
|
|
503
|
+
| x4.163842 published VR | 163,842 | 1.467x | **1.254x** |
|
|
504
|
+
| 15 km-in-136 km box | 38,857 | 2.725x | **0.979x** |
|
|
505
|
+
|
|
506
|
+
The uniform row is the instrument's known-answer: one class, and the
|
|
507
|
+
index-list launch prices within 0.2% of the pinned kernel. On the box mesh the
|
|
508
|
+
prize inverts into a projected slowdown because the whole mesh is already
|
|
509
|
+
below the occupancy knee — doubling its cells costs only 1.26–1.30x on the
|
|
510
|
+
cell kernels — so a launch over its 5,562-cell fine class costs nearly what
|
|
511
|
+
the whole mesh costs, and the fine class launches four times per macro step.
|
|
512
|
+
The steeper the refinement, the smaller the classes, the harder the floor
|
|
513
|
+
bites: `(1,2,4,8)` projects worse (0.904x), not better. Only the x4-size mesh
|
|
514
|
+
keeps every class above the knee, and even there the trio projects 1.254x
|
|
515
|
+
**before** interface bookkeeping; the shipped acoustic form measured its own
|
|
516
|
+
bookkeeping at about 7% of a step on that mesh, and the full-step form pays a
|
|
517
|
+
cost of the same character plus time interpolation at class boundaries.
|
|
518
|
+
|
|
519
|
+
So the verdict, measured rather than estimated: rebuilding the whole step
|
|
520
|
+
loop per class would buy roughly 1.1–1.2x on the one registered mesh large
|
|
521
|
+
enough to profit, and a slowdown on the small steep meshes the prize was
|
|
522
|
+
supposed to come from. Not worth a rewrite of every kernel's launch path.
|
|
523
|
+
What could reopen it: meshes an order of magnitude larger, where every class
|
|
524
|
+
sits above the occupancy knee, or overlapping different classes' independent
|
|
525
|
+
work in concurrent streams — neither is measured here, and the probe is the
|
|
526
|
+
instrument to re-run when either becomes real.
|
|
527
|
+
|
|
528
|
+
---
|
|
529
|
+
|
|
530
|
+
## Limitations
|
|
531
|
+
|
|
532
|
+
Read these before trusting a number.
|
|
533
|
+
|
|
534
|
+
### The GF convection scheme is a different generation from native
|
|
535
|
+
|
|
536
|
+
**Declared, measured, and not a defect in the seam.** The seam-level non-parity
|
|
537
|
+
was closed: the four auxiliary forcing lanes, shallow-on, and per-cell `dx` all
|
|
538
|
+
reach the scheme the way native feeds them. What remains is that the port's
|
|
539
|
+
Grell-Freitas body is **WRF v4.6.1's Freitas-2018 generation**, while MPAS
|
|
540
|
+
v8.4.1's `module_cu_gf.mpas.F` is the **2013 ensemble fork**. Verified by source
|
|
541
|
+
count on both sides:
|
|
542
|
+
|
|
543
|
+
- native has zero occurrences of `dicycle` and zero of `tau_ecmwf` — those
|
|
544
|
+
closures do not exist in it at all;
|
|
545
|
+
- native carries Fritsch-Chappell `AA0/1200s` closure members the port does not;
|
|
546
|
+
- native runs `c0=.002`; the port runs a temperature-scaled `c0=.004`;
|
|
547
|
+
- native's shallow scheme is non-precipitating (`c0=0`); the port's shallow
|
|
548
|
+
scheme folds `prets` into `pratec`.
|
|
549
|
+
|
|
550
|
+
Closing this means porting native's `cup_gf`/`cup_gf_sh` bodies. That is a
|
|
551
|
+
program, not a seam edit — and whether it should close is the referee's call.
|
|
552
|
+
Every run receipt carries `gf_native_parity_claim: false` next to
|
|
553
|
+
`gf_declared_divergence` naming exactly the above, and so does every history
|
|
554
|
+
file written. The field stopped being called a blocker when parity was
|
|
555
|
+
retired as a goal: this is a declared property of the product, judged by
|
|
556
|
+
obs-skill.
|
|
557
|
+
|
|
558
|
+
### Local time stepping is flux-conservative, not exactly conservative
|
|
559
|
+
|
|
560
|
+
**Only reachable with `--local-timestep`, which is off by default.** When two
|
|
561
|
+
neighbouring columns advance on different acoustic sub-steps, the mass each
|
|
562
|
+
carries across the edge between them is integrated at two different rates and
|
|
563
|
+
the two no longer cancel. Mass is then created or destroyed at every class
|
|
564
|
+
boundary, and that is what kills local time stepping in practice.
|
|
565
|
+
|
|
566
|
+
The remedy here is Berger-Colella refluxing on the acoustic mass flux: the
|
|
567
|
+
coarse column keeps predicting with its own sub-step, the interface edge
|
|
568
|
+
accumulates the fine-rate integral minus that prediction, and the residual is
|
|
569
|
+
handed to the coarse column at the end of the RK stage. No term is dropped and
|
|
570
|
+
none is double counted, so the statement is **conservative in the flux and
|
|
571
|
+
approximate at binary32 rounding** — the coarse side applies one rounded sum
|
|
572
|
+
where the fine side applied a sequence of rounded increments.
|
|
573
|
+
|
|
574
|
+
Measured, not asserted. Dry lane, water vapour a passive scalar, published
|
|
575
|
+
x4.163842 variable-resolution mesh (5.46 max/min spacing, 1,063 interface
|
|
576
|
+
edges), one model hour:
|
|
577
|
+
|
|
578
|
+
| arm | dry-mass drift | passive-qv drift |
|
|
579
|
+
|---|---|---|
|
|
580
|
+
| default | 3.09e-11 | 9.02e-10 |
|
|
581
|
+
| `--local-timestep` | 2.58e-11 | 6.36e-10 |
|
|
582
|
+
|
|
583
|
+
against a 2.0e-8 bound. A full-physics qv budget cannot decide this: water
|
|
584
|
+
vapour there has sources and sinks and drifts about 1.8e-4 over six minutes
|
|
585
|
+
from the microphysics alone.
|
|
586
|
+
|
|
587
|
+
### Three bias-shaped differences against native — declared divergences
|
|
588
|
+
|
|
589
|
+
Not blockers, and not evidence of a broken dycore. They are the measured price
|
|
590
|
+
of running ArWen's physics instead of MPAS's, stated so a user knows what they
|
|
591
|
+
are getting. What is still owed on them is the obs-skill comparison, which is
|
|
592
|
+
the verification of record for physics here. The register of all three — the
|
|
593
|
+
mechanism, the magnitude and the named observational referee for each — is
|
|
594
|
+
[`docs/declared-divergences.md`](docs/declared-divergences.md).
|
|
595
|
+
|
|
596
|
+
Measured over 24 h, two independent weather cases, two mixing regimes,
|
|
597
|
+
163,842-cell mesh, cell-aligned with no interpolation:
|
|
598
|
+
|
|
599
|
+
1. **Upper-level warm drift.** Above level 45 the port warms relative to native
|
|
600
|
+
at **+0.019 K/h, near-linear, one-signed**, reaching **+0.46 K at 24 h** —
|
|
601
|
+
and it is identical in both weather cases and both mixing regimes. Case
|
|
602
|
+
independence means it is a code path, not weather. The legacy-RRTMG radiation
|
|
603
|
+
lane is the prime suspect. Fine at 24 h; extrapolated (not measured) a 7-day
|
|
604
|
+
run carries about +3.2 K of stratospheric error, which disqualifies this
|
|
605
|
+
version for long-range work.
|
|
606
|
+
2. **Convective-to-explicit precipitation repartition.** The port's GF produces
|
|
607
|
+
about **a third less convective rain** (`rainc` -36 % / -34 % in the two
|
|
608
|
+
cases); explicit microphysics makes up roughly half of it (`rainnc`
|
|
609
|
+
+29 % / +25 %); **net domain-mean precipitation runs about 15 % dry**. This
|
|
610
|
+
is the whole-model price of the declared GF generation gap above, quantified.
|
|
611
|
+
3. **Downstream condensate surplus.** With more rain made explicitly, the port
|
|
612
|
+
carries **+50 % cloud water and +62 % rain water** in the domain mean by
|
|
613
|
+
24 h, with much heavier point extrema (max-cell 24 h precipitation 502 vs
|
|
614
|
+
308 mm). Probably a consequence of (2); it should be re-measured after any GF
|
|
615
|
+
fix before anyone touches microphysics.
|
|
616
|
+
|
|
617
|
+
Everything else in the comparison is chaos-shaped — symmetric, growing with lead
|
|
618
|
+
time, driven by convective cells landing in different places, with envelope
|
|
619
|
+
statistics that match (peak updraft 11.49 vs 11.26 m/s; domain means within
|
|
620
|
+
0.05 %). That shape is expected between a single-precision CPU model and a CUDA
|
|
621
|
+
port and proves nothing wrong. The three above are one-signed and
|
|
622
|
+
case-independent, which chaos cannot be, and each names a lane to fix.
|
|
623
|
+
|
|
624
|
+
### Regional / limited-area is not available
|
|
625
|
+
|
|
626
|
+
0.1 is global variable-resolution only. Use mesh refinement to get resolution
|
|
627
|
+
where you want it. The regional lane is 0.3 material and the CUDA lane refuses
|
|
628
|
+
it today.
|
|
629
|
+
|
|
630
|
+
### Other
|
|
631
|
+
|
|
632
|
+
- **Multi-GPU is built and proven but not shipped in 0.1** (two-node, bitwise
|
|
633
|
+
partition-invariant, 1.23x on 25 GbE). It is 0.2 material.
|
|
634
|
+
- **A case has been seen to refuse mid-run** on a vertical-velocity divergence
|
|
635
|
+
at levels 44-47. The refusal is the model declining to publish a step it does
|
|
636
|
+
not trust, which is the designed behaviour, but it means a given case can stop
|
|
637
|
+
early rather than produce a bad forecast.
|
|
638
|
+
## Licence and derivation
|
|
639
|
+
|
|
640
|
+
gpuwm-hex is **Apache-2.0**, the same licence gpuwm ships under.
|
|
641
|
+
|
|
642
|
+
It reimplements the dynamical core of MPAS-Atmosphere v8.4.1 (LANL/UCAR) for
|
|
643
|
+
CUDA devices, with pinned source-line citations. That upstream is BSD-3-Clause,
|
|
644
|
+
whose terms govern the MPAS-derived portions and travel with every copy of this
|
|
645
|
+
one: `NOTICE` reproduces the MPAS licence in full and carries the marking it
|
|
646
|
+
requires of derivative works. `LICENSE` and `NOTICE` both ship inside the wheel
|
|
647
|
+
and the sdist.
|
|
648
|
+
|
|
649
|
+
This is **not** the version available from LANS and UCAR, and neither they nor
|
|
650
|
+
their contributors endorse it. Results from gpuwm-hex are not results from
|
|
651
|
+
MPAS-Atmosphere; where the two are known to differ, the differences are the
|
|
652
|
+
measured ones stated above.
|
|
653
|
+
|
|
654
|
+
---
|
|
655
|
+
|
|
656
|
+
## Development
|
|
657
|
+
|
|
658
|
+
Tests run from this directory:
|
|
659
|
+
|
|
660
|
+
```sh
|
|
661
|
+
PYTHONPATH=src python -m pytest tests -q
|
|
662
|
+
```
|
|
663
|
+
|
|
664
|
+
Three tiers gate themselves and each names why it is skipping — see
|
|
665
|
+
[`tools/battery/README.md`](tools/battery/README.md) for what each covers and
|
|
666
|
+
how to run the gated ones:
|
|
667
|
+
|
|
668
|
+
| tier | selector | needs |
|
|
669
|
+
| --- | --- | --- |
|
|
670
|
+
| unit + packaging | `-m "not gpu and not bigcard and not assets"` | nothing but Python |
|
|
671
|
+
| assets | `-m assets` | about 6.9 GiB of byte-pinned mesh/static/init/native-history files |
|
|
672
|
+
| big card | `-m bigcard` | a CUDA device with about 26.4 GiB free (the tier's own gate constant; the measured x4 peak at the current pin is 20,902 MiB, and the gate has not been re-derived since the pin move) |
|
|
673
|
+
|
|
674
|
+
`GPUWM_HEX_NO_LOCAL_GPU=1` (or `GPUWM_NO_LOCAL_GPU=1`, honoured so a box
|
|
675
|
+
configured for the engine behaves the same) bans device contact outright.
|