Skip to content

Improvements to MOSEK Direct interface and a new persistent interface #1686

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

Merged
merged 46 commits into from
Nov 23, 2020

Conversation

Utkarsh-Detha
Copy link
Contributor

@Utkarsh-Detha Utkarsh-Detha commented Oct 29, 2020

Fixes #1513

Motivation:

While investigating why the direct interface was slow to pass the problem to the solver (see google group discussion), @aszekMosek found that _add_var and _add_constraint methods pass one variable/constraint to the MOSEK task per call, thereby making it a bit slow. Furthermore, @michaelbynum stated that this was a design choice made to accommodate persistent interfaces. This pull request is therefore motivated by a need to add some performance improvements to the MOSEK direct interface and making a new persistent interface for MOSEK.

Major changes proposed in this PR:

  1. add_vars and add_constraints methods are included. These take a list of variables/constraints (single data objects), extract the necessary information to be passed to the MOSEK task object, and make one call to the appropriate MOSEK Optimizer API methods. Naturally, _add_block has been overridden to use these methods.
  2. _add_var and _add_constraint methods are now simply calls to the add_vars and add_constraints methods.
  3. A persistent interface defined as the class MOSEKPersistent and registered in the SolverFactory as "mosek_persistent" is now available. Methods are available to remove variable(s) and constraint(s), modify variable(s), add a column etc.
  4. Included tests for the persistent interface in test_MOSEKPersistent.py.

Minor changes proposed in the PR:

  1. MosekDirect class is now called MOSEKDirect and the direct interface is registered in the SolverFactory as "mosek_direct".
  2. apply_solver method in the direct interface now prints a termination code in case of a significant failure.
  3. _get_cone_data method always sets the continuity relaxation to True.
  4. remove_vars, remove_constraints and update_vars methods in the persistent interface will accept multiple singleton var/constraint objects, passed as individual arguments. Analogous methods that accept a single object are also available.
  5. test_mosek.py is now called test_MOSEKDirect.py

Benchmarks (direct interface) [updated]

A performance improvement of about ~30 % was observed across the following benchmarks:

  1. (Small) Issue With cplex, solver_io='python' is much slower than solver_io='nl' or default #51: Averaging over 10 runs, the current master branch took 11.96 seconds, while this PR took 6.79 seconds.
  2. (Medium) A model to calculate Wasserstein distance, using n=2 images (see the linear model provided here): the current master branch took 75.14 seconds while the PR took 55.1 seconds. Solver time in both cases was around 1.5 seconds.
  3. (Large) A model to calculate Wasserstein distance, using n=20 images (see the linear model provided here): the current master branch took about 803 seconds while this PR took 581 seconds. Solver time in both cases was about 40 seconds.

NOTE: Suggestions made by @jsiirola further improved the performance of the interface, and the benchmark results provided above were updated accordingly.

I ran the benchmarks on an Intel i7 10875h, with 32 GB RAM.

Legal Acknowledgement

By contributing to this software project, I have read the contribution guide and agree to the following terms and conditions for my contribution:

  1. I agree my contributions are submitted under the BSD license.
  2. I represent I am authorized to make the contributions and grant the license. If my employer has rights to intellectual property that includes these contributions, I represent that I have received permission to make contributions and grant the required license on behalf of that employer.

1.) The direct interface is now registered as 'mosek_direct' in the SolverFactory
    (formerly called just 'mosek').
2.) License check is performed in the apply_solver method, instead of having a dedicated method
    for the purpose.
3.) 'apply_solver' method now records a termination code, which also provides a return code
    if mosek has a significant failure.
4.) (DISCUSS) '_get_cone_data' always sets the continuity relaxation to True.
5.) _get_expr_from_pyomo_repn method now has list comprehensions and map functions
    instead of the for loops that were used formerly. Time-it to see if it speeds things up
    significantly.
@Utkarsh-Detha
Copy link
Contributor Author

Could someone (maybe @jsiirola) give me a hint as to why the Jenkins CI /inspection has not started yet? I am still new to the world of CI/CD and any help would be greatly appreciated.

@blnicho
Copy link
Member

blnicho commented Nov 3, 2020

@Utkarsh-Detha someone on the core development team needs to do a preinspection on the PR before the Jenkins tests are launched. Our weekly developers meeting is on Tuesday so you should see some movement on this PR later today.

@codecov
Copy link

codecov bot commented Nov 3, 2020

Codecov Report

Merging #1686 (9cc429e) into master (d0635bf) will decrease coverage by 0.25%.
The diff coverage is 17.27%.

Impacted file tree graph

@@            Coverage Diff             @@
##           master    #1686      +/-   ##
==========================================
- Coverage   72.99%   72.74%   -0.26%     
==========================================
  Files         630      631       +1     
  Lines       90948    90131     -817     
==========================================
- Hits        66388    65565     -823     
- Misses      24560    24566       +6     
Impacted Files Coverage Δ
pyomo/solvers/plugins/solvers/mosek_direct.py 11.76% <9.86%> (-0.10%) ⬇️
pyomo/solvers/plugins/solvers/mosek_persistent.py 30.09% <30.09%> (ø)
pyomo/opt/base/solvers.py 86.99% <100.00%> (+0.02%) ⬆️
pyomo/solvers/plugins/solvers/__init__.py 100.00% <100.00%> (ø)
pyomo/common/timing.py 82.60% <0.00%> (-10.65%) ⬇️
pyomo/core/base/block.py 89.30% <0.00%> (-1.15%) ⬇️
pyomo/pysp/util/config.py 65.16% <0.00%> (-0.05%) ⬇️
pyomo/mpec/complementarity.py 95.65% <0.00%> (-0.03%) ⬇️
pyomo/contrib/gdpopt/config_options.py 100.00% <0.00%> (ø)
pyomo/contrib/interior_point/interface.py 82.87% <0.00%> (ø)
... and 7 more

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update d0635bf...6f863e3. Read the comment docs.

@Utkarsh-Detha
Copy link
Contributor Author

Thanks for your response, @blnicho ! One more thing: I see that the codecov/patch test has failed and the culprit seems to be the foqus_graph.py file (I say that simply because the delta is negative only in that file's case). However, I have not tampered with this file, and therefore do not understand how this could happen. Any advice would be much appreciated.

@jsiirola
Copy link
Member

jsiirola commented Nov 5, 2020

@Utkarsh-Detha, You always have to take codecov results with a grain of salt: there are frequently inexplicable changes in coverage that are unrelated to the PR. That said, codecov/patch fails when the PR patch (the set of changed lines) is covered at a level that is less than the overall project. In this case, that is because we do not (yet) have Mosek available on any of our testing platforms (as it requires a license, we cannot set it up on the public platforms and we have been having challenges getting approvals to provide the necessary information to obtain a license for our nonpublic testing platform).

@aszekMosek
Copy link
Contributor

Regarding testing you can use http://solve.mosek.com if you set it up appropriately: locally you would only need the interface, which is not licensed, and you would somehow have to hack the Pyomo interface to enable remote optimization.

See for example what happens in Julia:

https://github.com/MOSEK/Mosek.jl/blob/112e0d7d7f8bb753360abc3c3693f9dccd5c6fb7/test/apitest.jl#L43

@Utkarsh-Detha
Copy link
Contributor Author

@jsiirola am I right in thinking that the PR will not be considered for merging as it fails the codecov/patch test? If so, I can close this PR and open another one with @aszekMosek's suggestion incorporated in it. How would you suggest we proceed?

@jsiirola
Copy link
Member

jsiirola commented Nov 6, 2020

No - I do not think this PR needs to be closed, although an extension that supports the remote interface that @aszekMosek highlights would be great.

(I am most of the way through my review, so hopefully I can get that finished and posted soon)

Copy link
Member

@jsiirola jsiirola left a comment

Choose a reason for hiding this comment

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

This is a good start. Several questions below.

Runs a check for a valid Mosek license. Returns False
if Mosek fails to run on a trivial test case.
Runs a check for a valid MOSEK license. Returns False if MOSEK fails
to run on a trivial test case.
Copy link
Member

Choose a reason for hiding this comment

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

Question (unrelated to this PR), is it better/faster/more efficient to use checkoutlicense()/checkinlicense(), or the call to optimize() below?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I never thought about this before, but it is in fact more efficient to use the checkoutlicense methods! I ran a loop checking license availability 10**4 times, and found that if a license is available, then optimze() method will take about 69 seconds (84% CPU usage) and the checkoutlicense() method method will take about 58 seconds (with 94% CPU usage). I will make the necessary changes in the file.

@Utkarsh-Detha
Copy link
Contributor Author

Ah, I am sorry for the inconvenience. I will do as you suggested.

@Utkarsh-Detha
Copy link
Contributor Author

@jsiirola I have tried to follow the suggestions you made. For each suggestion that I implemented, I marked its corresponding comment as resolved. The comments that I did not mark as resolved are the ones that could use more of your attention. I was confused about:

  • name change to 'mosek_direct' (can the meta-solver wait until another PR?)
  • using the "_linear_canonical_form" attribute (any hints for an alternative approach)

Thank you for your immensely helpful review.

Copy link
Member

@jsiirola jsiirola left a comment

Choose a reason for hiding this comment

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

@Utkarsh-Detha: Thank you for all your work on this. I think we are very close. As to your two outstanding questions, a simple wrapper class can be:

@SolverFactory.register('mosek', doc='The MOSEK LP/MIP/QP solver')
class MOSEK(OptSolver):
    """The MOSEK LP/MIP/QP solver
    """

    def __new__(cls, *args, **kwds):
        mode = kwds.pop('solver_io', 'direct')
        #
        if mode in {'python', 'direct'}:
            opt = SolverFactory('mosek_direct', **kwds)
            if opt is None:
                logger.error('Python API for MOSEK is not installed')
            return opt
        if mode == 'persistent':
            opt = SolverFactory('mosek_persistent', **kwds)
            if opt is None:
                logger.error('Python API for MOSEK is not installed')
            return opt
        else:
            logger.error('Unknown/unsupported solver interface: %s' % (mode,))
            return None

As for using the "_linear_canonical_form" attribute, I don't have a good suggestion. We should probably file this as an issue to consider when we finally get around to updating how we generate / manage canonical representation caches (and more generally, constraints that embed special storage structures (and not let that hold up this PR).

else:
self._solver_model.putintparam(eval(param), option)
self._solver_model.putintparam(param, option)
Copy link
Member

Choose a reason for hiding this comment

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

Error checking: should you add an:

else:
    raise ValueError("unrecognized MOSEX option name: %s" % (key,))

Copy link
Contributor Author

Choose a reason for hiding this comment

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

In case of an invalid parameter name, an AttributeError will be raised when I use the getattr method to set the value for param. This exception will be caught in the except statement just below, and then re-raised. So this scenario should be covered.

@jsiirola
Copy link
Member

@michaelbynum: I think this PR is close, and we could really use your eyes on it (particularly around the persistent interface).

Copy link
Member

@jsiirola jsiirola left a comment

Choose a reason for hiding this comment

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

This looks good to me. Thank you @Utkarsh-Detha!

Copy link
Contributor

@michaelbynum michaelbynum left a comment

Choose a reason for hiding this comment

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

Overall, this looks good. I've requested a couple minor changes.

"bindings for Mosek?\n\n\t" +
"Error message: {0}".format(e))
raise Exception(msg)
def add_vars(self, var_seq):
Copy link
Contributor

Choose a reason for hiding this comment

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

Should these methods be private with public wrappers on the persistent interface? Users should never call this method on the direct interface, right?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Agreed, I will make the change.
However, I have a (sort-of) unrelated question about the wrapper method for adding constraints: if a user decides to add a cone, but one that is constructed as a domain, then they would be essentially adding a block. Do you think, then, that it would be a good idea to raise an error telling the user to switch to the add_block method, or would it be better to just call the add_block method from within the add_constraints wrapper method?

Copy link
Contributor

Choose a reason for hiding this comment

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

That is an excellent question. I would lean toward calling add_block within the add_constraints wrapper, but I don't have a strong preference.

:return: mosek.variabletype.type_int or mosek.variabletype.type_cont
self.add_constraints((con,))

def add_constraints(self, con_seq):
Copy link
Contributor

Choose a reason for hiding this comment

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

Same as above about private vs public.

@ghackebeil
Copy link
Member

ghackebeil commented Nov 20, 2020 via email

@michaelbynum
Copy link
Contributor

Yeah, @ghackebeil is probably right...

@Utkarsh-Detha
Copy link
Contributor Author

As the changes have been approved, I was wondering if there is something that needs to be done before the PR can be merged? Any hints would be appreciated. Thank you.

@michaelbynum michaelbynum merged commit 744b616 into Pyomo:master Nov 23, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Efficiency of MOSEK interface
7 participants