Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[DOC] Add tutorial on warm-start #372

Merged
merged 1 commit into from
Nov 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions docs/make.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
92 changes: 92 additions & 0 deletions docs/src/tutorials/hs15.jl
Original file line number Diff line number Diff line change
@@ -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

170 changes: 170 additions & 0 deletions docs/src/tutorials/warmstart.md
Original file line number Diff line number Diff line change
@@ -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).

Loading