Skip to content

Performance enhancements for Model.agents #2251

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

Merged
merged 32 commits into from
Aug 29, 2024
Merged

Conversation

quaquel
Copy link
Member

@quaquel quaquel commented Aug 27, 2024

This PR is a performance enhancement for Model.agents. It emerged out of a discussion on the weird scaling performance of the Boltzman wealth model.

Key changes

  • model.agents now returns the agentset as maintained by the model, rather than a new copy based on the hard references
  • agent registration and deregistration have been moved from the Agent into the model. The agent now calls model.register and model.deregister. This encapsulates everything cleanly inside the model class and makes Agent less dependent on the inner details of how Model manages the hard references to agents
  • the setup of the relevant datastructures is moved into its own helper method, again this cleans up code.

@quaquel quaquel requested a review from rht August 27, 2024 07:28
Copy link

Performance benchmarks:

Model Size Init time [95% CI] Run time [95% CI]
Schelling small 🔴 +77.0% [+76.3%, +77.6%] 🔵 -0.4% [-0.7%, -0.1%]
Schelling large 🔴 +94.2% [+92.9%, +95.3%] 🔵 -0.8% [-2.9%, +0.7%]
WolfSheep small 🔴 +130.1% [+128.1%, +131.9%] 🟢 -97.5% [-97.5%, -97.4%]
WolfSheep large 🔴 +130.8% [+129.1%, +132.4%] 🟢 -99.9% [-99.9%, -99.9%]
BoidFlockers small 🔴 +83.6% [+82.2%, +84.7%] 🔵 -0.3% [-1.0%, +0.6%]
BoidFlockers large 🔴 +83.1% [+82.0%, +84.1%] 🔵 -0.6% [-1.2%, +0.0%]

Copy link
Member

@EwoutH EwoutH left a comment

Choose a reason for hiding this comment

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

Thanks, looks interesting!

@EwoutH
Copy link
Member

EwoutH commented Aug 27, 2024

@quaquel can you reproduce the speedup of WolfSheep locally?

Edit: on of the benchmark models also gives a warning now:

FutureWarning: The Mesa Model class was not initialized. In the future, you need to explicitly initialize the Model by calling super().init() on initialization.
self.model.register_agent(self)

@quaquel
Copy link
Member Author

quaquel commented Aug 27, 2024

Performance benchmarks:

Model Size Init time [95% CI] Run time [95% CI]
Schelling small 🔴 +77.0% [+76.3%, +77.6%] 🔵 -0.4% [-0.7%, -0.1%]
Schelling large 🔴 +94.2% [+92.9%, +95.3%] 🔵 -0.8% [-2.9%, +0.7%]
WolfSheep small 🔴 +130.1% [+128.1%, +131.9%] 🟢 -97.5% [-97.5%, -97.4%]
WolfSheep large 🔴 +130.8% [+129.1%, +132.4%] 🟢 -99.9% [-99.9%, -99.9%]
BoidFlockers small 🔴 +83.6% [+82.2%, +84.7%] 🔵 -0.3% [-1.0%, +0.6%]
BoidFlockers large 🔴 +83.1% [+82.0%, +84.1%] 🔵 -0.6% [-1.2%, +0.0%]

I'll try to figure out what is happening with the init times here. Its probably overhead from setting up the additional datastructures, which we get back when running the model (at least for wolf sheep?).

@Corvince
Copy link
Contributor

Thanks for this PR, I like the way it encapsulates the logic much better.

I think it is a good default to return the actual agentset and not just a copy. I think this is actually more intuitive.

Regarding the copy function, doesn't agentset.select() already do that? I'm not arguing against adding an additional method but I just want to mention that it's already possible and it should rightfully be discussed separately from this PR

@quaquel
Copy link
Member Author

quaquel commented Aug 27, 2024

Regarding the copy function, doesn't agentset.select() already do that? I'm not arguing against adding an additional method but I just want to mention that it's already possible and it should rightfully be discussed separately from this PR

Yes, select does this as a corner case. But fair enought to seperate copy into a seperate PR

@quaquel
Copy link
Member Author

quaquel commented Aug 27, 2024

@quaquel can you reproduce the speedup of WolfSheep locally?

Reran benchmarks locally multiple times. Behavior is rather varied. In general inits are allways up, but not by the same percentage as shown above. Typicaly it is between 5% and 15% slower init times. I cannot see the massive speedups on wolf sheep. Rather, I see sometimes mosest increases or modest decreases. I am going to dig a bit more to see what is going on.

Copy link

Performance benchmarks:

Model Size Init time [95% CI] Run time [95% CI]
BoltzmannWealth small 🔴 +16.5% [+14.0%, +19.0%] 🟢 -12.1% [-12.2%, -11.9%]
BoltzmannWealth large 🔴 +47.5% [+21.2%, +84.9%] 🟢 -17.8% [-20.0%, -15.5%]
Schelling small 🔴 +10.5% [+10.1%, +10.9%] 🔵 -0.7% [-1.0%, -0.4%]
Schelling large 🔴 +13.7% [+12.8%, +14.7%] 🔵 +0.6% [-1.5%, +2.6%]
WolfSheep small 🔴 +21.8% [+20.3%, +23.3%] 🔵 -0.2% [-3.6%, +3.3%]
WolfSheep large 🔴 +20.0% [+19.1%, +20.9%] 🔵 +4.3% [+2.1%, +6.7%]
BoidFlockers small 🔴 +12.9% [+12.0%, +13.8%] 🔵 +0.6% [-0.2%, +1.2%]
BoidFlockers large 🔴 +12.4% [+11.7%, +13.2%] 🔵 -0.1% [-0.3%, +0.1%]

@property
def agent_types(self) -> list[type]:
"""Return a list of different agent types."""
return list(self.agents_.keys())
return list(self._agents_by_type.keys())
Copy link
Contributor

Choose a reason for hiding this comment

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

The previous and the new change have different meaning altogether (list of all type of each agents vs list(set(agent types))). I suppose the docstring is ambiguous even though it is closer to the former meaning.

Copy link
Member Author

Choose a reason for hiding this comment

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

That's not correct. In the old version self.agents_ was the exact same datastructure as what is now _agents_by_type. In the old version: self.agents_: defaultdict[type, dict] = defaultdict(dict). In the new version: self._agents_by_type: dict[type, AgentSet] = {}. So, in either case, keys is a list of types.

Copy link
Contributor

Choose a reason for hiding this comment

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

That means it still stands that the docstring is not clear enough.

Copy link
Member Author

Choose a reason for hiding this comment

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

This docstring has not been changed by me in this PR, so it is already in the existing code. If you suggest changing it, that would be fine, but why would that hold up this PR?

Also, what do you suggest changing? The return is a list, and this list contains the types of the agents in the model. The only, minor, ambiguity is that it is a list of the unique types of agents, not the type for each agent in the model.

@EwoutH
Copy link
Member

EwoutH commented Aug 28, 2024

Let me review it in depth tonight (I'm trying to stay in the model-building flow)

@EwoutH
Copy link
Member

EwoutH commented Aug 28, 2024

Do we need to modernize BoltzmannWealth first to see the performance changes? Because 20% speedup in the large case with 10.000 agents doesn't seem that much.

@quaquel
Copy link
Member Author

quaquel commented Aug 28, 2024

This version of the model is on a grid. #2224 is with all agents in the model. So, I am not sure what to expect in the first place.

@EwoutH
Copy link
Member

EwoutH commented Aug 28, 2024

Yeah so the Grid operations might be the bottleneck. Sounds logical.

I will try to update the benchmarks later tonight or tomorrow, and then we can bring this one in.

I'm also curious if we can speed up shuffle() at some point. With a model with 60.000 agents it's getting slow (also with inplace=True), but most notably, it's about the only AgentSet operation that's getting slow.

@quaquel
Copy link
Member Author

quaquel commented Aug 28, 2024

I'm also curious if we can speed up shuffle() at some point. With a model with 60.000 agents it's getting slow (also with inplace=True), but most notably, it's about the only AgentSet operation that's getting slow.

In the case of inplace=True, the slowdown happens either when creating the list from the keys, when applying random.shuffle, or rebuilding the dict from the shuffled keys. None of these is easy to speedup any further.

Copy link
Member

@EwoutH EwoutH left a comment

Choose a reason for hiding this comment

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

  • I really like the model.register / deregister. It's very clear and cleanly separated, and the right way around.
  • model.agents now is the ground truth right?
  • I would really appreciate an overview of all the variables and private variables we now keep around agents, agent types and AgentSets.

mesa/model.py Outdated
Comment on lines 118 to 120
self._agents = {}
self._agents_by_type: dict[type, AgentSet] = {}
self._all_agents = AgentSet([], self)
Copy link
Member

@EwoutH EwoutH Aug 28, 2024

Choose a reason for hiding this comment

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

Okay, so now we're keeping 4 records of all our agents (if we count self.agents)? Could you explain a bit why each is needed?

I think this can be a place where some performance is lost, in models that frequently kill and create agents (like wolf-sheep)?

Copy link
Member Author

Choose a reason for hiding this comment

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

self._agents contains the hard refs, so this is essential.

self._all_agents is the agentset with all agents. The main performance motivation for this PR. So again essential.

The only one you could debate is agents_by_type. This you could also do via self._all_agents.groupby(types).groups and thus as a property. What is better from a performance standpoint is hard to say. adding and removing stuff from dicts is relatively cheap to do.

Copy link
Member

Choose a reason for hiding this comment

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

Thanks, appreciated. Could you add this in a comment or docstring behind/above each?

self.agents is the only thing we formally expose, all others are private and use-at-you-own risk.

For now I see the use case of a separate _agents_by_type. We can dive into the performance later.

And at least we got rid of self.agents_!

Copy link
Member Author

Choose a reason for hiding this comment

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

self.agents is the only thing we formally expose, all others are private and use-at-you-own risk.

Which is exactly why I moved everything else into private. It keeps the public side the same as before.

I'll add a few comments on the datastructures later today

@quaquel
Copy link
Member Author

quaquel commented Aug 28, 2024

  • model.agents now is the ground truth right?

no, model._agents is the hardref dict and the ground truth.

  • I would really appreciate an overview of all the variables and private variables we now keep around agents, agent types and AgentSets.

can we wait with that until we remove all the old 2.x stuff from the model?

@EwoutH EwoutH added trigger-benchmarks Special label that triggers the benchmarking CI and removed trigger-benchmarks Special label that triggers the benchmarking CI labels Aug 28, 2024
Copy link

Performance benchmarks:

Model Size Init time [95% CI] Run time [95% CI]
BoltzmannWealth small 🔴 +31.7% [+29.3%, +34.0%] 🟢 -12.2% [-12.3%, -12.0%]
BoltzmannWealth large 🔴 +53.7% [+25.6%, +93.0%] 🟢 -16.4% [-18.6%, -14.1%]
Schelling small 🔴 +10.3% [+9.9%, +10.7%] 🔵 +0.4% [+0.2%, +0.6%]
Schelling large 🔴 +14.4% [+13.8%, +15.1%] 🔵 +1.7% [+0.9%, +2.4%]
WolfSheep small 🔴 +23.8% [+22.3%, +25.3%] 🔵 +0.6% [-2.8%, +4.1%]
WolfSheep large 🔴 +23.6% [+22.6%, +24.6%] 🔴 +10.1% [+9.2%, +10.8%]
BoidFlockers small 🔴 +11.0% [+10.4%, +11.5%] 🔵 -0.0% [-0.8%, +0.7%]
BoidFlockers large 🔴 +9.2% [+8.4%, +10.1%] 🔵 +0.5% [-0.1%, +1.0%]

@quaquel quaquel removed the 2 - WIP label Aug 29, 2024
@quaquel quaquel merged commit de01c3f into projectmesa:main Aug 29, 2024
10 of 12 checks passed
@quaquel quaquel deleted the boltzman branch August 29, 2024 07:01
@EwoutH EwoutH added the deprecation When a new deprecation is introduced label Aug 30, 2024
@EwoutH EwoutH added enhancement Release notes label and removed breaking Release notes label labels Aug 30, 2024
EwoutH pushed a commit to EwoutH/mesa that referenced this pull request Sep 24, 2024
This PR is a performance enhancement for Model.agents. It emerged from a discussion on [the weird scaling performance of the Boltzman wealth model](projectmesa#2224).

model.agents now returns the agentset as maintained by the model, rather than a new copy based on the hard references
agent registration and deregistration have been moved from the Agent into the model. The agent now calls model.register and model.deregister. This encapsulates everything cleanly inside the model class and makes Agent less dependent on the inner details of how Model manages the hard references to agents
the setup of the relevant datastructures is moved into its own helper method, again, this cleans up code.
@EwoutH
Copy link
Member

EwoutH commented Sep 24, 2024

Should register_agent() and deregister_agent() be private methods?

@quaquel
Copy link
Member Author

quaquel commented Sep 24, 2024

If you want to get technical, I guess in a java sense they would be protected rather than private. They are not private because that would mean that agent could not call model.register_agent. They are not public in the sense of user facing.

@Corvince
Copy link
Contributor

In Python speak, however, it is either a public or an internal method. Public is everything user-faced, documented and guaranteed to be stable. So if there isn't any user facing use case for these functions I think they should indeed be made non-public by a leading underscore. This means its meant to be used internally, which is exactly whats happening here.

@EwoutH
Copy link
Member

EwoutH commented Sep 24, 2024

Thanks for the explanation Jan, and I agree with Corvince.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
deprecation When a new deprecation is introduced enhancement Release notes label Performance
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants