Following the changes developed in https://github.com/JuliaOpt/JuMP.jl/pull/2090.
There will be a need in some downstream packages to combine solvers with their solver-specific parameters before attaching them to JuMP models. Notable examples include,
Providing the tooling for such tasks in JuMP will help standardize how this is accomplished in downstream packages.
Based a discussion of @mlubin and @ccoffrin in JuMP-dev on Jan. 12/13 2020.
CC @jd-lara
I agree that a standardized way to achieve this would be helpful.
One thing to keep in mind is that there are two kind of parameters: one for which the parameters is given as a value (e.g. some_tolerance = 1e-6) and one for which the parameter is given as a constructor (e.g. sub_solver = Ipopt.Optimizer for a Meta-solver).
In the second case, it is important that the sub-solver is given as a constructor and not as an instantiated object as the same parameter list may be used to instantiate several model instances.
This confusion was one of the reason with_optimizer at the JuMP level didn't fit since Meta-solvers also needed this sort of mechanism.
Therefore, it make sense to have this kind of mechanism at the MOI level so that the interface to set the sub-solver of a Meta-solver is the same as the interface to set the JuMP solver, see https://github.com/JuliaOpt/MathOptInterface.jl/pull/387 for a related discussion.
The current solution is to give functions
function configured_meta_solver()
optimizer = MetaSolverName.Optimizer()
sub_optimizer = Ipopt.Optimizer()
MOI.set(sub_optimizer, MOI.RawParameter("some_solerance"), 1e-6)
MOI.set(optimizer, MOI.RawParameter("sub_solver"), sub_optimizer)
end
model = Model(configured_meta_solver)
The advantage of this approach is that it's transparent to the user what happens while with with_optimizer, it was complicated to understand that with_optimizer(MetaSolverName.Optimizer, sub_solver = Ipopt.Optimizer(some_tolerance = 1e-6))
was not working as the sub_solver would be shared between different instances.
One solution would be to define RawParameters in MOI:
struct RawParameters
params::Vector{Pair}
end
or
struct RawParameters{P<:Tuple{<:Pair}}
params::P
end
and have:
optimizer = MetaSolverName.Optimizer
ipopt_params = MOI.RawParameters("some_tolerance" => 1e-6)
params = MOI.RawParameters(
"sub_solver" => Ipopt.Optimizer,
"sub_solver_params" => ipopt_params
)
model = Model(optimizer)
set_parameters(model, params)
It might be nice if JuMP's set_parameter was specialized for MOI.ModelLike, so that users didn't need to interact with MOI.RawParameter.
function configured_meta_solver()
optimizer = MetaSolverName.Optimizer()
sub_optimizer = Ipopt.Optimizer()
set_parameter(sub_optimizer, "some_solerance", 1e-6)
set_parameter(optimizer, "sub_solver", sub_optimizer)
end
model = Model(configured_meta_solver)
Re @blegat
This confusion was one of the reason with_optimizer at the JuMP level didn't fit since Meta-solvers also needed this sort of mechanism.
In our meta-solvers we resolved the issue like this,
juniper_solver = JuMP.with_optimizer(Juniper.Optimizer,
nl_solver=JuMP.with_optimizer(Ipopt.Optimizer, tol=1e-4, print_level=0),
mip_solver=JuMP.with_optimizer(Cbc.Optimizer, logLevel=0)
log_levels=[])
We still have the issue with parameter modification of a differed function call, but otherwise seemed to work well. So at first glance I don't think the tooling needs to distinguish between two classes of solver parameters, as long as the tooling can be applied recursively as you suggest the RawParameters example.
In your example how would set_parameters(model, params) know that "sub_solver_params" should be applied to the value of "sub_solver"? Keep in mind cases where a meta-solver will require more than one sub-solver, as is the case in Pavito, Juniper.
I would propose something along the lines of,
struct ParametrizedOptimizer
optimizer::MOI.ModelLike (correct type?)
params::Vector{Pair{String,<:Any}}
end
And a user facing helper function along these lines,
juniper_solver = parameterize(Juniper.Optimizer,[
"nl_solver" => parameterize(Ipopt.Optimizer,["tol"=>1e-4, "print_level"=>0]),
"mip_solver" => parameterize(Cbc.Optimizer,["logLevel"=>0]),
"log_levels" = []
])
This ParametrizedOptimizer would work similarly to how with_optimizer works now, but because the returned value is a struct and not a differed function call it be analyzed and mutated by the meta-solver or downstream modeling package.
Ping, @wikunia, @jd-lara, @kaarthiksundar for your two cents.
If this doesn't have any internal issues that I'm not aware of, I think the last version by @ccoffrin is the easiest to write/read for the user of these solvers.
Additionally I'm still a bit confused by set_parameters( needs the model and not the optimizer as the first parameter. (Also Mentioned by @jd-lara in the Gitter chat)
We seem to have a common point of confusion: as of JuMP 0.19, there are no solver objects.
Models _are_ optimizers. In particular
Ipopt.Optimizer()
is a _model_; you can add variables and constraints to it. It isn't some abstract "solver" entity.
In particular
Ipopt.Optimizer()is a model; you can add variables and constraints to it. It isn't some abstract "solver" entity.
Using JuMP v0.20 and Ipopt v0.6
using JuMP; using Ipopt
m = Ipopt.Optimizer()
@variable(m, x >= 0)
ERROR: Expected m to be a JuMP model, but it has type Ipopt.Optimizer
@odow, please elaborate.
It is at least the view on the MOI layer. There everything gets attached to the optimizer:
m = Ipopt.Optimizer()
x = MOI.add_variable(m)
@odow, please elaborate.
I meant that there is no longer IpoptSolver and IpoptModel. There is just Ipopt.Optimizer. So talking about the (JuMP) model and (Ipopt) optimizer separately doesn't make sense.
The old IpoptSolver corresponds to your ParameterizedOptimizer struct.
I think it is great that there is now only one concept of Foo.Optimizer, rather than FooSolver and FooModel. The heart of this discussion is that there still seems to be a use case/need for something like a ParameterizedOptimizer at the JuMP level for building meta-solvers and also packages that construct JuMP models without making any assumptions about the solvers that a user might like to use.
Regarding this point,
So talking about the (JuMP) model and (Ipopt) optimizer separately doesn't make sense.
In practice, if one does not provide a solver at the model creation time, the CachingOptimizer plays the role that the old JuMP model did. For example,
m = Model()
build_model(m)
optimize!(m, with_optimizer(Foo.Optimizer, args...))
In such cases, I do believe it is resonable to talk about the JuMP _model_ and the Ipopt _model_ as independent entities, at least until the call of optimize!.
Reading between the lines of this discussion, I think your intended use case as of JuMP-MOI is now,
m = Model(with_optimizer(Foo.Optimizer, args...))
build_model(m)
optimize!(m)
I can imagine that with some updates the "packages that construct JuMP models" use case could be adapted to this type of structure. However, I don't see a clear solution for the meta-solver case, which will want to make many instances of a user specified optimizer and connect them to different JuMP models in an automated way.
I follow @odow's comment about the fact that there isn't a distinction between the solver object and the JuMP object. My understanding is that this was the big change in JuMP 0.18 to 0.19 and newer versions. In fact, this concept is more evident when working in direct_mode. However, as a practical matter, one of the big advantages of JuMP when developing packages that build models is the use of the cache to store the model and then write it into the solver model. (this is pointed out by @ccoffrin in the previous post)
This feature allows having a model that is agnostic to the solver and its configurations in memory and then instantiating it according to the modelers' preferred configuration.
I don't follow why it is not possible when using the caching_optimizer to assign the solver model at the solve time and keep a functionality like the one we currently have.
Reading between the lines of this discussion, I think your intended use case as of JuMP-MOI is now
No. Sorry for being unclear.
PowerModels.jl et al should do:
model = Model()
build_model(model)
set_optimizer(model, Foo.Optimizer)
set_parameters(model, ...)
optimize!(model)
set_optimizer(model, Bar.Optimizer)
set_parameters(model, ...)
optimize!(model)
All that has changed is
optimize!(model, with_optimizer(Bar.Optimizer; kwargs...))
has become
set_optimizer(model, Bar.Optimizer)
set_parameters(model, kwargs...) # Excuse the kwargs... syntax. It really needs strings but you get the gist.
optimize!(model)
So for the price of two extra lines, we've made the passing parameters to the solver/model distinction clearer.
Agreed there are only a few extra lines at the JuMP level, but this will result in more arguments required at the top level of PowerModels and similar packages, becouse the solver and parameters will be separate entities. This gist highlights shows the changes over time and the set_parameters proposed change,
https://gist.github.com/ccoffrin/cf17514db200b3e4f597aec3af4380d1
If this goes live, I will most likely make a tool to bundle them back together, and so will @jd-lara. So it seems like having a solution for that usecase at the JuMP level would support standardization of the tooling across packages.
Given that solvers and their parameters are logically grouped and will often be passed around together, it makes sense to combine them into a struct for code tidiness.
I don't see a reason to restrict to only RawParameter. It could also be useful to set Silent or other solver-independent options.
I'd propose something like the following to be defined in MOI:
struct UninstantiatedOptimizer
optimizer # Function that takes zero arguments and returns a new optimizer.
params::Vector{Pair{AbstractOptimizerAttribute,<:Any}}
end
We can have constructors that take String => Any pairs and convert them to RawParameter(string) => Any pairs for convenience, e.g., XXX(Gurobi.Optimizer, MOI.Silent() => true, "Method" => 2) where XXX is some name that we have to bikeshed. JuMP would accept an UninstantiatedOptimizer in set_optimizer.
This is a big improvement from both MPB's solver constructors and with_optimizer because the parameters are stored transparently in the struct.
Let me know what you think of the implementation of the suggested solution: https://github.com/JuliaOpt/MathOptInterface.jl/pull/1008
We'll try to incorporate it in MOI v0.9.10 as it's the last blocker for JuMP v0.21
Most helpful comment
Given that solvers and their parameters are logically grouped and will often be passed around together, it makes sense to combine them into a struct for code tidiness.
I don't see a reason to restrict to only
RawParameter. It could also be useful to setSilentor other solver-independent options.I'd propose something like the following to be defined in MOI:
We can have constructors that take
String => Anypairs and convert them toRawParameter(string) => Anypairs for convenience, e.g.,XXX(Gurobi.Optimizer, MOI.Silent() => true, "Method" => 2)whereXXXis some name that we have to bikeshed. JuMP would accept anUninstantiatedOptimizerinset_optimizer.This is a big improvement from both MPB's solver constructors and
with_optimizerbecause the parameters are stored transparently in the struct.