Skip to content

Commit e57fabd

Browse files
committed
Experimental Feature - Accelerated NumPy
1 parent 6bf7e77 commit e57fabd

File tree

8 files changed

+819
-9
lines changed

8 files changed

+819
-9
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,4 @@ dist/
1818
doc/build/
1919
doc/source/**/generated/
2020
arch/univariate/recursions.c
21+
**/.DS_Store

arch/covariance/kernel.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,14 @@
44
from functools import cached_property
55
from typing import SupportsInt, cast
66

7-
import numpy as np
87
from pandas import DataFrame, Index
98
from pandas.util._decorators import Substitution
109

10+
from arch.experimental import numpy as np
1111
from arch.typing import ArrayLike, Float64Array
1212
from arch.utility.array import AbstractDocStringInheritor, ensure1d, ensure2d
1313

14+
1415
__all__ = [
1516
"Bartlett",
1617
"Parzen",
@@ -398,6 +399,7 @@ def cov(self) -> CovarianceEstimate:
398399
sr = x.T @ x / df
399400
w = self.kernel_weights
400401
num_weights = w.shape[0]
402+
x = np.asarray(self._x)
401403
oss = np.zeros((k, k))
402404
for i in range(1, num_weights):
403405
oss += w[i] * (x[i:].T @ x[:-i]) / df

arch/experimental/__init__.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
from .engine import (
2+
backend,
3+
set_backend,
4+
use_backend,
5+
NumpyEngine,
6+
LinAlgEngine,
7+
numpy,
8+
linalg,
9+
fori_loop,
10+
)

arch/experimental/engine.py

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
from contextlib import contextmanager
2+
from typing import Any
3+
4+
_BACKEND_ENGINE = "numpy"
5+
6+
7+
def backend():
8+
return _BACKEND_ENGINE
9+
10+
11+
def set_backend(library_name):
12+
"""
13+
Set backend engine.
14+
15+
The function sets the backend engine in global level.
16+
17+
Parameters
18+
----------
19+
library_name : str
20+
Library name. Default is `numpy`. Options are `numpy`, `tensorflow`,
21+
`cupy` and `jax`.
22+
"""
23+
assert library_name.lower() in ["numpy", "tensorflow", "cupy", "jax"], (
24+
"Only `numpy`, `tensorflow`, `cupy` and `jax` are supported, but not "
25+
f"{library_name}"
26+
)
27+
global _BACKEND_ENGINE
28+
_BACKEND_ENGINE = library_name
29+
30+
31+
@contextmanager
32+
def use_backend(library_name="numpy"):
33+
"""
34+
NumPy engine selection.
35+
36+
The function is a context manager to enable users to switch to a
37+
specific library as a replacement of NumPy in CPU.
38+
39+
Parameters
40+
----------
41+
library_name : str
42+
Library name. Default is `numpy`. Options are `numpy`, `tensorflow`,
43+
`cupy` and `jax`.
44+
"""
45+
assert library_name.lower() in ["numpy", "tensorflow", "cupy", "jax"], (
46+
"Only `numpy`, `tensorflow`, `cupy` and `jax` are supported, but not "
47+
f"{library_name}"
48+
)
49+
global _BACKEND_ENGINE
50+
_original = _BACKEND_ENGINE
51+
try:
52+
_BACKEND_ENGINE = library_name
53+
if _BACKEND_ENGINE == "tensorflow":
54+
import tensorflow.experimental.numpy as np
55+
56+
np.experimental_enable_numpy_behavior()
57+
yield
58+
finally:
59+
_BACKEND_ENGINE = _original
60+
61+
62+
class NumpyEngine:
63+
"""
64+
NumPy engine.
65+
"""
66+
67+
@property
68+
def name(self):
69+
"""
70+
Get engine name.
71+
"""
72+
global _BACKEND_ENGINE
73+
return _BACKEND_ENGINE
74+
75+
def __getattribute__(self, __name: str) -> Any:
76+
global _BACKEND_ENGINE
77+
try:
78+
if _BACKEND_ENGINE == "numpy":
79+
import numpy as anp
80+
elif _BACKEND_ENGINE == "tensorflow":
81+
import tensorflow.experimental.numpy as anp
82+
elif _BACKEND_ENGINE == "cupy":
83+
import cupy as anp
84+
elif _BACKEND_ENGINE == "jax":
85+
import jax.numpy as anp
86+
else:
87+
raise ValueError(f"Cannot recognize backend {_BACKEND_ENGINE}")
88+
except ImportError:
89+
raise ImportError(
90+
"Library `numpy` cannot be imported from backend engine "
91+
f"{_BACKEND_ENGINE}. Please make sure to install the library "
92+
f"via `pip install {_BACKEND_ENGINE}`."
93+
)
94+
95+
try:
96+
return getattr(anp, __name)
97+
except AttributeError:
98+
raise AttributeError(
99+
"Cannot get attribute / function from numpy library in "
100+
f"backend engine {_BACKEND_ENGINE}"
101+
)
102+
103+
104+
class LinAlgEngine:
105+
"""
106+
Linear algebra engine.
107+
"""
108+
109+
@property
110+
def name(self):
111+
"""
112+
Get engine name.
113+
"""
114+
global _BACKEND_ENGINE
115+
return _BACKEND_ENGINE
116+
117+
def __getattribute__(self, __name: str) -> Any:
118+
global _BACKEND_ENGINE
119+
try:
120+
if _BACKEND_ENGINE == "numpy":
121+
import numpy.linalg as alinalg
122+
elif _BACKEND_ENGINE == "tensorflow":
123+
import tensorflow.linalg as alinalg
124+
elif _BACKEND_ENGINE == "cupy":
125+
import cupy.linalg as alinalg
126+
elif _BACKEND_ENGINE == "jax":
127+
import jax.numpy.linalg as alinalg
128+
else:
129+
raise ValueError(f"Cannot recognize backend {_BACKEND_ENGINE}")
130+
except ImportError:
131+
raise ImportError(
132+
"Library `linalg` cannot be imported from backend engine "
133+
f"{_BACKEND_ENGINE}. Please make sure to install the library "
134+
f"via `pip install {_BACKEND_ENGINE}`."
135+
)
136+
137+
try:
138+
return getattr(alinalg, __name)
139+
except AttributeError:
140+
raise AttributeError(
141+
"Cannot get attribute / function from linalg library in "
142+
f"backend engine {_BACKEND_ENGINE}"
143+
)
144+
145+
146+
def fori_loop(lower, upper, body_fun, init_val=None):
147+
global _BACKEND_ENGINE
148+
if _BACKEND_ENGINE in ["numpy", "cupy"]:
149+
val = init_val
150+
for i in range(lower, upper):
151+
val = body_fun(i, val)
152+
return val
153+
elif _BACKEND_ENGINE == "jax":
154+
import jax.lax
155+
156+
return jax.lax.fori_loop(lower, upper, body_fun, init_val)
157+
elif _BACKEND_ENGINE == "tensorflow":
158+
import tensorflow as tf
159+
160+
i = tf.constant(lower)
161+
while_condition = lambda i: tf.less(i, upper)
162+
163+
def body(i, val):
164+
return [tf.add(i, 1), body_fun(val)]
165+
166+
return tf.while_loop(while_condition, body, [i, init_val])
167+
168+
raise ImportError(f"Cannot recognize backend {_BACKEND_ENGINE}")
169+
170+
171+
numpy = NumpyEngine()
172+
linalg = LinAlgEngine()

arch/unitroot/unitroot.py

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@
3737
squeeze,
3838
sum as npsum,
3939
)
40-
from numpy.linalg import LinAlgError, inv, lstsq, matrix_rank, pinv, qr, solve
40+
from numpy.linalg import LinAlgError, inv, lstsq, matrix_rank, pinv, solve
4141
from pandas import DataFrame
4242
from scipy.stats import norm
4343
from statsmodels.iolib.summary import Summary
@@ -86,6 +86,8 @@
8686
invalid_length_doc,
8787
)
8888
from arch.utility.timeseries import add_trend
89+
from arch.experimental import numpy as anp, linalg as alinalg
90+
8991

9092
__all__ = [
9193
"ADF",
@@ -337,8 +339,13 @@ def _autolag_ols(
337339
max_lags=maxlag, lag=max(exog_rank - startlag, 0)
338340
)
339341
)
340-
q, r = qr(exog)
341-
qpy = q.T @ endog
342+
343+
endog = anp.asarray(endog)
344+
exog = anp.asarray(exog)
345+
q, r = alinalg.qr(exog)
346+
# Convert it to 2-d so as to adapt to linalg.solve input format for all
347+
# engines
348+
qpy = (q.T @ endog)[:, anp.newaxis]
342349
ypy = endog.T @ endog
343350
xpx = exog.T @ exog
344351

@@ -347,12 +354,12 @@ def _autolag_ols(
347354
nobs = float(endog.shape[0])
348355
tstat[0] = inf
349356
for i in range(startlag, startlag + maxlag + 1):
350-
b = solve(r[:i, :i], qpy[:i])
351-
sigma2[i - startlag] = squeeze(ypy - b.T @ xpx[:i, :i] @ b) / nobs
357+
b = alinalg.solve(r[:i, :i], qpy[:i])
358+
sigma2[i - startlag] = anp.squeeze(ypy - b.T @ xpx[:i, :i] @ b) / nobs
352359
if lower_method == "t-stat" and i > startlag:
353-
xpxi = inv(xpx[:i, :i])
354-
stderr = sqrt(sigma2[i - startlag] * xpxi[-1, -1])
355-
tstat[i - startlag] = squeeze(b[-1]) / stderr
360+
xpxi = alinalg.inv(xpx[:i, :i])
361+
stderr = anp.sqrt(sigma2[i - startlag] * xpxi[-1, -1])
362+
tstat[i - startlag] = anp.squeeze(b[-1]) / stderr
356363

357364
return _select_best_ic(method, nobs, sigma2, tstat)
358365

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
.. module:: arch.experimental.engine
2+
:noindex:
3+
.. currentmodule:: arch.experimental.engine
4+
5+
Accelerated NumPy
6+
=================
7+
8+
The feature is to allow users to choose alternative NumPy-like engine
9+
to run on CPU and GPU. Currently, the following engine are supported in
10+
CPU and GPU runtime
11+
12+
13+
* `JAX <https://jax.readthedocs.io/en/latest/index.html#>`_
14+
15+
* `TensorFlow <https://www.tensorflow.org/guide/tf_numpy>`_
16+
17+
* `CuPy <https://docs.cupy.dev/en/stable/index.html>`_
18+
19+
There are two options users can switch the backend engine.
20+
21+
1. Context Manager
22+
23+
Users can use function ``use_backend`` in a ``with`` statement to temporarily
24+
switch the NumPy engine.
25+
26+
In the below example, assume that ``data`` object is a timeseries in NumPy array.
27+
The covariance estimation (NeweyWest) is computed in TensorFlow. Since the
28+
output is in TensorFlow Tensor type, the last line convert the long term
29+
covariance from Tensor to NumPy array type.
30+
31+
.. code-block:: python
32+
33+
import numpy as np
34+
35+
from arch.experimental import use_backend
36+
from arch.covariance.kernel import NeweyWest
37+
38+
with use_backend("tensorflow"):
39+
cov = NeweyWest(data).cov
40+
41+
long_term_cov = np.asarray(cov.long_term)
42+
43+
2. Global
44+
45+
Users can also configure the backend engine in global level with function
46+
``set_backend``.
47+
48+
.. code-block:: python
49+
50+
from arch.experimental import set_backend
51+
52+
set_backend("tensorflow")
53+
54+
# Output is already in TensorFlow Tensor type
55+
long_term_cov = NeweyWest(data).cov.long_term
56+
57+
For further examples, please refer to the example
58+
`notebook <experimental_accelerated_numpy.ipynb>`_.
59+
60+
Configure
61+
---------
62+
63+
.. autosummary::
64+
:toctree: generated/
65+
66+
use_backend
67+
set_backend
68+

doc/source/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ routines relevant for the analysis of financial data.
3535
Multiple Comparison Problems <multiple-comparison/multiple-comparisons>
3636
Unit Root Tests and Cointegration Analysis <unitroot/unitroot>
3737
Long-run Covariance Estimation <covariance/covariance>
38+
Experimental features <experimental/accelerated_numpy>
3839
API Reference <api>
3940
Change Log <changes>
4041

0 commit comments

Comments
 (0)