Creating and Assigning an Executor#

If multiple electrons can use the same compute resource, create one executor for the resource and assign it to the electrons rather than creating an executor every time an electron is instantiated.

Context#

Multiple electrons can and should use the same executor if they use the same back-end resource.

Recall that an executor is an abstracted “front end” to a computing resource, such as an HPC cloud or container cluster. One executor represents one particular computing resource or “back end.” The executor decouples the electron from the compute resource, such that you can assign any executor to any electron.

Creating multiple executors for the same resource does not improve performance.

Parallelization is done by the Covalent dispatcher after analyzing the transport graph, and is based on independence of the inputs and outputs of electron instances. There is no benefit to creating two or more “pipelines” to the resource.

With a single executor per compute resource, independent electrons (including multiple iterations of the same electron) are: 1. parallelized by the Covalent dispatcher; and 2. executed efficiently by the back end as separate computations.

“Efficiently” here means some combination of “in parallel,” “on multiple CPUs,” “load-balanced,” “distributed,” or whatever mechanisms the back end uses to improve performance.

Of course, different back-end resources are better suited for some tasks (electrons) than for others, but it’s up to you to match the right resource to the right executor. The executor framework gives you the flexibility to “mix and match” tasks and computation resources, for example to test a task on one resource and do production work on another.

Again, there is no performance advantage to be gained from creating multiple executors for the same back end. In fact, creating unneeded executors hurts performance by needlessly consuming dispatcher memory and CPU. As well, instantiating multiple executors complicates code maintenance. Defining the executor once means any modification can be done once, in one place.

Best Practice#

When creating multiple electrons that use the same computation resource, instantiate one executor and then assign the executor to electrons as they are defined.

Example#

Contrast the two examples below.

Example 2: Improved#

This example demonstrates the better technique: creating one executor and assigning it to each new instance of the electron.

[23]:
import covalent as ct
import random
from covalent.executor import DaskExecutor

# Technique 2 (better):

same_executor = LocalExecutor(log_stdout="./exec_stdout_common.txt")  # One executor is all you need here.

@ct.electron(executor=same_executor)  # Assign the executor.
def task_1():
    return random.sample(range(10, 30), 5)


@ct.electron(executor=same_executor)  # Same executor, different electron.
def task_2(x_list):

    squares = []
    for x in x_list:
        squares.append(x ** 2)

    return squares

@ct.lattice
def workflow_2():
    task_1
    random_list = task_1()
    return task_2(random_list)

id = ct.dispatch(workflow_2)()
res = ct.get_result(id, wait=True)
print(res)

Lattice Result
==============
status: COMPLETED
result: [676, 841, 729, 225, 289]
input args: []
input kwargs: {}
error: None

start_time: 2023-02-21 17:55:22.717915
end_time: 2023-02-21 17:55:22.812049

results_dir: /Users/mini-me/agnostiq/covalent/doc/source/developer/patterns/results
dispatch_id: 0225a221-4ecf-4d87-b92b-21fe3ff1776f

Node Outputs
------------
task_1(0): [26, 29, 27, 15, 17]
task_2(1): [676, 841, 729, 225, 289]

As with the previous example, go to the Covalent UI and examine the stdout log name in the Executor details for evidence as to which executor was used with each electron. In this example the log is the same (exec_stdout_common.txt). (Also, take care to note that in general this doesn’t prove that there is only one executor, since different executors can share the same log file.)