Great, thanks for the detailed explanation, it’s really helpful!
You definitely seem to know what you’re doing! Here are a few comments in case you haven’t thought about it already:

The way you “bind” a PyTorch future to an asyncio future, through add_done_callback, works well for successful RPCs but it might not work in case of errors, as calling fut.value() would raise an exception, and this f.set_result would never be called. In those cases your asyncio future would never become complete and any piece of code that’s awaiting it might remain stuck forever, leading to a deadlock.
By running asyncio.get_event_loop().run_until_complete(...) (which BTW I think can be shortened to just asyncio.run(...) starting in Python 3.7) you block the calling thread until all asyncio tasks launched within that loop are complete. The thread, in this case, comes from RPC’s internal pool, which has only a limited number of threads. This means that you can only have so many active RPC calls at the same time. This may be exactly what you want, but another way of doing this could be for you to manually start a dedicated single thread for the asyncio loop, and have all incoming RPC calls schedule a task to that thread. If you use the async_execution decorator you can also create a RPC future, have your asyncio task complete that future at a later point in time, and have your RPC function return immediately. This way you could release the thread to the RPC pool while your asyncio thread continues processing the task, meaning the RPC thread could be reused for another request. At the end, it means your asyncio thread would become able to handle an arbitrarily large number of requests at the same time.