GitHub

@@ -48,6 +48,18 @@ tags: [hide-output]

4848

!pip install quantecon jax

4949

```

505051+

```{admonition} GPU

52+

:class: warning

53+54+

This lecture is accelerated via [hardware](status:machine-details) that has access to a GPU and target JAX for GPU programming.

55+56+

Free GPUs are available on Google Colab.

57+

To use this option, please click on the play icon top right, select Colab, and set the runtime environment to include a GPU.

58+59+

Alternatively, if you have your own GPU, you can follow the [instructions](https://github.com/google/jax) for installing JAX with GPU support.

60+

If you would like to install JAX running on the `cpu` only you can use `pip install jax[cpu]`

61+

```

62+5163

We will use the following imports.

52645365

```{code-cell} ipython3

@@ -317,7 +329,7 @@ with qe.Timer(precision=8):

317329

z_max = jnp.max(f(x_mesh, y_mesh)).block_until_ready()

318330

```

319331320-

Once compiled, JAX will be significantly faster than NumPy, especially if you are using a GPU.

332+

Once compiled, JAX is significantly faster than NumPy due to GPU acceleration.

321333322334

The compilation overhead is a one-time cost that pays off when the function is called repeatedly.

323335

@@ -370,23 +382,29 @@ with qe.Timer(precision=8):

370382

z_max.block_until_ready()

371383

```

372384373-

The execution time is similar to the mesh operation but, by avoiding the large input arrays `x_mesh` and `y_mesh`,

374-

we are using far less memory.

385+

By avoiding the large input arrays `x_mesh` and `y_mesh`, this `vmap` version uses far less memory.

386+387+

When run on a CPU, its runtime is similar to that of the meshgrid version.

375388376-

In addition, `vmap` allows us to break vectorization up into stages, which is

377-

often easier to comprehend than the traditional approach.

389+

When run on a GPU, it is usually significantly faster.

378390379-

This will become more obvious when we tackle larger problems.

391+

In fact, using `vmap` has another advantage: It allows us to break vectorization up into stages.

392+393+

This leads to code that is often easier to comprehend than traditional vectorized code.

394+395+

We will investigate these ideas more when we tackle larger problems.

380396381397382398

### vmap version 2

383399384400

We can be still more memory efficient using vmap.

385401386-

While we avoided large input arrays in the preceding version,

402+

While we avoid large input arrays in the preceding version,

387403

we still create the large output array `f(x,y)` before we compute the max.

388404389-

Let's use a slightly different approach that takes the max to the inside.

405+

Let's try a slightly different approach that takes the max to the inside.

406+407+

Because of this change, we never compute the two-dimensional array `f(x,y)`.

390408391409

```{code-cell} ipython3

392410

@jax.jit

@@ -399,23 +417,28 @@ def compute_max_vmap_v2(grid):

399417

return jnp.max(f_vec_max(grid))

400418

```

401419402-

Let's try it

420+

Here

421+422+

* `f_vec_x_max` computes the max along any given row

423+

* `f_vec_max` is a vectorized version that can compute the max of all rows in parallel.

424+425+

We apply this function to all rows and then take the max of the row maxes.

426+427+

Let's try it.

403428404429

```{code-cell} ipython3

405430

with qe.Timer(precision=8):

406431

z_max = compute_max_vmap_v2(grid).block_until_ready()

407432

```

408433409-410434

Let's run it again to eliminate compilation time:

411435412436

```{code-cell} ipython3

413437

with qe.Timer(precision=8):

414438

z_max = compute_max_vmap_v2(grid).block_until_ready()

415439

```

416440417-

We don't get much speed gain but we do save some memory.

418-441+

If you are running this on a GPU, as we are, you should see another nontrivial speed gain.

419442420443421444

### Summary

@@ -497,7 +520,9 @@ Now let's create a JAX version using `lax.scan`:

497520

from jax import lax

498521

from functools import partial

499522500-

@partial(jax.jit, static_argnums=(1,))

523+

cpu = jax.devices("cpu")[0]

524+525+

@partial(jax.jit, static_argnums=(1,), device=cpu)

501526

def qm_jax(x0, n, α=4.0):

502527

def update(x, t):

503528

x_new = α * x * (1 - x)

@@ -509,6 +534,16 @@ def qm_jax(x0, n, α=4.0):

509534510535

This code is not easy to read but, in essence, `lax.scan` repeatedly calls `update` and accumulates the returns `x_new` into an array.

511536537+

```{note}

538+

Sharp readers will notice that we specify `device=cpu` in the `jax.jit` decorator.

539+540+

The computation consists of many very small `lax.scan` iterations that must run sequentially, leaving little opportunity for the GPU to exploit parallelism.

541+542+

As a result, kernel-launch overhead tends to dominate on the GPU, making the CPU a better fit for this workload.

543+544+

Curious readers can try removing this option to see how performance changes.

545+

```

546+512547

Let's time it with the same parameters:

513548514549

```{code-cell} ipython3

Read the original on github.com ↗