-
Notifications
You must be signed in to change notification settings - Fork 8
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
Reducing memory footprint #121
Comments
Is this related to what we removed for type stability of the code that uses ACSets with attribute variables? @kris-brown, @mehalter do you remember what issue/PR that is associated with? I couldn't find it with the issue search. |
The PR that added variables to ACSets is here: AlgebraicJulia/Catlab.jl#740 |
I've also noticed strange cases where the type should have been inferred but I got |
It would be good to track this down. From earlier discussions with Sean, it sounds like we had a performance regression on basic acset operations at some point. |
I thought we replaced views with copies at one point to handle the case of Variable Attributes. I remember that we decided to live with a performance problem because the usability of having to convert everything you accessed was too high. |
We did replace views with copies for vectorized access to subparts. But the elementwise assignments in the OP should be fast and they shouldn't allocate since all the needed info is available at compile time (being hardcoded symbols). I think it's a regression that they are doing all that allocation, though I don't know when the regression happened. |
@GeorgeR227 is going to hit bisect to find the root cause and then we can see what to do and fix it. |
Great! |
From what I can see, it seems like this issue has always persisted in ACSets.jl. I've gone back to 0915821 when using ACSets
SchTest = BasicSchema([:V,:E], [(:v0,:E,:V)])
@acset_type Test(SchTest, index=[:v0])
s = Test()
add_part!(s, :V)
add_part!(s, :E)
s[1, :v0] = 1
@time s[1, :v0] = 1
v0 = @view s[:v0]
v0[1] = 1
@time v0[1] = 1 and here are the results from then: The first is the regular ACSet setting while the second is the What's interesting is that this allocation size is actually 64 bytes on the most recent commit and I've tracked it down to fb4e036 as to when the size of this allocation increased from 48 to 64 bytes. |
Thanks for looking into this. It's possible that the regression happened before the acset code was migrated out of Catlab and into its own package. Actually, investigating further, I believe the problem is upstream. Run this code: using ACSets
@inline my_set_subpart!(acs::SimpleACSet, part, name::Symbol, subpart) =
_set_subpart!(acs, part, Val{name}, subpart)
@inline _set_subpart!(acs::SimpleACSet, part, ::Type{Val{name}}, subpart) where name =
acs.subparts[name][part] = subpart
SchTest = BasicSchema([:V,:E], [(:v0,:E,:V)])
@acset_type Test(SchTest, index=[:v0])
s = Test()
add_part!(s, :V)
add_part!(s, :E)
_set_subpart!(s, 1, Val{:v0}, 1)
@time _set_subpart!(s, 1, Val{:v0}, 1)
my_set_subpart!(s, 1, :v0, 1)
@time my_set_subpart!(s, 1, :v0, 1) I get the following result: julia> _set_subpart!(s, 1, Val{:v0}, 1)
1
julia> @time _set_subpart!(s, 1, Val{:v0}, 1)
0.000007 seconds
1
julia> my_set_subpart!(s, 1, :v0, 1)
1
julia> @time my_set_subpart!(s, 1, :v0, 1)
0.000011 seconds (2 allocations: 96 bytes) It appears that the compiler is not propagating the constant symbol |
I can confirm the issue is a type propagation issue, I've modified the code slightly to enforce the type we expect and now see no allocations. julia> @inline my_set_subpart!(acs::SimpleACSet, part, name::Symbol, subpart) =
_set_subpart!(acs, part, Val{name}::Type{Val{:v0}}, subpart)
my_set_subpart! (generic function with 1 method)
julia> my_set_subpart!(s, 1, :v0, 1)
1
julia> @time my_set_subpart!(s, 1, :v0, 1)
0.000014 seconds
1 Here's the julia> @code_warntype my_set_subpart!(s, 1, :v0, 1)
MethodInstance for my_set_subpart!(::Test, ::Int64, ::Symbol, ::Int64)
from my_set_subpart!(acs::SimpleACSet, part, name::Symbol, subpart) @ Main REPL[95]:1
Arguments
#self#::Core.Const(my_set_subpart!)
acs::Test
part::Int64
name::Symbol
subpart::Int64
Body::Int64
1 ─ nothing
│ %2 = Core.apply_type(Main.Val, name)::Type{Val{_A}} where _A
│ %3 = Main.Type::Core.Const(Type)
│ %4 = Core.apply_type(Main.Val, :v0)::Core.Const(Val{:v0})
│ %5 = Core.apply_type(%3, %4)::Core.Const(Type{Val{:v0}})
│ %6 = Core.typeassert(%2, %5)::Core.Const(Val{:v0})
│ %7 = Main._set_subpart!(acs, part, %6, subpart)::Int64
└── return %7 |
I investigated this issue a bit, and it seems that constant-propagation for julia> call_my_set_subpart!(acs::SimpleACSet, part, subpart) =
my_set_subpart!(acs, part, :v0, subpart);
julia> call_my_set_subpart!(s, 1, 1);
julia> @time call_my_set_subpart!(s, 1, 1);
0.000004 seconds The real problem appears to be that constant-propagation for |
So would making the caller use a value type like |
I guess we could also generate singleton types when we make the ACSet instance and then have the user import those as module level constants. What is the difference between Val{s} where s is a Symbol and a singleton type called s? |
I investigated the issue a bit further and it seems possible to solve the problem without those workarounds. Firstly, the allocations occurring in the reduced example ( Going back to the original issue, the initial problem reported in this issue seems to be due to the lack of constant-propagation for |
These functions can be significantly optimized with constant information available within their arguments, so it would be beneficial to enforce constant propagation for them. This can be achieved using `Base.@constprop :aggressive` or `@inline`, and since these functions are likely to be used frequently, I think promoting inline expansion with `@inline` is a good approach, so I chose to use `@inline`. - fixes AlgebraicJulia#121
xref #133 |
@aviatesk, thanks so much for helping us diagnose and fix this issue! I got thrown off track by the interaction between the top-level call and the |
It seems like some of basic ACSet fuctionality, like index setting, has a much larger than expected memory footprint.
I've been writing various performance-oriented functions for CombinatorialSpaces.jl and a main way I've gotten around this is that instead of using the ACSet indexing, I've just grabbing entire views of ACSets columns at a time and indexing that view. Below is a small example demonstrating the difference:
Below is the result of running this code. We can see that the view uses substantially less memory and thus also speeds up the code by a lot.
While I would like to use the nice ACSet interface, performance-wise I keep having to avoid it. While all of my examples here relate to the CombinatorialSpaces package, I think the issue itself stems from the underlying ACSet fuctionality, since the meshes used there are built on it.
The text was updated successfully, but these errors were encountered: