-
Notifications
You must be signed in to change notification settings - Fork 16
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[DOC] Add tutorial on warm-start (#372)
- Loading branch information
Showing
3 changed files
with
265 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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). | ||
|