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

Check performance of multi-agent models for dynamic dispatch #445

Open
Datseris opened this issue Feb 28, 2021 · 25 comments
Open

Check performance of multi-agent models for dynamic dispatch #445

Datseris opened this issue Feb 28, 2021 · 25 comments
Labels
hard multi-agent Related with multi-agent models performance

Comments

@Datseris
Copy link
Member

Datseris commented Feb 28, 2021

EDIT: See #445 (comment) for a potential solution!!!


In Julia when iterating over a non-concretely typed container (e.g. Dict{Int, Union}) the iterables are not type stable. If they are given to a function (e.g. agent_step!) Julia does dynamic dispatch instead of static, which is a big hit for performance.

We should see if our simple Wolf/Sheep/Grass model does dynamic or static dispatch. Set up the model in a way that no agents will be created or killed, but only moved and change values. Be sure that no structs/arrays are created during model evolution.

Then, benchmark the following:

sheeps = collect_all_model_sheeps
wolfs = # same
grasses = #same 

function custom_step(sheeps, wolfs, grasses, model)
    for s in sheeps; agent_step!(s, model); end
    for w  in wolfs; agent_step!(w, model); end
    for g in grasses; agent_step!(g, model); end
end

and compare it with:

function our_step(model)
    for a in schedule(model)
         agent_step!(a, model) # uses Multiple Dispatch
    end
end

and check the allocation and time differences between the two. Do some schedulers (e.g. the scheduling by Type) improve this?

Should we re-work the internals of step! for multi-agent models so that it explicitly separates agents into types first? Is it worth it? Otherwise, we can simply put a note on the "Performance tips" section of the docs of users writing their own custom model stepping function that collects each agent type first and then steps each one individually.

(this Performance section is still to be written and will also have stuff like immutable types, and using a single agent type for maximal performance)

@Libbum
Copy link
Member

Libbum commented Feb 28, 2021

Sounds like a decent plan. If it works we can make a macro to do the step expansion, so worth it: probably. Depending on how much speedup we see. But it shouldn't take too much effort at least.

@AayushSabharwal
Copy link
Collaborator

AayushSabharwal commented Mar 11, 2021

I benchmarked this, using the following setup:
https://gist.github.com/AayushSabharwal/f5870d837635a4510c41e57087b31a64

julia> include("src\\predator-prey-check.jl")
julia> m = initialize_model()
AgentBasedModel with 550 agents of type Union{Grass, Sheep, Wolf}
 space: GridSpace with size (20, 20), metric=chebyshev and periodic=false
 scheduler: by_union
julia> sheep = [a for a in allagents(m) if typeof(a) == Sheep];
julia> wolves = [a for a in allagents(m) if typeof(a) == Wolf];
julia> grass = [a for a in allagents(m) if typeof(a) == Grass];
julia> function customstep(s, w, g, model)
           for x in s; agent_step!(x, model); end
           for x in w; agent_step!(x, model); end
           for x in g; agent_step!(x, model); end
       end
customstep (generic function with 1 method)

julia> @btime customstep($sheep, $wolves, $grass, $m)
  87.799 μs (1351 allocations: 120.19 KiB)

julia> function ourstep(model)
           for a in Agents.schedule(model)
               agent_step!(model[a], model)
           end
       end
ourstep (generic function with 1 method)

julia> @btime ourstep($m)
  195.000 μs (2553 allocations: 189.20 KiB)

ourstep takes a little over double the time

@Libbum
Copy link
Member

Libbum commented Mar 11, 2021

Good to know how this scales with time as well, but need to be a bit careful with wolf-sheep, since it has the possibility to collapse a population a certain percentage of the time and therefore run much quicker—skewing the results.

Here's the same setup as above, but using this benchmark call.

using Test
m = initialize_model()
sheep = [a for a in allagents(m) if typeof(a) == Sheep];
wolves = [a for a in allagents(m) if typeof(a) == Wolf];
grass = [a for a in allagents(m) if typeof(a) == Grass];
function customstep(s, w, g, model)
for t in 1:50
    for x in s; agent_step!(x, model); end
    for x in w; agent_step!(x, model); end
    for x in g; agent_step!(x, model); end
end
end
function ourstep(model)
for t in 1:50
    for a in Agents.schedule(model)
        agent_step!(model[a], model)
    end
end
end
n = deepcopy(m)

@btime customstep($sheep, $wolves, $grass, $m) teardown = (@test count(i -> i isa Sheep, allagents(m)) > 0 &&
       count(i -> i isa Wolf, allagents(m)) > 0)
# 4.812 ms (60435 allocations: 5.34 MiB)

@btime ourstep($n) teardown = (@test count(i -> i isa Sheep, allagents(n)) > 0 &&
       count(i -> i isa Wolf, allagents(n)) > 0)
# 11.928 ms (120595 allocations: 8.71 MiB)

So yes, looks like it'll be good to write a macro for this.

@Datseris
Copy link
Member Author

Why a macro? We can do this via standard dispatch. The default scheduler changes if the type AgentType is a Union, and step! can also change accordingly to do what customstep does.

@Datseris
Copy link
Member Author

Importantly, we should do this for the paper, as this model is one we are not much more performant than the competitors

@Libbum
Copy link
Member

Libbum commented Mar 11, 2021

Ah, so you suggest making by_type the default if we're a mixed model, then use that order. I guess what I'm not following there is for the moment we do the ordering and vcat the result. Calling the step function then just gives is this list of agents via the scheduler. Even if this list is ordered, using the standard step doesn't work. We need to use the customstep layout.

Is the suggestion here to not do the vcat, and instead return a list of lists over each agent type, then splat that into our step function? My suggestion of the macro was just since we don't know for sure how many separate agent types we will have.

Paper: yeah, that already crossed my mind.

@Datseris
Copy link
Member Author

We can't be using by_type as a vector of mixed agents is still type unstable. We must separate the list to ids per type. However, we can still use by_type.

My suggestion is to use multiple dispatch to make step! behave like customstep. This is possible. Then, by_type is not called explicitly, but instead agent ids are grouped to their type and used in customstep. You can do multiple dispatch based on function types.

E.g. f(x, y) = y(x) and f(x, ::typeof(rand)) = rand().

@itsdfish
Copy link
Contributor

Last year I did some benchmarking to investigate the performance cost of mixed types. The benchmark varied the number of types up to 15 while holding constant the number of interactions. Something along those lines might be useful for understanding how performance scales with the number of agent types. Another factor that could be important is the time required for each interaction. My best guess is that the performance hit becomes less noticeable as the time required to compute an interaction increases.

Also, I came across TypeSortedCollections some time ago, which seems to automate the process of grouping mixed collections by type.

@Libbum
Copy link
Member

Libbum commented Mar 21, 2021

Looks interesting! Will certainly take a deeper look.

@Datseris
Copy link
Member Author

@Libbum
Copy link
Member

Libbum commented Mar 25, 2021

That looks pretty awesome.

@Datseris
Copy link
Member Author

@Libbum would you please be so kind and tell me in your comment #445 (comment) what is initialize_model()?

@Datseris
Copy link
Member Author

@Libbum
Copy link
Member

Libbum commented Mar 29, 2021

what is initialize_model()?

It's the one from Aayush's gist: https://gist.github.com/AayushSabharwal/f5870d837635a4510c41e57087b31a64

don't know how to dispatch on union types

This is why I was thinking a macro may be a solution here, we can use our union_types method to assist.

@Datseris
Copy link
Member Author

Our union_types is not type stable:

image

Even though I've implemented my suggestion here, it doesn't improve performance because I can't get the individual agent types in a type-stable manner.

@Datseris
Copy link
Member Author

I am thinking how deep we should go here. I even though that we should have a tuple of dictionaries where each dictionary has only agents of one type... This goes too far though, as it would take tremendous amount of extra source code, simply because of the scheduling... Even the most basic schedulers like random activation would have to be separated into generating vectors of vectors, etc. The scheduler API would become much more complex for multi-agent modes, etc...

If you see #472 I simply suggest to not use multi-agent versions for performance critical code. I stand that it is probably to our benefit to keep the current status quo, if my branch multiagent does not lead anywhere. We're already faster than anyone else, and also simplest than anyonelese. Sacrificing simplicity to make us even faster might not be the best idea.

@Libbum
Copy link
Member

Libbum commented Mar 31, 2021

I'm confused about this point. There's a working solution above, are you saying it's impossible to generalise?

I don't mind adding a discussion to the docs either way, but struggle to think why we can't get the custom_step working.

@Datseris
Copy link
Member Author

Datseris commented Mar 31, 2021

There's a working solution above, are you saying it's impossible to generalise?

Yeah it's working because we you can do:

sheep = [a for a in allagents(m) if typeof(a) == Sheep];
wolves = [a for a in allagents(m) if typeof(a) == Wolf];
grass = [a for a in allagents(m) if typeof(a) == Grass];

and you write explicitly the types. Getting these types form the Union in a type-stable manner is something I have NOT been able to do yet.

What we can do is alter the code we have now and add a new entry to the model struct that is a tuple of the agent types. Then this type-unstable operation is done once at model creation and does not propagate further down the line.

@Datseris
Copy link
Member Author

Datseris commented Mar 31, 2021

My two discourse posts based on my work so far on this:

https://discourse.julialang.org/t/iterating-through-types-of-a-union-in-a-type-stable-manner/58285/2

https://discourse.julialang.org/t/dispatch-on-precice-union-instance-in-place-of-subtyping-supertype-of-union/58117

Seems like our current approach of using Unions is not good in general. However it plays well with the schedulers and the fact that we map IDs to Agents.

@Libbum
Copy link
Member

Libbum commented Mar 31, 2021

Geez. Rough!

What we can do is alter the code we have now and add a new entry to the model struct that is a tuple of the agent types. Then this type-unstable operation is done once at model creation and does not propagate further down the line.

That sounds like a good way forward. I'm really not sure how we'd get around the Union approach right now.

@Datseris
Copy link
Member Author

I'm really not sure how we'd get around the Union approach right now.

I'm genuinely not sure how much effort we should be spending into it... In the sense that for really complex models, the user might go the only having model_step! route. There, if they themselves do not make proper scheduling, then all of our effort will be wasted.

Perhaps the best thing here is to document how to do it properly without internal magic, and showcase this with the wolf-sheep model.

@Datseris
Copy link
Member Author

Datseris commented Apr 1, 2021

This is an extremely difficult issue. We have to spend a lot of effort and come up with a fundamentally different internal design to really allow performant multi-agent types the way we do it now.

I'll wrap up the documentation indications for this and Pull Request them. Then we should open a different issue discussing design. Quite large project, I don't feel I can do it at the moment.

@Datseris
Copy link
Member Author

BIG NEWS HERE: https://discourse.julialang.org/t/ann-virtual-jl-optimizing-dynamic-calls-away-through-virtual-multiple-dispatch/84413

I think this package is probably the way to go to solve our problem!!! We need to test it!

@Tortar
Copy link
Member

Tortar commented Aug 27, 2023

there is a another problem which is much more impacting in terms of performance related to all of this: with a union of types all operations which require to look at an agent inside the model will be type-unstable: consider for example that you do a nearby search for an agent and then you iterate over those agents: the loop will be type unstable since you do model[id] which is type unstable. As another example also collect(nearby_agents(...)) will be problematic. This is why both https://github.com/JuliaDynamics/Agents.jl/blob/main/test/performance/branching_faster_than_dispatch.jl and https://github.com/JuliaDynamics/Agents.jl/blob/main/test/performance/variable_agent_types_simple_dynamics.jl are actually slow in the multitype version. The dynamic dispatch on the step! function has a negligible effect in comparison.

@Tortar
Copy link
Member

Tortar commented Aug 27, 2023

This is why we should be really try to implement #841. This won't solve all problems but with some attention from the user side it will be possible to have (almost) type stable operations.

@Datseris Datseris added the multi-agent Related with multi-agent models label Aug 27, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
hard multi-agent Related with multi-agent models performance
Projects
None yet
Development

Successfully merging a pull request may close this issue.

5 participants