parsl 2023.5.29__py3-none-any.whl → 2023.6.12__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.
- parsl/addresses.py +2 -1
- parsl/app/errors.py +6 -20
- parsl/benchmark/perf.py +3 -1
- parsl/configs/vineex_local.py +26 -0
- parsl/data_provider/data_manager.py +2 -1
- parsl/data_provider/files.py +1 -1
- parsl/data_provider/globus.py +1 -1
- parsl/dataflow/memoization.py +1 -1
- parsl/executors/taskvine/__init__.py +3 -0
- parsl/executors/taskvine/errors.py +22 -0
- parsl/executors/taskvine/exec_parsl_function.py +207 -0
- parsl/executors/taskvine/executor.py +1055 -0
- parsl/executors/workqueue/executor.py +9 -7
- parsl/launchers/base.py +17 -0
- parsl/launchers/launchers.py +1 -16
- parsl/monitoring/monitoring.py +19 -8
- parsl/monitoring/visualization/plots/default/workflow_plots.py +32 -29
- parsl/providers/cluster_provider.py +2 -2
- parsl/providers/condor/condor.py +1 -1
- parsl/providers/kubernetes/kube.py +2 -1
- parsl/providers/slurm/slurm.py +1 -1
- parsl/tests/configs/taskvine_ex.py +11 -0
- parsl/tests/conftest.py +6 -6
- parsl/tests/scaling_tests/vineex_condor.py +10 -0
- parsl/tests/scaling_tests/vineex_local.py +10 -0
- parsl/tests/test_bash_apps/test_pipeline.py +2 -2
- parsl/tests/test_error_handling/test_retry_handler.py +1 -1
- parsl/tests/test_monitoring/test_viz_colouring.py +17 -0
- parsl/utils.py +2 -2
- parsl/version.py +1 -1
- {parsl-2023.5.29.dist-info → parsl-2023.6.12.dist-info}/METADATA +3 -3
- {parsl-2023.5.29.dist-info → parsl-2023.6.12.dist-info}/RECORD +45 -36
- parsl/tests/configs/workqueue_blocks.py +0 -12
- /parsl/tests/{workqueue_tests → scaling_tests}/__init__.py +0 -0
- /parsl/tests/{workqueue_tests → scaling_tests}/htex_local.py +0 -0
- /parsl/tests/{workqueue_tests → scaling_tests}/local_threads.py +0 -0
- /parsl/tests/{workqueue_tests → scaling_tests}/test_scale.py +0 -0
- /parsl/tests/{workqueue_tests → scaling_tests}/wqex_condor.py +0 -0
- /parsl/tests/{workqueue_tests → scaling_tests}/wqex_local.py +0 -0
- {parsl-2023.5.29.data → parsl-2023.6.12.data}/scripts/exec_parsl_function.py +0 -0
- {parsl-2023.5.29.data → parsl-2023.6.12.data}/scripts/parsl_coprocess.py +0 -0
- {parsl-2023.5.29.data → parsl-2023.6.12.data}/scripts/process_worker_pool.py +0 -0
- {parsl-2023.5.29.dist-info → parsl-2023.6.12.dist-info}/LICENSE +0 -0
- {parsl-2023.5.29.dist-info → parsl-2023.6.12.dist-info}/WHEEL +0 -0
- {parsl-2023.5.29.dist-info → parsl-2023.6.12.dist-info}/entry_points.txt +0 -0
- {parsl-2023.5.29.dist-info → parsl-2023.6.12.dist-info}/top_level.txt +0 -0
parsl/addresses.py
CHANGED
@@ -113,7 +113,8 @@ def get_all_addresses() -> Set[str]:
|
|
113
113
|
logger.exception("Ignoring failure to fetch address from interface {}".format(interface))
|
114
114
|
pass
|
115
115
|
|
116
|
-
resolution_functions
|
116
|
+
resolution_functions: List[Callable[[], str]]
|
117
|
+
resolution_functions = [address_by_hostname, address_by_route, address_by_query]
|
117
118
|
for f in resolution_functions:
|
118
119
|
try:
|
119
120
|
s_addresses.add(f())
|
parsl/app/errors.py
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
"""Exceptions raised by Apps."""
|
2
2
|
from functools import wraps
|
3
|
-
from typing import Callable, List,
|
3
|
+
from typing import Callable, List, Optional, TypeVar, Union
|
4
|
+
from typing_extensions import ParamSpec
|
4
5
|
from types import TracebackType
|
5
6
|
import logging
|
6
7
|
from tblib import Traceback
|
@@ -132,28 +133,13 @@ class RemoteExceptionWrapper:
|
|
132
133
|
return v
|
133
134
|
|
134
135
|
|
136
|
+
P = ParamSpec('P')
|
135
137
|
R = TypeVar('R')
|
136
138
|
|
137
|
-
|
138
|
-
|
139
|
-
# However, there is no provision in Python typing for pattern matching all possible types of
|
140
|
-
# callable arguments. This is because Callable[] is, in the infinite wisdom of the typing module,
|
141
|
-
# only used for callbacks: "There is no syntax to indicate optional or keyword arguments; such
|
142
|
-
# function types are rarely used as callback types.".
|
143
|
-
# The alternative supported by the typing module, of saying Callable[..., R] ->
|
144
|
-
# Callable[..., Union[R, R2]] results in no pattern matching between the first and second
|
145
|
-
# ellipsis.
|
146
|
-
# Yet another bogus solution that was here previously would simply define wrap_error as
|
147
|
-
# wrap_error(T) -> T, where T was a custom TypeVar. This obviously missed the fact that
|
148
|
-
# the returned function had its return signature modified.
|
149
|
-
# Ultimately, the best choice appears to be Callable[..., R] -> Callable[..., Union[R, ?Exception]],
|
150
|
-
# since it results in the correct type specification for the return value(s) while treating the
|
151
|
-
# arguments as Any.
|
152
|
-
|
153
|
-
|
154
|
-
def wrap_error(func: Callable[..., R]) -> Callable[..., Union[R, RemoteExceptionWrapper]]:
|
139
|
+
|
140
|
+
def wrap_error(func: Callable[P, R]) -> Callable[P, Union[R, RemoteExceptionWrapper]]:
|
155
141
|
@wraps(func)
|
156
|
-
def wrapper(*args:
|
142
|
+
def wrapper(*args: P.args, **kwargs: P.kwargs) -> Union[R, RemoteExceptionWrapper]:
|
157
143
|
import sys
|
158
144
|
from parsl.app.errors import RemoteExceptionWrapper
|
159
145
|
try:
|
parsl/benchmark/perf.py
CHANGED
@@ -4,6 +4,8 @@ import time
|
|
4
4
|
import concurrent.futures
|
5
5
|
import parsl
|
6
6
|
|
7
|
+
min_iterations = 2
|
8
|
+
|
7
9
|
|
8
10
|
# TODO: factor with conftest.py where this is copy/pasted from?
|
9
11
|
def load_dfk_from_config(filename):
|
@@ -34,7 +36,7 @@ def performance(*, resources: dict, target_t: float):
|
|
34
36
|
|
35
37
|
iteration = 1
|
36
38
|
|
37
|
-
while delta_t < threshold_t:
|
39
|
+
while delta_t < threshold_t or iteration <= min_iterations:
|
38
40
|
print(f"==== Iteration {iteration} ====")
|
39
41
|
print(f"Will run {n} tasks to target {target_t} seconds runtime")
|
40
42
|
start_t = time.time()
|
@@ -0,0 +1,26 @@
|
|
1
|
+
from parsl.config import Config
|
2
|
+
from parsl.executors.taskvine import TaskVineExecutor
|
3
|
+
|
4
|
+
import uuid
|
5
|
+
|
6
|
+
config = Config(
|
7
|
+
executors=[
|
8
|
+
TaskVineExecutor(
|
9
|
+
label="parsl-vine-example",
|
10
|
+
|
11
|
+
# If a project_name is given, then TaskVine will periodically
|
12
|
+
# report its status and performance back to the global TaskVine catalog,
|
13
|
+
# which can be viewed here: http://ccl.cse.nd.edu/software/taskvine/status
|
14
|
+
|
15
|
+
# To disable status reporting, comment out the project_name.
|
16
|
+
project_name="parsl-vine-" + str(uuid.uuid4()),
|
17
|
+
|
18
|
+
# The port number that TaskVine will listen on for connecting workers
|
19
|
+
# 0 means a random port.
|
20
|
+
port=0,
|
21
|
+
|
22
|
+
# A shared filesystem is not needed when using TaskVine.
|
23
|
+
shared_fs=False
|
24
|
+
)
|
25
|
+
]
|
26
|
+
)
|
@@ -16,7 +16,8 @@ logger = logging.getLogger(__name__)
|
|
16
16
|
|
17
17
|
# these will be shared between all executors that do not explicitly
|
18
18
|
# override, so should not contain executor-specific state
|
19
|
-
default_staging
|
19
|
+
default_staging: List[Staging]
|
20
|
+
default_staging = [NoOpFileStaging(), FTPSeparateTaskStaging(), HTTPSeparateTaskStaging()]
|
20
21
|
|
21
22
|
|
22
23
|
class DataManager:
|
parsl/data_provider/files.py
CHANGED
@@ -44,7 +44,7 @@ class File:
|
|
44
44
|
self.netloc = parsed_url.netloc
|
45
45
|
self.path = parsed_url.path
|
46
46
|
self.filename = os.path.basename(self.path)
|
47
|
-
self.local_path
|
47
|
+
self.local_path: Optional[str] = None
|
48
48
|
|
49
49
|
def cleancopy(self) -> "File":
|
50
50
|
"""Returns a copy of the file containing only the global immutable state,
|
parsl/data_provider/globus.py
CHANGED
@@ -94,7 +94,7 @@ class Globus:
|
|
94
94
|
with 60 second timeout limit. If the task is ACTIVE after time runs out 'task_wait' returns False,
|
95
95
|
and True otherwise.
|
96
96
|
"""
|
97
|
-
while not tc.task_wait(task['task_id'], 60
|
97
|
+
while not tc.task_wait(task['task_id'], timeout=60):
|
98
98
|
task = tc.get_task(task['task_id'])
|
99
99
|
# Get the last error Globus event
|
100
100
|
events = tc.task_event_list(task['task_id'], num_results=1, filter='is_error:1')
|
parsl/dataflow/memoization.py
CHANGED
@@ -176,7 +176,7 @@ class Memoizer:
|
|
176
176
|
- hash (str) : A unique hash string
|
177
177
|
"""
|
178
178
|
|
179
|
-
t
|
179
|
+
t: List[bytes] = []
|
180
180
|
|
181
181
|
# if kwargs contains an outputs parameter, that parameter is removed
|
182
182
|
# and normalised differently - with output_ref set to True.
|
@@ -0,0 +1,22 @@
|
|
1
|
+
from parsl.errors import ParslError
|
2
|
+
from parsl.app.errors import AppException
|
3
|
+
|
4
|
+
|
5
|
+
class TaskVineTaskFailure(AppException):
|
6
|
+
"""A failure executing a task in taskvine
|
7
|
+
|
8
|
+
Contains:
|
9
|
+
reason(string)
|
10
|
+
status(int)
|
11
|
+
"""
|
12
|
+
|
13
|
+
def __init__(self, reason, status):
|
14
|
+
self.reason = reason
|
15
|
+
self.status = status
|
16
|
+
|
17
|
+
|
18
|
+
class TaskVineFailure(ParslError):
|
19
|
+
"""A failure in the taskvine executor that prevented the task to be
|
20
|
+
executed.""
|
21
|
+
"""
|
22
|
+
pass
|
@@ -0,0 +1,207 @@
|
|
1
|
+
from parsl.app.errors import RemoteExceptionWrapper
|
2
|
+
from parsl.data_provider.files import File
|
3
|
+
from parsl.utils import get_std_fname_mode
|
4
|
+
import traceback
|
5
|
+
import sys
|
6
|
+
import pickle
|
7
|
+
|
8
|
+
# This scripts executes a parsl function which is pickled in a file:
|
9
|
+
#
|
10
|
+
# exec_parsl_function.py map_file function_file result_file
|
11
|
+
#
|
12
|
+
# map_file: Contains a pickled dictionary that indicates which local_paths the
|
13
|
+
# parsl Files should take.
|
14
|
+
#
|
15
|
+
# function_file: Contains a pickle parsl function.
|
16
|
+
#
|
17
|
+
# result_file: It will contain the result of the function, including any
|
18
|
+
# exception generated. Exceptions will be wrapped with RemoteExceptionWrapper.
|
19
|
+
#
|
20
|
+
# Exit codes:
|
21
|
+
# 0: The function was evaluated to completion. The result or any exception
|
22
|
+
# wrapped with RemoteExceptionWrapper were written to result_file.
|
23
|
+
# anything else: There was an error that prevented writing to the result file altogether.
|
24
|
+
# The exit code corresponds to whatever the python interpreter gives.
|
25
|
+
#
|
26
|
+
|
27
|
+
|
28
|
+
def load_pickled_file(filename):
|
29
|
+
with open(filename, "rb") as f_in:
|
30
|
+
return pickle.load(f_in)
|
31
|
+
|
32
|
+
|
33
|
+
def dump_result_to_file(result_file, result_package):
|
34
|
+
with open(result_file, "wb") as f_out:
|
35
|
+
pickle.dump(result_package, f_out)
|
36
|
+
|
37
|
+
|
38
|
+
def remap_location(mapping, parsl_file):
|
39
|
+
if not isinstance(parsl_file, File):
|
40
|
+
return
|
41
|
+
# Below we rewrite .local_path when scheme != file only when the local_name
|
42
|
+
# was given by the main parsl process. This is the case when scheme !=
|
43
|
+
# 'file' but .local_path (via filepath) is in mapping.
|
44
|
+
if parsl_file.scheme == 'file' or parsl_file.local_path:
|
45
|
+
master_location = parsl_file.filepath
|
46
|
+
if master_location in mapping:
|
47
|
+
parsl_file.local_path = mapping[master_location]
|
48
|
+
|
49
|
+
|
50
|
+
def remap_list_of_files(mapping, maybe_files):
|
51
|
+
for maybe_file in maybe_files:
|
52
|
+
remap_location(mapping, maybe_file)
|
53
|
+
|
54
|
+
|
55
|
+
def remap_all_files(mapping, fn_args, fn_kwargs):
|
56
|
+
# remap any positional argument given to the function that looks like a
|
57
|
+
# File
|
58
|
+
remap_list_of_files(mapping, fn_args)
|
59
|
+
|
60
|
+
# remap any keyword argument in the same way, but we need to treat
|
61
|
+
# "inputs" and "outputs" specially because they are lists, and
|
62
|
+
# "stdout" and "stderr", because they are not File's.
|
63
|
+
for kwarg, maybe_file in fn_kwargs.items():
|
64
|
+
if kwarg in ["inputs", "outputs"]:
|
65
|
+
remap_list_of_files(mapping, maybe_file)
|
66
|
+
if kwarg in ["stdout", "stderr"]:
|
67
|
+
if maybe_file:
|
68
|
+
(fname, mode) = get_std_fname_mode(kwarg, maybe_file)
|
69
|
+
if fname in mapping:
|
70
|
+
fn_kwargs[kwarg] = (mapping[fname], mode)
|
71
|
+
else:
|
72
|
+
# Treat anything else as a possible File to be remapped.
|
73
|
+
remap_location(mapping, maybe_file)
|
74
|
+
|
75
|
+
|
76
|
+
def unpack_function(function_info, user_namespace):
|
77
|
+
if "source code" in function_info:
|
78
|
+
return unpack_source_code_function(function_info, user_namespace)
|
79
|
+
elif "byte code" in function_info:
|
80
|
+
return unpack_byte_code_function(function_info, user_namespace)
|
81
|
+
else:
|
82
|
+
raise ValueError("Function file does not have a valid function representation.")
|
83
|
+
|
84
|
+
|
85
|
+
def unpack_source_code_function(function_info, user_namespace):
|
86
|
+
source_code = function_info["source code"]
|
87
|
+
name = function_info["name"]
|
88
|
+
args = function_info["args"]
|
89
|
+
kwargs = function_info["kwargs"]
|
90
|
+
return (source_code, name, args, kwargs)
|
91
|
+
|
92
|
+
|
93
|
+
def unpack_byte_code_function(function_info, user_namespace):
|
94
|
+
from parsl.serialize import unpack_apply_message
|
95
|
+
func, args, kwargs = unpack_apply_message(function_info["byte code"], user_namespace, copy=False)
|
96
|
+
return (func, 'parsl_function_name', args, kwargs)
|
97
|
+
|
98
|
+
|
99
|
+
def encode_function(user_namespace, fn, fn_name, fn_args, fn_kwargs):
|
100
|
+
# Returns a tuple (code, result_name)
|
101
|
+
# code can be exec in the user_namespace to produce result_name.
|
102
|
+
prefix = "parsl_"
|
103
|
+
args_name = prefix + "args"
|
104
|
+
kwargs_name = prefix + "kwargs"
|
105
|
+
result_name = prefix + "result"
|
106
|
+
|
107
|
+
# Add variables to the namespace to make function call
|
108
|
+
user_namespace.update({args_name: fn_args,
|
109
|
+
kwargs_name: fn_kwargs,
|
110
|
+
result_name: result_name})
|
111
|
+
|
112
|
+
if isinstance(fn, str):
|
113
|
+
code = encode_source_code_function(user_namespace, fn, fn_name, args_name, kwargs_name, result_name)
|
114
|
+
elif callable(fn):
|
115
|
+
code = encode_byte_code_function(user_namespace, fn, fn_name, args_name, kwargs_name, result_name)
|
116
|
+
else:
|
117
|
+
raise ValueError("Function object does not look like a function.")
|
118
|
+
|
119
|
+
return (code, result_name)
|
120
|
+
|
121
|
+
|
122
|
+
def encode_source_code_function(user_namespace, fn, fn_name, args_name, kwargs_name, result_name):
|
123
|
+
# We drop the first line as it names the parsl decorator used (i.e., @python_app)
|
124
|
+
source = fn.split('\n')[1:]
|
125
|
+
fn_app = "{0} = {1}(*{2}, **{3})".format(result_name, fn_name, args_name, kwargs_name)
|
126
|
+
|
127
|
+
source.append(fn_app)
|
128
|
+
|
129
|
+
code = "\n".join(source)
|
130
|
+
return code
|
131
|
+
|
132
|
+
|
133
|
+
def encode_byte_code_function(user_namespace, fn, fn_name, args_name, kwargs_name, result_name):
|
134
|
+
user_namespace.update({fn_name: fn})
|
135
|
+
code = "{0} = {1}(*{2}, **{3})".format(result_name, fn_name, args_name, kwargs_name)
|
136
|
+
return code
|
137
|
+
|
138
|
+
|
139
|
+
def load_function(map_file, function_file):
|
140
|
+
# Decodes the function and its file arguments to be executed into
|
141
|
+
# function_code, and updates a user namespace with the function name and
|
142
|
+
# the variable named result_name. When the function is executed, its result
|
143
|
+
# will be stored in this variable in the user namespace.
|
144
|
+
# Returns (namespace, function_code, result_name)
|
145
|
+
|
146
|
+
# Create the namespace to isolate the function execution.
|
147
|
+
user_ns = locals()
|
148
|
+
user_ns.update({'__builtins__': __builtins__})
|
149
|
+
|
150
|
+
function_info = load_pickled_file(function_file)
|
151
|
+
|
152
|
+
(fn, fn_name, fn_args, fn_kwargs) = unpack_function(function_info, user_ns)
|
153
|
+
|
154
|
+
mapping = load_pickled_file(map_file)
|
155
|
+
remap_all_files(mapping, fn_args, fn_kwargs)
|
156
|
+
|
157
|
+
(code, result_name) = encode_function(user_ns, fn, fn_name, fn_args, fn_kwargs)
|
158
|
+
|
159
|
+
return (user_ns, code, result_name)
|
160
|
+
|
161
|
+
|
162
|
+
def execute_function(namespace, function_code, result_name):
|
163
|
+
# On executing the function inside the namespace, its result will be in a
|
164
|
+
# variable named result_name.
|
165
|
+
|
166
|
+
exec(function_code, namespace, namespace)
|
167
|
+
result = namespace.get(result_name)
|
168
|
+
|
169
|
+
return result
|
170
|
+
|
171
|
+
|
172
|
+
if __name__ == "__main__":
|
173
|
+
try:
|
174
|
+
# parse the three required command line arguments:
|
175
|
+
# map_file: contains a pickled dictionary to map original names to
|
176
|
+
# names at the execution site.
|
177
|
+
# function_file: contains the pickled parsl function to execute.
|
178
|
+
# result_file: any output (including exceptions) will be written to
|
179
|
+
# this file.
|
180
|
+
try:
|
181
|
+
(map_file, function_file, result_file) = sys.argv[1:]
|
182
|
+
except ValueError:
|
183
|
+
print("Usage:\n\t{} function result mapping\n".format(sys.argv[0]))
|
184
|
+
raise
|
185
|
+
|
186
|
+
try:
|
187
|
+
(namespace, function_code, result_name) = load_function(map_file, function_file)
|
188
|
+
except Exception:
|
189
|
+
print("There was an error setting up the function for execution.")
|
190
|
+
raise
|
191
|
+
|
192
|
+
try:
|
193
|
+
result = execute_function(namespace, function_code, result_name)
|
194
|
+
except Exception:
|
195
|
+
print("There was an error executing the function.")
|
196
|
+
raise
|
197
|
+
except Exception:
|
198
|
+
traceback.print_exc()
|
199
|
+
result = RemoteExceptionWrapper(*sys.exc_info())
|
200
|
+
|
201
|
+
# Write out function result to the result file
|
202
|
+
try:
|
203
|
+
dump_result_to_file(result_file, result)
|
204
|
+
except Exception:
|
205
|
+
print("Could not write to result file.")
|
206
|
+
traceback.print_exc()
|
207
|
+
sys.exit(1)
|