Skip to content

Faster later calls of the generic interpolation path: memoize refactor and numpy flattening - #9

Merged
matschaer merged 5 commits into
mainfrom
perf/generic-path-next-calls
Nov 12, 2026
Merged

matschaer merged 5 commits into
mainfrom
perf/generic-path-next-calls

Conversation

@matschaer

Copy link
Copy Markdown
Collaborator

The generic (non-regular) path of interp2poi and interp2grid was much slower on later calls than the regular path, even though the neighbor indices were cached. Profiling showed that the time went into flattening the grids, not into the lookup. For example, for GFS 0.25° → 33 POIs, about 0.13 s of the 0.16 s per call went to xarray.stack (pandas MultiIndex), against 0.012 s for the lookup.

Changes

memoize refactor (grid_process/optimization.py)

  • The least recently used result is evicted, instead of the oldest one.
  • An optional key= function computes the cache key, only when caching is active. The grid-process call sites no longer build keys.
  • A cache size of 0 returns the function undecorated: no key, no lookup, no overhead.
  • The cache is thread-safe (a lock around the cache operations, not around the function).
  • It is fork-safe: a fork waits until no thread is using the cache, so a forked child never inherits a held lock or a half-updated cache.
  • The hkey= keyword argument still works as before (it is used by seamlessweatherprovider).

Cache key fixes

  • The POI and grid reprojection keys now include the source CRS. The same coordinates in another CRS could get a wrong cached result.
  • The regular-path nearest cache no longer fails when the POI dimension name is not poi (GRID_PROCESS__data_model__poi__dim).
  • Array keys include the shape and dtype besides the bytes.

Cache documentation: a new "In-memory caching" section on the Usage page lists the four caches, their sizes, and how to set or deactivate them with GRID_PROCESS__cache_size__<name> (set before importing grid-process). The interp2poi/interp2grid notes and grid_process.yaml point to it.

Numpy flattening (grid_process/grid_interp.py)

  • Structured grids are flattened with numpy (broadcast + ravel) instead of xarray.stack. _flatten_coords_crs is removed.
  • interp2poi still excludes grid points with NaN in all fields, as before. It now looks for them only among the points with NaN in the middle field. The first field is not used because accumulated variables are NaN everywhere at the first lead time.
  • The flattened POI data stays contiguous in memory (one copy, as on main). Gathering neighbors from a strided view was 2–4× slower.
  • interp2grid is unchanged in this respect, after an evaluation:
    • native ICON data from OGD is already contiguous;
    • for the operational gridefix inputs, a copy gave mixed speed and up to +0.28 GB of peak memory.

Fix: interp2poi raised an IndexError (in _fullnan_ds, also on main) when all POIs are outside an unstructured grid. They now get NaN, without numpy "Mean of empty slice" warnings. This fix is its own commit and can be moved to a separate PR if preferred.

Tests: new tests cover:

  • memoize: LRU, key=, size 0, hkey, metadata, and a fork while another thread holds the lock;
  • the flattening helpers, compared with xarray.stack;
  • the NaN-point exclusion, including precipitation-like and scattered-NaN cases, compared with xarray;
  • POIs outside an unstructured grid.

Plus the ruff E731 fixes in the tests.

Results

The benchmark ran 124 cases (operational gridefix IFS/ICON-CH1/ICON-CH2 to production POIs and seamless2km, native ICON from OGD, notebooks, global, topo), main vs branch in fresh processes, 2 rounds.

  • Values: identical in all cases, before and after the later fixes. The operational interp2poi cases were rerun after the middle-field change.
  • Next calls: geometric mean 0.88–0.93× of main on the interp2poi cases. No case is consistently slower.
Case (next call) main branch
ICON-CH1 / CH2 / CH2-LV95 nearest → production POIs 0.10 / 0.22 / 0.27 s 0.05 / 0.11 / 0.13 s
ICON nearest_alt → production POIs 0.12 / 0.21 s 0.05 / 0.11 s
Global IFS → 5000 world POIs 1.45 s, peak 1.45 GB 0.45 s, peak 1.05 GB
GFS 0.25° → 33 POIs (notebook 05 size) 0.13 s 0.05 s
GFS 0.25° → global 0.1° grid (notebook 06 size) 1.6 s 0.5 s
  • On par: operational temp, large-k idw, snowlim, and interp2grid from gridefix or native ICON. Their time goes into the interpolation itself.
  • Memory: peak memory is never higher than on main.

Checks

  • pytest: 252 passed.
  • ruff format --check and ruff check: clean (0 errors, including the E731 fixes).
  • mypy: clean.
  • pylint: 9.55, up from 9.52.
  • Docs build without warnings.
  • With GRID_PROCESS__cache_size__<name>=0, all five cached functions are undecorated and the results are identical.

The example notebooks were not re-executed (their timing tables would change).

🤖 Generated with Claude Code

Mathieu Schaer and others added 5 commits November 12, 2026 00:49
- memoize(maxsize, key=None): with a key function, the key is computed by the
  decorator only when caching is active; with maxsize <= 0 the function is
  returned undecorated. Without key, the hkey keyword argument works as before.
- Least recently used eviction (OrderedDict) instead of the oldest entry,
  and a lock around the cache so that it is safe to use from threads.
- The caches of grid_interp use key functions with exact array keys (shape,
  dtype, content). The reprojection keys now include the source CRS, and the
  regular grid nearest key no longer assumes a POI dimension named "poi".
- Document the in-memory caches and how to set their sizes or deactivate
  them with GRID_PROCESS__cache_size__<name> (Usage page, docstrings, yaml).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- _flatten_concat (interp2poi) and _flatten_concat_grids (interp2grid) get
  the flat coordinates and values with numpy (_flat_coord, _flat_values)
  instead of xarray.stack, which built a pandas MultiIndex over all points.
- The grid points NaN in all fields are found from the candidates NaN in
  their first field (_valid_points), and the data are copied only when some
  are excluded. Same results as isnull().all(), including NaT and no field.
- Remove _flatten_coords_crs, replaced by dm.get_coords_name_crs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…red grid

interp2poi raised an IndexError in _fullnan_ds when slicing an unstructured grid left no grid point (all POIs
outside of the domain): the empty spatial dimension is now padded so that the all-NaN output can be built.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
data4web-pipelines uses it to slice the input data with the largest buffer of its configured interpolation
methods. No alias is kept for the private name.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@matschaer
matschaer merged commit 95d3ab4 into main Nov 12, 2026
3 checks passed
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.

1 participant