You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: lectures/sargent_surico.md
+209-7Lines changed: 209 additions & 7 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -29,6 +29,9 @@ kernelspec:
29
29
:depth: 2
30
30
```
31
31
32
+
```{include} _admonition/gpu.md
33
+
```
34
+
32
35
In addition to what's in Anaconda, this lecture uses `pandas_datareader` to download
33
36
macroeconomic data and `jax`, `numpyro` and `arviz` for the Hamiltonian Monte Carlo
34
37
section at the end:
@@ -63,7 +66,7 @@ We do all of this from scratch in Python.
63
66
64
67
We write our own solver for linear rational expectations models, our own Kalman filter, and our own Metropolis-Hastings sampler, so that every step is visible.
65
68
66
-
A final section then uses the estimated model as a test bed for Hamiltonian Monte Carlo, which turns out to require replacing the model solver with a differentiable one.
69
+
A final section then uses the estimated model as a test bed for Hamiltonian Monte Carlo, which turns out to require replacing the model solver with a differentiable one and, if a GPU is to be worth using, the Kalman filter with one that works on all dates at once.
67
70
68
71
Along the way we flag several places where the published paper's statement or implementation of its model needs care, and we check each of them numerically.
69
72
@@ -1384,6 +1387,10 @@ $$ (eq:ss_fixedpoint)
1384
1387
Iterating {eq}`eq:ss_fixedpoint` from $G = 0$ involves nothing but matrix products
1385
1388
and linear solves, every one of them differentiable.
1386
1389
1390
+
We enable 64-bit precision, which Kalman filtering needs, and we let JAX use
1391
+
whatever hardware it finds; a later subsection reorganizes the filter so that a
1392
+
GPU, if one is present, is actually worth using.
1393
+
1387
1394
```{code-cell} ipython3
1388
1395
import jax
1389
1396
import jax.numpy as jnp
@@ -1393,7 +1400,7 @@ from jax import lax
1393
1400
from numpyro.infer import MCMC, NUTS
1394
1401
1395
1402
jax.config.update('jax_enable_x64', True)
1396
-
jax.config.update('jax_platform_name', 'cpu')
1403
+
print(f'JAX backend: {jax.default_backend()}')
1397
1404
1398
1405
U, NJ = 8, 9 # y = [pi, x, dm, R, e, a, chi, z, u]
1399
1406
@@ -1565,12 +1572,191 @@ whole point of reverse-mode differentiation.
1565
1572
1566
1573
A finite-difference gradient would need at least nineteen likelihood evaluations.
1567
1574
1575
+
### Parallelizing the filter over time
1576
+
1577
+
On a CPU the story could end here, but on a GPU the filter above is very slow, and
1578
+
the reason is worth understanding because it has nothing to do with arithmetic
1579
+
speed.
1580
+
1581
+
A GPU is thousands of arithmetic units that want a few large array operations; what
1582
+
our `lax.scan` gives it is a chain of about a thousand tiny dependent ones per
1583
+
likelihood, since every date's handful of $11 \times 11$ products must wait for the
1584
+
date before it, and each little operation pays a fixed kernel launch overhead that
1585
+
dwarfs the arithmetic inside it.
1586
+
1587
+
Multiply that chain by the hundreds of thousands of leapfrog steps in a NUTS run
1588
+
and the GPU spends nearly all of its time waiting rather than computing.
1589
+
1590
+
Kalman filtering looks irreducibly sequential, but it is not.
1591
+
1592
+
{cite:t}`SarkkaGarcia2021` showed that the filtering recursion is the repeated
1593
+
application of an *associative* binary operation, and any associative operation
1594
+
over $T$ items can be evaluated in a balanced tree of depth $\log_2 T$ rather than
1595
+
a chain of length $T$, which is the same observation that lets parallel hardware
1596
+
compute cumulative sums.
1597
+
1598
+
`jax.lax.associative_scan` supplies the tree; our job is to supply the elements
1599
+
and the binary operation.
1600
+
1601
+
The element for date $k$ packages what observation $Y_k$ says about the state
1602
+
given the previous state, as five arrays $(A_k, b_k, C_k, \eta_k, J_k)$ that
print(f'{nuts_seconds:.0f} seconds for 4 chains of 400 draws')
1826
+
print(f'{nuts_seconds:.0f} seconds for 4 {chain_method} chains of 400 draws '
1827
+
f'on the {jax.default_backend()}')
1634
1828
print(f'mean leapfrog steps per iteration '
1635
1829
f'{np.asarray(extra["num_steps"]).mean():.0f}')
1636
1830
print(f'divergences '
@@ -1892,6 +2086,12 @@ Replacing an eigenvalue-sorting solver by a fixed point that uses only linear al
1892
2086
buys exact gradients for about the cost of one extra likelihood evaluation, and that
1893
2087
is enough to put NUTS within reach.
1894
2088
2089
+
A second obstacle appears on a GPU, and it too is algorithmic rather than
2090
+
statistical: a sequential filter over tiny matrices leaves massively parallel
2091
+
hardware idle, and the cure is again to reorganize the computation, across time by
2092
+
the associative scan of {cite:t}`SarkkaGarcia2021` and across chains by
2093
+
vectorizing them, so that every kernel launch carries real work.
2094
+
1895
2095
The payoff was not only speed.
1896
2096
1897
2097
Cheap chains made it cheap to run several of them and inspect their diagnostics, and
@@ -1934,7 +2134,9 @@ On the computational side, the model turned out to be a useful test bed for Hami
1934
2134
1935
2135
The barrier to using it on DSGE models is not statistical but algorithmic: the standard solvers sort eigenvalues, and sorting has no derivative.
1936
2136
1937
-
Swapping in a fixed-point solver that uses only linear algebra restores exact gradients, and the sampler that becomes available is far more efficient per unit of computing time on a posterior as badly scaled as this one.
2137
+
Swapping in a fixed-point solver that uses only linear algebra restores exact gradients, and a second swap, of the sequential Kalman recursion for an associative scan that a GPU can evaluate in $\log_2 T$ rounds, lets modern parallel hardware carry the sampler.
2138
+
2139
+
The sampler that becomes available is far more efficient per unit of computing time on a posterior as badly scaled as this one.
0 commit comments