[jv] Convert to JAX using nested vmap (supersedes #618)#984
Conversation
Rewrites the on-the-job search lecture to use JAX on the GPU, replacing the JVWorker class, operator_factory, and the nested prange/for loops. The Bellman right-hand side is written as a scalar function _B of one state x and one action pair (s, ϕ), so it reads like the equation it implements. Three applications of jax.vmap then vectorize it over ϕ, s and x in turn, playing the role of a triple loop. T and get_greedy both reduce over the result, which removes the duplicated grid-search code the two functions previously carried. Other changes: - Model primitives move to a NamedTuple with a factory function, and the two action grids are stored alongside the state grid. - solve_model uses a bounded jax.lax.while_loop and returns the iteration count and final error so callers can check convergence. - The 45 degree diagram in Exercise 1 draws all realizations in one vectorized call rather than looping over states and draws, and plots them with a single ax.plot rather than 5,000. - Exercise 2 evaluates w*(ϕ) on the grid rather than via a list comprehension. - Adds the shared GPU admonition. Validated against the Numba implementation on main, using identical draws from f: both converge in 205 iterations, the value functions agree to 7.5e-06 (relative 6.2e-07), and the s and ϕ policies select identical grid points at every state. Checked float32 against float64 -- the policies are unchanged and Monte Carlo error at mc_size=100 dominates, so the default precision is kept. All twelve code cells execute on an RTX 4080, and the three figures reproduce the behaviour the surrounding prose describes. Supersedes #618. Co-Authored-By: Humphrey Yang <u6474961@anu.edu.au> Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
📖 Netlify Preview Ready!Preview URL: https://pr-984--sunny-cactus-210e3e.netlify.app Commit: 📚 Changed LecturesBuild Info
|
|
Many thanks @HumphreyYang , and sorry to supercede your PR. my bad for being slow with a review. |
|
@HumphreyYang @mmcky @kp992 @xuanguang-li This is a JAX conversion, initially kicked off by @HumphreyYang . We have a lot of open PRs and it will probably help the monorepo move if we have fewer, so I'll go ahead and merge. The style is compliant with the manual. Note the nested vmap style used to define the Bellman operator, which @kp992 initiated in a different setting a few years ago. I prefer this over heavily vectorized code, since it's closer to the maths. That preference is now noted in the manual in a recently merged PR. |
|
+1 @jstac. Nested vmap helps to directly relate the maths and also directly relates how we would write it with simple for loop in python. So it helps the readers to understand this easily. |
|
Thanks @kp992 , it's now in the manual: https://manual.quantecon.org/styleguide/jax.html#scalar-kernel-plus-nested-vmap |
|
Hi @jstac, This form of the value function looks great! I think the single-state argument version is much easier to understand because we don't have to imagine how the vector is stacked. It also reminds me of the vectorized functions in the basic Python lectures. I remember that some functions can be vectorized automatically. I'm not sure whether the same is true for JAX functions, but if so, the recursive |
Converts
jv.mdto JAX. This supersedes #618 by @HumphreyYang, and builds directly on it — theNamedTuplerefactor, thevmap'd grid search and thejnp.wherehandling of the feasibility constraint all come from that PR. Humphrey is credited as co-author on the commit.Why a separate PR
Reviewing #618 turned into a wider discussion about when a lecture should use JAX at all, so rather than pile more requests onto that branch it seemed cleaner to start from
mainand apply what came out of it. #618 can be closed in favour of this.Approach
The Bellman right-hand side is written as a scalar function of one state and one action pair, so it reads like the equation it implements:
Three applications of
jax.vmapthen vectorize it overϕ,sandxin turn, standing in for the triple loop:This follows the pattern in
opt_savings_2. The advantage over writing one big broadcast expression is that the reader never has to decode[:, None]placement to recover the maths — andTandget_greedybecomejnp.maxandjnp.argmaxover the same arrayB, which removes the ~20 lines of grid-search code the two functions otherwise duplicate.Other changes
NamedTuplewith a factory function; both action grids are stored alongside the state grid rather than rebuilt insideT.solve_modeluses a boundedjax.lax.while_loopand returns the iteration count and final error, so non-convergence is visible rather than silent.ax.plotrather than 5,000.w*(ϕ)on the grid instead of via a list comprehension.{include} _admonition/gpu.md.!pip install jax— CI already installsjax[cuda13], and no CPU pin, so the lecture uses the GPU the runner provides.A note on why JAX here
The lecture now says this explicitly, because it seemed worth being honest about:
Validation
Checked against the Numba implementation on
main, using identical draws fromfso the two are comparable:max abs(v_numba - v_new)7.5e-06(relative6.2e-07)sandϕpoliciesAlso checked float32 against float64: the policy grid indices are unchanged and Monte Carlo error at
mc_size=100dominates the difference, so the default precision is kept andjax_enable_x64is not set.All twelve code cells execute on a GPU (RTX 4080), and the three figures reproduce the behaviour the prose describes —
shigh then dropping to zero,ϕrising then declining, the 45 degree diagram random below ≈0.2 and deterministic above it converging near 1, andw*(ϕ)peaking at ≈0.6. Worth confirming against the preview build once CI runs.Related
Our JAX guidance has been consolidated into a single page in QuantEcon/QuantEcon.manual#113, which is where the conventions used here are written down.
🤖 Generated with Claude Code