Parallel Value Function Iteration In Julia
Serial
Here we perform a serial (single-core) value function iteration to solve a simple savings problem with idiosyncratic income shocks. To solve the Bellman equation at each point in the state space, we use numerical optimization (Brent’s method) and monotonic cubic spline interpolation over the expected value function. For simplicity we iterate a fixed 100 times.
using InstantiateFromURL
github_project("aaowens/aaowens.github.io", path = "_misc/ParallelVFI", force = true)
Info Local TOML exists; removing now.
Activating environment at `~/Documents/data/myblog/aaowens.github.io/_posts
_content/Project.toml`
Precompiling project...
Info Project name is NA, version is NA
using Interpolations, Optim, BenchmarkTools, QuantEcon
const agrid = exp.(range(log(1), stop = log(200), length = 100 )) .- 1
uc(c) = c > 0 ? c^(-2.5)/-2.5 : -Inf
u(a, ap, x) = uc(a - ap + x)
maxap(a, x) = a + x
const beta = 0.96
using Random
Random.seed!(2)
mc = QuantEcon.tauchen(20, .8, .5)
const P = mc.p
const xgrid = exp.(mc.state_values) .+ 0.1
20-element Array{Float64,1}:
0.18208499862389876
0.20679522050714869
0.23894401308852461
0.2807706251409615
0.3351883912625588
0.40598765336756437
0.4980997680657806
0.6179405887454282
0.77385734679864
0.9767100584536119
1.240627953743172
1.583993614896087
2.030723372003401
2.61193313891799
3.3681057192812967
4.351910541297183
5.631872223267574
7.297143495221169
9.463714923300914
12.282493960703478
# Bellman step given evf and x
function bellman(evf, x)
vf, pol = similar(evf), similar(evf)
for i in eachindex(vf)
a = agrid[i]
obj = ap -> -(u(a, ap, x) + beta * evf(ap))
upper = min(maxap(a, x), agrid[end])
sol = optimize(obj, agrid[1], .99*upper)
vf[i], pol[i] = -sol.minimum, sol.minimizer
end
return vf, pol
end
function expectation(bigvf, P)
evf = similar(bigvf)
for a_nx in 1:size(bigvf, 1)
for x_nx in 1:size(P, 1)
ev = 0.
for xp_nx in 1:size(P, 2)
ev += bigvf[a_nx, xp_nx] * P[x_nx, xp_nx]
end
evf[a_nx, x_nx] = ev
end
end
return evf
end
expectation (generic function with 1 method)
vfall = zeros(length(agrid), length(xgrid))
polall = zeros(length(agrid), length(xgrid))
function test_serial(vfall, polall)
fill!(vfall, 0) # reset the value function each time we benchmark
fill!(polall, 0)
for i in 1:100
vfe = expectation(vfall, P)
for x_nx in eachindex(xgrid)
vfold = interpolate(agrid, vfe[:, x_nx], SteffenMonotonicInterpolation())
vf, pol = bellman(vfold, xgrid[x_nx])
vfall[:, x_nx] = vf; polall[:, x_nx] = pol
end
end
return vfall, polall
end
vfall, polall = test_serial(vfall, polall)
@time vfall, polall = test_serial(vfall, polall);
0.419485 seconds (618.11 k allocations: 53.327 MiB, 3.48% gc time)
Threads
Here we use multithreading. See https://docs.julialang.org/en/v1/manual/parallel-computing/#Setup-1 for how to enable this. There is no interaction between loop iterations, so this is thread-safe. We check the output at the end.
vfall2 = zeros(length(agrid), length(xgrid))
polall = zeros(length(agrid), length(xgrid))
function test_threads(vfall, polall)
fill!(vfall, 0) # reset the value function each time we benchmark
fill!(polall, 0)
for i in 1:100
vfe = expectation(vfall, P)
Threads.@threads for x_nx in eachindex(xgrid)
vfold = interpolate(agrid, vfe[:, x_nx], SteffenMonotonicInterpolation())
vf, pol = bellman(vfold, xgrid[x_nx])
vfall[:, x_nx] = vf; polall[:, x_nx] = pol
end
end
return vfall, polall
end
vfall, polall = test_threads(vfall2, polall)
@time vfall, polall = test_threads(vfall2, polall);
0.433563 seconds (619.01 k allocations: 53.406 MiB, 3.02% gc time)
all(vfall .== vfall2) #did multithreading give the same result as serial?
true
Threads is about 4x faster than serial with 4 threads. To check that we solved the model correctly, let’s plot the output.
using Plots
# net savings
p1 = plot(agrid, polall[:, 1] .- agrid)
plot!(agrid, polall[:, 10] .- agrid)
plot!(agrid, polall[:, 15] .- agrid)
# value
plot(agrid, vfall[:, 5])
plot!(agrid, vfall[:, 10])
plot!(agrid, vfall[:, 15])