parsl 2025.9.8__py3-none-any.whl → 2025.11.10__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. parsl/app/bash.py +1 -1
  2. parsl/benchmark/perf.py +73 -17
  3. parsl/concurrent/__init__.py +95 -14
  4. parsl/curvezmq.py +0 -16
  5. parsl/data_provider/globus.py +3 -1
  6. parsl/dataflow/dflow.py +106 -204
  7. parsl/dataflow/memoization.py +146 -19
  8. parsl/dataflow/states.py +5 -5
  9. parsl/executors/base.py +2 -2
  10. parsl/executors/execute_task.py +2 -8
  11. parsl/executors/flux/executor.py +4 -6
  12. parsl/executors/globus_compute.py +0 -4
  13. parsl/executors/high_throughput/executor.py +86 -24
  14. parsl/executors/high_throughput/interchange.py +39 -20
  15. parsl/executors/high_throughput/mpi_executor.py +1 -2
  16. parsl/executors/high_throughput/mpi_resource_management.py +7 -14
  17. parsl/executors/high_throughput/process_worker_pool.py +32 -7
  18. parsl/executors/high_throughput/zmq_pipes.py +36 -67
  19. parsl/executors/radical/executor.py +2 -6
  20. parsl/executors/radical/rpex_worker.py +2 -2
  21. parsl/executors/taskvine/executor.py +5 -1
  22. parsl/executors/threads.py +5 -2
  23. parsl/jobs/states.py +2 -2
  24. parsl/jobs/strategy.py +7 -6
  25. parsl/monitoring/monitoring.py +2 -2
  26. parsl/monitoring/radios/filesystem.py +2 -1
  27. parsl/monitoring/radios/htex.py +2 -1
  28. parsl/monitoring/radios/multiprocessing.py +2 -1
  29. parsl/monitoring/radios/udp.py +2 -1
  30. parsl/multiprocessing.py +0 -49
  31. parsl/providers/base.py +24 -37
  32. parsl/providers/pbspro/pbspro.py +1 -1
  33. parsl/serialize/__init__.py +6 -9
  34. parsl/serialize/facade.py +0 -32
  35. parsl/tests/configs/local_threads_globus.py +18 -14
  36. parsl/tests/configs/taskvine_ex.py +1 -1
  37. parsl/tests/sites/test_concurrent.py +51 -3
  38. parsl/tests/test_checkpointing/test_periodic.py +15 -9
  39. parsl/tests/test_checkpointing/test_regression_233.py +0 -1
  40. parsl/tests/test_curvezmq.py +0 -42
  41. parsl/tests/test_execute_task.py +2 -11
  42. parsl/tests/test_htex/test_command_concurrency_regression_1321.py +54 -0
  43. parsl/tests/test_htex/test_htex.py +36 -1
  44. parsl/tests/test_htex/test_interchange_exit_bad_registration.py +2 -2
  45. parsl/tests/test_htex/test_priority_queue.py +26 -3
  46. parsl/tests/test_htex/test_zmq_binding.py +2 -1
  47. parsl/tests/test_mpi_apps/test_mpi_scheduler.py +18 -43
  48. parsl/tests/test_python_apps/test_basic.py +0 -14
  49. parsl/tests/test_python_apps/test_depfail_propagation.py +11 -1
  50. parsl/tests/test_python_apps/test_exception.py +19 -0
  51. parsl/tests/test_python_apps/test_garbage_collect.py +1 -6
  52. parsl/tests/test_python_apps/test_memoize_2.py +11 -1
  53. parsl/tests/test_regression/test_3874.py +47 -0
  54. parsl/tests/test_scaling/test_regression_3696_oscillation.py +1 -0
  55. parsl/tests/test_staging/test_staging_globus.py +2 -2
  56. parsl/tests/unit/test_globus_compute_executor.py +11 -2
  57. parsl/utils.py +8 -3
  58. parsl/version.py +1 -1
  59. {parsl-2025.9.8.data → parsl-2025.11.10.data}/scripts/interchange.py +39 -20
  60. {parsl-2025.9.8.data → parsl-2025.11.10.data}/scripts/process_worker_pool.py +32 -7
  61. {parsl-2025.9.8.dist-info → parsl-2025.11.10.dist-info}/METADATA +64 -50
  62. {parsl-2025.9.8.dist-info → parsl-2025.11.10.dist-info}/RECORD +68 -74
  63. {parsl-2025.9.8.dist-info → parsl-2025.11.10.dist-info}/WHEEL +1 -1
  64. parsl/tests/configs/local_threads_checkpoint_periodic.py +0 -11
  65. parsl/tests/configs/local_threads_no_cache.py +0 -11
  66. parsl/tests/site_tests/test_provider.py +0 -88
  67. parsl/tests/site_tests/test_site.py +0 -70
  68. parsl/tests/test_aalst_patterns.py +0 -474
  69. parsl/tests/test_docs/test_workflow2.py +0 -42
  70. parsl/tests/test_error_handling/test_rand_fail.py +0 -171
  71. parsl/tests/test_regression/test_854.py +0 -62
  72. parsl/tests/test_serialization/test_pack_resource_spec.py +0 -23
  73. {parsl-2025.9.8.data → parsl-2025.11.10.data}/scripts/exec_parsl_function.py +0 -0
  74. {parsl-2025.9.8.data → parsl-2025.11.10.data}/scripts/parsl_coprocess.py +0 -0
  75. {parsl-2025.9.8.dist-info → parsl-2025.11.10.dist-info}/entry_points.txt +0 -0
  76. {parsl-2025.9.8.dist-info → parsl-2025.11.10.dist-info/licenses}/LICENSE +0 -0
  77. {parsl-2025.9.8.dist-info → parsl-2025.11.10.dist-info}/top_level.txt +0 -0
parsl/app/bash.py CHANGED
@@ -88,7 +88,7 @@ def remote_side_bash_executor(func, *args, **kwargs):
88
88
  raise pe.AppTimeout(f"App {func_name} exceeded walltime: {timeout} seconds")
89
89
 
90
90
  except Exception as e:
91
- raise pe.AppException(f"App {func_name} caught exception with returncode: {returncode}", e)
91
+ raise pe.AppException(f"App {func_name} caught exception", e)
92
92
 
93
93
  if returncode != 0:
94
94
  raise pe.BashExitFailure(func_name, proc.returncode)
parsl/benchmark/perf.py CHANGED
@@ -2,46 +2,65 @@ import argparse
2
2
  import concurrent.futures
3
3
  import importlib
4
4
  import time
5
+ from typing import Any, Dict, Literal
5
6
 
6
7
  import parsl
8
+ from parsl.dataflow.dflow import DataFlowKernel
9
+ from parsl.errors import InternalConsistencyError
10
+
11
+ VALID_NAMED_ITERATION_MODES = ("estimate", "exponential")
7
12
 
8
13
  min_iterations = 2
9
14
 
10
15
 
11
16
  # TODO: factor with conftest.py where this is copy/pasted from?
12
- def load_dfk_from_config(filename):
17
+ def load_dfk_from_config(filename: str) -> DataFlowKernel:
13
18
  spec = importlib.util.spec_from_file_location('', filename)
19
+
20
+ if spec is None:
21
+ raise RuntimeError("Could not import configuration")
22
+
23
+ module = importlib.util.module_from_spec(spec)
24
+
25
+ if spec.loader is None:
26
+ raise RuntimeError("Could not load configuration")
27
+
28
+ spec.loader.exec_module(module)
29
+
14
30
  module = importlib.util.module_from_spec(spec)
15
31
  spec.loader.exec_module(module)
16
32
 
17
33
  if hasattr(module, 'config'):
18
- parsl.load(module.config)
34
+ return parsl.load(module.config)
19
35
  elif hasattr(module, 'fresh_config'):
20
- parsl.load(module.fresh_config())
36
+ return parsl.load(module.fresh_config())
21
37
  else:
22
38
  raise RuntimeError("Config module does not define config or fresh_config")
23
39
 
24
40
 
25
41
  @parsl.python_app
26
- def app(extra_payload, parsl_resource_specification={}):
42
+ def app(extra_payload: Any, parsl_resource_specification: Dict = {}) -> int:
27
43
  return 7
28
44
 
29
45
 
30
- def performance(*, resources: dict, target_t: float, args_extra_size: int):
31
- n = 10
46
+ def performance(*, resources: dict, target_t: float, args_extra_size: int, iterate_mode: str | list[int]) -> None:
32
47
 
33
48
  delta_t: float
34
- delta_t = 0
35
-
36
- threshold_t = int(0.75 * target_t)
37
49
 
38
50
  iteration = 1
39
51
 
40
52
  args_extra_payload = "x" * args_extra_size
41
53
 
42
- while delta_t < threshold_t or iteration <= min_iterations:
54
+ if isinstance(iterate_mode, list):
55
+ n = iterate_mode[0]
56
+ else:
57
+ n = 10
58
+
59
+ iterate = True
60
+
61
+ while iterate:
43
62
  print(f"==== Iteration {iteration} ====")
44
- print(f"Will run {n} tasks to target {target_t} seconds runtime")
63
+ print(f"Will run {n} tasks")
45
64
  start_t = time.time()
46
65
 
47
66
  fs = []
@@ -65,10 +84,42 @@ def performance(*, resources: dict, target_t: float, args_extra_size: int):
65
84
  print(f"Runtime: actual {delta_t:.3f}s vs target {target_t}s")
66
85
  print(f"Tasks per second: {rate:.3f}")
67
86
 
68
- n = max(1, int(target_t * rate))
69
-
70
87
  iteration += 1
71
88
 
89
+ # decide upon next iteration
90
+
91
+ match iterate_mode:
92
+ case "estimate":
93
+ n = max(1, int(target_t * rate))
94
+ iterate = delta_t < (0.75 * target_t) or iteration <= min_iterations
95
+ case "exponential":
96
+ n = int(n * 2)
97
+ iterate = delta_t < target_t or iteration <= min_iterations
98
+ case seq if isinstance(seq, list) and iteration <= len(seq):
99
+ n = seq[iteration - 1]
100
+ iterate = True
101
+ case seq if isinstance(seq, list):
102
+ iterate = False
103
+ case _:
104
+ raise InternalConsistencyError(f"Bad iterate mode {iterate_mode} - should have been validated at arg parse time")
105
+
106
+
107
+ def validate_int_list(v: str) -> list[int] | Literal[False]:
108
+ try:
109
+ return list(map(int, v.split(",")))
110
+ except ValueError:
111
+ return False
112
+
113
+
114
+ def iteration_mode(v: str) -> str | list[int]:
115
+ match v:
116
+ case s if s in VALID_NAMED_ITERATION_MODES:
117
+ return s
118
+ case _ if seq := validate_int_list(v):
119
+ return seq
120
+ case _:
121
+ raise argparse.ArgumentTypeError(f"Invalid iteration mode: {v}")
122
+
72
123
 
73
124
  def cli_run() -> None:
74
125
  parser = argparse.ArgumentParser(
@@ -82,6 +133,12 @@ Example usage: python -m parsl.benchmark.perf --config parsl/tests/configs/workq
82
133
  parser.add_argument("--resources", metavar="EXPR", help="parsl_resource_specification dictionary")
83
134
  parser.add_argument("--time", metavar="SECONDS", help="target number of seconds for an iteration", default=120, type=float)
84
135
  parser.add_argument("--argsize", metavar="BYTES", help="extra bytes to add into app invocation arguments", default=0, type=int)
136
+ parser.add_argument("--version", action="version", version=f"parsl-perf from Parsl {parsl.__version__}")
137
+ parser.add_argument("--iterate",
138
+ metavar="MODE",
139
+ help="Iteration mode: " + ", ".join(VALID_NAMED_ITERATION_MODES) + ", or sequence of explicit sizes",
140
+ type=iteration_mode,
141
+ default="estimate")
85
142
 
86
143
  args = parser.parse_args()
87
144
 
@@ -90,10 +147,9 @@ Example usage: python -m parsl.benchmark.perf --config parsl/tests/configs/workq
90
147
  else:
91
148
  resources = {}
92
149
 
93
- load_dfk_from_config(args.config)
94
- performance(resources=resources, target_t=args.time, args_extra_size=args.argsize)
95
- print("Cleaning up DFK")
96
- parsl.dfk().cleanup()
150
+ with load_dfk_from_config(args.config):
151
+ performance(resources=resources, target_t=args.time, args_extra_size=args.argsize, iterate_mode=args.iterate)
152
+ print("Tests complete - leaving DFK block")
97
153
  print("The end")
98
154
 
99
155
 
@@ -1,42 +1,88 @@
1
1
  """Interfaces modeled after Python's `concurrent library <https://docs.python.org/3/library/concurrent.html>`_"""
2
2
  import time
3
3
  from concurrent.futures import Executor
4
- from typing import Callable, Dict, Iterable, Iterator, Optional
4
+ from contextlib import AbstractContextManager
5
+ from typing import Callable, Dict, Iterable, Iterator, Literal, Optional
5
6
  from warnings import warn
6
7
 
7
- from parsl import Config, DataFlowKernel
8
+ from parsl import Config, DataFlowKernel, load
8
9
  from parsl.app.python import PythonApp
9
10
 
10
11
 
11
- class ParslPoolExecutor(Executor):
12
+ class ParslPoolExecutor(Executor, AbstractContextManager):
12
13
  """An executor that uses a pool of workers managed by Parsl
13
14
 
14
15
  Works just like a :class:`~concurrent.futures.ProcessPoolExecutor` except that tasks
15
16
  are distributed across workers that can be on different machines.
16
- Create a new executor by supplying a Parsl :class:`~parsl.Config` object to define
17
- how to create new workers, Parsl will set up and tear down workers on your behalf.
18
17
 
19
- Note: Parsl does not support canceling tasks. The :meth:`map` method does not cancel work
18
+ Create a new executor using one of two methods:
19
+
20
+ 1. Supplying a Parsl :class:`~parsl.Config` that defines how to create new workers.
21
+ The executor will start a new Parsl Data Flow Kernel (DFK) when it is entered as a context manager.
22
+
23
+ 2. Supplying an already-started Parsl :class:`~parsl.DataFlowKernel` (DFK).
24
+ The executor assumes you will start and stop the Parsl DFK outside the Executor.
25
+
26
+ The futures returned by :meth:`submit` and :meth:`map` are Parsl futures and will work
27
+ with the same function chaining mechanisms as when using Parsl with decorators.
28
+
29
+ .. code-block:: python
30
+
31
+ def f(x):
32
+ return x + 1
33
+
34
+ @python_app
35
+ def parity(x):
36
+ return 'odd' if x % 2 == 1 else 'even'
37
+
38
+ with ParslPoolExecutor(config=my_parsl_config) as executor:
39
+ future_1 = executor.submit(f, 1)
40
+ assert parity(future_1) == 'even' # Function chaining, as expected
41
+
42
+ future_2 = executor.submit(f, future_1)
43
+ assert future_2.result() == 3 # Chaining works with `submit` too
44
+
45
+ Parsl does not support canceling tasks. The :meth:`map` method does not cancel work
20
46
  when one member of the run fails or a timeout is reached
21
47
  and :meth:`shutdown` does not cancel work on completion.
22
48
  """
23
49
 
24
- def __init__(self, config: Config):
50
+ def __init__(self, config: Config | None = None, dfk: DataFlowKernel | None = None, executors: Literal['all'] | list[str] = 'all'):
25
51
  """Create the executor
26
52
 
27
53
  Args:
28
54
  config: Configuration for the Parsl Data Flow Kernel (DFK)
55
+ dfk: DataFlowKernel of an already-started parsl
56
+ executors: List of executors to use for supplied functions
29
57
  """
58
+ if (config is not None) and (dfk is not None):
59
+ raise ValueError('Specify only one of config or dfk')
60
+ if (config is None) and (dfk is None):
61
+ raise ValueError('Must specify one of config or dfk')
30
62
  self._config = config
31
- self.dfk = DataFlowKernel(self._config)
32
63
  self._app_cache: Dict[Callable, PythonApp] = {} # Cache specific to this instance: https://stackoverflow.com/questions/33672412
64
+ self._dfk = dfk
65
+ self.executors = executors
66
+
67
+ # Start workers immediately
68
+ if self._config is not None:
69
+ self._dfk = load(self._config)
70
+
71
+ def __exit__(self, exc_type, exc_val, exc_tb):
72
+ if self._dfk is None: # Nothing has been started, do nothing
73
+ return
74
+ elif self._config is not None: # The executors are being managed by this class, shut them down
75
+ self.shutdown(wait=True)
76
+ return
77
+ else: # The DFK is managed elsewhere, do nothing
78
+ return
33
79
 
34
80
  @property
35
81
  def app_count(self):
36
82
  """Number of functions currently registered with the executor"""
37
83
  return len(self._app_cache)
38
84
 
39
- def _get_app(self, fn: Callable) -> PythonApp:
85
+ def get_app(self, fn: Callable) -> PythonApp:
40
86
  """Create a PythonApp for a function
41
87
 
42
88
  Args:
@@ -46,22 +92,53 @@ class ParslPoolExecutor(Executor):
46
92
  """
47
93
  if fn in self._app_cache:
48
94
  return self._app_cache[fn]
49
- app = PythonApp(fn, data_flow_kernel=self.dfk)
95
+ app = PythonApp(fn, data_flow_kernel=self._dfk, executors=self.executors)
50
96
  self._app_cache[fn] = app
51
97
  return app
52
98
 
53
99
  def submit(self, fn, *args, **kwargs):
54
- app = self._get_app(fn)
100
+ """Submits a callable to be executed with the given arguments.
101
+
102
+ Schedules the callable to be executed as ``fn(*args, **kwargs)`` and returns
103
+ a Future instance representing the execution of the callable.
104
+
105
+ Returns:
106
+ A Future representing the given call.
107
+ """
108
+
109
+ if self._dfk is None:
110
+ raise RuntimeError('Executor has been shut down.')
111
+ app = self.get_app(fn)
55
112
  return app(*args, **kwargs)
56
113
 
57
114
  # TODO (wardlt): This override can go away when Parsl supports cancel
58
115
  def map(self, fn: Callable, *iterables: Iterable, timeout: Optional[float] = None, chunksize: int = 1) -> Iterator:
116
+ """Returns an iterator equivalent to map(fn, iter).
117
+
118
+ Args:
119
+ fn: A callable that will take as many arguments as there are
120
+ passed iterables.
121
+ timeout: The maximum number of seconds to wait. If None, then there
122
+ is no limit on the wait time.
123
+ chunksize: If greater than one, the iterables will be chopped into
124
+ chunks of size chunksize and submitted to the process pool.
125
+ If set to one, the items in the list will be sent one at a time.
126
+
127
+ Returns:
128
+ An iterator equivalent to: map(func, ``*iterables``) but the calls may
129
+ be evaluated out-of-order.
130
+
131
+ Raises:
132
+ TimeoutError: If the entire result iterator could not be generated
133
+ before the given timeout.
134
+ Exception: If ``fn(*args)`` raises for any values.
135
+ """
59
136
  # This is a version of the CPython 3.9 `.map` implementation modified to not use `cancel`
60
137
  if timeout is not None:
61
138
  end_time = timeout + time.monotonic()
62
139
 
63
140
  # Submit the applications
64
- app = self._get_app(fn)
141
+ app = self.get_app(fn)
65
142
  fs = [app(*args) for args in zip(*iterables)]
66
143
 
67
144
  # Yield the futures as completed
@@ -78,8 +155,12 @@ class ParslPoolExecutor(Executor):
78
155
  return result_iterator()
79
156
 
80
157
  def shutdown(self, wait: bool = True, *, cancel_futures: bool = False) -> None:
158
+ if self._dfk is None:
159
+ return # Do nothing. Nothing is active
81
160
  if cancel_futures:
82
161
  warn(message="Canceling on-going tasks is not supported in Parsl")
83
162
  if wait:
84
- self.dfk.wait_for_current_tasks()
85
- self.dfk.cleanup()
163
+ self._dfk.wait_for_current_tasks()
164
+ if self._config is not None: # The executors are being managed
165
+ self._dfk.cleanup() # Shutdown the DFK
166
+ self._dfk = None
parsl/curvezmq.py CHANGED
@@ -101,17 +101,6 @@ class BaseContext(metaclass=ABCMeta):
101
101
  """
102
102
  self._ctx.destroy(linger)
103
103
 
104
- def recreate(self, linger: Optional[int] = None):
105
- """Destroy then recreate the context.
106
-
107
- Parameters
108
- ----------
109
- linger : int, optional
110
- If specified, set LINGER on sockets prior to closing them.
111
- """
112
- self.destroy(linger)
113
- self._ctx = zmq.Context()
114
-
115
104
 
116
105
  class ServerContext(BaseContext):
117
106
  """CurveZMQ server context
@@ -175,11 +164,6 @@ class ServerContext(BaseContext):
175
164
  self.auth_thread.stop()
176
165
  super().destroy(linger)
177
166
 
178
- def recreate(self, linger: Optional[int] = None):
179
- super().recreate(linger)
180
- if self.auth_thread:
181
- self.auth_thread = self._start_auth_thread()
182
-
183
167
 
184
168
  class ClientContext(BaseContext):
185
169
  """CurveZMQ client context
@@ -4,7 +4,6 @@ import os
4
4
  from functools import partial
5
5
  from typing import Optional
6
6
 
7
- import globus_sdk
8
7
  import typeguard
9
8
 
10
9
  import parsl
@@ -79,6 +78,7 @@ class Globus:
79
78
 
80
79
  @classmethod
81
80
  def transfer_file(cls, src_ep, dst_ep, src_path, dst_path):
81
+ import globus_sdk
82
82
  tc = globus_sdk.TransferClient(authorizer=cls.authorizer)
83
83
  td = globus_sdk.TransferData(tc, src_ep, dst_ep)
84
84
  td.add_item(src_path, dst_path)
@@ -140,6 +140,7 @@ class Globus:
140
140
  def _do_native_app_authentication(cls, client_id, redirect_uri,
141
141
  requested_scopes=None):
142
142
 
143
+ import globus_sdk
143
144
  client = globus_sdk.NativeAppAuthClient(client_id=client_id)
144
145
  client.oauth2_start_flow(
145
146
  requested_scopes=requested_scopes,
@@ -154,6 +155,7 @@ class Globus:
154
155
 
155
156
  @classmethod
156
157
  def _get_native_app_authorizer(cls, client_id):
158
+ import globus_sdk
157
159
  tokens = None
158
160
  try:
159
161
  tokens = cls._load_tokens_from_file(cls.TOKEN_FILE)