-
-
Notifications
You must be signed in to change notification settings - Fork 400
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
Macro to update expression in-place #3931
Comments
There is already julia> x = 1
1
julia> @expression(model, x += 2)
ERROR: UndefVarError: `x` not defined
Stacktrace:
[1] macro expansion
@ ~/.julia/dev/MutableArithmetics/src/rewrite.jl:376 [inlined]
[2] macro expansion
@ ~/.julia/packages/JuMP/CU7H5/src/macros.jl:264 [inlined]
[3] macro expansion
@ ~/.julia/packages/JuMP/CU7H5/src/macros/@expression.jl:86 [inlined]
[4] macro expansion
@ ~/.julia/packages/JuMP/CU7H5/src/macros.jl:400 [inlined]
[5] top-level scope |
But also, |
@expression(EP, ePowerBalance_VRE_STOR[t = 1:T, z = 1:Z], JuMP.AffExpr())
for t in 1:T, z in 1:Z
if !isempty(resources_in_zone_by_rid(gen_VRE_STOR, z))
ePowerBalance_VRE_STOR[t, z] += sum(EP[:vP][y, t]
for y in resources_in_zone_by_rid(gen_VRE_STOR,
z))
end
end should be @expression(EP, ePowerBalance_VRE_STOR[t = 1:T, z = 1:Z], zero(JuMP.AffExpr))
for z in 1:Z
resources = resources_in_zone_by_rid(gen_VRE_STOR, z)
if !isempty(resources)
vP = EP[:vP]
for t in 1:T
ePowerBalance_VRE_STOR[t, z] =
@expression(EP, sum(vP[y, t] for y in resources))
end
end
end |
I don't know that other macro would have helped. It's things like "don't compute |
We can't make julia> a = BigInt(1)
1
julia> b = a
1
julia> b += BigInt(2)
3
julia> a
1
|
Here was my diff to MA diff --git a/src/rewrite_generic.jl b/src/rewrite_generic.jl
index de2234f..a8c1477 100644
--- a/src/rewrite_generic.jl
+++ b/src/rewrite_generic.jl
@@ -101,6 +101,22 @@ function _rewrite_generic(stack::Expr, expr::Expr)
root = gensym()
push!(stack.args, :($root = $if_expr))
return root, is_mutable
+ elseif Meta.isexpr(expr, :+=, 2)
+ # x += y -> operate!!(add_mul, x, y)
+ y = expr.args[2]
+ rhs = Expr(:call, operate!!, add_mul, esc(expr.args[1]))
+ if Meta.isexpr(y, :call) && y.args[1] == :*
+ for i in 2:length(y.args)
+ arg_root, _ = _rewrite_generic(stack, y.args[i])
+ push!(rhs.args, arg_root)
+ end
+ else
+ y_root, _ = _rewrite_generic(stack, y)
+ push!(rhs.args, y_root)
+ end
+ root = gensym()
+ push!(stack.args, :($root = $rhs))
+ return root, true
elseif !Meta.isexpr(expr, :call)
# In situations like `x[i]`, we do not attempt to rewrite. Return `expr`
# and don't let future callers mutate. julia> @macroexpand @expression(model, y += x)
quote
#= REPL[20]:1 =#
JuMP._valid_model(model, :model)
begin
#= /Users/oscar/.julia/packages/JuMP/CU7H5/src/macros.jl:399 =#
let model = model
#= /Users/oscar/.julia/packages/JuMP/CU7H5/src/macros.jl:400 =#
begin
#= /Users/oscar/.julia/packages/JuMP/CU7H5/src/macros/@expression.jl:86 =#
begin
#= /Users/oscar/.julia/packages/JuMP/CU7H5/src/macros.jl:264 =#
var"#112###246" = begin
#= /Users/oscar/.julia/dev/MutableArithmetics/src/rewrite.jl:374 =#
let
#= /Users/oscar/.julia/dev/MutableArithmetics/src/rewrite.jl:375 =#
begin
#= /Users/oscar/.julia/dev/MutableArithmetics/src/rewrite.jl:371 =#
var"#113###247" = (MutableArithmetics.operate!!)(MutableArithmetics.add_mul, y, x)
end
#= /Users/oscar/.julia/dev/MutableArithmetics/src/rewrite.jl:376 =#
var"#113###247"
end
end
#= /Users/oscar/.julia/packages/JuMP/CU7H5/src/macros.jl:265 =#
var"#114###248" = (JuMP.flatten!)(var"#112###246")
end
#= /Users/oscar/.julia/packages/JuMP/CU7H5/src/macros/@expression.jl:89 =#
JuMP._replace_zero(model, var"#114###248")
end
end
end
end
julia> @macroexpand @expression(model, y += 2 * x)
quote
#= REPL[21]:1 =#
JuMP._valid_model(model, :model)
begin
#= /Users/oscar/.julia/packages/JuMP/CU7H5/src/macros.jl:399 =#
let model = model
#= /Users/oscar/.julia/packages/JuMP/CU7H5/src/macros.jl:400 =#
begin
#= /Users/oscar/.julia/packages/JuMP/CU7H5/src/macros/@expression.jl:86 =#
begin
#= /Users/oscar/.julia/packages/JuMP/CU7H5/src/macros.jl:264 =#
var"#120###249" = begin
#= /Users/oscar/.julia/dev/MutableArithmetics/src/rewrite.jl:374 =#
let
#= /Users/oscar/.julia/dev/MutableArithmetics/src/rewrite.jl:375 =#
begin
#= /Users/oscar/.julia/dev/MutableArithmetics/src/rewrite.jl:371 =#
var"#121###250" = (MutableArithmetics.operate!!)(MutableArithmetics.add_mul, y, 2, x)
end
#= /Users/oscar/.julia/dev/MutableArithmetics/src/rewrite.jl:376 =#
var"#121###250"
end
end
#= /Users/oscar/.julia/packages/JuMP/CU7H5/src/macros.jl:265 =#
var"#122###251" = (JuMP.flatten!)(var"#120###249")
end
#= /Users/oscar/.julia/packages/JuMP/CU7H5/src/macros/@expression.jl:89 =#
JuMP._replace_zero(model, var"#122###251")
end
end
end
end |
I think this is another issue that is really asking for a way to accurately profile where time is spent in JuMP. The issue is that the Julia profiler doesn't really help if you have lots of dispersed See GenXProject/GenX.jl#815 (comment), where half the remaining time is GC. @joaquimg have you tried profiling allocations instead of time? |
More details about GenX is what I am currently looking at.
yup I have been checking for improvements by looking at allocations reduction, so we can reduce GC pressure. Still thinking about the best way to report. |
I dont fully get it. The example is showing that b is assigned to a new expression, which is the same as in JuMP: Using in JuMP: Your example reads the same: julia> a = 0 * x + 1
1
julia> b = a
1
julia> b += 2 # new expression created
3
julia> a
1 and julia> a = 0 * x + 1
1
julia> b = a
1
julia> add_to_expression!(b, 2)
3
julia> a
3 so I would expect: julia> a = 0 * x + 1
1
julia> b = a
1
julia> @expression(model, b += 2) # in-place
3
julia> a
3 On the other hand, if the argument is coming from: "macro never modify their inputs (except model)", then I understand. In which case another macro would make sense: |
|
Also, analogous to julia> a = [1]
1-element Vector{Int64}:
1
julia> b = a
1-element Vector{Int64}:
1
julia> @. b += [2]
1-element Vector{Int64}:
3
julia> a
1-element Vector{Int64}:
3 |
Thanks! I have been using PProf allocation tracker. I will check is there are other cool stuff there. |
The rewrite macros shouldn't change the semantics of the code that runs. They should strictly be a performance improvement only. That's why we're so careful to track which things are mutable (meaning we created the object, and can thus change it without worrying that the user holds a reference to it).
Ah now this is very subtle. Because you are updating the elements of |
This is a fair point, it is the JuMP rule. So I agree we should not do Still, a new macro might make sense. |
You mean this macro: julia> using JuMP
julia> macro add_to(expr)
@assert Meta.isexpr(expr, :+=)
x, y = expr.args
ret = Expr(:call, JuMP.add_to_expression!, x)
if Meta.isexpr(y, :call) && y.args[1] == :*
append!(ret.args, y.args[2:end])
else
push!(ret.args, y)
end
return esc(ret)
end
@add_to (macro with 1 method)
julia> model = Model()
A JuMP Model
├ solver: none
├ objective_sense: FEASIBILITY_SENSE
├ num_variables: 0
├ num_constraints: 0
└ Names registered in the model: none
julia> @variable(model, x)
x
julia> y = zero(AffExpr)
0
julia> @macroexpand @add_to y += 2 * x
:((JuMP.add_to_expression!)(y, 2, x))
julia> @macroexpand @add_to y += x
:((JuMP.add_to_expression!)(y, x))
julia> @macroexpand @add_to y += x / 2
:((JuMP.add_to_expression!)(y, x / 2)) This doesn't seem like it offers much benefit over the functional form. I think the bar for adding a new macro should really be: can this be achieved any other way? So far, our only utility macro is |
Besides: @add_to y += 2 * x A more sophisticated @add_to y += sum(x[i] for i in 1:N) sure you could: add_to_expression!(y, @expression(model, sum(x[i] for i in 1:N))) but you would be potentially allocating a massive array and then throwing it away. |
We don't have access to I think just no. I'm strongly against this. # Equivalent to `y += 2 * x`, but it modifies `y` in-place without allocating.
add_to_expression!(y, 2, x) |
Recording my latest idea here: @add_to_expression!(y, 2 * x)
@add_to_expression!(y, sum(x[i] for i in 1:N)) |
There doesn't seem much benefit compared to add_to_expression!(y, 2, x)
Okay, I understand the motivation for this one, but still: expr = @expression(model, sum(x[i] for i in 1:N))
add_to_expression!(y, expr) I tend to think that people writing |
I agree,
I agree in part.
|
Then do: for i in 1:N
add_to_expression!(y, x[i])
end
Yip. See p131 of https://www.osti.gov/servlets/purl/1771935 We could also just document |
The concrete proposal for discussion is a suggestion to add some variation of this comment: julia> using JuMP
julia> macro add_to(expr)
@assert Meta.isexpr(expr, :+=)
x, y = expr.args
ret = Expr(:call, JuMP.add_to_expression!, x)
if Meta.isexpr(y, :call) && y.args[1] == :*
append!(ret.args, y.args[2:end])
else
push!(ret.args, y)
end
return esc(ret)
end
@add_to (macro with 1 method)
julia> model = Model()
A JuMP Model
├ solver: none
├ objective_sense: FEASIBILITY_SENSE
├ num_variables: 0
├ num_constraints: 0
└ Names registered in the model: none
julia> @variable(model, x)
x
julia> y = zero(AffExpr)
0
julia> @macroexpand @add_to y += 2 * x
:((JuMP.add_to_expression!)(y, 2, x))
julia> @macroexpand @add_to y += x
:((JuMP.add_to_expression!)(y, x))
julia> @macroexpand @add_to y += x / 2
:((JuMP.add_to_expression!)(y, x / 2)) Pros:
Cons:
|
I would just do this with |
We can't do it in |
I see. So it's kind of like |
If we add something like this, I don't really think that MutableArithmetics is a good home for it. I'd like it to be user-friendly at the JuMP level. People using MA for performance can opt-in to Even if we add this, we still have to teach them to use it. And "add this extra package" is a large step (even if it is already a transitive dependency). |
I think the concern is that |
@lbonaldo any thoughts on this issue? |
My intuition is that performance and memory allocation are not something that users will immediately think about when getting started with JuMP or prototyping new models for a research project. They are likely to treat JuMP objects as regular variables without thinking much about the cost of operations between them. However, when dealing with very large models, these aspects become crucial. In our group, we're gradually transitioning to a more idiomatic JuMP style, thanks largely to @RuaridhMacd and your suggestions. We've been replacing all instances of the That’s just my intuition— I'm tagging @RuaridhMacd who has more experience and may have additional insights. |
If you're happy to use We can address the underlying issue with better profiling support. Even a documentation checklist of things to investigate could be useful. |
Thanks @lbonaldo Yes, I agree that 'add_to_expression!' and its derivatives are working well for GenX. It's no more difficult than += once people are aware of the issue |
yes, tutorials on how to effectively profile JuMP code and follow JuMP best practices would be very helpful. |
Are very common operations for JuMP users.
add_to_expression!
is there to solve the performance issue here.However, should we consider a macro to properly rewrite and keep readability?
It could also be
@update
,@operate
etcThis pattern is heavily used here: https://github.com/GenXProject/GenX.jl/blob/main/src/model/resources/vre_stor/vre_stor.jl
The text was updated successfully, but these errors were encountered: