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

Rewrite CPLEX using Clang #316

Merged
merged 15 commits into from
Sep 28, 2020
Merged

Rewrite CPLEX using Clang #316

merged 15 commits into from
Sep 28, 2020

Conversation

odow
Copy link
Member

@odow odow commented Sep 23, 2020

Tests currently fail due to a use-after-free error with the callbacks on my Mac. Still debugging why/where.

Same comment as Gurobi: jump-dev/Gurobi.jl#346

Getting this shipshape before I step back from maintaining it.

I don't know what the deprecation path is.

Closes #16
Closes #42
Closes #92
Closes #140
Closes #121
Closes #172
Closes #174
Closes #175
Closes #209
Closes #268
Closes #269
Closes #270
Closes #279
Closes #287
Closes #297
Closes #300
Closes #304
Closes #312
Closes #313

@codecov
Copy link

codecov bot commented Sep 23, 2020

Codecov Report

Merging #316 into master will decrease coverage by 15.73%.
The diff coverage is 36.12%.

Impacted file tree graph

@@             Coverage Diff             @@
##           master     #316       +/-   ##
===========================================
- Coverage   69.80%   54.07%   -15.74%     
===========================================
  Files          16        7        -9     
  Lines        2520     2702      +182     
===========================================
- Hits         1759     1461      -298     
- Misses        761     1241      +480     
Impacted Files Coverage Δ
src/MOI/MOI_wrapper.jl 89.20% <ø> (+2.94%) ⬆️
src/gen/libcpx_api.jl 7.28% <ø> (ø)
src/deprecated_functions.jl 1.12% <1.12%> (ø)
src/MOI/conflicts.jl 71.64% <71.64%> (ø)
src/MOI/MOI_callbacks.jl 91.83% <92.72%> (+2.22%) ⬆️
src/CPLEX.jl 100.00% <100.00%> (ø)
src/MOI/indicator_constraint.jl 93.75% <100.00%> (+6.51%) ⬆️
... and 3 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 02d4ef5...d45dcdb. Read the comment docs.

@odow
Copy link
Member Author

odow commented Sep 24, 2020

As the deprecation path, any use of existing functions (or the .inner field of CPLEX.Optimizer) now throws an informative error message:

The C API of CPLEX.jl has been rewritten to expose the complete C API, and
all old functions have been removed.

For example:

    is_point = CPLEX.cbcandidateispoint(cb_data)

is now

    is_point_P = Ref{Cint}()
    CPXcallbackcandidateispoint(cb_data, is_point_P)
    if ret != 0
        # Do something because the call failed
    end
    is_point = is_point_P[]

For users of `CPLEX.Optimizer`, `model.inner` has been replaced by the fields
`model.env` and `model.lp`, which correspond to the environment and problem
pointers at the C API level.

For example:

    stat = CPLEX.get_status(model.inner)

is now:

    stat = CPXgetstat(model.env, model.lp)

The new API is more verbose, but the names and function arguments are now
identical to the C API, documentation for which is available at:
https://www.ibm.com/support/knowledgecenter/SSSA5P_12.10.0/ilog.odms.cplex.help/refcallablelibrary/groups/homepagecallable.html

To revert to the old API, use:

    import Pkg
    Pkg.add(Pkg.PackageSpec(name = "CPLEX", version = v"0.6"))

Then restart Julia for the change to take effect.

@odow
Copy link
Member Author

odow commented Sep 25, 2020

Proposed discourse post below. I think the post, plus the error message, plus code to revert the changes, plus the fact that the people that will be hit with this are people writing solver-dependent callbacks and so hopefully more knowledgable/already fought with the existing API will mean that the pain should be short-lived.

ANN: breaking changes to CPLEX.jl and Gurobi.jl

_tl;dr: If you use **any** lower-level functionality in CPLEX.jl or Gurobi.jl
(e.g., solver-dependent callbacks, or functions from the C API), your code will
break. If you just use  `CPLEX.Optimizer()` or `Gurobi.Optimizer()` with JuMP, 
nothing changes._

Dear all,

We have some big breaking changes landing in the [CPLEX.jl](https://github.com/jump-dev/CPLEX.jl)
and [Gurobi.jl](https://github.com/jump-dev/Gurobi.jl) wrappers.

**All low-level functions in the wrapper have been removed and replaced by a
complete rewrite using Clang.jl. If you used any of these functions, your code
will break.**

## Why did we do this?

CPLEX and Gurobi are two of the oldest solver wrappers, and the C wrappers have
evolved on an ad-hoc basis over time. (The first [commit with working code](https://github.com/jump-dev/Gurobi.jl/commit/300725f809eff25c6564ac3bc42c7bcf3df2b35a)
was made to Gurobi.jl on March 9, 2013!) This meant that we had frequent issues
with "missing" functions in the C API that were not wrapped.

The age of the code also meant that there was not a consistent naming scheme. A
C function like `GRBaddqpterms` might be called `add_qpterms!`, `add_qp_terms`,
or `c_api_addqpterms`. In addition, the arguments to the functions were often
different to the underlying C API, and some arguments were not exposed. Combined
with the lack of documentation, this meant it was hard to identify which Julia
function matched which C function, and which arguments to supply.

The main reason you would want to use the C API is in a solver-dependent
callback. In addition to now supporting the full functionality of a 
solver-dependent callback, we now have documentation. See the Solver-dependent 
callbacks section below.

## What did we do?

We used [Clang.jl](https://github.com/JuliaInterop/Clang.jl) to wrap the
complete C API of each solver.

The functions now have a consistent naming scheme, the arguments are all
available and consistent, and the complete API is already documented by each
solver.

## My code broke and I want to go back

If your code breaks, and you want to revert to the old API, use:
```julia
import Pkg
Pkg.add(Pkg.PackageSpec(name = "CPLEX", version = v"0.6"))
# or
Pkg.add(Pkg.PackageSpec(name = "Gurobi", version = v"0.8"))
```
Then restart Julia for the change to take effect.

## CPLEX Changes

CPLEX has [extensive documentation for their C API](https://www.ibm.com/support/knowledgecenter/SSSA5P_12.10.0/ilog.odms.cplex.help/refcallablelibrary/groups/homepagecallable.html).

The main changes are:
* Function names have changed. For example, `CPLEX.close_CPLEX(env)` is now 
    `CPXcloseCPLEX(env)`.
* For users of `CPLEX.Optimizer`, `model.inner` has been replaced by the fields
    `model.env` and `model.lp`, which correspond to the environment and problem
    pointers at the C API level. For example:
    ```julia
    stat = CPLEX.get_status(model.inner)
    ```
    is now:
    ```julia
    stat = CPXgetstat(model.env, model.lp)
    ```
* Querying functionality has changed. For example:
    ```julia
    is_point = CPLEX.cbcandidateispoint(cb_data)
    ```
    is now:
    ```julia
    is_point_P = Ref{Cint}()
    CPXcallbackcandidateispoint(cb_data, is_point_P)
    if ret != 0
        # Do something because the call failed
    end
    is_point = is_point_P[]
    ```

## Gurobi changes

Gurobi has [extensive documentation for their C API](https://www.gurobi.com/documentation/9.0/refman/c_api_details.html).

The main changes are:
* Constants have changed. For example `CB_MIPNODE` is now `GRB_CB_MIPNODE`
    to match the C API.
* Function names have changed. For example `free_env(env)` is now
    `GRBfreeenv(env)`.
* For users of `Gurobi.Optimizer()`, `model.inner` is now a pointer to the C 
    model, instead of a `Gurobi.Model` object. However, conversion means that 
    you should always pass `model` instead of `model.inner` to the low-level 
    functions. For example:
    ```julia
    jump_model = direct_model(Gurobi.Optimizer())
    model = backend(jump_model)  # model is Gurobi.Optimizer
    # Old
    Gurobi.tune_model(model.inner)
    # New
    GRBtunemodel(model)
    ```
* Some functions have been removed entirely. For example:
    ```julia
    using JuMP, Gurobi
    model = direct_model(Gurobi.Optimizer())
    optimize!(model)
    grb_model = backend(model)
    stat = Gurobi.get_status_code(grb_model.inner)
    ```
    is now:
    ```julia
    using JuMP, Gurobi
    model = direct_model(Gurobi.Optimizer())
    optimize!(model)
    valueP = Ref{Cint}()
    grb_model = backend(model)
    ret = GRBgetintattr(grb_model, "Status", valueP)
    if ret != 0
        # Do something because the call failed
    end
    stat = valueP[]
    ```

## Solver-dependent callbacks

Here is an example with the latest syntax for CPLEX:
```julia
using JuMP, CPLEX, Test

model = direct_model(CPLEX.Optimizer())
set_silent(model)

# This is very, very important!!! Only use callbacks in single-threaded mode.
MOI.set(model, MOI.NumberOfThreads(), 1)

@variable(model, 0 <= x <= 2.5, Int)
@variable(model, 0 <= y <= 2.5, Int)
@objective(model, Max, y)
cb_calls = Clong[]
function my_callback_function(cb_data::CPLEX.CallbackContext, context_id::Clong)
    # You can reference variables outside the function as normal
    push!(cb_calls, context_id)
    # You can select where the callback is run
    if context_id != CPX_CALLBACKCONTEXT_CANDIDATE
        return
    end
    # You can query CALLBACKINFO items
    valueP = Ref{Cdouble}()
    ret = CPXcallbackgetinfodbl(cb_data, CPXCALLBACKINFO_BEST_BND, valueP)
    @info "Best bound is currently: $(valueP[])"
    # As well as any other C API
    x_p = Vector{Cdouble}(undef, 2)
    obj_p = Ref{Cdouble}()
    ret = CPXcallbackgetincumbent(cb_data, x_p, 0, 1, obj_p)
    if ret == 0
        @info "Objective incumbent is: $(obj_p[])"
        @info "Incumbent solution is: $(x_p)"
        # Use CPLEX.column to map between variable references and the 1-based
        # column.
        x_col = CPLEX.column(cb_data, index(x))
        @info "x = $(x_p[x_col])"
    else
        # Unable to query incumbent.
    end

    # Before querying `callback_value`, you must call:
    CPLEX.load_callback_variable_primal(cb_data, context_id)
    x_val = callback_value(cb_data, x)
    y_val = callback_value(cb_data, y)
    # You can submit solver-independent MathOptInterface attributes such as
    # lazy constraints, user-cuts, and heuristic solutions.
    if y_val - x_val > 1 + 1e-6
        con = @build_constraint(y - x <= 1)
        MOI.submit(model, MOI.LazyConstraint(cb_data), con)
    elseif y_val + x_val > 3 + 1e-6
        con = @build_constraint(y + x <= 3)
        MOI.submit(model, MOI.LazyConstraint(cb_data), con)
    end
end
MOI.set(model, CPLEX.CallbackFunction(), my_callback_function)
optimize!(model)
@test termination_status(model) == MOI.OPTIMAL
@test primal_status(model) == MOI.FEASIBLE_POINT
@test value(x) == 1
@test value(y) == 2
```

Here is an example with the latest syntax for Gurobi:
```julia
using JuMP, Gurobi, Test

model = direct_model(Gurobi.Optimizer())
@variable(model, 0 <= x <= 2.5, Int)
@variable(model, 0 <= y <= 2.5, Int)
@objective(model, Max, y)
cb_calls = Cint[]
function my_callback_function(cb_data, cb_where::Cint)
    # You can reference variables outside the function as normal
    push!(cb_calls, cb_where)
    # You can select where the callback is run
    if cb_where != GRB_CB_MIPSOL && cb_where != GRB_CB_MIPNODE
        return
    end
    # You can query a callback attribute using GRBcbget
    if cb_where == GRB_CB_MIPNODE
        resultP = Ref{Cint}()
        GRBcbget(cb_data, cb_where, GRB_CB_MIPNODE_STATUS, resultP)
        if resultP[] != GRB_OPTIMAL
            return  # Solution is something other than optimal.
        end
    end
    # Before querying `callback_value`, you must call:
    Gurobi.load_callback_variable_primal(cb_data, cb_where)
    x_val = callback_value(cb_data, x)
    y_val = callback_value(cb_data, y)
    # You can submit solver-independent MathOptInterface attributes such as
    # lazy constraints, user-cuts, and heuristic solutions.
    if y_val - x_val > 1 + 1e-6
        con = @build_constraint(y - x <= 1)
        MOI.submit(model, MOI.LazyConstraint(cb_data), con)
    elseif y_val + x_val > 3 + 1e-6
        con = @build_constraint(y + x <= 3)
        MOI.submit(model, MOI.LazyConstraint(cb_data), con)
    end
end
# You _must_ set this parameter if using lazy constraints.
MOI.set(model, MOI.RawParameter("LazyConstraints"), 1)
MOI.set(model, Gurobi.CallbackFunction(), my_callback_function)
optimize!(model)
@test termination_status(model) == MOI.OPTIMAL
@test primal_status(model) == MOI.FEASIBLE_POINT
@test value(x) == 1
@test value(y) == 2
```

@odow
Copy link
Member Author

odow commented Sep 25, 2020

This PR is a little hard to review, so I propose merging into master to make it easier for people to try out, with the view to waiting a while before tagging and releasing. Then any extra changes will be much easier to review.

@mlubin
Copy link
Member

mlubin commented Sep 25, 2020

Yeah I don't think I'll be able to do a thorough review of this. Perhaps leave open for a couple days before merging in case some of the repo watchers plan to take a look?

@odow
Copy link
Member Author

odow commented Sep 25, 2020

The main things to test/review are the updated callbacks and deprecation strategy.

@odow
Copy link
Member Author

odow commented Sep 25, 2020

Ha. I obviously forgot to update the binary for travis. Nice to know that the MOI wrapper also works for 12.9 though. But I think we should keep the lower-bound of 12.10.

I'm traveling the next few days, so I probably won't have time to update the binary before next week.

@blegat
Copy link
Member

blegat commented Sep 26, 2020

In the discourse post:

grb_model = backend(model)
# Old
Gurobi.tune_model(model.inner)
# New
GRBtunemodel(model)

should be

grb_model = backend(model)
# Old
Gurobi.tune_model(grb_model.inner)
# New
GRBtunemodel(grb_model.inner)

@odow
Copy link
Member Author

odow commented Sep 28, 2020

Merging this for now, with the expectation that I will fix a couple of the remaining unresolved issues and give people a few weeks to test before tagging a new release. Thoughts on adjusting the discourse announcement to be "Breaking changes are coming, test on the master branch if you want to see them" rather than "Breaking changes just landed and everything broke"?

@odow odow merged commit 4509ec9 into master Sep 28, 2020
@odow odow deleted the od/rewrite branch September 28, 2020 22:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment