Improving Performance
With a lot of simulations, performance will be a core concern. Hades is built with performance in mind but with a primary focus on IO-bound workloads rather than CPU-bound ones.
Hades runs on a single thread, but using asyncio
, event notifications happen concurrently.
This means that depending on what the notify()
method does on the processes within the simulation, it may be far quicker than a multi-processing approach using all of your CPU cores, or a bit slower than running synchronously.
Let's take two examples
CPU Bound¶
As you might notice in the following example, none of the methods called when a Boid
process (from the boids example) reacts to a BoidMoved
event, are async
flavoured.
case BoidMoved(t=t, boid_id=boid_id, movement=movement):
self._other_boid_positions[boid_id] = movement
if boid_id == self._boid_identifier:
# We will only get this once per time step so its our opportunity to move!
if not self._movement:
self._movement = BoidMovement(**movement.dict())
self._move_to_target_worm()
self._cohere()
self._ensure_seperation()
self._align()
self._slow_down()
self._movement.move(grid_size=self._grid_size)
if self._target_worm:
worm_id, worm_position = self._target_worm
if self._movement.distance(worm_position) < self._worm_eat_distance:
_logger.info(f"worm {worm_id} eaten by boid {self._boid_identifier}")
self.add_event(
WormEaten(
t=t + 1,
worm_id=worm_id,
boid_id=self._boid_identifier,
)
)
self.add_event(
BoidMoved(t=t + 1, boid_id=boid_id, movement=ImmutableMovement(**self._movement.dict()))
)
return NotificationResponse.ACK
This means that we will get no speed up from running them concurrently in an asyncio.gather
. An approach utilising multiple CPU cores or at least not slowing stuff down by creating coroutines etc may be faster here.
However, CPU bound tasks may still benefit from the Hades approach. After all there is a limit to the number of cores likely to be present on a physical machine vs. on any machine over the network!
We could, for example, implement an API endpoint which takes the BoidMoved
event over HTTP and does all the processing to return another event. We could then scale to millions of Boids being handled in a reasonable time frame!
IO Bound¶
IO Bound tasks are Hades' bread and butter. When there are multiple IO-bound things being done through asyncio
by separate processes during a timestep, or even by the same process, but in response to a different event, they will all be done concurrently before moving to the next timestep.
Note
Concurrent handling of events within the same process does have some things to be careful of (see process)
else:
return ""
async def notify(self, event: Event):
match event:
case SimulationStarted():
motives = await plaintext_chat_response(
messages=[
GPTMessage(
role=GPTMessageRole.USER,
content=f""""
Describe the core motives of {self._name} at the start of the Odyssey in less than 200 words.""",
)
]
)
print(f"{self._name} Motivations:\n{motives}")
self._motives = motives
return NotificationResponse.ACK
case StoryUnfolded() as e:
response = await plaintext_chat_response(
messages=[
GPTMessage(
role=GPTMessageRole.SYSTEM,
content=god_prompt.format(
god_name=self._name, motives=self._motives, day=e.t, history=self.history_message
),
),
GPTMessage(
role=GPTMessageRole.USER,
content=e.chapter,
),
]
)
self._history.append(str(e))
if response:
event = CharacterActed(t=e.t, action=response, character_name=self._name)
Optimising for Performance¶
Apart from ensuring you are taking advantage of async
implementations for IO bound tasks within processes (e.g. httpx
instead of requests
), there are a number of other performance optimisations you can make in terms of configuring Hades
.
These arguments are detailed in Hades, and mostly involve removing some non-essential functionality to give better performance.
These are used to speed things up in the boids example.