From a1dc3a604b68581d3a51bb20433444c38d6279f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Pacaud?= Date: Tue, 12 Nov 2024 16:13:12 +0100 Subject: [PATCH] [DOC] Add tutorial on warm-start (#372) --- docs/make.jl | 3 + docs/src/tutorials/hs15.jl | 92 +++++++++++++++++ docs/src/tutorials/warmstart.md | 170 ++++++++++++++++++++++++++++++++ 3 files changed, 265 insertions(+) create mode 100644 docs/src/tutorials/hs15.jl create mode 100644 docs/src/tutorials/warmstart.md diff --git a/docs/make.jl b/docs/make.jl index 3f10590d..c1f52922 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -17,6 +17,9 @@ makedocs( "Installation" => "installation.md", "Quickstart" => "quickstart.md", "Options" => "options.md", + "Tutorials" => [ + "Warm-start" => "tutorials/warmstart.md", + ], "Manual" => [ "IPM solver" => "man/solver.md", "KKT systems" => "man/kkt.md", diff --git a/docs/src/tutorials/hs15.jl b/docs/src/tutorials/hs15.jl new file mode 100644 index 00000000..02493aff --- /dev/null +++ b/docs/src/tutorials/hs15.jl @@ -0,0 +1,92 @@ +using NLPModels + +struct HS15Model{T} <: NLPModels.AbstractNLPModel{T,Vector{T}} + meta::NLPModels.NLPModelMeta{T, Vector{T}} + params::Vector{T} + counters::NLPModels.Counters +end + +function HS15Model(;T = Float64, x0=zeros(T,2), y0=zeros(T,2)) + return HS15Model( + NLPModels.NLPModelMeta( + 2, #nvar + ncon = 2, + nnzj = 4, + nnzh = 3, + x0 = x0, + y0 = y0, + lvar = T[-Inf, -Inf], + uvar = T[0.5, Inf], + lcon = T[1.0, 0.0], + ucon = T[Inf, Inf], + minimize = true + ), + T[100, 1], + NLPModels.Counters() + ) +end + +function NLPModels.obj(nlp::HS15Model, x::AbstractVector) + p1, p2 = nlp.params + return p1 * (x[2] - x[1]^2)^2 + (p2 - x[1])^2 +end + +function NLPModels.grad!(nlp::HS15Model{T}, x::AbstractVector, g::AbstractVector) where T + p1, p2 = nlp.params + z = x[2] - x[1]^2 + g[1] = -T(4) * p1 * z * x[1] - T(2) * (p2 - x[1]) + g[2] = T(2) * p1 * z + return g +end + +function NLPModels.cons!(nlp::HS15Model, x::AbstractVector, c::AbstractVector) + c[1] = x[1] * x[2] + c[2] = x[1] + x[2]^2 + return c +end + +function NLPModels.jac_structure!(nlp::HS15Model, I::AbstractVector{T}, J::AbstractVector{T}) where T + copyto!(I, [1, 1, 2, 2]) + copyto!(J, [1, 2, 1, 2]) + return I, J +end + +function NLPModels.jac_coord!(nlp::HS15Model{T}, x::AbstractVector, J::AbstractVector) where T + J[1] = x[2] # (1, 1) + J[2] = x[1] # (1, 2) + J[3] = T(1) # (2, 1) + J[4] = T(2)*x[2] # (2, 2) + return J +end + +function NLPModels.jprod!(nlp::HS15Model{T}, x::AbstractVector, v::AbstractVector, jv::AbstractVector) where T + jv[1] = x[2] * v[1] + x[1] * v[2] + jv[2] = v[1] + T(2) * x[2] * v[2] + return jv +end + +function NLPModels.jtprod!(nlp::HS15Model{T}, x::AbstractVector, v::AbstractVector, jv::AbstractVector) where T + jv[1] = x[2] * v[1] + v[2] + jv[2] = x[1] * v[1] + T(2) * x[2] * v[2] + return jv +end + +function NLPModels.hess_structure!(nlp::HS15Model, I::AbstractVector{T}, J::AbstractVector{T}) where T + copyto!(I, [1, 2, 2]) + copyto!(J, [1, 1, 2]) + return I, J +end + +function NLPModels.hess_coord!(nlp::HS15Model{T}, x, y, H::AbstractVector; obj_weight=T(1)) where T + p1, p2 = nlp.params + # Objective + H[1] = obj_weight * (-T(4) * p1 * x[2] + T(12) * p1 * x[1]^2 + T(2)) + H[2] = obj_weight * (-T(4) * p1 * x[1]) + H[3] = obj_weight * T(2) * p1 + # First constraint + H[2] += y[1] * T(1) + # Second constraint + H[3] += y[2] * T(2) + return H +end + diff --git a/docs/src/tutorials/warmstart.md b/docs/src/tutorials/warmstart.md new file mode 100644 index 00000000..ffc06bb9 --- /dev/null +++ b/docs/src/tutorials/warmstart.md @@ -0,0 +1,170 @@ +# Warmstarting MadNLP + +```@meta +CurrentModule = MadNLP +``` +```@setup warmstart +using NLPModels +using MadNLP +include("hs15.jl") + +``` + +We use a parameterized version of the instance HS15 +used in the [introduction](../quickstart.md). This updated +version of `HS15Model` stores the parameters of the model in an +attribute `nlp.params`: +```@example warmstart + +nlp = HS15Model() +println(nlp.params) +``` +By default the parameters are set to `[100.0, 1.0]`. +In a first solve, we find a solution associated to these parameters. +We want to warmstart MadNLP from the solution found in the first solve, +after a small update in the problem's parameters. + +!!! info + It is known that the interior-point method has a poor + support of warmstarting, on the contrary to active-set methods. + However, if the parameter changes remain reasonable and do not lead + to significant changes in the active set, warmstarting the + interior-point algorithm can significantly reduces the total number of barrier iterations + in the second solve. + +!!! warning + The warm-start described in this tutorial remains basic. + Its main application is updating the solution of a parametric + problem after an update in the parameters. **The warm-start + always assumes that the structure of the problem remains the same between + two consecutive solves**. + MadNLP cannot be warm-started if variables or constraints + are added to the problem. + +## Naive solution: starting from the previous solution +By default, MadNLP starts its interior-point algorithm +at the primal variable stored in `nlp.meta.x0`. We can +access this attribute using the function `get_x0`: +```@example warmstart +x0 = NLPModels.get_x0(nlp) + +``` +Here, we observe that the initial solution is `[0, 0]`. + +We solve the problem using the function [`madnlp`](@ref): +```@example warmstart +results = madnlp(nlp) +nothing +``` +MadNLP converges in 19 barrier iterations. The solution is: +```@example warmstart +println("Objective: ", results.objective) +println("Solution: ", results.solution) +``` + +### Solution 1: updating the starting solution +We have found a solution to the problem. Now, what happens if we update +the parameters inside `nlp`? +```@example warmstart +nlp.params .= [101.0, 1.1] +``` +As MadNLP starts the algorithm at `nlp.meta.x0`, we pass +the previous solution to the initial vector: +```@example warmstart +copyto!(NLPModels.get_x0(nlp), results.solution) +``` +Solving the problem again with MadNLP, we observe that MadNLP converges +in only 6 iterations: +```@example warmstart +results_new = madnlp(nlp) +nothing + +``` +By decreasing the initial barrier parameter, we can reduce the total number +of iterations to 5: +```@example warmstart +results_new = madnlp(nlp; mu_init=1e-7) +nothing + +``` + +The final solution is slightly different from the previous one, as we have +updated the parameters inside the model `nlp`: +```@example warmstart +results_new.solution + +``` + +!!! info + Similarly as with the primal solution, we can pass the initial dual solution to MadNLP + using the function `get_y0`. We can overwrite the value of `y0` in `nlp` using: + ``` + copyto!(NLPModels.get_y0(nlp), results.multipliers) + ``` + In our particular example, setting the dual multipliers has only a minor influence + on the convergence of the algorithm. + + +## Advanced solution: keeping the solver in memory + +The previous solution works, but is wasteful in resource: each time we call +the function [`madnlp`](@ref) we create a new instance of [`MadNLPSolver`](@ref), +leading to a significant number of memory allocations. A workaround is to keep +the solver in memory to have more fine-grained control on the warm-start. + +We start by creating a new model `nlp` and we instantiate a new instance +of [`MadNLPSolver`](@ref) attached to this model: +```@example warmstart +nlp = HS15Model() +solver = MadNLP.MadNLPSolver(nlp) +``` +Note that +```@example warmstart +nlp === solver.nlp +``` +Hence, updating the parameter values in `nlp` will automatically update the +parameters in the solver. + +We solve the problem using the function [`solve!`](@ref): +```@example warmstart +results = MadNLP.solve!(solver) +``` +Before warmstarting MadNLP, we proceed as before and update the parameters +and the primal solution in `nlp`: +```@example warmstart +nlp.params .= [101.0, 1.1] +copyto!(NLPModels.get_x0(nlp), results.solution) +``` +MadNLP stores in memory the dual solutions computed during the first solve. +One can access to the (scaled) multipliers as +```@example warmstart +solver.y +``` +and to the multipliers of the bound constraints with +```@example warmstart +[solver.zl.values solver.zu.values] +``` + +!!! warning + If we call the function [`solve!`](@ref) a second-time, + MadNLP will use the following rule: + - The initial primal solution is copied from `NLPModels.get_x0(nlp)` + - The initial dual solution is directly taken from the values specified + in `solver.y`, `solver.zl` and `solver.zu`. + (MadNLP is not using the values stored in `nlp.meta.y0` in the second solve). + +As before, it is advised to decrease the initial barrier parameter: +if the initial point is close enough to the solution, this reduces drastically +the total number of iterations. +We solve the problem again using: +```@example warmstart +MadNLP.solve!(solver; mu_init=1e-7) +nothing +``` +Three observations are in order: +- The iteration count starts directly from the previous count (as stored in `solver.cnt.k`). +- MadNLP converges in only 4 iterations. +- The factorization stored in `solver` is directly re-used, leading to significant savings. + As a consequence, the warm-start does not work if the structure of the problem changes between + the first and the second solve (e.g, if variables or constraints are added to the constraints). +