snakemake-executor-plugin-slurm 0.15.1__tar.gz → 1.0.0__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.

@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: snakemake-executor-plugin-slurm
3
- Version: 0.15.1
3
+ Version: 1.0.0
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.2.0,<0.3.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
  [![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](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
  [![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](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).
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "snakemake-executor-plugin-slurm"
3
- version = "0.15.1"
3
+ version = "1.0.0"
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.2.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 get_cpus_per_task
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
@@ -180,7 +180,9 @@ class Executor(RemoteExecutor):
180
180
  group_or_rule = f"group_{job.name}" if job.is_group() else f"rule_{job.name}"
181
181
 
182
182
  try:
183
- wildcard_str = "_".join(job.wildcards) if job.wildcards else ""
183
+ wildcard_str = (
184
+ "_".join(job.wildcards).replace("/", "_") if job.wildcards else ""
185
+ )
184
186
  except AttributeError:
185
187
  wildcard_str = ""
186
188
 
@@ -217,6 +219,8 @@ class Executor(RemoteExecutor):
217
219
  if self.workflow.executor_settings.requeue:
218
220
  call += " --requeue"
219
221
 
222
+ call += set_gres_string(job)
223
+
220
224
  if job.resources.get("clusters"):
221
225
  call += f" --clusters {job.resources.clusters}"
222
226
 
@@ -247,7 +251,11 @@ class Executor(RemoteExecutor):
247
251
 
248
252
  # fixes #40 - set ntasks regardless of mpi, because
249
253
  # SLURM v22.05 will require it for all jobs
250
- call += f" --ntasks={job.resources.get('tasks', 1)}"
254
+ gpu_job = job.resources.get("gpu") or "gpu" in job.resources.get("gres", "")
255
+ if gpu_job:
256
+ call += f" --ntasks-per-gpu={job.resources.get('tasks', 1)}"
257
+ else:
258
+ call += f" --ntasks={job.resources.get('tasks', 1)}"
251
259
  # MPI job
252
260
  if job.resources.get("mpi", False):
253
261
  if not job.resources.get("tasks_per_node") and not job.resources.get(
@@ -259,8 +267,9 @@ class Executor(RemoteExecutor):
259
267
  "Probably not what you want."
260
268
  )
261
269
 
262
- call += f" --cpus-per-task={get_cpus_per_task(job)}"
263
-
270
+ # we need to set cpus-per-task OR cpus-per-gpu, the function
271
+ # will return a string with the corresponding value
272
+ call += f" {get_cpu_setting(job, gpu_job)}"
264
273
  if job.resources.get("slurm_extra"):
265
274
  self.check_slurm_extra(job)
266
275
  call += f" {job.resources.slurm_extra}"
@@ -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