Skip to content

Commit e722541

Browse files
committed
[Feature] Nested executor
1 parent 4848ef9 commit e722541

3 files changed

Lines changed: 135 additions & 4 deletions

File tree

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
from concurrent.futures import Future
2+
from typing import Callable, Optional
3+
4+
from executorlib.standalone.interactive.arguments import (
5+
get_future_objects_from_input,
6+
)
7+
8+
9+
class BackendExecutor:
10+
def __init__(self):
11+
self._tasks_dict = {}
12+
self._future_dict = {}
13+
14+
@property
15+
def tasks(self):
16+
return self._tasks_dict
17+
18+
def batched(
19+
self,
20+
iterable: list[Future],
21+
n: int,
22+
) -> list[Future]:
23+
"""
24+
Batch futures from the iterable into tuples of length n. The last batch may be shorter than n.
25+
26+
Args:
27+
iterable (list): list of future objects to batch based on which future objects finish first
28+
n (int): badge size
29+
30+
Returns:
31+
list[Future]: list of future objects one for each batch
32+
"""
33+
raise NotImplementedError("The batched method is not implemented.")
34+
35+
def map(
36+
self,
37+
fn: Callable,
38+
*iterables,
39+
timeout: Optional[float] = None,
40+
chunksize: int = 1,
41+
):
42+
"""Returns an iterator equivalent to map(fn, iter).
43+
44+
Args:
45+
fn: A callable that will take as many arguments as there are
46+
passed iterables.
47+
timeout: The maximum number of seconds to wait. If None, then there
48+
is no limit on the wait time.
49+
chunksize: The size of the chunks the iterable will be broken into
50+
before being passed to a child process. This argument is only
51+
used by ProcessPoolExecutor; it is ignored by
52+
ThreadPoolExecutor.
53+
54+
Returns:
55+
An iterator equivalent to: map(func, *iterables) but the calls may
56+
be evaluated out-of-order.
57+
58+
Raises:
59+
TimeoutError: If the entire result iterator could not be generated
60+
before the given timeout.
61+
Exception: If fn(*args) raises for any values.
62+
"""
63+
raise NotImplementedError("The map method is not implemented.")
64+
65+
def submit( # type: ignore
66+
self,
67+
fn: Callable,
68+
/,
69+
*args,
70+
resource_dict: Optional[dict] = None,
71+
**kwargs,
72+
) -> Future:
73+
"""
74+
Submits a callable to be executed with the given arguments.
75+
76+
Schedules the callable to be executed as fn(*args, **kwargs) and returns
77+
a Future instance representing the execution of the callable.
78+
79+
Args:
80+
fn (callable): function to submit for execution
81+
args: arguments for the submitted function
82+
kwargs: keyword arguments for the submitted function
83+
resource_dict (dict): A dictionary of resources required by the task. With the following keys:
84+
- cores (int): number of MPI cores to be used for each function call
85+
- threads_per_core (int): number of OpenMP threads to be used for each function call
86+
- gpus_per_core (int): number of GPUs per worker - defaults to 0
87+
- cwd (str/None): current working directory where the parallel python task is executed
88+
- openmpi_oversubscribe (bool): adds the `--oversubscribe` command line flag (OpenMPI and
89+
SLURM only) - default False
90+
- slurm_cmd_args (list): Additional command line arguments for the srun call (SLURM only)
91+
- error_log_file (str): Name of the error log file to use for storing exceptions raised
92+
by the Python functions submitted to the Executor.
93+
94+
Returns:
95+
Future: A Future representing the given call.
96+
"""
97+
if resource_dict is None:
98+
resource_dict = {}
99+
f: Future = Future()
100+
self._future_dict[f] = id(f)
101+
self._tasks_dict[id(f)] = {
102+
"fn": fn,
103+
"args": args,
104+
"kwargs": kwargs,
105+
"resource_dict": resource_dict,
106+
"dependencies": [
107+
self._future_dict[future_dependency]
108+
for future_dependency in get_future_objects_from_input(args=args, kwargs=kwargs)
109+
],
110+
}
111+
return f

src/executorlib/backend/interactive_parallel.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import cloudpickle
77
import zmq
88

9+
from executorlib.backend.executor_nested import BackendExecutor
910
from executorlib.standalone.error import backend_write_error_file
1011
from executorlib.standalone.interactive.backend import call_funct, parse_arguments
1112
from executorlib.standalone.interactive.communication import (
@@ -43,7 +44,10 @@ def main() -> None:
4344
host=argument_dict["host"], port=argument_dict["zmqport"]
4445
)
4546

46-
memory = {"executorlib_worker_id": int(argument_dict["worker_id"])}
47+
memory = {
48+
"executorlib_worker_id": int(argument_dict["worker_id"]),
49+
"executorlib_executor": BackendExecutor(),
50+
}
4751

4852
# required for flux interface - otherwise the current path is not included in the python path
4953
cwd = abspath(".")
@@ -90,7 +94,13 @@ def main() -> None:
9094
else:
9195
# Send output
9296
if mpi_rank_zero:
93-
interface_send(socket=socket, result_dict={"result": output_reply})
97+
interface_send(
98+
socket=socket,
99+
result_dict={
100+
"result": output_reply,
101+
"tasks_nested": memory["executorlib_executor"].tasks,
102+
},
103+
)
94104
elif (
95105
"init" in input_dict
96106
and input_dict["init"]

src/executorlib/backend/interactive_serial.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from os.path import abspath
33
from typing import Optional
44

5+
from executorlib.backend.executor_nested import BackendExecutor
56
from executorlib.standalone.error import backend_write_error_file
67
from executorlib.standalone.interactive.backend import call_funct, parse_arguments
78
from executorlib.standalone.interactive.communication import (
@@ -29,7 +30,10 @@ def evaluate_cmd(argument_lst: Optional[list[str]] = None):
2930
host=argument_dict["host"], port=argument_dict["zmqport"]
3031
)
3132

32-
memory = {"executorlib_worker_id": int(argument_dict["worker_id"])}
33+
memory = {
34+
"executorlib_worker_id": int(argument_dict["worker_id"]),
35+
"executorlib_executor": BackendExecutor(),
36+
}
3337

3438
# required for flux interface - otherwise the current path is not included in the python path
3539
cwd = abspath(".")
@@ -65,7 +69,13 @@ def evaluate_cmd(argument_lst: Optional[list[str]] = None):
6569
)
6670
else:
6771
# Send output
68-
interface_send(socket=socket, result_dict={"result": output})
72+
interface_send(
73+
socket=socket,
74+
result_dict={
75+
"result": output,
76+
"tasks_nested": memory["executorlib_executor"].tasks,
77+
},
78+
)
6979
elif (
7080
"init" in input_dict
7181
and input_dict["init"]

0 commit comments

Comments
 (0)