Skip to main content
When a Tensorlake function is called by another function the calling function blocks until the called function completes and returns its result. Applications that use blocking calls like that can face multiple challenges:
  1. Wasted Resources: While waiting for the call result, the calling function container cannot perform other tasks while still consuming its compute resources.
  2. Higher Resource Usage: More function containers are required to handle the same number of concurrent application requests if each request blocks multiple function containers.
  3. Higher Latency: Sequential blocking calls can lead to increased overall latency, especially when multiple function calls are involved.
To address these challenges, Tensorlake introduces the concept of awaitables and tail calls.

Awaitables

An awaitable defines a function call without running it. An awaitable can then be passed as an argument to another function call or be returned from a function as a “tail call”. When an awaitable is passed as a function call argument, this tells Tensorlake where to obtain the argument value without having it already available in the calling function. When an awaitable is returned as a tail call, this tells Tensorlake to run the function call defined by the awaitable and then use its return value as the the calling function return value. An awaitable defines a function call without running it. An awaitable can then be passed as an argument to another function call or be returned from a function as a “tail call”. When an awaitable is passed as a function call argument, Tensorlake holds its reference until it needs to make the function’s value available in the calling function. When an awaitable is returned as a tail call, Tensorlake runs the function call defined by the awaitable and then use its return value as the the calling function return value. Tensorlake runs function calls defined by the awaitables automatically as soon as all their data dependencies (function argument values) are available. All function calls that don’t depend on each other run in parallel. This allows Tensorlake to optimize resource usage and reduce overall application latency. Applications that use awaitables and tail calls reduce their overall latency and resource usage significantly.

Using Awaitables and Tail Calls

To define an awaitable function call, use the awaitable property of the function. The awaitable property has the same methods and signature as the function itself. For example:
from tensorlake.applications import application, function, Awaitable

@application()
@function()
def greet(name: str) -> str:
    # Returns an awaitable for `say_hello_and_say_joke(capitalize(name), make_joke(name))` function call.
    # This is a tail call. `greet` doesn't block waiting for any of the function calls to complete.
    # Once returned Tensorlake will run the tail call and use `say_hello_and_say_joke` return value as the
    # return value of `greet`. The `say_hello_and_say_joke` function call will run as soon as both its
    # arguments are available. Both arguments are computed in parallel because they don't depend on each other.
    capitalized_name: Awaitable = capitalize.awaitable(name)
    joke: Awaitable = make_joke.awaitable(name)
    return say_hello_and_say_joke.awaitable(capitalized_name, joke=joke)


@function()
def say_hello_and_say_joke(name: str, joke: str) -> str:
    return f"Hello, {name}! Here's a joke for you: {joke}"


@function()
def capitalize(text: str) -> str:
    return text.upper()


@function()
def make_joke(name: str) -> str:
    return f"Why did {name} cross the road? To get to the other side!"

Creating Awaitable objects for map/reduce operations

Awaitable objects for map/reduce operations are also created using the awaitable property of a function. These awaitable properties also require the name of the operation that they will perform as. For example:
from tensorlake.applications import Awaitable, function

@function()
def my_function(x: int, y: int) -> int:
    return x + y


@function()
def my_map_function(value: int) -> int:
    return value * 2

@application()
@function()
def my_application(value: int) -> str:
    # Regular function call that blocks until the function completes.
    # my_function_result == 3
    my_function_result: int = my_function(1, 2)
    # Create an awaitable for the same function call.
    my_function_awaitable: Awaitable = my_function.awaitable(1, 2)

    # Regular map operation that blocks until all mapped calls complete.
    # my_map_function_map_result == [2, 4, 6]
    my_map_function.map([1, 2, 3])
    # Create an awaitable for the same map operation.
    my_map_function_awaitable: Awaitable = my_map_function.awaitable.map([1, 2, 3])

    # Reduce operation that blocks until all reduce calls complete.
    my_function.reduce([1, 2, 3])
    # Create an awaitable for the same reduce operation.
    my_function_reduce_awaitable: Awaitable = my_function.awaitable.reduce([1, 2, 3])

    # None of the awaitables above will ever run because they are not returned as tail calls
    # or passed as arguments to other function calls that actually run.
    return "Done"

Using Awaitable objects

Running an Awaitable object

An operation (i.e. a function call) defined by an Awaitable object can be started by calling the run method on the Awaitable object.
from tensorlake.applications import application, function, Future

@application()
@function()
def my_application(name: str) -> str:
    # Creates Awaitable object for the `capitalize(name)` function call and runs it immediately.
    # `.run()` method doesn't block the calling function until the function call completes.
    # It returns a Future object that can be used to get the result of the function call later.
    capitalized_name_future: Future = capitalize.awaitable(name).run()
    # Blocks until the `capitalize` function call completes and get its return value.
    capitalized_name: str = capitalized_name_future.result()
    return f"Hello, {capitalized_name}!"

See more about Futures and parallel function calls. At the moment an Awaitable object with already started function call can’t be used as an argument to another function call or returned as a tail call. This might change in the future.

Wrapping of Awaitable objects is not allowed

When passing Awaitable objects as arguments to function calls, or returning them as tail calls from functions, the Awaitable objects cannot be wrapped into any other objects. For example:
from tensorlake.applications import application, function, Awaitable

@application()
@function()
def my_application(name: str) -> tuple[str, str]:
    capitalized_name: Awaitable = capitalize.awaitable(name)
    # This is NOT allowed. Awaitable objects cannot be wrapped into other objects.
    # Tensorlake will see the returned tuple as a regular function return value, not a tail call.
    # It'll attempt to serialize the Awaitable object which will fail.
    return capitalized_name, "All good!"