The previous post covered process pools for CPU-bound work. I have modified the Arbitrum bot project to utilize this process pool approach. The implementation at the arb helper level is a bit hacky, but the performance improvements too good to delay sharing them.
Much of the bot structure is unchanged, but the activate_arbs
coroutine has been modified heavily. The primary improvements are within that coroutine and the three functions that handle arbitrage calculation updates:
_activate_and_record
_reactivate
calculate_arb_in_pool
The first two are private functions within activate_arbs
, and the third is a global function that is triggered by the long-running process_onchain_arbs
task.
They achieve the fabled multi-core operation by creating tasks via the asyncio loop’s run_in_executor
method.
Here is an example:
async def _reactivate(arbs: List):
loop = asyncio.get_running_loop()
_tasks = [
loop.run_in_executor(
executor=process_pool,
func=arb_helper.calculate_arbitrage_return_best,
)
for arb_helper in arbs
if arb_helper.auto_update()
]
for task in asyncio.as_completed(_tasks):
try:
arb_id, best = await task
except bot.exceptions.ArbitrageError:
pass
except Exception as e:
print(f"(_reactivate as_completed) catch-all: {e}")
print(type(e))
else:
arb_helper = active_arbs[arb_id]
arb_helper.best = best
It works by creating tasks, stored inside a _tasks
list, that will run the calculate_arbitrage_return_best
method inside a process pool. These tasks are managed by the asyncio event loop behind the scenes, and we instruct the function to process them as they are completed using asyncio.as_completed
. Then the task is awaited, which will return an arbitrage ID and the best
dictionary that the arbitrage helper generated. Or it will raise an exception! Either way, the results are handled appropriately in the except/else
blocks.
Since these results are calculated in a separate process pool with a pickled/unpickled object, it loses connection with the original. Thus we need a way to identify which helper the result is associated with, so the return value of calculate_arbitrage_return_best
is a tuple containing the helper ID. That helper ID can be used to look up the “in scope” object within the active bot, and those results can be simply updated in place.
The other functions are similar from the perpective of the process pool, but differ slightly in how they handle the results after they are retrieved. Previously-activated arbitrage helpers are moved directly to active_arbs
, whereas newly-activated arbitrage helpers are moved to active_arbs
and their values recorded in the “whitelist” for faster activation next time. calculate_arb_in_pool
is called on already activated arbs, so no special action is taken besides retrieving and updating the results.
By default, this bot is set up to use 16 workers in the process pool (16 CPU cores). If you want to use more or less, edit the constructor in the main
coroutine. An empty call to ProcessPoolExecutor()
will set the value automatically, appropriate for your system.
arbitrum_cycle_improved.py