Skip to content

Commit 9b5f167

Browse files
authored
Cache model-constant computations to minimize allocations across all modules (#234)
1 parent f7a513b commit 9b5f167

32 files changed

Lines changed: 7312 additions & 3491 deletions

.github/workflows/ci.yml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,7 @@ jobs:
152152
arch: x64
153153
test_set: "jet"
154154
- version: 'lts'
155-
os: ubuntu-latest
155+
os: macOS-latest
156156
arch: x64
157157
test_set: "jet"
158158
steps:
@@ -186,9 +186,9 @@ jobs:
186186
- name: Set Custom Test Environment Variable (non-Windows)
187187
if: matrix.os != 'windows-latest'
188188
run: echo "TEST_SET=${{ matrix.test_set }}" >> $GITHUB_ENV
189-
- name: Set JULIA_NUM_THREADS for estimation tests
190-
if: (matrix.version == '1' && (matrix.test_set == 'estimation' || matrix.test_set == 'estimate_sw07' || matrix.test_set == '1st_order_inversion_estimation' || matrix.test_set == 'estimation_pigeons' || matrix.test_set == '1st_order_inversion_estimation_pigeons'))
191-
run: echo "JULIA_NUM_THREADS=auto" >> $GITHUB_ENV
189+
# - name: Set JULIA_NUM_THREADS for estimation tests
190+
# if: (matrix.version == '1' && (matrix.test_set == 'estimation' || matrix.test_set == 'estimate_sw07' || matrix.test_set == '1st_order_inversion_estimation' || matrix.test_set == 'estimation_pigeons' || matrix.test_set == '1st_order_inversion_estimation_pigeons'))
191+
# run: echo "JULIA_NUM_THREADS=auto" >> $GITHUB_ENV
192192
- uses: julia-actions/cache@v2
193193
- uses: julia-actions/julia-buildpkg@v1
194194
- uses: julia-actions/julia-runtest@v1

AGENTS.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,14 @@ like `[0]` (present), `[1]` (future), `[-1]` (past), and `[x]` (shock).
3030

3131
## Tests
3232

33+
Before running tests, activate the test environment and instantiate dependencies:
34+
35+
```julia
36+
using Pkg
37+
Pkg.activate("test")
38+
Pkg.instantiate()
39+
```
40+
3341
Tests are organized by test sets specified via `TEST_SET`:
3442

3543
```bash

AGENT_PROGRESS.md

Lines changed: 1334 additions & 0 deletions
Large diffs are not rendered by default.

CLAUDE.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,14 @@ MacroModelling.jl is a Julia package for developing and solving Dynamic Stochast
1313

1414
### Running Tests
1515

16+
Before running tests, activate the test environment and instantiate dependencies:
17+
18+
```julia
19+
using Pkg
20+
Pkg.activate("test")
21+
Pkg.instantiate()
22+
```
23+
1624
Tests are organized by test sets specified via the `TEST_SET` environment variable:
1725

1826
```bash

benchmark/benchmarks.jl

Lines changed: 81 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,56 @@ import LinearAlgebra as ℒ
1616
using MacroModelling
1717
import MacroModelling: clear_solution_caches!, get_NSSS_and_parameters, calculate_jacobian, merge_calculation_options, solve_lyapunov_equation, ℳ
1818

19+
# Check if new workspace API is available (not present in old package versions)
20+
const HAS_WORKSPACE_API = isdefined(MacroModelling, :Lyapunov_workspace)
21+
22+
# Conditionally import workspace types only if they exist
23+
if HAS_WORKSPACE_API
24+
import MacroModelling: Lyapunov_workspace, lyapunov_workspace, ensure_lyapunov_workspace!, ensure_qme_workspace!, ensure_sylvester_1st_order_workspace!
25+
end
26+
27+
# Version-aware wrapper for solve_lyapunov_equation benchmarking
28+
# For new API: uses pre-allocated workspace for true benchmark of workspace reuse
29+
# For old API: calls without workspace argument
30+
function solve_lyapunov_for_bench(A, C, lyap_ws; lyapunov_algorithm::Symbol = :doubling)
31+
if HAS_WORKSPACE_API
32+
# New API - reuse pre-allocated workspace (shows benefit of workspace caching)
33+
return solve_lyapunov_equation(A, C, lyap_ws; lyapunov_algorithm = lyapunov_algorithm)
34+
else
35+
# Old API - no workspace argument
36+
return solve_lyapunov_equation(A, C; lyapunov_algorithm = lyapunov_algorithm)
37+
end
38+
end
39+
40+
function timings_for_bench(𝓂::ℳ)
41+
if hasproperty(𝓂, :timings)
42+
out = 𝓂.timings
43+
else
44+
out = 𝓂.constants.post_model_macro
45+
end
46+
return out
47+
end
48+
49+
function first_order_solution_for_bench(∇₁::AbstractMatrix, 𝓂::ℳ; opts = merge_calculation_options())
50+
if HAS_WORKSPACE_API
51+
qme_ws = ensure_qme_workspace!(𝓂)
52+
sylv_ws = ensure_sylvester_1st_order_workspace!(𝓂)
53+
out = calculate_first_order_solution(∇₁, 𝓂.constants, qme_ws, sylv_ws; opts = opts)
54+
else
55+
out = calculate_first_order_solution(∇₁; T = timings_for_bench(𝓂), opts = opts)
56+
end
57+
return out
58+
end
59+
60+
function calculate_jacobian_for_bench(parameters, SS_and_pars, 𝓂::ℳ)
61+
if hasmethod(calculate_jacobian, Tuple{typeof(parameters), typeof(SS_and_pars), ℳ})
62+
out = calculate_jacobian(parameters, SS_and_pars, 𝓂)
63+
else
64+
out = calculate_jacobian(parameters, SS_and_pars, 𝓂.caches, 𝓂.functions.jacobian)
65+
end
66+
return out
67+
end
68+
1969

2070
function run_benchmarks!(𝓂::ℳ, SUITE::BenchmarkGroup)
2171
SUITE[𝓂.model_name] = BenchmarkGroup()
@@ -34,37 +84,46 @@ function run_benchmarks!(𝓂::ℳ, SUITE::BenchmarkGroup)
3484
SUITE[𝓂.model_name]["NSSS"] = @benchmarkable get_NSSS_and_parameters($𝓂, $𝓂.parameter_values) setup = clear_solution_caches!($𝓂, :first_order)
3585

3686

37-
∇₁ = calculate_jacobian(𝓂.parameter_values, reference_steady_state, 𝓂)
87+
∇₁ = calculate_jacobian_for_bench(𝓂.parameter_values, reference_steady_state, 𝓂)
3888

3989
clear_solution_caches!(𝓂, :first_order)
4090

41-
SUITE[𝓂.model_name]["jacobian"] = @benchmarkable calculate_jacobian($𝓂.parameter_values, $reference_steady_state, $𝓂) setup = clear_solution_caches!($𝓂, :first_order)
91+
SUITE[𝓂.model_name]["jacobian"] = @benchmarkable calculate_jacobian_for_bench($𝓂.parameter_values, $reference_steady_state, $𝓂) setup = clear_solution_caches!($𝓂, :first_order)
4292

4393

4494
SUITE[𝓂.model_name]["qme"] = BenchmarkGroup()
45-
46-
sol, qme_sol, solved = calculate_first_order_solution(∇₁; T = 𝓂.timings, opts = merge_calculation_options(quadratic_matrix_equation_algorithm = :schur))
47-
95+
96+
qme_schur_opts = merge_calculation_options(quadratic_matrix_equation_algorithm = :schur)
97+
qme_doubling_opts = merge_calculation_options(quadratic_matrix_equation_algorithm = :doubling)
98+
99+
sol, qme_sol, solved = first_order_solution_for_bench(∇₁, 𝓂; opts = qme_schur_opts)
100+
48101
clear_solution_caches!(𝓂, :first_order)
49-
50-
SUITE[𝓂.model_name]["qme"]["schur"] = @benchmarkable calculate_first_order_solution($∇₁; T = $𝓂.timings, opts = merge_calculation_options(quadratic_matrix_equation_algorithm = :schur)) setup = clear_solution_caches!($𝓂, :first_order)
51-
52-
SUITE[𝓂.model_name]["qme"]["doubling"] = @benchmarkable calculate_first_order_solution($∇₁; T = $𝓂.timings, opts = merge_calculation_options(quadratic_matrix_equation_algorithm = :doubling)) setup = clear_solution_caches!($𝓂, :first_order)
53-
54-
55-
A = @views sol[:, 1:𝓂.timings.nPast_not_future_and_mixed] *.diagm(ones(𝓂.timings.nVars))[𝓂.timings.past_not_future_and_mixed_idx,:]
56-
57-
C = @views sol[:, 𝓂.timings.nPast_not_future_and_mixed+1:end]
58-
102+
103+
SUITE[𝓂.model_name]["qme"]["schur"] = @benchmarkable first_order_solution_for_bench($∇₁, $𝓂; opts = $qme_schur_opts) setup = clear_solution_caches!($𝓂, :first_order)
104+
SUITE[𝓂.model_name]["qme"]["doubling"] = @benchmarkable first_order_solution_for_bench($∇₁, $𝓂; opts = $qme_doubling_opts) setup = clear_solution_caches!($𝓂, :first_order)
105+
106+
T = timings_for_bench(𝓂)
107+
108+
A = @views sol[:, 1:T.nPast_not_future_and_mixed] *.diagm(ones(T.nVars))[T.past_not_future_and_mixed_idx,:]
109+
110+
C = @views sol[:, T.nPast_not_future_and_mixed+1:end]
111+
59112
CC = C * C'
60113

61-
solve_lyapunov_equation(A, CC)
114+
# Create workspace once before benchmarks (new API) or use nothing (old API)
115+
# For new API: pre-allocated workspace shows benefit of workspace caching
116+
# For old API: workspace is not used
117+
lyap_ws = HAS_WORKSPACE_API ? Lyapunov_workspace(size(A, 1)) : nothing
118+
119+
# Warm up call
120+
solve_lyapunov_for_bench(A, CC, lyap_ws)
62121

63122
SUITE[𝓂.model_name]["lyapunov"] = BenchmarkGroup()
64-
SUITE[𝓂.model_name]["lyapunov"]["doubling"] = @benchmarkable solve_lyapunov_equation($A, $CC, lyapunov_algorithm = :doubling) # setup = clear_solution_caches!($𝓂, :first_order)
65-
SUITE[𝓂.model_name]["lyapunov"]["bartels_stewart"] = @benchmarkable solve_lyapunov_equation($A, $CC, lyapunov_algorithm = :bartels_stewart) # setup = clear_solution_caches!($𝓂, :first_order)
66-
SUITE[𝓂.model_name]["lyapunov"]["bicgstab"] = @benchmarkable solve_lyapunov_equation($A, $CC, lyapunov_algorithm = :bicgstab) # setup = clear_solution_caches!($𝓂, :first_order)
67-
SUITE[𝓂.model_name]["lyapunov"]["gmres"] = @benchmarkable solve_lyapunov_equation($A, $CC, lyapunov_algorithm = :gmres) # setup = clear_solution_caches!($𝓂, :first_order)
123+
SUITE[𝓂.model_name]["lyapunov"]["doubling"] = @benchmarkable solve_lyapunov_for_bench($A, $CC, $lyap_ws, lyapunov_algorithm = :doubling)
124+
SUITE[𝓂.model_name]["lyapunov"]["bartels_stewart"] = @benchmarkable solve_lyapunov_for_bench($A, $CC, $lyap_ws, lyapunov_algorithm = :bartels_stewart)
125+
SUITE[𝓂.model_name]["lyapunov"]["bicgstab"] = @benchmarkable solve_lyapunov_for_bench($A, $CC, $lyap_ws, lyapunov_algorithm = :bicgstab)
126+
SUITE[𝓂.model_name]["lyapunov"]["gmres"] = @benchmarkable solve_lyapunov_for_bench($A, $CC, $lyap_ws, lyapunov_algorithm = :gmres)
68127

69128

70129
clear_solution_caches!(𝓂, :first_order)
@@ -103,7 +162,7 @@ run_benchmarks!(Smets_Wouters_2007, SUITE)
103162
# end
104163
# end
105164

106-
# If a cache of tuned parameters already exists, use it, otherwise, tune and cache
165+
# If a caches of tuned parameters already exists, use it, otherwise, tune and caches
107166
# the benchmark parameters. Reusing cached parameters is faster and more reliable
108167
# than re-tuning `SUITE` every time the file is included.
109168
# paramspath = joinpath(dirname(@__FILE__), "params.json")
@@ -113,4 +172,4 @@ run_benchmarks!(Smets_Wouters_2007, SUITE)
113172
# else
114173
# tune!(SUITE)
115174
# BenchmarkTools.save(paramspath, params(SUITE))
116-
# end
175+
# end

docs/make.jl

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
using Documenter
88
using MacroModelling
9-
import Turing, StatsPlots
9+
import Optim, StatsPlots, Turing
1010
using DocumenterCitations
1111

1212
bib = CitationBibliography(
@@ -23,7 +23,12 @@ makedocs(
2323
# doctest = false,
2424
# draft = true,
2525
format = Documenter.HTML(size_threshold = 204800*10),
26-
modules = [MacroModelling],
26+
modules = [
27+
MacroModelling,
28+
Base.get_extension(MacroModelling, :OptimExt),
29+
Base.get_extension(MacroModelling, :StatsPlotsExt),
30+
Base.get_extension(MacroModelling, :TuringExt),
31+
],
2732
pages = [
2833
"Introduction" => "index.md",
2934
"Tutorials" => [

docs/src/api.md

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
11
```@autodocs
2-
Modules = [MacroModelling]
2+
Modules = [
3+
MacroModelling,
4+
Base.get_extension(MacroModelling, :OptimExt),
5+
Base.get_extension(MacroModelling, :StatsPlotsExt),
6+
Base.get_extension(MacroModelling, :TuringExt),
7+
]
38
Order = [:function, :macro, :module]
4-
```
9+
```

ext/OptimExt.jl

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
module OptimExt
22

3-
import MacroModelling: find_shocks_conditional_forecast, find_SS_solver_parameters!, Tolerances, ℳ, calculate_SS_solver_runtime_and_loglikelihood, solver_parameters
3+
import MacroModelling: find_shocks_conditional_forecast, find_SS_solver_parameters!, Tolerances, ℳ, calculate_SS_solver_runtime_and_loglikelihood, solver_parameters, find_shocks_workspace
44
import Optim
55

66
# Helper function for LBFGS optimization objective
@@ -24,8 +24,8 @@ end
2424
conditions::Vector{Float64},
2525
cond_var_idx::Vector{Int},
2626
free_shock_idx::Vector{Int},
27-
pruning::Bool,
28-
S₁, S₂, S₃, timings; verbose::Bool = false)
27+
state_update::Function,
28+
S₁, S₂, S₃, constants, ws; verbose::Bool = false)
2929
3030
Find shocks that satisfy conditional forecast constraints using LBFGS optimizer.
3131
@@ -37,10 +37,11 @@ Note: This is the Optim-based implementation. It requires the Optim.jl extension
3737
- `conditions`: Target values for conditioned variables
3838
- `cond_var_idx`: Indices of conditioned variables
3939
- `free_shock_idx`: Indices of free shocks to be determined
40-
- `pruning`: Whether pruning is used
40+
- `state_update`: State update function for the selected algorithm
4141
- `S₁`: First-order solution matrix
4242
- `S₂`, `S₃`: Higher-order perturbation matrices (not used in LBFGS, for compatibility only)
43-
- `timings`: Model timings structure
43+
- `constants`: Model constants (unused for LBFGS)
44+
- `ws`: Find shocks workspace (unused for LBFGS)
4445
4546
# Returns
4647
- `x`: Vector of optimal shock values
@@ -55,7 +56,9 @@ function find_shocks_conditional_forecast(::Val{:LBFGS},
5556
free_shock_idx::Vector{Int},
5657
state_update::Function,
5758
# pruning::Bool,
58-
S₁, S₂, S₃, timings; verbose::Bool = false)
59+
S₁, S₂, S₃, constants,
60+
ws::find_shocks_workspace{Float64};
61+
verbose::Bool = false)
5962

6063
pruning = typeof(initial_state) <: Vector{Vector{Float64}}
6164
precision_factor = 1.0
@@ -94,7 +97,7 @@ function find_shocks_conditional_forecast(::Val{:LBFGS},
9497
end
9598

9699
"""
97-
find_SS_solver_parameters!(::Val{:SAMIN}, 𝓂::ℳ; maxtime::Int = 120, maxiter::Int = 2500000, tol::Tolerances = Tolerances(), verbosity = 0)
100+
find_SS_solver_parameters!(::Val{:SAMIN}, 𝒂::ℳ; maxtime::Real = 120, maxiter::Int = 2500000, tol::Tolerances = Tolerances(), verbosity = 0)
98101
99102
Find optimal steady state solver parameters using Optim's SAMIN algorithm.
100103
@@ -109,7 +112,7 @@ It uses Simulated Annealing with Metropolis acceptance (SAMIN) from Optim.jl.
109112
- `verbosity`: Verbosity level for output
110113
"""
111114
function find_SS_solver_parameters!(::Val{:SAMIN}, 𝓂::ℳ;
112-
maxtime::Int = 120,
115+
maxtime::Real = 120,
113116
maxiter::Int = 2500000,
114117
tol::Tolerances = Tolerances(),
115118
verbosity = 0)
@@ -131,10 +134,10 @@ function find_SS_solver_parameters!(::Val{:SAMIN}, 𝓂::ℳ;
131134

132135
par_inputs = solver_parameters(pars..., 1, 0.0, 2)
133136

134-
SS_and_pars, (solution_error, iters) = 𝓂.SS_solve_func(𝓂.parameter_values, 𝓂, tol, false, true, [par_inputs])
137+
SS_and_pars, (solution_error, iters) = 𝓂.functions.NSSS_solve(𝓂.parameter_values, 𝓂, tol, false, true, [par_inputs])
135138

136139
if solution_error < tol.NSSS_acceptance_tol
137-
push!(𝓂.solver_parameters, par_inputs)
140+
push!(MacroModelling.DEFAULT_SOLVER_PARAMETERS, par_inputs)
138141
return true
139142
else
140143
return false

0 commit comments

Comments
 (0)