Repository navigation
Conversation
np.vectorize looped over all non-core dimensions, although the new coordinates only vary along some of them (GH10683). _interpnd now loops over only those dimensions itself and passes all others to the interpolator in bulk, for numpy and dask arrays alike. The localization and conversion of the coordinates now happens once per call instead of once per loop iteration. Also fix interp with empty new coordinates for the linear and nearest methods. Co-authored-by: Claude <noreply@anthropic.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Claude <noreply@anthropic.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Claude <noreply@anthropic.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Claude <noreply@anthropic.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
It's somehow much uglier than just |
|
I can take a look later. My conclusion a year ago was that there are no real nice solutions without reinventing parts of apply_ufunc. Perhaps there's a vectorization helper for apply-ufunc that we can use here? But this is why i accepted the slowdown for #9881; indeed it was much worse than expected though. |
we can totally construct an interpolator outside the loop no? |
I let Claude benchmark several variations of that and neither showed a clear performance improvement. There are always tradeoffs for small or large arrays while just adding more code boilerplate. |
Thanks! I couln't find any such helper, but thinking about it we could totally turn the new interp code into such a helper. Potentially even add it as a new |
vectorize="shared" only loops over the broadcast dimensions along which more than one input varies and passes all others to func in bulk. interp now uses it instead of its own loop, and localizes and converts datetime coordinates once per call before apply_ufunc. Co-authored-by: Claude <noreply@anthropic.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
@dcherian I turned the loop into a helper and added it to [This is Claude Code on behalf of Michael Niklas] |
Co-authored-by: Claude <noreply@anthropic.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Tag the cells that fail on purpose with raises-exception so that the notebook is executed in the documentation build again, fix outdated text and links, and print simple values in the numba example. Co-authored-by: Claude <noreply@anthropic.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
🚀 Some benchmarks got better. To infinity and beyond!
🚀 better · 🐌 worse · 💥 now failing · 🩹 fixed · 🤷 not comparable Comparing |
Co-authored-by: Claude <noreply@anthropic.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Claude <noreply@anthropic.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Claude <noreply@anthropic.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts: # xarray/core/missing.py
|
@dcherian do you like the new vectorize option? In xarray I didn't find any other usecase for it (most applications are differing only by none or a single dim and there |
|
@dcherian sorry for the repeated ping. Did you have time to think about this? I think it's a clean interface. |
|
Sorry for the slow reply, ironically the more interesting PRs tend to get procrastinated upon.
Can we instead have I am unsure of cc @shoyer |
No worries
Sounds also like a useful feature. Why not both, explicit dimensions and "shared"? I'm sure both options share some similarities and don't need separate code paths. |
I worry that |
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e notebook apply_ufunc now takes the dimensions to loop over, e.g. vectorize=["time"], instead of the implicit "shared" mode. The apply_ufunc example notebook is removed in favor of the xarray-tutorial interpolation example. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Ok done. Seems sensible to me. |
| ) | ||
| var = Variable([f"dim_{dim}" for dim in range(ndim)], data, fastpath=True) | ||
|
|
||
| if interp_kwargs.get("method") in ["linear", "nearest"]: |
There was a problem hiding this comment.
i thought the point here was to do this for dask on a chunk-by-chunk basis?
There was a problem hiding this comment.
Per chunk, _localize never reduced I/O: the core dimension is a single chunk, so each block already contains all of x and localizing only shrank the in-memory array passed to scipy. For linear/nearest that doesn't save time either, since scipy only does a binary search into x (assume_sorted=True), while _localize itself is O(n) (x.to_index() and get_indexer) and ran for every block.
With 1 GB of data (64 × 2M points, 16 chunks) and chunked new coordinates, this PR is as fast or faster than main:
| new coordinates | method | main | this PR |
|---|---|---|---|
| narrow (500 of 2M) | linear | 228 ms | 161 ms |
| narrow (500 of 2M) | nearest | 231 ms | 241 ms |
wide (all of x) |
linear | 280 ms | 199 ms |
wide (all of x) |
nearest | 325 ms | 245 ms |
If the new coordinates are in memory, localizing once before apply_ufunc is better, because the lazy isel also drops the chunks of var that aren't needed.
Or was there another reason to localize per chunk that I'm missing?
There was a problem hiding this comment.
yes i believe its about controlling the memory used by scipy. Can you check memray
There was a problem hiding this comment.
I checked with memray, localizing per chunk doesn't reduce memory either. ScipyInterpolator passes copy=False and scipy's reshape of the data was a view in all layouts I tried, including the interpolated dimension in the middle ((y, x, w)), so scipy never copies the block. For linear/nearest, scipy only allocates arrays of size (new points × other dims), independent of the length of x. The peak is dask concatenating blocks, same on both branches:
| data (dask, chunked new coords) | method | main | this PR |
|---|---|---|---|
| (y, x) 64 × 2M, 16 blocks, sync scheduler | linear | 282 MB | 273 MB |
| nearest | 282 MB | 298 MB | |
| (y, x, w) 16 × 200k × 10, 4 blocks | linear | 212 MB | 212 MB |
| nearest | 209 MB | 210 MB |
(narrow new coordinates covering 500 points of x; covering all of x gives the same numbers)
Do you have a link to an older issue where the memory problem came up? Then I can test that case specifically.
There was a problem hiding this comment.
Found a case where it matters: integer data. scipy casts y to float64 (interp1d.__init__, and RegularGridInterpolator._check_values for n-D), so without localizing it copies the whole block instead of the small slice that is needed. Float data isn't affected, which is why my earlier test showed no difference.
memray, synchronous scheduler, chunked new coordinates covering a narrow range, peak in MiB (one block is 61 MiB for 1-D and 31 MiB for n-D):
| data | method | #4155 | main | PR before dda83a0 | PR after dda83a0 |
|---|---|---|---|---|---|
1-D float (y, x) 64 × 2M |
linear / nearest | 157 / 157 | 189 / 190 | 174 / 189 | 189 / 189 |
| 1-D int | linear / nearest | 157 / 157 | 189 / 189 | 235 / 250 | 189 / 189 |
n-D pointwise float (t, y, x) 16 × 2000² |
linear / nearest | 36 / 36 | 82 / 82 | 82 / 82 | 83 / 82 |
| n-D pointwise int | linear / nearest | 36 / 36 | 82 / 82 | 113 / 113 | 82 / 82 |
The #4155 column (where per-chunk localizing was added, run with 2020 dependencies) is lower because until #9881 _localize computed chunked new coordinates eagerly (new_x.values) and sliced the dask array before interpolating. Since #9881 skips chunked new coordinates there, per-chunk localizing is the only localization left for them.
So I restored per-chunk localizing in _interpnd when the new coordinates are chunked, and kept localizing once before apply_ufunc when they are in memory (dda83a0). With that, this PR matches main in all cases (last column). I also added a peakmem asv benchmark for this case, which goes from ~593 MiB to ~700 MiB for int data without the fix.
There was a problem hiding this comment.
Ok that's not very important then. Perhaps some upstream work has improved the situation
dcherian
left a comment
There was a problem hiding this comment.
Didn't review _vectorize_dims too closely but in general this seems ok. left some comments to be addressed.
Lets wait a couple of days to see if Stephan has any input. If not, lets merge.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Deepak Cherian <dcherian@users.noreply.github.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`_localize` skips chunked destination coordinates, so without localizing per block scipy casts the whole block to float for integer data. Add a peakmem benchmark for this case. Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Claude <noreply@anthropic.com>
Fixes the performance regression from #9881 when the new coordinates vary along a dimension of the interpolated object, e.g.
da[t, r, z].interp(z=new_z[t]).apply_ufuncaccepts dimension names forvectorize, e.g.vectorize=["t"]: it only loops over these broadcast dimensions, in the given order, and passes all others tofuncin bulk (which must then broadcast its inputs following numpy rules).interpuses it to only loop over the dimensions its new coordinates vary along, i.e.tinstead oftandrlikenp.vectorize. Works for numpy and dask alike, without changing chunks or the task graph._localizeand the datetime conversion run once per call beforeapply_ufuncinstead of once per loop iteration.method="linear"/"nearest".InterpolationVectorized/InterpolationVectorizedDask.apply_ufuncdocstring examples from basic to advanced. Theapply_ufunc_vectorize_1dexample notebook is removed in favor of the xarray-tutorial interpolation example. The newvectorizeoption is documented there and on the automatic vectorization page in apply_ufunc: only loop over some dimensions with vectorize=[dims] xarray-contrib/xarray-tutorial#379.Looping over explicit dimensions could also enable multidimensional weights in
polyfit(#9846): loop over the dimensions the weights vary along and solve all other series in onelstsqcall.This is an alternative to #11496, which declares the free dimensions as core dimensions (numpy only).
The remaining gap for
linearcompared to before #9881 comes from the Python loop overt: each iteration builds its own scipy interpolator. Before #9881, the shared dimensions were added as extra interpolation dimensions, which needs merged dask chunks, only works for n-D capable methods with numeric coordinates, and leaks NaNs between slices.whats-new.rstAI Disclosure
🤖 Generated with Claude Code