snakemake-executor-plugin-slurm 0.15.1__tar.gz → 1.0.1__tar.gz
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.
Potentially problematic release.
This version of snakemake-executor-plugin-slurm might be problematic. Click here for more details.
- {snakemake_executor_plugin_slurm-0.15.1 → snakemake_executor_plugin_slurm-1.0.1}/PKG-INFO +3 -2
- {snakemake_executor_plugin_slurm-0.15.1 → snakemake_executor_plugin_slurm-1.0.1}/README.md +1 -1
- {snakemake_executor_plugin_slurm-0.15.1 → snakemake_executor_plugin_slurm-1.0.1}/pyproject.toml +2 -2
- {snakemake_executor_plugin_slurm-0.15.1 → snakemake_executor_plugin_slurm-1.0.1}/snakemake_executor_plugin_slurm/__init__.py +48 -18
- snakemake_executor_plugin_slurm-1.0.1/snakemake_executor_plugin_slurm/utils.py +104 -0
- snakemake_executor_plugin_slurm-0.15.1/snakemake_executor_plugin_slurm/utils.py +0 -42
- {snakemake_executor_plugin_slurm-0.15.1 → snakemake_executor_plugin_slurm-1.0.1}/LICENSE +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: snakemake-executor-plugin-slurm
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 1.0.1
|
|
4
4
|
Summary: A Snakemake executor plugin for submitting jobs to a SLURM cluster.
|
|
5
5
|
License: MIT
|
|
6
6
|
Keywords: snakemake,plugin,executor,cluster,slurm
|
|
@@ -12,7 +12,7 @@ Classifier: Programming Language :: Python :: 3
|
|
|
12
12
|
Classifier: Programming Language :: Python :: 3.11
|
|
13
13
|
Classifier: Programming Language :: Python :: 3.12
|
|
14
14
|
Classifier: Programming Language :: Python :: 3.13
|
|
15
|
-
Requires-Dist: snakemake-executor-plugin-slurm-jobstep (>=0.
|
|
15
|
+
Requires-Dist: snakemake-executor-plugin-slurm-jobstep (>=0.3.0,<0.4.0)
|
|
16
16
|
Requires-Dist: snakemake-interface-common (>=1.13.0,<2.0.0)
|
|
17
17
|
Requires-Dist: snakemake-interface-executor-plugins (>=9.1.1,<10.0.0)
|
|
18
18
|
Requires-Dist: throttler (>=1.2.2,<2.0.0)
|
|
@@ -25,3 +25,4 @@ Description-Content-Type: text/markdown
|
|
|
25
25
|
[](https://gitpod.io/#https://github.com/snakemake/snakemake-executor-plugin-slurm)
|
|
26
26
|
|
|
27
27
|
For documentation, see the [Snakemake plugin catalog](https://snakemake.github.io/snakemake-plugin-catalog/plugins/executor/slurm.html).
|
|
28
|
+
|
|
@@ -2,4 +2,4 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://gitpod.io/#https://github.com/snakemake/snakemake-executor-plugin-slurm)
|
|
4
4
|
|
|
5
|
-
For documentation, see the [Snakemake plugin catalog](https://snakemake.github.io/snakemake-plugin-catalog/plugins/executor/slurm.html).
|
|
5
|
+
For documentation, see the [Snakemake plugin catalog](https://snakemake.github.io/snakemake-plugin-catalog/plugins/executor/slurm.html).
|
{snakemake_executor_plugin_slurm-0.15.1 → snakemake_executor_plugin_slurm-1.0.1}/pyproject.toml
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "snakemake-executor-plugin-slurm"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "1.0.1"
|
|
4
4
|
description = "A Snakemake executor plugin for submitting jobs to a SLURM cluster."
|
|
5
5
|
authors = [
|
|
6
6
|
"Christian Meesters <meesters@uni-mainz.de>",
|
|
@@ -17,7 +17,7 @@ keywords = ["snakemake", "plugin", "executor", "cluster", "slurm"]
|
|
|
17
17
|
python = "^3.11"
|
|
18
18
|
snakemake-interface-common = "^1.13.0"
|
|
19
19
|
snakemake-interface-executor-plugins = "^9.1.1"
|
|
20
|
-
snakemake-executor-plugin-slurm-jobstep = "^0.
|
|
20
|
+
snakemake-executor-plugin-slurm-jobstep = "^0.3.0"
|
|
21
21
|
throttler = "^1.2.2"
|
|
22
22
|
|
|
23
23
|
[tool.poetry.group.dev.dependencies]
|
|
@@ -26,9 +26,9 @@ from snakemake_interface_executor_plugins.jobs import (
|
|
|
26
26
|
JobExecutorInterface,
|
|
27
27
|
)
|
|
28
28
|
from snakemake_interface_common.exceptions import WorkflowError
|
|
29
|
-
from snakemake_executor_plugin_slurm_jobstep import
|
|
29
|
+
from snakemake_executor_plugin_slurm_jobstep import get_cpu_setting
|
|
30
30
|
|
|
31
|
-
from .utils import delete_slurm_environment, delete_empty_dirs
|
|
31
|
+
from .utils import delete_slurm_environment, delete_empty_dirs, set_gres_string
|
|
32
32
|
|
|
33
33
|
|
|
34
34
|
@dataclass
|
|
@@ -85,6 +85,15 @@ class ExecutorSettings(ExecutorSettingsBase):
|
|
|
85
85
|
"required": False,
|
|
86
86
|
},
|
|
87
87
|
)
|
|
88
|
+
no_account: bool = field(
|
|
89
|
+
default=False,
|
|
90
|
+
metadata={
|
|
91
|
+
"help": "Do not use any account for submission. "
|
|
92
|
+
"This flag has no effect, if not set.",
|
|
93
|
+
"env_var": False,
|
|
94
|
+
"required": False,
|
|
95
|
+
},
|
|
96
|
+
)
|
|
88
97
|
|
|
89
98
|
|
|
90
99
|
# Required:
|
|
@@ -180,7 +189,9 @@ class Executor(RemoteExecutor):
|
|
|
180
189
|
group_or_rule = f"group_{job.name}" if job.is_group() else f"rule_{job.name}"
|
|
181
190
|
|
|
182
191
|
try:
|
|
183
|
-
wildcard_str =
|
|
192
|
+
wildcard_str = (
|
|
193
|
+
"_".join(job.wildcards).replace("/", "_") if job.wildcards else ""
|
|
194
|
+
)
|
|
184
195
|
except AttributeError:
|
|
185
196
|
wildcard_str = ""
|
|
186
197
|
|
|
@@ -211,12 +222,16 @@ class Executor(RemoteExecutor):
|
|
|
211
222
|
f"--comment '{comment_str}'"
|
|
212
223
|
)
|
|
213
224
|
|
|
214
|
-
|
|
225
|
+
if not self.workflow.executor_settings.no_account:
|
|
226
|
+
call += self.get_account_arg(job)
|
|
227
|
+
|
|
215
228
|
call += self.get_partition_arg(job)
|
|
216
229
|
|
|
217
230
|
if self.workflow.executor_settings.requeue:
|
|
218
231
|
call += " --requeue"
|
|
219
232
|
|
|
233
|
+
call += set_gres_string(job)
|
|
234
|
+
|
|
220
235
|
if job.resources.get("clusters"):
|
|
221
236
|
call += f" --clusters {job.resources.clusters}"
|
|
222
237
|
|
|
@@ -247,7 +262,11 @@ class Executor(RemoteExecutor):
|
|
|
247
262
|
|
|
248
263
|
# fixes #40 - set ntasks regardless of mpi, because
|
|
249
264
|
# SLURM v22.05 will require it for all jobs
|
|
250
|
-
|
|
265
|
+
gpu_job = job.resources.get("gpu") or "gpu" in job.resources.get("gres", "")
|
|
266
|
+
if gpu_job:
|
|
267
|
+
call += f" --ntasks-per-gpu={job.resources.get('tasks', 1)}"
|
|
268
|
+
else:
|
|
269
|
+
call += f" --ntasks={job.resources.get('tasks', 1)}"
|
|
251
270
|
# MPI job
|
|
252
271
|
if job.resources.get("mpi", False):
|
|
253
272
|
if not job.resources.get("tasks_per_node") and not job.resources.get(
|
|
@@ -259,8 +278,9 @@ class Executor(RemoteExecutor):
|
|
|
259
278
|
"Probably not what you want."
|
|
260
279
|
)
|
|
261
280
|
|
|
262
|
-
|
|
263
|
-
|
|
281
|
+
# we need to set cpus-per-task OR cpus-per-gpu, the function
|
|
282
|
+
# will return a string with the corresponding value
|
|
283
|
+
call += f" {get_cpu_setting(job, gpu_job)}"
|
|
264
284
|
if job.resources.get("slurm_extra"):
|
|
265
285
|
self.check_slurm_extra(job)
|
|
266
286
|
call += f" {job.resources.slurm_extra}"
|
|
@@ -625,35 +645,45 @@ We leave it to SLURM to resume your job(s)"""
|
|
|
625
645
|
"""
|
|
626
646
|
tests whether the given account is registered, raises an error, if not
|
|
627
647
|
"""
|
|
628
|
-
cmd =
|
|
648
|
+
cmd = "sshare -U --format Account --noheader"
|
|
629
649
|
try:
|
|
630
650
|
accounts = subprocess.check_output(
|
|
631
651
|
cmd, shell=True, text=True, stderr=subprocess.PIPE
|
|
632
652
|
)
|
|
633
653
|
except subprocess.CalledProcessError as e:
|
|
634
|
-
|
|
635
|
-
"Unable to test the validity of the given or guessed
|
|
636
|
-
f"SLURM account '{account}' with
|
|
654
|
+
sshare_report = (
|
|
655
|
+
"Unable to test the validity of the given or guessed"
|
|
656
|
+
f" SLURM account '{account}' with sshare: {e.stderr}."
|
|
637
657
|
)
|
|
658
|
+
accounts = ""
|
|
659
|
+
|
|
660
|
+
if not accounts.strip():
|
|
661
|
+
cmd = f'sacctmgr -n -s list user "{os.environ["USER"]}" format=account%256'
|
|
638
662
|
try:
|
|
639
|
-
cmd = "sshare -U --format Account --noheader"
|
|
640
663
|
accounts = subprocess.check_output(
|
|
641
664
|
cmd, shell=True, text=True, stderr=subprocess.PIPE
|
|
642
665
|
)
|
|
643
|
-
except subprocess.CalledProcessError as
|
|
644
|
-
|
|
645
|
-
"Unable to test the validity of the given or guessed"
|
|
646
|
-
f"
|
|
666
|
+
except subprocess.CalledProcessError as e:
|
|
667
|
+
sacctmgr_report = (
|
|
668
|
+
"Unable to test the validity of the given or guessed "
|
|
669
|
+
f"SLURM account '{account}' with sacctmgr: {e.stderr}."
|
|
647
670
|
)
|
|
648
671
|
raise WorkflowError(
|
|
649
|
-
f"The '
|
|
650
|
-
f"and likewise '
|
|
672
|
+
f"The 'sshare' reported: '{sshare_report}' "
|
|
673
|
+
f"and likewise 'sacctmgr' reported: '{sacctmgr_report}'."
|
|
651
674
|
)
|
|
652
675
|
|
|
653
676
|
# The set() has been introduced during review to eliminate
|
|
654
677
|
# duplicates. They are not harmful, but disturbing to read.
|
|
655
678
|
accounts = set(_.strip() for _ in accounts.split("\n") if _)
|
|
656
679
|
|
|
680
|
+
if not accounts:
|
|
681
|
+
self.logger.warning(
|
|
682
|
+
f"Both 'sshare' and 'sacctmgr' returned empty results for account "
|
|
683
|
+
f"'{account}'. Proceeding without account validation."
|
|
684
|
+
)
|
|
685
|
+
return ""
|
|
686
|
+
|
|
657
687
|
if account not in accounts:
|
|
658
688
|
raise WorkflowError(
|
|
659
689
|
f"The given account {account} appears to be invalid. Available "
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# utility functions for the SLURM executor plugin
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import re
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from snakemake_interface_executor_plugins.jobs import (
|
|
8
|
+
JobExecutorInterface,
|
|
9
|
+
)
|
|
10
|
+
from snakemake_interface_common.exceptions import WorkflowError
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def delete_slurm_environment():
|
|
14
|
+
"""
|
|
15
|
+
Function to delete all environment variables
|
|
16
|
+
starting with 'SLURM_'. The parent shell will
|
|
17
|
+
still have this environment. This is needed to
|
|
18
|
+
submit within a SLURM job context to avoid
|
|
19
|
+
conflicting environments.
|
|
20
|
+
"""
|
|
21
|
+
for var in os.environ:
|
|
22
|
+
if var.startswith("SLURM_"):
|
|
23
|
+
del os.environ[var]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def delete_empty_dirs(path: Path) -> None:
|
|
27
|
+
"""
|
|
28
|
+
Function to delete all empty directories in a given path.
|
|
29
|
+
This is needed to clean up the working directory after
|
|
30
|
+
a job has sucessfully finished. This function is needed because
|
|
31
|
+
the shutil.rmtree() function does not delete empty
|
|
32
|
+
directories.
|
|
33
|
+
"""
|
|
34
|
+
if not path.is_dir():
|
|
35
|
+
return
|
|
36
|
+
|
|
37
|
+
# Process subdirectories first (bottom-up)
|
|
38
|
+
for child in path.iterdir():
|
|
39
|
+
if child.is_dir():
|
|
40
|
+
delete_empty_dirs(child)
|
|
41
|
+
|
|
42
|
+
try:
|
|
43
|
+
# Check if directory is now empty after processing children
|
|
44
|
+
if not any(path.iterdir()):
|
|
45
|
+
path.rmdir()
|
|
46
|
+
except (OSError, FileNotFoundError) as e:
|
|
47
|
+
# Provide more context in the error message
|
|
48
|
+
raise OSError(f"Failed to remove empty directory {path}: {e}") from e
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def set_gres_string(job: JobExecutorInterface) -> str:
|
|
52
|
+
"""
|
|
53
|
+
Function to set the gres string for the SLURM job
|
|
54
|
+
based on the resources requested in the job.
|
|
55
|
+
"""
|
|
56
|
+
# generic resources (GRES) arguments can be of type
|
|
57
|
+
# "string:int" or "string:string:int"
|
|
58
|
+
gres_re = re.compile(r"^[a-zA-Z0-9_]+(:[a-zA-Z0-9_]+)?:\d+$")
|
|
59
|
+
# gpu model arguments can be of type "string"
|
|
60
|
+
gpu_model_re = re.compile(r"^[a-zA-Z0-9_]+$")
|
|
61
|
+
# The Snakemake resources can be only be of type "int",
|
|
62
|
+
# hence no further regex is needed.
|
|
63
|
+
|
|
64
|
+
gpu_string = None
|
|
65
|
+
if job.resources.get("gpu"):
|
|
66
|
+
gpu_string = str(job.resources.get("gpu"))
|
|
67
|
+
|
|
68
|
+
gpu_model = None
|
|
69
|
+
if job.resources.get("gpu_model"):
|
|
70
|
+
gpu_model = job.resources.get("gpu_model")
|
|
71
|
+
|
|
72
|
+
# ensure that gres is not set, if gpu and gpu_model are set
|
|
73
|
+
if job.resources.get("gres") and gpu_string:
|
|
74
|
+
raise WorkflowError(
|
|
75
|
+
"GRES and GPU are set. Please only set one of them.", rule=job.rule
|
|
76
|
+
)
|
|
77
|
+
elif not job.resources.get("gres") and not gpu_model and not gpu_string:
|
|
78
|
+
return ""
|
|
79
|
+
|
|
80
|
+
if job.resources.get("gres"):
|
|
81
|
+
# Validate GRES format (e.g., "gpu:1", "gpu:tesla:2")
|
|
82
|
+
gres = job.resources.gres
|
|
83
|
+
if not gres_re.match(gres):
|
|
84
|
+
raise WorkflowError(
|
|
85
|
+
f"Invalid GRES format: {gres}. Expected format: "
|
|
86
|
+
"'<name>:<number>' or '<name>:<type>:<number>' "
|
|
87
|
+
"(e.g., 'gpu:1' or 'gpu:tesla:2')"
|
|
88
|
+
)
|
|
89
|
+
return f" --gres={job.resources.gres}"
|
|
90
|
+
|
|
91
|
+
if gpu_model and gpu_string:
|
|
92
|
+
# validate GPU model format
|
|
93
|
+
if not gpu_model_re.match(gpu_model):
|
|
94
|
+
raise WorkflowError(
|
|
95
|
+
f"Invalid GPU model format: {gpu_model}."
|
|
96
|
+
" Expected format: '<name>' (e.g., 'tesla')"
|
|
97
|
+
)
|
|
98
|
+
return f" --gpus={gpu_model}:{gpu_string}"
|
|
99
|
+
elif gpu_model and not gpu_string:
|
|
100
|
+
raise WorkflowError("GPU model is set, but no GPU number is given")
|
|
101
|
+
elif gpu_string:
|
|
102
|
+
# we assume here, that the validator ensures that the 'gpu_string'
|
|
103
|
+
# is an integer
|
|
104
|
+
return f" --gpus={gpu_string}"
|
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
# utility functions for the SLURM executor plugin
|
|
2
|
-
|
|
3
|
-
import os
|
|
4
|
-
from pathlib import Path
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
def delete_slurm_environment():
|
|
8
|
-
"""
|
|
9
|
-
Function to delete all environment variables
|
|
10
|
-
starting with 'SLURM_'. The parent shell will
|
|
11
|
-
still have this environment. This is needed to
|
|
12
|
-
submit within a SLURM job context to avoid
|
|
13
|
-
conflicting environments.
|
|
14
|
-
"""
|
|
15
|
-
for var in os.environ:
|
|
16
|
-
if var.startswith("SLURM_"):
|
|
17
|
-
del os.environ[var]
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
def delete_empty_dirs(path: Path) -> None:
|
|
21
|
-
"""
|
|
22
|
-
Function to delete all empty directories in a given path.
|
|
23
|
-
This is needed to clean up the working directory after
|
|
24
|
-
a job has sucessfully finished. This function is needed because
|
|
25
|
-
the shutil.rmtree() function does not delete empty
|
|
26
|
-
directories.
|
|
27
|
-
"""
|
|
28
|
-
if not path.is_dir():
|
|
29
|
-
return
|
|
30
|
-
|
|
31
|
-
# Process subdirectories first (bottom-up)
|
|
32
|
-
for child in path.iterdir():
|
|
33
|
-
if child.is_dir():
|
|
34
|
-
delete_empty_dirs(child)
|
|
35
|
-
|
|
36
|
-
try:
|
|
37
|
-
# Check if directory is now empty after processing children
|
|
38
|
-
if not any(path.iterdir()):
|
|
39
|
-
path.rmdir()
|
|
40
|
-
except (OSError, FileNotFoundError) as e:
|
|
41
|
-
# Provide more context in the error message
|
|
42
|
-
raise OSError(f"Failed to remove empty directory {path}: {e}") from e
|
|
File without changes
|