Skip to content

Commit 17a5cad

Browse files
committed
globus compute executor changes/tests moved onto this specific branch
1 parent 68bc11a commit 17a5cad

4 files changed

Lines changed: 626 additions & 1 deletion

File tree

libensemble/executors/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from libensemble.executors.executor import Executor
2+
from libensemble.executors.globus_compute_executor import GlobusComputeExecutor, GlobusComputeTask
23
from libensemble.executors.mpi_executor import MPIExecutor
34

4-
__all__ = ["Executor", "MPIExecutor"]
5+
__all__ = ["Executor", "GlobusComputeExecutor", "GlobusComputeTask", "MPIExecutor"]
Lines changed: 279 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,279 @@
1+
import logging
2+
import os
3+
from concurrent.futures import Future, TimeoutError
4+
from typing import Any
5+
6+
from libensemble.executors.executor import Application, Executor, ExecutorException, Task, TimeoutExpired
7+
from libensemble.utils.globus_compute import GCSession
8+
from libensemble.utils.timer import TaskTimer
9+
10+
logger = logging.getLogger(__name__)
11+
12+
13+
class GlobusComputeTask(Task):
14+
"""A :class:`~libensemble.executors.executor.Task` wrapping a
15+
``concurrent.futures.Future`` returned by Globus Compute.
16+
17+
Instead of managing a local subprocess, this task polls a remote
18+
computation via the future's ``done()`` / ``result()`` APIs.
19+
"""
20+
21+
def __init__(self, future, app=None, app_args=None, workerid=None):
22+
self.id = next(Task.newid)
23+
self.reset()
24+
self.timer = TaskTimer()
25+
self.app = app
26+
self.app_args = app_args
27+
self.workerID = workerid
28+
self._gc_future = future
29+
30+
worker_name = f"_worker{self.workerID}" if self.workerID else ""
31+
self.name = Task.prefix + f"_{app.name}{worker_name}_{self.id}"
32+
self.stdout = ""
33+
self.stderr = ""
34+
self.workdir = None
35+
self.dry_run = False
36+
self.runline = None
37+
self.run_attempts = 0
38+
self.env = {}
39+
self.ngpus_req = 0
40+
41+
self.state = "RUNNING"
42+
self.timer.start()
43+
self.submit_time = self.timer.tstart
44+
45+
def _check_poll(self):
46+
if self.finished:
47+
return False
48+
return True
49+
50+
def poll(self):
51+
if not self._check_poll():
52+
return
53+
if self._gc_future.done():
54+
try:
55+
self._gc_future.result()
56+
self.finished = True
57+
self.success = True
58+
self.state = "FINISHED"
59+
except Exception:
60+
self.finished = True
61+
self.success = False
62+
self.state = "FAILED"
63+
self.calc_task_timing()
64+
else:
65+
self.state = "RUNNING"
66+
self.runtime = self.timer.elapsed
67+
68+
def wait(self, timeout=None):
69+
if not self._check_poll():
70+
return
71+
try:
72+
self._gc_future.result(timeout=timeout)
73+
self.finished = True
74+
self.success = True
75+
self.state = "FINISHED"
76+
except TimeoutError:
77+
raise TimeoutExpired(self.name, timeout)
78+
except Exception:
79+
self.finished = True
80+
self.success = False
81+
self.state = "FAILED"
82+
self.calc_task_timing()
83+
84+
def kill(self, wait_time=None):
85+
self._gc_future.cancel()
86+
self.state = "USER_KILLED"
87+
self.finished = True
88+
self.calc_task_timing()
89+
90+
def result(self, timeout=None):
91+
self.wait(timeout=timeout)
92+
return self.state
93+
94+
def running(self):
95+
self.poll()
96+
return self.state == "RUNNING"
97+
98+
def done(self):
99+
self.poll()
100+
return self.finished
101+
102+
def cancelled(self):
103+
self.poll()
104+
return self.state == "USER_KILLED"
105+
106+
107+
class GlobusComputeExecutor(Executor):
108+
"""An :class:`~libensemble.executors.executor.Executor` that submits
109+
Python callables to Globus Compute instead of launching local subprocesses.
110+
111+
Usage in a top-level script::
112+
113+
from libensemble.executors.globus_compute_executor import GlobusComputeExecutor
114+
115+
exctr = GlobusComputeExecutor(endpoint_id="...")
116+
117+
Inside a simulator function::
118+
119+
task = info["executor"].submit(func=my_remote_func, app_args=...)
120+
while not task.finished:
121+
task.poll()
122+
if info["executor"].manager_kill_received():
123+
task.kill()
124+
break
125+
time.sleep(0.1)
126+
"""
127+
128+
def __init__(self, endpoint_id: str):
129+
self.manager_signal = None
130+
self.default_apps: dict[str, Application | None] = {"sim": None, "gen": None}
131+
self.apps: dict[str, Application] = {}
132+
self.wait_time = 60
133+
self.list_of_tasks: list[GlobusComputeTask] = []
134+
self.workerID = None
135+
self.comm = None
136+
self.last_task = 0
137+
self.base_dir = os.getcwd()
138+
139+
self.endpoint_id = endpoint_id
140+
self._gc_executor = None
141+
self._func_cache: dict[int, str] = {}
142+
143+
def _ensure_gc(self):
144+
if self._gc_executor is None:
145+
self._gc_executor = GCSession.get_or_create_executor(self.endpoint_id)
146+
return self._gc_executor
147+
148+
def _get_func_id(self, func) -> str:
149+
key = id(func)
150+
if key in self._func_cache:
151+
return self._func_cache[key]
152+
executor = self._ensure_gc()
153+
if executor is None:
154+
raise RuntimeError(
155+
"Globus Compute SDK is not installed. " "Install it with: pip install globus-compute-sdk"
156+
)
157+
fid = executor.register_function(func)
158+
self._func_cache[key] = fid
159+
return fid
160+
161+
def register_app(
162+
self,
163+
full_path: str,
164+
app_name: str | None = None,
165+
calc_type: str | None = None,
166+
desc: str | None = None,
167+
precedent: str = "",
168+
pyobj: Any | None = None,
169+
) -> None:
170+
"""Register an application.
171+
172+
If *pyobj* is provided the application is treated as a remote
173+
Python callable. Otherwise the base-class behaviour applies
174+
(local executable).
175+
"""
176+
if not app_name:
177+
app_name = os.path.split(full_path)[1]
178+
179+
app = Application(full_path, app_name, calc_type, desc, pyobj, precedent)
180+
self.apps[app_name] = app
181+
182+
if calc_type is not None:
183+
if calc_type not in self.default_apps:
184+
raise ExecutorException(f"Unrecognized calculation type {calc_type}")
185+
self.default_apps[calc_type] = app
186+
187+
def submit(
188+
self,
189+
calc_type: str | None = None,
190+
app_name: str | None = None,
191+
app_args: str | None = None,
192+
func: Any = None,
193+
stdout: str | None = None,
194+
stderr: str | None = None,
195+
dry_run: bool = False,
196+
wait_on_start: bool = False,
197+
**kwargs,
198+
) -> GlobusComputeTask:
199+
"""Submit a function or registered application to Globus Compute.
200+
201+
Parameters
202+
----------
203+
calc_type : str, optional
204+
Calculation type (``"sim"`` or ``"gen"``). Used with *app_name*.
205+
app_name : str, optional
206+
Name of a previously registered application.
207+
app_args : str, optional
208+
Arguments passed alongside the function.
209+
func : Callable, optional
210+
A Python callable to execute remotely. Takes precedence over
211+
*app_name* / *calc_type*.
212+
stdout, stderr : str, optional
213+
Ignored (stubs for API compatibility).
214+
dry_run : bool, optional
215+
If True, return a task without actually submitting.
216+
wait_on_start : bool, optional
217+
If True, block until the task is reported as started.
218+
219+
Returns
220+
-------
221+
GlobusComputeTask
222+
"""
223+
if dry_run:
224+
raise NotImplementedError("dry_run is not supported for GlobusComputeExecutor")
225+
226+
if func is not None:
227+
fid = self._get_func_id(func)
228+
app = Application(full_path="", name=func.__name__, calc_type="sim", pyobj=func)
229+
elif app_name is not None:
230+
app = self.get_app(app_name)
231+
if app.pyobj is not None:
232+
fid = self._get_func_id(app.pyobj)
233+
else:
234+
raise ValueError(
235+
f"Application '{app_name}' has no pyobj callable registered. "
236+
"Use the `func=...` argument, or register an app with `pyobj=`."
237+
)
238+
elif calc_type is not None:
239+
app = self.default_app(calc_type)
240+
if app.pyobj is not None:
241+
fid = self._get_func_id(app.pyobj)
242+
else:
243+
raise ValueError(
244+
f"Default {calc_type} app has no pyobj callable. "
245+
"Use the `func=...` argument, or register an app with `pyobj=`."
246+
)
247+
else:
248+
raise ValueError("One of `func`, `app_name`, or `calc_type` must be provided")
249+
250+
args = app_args
251+
future: Future = self._ensure_gc().submit_to_registered_function(fid, args)
252+
task = GlobusComputeTask(future, app=app, app_args=args, workerid=self.workerID)
253+
self.list_of_tasks.append(task)
254+
255+
if wait_on_start:
256+
task.wait()
257+
258+
return task
259+
260+
def set_workerID(self, workerid) -> None:
261+
"""Sets the worker ID for this executor."""
262+
self.workerID = workerid
263+
264+
def set_worker_info(self, comm=None, workerid=None) -> None:
265+
"""Sets worker info for this executor."""
266+
self.workerID = workerid
267+
self.comm = comm
268+
269+
def serial_setup(self):
270+
pass
271+
272+
def set_resources(self, resources):
273+
pass
274+
275+
def add_platform_info(self, platform_info=None):
276+
pass
277+
278+
def set_gen_procs_gpus(self, libE_info):
279+
pass

0 commit comments

Comments
 (0)