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

Add MOI.attribute_value_type #1283

Merged
merged 16 commits into from
Aug 8, 2021
Merged

Add MOI.attribute_value_type #1283

merged 16 commits into from
Aug 8, 2021

Conversation

odow
Copy link
Member

@odow odow commented Mar 11, 2021

Originally proposed all the way back in #31.

Problem

Lots of MOI.get fail to infer. For example, getting the termination status
of a caching optimizer is Any instead of TerminationStatusCode.

julia> b = backend(model)
MOIU.CachingOptimizer{MOI.AbstractOptimizer, MOIU.UniversalFallback{MOIU.Model{Float64}}}
in state ATTACHED_OPTIMIZER
in mode AUTOMATIC
with model cache MOIU.UniversalFallback{MOIU.Model{Float64}}
  with 1 optimizer attribute
  fallback for MOIU.Model{Float64}
with optimizer MOIB.LazyBridgeOptimizer{GLPK.Optimizer}
  with 0 variable bridges
  with 0 constraint bridges
  with 0 objective bridges
  with inner model A GLPK model

julia> @code_warntype MOI.get(b, MOI.TerminationStatus())
Variables
  #self#::Core.Const(MathOptInterface.get)
  model::MathOptInterface.Utilities.CachingOptimizer{MathOptInterface.AbstractOptimizer, MathOptInterface.Utilities.UniversalFallback{MathOptInterface.Utilities.Model{Float64}}}
  attr::Core.Const(MathOptInterface.TerminationStatus())

Body::Any
1%1  = MathOptInterface.is_set_by_optimize::Core.Const(MathOptInterface.is_set_by_optimize)
│   %2  = (%1)(attr)::Core.Const(true)
│         %2%4  = MathOptInterface.Utilities.state(model)::MathOptInterface.Utilities.CachingOptimizerState%5  = (%4 == MathOptInterface.Utilities.NO_OPTIMIZER)::Bool
└──       goto #4 if not %5
2%7  = MathOptInterface.TerminationStatus::Core.Const(MathOptInterface.TerminationStatus)
│   %8  = (%7)()::Core.Const(MathOptInterface.TerminationStatus())
│   %9  = (attr == %8)::Core.Const(true)
│         %9%11 = MathOptInterface.OPTIMIZE_NOT_CALLED::Core.Const(MathOptInterface.OPTIMIZE_NOT_CALLED)
└──       return %11
3 ─       Core.Const(:(MathOptInterface.PrimalStatus))
│         Core.Const(:((%13)()))
│         Core.Const(:(attr == %14))
│         Core.Const(:(%15))
│         Core.Const(:(MathOptInterface.NO_SOLUTION))
│         Core.Const(:(return %17))
│         Core.Const(:(MathOptInterface.DualStatus))
│         Core.Const(:((%19)()))
│         Core.Const(:(attr == %20))
│         Core.Const(:(%21))
│         Core.Const(:(MathOptInterface.NO_SOLUTION))
│         Core.Const(:(return %23))
│         Core.Const(:(Base.string("Cannot query ", attr, " from caching optimizer because no")))
│         Core.Const(:(%25 * " optimizer is attached."))
└──       Core.Const(:(MathOptInterface.Utilities.error(%26)))
4%28 = Base.getproperty(model, :optimizer_to_model_map)::MathOptInterface.Utilities.IndexMap%29 = MathOptInterface.get::Core.Const(MathOptInterface.get)
│   %30 = Base.getproperty(model, :optimizer)::Union{Nothing, MathOptInterface.AbstractOptimizer}%31 = (%29)(%30, attr)::Any%32 = MathOptInterface.Utilities.map_indices(%28, %31)::Any
└──       return %32
5 ─       Core.Const(:(MathOptInterface.get))
│         Core.Const(:(Base.getproperty(model, :model_cache)))
│         Core.Const(:((%34)(%35, attr)))
└──       Core.Const(:(return %36))

Whereas with this PR we have

julia> @code_warntype MOI.get(b, MOI.TerminationStatus())
Variables
  #self#::Core.Const(MathOptInterface.get)
  model::MathOptInterface.Utilities.CachingOptimizer{MathOptInterface.AbstractOptimizer, MathOptInterface.Utilities.UniversalFallback{MathOptInterface.Utilities.Model{Float64}}}
  attr::Core.Const(MathOptInterface.TerminationStatus())

Body::MathOptInterface.TerminationStatusCode
1%1  = MathOptInterface.is_set_by_optimize::Core.Const(MathOptInterface.is_set_by_optimize)
│   %2  = (%1)(attr)::Core.Const(true)
│         %2%4  = MathOptInterface.Utilities.state(model)::MathOptInterface.Utilities.CachingOptimizerState%5  = (%4 == MathOptInterface.Utilities.NO_OPTIMIZER)::Bool
└──       goto #4 if not %5
2%7  = MathOptInterface.TerminationStatus::Core.Const(MathOptInterface.TerminationStatus)
│   %8  = (%7)()::Core.Const(MathOptInterface.TerminationStatus())
│   %9  = (attr == %8)::Core.Const(true)
│         %9%11 = MathOptInterface.OPTIMIZE_NOT_CALLED::Core.Const(MathOptInterface.OPTIMIZE_NOT_CALLED)
└──       return %11
3 ─       Core.Const(:(MathOptInterface.PrimalStatus))
│         Core.Const(:((%13)()))
│         Core.Const(:(attr == %14))
│         Core.Const(:(%15))
│         Core.Const(:(MathOptInterface.NO_SOLUTION))
│         Core.Const(:(return %17))
│         Core.Const(:(MathOptInterface.DualStatus))
│         Core.Const(:((%19)()))
│         Core.Const(:(attr == %20))
│         Core.Const(:(%21))
│         Core.Const(:(MathOptInterface.NO_SOLUTION))
│         Core.Const(:(return %23))
│         Core.Const(:(Base.string("Cannot query ", attr, " from caching optimizer because no")))
│         Core.Const(:(%25 * " optimizer is attached."))
└──       Core.Const(:(MathOptInterface.Utilities.error(%26)))
4%28 = Base.getproperty(model, :optimizer_to_model_map)::MathOptInterface.Utilities.IndexMap%29 = MathOptInterface.get::Core.Const(MathOptInterface.get)
│   %30 = Base.getproperty(model, :optimizer)::Union{Nothing, MathOptInterface.AbstractOptimizer}%31 = (%29)(%30, attr)::Any%32 = MathOptInterface.return_type::Core.Const(MathOptInterface.return_type)
│   %33 = (%32)(attr)::Core.Const(MathOptInterface.TerminationStatusCode)
│   %34 = Core.typeassert(%31, %33)::MathOptInterface.TerminationStatusCode%35 = MathOptInterface.Utilities.map_indices(%28, %34)::MathOptInterface.TerminationStatusCode
└──       return %35
5 ─       Core.Const(:(MathOptInterface.get))
│         Core.Const(:(Base.getproperty(model, :model_cache)))
│         Core.Const(:((%37)(%38, attr)))
└──       Core.Const(:(return %39))

Unfortunately, attributes like MOI.VariablePrimal fail to infer, because we don't know if they are Float64 or not.

Alternatives

If we won't want to add MOI.return_type, I can move it into CachingOptimizer.

@odow odow added Submodule: Utilities About the Utilities submodule Type: Performance labels Mar 11, 2021
Copy link
Contributor

@joehuchette joehuchette left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this a general problem that a number of optimizers run into? Or is it mostly just a problem with the CachingOptimizer, because of it's extra layer or redirection?

src/attributes.jl Outdated Show resolved Hide resolved
@odow
Copy link
Member Author

odow commented Mar 11, 2021

This is mainly CachingOptimizer with this problem, because it has a backend of Union{Nothing,AbstractOptimizer} which needs dynamic dispatch.

But more widely, standardizing the return types of MOI.xxx functions would be a good thing. There are lots of things like
#1282 which cause issues if the return types are not explicitly stated.

@blegat
Copy link
Member

blegat commented Mar 11, 2021

Alternatively, we could avoid having a non-concrete type for the optimizer as suggested in jump-dev/JuMP.jl#2520.
We have a non-concrete type for the JuMP backend so we need to annotate all JuMP function to avoid these issues.
Now we need to workaround the same issues at the level of the CachingOptimizer.
Only one non-concrete type is needed to be able to implement set_optimizer so why have two ?

@mlubin
Copy link
Member

mlubin commented Mar 11, 2021

we need to annotate all JuMP function to avoid these issues

If these types are properly defined in MOI, then we can add unit tests that solvers return the correct types. This prevents surprises when using the solver from JuMP.

In terms of bikeshedding, I'd suggest attribute_value_type instead of return_type. "Return type" applies to functions, and attributes aren't functions.

@blegat
Copy link
Member

blegat commented Mar 11, 2021

Independently of the alternative, this could indeed be useful, attribute_value_type seems more appropriate indeed.
We could have

attribute_value_type(::ModelLike, attr::AnyAttribute) = attribute_value_type(attr)
attribute_value_type(::ModelLike, attr::Union{MOI.VariablePrimal, MOI.VariablePrimalStart, ...}) = MOI.get(model, CoefficientType())

If there is only one method of attribute_value_type(::ModelLike, attr::AnyAttribute) for a specific attribute, Julia might be able to get around not knowing the model type but I'm not sure, we need to check.

@joehuchette
Copy link
Contributor

I'm a +1 to standardizing the "attribute value types" of MOI attributes, and having MOIT unit tests for this expected behavior.

@odow odow force-pushed the od/rettype branch 2 times, most recently from d639eea to dc25770 Compare March 17, 2021 18:27
function $(f)(model::MOI.ModelLike, config::TestConfig)
MOI.empty!(model)
MOI.optimize!(model)
attribute = MOI.$(attr)()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe skip the test is supports is false

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For now I've made these tests opt-in individually (They aren't in the unittest testset). We probably want to wait a few releases for solvers to adapt before enforcing it and breaking every solver.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What are the changes needed in the solver ? I assume they already have objects of the correct type returned, don't they ?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They will return the correct object, but they may not infer correctly.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now that we're breaking things, I added the tests by default. It would be good for solvers to fix them.

src/attributes.jl Outdated Show resolved Hide resolved
@odow
Copy link
Member Author

odow commented Apr 19, 2021

@blegat how is this. I changed to Int64.

@odow
Copy link
Member Author

odow commented Apr 30, 2021

@blegat bump.

src/Utilities/model.jl Outdated Show resolved Hide resolved
@odow odow added this to the v0.10 milestone May 11, 2021
@odow odow changed the title Add MOI.return_type Add MOI.attribute_value_type May 11, 2021
MOI.get(model, attribute)
catch err
if err isa ArgumentError
return # Solver does not support accessing the attribute.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@test !MOI.supports(...) ?

@odow odow requested a review from blegat August 5, 2021 23:20
@odow odow merged commit 3093a7c into master Aug 8, 2021
@odow odow deleted the od/rettype branch August 8, 2021 23:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Development

Successfully merging this pull request may close these issues.

5 participants