Optimise memory allocation in batchprocessing#501
Merged
Conversation
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #501 +/- ##
=====================================
Coverage 78.1% 78.1%
=====================================
Files 33 33
Lines 6626 6627 +1
Branches 1260 1260
=====================================
+ Hits 5173 5174 +1
Misses 1180 1180
Partials 273 273
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
rbeucher
approved these changes
Jul 14, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fix OOM and poor parallelism in batch CMORisation of long daily/heavy variables
Summary
Batch CMORisation runs one PBS job per variable. Two problems made the heavy
variables unusable:
as a single process with N threads, so every netCDF4 read serialised on the
global HDF5 lock.
tasmax/tasmincould not complete at all on a long run (5,352 dailyfiles, ~446 years). Workers were repeatedly OOM-killed on a
finalize-hlgfinalizecomputetask and the job died.The root cause of (2) turned out to be a latent eager-compute bug:
np.isclose(dask_array, ...)is not a ufunc, so it does not dispatch lazily— it materialises the entire array into memory. Applied to the raw daily series
during missing-value normalisation, it loaded ~18 GB into a single worker.
After the fixes, all 22 ILAMB variables complete, with no memory pressure, and
outputs are bit-identical.
Root causes
1. Eager
np.iscloseon Dask arrays (the OOM)normalize_dataset_missing_valuesbuilt its NaN mask withnp.isclose(var, missing_value, ...). Verified behaviour:np.iscloseis a compound function, not a ufunc, so it bypasses Dask'sdispatch and coerces the array to NumPy. The daily source
fld_s03i236carriesmissing_value = _FillValue = 1e+20, so the mask was built over the raw dailyseries (~166,000 timesteps ≈ 18.5 GB) inside
_normalize_missing_values_early— during
load_dataset, i.e. before the monthly resample and before any write.That single materialisation is the
finalizetask that killed the workers.Log ordering confirmed it: the first event in the failing run was
Could not normalize missing values early: ... finalize ... 4 workers died.2. Threaded Dask client → ~1 core
processes=Falsewith N threads serialises all netCDF4 reads on the HDF5 lock.Measured on a 12-core node (1032 cold files, cVeg read+sum):
Results were bit-identical (
sum=1.19124e+49).3. Task-graph explosion on daily inputs
Daily files are stored with on-disk HDF5 chunking
[1, 145, 192]— one chunkper day.
chunks={}inherits that layout, so a 31-day file became 31 Daskchunks (~650k tasks at full scale). Monthly variables were unaffected (their
on-disk chunk is the whole field).
4. Monolithic write
The driver defaults
enable_chunking=False, sowrite()materialised the wholevariable in one
.compute()rather than streaming per time-chunk.Changes
vocabulary_processors.pynp.isclose(var, m, rtol=0, atol=a)with the Dask-native, lazyabs(var - m) <= a(exactly equivalent atrtol=0). This is the OOM fix.derivations/calc_utils.py_mask_missing_values_for_reduction(same bug, fires during the resample).base.pychunks={"time": -1}on the multi-file read — one chunk per file. Collapses the daily task graph ~26x. Monthly inputs unaffected (time size 1).templates/cmor_python_script.j2processes=TrueDask client, sized per variable after file discovery;enable_chunking=Trueso writes stream in bounded time-chunks.executors/dask_config.pyPBS_NCPUS/PBS_MEM_GB; probes one input file to pick a per-worker memory floor.Worker sizing
All parallelism comes from processes;
threads_per_worker=1. Extra threadsadd no read throughput (HDF5 lock + GIL) while doubling a worker's resident
data — the opposite of what memory-limited variables need.
Per-worker memory floors are env-overridable
(
MOPPY_WORKER_GB_LIGHT|MEDIUM|HEAVY), calibrated from measured peaks once thepipeline was fully streaming (~12.5 GB daily, ~10 GB carbon, ~6 GB light):
tas,prcVeg,cSoil,nbptasmax,tasminValidation
Full-scale batch (5,352 daily files, ~446 years) — all 22 variables pass
tasmaxtasmincVegcSoilnbptasPreviously
tasmax/tasminfailed outright (workers OOM-killed onfinalize). All other variables complete with zero memory-pressure warnings.Correctness
tasmaxoutput is bit-exact to an independently computed monthly maximumof the daily source (and clearly not the mean).
cVegoutput is bit-identical withenable_chunkingon vs off (data,coords and all bounds variables).
processes=Trueproduces bit-identical results to the threaded client at fullscale.
np.isclose.Test suite
No new failures introduced; 26 previously-failing tests now pass. The 11
remaining failures are a strict subset of the pre-existing ones (
test_cmip7_*CV tests and two
test_memory_usagetests that also fail on clean HEAD).Compliance (IOOS compliance-checker, all 22 outputs)
outputs were written flat (
drs_rootunset), soDRS project root 'cmip6' not found in the file path;PATH001/PATH002merely inherit that failure. Nota data problem.
tasmax/tasminhave 3 extra WCRP items (branded variable not in thechecker's registry).
tsl— a variable this PR does not touch — has theidentical 3 items, confirming a pre-existing checker-registry gap rather
than a regression.