Skip to content

Commit 7f3ff90

Browse files
Reapply "Merge pull request #67 from harmoniqs/benchmarks/directtrajopt-initial"
This reverts commit 83da076.
1 parent 09f7920 commit 7f3ff90

8 files changed

Lines changed: 387 additions & 11 deletions

File tree

.github/workflows/benchmark.yml

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
name: Benchmarks
2+
on:
3+
push:
4+
tags: ['v*']
5+
pull_request:
6+
paths:
7+
- 'src/**'
8+
- 'benchmark/**'
9+
- '.github/workflows/benchmark.yml'
10+
workflow_dispatch:
11+
12+
concurrency:
13+
group: ${{ github.workflow }}-${{ github.ref }}
14+
cancel-in-progress: ${{ startsWith(github.ref, 'refs/pull/') }}
15+
16+
jobs:
17+
benchmark:
18+
name: Benchmark suite
19+
runs-on: ubuntu-latest
20+
timeout-minutes: 60
21+
permissions:
22+
actions: write
23+
contents: read
24+
steps:
25+
- uses: actions/checkout@v6
26+
27+
- uses: julia-actions/setup-julia@v2
28+
with:
29+
version: '1.11'
30+
arch: x64
31+
32+
- uses: julia-actions/cache@v2
33+
34+
- name: Instantiate benchmark environment
35+
run: julia --project=benchmark -e 'using Pkg; Pkg.instantiate()'
36+
37+
- name: Run benchmarks
38+
run: |
39+
julia --project=benchmark -t auto -e '
40+
using TestItemRunner
41+
TestItemRunner.run_tests("benchmark/")
42+
'
43+
44+
- name: Upload benchmark artifacts
45+
if: always()
46+
uses: actions/upload-artifact@v4
47+
with:
48+
name: benchmark-${{ github.event.pull_request.number || github.ref_name }}-${{ github.sha }}
49+
path: benchmark/results/
50+
retention-days: 90

benchmark/.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
results/
2+
Manifest.toml

benchmark/BenchmarkUtils.jl

Lines changed: 0 additions & 1 deletion
This file was deleted.

benchmark/Project.toml

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
[deps]
2+
BenchmarkTools = "6e4b80f9-dd63-53aa-95a3-0cdb28fa8baf"
3+
Dates = "ade2ca70-3891-5945-98fb-dc099432e06a"
4+
DirectTrajOpt = "c823fa1f-8872-4af5-b810-2b9b72bbbf56"
5+
ExponentialAction = "e24c0720-ea99-47e8-929e-571b494574d3"
6+
ForwardDiff = "f6369f11-7733-5829-9624-2563aa707210"
7+
HarmoniqsBenchmarks = "f45d0b76-2d23-4568-9599-481e0da131db"
8+
LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e"
9+
MadNLP = "2621e9c9-9eb4-46b1-8089-e8c72242dfb6"
10+
MathOptInterface = "b8f27783-ece8-5eb3-8dc8-9495eed66fee"
11+
NamedTrajectories = "538bc3a1-5ab9-4fc3-b776-35ca1e893e08"
12+
Printf = "de0858da-6303-5e67-8744-51eddeeeb8d7"
13+
Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"
14+
SparseArrays = "2f01184e-e22b-5df5-ae63-d93ebab69eaf"
15+
TestItemRunner = "f8b46487-2199-4994-9208-9a1283c18c0a"
16+
TestItems = "1c621080-faea-4a02-84b6-bbd5e436b8fe"
17+
18+
[sources]
19+
DirectTrajOpt = {path = ".."}
20+
HarmoniqsBenchmarks = {url = "https://github.com/harmoniqs/HarmoniqsBenchmarks.jl"}

benchmark/README.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# DirectTrajOpt Benchmarks
2+
3+
Benchmark suite for DirectTrajOpt.jl comparing Ipopt and MadNLP solver performance.
4+
5+
## Running locally
6+
7+
```bash
8+
# From DirectTrajOpt.jl root
9+
julia --project=benchmark -e 'using Pkg; Pkg.instantiate()'
10+
11+
julia --project=benchmark -t auto -e '
12+
using TestItemRunner
13+
TestItemRunner.run_tests("benchmark/")
14+
'
15+
```
16+
17+
Artifacts are saved as JLD2 files in `benchmark/results/` (gitignored).
18+
19+
## Benchmark suites
20+
21+
- **Evaluator micro-benchmarks**`BenchmarkTools.@benchmark` timings for each MOI eval function (objective, gradient, constraint, jacobian, hessian_lagrangian) on bilinear N=51
22+
- **Ipopt vs MadNLP** — full solve comparison on bilinear N=51
23+
- **Memory scaling study** — N ∈ {25, 51, 101} × state_dim ∈ {4, 8, 16}
24+
25+
## Schema
26+
27+
Results use `BenchmarkResult` / `MicroBenchmarkResult` from [HarmoniqsBenchmarks.jl](https://github.com/harmoniqs/HarmoniqsBenchmarks.jl).
28+
29+
Load with:
30+
```julia
31+
using HarmoniqsBenchmarks
32+
results = load_results("benchmark/results/ipopt_vs_madnlp_N51_<sha>.jld2")
33+
```

benchmark/benchmarks.jl

Lines changed: 272 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,272 @@
1+
using TestItems
2+
3+
@testitem "Evaluator micro-benchmarks: bilinear N=51" begin
4+
using HarmoniqsBenchmarks, BenchmarkTools, DirectTrajOpt, NamedTrajectories
5+
using SparseArrays, ExponentialAction, MathOptInterface, Random, Dates, Printf
6+
const MOI = MathOptInterface
7+
8+
Random.seed!(42)
9+
N = 51;
10+
Δt = 0.1;
11+
u_bound = 0.1;
12+
ω = 0.1
13+
Gx = sparse(Float64[0 0 0 1; 0 0 1 0; 0 -1 0 0; -1 0 0 0])
14+
Gy = sparse(Float64[0 -1 0 0; 1 0 0 0; 0 0 0 -1; 0 0 1 0])
15+
Gz = sparse(Float64[0 0 1 0; 0 0 0 -1; -1 0 0 0; 0 1 0 0])
16+
G(u) = ω * Gz + u[1] * Gx + u[2] * Gy
17+
18+
traj = NamedTrajectory(
19+
(
20+
x = 2rand(4, N) .- 1,
21+
u = u_bound*(2rand(2, N) .- 1),
22+
du = randn(2, N),
23+
ddu = randn(2, N),
24+
Δt = fill(Δt, N),
25+
);
26+
controls = (:ddu, :Δt),
27+
timestep = :Δt,
28+
bounds = (u = u_bound, Δt = (0.01, 0.5)),
29+
initial = (x = [1.0, 0.0, 0.0, 0.0], u = zeros(2)),
30+
final = (u = zeros(2),),
31+
goal = (x = [0.0, 1.0, 0.0, 0.0],),
32+
)
33+
integrators = [
34+
BilinearIntegrator(G, :x, :u, traj),
35+
DerivativeIntegrator(:u, :du, traj),
36+
DerivativeIntegrator(:du, :ddu, traj),
37+
]
38+
J = QuadraticRegularizer(:u, traj, 1.0) + QuadraticRegularizer(:du, traj, 1.0)
39+
prob = DirectTrajOptProblem(traj, J, integrators)
40+
41+
evaluator, Z_vec = build_evaluator(prob)
42+
dims = evaluator_dims(evaluator)
43+
44+
g = zeros(dims.n_constraints)
45+
grad = zeros(dims.n_variables)
46+
H = zeros(dims.n_hessian_entries)
47+
Jac = zeros(dims.n_jacobian_entries)
48+
sigma = 1.0
49+
mu = ones(dims.n_constraints)
50+
51+
benchmarks = Dict{Symbol,EvalBenchmark}(
52+
:eval_objective =>
53+
trial_to_eval_benchmark(@benchmark(MOI.eval_objective($evaluator, $Z_vec))),
54+
:eval_gradient => trial_to_eval_benchmark(
55+
@benchmark(MOI.eval_objective_gradient($evaluator, $grad, $Z_vec))
56+
),
57+
:eval_constraint => trial_to_eval_benchmark(
58+
@benchmark(MOI.eval_constraint($evaluator, $g, $Z_vec))
59+
),
60+
:eval_jacobian => trial_to_eval_benchmark(
61+
@benchmark(MOI.eval_constraint_jacobian($evaluator, $Jac, $Z_vec))
62+
),
63+
:eval_hessian_lagrangian => trial_to_eval_benchmark(
64+
@benchmark(MOI.eval_hessian_lagrangian($evaluator, $H, $Z_vec, $sigma, $mu))
65+
),
66+
)
67+
68+
result = MicroBenchmarkResult(
69+
package = "DirectTrajOpt",
70+
package_version = "0.8.10",
71+
commit = (
72+
try
73+
String(strip(read(`git rev-parse --short HEAD`, String)))
74+
catch
75+
; "unknown"
76+
end
77+
),
78+
benchmark_name = "evaluator_micro_bilinear_N51",
79+
N = N,
80+
state_dim = 4,
81+
control_dim = 2,
82+
eval_benchmarks = benchmarks,
83+
julia_version = string(VERSION),
84+
timestamp = Dates.now(),
85+
runner = get(ENV, "BENCHMARK_RUNNER", "local"),
86+
n_threads = Threads.nthreads(),
87+
)
88+
89+
println("\n=== Evaluator Micro-benchmarks (bilinear N=$N) ===")
90+
for (name, eb) in sort(collect(result.eval_benchmarks), by = first)
91+
@printf(
92+
" %-25s median: %8.1f ns allocs: %d memory: %d bytes\n",
93+
name,
94+
eb.median_ns,
95+
eb.allocs,
96+
eb.memory_bytes
97+
)
98+
end
99+
100+
results_dir = joinpath(@__DIR__, "results")
101+
save_micro_results(results_dir, result.benchmark_name, result)
102+
println(" Saved to $results_dir/")
103+
end
104+
105+
@testitem "Ipopt vs MadNLP: bilinear N=51" begin
106+
using HarmoniqsBenchmarks, DirectTrajOpt, NamedTrajectories
107+
using SparseArrays, ExponentialAction, Random, Dates
108+
import MadNLP
109+
110+
const MadNLPSolverExt = [
111+
mod for mod in reverse(Base.loaded_modules_order) if Symbol(mod) == :MadNLPSolverExt
112+
][1]
113+
114+
function make_bilinear_problem(; seed = 42)
115+
Random.seed!(seed)
116+
N = 51;
117+
Δt = 0.1;
118+
u_bound = 0.1;
119+
ω = 0.1
120+
Gx = sparse(Float64[0 0 0 1; 0 0 1 0; 0 -1 0 0; -1 0 0 0])
121+
Gy = sparse(Float64[0 -1 0 0; 1 0 0 0; 0 0 0 -1; 0 0 1 0])
122+
Gz = sparse(Float64[0 0 1 0; 0 0 0 -1; -1 0 0 0; 0 1 0 0])
123+
G(u) = ω * Gz + u[1] * Gx + u[2] * Gy
124+
125+
traj = NamedTrajectory(
126+
(
127+
x = 2rand(4, N) .- 1,
128+
u = u_bound*(2rand(2, N) .- 1),
129+
du = randn(2, N),
130+
ddu = randn(2, N),
131+
Δt = fill(Δt, N),
132+
);
133+
controls = (:ddu, :Δt),
134+
timestep = :Δt,
135+
bounds = (u = u_bound, Δt = (0.01, 0.5)),
136+
initial = (x = [1.0, 0.0, 0.0, 0.0], u = zeros(2)),
137+
final = (u = zeros(2),),
138+
goal = (x = [0.0, 1.0, 0.0, 0.0],),
139+
)
140+
integrators = [
141+
BilinearIntegrator(G, :x, :u, traj),
142+
DerivativeIntegrator(:u, :du, traj),
143+
DerivativeIntegrator(:du, :ddu, traj),
144+
]
145+
J = QuadraticRegularizer(:u, traj, 1.0) + QuadraticRegularizer(:du, traj, 1.0)
146+
return DirectTrajOptProblem(traj, J, integrators)
147+
end
148+
149+
prob_ipopt = make_bilinear_problem()
150+
result_ipopt = benchmark_solve!(
151+
prob_ipopt,
152+
IpoptOptions(max_iter = 200, print_level = 0);
153+
benchmark_name = "bilinear_N51_ipopt",
154+
)
155+
156+
prob_madnlp = make_bilinear_problem()
157+
result_madnlp = benchmark_solve!(
158+
prob_madnlp,
159+
MadNLPSolverExt.MadNLPOptions(max_iter = 200, print_level = 1);
160+
benchmark_name = "bilinear_N51_madnlp",
161+
)
162+
163+
println("\n=== Ipopt vs MadNLP: bilinear N=51 ===")
164+
println(
165+
" Ipopt: $(round(result_ipopt.wall_time_s, digits=3))s, $(result_ipopt.total_allocations_bytes ÷ 1024) KB alloc",
166+
)
167+
println(
168+
" MadNLP: $(round(result_madnlp.wall_time_s, digits=3))s, $(result_madnlp.total_allocations_bytes ÷ 1024) KB alloc",
169+
)
170+
171+
results_dir = joinpath(@__DIR__, "results")
172+
save_results(results_dir, "ipopt_vs_madnlp_N51", [result_ipopt, result_madnlp])
173+
end
174+
175+
@testitem "Memory scaling: N and state_dim sweep" begin
176+
using HarmoniqsBenchmarks, DirectTrajOpt, NamedTrajectories
177+
using SparseArrays, ExponentialAction, Random, Dates, Printf
178+
import MadNLP
179+
180+
const MadNLPSolverExt = [
181+
mod for mod in reverse(Base.loaded_modules_order) if Symbol(mod) == :MadNLPSolverExt
182+
][1]
183+
184+
function make_scaled_problem(; N, state_dim, n_controls = 2, seed = 42)
185+
Random.seed!(seed)
186+
G_drift = sparse(randn(state_dim, state_dim))
187+
G_drives = [sparse(randn(state_dim, state_dim)) for _ = 1:n_controls]
188+
G(u) = G_drift + sum(u[i] * G_drives[i] for i = 1:n_controls)
189+
190+
x_init = zeros(state_dim);
191+
x_init[1] = 1.0
192+
x_goal = zeros(state_dim);
193+
x_goal[min(2, state_dim)] = 1.0
194+
195+
traj = NamedTrajectory(
196+
(
197+
x = randn(state_dim, N),
198+
u = 0.1*randn(n_controls, N),
199+
du = randn(n_controls, N),
200+
Δt = fill(0.1, N),
201+
);
202+
controls = (:du, :Δt),
203+
timestep = :Δt,
204+
bounds = (u = 1.0, Δt = (0.01, 0.5)),
205+
initial = (x = x_init, u = zeros(n_controls)),
206+
final = (u = zeros(n_controls),),
207+
goal = (x = x_goal,),
208+
)
209+
integrators =
210+
[BilinearIntegrator(G, :x, :u, traj), DerivativeIntegrator(:u, :du, traj)]
211+
J = QuadraticRegularizer(:u, traj, 1.0)
212+
return DirectTrajOptProblem(traj, J, integrators)
213+
end
214+
215+
N_values = [25, 51, 101]
216+
dim_values = [4, 8, 16]
217+
results = BenchmarkResult[]
218+
219+
println("\n=== Memory Scaling Study ===")
220+
@printf(
221+
" %5s | %5s | %12s | %12s | %12s | %12s\n",
222+
"N",
223+
"dim",
224+
"Ipopt (s)",
225+
"Ipopt (KB)",
226+
"MadNLP (s)",
227+
"MadNLP (KB)"
228+
)
229+
@printf(
230+
" %5s-+-%5s-+-%12s-+-%12s-+-%12s-+-%12s\n",
231+
"-"^5,
232+
"-"^5,
233+
"-"^12,
234+
"-"^12,
235+
"-"^12,
236+
"-"^12
237+
)
238+
239+
for N in N_values
240+
for dim in dim_values
241+
prob = make_scaled_problem(; N = N, state_dim = dim)
242+
r_ipopt = benchmark_solve!(
243+
prob,
244+
IpoptOptions(max_iter = 50, print_level = 0);
245+
benchmark_name = "scaling_N$(N)_d$(dim)_ipopt",
246+
)
247+
push!(results, r_ipopt)
248+
249+
prob = make_scaled_problem(; N = N, state_dim = dim)
250+
r_madnlp = benchmark_solve!(
251+
prob,
252+
MadNLPSolverExt.MadNLPOptions(max_iter = 50, print_level = 1);
253+
benchmark_name = "scaling_N$(N)_d$(dim)_madnlp",
254+
)
255+
push!(results, r_madnlp)
256+
257+
@printf(
258+
" %5d | %5d | %12.3f | %12d | %12.3f | %12d\n",
259+
N,
260+
dim,
261+
r_ipopt.wall_time_s,
262+
r_ipopt.total_allocations_bytes ÷ 1024,
263+
r_madnlp.wall_time_s,
264+
r_madnlp.total_allocations_bytes ÷ 1024
265+
)
266+
end
267+
end
268+
269+
results_dir = joinpath(@__DIR__, "results")
270+
save_results(results_dir, "memory_scaling", results)
271+
println("\n Saved $(length(results)) results to $results_dir/")
272+
end

0 commit comments

Comments
 (0)