Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Speed up quantiles with sorting #1513

Merged
merged 63 commits into from
Jul 23, 2024
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
63 commits
Select commit Hold shift + click to select a range
7d752a4
Improvements to sdba _quantile function
SarahG-579462 Oct 26, 2023
2de9086
Add to vecquantiles
SarahG-579462 Oct 26, 2023
65ddbdd
fix vecquantiles?
SarahG-579462 Oct 26, 2023
719e9c6
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Oct 26, 2023
a798e8a
pre-commit and proper signature orders
SarahG-579462 Oct 26, 2023
a134bc2
Remove changes to vecquantiles
SarahG-579462 Oct 30, 2023
749a467
Reformat sdba-speed notebook
SarahG-579462 Oct 30, 2023
ea57cf5
Update AUTHORS.rst
SarahG-579462 Oct 30, 2023
7393c34
change criteria for nanquantile algorithm
SarahG-579462 Oct 30, 2023
8845208
precompilation for most nbutils
SarahG-579462 Oct 31, 2023
6e35ec4
fix escores
juliettelavoie Nov 16, 2023
88aadeb
Merge branch 'master' into speed-up-quantile
Zeitsperre Nov 16, 2023
500428f
Merge branch 'master' into speed-up-quantile
Zeitsperre Dec 12, 2023
f81f484
Merge branch 'master' into speed-up-quantile
coxipi Jan 15, 2024
5741c2d
`allow_sortquantile` option
coxipi Apr 29, 2024
006d26e
Merge branch 'main' of github.com:Ouranosinc/xclim into speed-up-quan…
coxipi Apr 30, 2024
33a79b0
time dimensions not stacked = faster quantile
coxipi Apr 30, 2024
b629e7d
Merge branch 'main' of github.com:Ouranosinc/xclim into speed-up-quan…
coxipi Apr 30, 2024
8e1e362
correct nd vs.1d condition & clean
coxipi Apr 30, 2024
e4da29e
np reshape and apply_ufunc -> avoid stack, etc
coxipi May 3, 2024
3d42f87
add fastnanquantile as option, _sort True by default
coxipi May 6, 2024
d4430d6
Merge branch 'main' of github.com:Ouranosinc/xclim into speed-up-quan…
coxipi May 6, 2024
354d78f
update doc
coxipi May 6, 2024
5c8667a
move benchmark tests to hidden folder
coxipi May 6, 2024
2842b39
update doc, ignore notebook
coxipi May 6, 2024
3a1cde8
Merge branch 'main' of github.com:Ouranosinc/xclim into speed-up-quan…
coxipi May 8, 2024
d63af58
add 1d numba-compiled implementation of core.utils._nan_quantile
SarahG-579462 May 14, 2024
915cd6a
add test_quantile.ipynb temporarily, to test numpy-level implementations
SarahG-579462 May 14, 2024
2810d86
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] May 14, 2024
b2e08cb
Merge branch 'main' into speed-up-quantile
coxipi Jun 6, 2024
89fdf00
Merge branch 'main' into speed-up-quantile
Zeitsperre Jun 11, 2024
11858ae
Merge branch 'main' into speed-up-quantile
coxipi Jun 17, 2024
5ab3f2d
Merge branch 'main' of https://github.com/Ouranosinc/xclim into speed…
coxipi Jun 18, 2024
7bb324e
remove _sortquantile, use utils._nan_quantile
coxipi Jun 18, 2024
8333357
_nan_quantile in vecquantiles
coxipi Jun 18, 2024
b9d2126
Merge branch 'speed-up-quantile' of https://github.com/Ouranosinc/xcl…
coxipi Jun 18, 2024
6dc179f
remove mentions of _sortquantile
coxipi Jun 18, 2024
d6c87c0
revert: np.nanquantile in vecquantiles
coxipi Jun 18, 2024
0c1ea62
quantile -> nanquantile (duh)
coxipi Jun 18, 2024
8191aa0
add issue number
coxipi Jun 18, 2024
1159cdd
public nan_quantile & add back docstring
coxipi Jun 20, 2024
8d5e4d6
Delete test_quantile.ipynb
coxipi Jun 21, 2024
c001863
remove comments
coxipi Jun 21, 2024
a6807ee
revert unwanted AUTHORS change
coxipi Jun 21, 2024
2204a80
Merge branch 'main' of https://github.com/Ouranosinc/xclim into speed…
coxipi Jun 27, 2024
3e32930
Merge branch 'main' into speed-up-quantile
Zeitsperre Jul 2, 2024
980ec94
1d numba compatible _nan_quantile in sdba
coxipi Jul 9, 2024
99c368a
Merge branch 'speed-up-quantile' of https://github.com/Ouranosinc/xcl…
coxipi Jul 9, 2024
ccc8291
test nbu.quantile
coxipi Jul 9, 2024
7905a15
conserve arr.dtype
coxipi Jul 9, 2024
d5b81c0
Merge branch 'main' of https://github.com/Ouranosinc/xclim into speed…
coxipi Jul 9, 2024
a2682ef
CHANGELOG formatting
coxipi Jul 9, 2024
1585fc0
unwanted space
coxipi Jul 9, 2024
10b13dd
respect np2 new conventions
coxipi Jul 9, 2024
53c9f23
extras dependency (fastnanquantile)
coxipi Jul 12, 2024
48b2e14
Merge branch 'main' of https://github.com/Ouranosinc/xclim into speed…
coxipi Jul 12, 2024
9206b73
extra mention of extra
coxipi Jul 12, 2024
d876376
Merge branch 'main' of https://github.com/Ouranosinc/xclim into speed…
coxipi Jul 19, 2024
0b58a78
Merge branch 'main' into speed-up-quantile
Zeitsperre Jul 19, 2024
6542128
Merge branch 'main' into speed-up-quantile
Zeitsperre Jul 19, 2024
f93e71f
Merge branch 'main' of https://github.com/Ouranosinc/xclim into speed…
coxipi Jul 19, 2024
e3d0112
Merge branch 'speed-up-quantile' of https://github.com/Ouranosinc/xcl…
coxipi Jul 19, 2024
df6963b
Merge branch 'main' into speed-up-quantile
coxipi Jul 23, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions AUTHORS.rst
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ Contributors
* David Caron `@davidcaron <https://github.com/davidcaron>`_
* Carsten Ehbrecht <ehbrecht@dkrz.de> `@cehbrecht <https://github.com/cehbrecht>`_
* Jeremy Fyke `@jeremyfyke <https://github.com/jeremyfyke>`_
* Sarah Gammon `@SarahG-579462 <https://github.com/SarahG-579462>`
coxipi marked this conversation as resolved.
Show resolved Hide resolved
* Tom Keel <thomas.keel.18@ucl.ac.uk> `@Thomasjkeel <https://github.com/Thomasjkeel>`_
* Marie-Pier Labonté <labonte.marie-pier@ouranos.ca> `@marielabonte <https://github.com/marielabonte>`_
* Ludwig Lierhammer <ludwig.lierhammer@hereon.de> `@ludwiglierhammer <https://github.com/ludwiglierhammer>`_
Expand Down
94 changes: 94 additions & 0 deletions docs/notebooks/sdba-speed.ipynb
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
{
"cells": [
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"from __future__ import annotations\n",
"\n",
"import importlib\n",
"import os\n",
"import time\n",
"\n",
"import numpy as np\n",
"\n",
"from xclim import sdba\n",
"\n",
"q = 50\n",
"sizes = [25, 50, 75, 100, 200, 300, 400, 500, 600, 700, 800, 900, 1000, 1500, 2000]\n",
"tsort = np.zeros(len(sizes))\n",
"tnan = np.zeros(len(sizes))\n",
"num_tests = 500\n",
"\n",
"os.environ[\"USE_NANQUANTILE\"] = \"\"\n",
"importlib.reload(sdba.nbutils)\n",
"sdba.nbutils._choosequantile(np.random.randn(100), np.array([0.5]))\n",
"for i in range(len(sizes)):\n",
" for k in range(num_tests):\n",
" arr = np.random.randn(sizes[i])\n",
" t0 = time.time()\n",
" sdba.nbutils._choosequantile(arr, np.linspace(0, 1, q))\n",
" tsort[i] += time.time() - t0\n",
"\n",
"os.environ[\"USE_NANQUANTILE\"] = \"True\"\n",
"importlib.reload(sdba.nbutils)\n",
"sdba.nbutils._choosequantile(np.random.randn(100), np.array([0.5]))\n",
"for i in range(len(sizes)):\n",
" for k in range(num_tests):\n",
" arr = np.random.randn(sizes[i])\n",
" t0 = time.time()\n",
" sdba.nbutils._choosequantile(arr, np.linspace(0, 1, q))\n",
" tnan[i] += time.time() - t0\n",
"print(\n",
" \"\\n\".join(\n",
" [\n",
" f\"{x:4.0f}, {y:.5f}, {z:.5f}, {z / y:.2f}\"\n",
" for x, y, z in zip(sizes, tsort, tnan)\n",
" ]\n",
" )\n",
")\n",
"import matplotlib.pyplot as plt\n",
"\n",
"plt.plot(sizes, tsort / num_tests, label=\"sortquantile\")\n",
"plt.plot(sizes, tnan / num_tests, label=\"nanquantile\")\n",
"plt.legend()\n",
"plt.title(\n",
" \"Quantile calculation using sortquantile and nanquantile, average time vs array size, for 50 quantiles\"\n",
")\n",
"plt.xlabel(\"Array size\")\n",
"plt.ylabel(\"Time (s)\")"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"plt.plot(sizes, tnan / tsort, label=\"Ratio of times, nanquantile / sortquantile\")\n",
"plt.legend()\n",
"plt.title(\"Ratio of times, nanquantile / sortquantile\")\n",
"plt.xlabel(\"Array size\")\n",
"plt.ylabel(\"Time ratio\")"
]
}
],
"metadata": {
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.11.6"
}
},
"nbformat": 4,
"nbformat_minor": 2
}
188 changes: 161 additions & 27 deletions xclim/sdba/nbutils.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,114 @@
"""
from __future__ import annotations

from os import environ

import numpy as np
from numba import boolean, float32, float64, guvectorize, njit
from numba import boolean, float32, float64, guvectorize, int64, njit
from xarray import DataArray
from xarray.core import utils

USE_NANQUANTILE = environ.get("USE_NANQUANTILE", False)
coxipi marked this conversation as resolved.
Show resolved Hide resolved


@njit(
[
int64(float32[:]),
int64(float64[:]),
],
fastmath=False,
nogil=True,
cache=False,
coxipi marked this conversation as resolved.
Show resolved Hide resolved
)
def numnan_sorted(s):
"""Given a sorted array s, return the number of NaNs."""
# Given a sorted array s, return the number of NaNs.
# This is faster than np.isnan(s).sum(), but only works if s is sorted,
# and only for
ind = 0
for i in range(s.size - 1, 0, -1):
if np.isnan(s[i]):
ind += 1
else:
return ind
return ind


@njit(
[
float32[:](float32[:], float32[:]),
float64[:](float64[:], float64[:]),
],
fastmath=False,
nogil=True,
cache=False,
)
def _sortquantile(arr, q):
"""Sorts arr into ascending order,
then computes the quantiles as a linear interpolation
between the sorted values.
"""
sortarr = np.sort(arr)
numnan = numnan_sorted(sortarr)
# compute the indices where each quantile should go:
# nb: nan goes to the end, so we need to subtract numnan to the size.
indices = q * (arr.size - 1 - numnan)
# compute the quantiles manually to avoid casting to float64:
# (alternative is to use np.interp(indices, np.arange(arr.size), sortarr))
frac = indices % 1
low = np.floor(indices).astype(np.int64)
high = np.ceil(indices).astype(np.int64)
return (1 - frac) * sortarr[low] + frac * sortarr[high]


@njit(
[
float32[:](float32[:], float32[:]),
float64[:](float64[:], float64[:]),
],
fastmath=False,
nogil=True,
cache=False,
)
def _choosequantile(arr, q):
# When the number of quantiles requested is large,
# it becomes more efficient to sort the array,
# and simply obtain the quantiles from the sorted array.
# The first method is O(arr.size*q.size),
# the second O(arr.size*log(arr.size) + q.size) amortized.
if (q.size <= 10) or (q.size <= np.log(arr.size)) or (USE_NANQUANTILE):
return np.nanquantile(arr, q).astype(arr.dtype)
else:
return _sortquantile(arr, q)


@njit(
[
float32[:](float32[:], float32[:]),
float64[:](float64[:], float64[:]),
float32[:, :](float32[:, :], float32[:]),
float64[:, :](float64[:, :], float64[:]),
],
fastmath=False,
nogil=True,
cache=False,
)
def _quantile(arr, q):
if arr.ndim == 1:
out = np.empty((q.size,), dtype=arr.dtype)
out[:] = _choosequantile(arr, q)
else:
out = np.empty((arr.shape[0], q.size), dtype=arr.dtype)
for index in range(out.shape[0]):
out[index] = _choosequantile(arr[index], q)
return out


@guvectorize(
[(float32[:], float32, float32[:]), (float64[:], float64, float64[:])],
"(n),()->()",
nopython=True,
cache=True,
cache=False,
)
def _vecquantiles(arr, rnk, res):
if np.isnan(rnk):
Expand Down Expand Up @@ -42,18 +139,6 @@ def vecquantiles(da, rnk, dim):
return res


@njit
def _quantile(arr, q):
if arr.ndim == 1:
out = np.empty((q.size,), dtype=arr.dtype)
out[:] = np.nanquantile(arr, q)
else:
out = np.empty((arr.shape[0], q.size), dtype=arr.dtype)
for index in range(out.shape[0]):
out[index] = np.nanquantile(arr[index], q)
return out


def quantile(da, q, dim):
"""Compute the quantiles from a fixed list `q`."""
coxipi marked this conversation as resolved.
Show resolved Hide resolved
# We have two cases :
Expand All @@ -66,14 +151,13 @@ def quantile(da, q, dim):
dims = [dim] if isinstance(dim, str) else dim
tem = utils.get_temp_dimname(da.dims, "temporal")
da = da.stack({tem: dims})

# So we cut in half the definitions to declare in numba
# We still use q as the coords so it corresponds to what was done upstream
if not hasattr(q, "dtype") or q.dtype != da.dtype:
qc = np.array(q, dtype=da.dtype)
else:
qc = q

# if not hasattr(q, "dtype") or q.dtype != da.dtype or q.shape[0] != da.shape[0]:
# qc = np.array(q, dtype=da.dtype)
# else:
# qc = q
qc = np.array(q, dtype=da.dtype)
coxipi marked this conversation as resolved.
Show resolved Hide resolved
if len(da.dims) > 1:
# There are some extra dims
extra = utils.get_temp_dimname(da.dims, "extra")
Expand All @@ -98,7 +182,15 @@ def quantile(da, q, dim):
return res


@njit
@njit(
[
float32[:, :](float32[:, :]),
float64[:, :](float64[:, :]),
],
fastmath=False,
nogil=True,
cache=False,
)
def remove_NaNs(x): # noqa
"""Remove NaN values from series."""
remove = np.zeros_like(x[0, :], dtype=boolean)
Expand All @@ -107,7 +199,15 @@ def remove_NaNs(x): # noqa
return x[:, ~remove]


@njit(fastmath=True)
@njit(
[
float32(float32[:, :], float32[:, :]),
float64(float64[:, :], float64[:, :]),
],
fastmath=True,
coxipi marked this conversation as resolved.
Show resolved Hide resolved
nogil=True,
cache=False,
)
def _correlation(X, Y):
"""Compute a correlation as the mean of pairwise distances between points in X and Y.

Expand All @@ -124,7 +224,15 @@ def _correlation(X, Y):
return d / (X.shape[1] * Y.shape[1])


@njit(fastmath=True)
@njit(
[
float32(float32[:, :]),
float64(float64[:, :]),
],
fastmath=True,
coxipi marked this conversation as resolved.
Show resolved Hide resolved
nogil=True,
cache=False,
)
def _autocorrelation(X):
"""Mean of the NxN pairwise distances of points in X of shape KxN.

Expand All @@ -147,7 +255,7 @@ def _autocorrelation(X):
],
"(k, n),(k, m)->()",
nopython=True,
cache=True,
cache=False,
)
def _escore(tgt, sim, out):
"""E-score based on the Székely-Rizzo e-distances between clusters.
Expand All @@ -170,7 +278,17 @@ def _escore(tgt, sim, out):
out[0] = w * (sXY + sXY - sXX - sYY) / 2


@njit
@njit(
# [
coxipi marked this conversation as resolved.
Show resolved Hide resolved
# float32[:,:](float32[:]),
# float64[:,:](float64[:]),
# float32[:,:](float32[:,:]),
# float64[:,:](float64[:,:]),
# ],
fastmath=False,
nogil=True,
cache=False,
)
def _first_and_last_nonnull(arr):
"""For each row of arr, get the first and last non NaN elements."""
out = np.empty((arr.shape[0], 2))
Expand All @@ -183,7 +301,15 @@ def _first_and_last_nonnull(arr):
return out


@njit
@njit(
# [
coxipi marked this conversation as resolved.
Show resolved Hide resolved
# float64[:](float32[:],float32[:],float32[:],float32[:],float32[:],float32[:],optional(typeof("constant"))),
# float64[:](float64[:],float64[:],float64[:],float64[:],float64[:],float64[:],optional(typeof("constant"))),
# ],
fastmath=False,
nogil=True,
cache=False,
)
def _extrapolate_on_quantiles(
interp, oldx, oldg, oldy, newx, newg, method="constant"
): # noqa
Expand All @@ -207,7 +333,15 @@ def _extrapolate_on_quantiles(
return interp


@njit
@njit(
# [
coxipi marked this conversation as resolved.
Show resolved Hide resolved
# typeof((float64[:,:],float64,float64))(float32[:],float32[:],optional(boolean)),
# typeof((float64[:,:],float64,float64))(float64[:],float64[:],optional(boolean)),
# ],
fastmath=False,
nogil=True,
cache=False,
)
def _pairwise_haversine_and_bins(lond, latd, transpose=False):
"""Inter-site distances with the haversine approximation."""
N = lond.shape[0]
Expand Down
Loading