Client tutorial
This tutorial walks through the key concepts used by the ExaDeploy client. Make sure that you've installed the Python client before following along.
Setting up the cluster
In a terminal, start the end-to-end image. This starts up a full temporary ExaDeploy cluster on your local machine.
python3 -m exa local_e2e gcr.io/exafunction/local_e2e:...
If you don't have a local_e2e
image, contact Exafunction to get one.
If you've set up a cluster in the cloud from following the quick start guides, you can feel free to use those as well.
Start the interpreter
# These variables assume you're using the local_e2e image.
# Change them if using a real cluster.
EXA_SCHEDULER_ADDRESS=localhost:50050 \
EXA_MODULE_REPOSITORY_ADDRESS=localhost:50051 \
python3
Defining remote code
We'll create a simple function to add two arrays.
import exa
import numpy as np
class AdditionAutoModule(exa.AutoModule):
@exa.export
def add(self, a: np.ndarray, b: np.ndarray) -> np.ndarray:
return a + b
Subclasses of exa.AutoModule
contain related groups of pure Python functions which may share resources, such as a deep learning model and weights. The exa.export
decorator marks a function as a remote function. Commonly used types such as np.ndarray
are automatically recognized and serialized in exa.AutoModule
. When relying on autoconversion of types, the type annotations are required.
Uploading the code
from exa import repo
repo.register_py_interpreter_module("InterpreterModule")
The repo
module offers us shortcuts to access a singleton of the exa.ModuleRepository
class, which is a client to the ExaDeploy module repository. The register_py_interpreter_module
function defines a set of requirements and a file tree to be uploaded to the repository. In this case, neither is necessary.
module = exa.new_module_from_cls(AdditionAutoModule, "InterpreterModule")
The exa
module contains some shortcuts to access a singleton of the exa.Session
class, which is a client to the ExaDeploy scheduler and runners. The new_module_from_cls
function uploads the code for AdditionAutoModule
to a runner that has already loaded the InterpreterModule
that was just defined and uploaded to the module repository.
There are three places where we can declare code:
- In exported functions: These are uploaded after the session is created on a per-client basis.
- In the
InterpreterModule
or some other plugin: These files and requirements are set up once per runner when it gets assigned. They can be dynamically loaded and unloaded without replacing the runner entirely, but require registration in the module repository. - In the image of the runner: (Not shown in this example.) These are baked into the image of the runner.
You have the flexibility of deciding where to put your code between these options. Exafunction has implemented a large number of common plugins and functions to conveniently wrap common frameworks.
If possible, we recommend putting code in the plugin. However, adding them to the runner image is most convenient.
Executing the code
result = module.add(np.array([1, 2, 3]), np.array([4, 5, 6]))
assert np.array_equal(exa.get(result), np.array([5, 7, 9]))
The module.add
function is now a remote function that can be called with local arguments. The exa.get
function retrieves the result of a remote function call and converts it back to local types. If the remote function is not yet complete, it will block until the result is available.
Remote pipelining
The result
object contains exa.Value
objects and tries to behave asynchronously — the function will usually return immediately, and the exa.Value
objects will contain handles to the remote values which may not yet be computed. If a value isn't needed locally, the exa.Value
object can be passed to another remote function call, without using exa.get
.
result2 = module.add(np.array([1, 2, 3]), np.array([4, 5, 6]))
result3 = module.add(result, result2)
assert np.array_equal(exa.get(result3), np.array([10, 14, 18]))
In this example, the data for result2
never needs to be downloaded locally to the client. The data for result
has already been fetched in the previous code block, but it doesn't need to be reuploaded. This helps save network bandwidth.
Freeing GPU resources
While the exa.Value
object exists, the runner will retain the value, so it must be explicitly deleted to avoid growing memory usage over time. This convention was chosen to allow for simple pipelining of remote functions.
del result, result2, result3
Usually it's not necessary to del
the exa.Value
objects. The Python garbage collector will automatically free them when they are no longer referenced, such as when they go out of scope from a function.
Putting it all together
import exa
import numpy as np
from exa import repo
class AdditionAutoModule(exa.AutoModule):
@exa.export
def add(self, a: np.ndarray, b: np.ndarray) -> np.ndarray:
return a + b
repo.register_py_interpreter_module("InterpreterModule")
module = exa.new_module_from_cls(AdditionAutoModule, "InterpreterModule")
result = module.add(np.array([1, 2, 3]), np.array([4, 5, 6]))
assert np.array_equal(exa.get(result), np.array([5, 7, 9]))
result2 = module.add(np.array([1, 2, 3]), np.array([4, 5, 6]))
result3 = module.add(result, result2)
assert np.array_equal(exa.get(result3), np.array([10, 14, 18]))
del result, result2, result3
The code can be run with:
# These variables assume you're using the local_e2e image.
# Change them if using a real cluster.
EXA_SCHEDULER_ADDRESS=localhost:50050 \
EXA_MODULE_REPOSITORY_ADDRESS=localhost:50051 \
python3 demo_simple.py