Skip to content

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.

        )
    hades.register_process(
        process=PredefinedEventAdder(