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 rewrite_call_expression through _rewrite_expr. #2241

Closed
wants to merge 14 commits into from

Conversation

dourouc05
Copy link
Contributor

@dourouc05 dourouc05 commented May 12, 2020

Excerpt from #2051. New take on #2229, much more general (and not much longer — unless you take fdef385, which unfortunately does not work…).

I'd prefer to see this PR merged instead of #2229, due to the functionalities that are possible with this, but maybe this is not the best way to implement it…

The general goal is to allow rewriting expressions at the JuMP level, only based on syntactic information, to provide more convenience for users. This only provides more points for extensions to plug into JuMP.

More in details (based on the current developments of the CP sets): the modelling layer can decide to rewrite an expression like y <= count(x .== 5) as two constraints, a CP one (say z == count(x .== 5)), and a purely linear one (say y <= z), all of this at JuMP layer. These new constraints can then be bridged by MOI if needed. The constraints to model in the first hand (like y <= count(x .== 5))) should not have MOI equivalents (because that would create a very very large number of sets to support: instead of just Count, you would need LessThanCount, maybe AffineLessThanCount later on, and so on).

This thus raises the issue of deleting the new constraints: as a user, it only makes sense to have a reference to the constraint y <= count(x .== 5); to delete it, two constraints and a variable must be deleted. The existing system of bridges does not work, as it takes as input a MOI constraint (which, by design, the user-specified constraint have a low likelihood to be). Nothing is implemented regarding this. My idea is to have _rewrite_expr to return the new things that the function call created (variables, constraints), so that JuMP can get rid of all of it when deleting the constraint.

@dourouc05
Copy link
Contributor Author

@blegat @odow @mlubin Is this approach viable?

(Thinking about it, I suppose _rewrite_expr should lose its starting underscore, as it would be a part of the public interface.)

@codecov
Copy link

codecov bot commented May 20, 2020

Codecov Report

Merging #2241 into master will increase coverage by 0.00%.
The diff coverage is 100.00%.

Impacted file tree graph

@@           Coverage Diff           @@
##           master    #2241   +/-   ##
=======================================
  Coverage   91.38%   91.38%           
=======================================
  Files          42       42           
  Lines        4189     4250   +61     
=======================================
+ Hits         3828     3884   +56     
- Misses        361      366    +5     
Impacted Files Coverage Δ
src/macros.jl 93.34% <100.00%> (+0.55%) ⬆️
src/Containers/vectorized_product_iterator.jl 35.71% <0.00%> (-7.15%) ⬇️
src/mutable_arithmetics.jl 85.71% <0.00%> (-6.35%) ⬇️
src/Containers/SparseAxisArray.jl 74.64% <0.00%> (-4.52%) ⬇️
src/copy.jl 88.00% <0.00%> (-4.00%) ⬇️
src/Containers/DenseAxisArray.jl 85.29% <0.00%> (-1.97%) ⬇️
src/constraints.jl 91.11% <0.00%> (-0.75%) ⬇️
src/nlp.jl 92.79% <0.00%> (-0.03%) ⬇️
src/JuMP.jl 75.00% <0.00%> (ø)
src/aff_expr.jl 88.02% <0.00%> (+0.17%) ⬆️
... 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 bc37e5a...3a985e8. Read the comment docs.

@dourouc05
Copy link
Contributor Author

I just added a few tests for binary operators.

src/macros.jl Outdated
_rewrite_expr(_error::Function, head::Val{:call}, args...) =
_rewrite_expr(_error, Val(:call), Val(args[1]), args[2:end]...)

function _rewrite_expr(_error::Function, ::Val{:call}, op::Union{Val{:+}, Val{:-}, Val{:*}, Val{:/}}, args...)
Copy link
Member

Choose a reason for hiding this comment

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

Why have a method with OP above which does not rewrite recursively and this one which rewrite recursively ?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The previous one is a fallback for unimplemented operators. I don't know if it's the most sensible default: I try to enable this code as rarely as possible, to avoid breaking things.

@dourouc05 dourouc05 requested a review from blegat June 20, 2020 21:36
@dourouc05
Copy link
Contributor Author

@odow @blegat Does this approach make sense? In other words, might the ideas behind this PR get merged? What do you think of the concept of one JuMP-level constraint mapped to several MOI constraints (say, CompoundConstraint)?

Copy link
Member

@odow odow left a comment

Choose a reason for hiding this comment

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

Can we benchmark this?

I removed this approach in MathOptFormat because it was poorly inferred:
https://github.com/jump-dev/MathOptInterface.jl/pull/1111/files#diff-58997e5cd3d0fcda1923fbd4f434f673R98-R132

@dourouc05
Copy link
Contributor Author

dourouc05 commented Jul 19, 2020

I did a little benchmark on this (https://github.com/dourouc05/JuMP.jl/blob/dourouc05-rewrite-complex/benchmark/parse_constraints.jl). Indeed, without writing complex things, this code may make parsing the constraints twice as slow.

Before:

julia> include("C:\\Users\\Thibaut\\.julia\\dev\\JuMP\\benchmark\\parse_constraints.jl")
(1/2) benchmarking "model0"...
done (took 5.4042206 seconds)
(2/2) benchmarking "model1"...
done (took 5.480212001 seconds)
2-element BenchmarkTools.BenchmarkGroup:
  tags: []
  "model0" => Trial(28.958 ms)
  "model1" => Trial(27.174 ms)

With this PR:

julia> include("C:\\Users\\Thibaut\\.julia\\dev\\JuMP\\benchmark\\parse_constraints.jl")
(1/2) benchmarking "model0"...
done (took 5.499017699 seconds)
(2/2) benchmarking "model1"...
done (took 5.5020427 seconds)
2-element BenchmarkTools.BenchmarkGroup:
  tags: []
  "model0" => Trial(29.167 ms)
  "model1" => Trial(55.927 ms)

While you don't look opposed to the idea, I guess the implementation is not performant enough.

I'm not sure I get the details of the code you point at. You are generating "fast" code to parse the head of an expression, right? Plus using Object so that Julia doesn't generate too much code for different types?

It looks like this implementation spends a lot of time for type inference:

Before:

julia> @profile model1();

julia> Profile.print()
Overhead ╎ [+additional indent] Count File:Line; Function
=========================================================
 ╎4 @Base\task.jl:358; (::REPL.var"#26#27"{REPL.REPLBackend})()
 ╎ 4 D:\buildbot\worker\package_win64\build\usr\share\julia\stdlib\v1.4\REPL\src\REPL.jl:118; macro expansion
 ╎  4 D:\buildbot\worker\package_win64\build\usr\share\julia\stdlib\v1.4\REPL\src\REPL.jl:86; eval_user_input(::Any, ::REPL.REPLBackend)
 ╎   4 @Base\boot.jl:331; eval(::Module, ::Any)
 ╎    4 @Revise\src\Revise.jl:1184; run_backend(::REPL.REPLBackend)
 ╎     4 D:\buildbot\worker\package_win64\build\usr\share\julia\stdlib\v1.4\REPL\src\REPL.jl:86; eval_user_input(::Any, ::REPL.REPLBackend)
 ╎    ╎ 4 @Base\boot.jl:331; eval(::Module, ::Any)
 ╎    ╎  4 @JuMP\benchmark\parse_constraints.jl:18; model1()
 ╎    ╎   4 @Base\client.jl:449; eval(::Expr)
 ╎    ╎    4 @Base\boot.jl:331; eval
 ╎    ╎     4 @JuMP\src\Containers\container.jl:65; container(::Function, ::JuMP.Containers.VectorizedProductIterator{Tuple{Base.OneTo{Int64}}})
 ╎    ╎    ╎ 4 @JuMP\src\Containers\container.jl:70; container
2╎    ╎    ╎  4 @Base\abstractarray.jl:2098; map(::Function, ::JuMP.Containers.VectorizedProductIterator{Tuple{Base.OneTo{Int64}}})
 ╎    ╎    ╎   2 @Base\compiler\typeinfer.jl:605; typeinf_ext(::Core.MethodInstance, ::UInt64)
 ╎    ╎    ╎    2 @Base\compiler\typeinfer.jl:574; typeinf_ext(::Core.MethodInstance, ::Core.Compiler.Params)
 ╎    ╎    ╎     2 @Base\compiler\typeinfer.jl:12; typeinf(::Core.Compiler.InferenceState)
 ╎    ╎    ╎    ╎ 2 @Base\compiler\abstractinterpretation.jl:1283; typeinf_nocycle(::Core.Compiler.InferenceState)
 ╎    ╎    ╎    ╎  2 @Base\compiler\abstractinterpretation.jl:1227; typeinf_local(::Core.Compiler.InferenceState)
 ╎    ╎    ╎    ╎   2 @Base\compiler\abstractinterpretation.jl:974; abstract_eval(::Any, ::Array{Any,1}, ::Core.Compiler.InferenceState)
 ╎    ╎    ╎    ╎    2 @Base\compiler\abstractinterpretation.jl:880; abstract_call(::Array{Any,1}, ::Array{Any,1}, ::Array{Any,1}, ::Core.Compiler.InferenceState)
 ╎    ╎    ╎    ╎     2 @Base\compiler\abstractinterpretation.jl:895; abstract_call(::Array{Any,1}, ::Array{Any,1}, ::Array{Any,1}, ::Core.Compiler.InferenceState, ::Int64)
 ╎    ╎    ╎    ╎    ╎ 2 @Base\compiler\abstractinterpretation.jl:873; abstract_call_known(::Any, ::Array{Any,1}, ::Array{Any,1}, ::Array{Any,1}, ::Core.Compiler.InferenceState, ::Int64)
 ╎    ╎    ╎    ╎    ╎  1 @Base\compiler\abstractinterpretation.jl:101; abstract_call_gf_by_type(::Any, ::Array{Any,1}, ::Any, ::Core.Compiler.InferenceState, ::Int64)
 ╎    ╎    ╎    ╎    ╎   1 @Base\compiler\abstractinterpretation.jl:404; abstract_call_method(::Method, ::Any, ::Core.SimpleVector, ::Bool, ::Core.Compiler.InferenceState)
 ╎    ╎    ╎    ╎    ╎    1 @Base\compiler\typeinfer.jl:488; typeinf_edge(::Method, ::Any, ::Core.SimpleVector, ::Core.Compiler.InferenceState)
 ╎    ╎    ╎    ╎    ╎     1 @Base\compiler\typeinfer.jl:12; typeinf(::Core.Compiler.InferenceState)
 ╎    ╎    ╎    ╎    ╎    ╎ 1 @Base\compiler\abstractinterpretation.jl:1283; typeinf_nocycle(::Core.Compiler.InferenceState)
 ╎    ╎    ╎    ╎    ╎    ╎  1 @Base\compiler\abstractinterpretation.jl:1227; typeinf_local(::Core.Compiler.InferenceState)
 ╎    ╎    ╎    ╎    ╎    ╎   1 @Base\compiler\abstractinterpretation.jl:974; abstract_eval(::Any, ::Array{Any,1}, ::Core.Compiler.InferenceState)
 ╎    ╎    ╎    ╎    ╎    ╎    1 @Base\compiler\abstractinterpretation.jl:880; abstract_call(::Array{Any,1}, ::Array{Any,1}, ::Array{Any,1}, ::Core.Compiler.InferenceState)
 ╎    ╎    ╎    ╎    ╎    ╎     1 @Base\compiler\abstractinterpretation.jl:895; abstract_call(::Array{Any,1}, ::Array{Any,1}, ::Array{Any,1}, ::Core.Compiler.InferenceState, ::Int64)
 ╎    ╎    ╎    ╎    ╎    ╎    ╎ 1 @Base\compiler\abstractinterpretation.jl:873; abstract_call_known(::Any, ::Array{Any,1}, ::Array{Any,1}, ::Array{Any,1}, ::Core.Compiler.InferenceState, ::I...
 ╎    ╎    ╎    ╎    ╎    ╎    ╎  1 @Base\compiler\abstractinterpretation.jl:101; abstract_call_gf_by_type(::Any, ::Array{Any,1}, ::Any, ::Core.Compiler.InferenceState, ::Int64)
 ╎    ╎    ╎    ╎    ╎    ╎    ╎   1 @Base\compiler\abstractinterpretation.jl:404; abstract_call_method(::Method, ::Any, ::Core.SimpleVector, ::Bool, ::Core.Compiler.InferenceState)
 ╎    ╎    ╎    ╎    ╎    ╎    ╎    1 @Base\compiler\typeinfer.jl:488; typeinf_edge(::Method, ::Any, ::Core.SimpleVector, ::Core.Compiler.InferenceState)
 ╎    ╎    ╎    ╎    ╎    ╎    ╎     1 @Base\compiler\typeinfer.jl:33; typeinf(::Core.Compiler.InferenceState)
 ╎    ╎    ╎    ╎    ╎    ╎    ╎    ╎ 1 @Base\compiler\optimize.jl:169; optimize(::Core.Compiler.OptimizationState, ::Any)
 ╎    ╎    ╎    ╎    ╎    ╎    ╎    ╎  1 @Base\compiler\ssair\driver.jl:112; run_passes(::Core.CodeInfo, ::Int64, ::Core.Compiler.OptimizationState)
 ╎    ╎    ╎    ╎    ╎    ╎    ╎    ╎   1 @Base\compiler\ssair\driver.jl:102; just_construct_ssa(::Core.CodeInfo, ::Array{Any,1}, ::Int64, ::Core.Compiler.OptimizationState)
 ╎    ╎    ╎    ╎    ╎    ╎    ╎    ╎    1 @Base\compiler\ssair\slot2ssa.jl:55; scan_slot_def_use(::Int64, ::Core.CodeInfo, ::Array{Any,1})
1╎    ╎    ╎    ╎    ╎    ╎    ╎    ╎     1 @Base\compiler\ssair\slot2ssa.jl:23; scan_entry!(::Array{Core.Compiler.SlotInfo,1}, ::Int64, ::Any)
 ╎    ╎    ╎    ╎    ╎  1 @Base\compiler\abstractinterpretation.jl:124; abstract_call_gf_by_type(::Any, ::Array{Any,1}, ::Any, ::Core.Compiler.InferenceState, ::Int64)
 ╎    ╎    ╎    ╎    ╎   1 @Base\compiler\abstractinterpretation.jl:246; abstract_call_method_with_const_args(::Any, ::Any, ::Array{Any,1}, ::Core.SimpleVector, ::Core.Compiler.InferenceSt...
 ╎    ╎    ╎    ╎    ╎    1 @Base\compiler\inferenceresult.jl:12; InferenceResult
 ╎    ╎    ╎    ╎    ╎     1 @Base\compiler\inferenceresult.jl:47; matching_cache_argtypes(::Core.MethodInstance, ::Array{Any,1})
 ╎    ╎    ╎    ╎    ╎    ╎ 1 @Base\compiler\inferenceresult.jl:64; matching_cache_argtypes(::Core.MethodInstance, ::Nothing)
Total snapshots: 4

After:

Overhead ╎ [+additional indent] Count File:Line; Function
=========================================================
 ╎3 @Base\task.jl:358; (::REPL.var"#26#27"{REPL.REPLBackend})()
 ╎ 3 D:\buildbot\worker\package_win64\build\usr\share\julia\stdlib\v1.4\REPL\src\REPL.jl:118; macro expansion
 ╎  3 D:\buildbot\worker\package_win64\build\usr\share\julia\stdlib\v1.4\REPL\src\REPL.jl:86; eval_user_input(::Any, ::REPL.REPLBackend)
 ╎   3 @Base\boot.jl:331; eval(::Module, ::Any)
 ╎    3 @Revise\src\Revise.jl:1184; run_backend(::REPL.REPLBackend)
 ╎     3 D:\buildbot\worker\package_win64\build\usr\share\julia\stdlib\v1.4\REPL\src\REPL.jl:86; eval_user_input(::Any, ::REPL.REPLBackend)
 ╎    ╎ 3 @Base\boot.jl:331; eval(::Module, ::Any)
 ╎    ╎  3 @JuMP\benchmark\parse_constraints.jl:18; model1()
 ╎    ╎   3 @Base\client.jl:449; eval(::Expr)
 ╎    ╎    3 @Base\boot.jl:331; eval
 ╎    ╎     3 @JuMP\src\Containers\container.jl:65; container(::Function, ::JuMP.Containers.VectorizedProductIterator{Tuple{Base.OneTo{Int64}}})
 ╎    ╎    ╎ 3 @JuMP\src\Containers\container.jl:70; container
2╎    ╎    ╎  3 @Base\abstractarray.jl:2098; map(::Function, ::JuMP.Containers.VectorizedProductIterator{Tuple{Base.OneTo{Int64}}})
 ╎    ╎    ╎   1 @Base\compiler\typeinfer.jl:605; typeinf_ext(::Core.MethodInstance, ::UInt64)
 ╎    ╎    ╎    1 @Base\compiler\typeinfer.jl:574; typeinf_ext(::Core.MethodInstance, ::Core.Compiler.Params)
 ╎    ╎    ╎     1 @Base\compiler\typeinfer.jl:12; typeinf(::Core.Compiler.InferenceState)
 ╎    ╎    ╎    ╎ 1 @Base\compiler\abstractinterpretation.jl:1283; typeinf_nocycle(::Core.Compiler.InferenceState)
 ╎    ╎    ╎    ╎  1 @Base\compiler\abstractinterpretation.jl:1227; typeinf_local(::Core.Compiler.InferenceState)
 ╎    ╎    ╎    ╎   1 @Base\compiler\abstractinterpretation.jl:974; abstract_eval(::Any, ::Array{Any,1}, ::Core.Compiler.InferenceState)
 ╎    ╎    ╎    ╎    1 @Base\compiler\abstractinterpretation.jl:880; abstract_call(::Array{Any,1}, ::Array{Any,1}, ::Array{Any,1}, ::Core.Compiler.InferenceState)
 ╎    ╎    ╎    ╎     1 @Base\compiler\abstractinterpretation.jl:895; abstract_call(::Array{Any,1}, ::Array{Any,1}, ::Array{Any,1}, ::Core.Compiler.InferenceState, ::Int64)
 ╎    ╎    ╎    ╎    ╎ 1 @Base\compiler\abstractinterpretation.jl:873; abstract_call_known(::Any, ::Array{Any,1}, ::Array{Any,1}, ::Array{Any,1}, ::Core.Compiler.InferenceState, ::Int64)
 ╎    ╎    ╎    ╎    ╎  1 @Base\compiler\abstractinterpretation.jl:101; abstract_call_gf_by_type(::Any, ::Array{Any,1}, ::Any, ::Core.Compiler.InferenceState, ::Int64)
 ╎    ╎    ╎    ╎    ╎   1 @Base\compiler\abstractinterpretation.jl:404; abstract_call_method(::Method, ::Any, ::Core.SimpleVector, ::Bool, ::Core.Compiler.InferenceState)
 ╎    ╎    ╎    ╎    ╎    1 @Base\compiler\typeinfer.jl:488; typeinf_edge(::Method, ::Any, ::Core.SimpleVector, ::Core.Compiler.InferenceState)
 ╎    ╎    ╎    ╎    ╎     1 @Base\compiler\typeinfer.jl:12; typeinf(::Core.Compiler.InferenceState)
 ╎    ╎    ╎    ╎    ╎    ╎ 1 @Base\compiler\abstractinterpretation.jl:1283; typeinf_nocycle(::Core.Compiler.InferenceState)
 ╎    ╎    ╎    ╎    ╎    ╎  1 @Base\compiler\abstractinterpretation.jl:1227; typeinf_local(::Core.Compiler.InferenceState)
 ╎    ╎    ╎    ╎    ╎    ╎   1 @Base\compiler\abstractinterpretation.jl:974; abstract_eval(::Any, ::Array{Any,1}, ::Core.Compiler.InferenceState)
 ╎    ╎    ╎    ╎    ╎    ╎    1 @Base\compiler\abstractinterpretation.jl:880; abstract_call(::Array{Any,1}, ::Array{Any,1}, ::Array{Any,1}, ::Core.Compiler.InferenceState)
 ╎    ╎    ╎    ╎    ╎    ╎     1 @Base\compiler\abstractinterpretation.jl:895; abstract_call(::Array{Any,1}, ::Array{Any,1}, ::Array{Any,1}, ::Core.Compiler.InferenceState, ::Int64)
 ╎    ╎    ╎    ╎    ╎    ╎    ╎ 1 @Base\compiler\abstractinterpretation.jl:873; abstract_call_known(::Any, ::Array{Any,1}, ::Array{Any,1}, ::Array{Any,1}, ::Core.Compiler.InferenceState, ::I...
 ╎    ╎    ╎    ╎    ╎    ╎    ╎  1 @Base\compiler\abstractinterpretation.jl:101; abstract_call_gf_by_type(::Any, ::Array{Any,1}, ::Any, ::Core.Compiler.InferenceState, ::Int64)
 ╎    ╎    ╎    ╎    ╎    ╎    ╎   1 @Base\compiler\abstractinterpretation.jl:404; abstract_call_method(::Method, ::Any, ::Core.SimpleVector, ::Bool, ::Core.Compiler.InferenceState)
 ╎    ╎    ╎    ╎    ╎    ╎    ╎    1 @Base\compiler\typeinfer.jl:488; typeinf_edge(::Method, ::Any, ::Core.SimpleVector, ::Core.Compiler.InferenceState)
 ╎    ╎    ╎    ╎    ╎    ╎    ╎     1 @Base\compiler\typeinfer.jl:12; typeinf(::Core.Compiler.InferenceState)
 ╎    ╎    ╎    ╎    ╎    ╎    ╎    ╎ 1 @Base\compiler\abstractinterpretation.jl:1283; typeinf_nocycle(::Core.Compiler.InferenceState)
 ╎    ╎    ╎    ╎    ╎    ╎    ╎    ╎  1 @Base\compiler\abstractinterpretation.jl:1213; typeinf_local(::Core.Compiler.InferenceState)
 ╎    ╎    ╎    ╎    ╎    ╎    ╎    ╎   1 @Base\compiler\abstractinterpretation.jl:974; abstract_eval(::Any, ::Array{Any,1}, ::Core.Compiler.InferenceState)
 ╎    ╎    ╎    ╎    ╎    ╎    ╎    ╎    1 @Base\compiler\abstractinterpretation.jl:880; abstract_call(::Array{Any,1}, ::Array{Any,1}, ::Array{Any,1}, ::Core.Compiler.InferenceState)
 ╎    ╎    ╎    ╎    ╎    ╎    ╎    ╎     1 @Base\compiler\abstractinterpretation.jl:895; abstract_call(::Array{Any,1}, ::Array{Any,1}, ::Array{Any,1}, ::Core.Compiler.InferenceState, ::Int64)
 ╎    ╎    ╎    ╎    ╎    ╎    ╎    ╎    ╎ 1 @Base\compiler\abstractinterpretation.jl:873; abstract_call_known(::Any, ::Array{Any,1}, ::Array{Any,1}, ::Array{Any,1}, ::Core.Compiler.InferenceStat...
 ╎    ╎    ╎    ╎    ╎    ╎    ╎    ╎    ╎  1 @Base\compiler\abstractinterpretation.jl:101; abstract_call_gf_by_type(::Any, ::Array{Any,1}, ::Any, ::Core.Compiler.InferenceState, ::Int64)
 ╎    ╎    ╎    ╎    ╎    ╎    ╎    ╎    ╎   1 @Base\compiler\abstractinterpretation.jl:404; abstract_call_method(::Method, ::Any, ::Core.SimpleVector, ::Bool, ::Core.Compiler.InferenceState)
 ╎    ╎    ╎    ╎    ╎    ╎    ╎    ╎    ╎    1 @Base\compiler\typeinfer.jl:488; typeinf_edge(::Method, ::Any, ::Core.SimpleVector, ::Core.Compiler.InferenceState)
 ╎    ╎    ╎    ╎    ╎    ╎    ╎    ╎    ╎     1 @Base\compiler\typeinfer.jl:12; typeinf(::Core.Compiler.InferenceState)
 ╎    ╎    ╎    ╎    ╎    ╎    ╎    ╎    ╎    ╎ 1 @Base\compiler\abstractinterpretation.jl:1283; typeinf_nocycle(::Core.Compiler.InferenceState)
 ╎    ╎    ╎    ╎    ╎    ╎    ╎    ╎    ╎    ╎  1 @Base\compiler\abstractinterpretation.jl:1213; typeinf_local(::Core.Compiler.InferenceState)
 ╎    ╎    ╎    ╎    ╎    ╎    ╎    ╎    ╎    ╎   1 @Base\compiler\abstractinterpretation.jl:974; abstract_eval(::Any, ::Array{Any,1}, ::Core.Compiler.InferenceState)
 ╎    ╎    ╎    ╎    ╎    ╎    ╎    ╎    ╎    ╎    1 @Base\compiler\abstractinterpretation.jl:880; abstract_call(::Array{Any,1}, ::Array{Any,1}, ::Array{Any,1}, ::Core.Compiler.InferenceState)
 ╎    ╎    ╎    ╎    ╎    ╎    ╎    ╎    ╎    ╎     1 @Base\compiler\abstractinterpretation.jl:895; abstract_call(::Array{Any,1}, ::Array{Any,1}, ::Array{Any,1}, ::Core.Compiler.InferenceState, ::Int64)
 ╎    ╎    ╎    ╎    ╎    ╎    ╎    ╎    ╎    ╎    ╎ 1 @Base\compiler\abstractinterpretation.jl:673; abstract_call_known(::Any, ::Array{Any,1}, ::Array{Any,1}, ::Array{Any,1}, ::Core.Compiler.Inferen...
 ╎    ╎    ╎    ╎    ╎    ╎    ╎    ╎    ╎    ╎    ╎  1 @Base\compiler\abstractinterpretation.jl:604; abstract_apply(::Any, ::Any, ::Array{Any,1}, ::Array{Any,1}, ::Core.Compiler.InferenceState, ::In...
 ╎    ╎    ╎    ╎    ╎    ╎    ╎    ╎    ╎    ╎    ╎   1 @Base\compiler\abstractinterpretation.jl:895; abstract_call(::Nothing, ::Array{Any,1}, ::Array{Any,1}, ::Core.Compiler.InferenceState, ::Int64)
 ╎    ╎    ╎    ╎    ╎    ╎    ╎    ╎    ╎    ╎    ╎    1 @Base\compiler\abstractinterpretation.jl:873; abstract_call_known(::Any, ::Nothing, ::Array{Any,1}, ::Array{Any,1}, ::Core.Compiler.InferenceS...
 ╎    ╎    ╎    ╎    ╎    ╎    ╎    ╎    ╎    ╎    ╎     1 @Base\compiler\abstractinterpretation.jl:50; abstract_call_gf_by_type(::Any, ::Array{Any,1}, ::Any, ::Core.Compiler.InferenceState, ::Int64)
 ╎    ╎    ╎    ╎    ╎    ╎    ╎    ╎    ╎    ╎    ╎    ╎ 1 @Base\reflection.jl:841; _methods_by_ftype
Total snapshots: 3

However, I don't know how to interpret these…

https://github.com/thautwarm/MLStyle.jl could be a solution to implement this pattern matching, with high performance, but no extension possibilities (which is the point, here).

@blegat
Copy link
Member

blegat commented Jul 19, 2020

What do you think of the concept of one JuMP-level constraint mapped to several MOI constraints (say, CompoundConstraint)?

You could generate a single constraint that is bridged into several constraints. I'd prefer not having constraint returning several MOI constraints

@dourouc05
Copy link
Contributor Author

You could generate a single constraint that is bridged into several constraints. I'd prefer not having constraint returning several MOI constraints

Which "single" constraint? Does MOI need a way to represent constraints like count(x .== 0) <= count(y .<= z) + minimum(abs.(x))? That would require adding count as a new type of function, along with minimum and anything that might be used in an expression, but also a way to combine all these (at least in an affine way, but probably also nonlinear). It would make MOI's CP extension much much more complex. The solver interface would have more information about the constraints and could potentially use it (some CP solvers have a way to represent x <= count(sth) directly, but not all of them… and none can really gulp down more complex things), but implementing such an interface would be much more cumbersome (all solver interfaces would need a way to parse these expressions).

That's why I think it would make sense, at the modelling layer, to decompose complex constraints into bite-sized MOI constraints, at the lowest level of abstraction that makes sense based on the solver APIs (that would mean adding things like x <= count(sth) in the MOI extensions, but nothing more complicated).

@odow
Copy link
Member

odow commented Jul 20, 2020

could be a solution to implement this pattern matching, with high performance, but no extension possibilities (which is the point, here).

Yes. This is the point I settled on. Create a giant if statement with the common ones first. Make it easy to extend if we add stuff to JuMP, but don't make it externally extensible.

@dourouc05
Copy link
Contributor Author

Yes. This is the point I settled on. Create a giant if statement with the common ones first. Make it easy to extend if we add stuff to JuMP, but don't make it externally extensible.

That would defeat the whole point of this, which is to allow extensions of JuMP syntax. Or maybe there is a way to keep a fast path for the common case, and keep the flexibility for extensions (with a performance penalty)?

@blegat
Copy link
Member

blegat commented Jul 20, 2020

It seems your use case is similar to disciplined convex reformulation. In JuMP, you need to write your convex model as constraints that are either VectorOfVariables-in-Cone or VectorAffineFunction-in-Cone.
However, if a nonlinear expression is disciplined convex (which is stronger than convex), there is an automatic way to rewrite it as constraints of the form just mentioned.
Convex.jl does this reformulation for instance.
The plan to bring this kind of reformulation in JuMP is to wait for jump-dev/MathOptInterface.jl#846 and then to add bridges for nonlinear expression into convex constraints.
This is a bit different from other bridges as you cannot determine just from the type of the constraints whether you will be able to transform it into convex constraints and which constraints you are going to produce so we might need to adapt LazyBridgeOptimizer or apply this bridge with a SingleBridgeOptimizer.
You could have the same approach and bridge nonlinear expression into constraint programming sets in bridges instead of doing it during the parsing of the macros.

@dourouc05
Copy link
Contributor Author

It seems your use case is similar to disciplined convex reformulation.

Indeed, it's the same issue, as DCP may decompose a single expression in many constraints. For now, I can live without the possibility to remove CP constraints :)!

(By the way, I didn't see any documents giving ideas on how to implement bridges on top of these nonlinear expressions, that's why I didn't think this issue was relevant.)

@dourouc05
Copy link
Contributor Author

@odow I've been benchmarking this code a tad more, and it looks like my previous figures where wrong (I was using Revise to test the latest version of the code). When restarting Julia between runs (i.e. with and without this PR), I cannot find a real performance difference.

Before:

julia> include("C:\\Users\\Thibaut\\.julia\\dev\\JuMP\\benchmark\\parse_constraints.jl")
(1/2) benchmarking "model0"...
done (took 5.4042206 seconds)
(2/2) benchmarking "model1"...
done (took 5.480212001 seconds)
2-element BenchmarkTools.BenchmarkGroup:
  tags: []
  "model0" => Trial(29.564 ms)
  "model1" => Trial(53.224 ms)

After:

julia> include("C:\\Users\\Thibaut\\.julia\\dev\\JuMP\\benchmark\\parse_constraints.jl")
(1/2) benchmarking "model0"...
done (took 5.4042206 seconds)
(2/2) benchmarking "model1"...
done (took 5.480212001 seconds)
2-element BenchmarkTools.BenchmarkGroup:
  tags: []
  "model0" => Trial(28.770 ms)
  "model1" => Trial(53.410 ms)

I've run both quite a few times, and the figures never wander too far from these (give or take one or two milliseconds, for both). I've also tested to replace all these functions by a single one and a large if, but it gave no real difference.

@blegat
Copy link
Member

blegat commented Jul 22, 2020

I think the difference will be mainly on the compilation since it will need to compile many more methods due to Val

@dourouc05
Copy link
Contributor Author

By the way, is performance really important? Most use cases I can think of should rather be implemented in nonlinear bridges rather than this way (everything, except overloading operators like && between binary variables: maybe that should go into MutableArithmetics?). As I understand the new nonlinear implementation is still far away, it might be good to include this in the meantime.

@dourouc05
Copy link
Contributor Author

@blegat @odow @mlubin As this code is intended to provide extension points for only a limited amount of time (until MOI fully supports nonlinearity), maybe it's in a good-enough shape to be merged? (At least as a 'private' API, i.e. not exported and potentially undocumented either.)

@blegat
Copy link
Member

blegat commented Aug 20, 2020

I'm a bit hesitant because it significantly increase compile time

@dourouc05
Copy link
Contributor Author

I made a few benchmarks on my own.

JuMP 0.21.5:

  • compilation from scratch: 16.980 s
  • precompiled (second using): 5.799 s
  • .ji file: 1,518,406 bytes
               _
   _       _ _(_)_     |  Documentation: https://docs.julialang.org
  (_)     | (_) (_)    |
   _ _   _| |_  __ _   |  Type "?" for help, "]?" for Pkg help.
  | | | | | | |/ _` |  |
  | | |_| | | | (_| |  |  Version 1.5.0 (2020-08-01)
 _/ |\__'_|_|_|\__'_|  |  Official https://julialang.org/ release
|__/                   |

julia> @time using JuMP
[ Info: Precompiling JuMP [4076af6c-e467-56ae-b986-b466b2749572]
 16.980351 seconds (10.54 M allocations: 620.875 MiB, 1.16% gc time)
               _
   _       _ _(_)_     |  Documentation: https://docs.julialang.org
  (_)     | (_) (_)    |
   _ _   _| |_  __ _   |  Type "?" for help, "]?" for Pkg help.
  | | | | | | |/ _` |  |
  | | |_| | | | (_| |  |  Version 1.5.0 (2020-08-01)
 _/ |\__'_|_|_|\__'_|  |  Official https://julialang.org/ release
|__/                   |

julia> @time using JuMP
  5.799019 seconds (9.72 M allocations: 579.062 MiB, 2.99% gc time)

This PR (not rebased on current JuMP):

  • compilation from scratch: 17.092 s
  • precompiled (second using): 5.337 s
  • .ji file: 1,522,398 bytes
               _
   _       _ _(_)_     |  Documentation: https://docs.julialang.org
  (_)     | (_) (_)    |
   _ _   _| |_  __ _   |  Type "?" for help, "]?" for Pkg help.
  | | | | | | |/ _` |  |
  | | |_| | | | (_| |  |  Version 1.5.0 (2020-08-01)
 _/ |\__'_|_|_|\__'_|  |  Official https://julialang.org/ release
|__/                   |

julia> @time using JuMP
[ Info: Precompiling JuMP [4076af6c-e467-56ae-b986-b466b2749572]
 17.092835 seconds (10.54 M allocations: 620.675 MiB, 1.11% gc time)
               _
   _       _ _(_)_     |  Documentation: https://docs.julialang.org
  (_)     | (_) (_)    |
   _ _   _| |_  __ _   |  Type "?" for help, "]?" for Pkg help.
  | | | | | | |/ _` |  |
  | | |_| | | | (_| |  |  Version 1.5.0 (2020-08-01)
 _/ |\__'_|_|_|\__'_|  |  Official https://julialang.org/ release
|__/                   |

julia> @time using JuMP
  5.337707 seconds (9.72 M allocations: 578.967 MiB, 3.79% gc time)

Details of my version of Julia:

julia> versioninfo()
Julia Version 1.5.0
Commit 96786e22cc (2020-08-01 23:44 UTC)
Platform Info:
  OS: Windows (x86_64-w64-mingw32)
  CPU: Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz
  WORD_SIZE: 64
  LIBM: libopenlibm
  LLVM: libLLVM-9.0.1 (ORCJIT, skylake)

The difference in timing is not that large (although it's far from a scientific comparison).

@blegat
Copy link
Member

blegat commented Oct 5, 2020

We should also compare the compilation time of @constraint calls.

@dourouc05
Copy link
Contributor Author

I already did some: #2241 (comment)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

Successfully merging this pull request may close these issues.

3 participants