In this post, we would like to discuss the interface of relax build and runtime UX. The following code summarizes the current set of use-cases.
# build and directly run
ex: relax.Executable = relax.vm.build(mod, target)
vm = relax.VirtualMachine(ex, device)
# export to library and run
ex.export_library("exported.so")
loaded_mod = ex.load_library("exported.so")
vm = relax.VirtualMachine(ex, device)
The main issue we will encounter here is that direct build and run may not always work, especially when the result module contains source modules(such as cutlass BYOC). In which case export and load back is necessary. However, we can also not by default export and load back, because the loaded back library is not exportable again.
To explain the situation further, we know that modules can have a subset of the following properties:
-
dso_exportable
: can be exported into object or source that then compiled into a so -
binary_exportable
: can be exported via SaveBinary, when loaded back it will become runnable -
runnable
: directly runnable and can obtain packed functions
Note-ably, some modules may have subset of these properties
- llvm:
{runnable, dso_exportable}
- a python module backed by python function:
{runnable}
- cuda file:
{dso_exportable}
- vm bytecode:
{binary_exportable, runnable}
If we encounter a module that is exportable(either dso or binary) but is not runnable, then we will need to export it and load back. If we encounter a module that is runnable, but not exportable(e.g. python native module in the same process), then we cannot simply call export library. To handle all scenarios, it is helpful to look at the two primary use-cases here:
- U0: JIT, build and directly generate runnable code in the same process
- U1: Export, and then load back in same or another process
We propose to update the UX to make these two use-cases explicit. Specifically, relax.Executable
will no longer be directly acceptable by VirtualMachine. Instead, we introduce a jit()
function to explicit generate a runnable runtime module from the executable collection.
# build.py: compile time construct
class Executable:
def export_library(name, fcompile=None):
# redirects to mod.export_library
def jit(fcompile=None) -> runtime.Module:
"""Just in time compile the executable to a runtime.Module
Examples
--------
.. code:: python
ex = relax.build(mod, target)
relax.VirtualMachine(ex.jit("nvcc"))
"""
pass
# vm.py: runtime only
class VirtualMachine:
def __init__(self, mod: runtime.Module, device: Device):
# avoid look at class to isolate runtime
if (not isinstance(mod, runtime.Module) and
type(mod).__name__ == "Executable"):
raise ValueError(
"Please explicit run mod.jit()"
"or export then load it back before passing to VirtualMachine")
So the new flow becomes
# build and directly run
ex: relax.Executable = relax.build(mod, target)
vm = relax.VirtualMachine(ex.jit(), device)
# export to library and run
ex.export_library("exported.so")
loaded_mod = ex.load_library("exported.so")
vm = relax.VirtualMachine(loaded_mod, device)
Base on the properties of each module in our collection, we define the behavior of jit as follows:
- jit
- collect all
dso_exportable
andbinary_exportable
, callexport_library
to put them into a dso - load back and re-import the runnable modules
- collect all
- export_library: require all modules to be exportable or binary_exportable
Discussion
There are several pts that can be discussed, for example, the naming choice of the relax.Executable
which collects the results of the build.