Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions docs/further.md
Original file line number Diff line number Diff line change
Expand Up @@ -356,6 +356,22 @@ When submitting array jobs, the `--slurm-array-limit` flag defines the
maximum number of array tasks to be submitted in one job submission.
If the number of tasks exceeds this limit, multiple array job submissions will be performed. This is useful to avoid hitting cluster limits on the maximum number of array tasks per job. Please obey your cluster limits and set this flag accordingly.

##### Array memory adjustment

By default, the plugin increases an explicit memory request for an array job to
account for the encoded array-job payload. If a job has no memory constraint,
the plugin adds a minimal `--mem` request so that this adjustment is not lost.

Some clusters derive memory allocation from other requested resources and do
not allow an explicit memory option. Disable the adjustment on such clusters:

```console
snakemake --slurm-disable-memory-fudge ...
```

The default is `false`, preserving the standard array submission behavior. In a
Snakemake profile, use `slurm-disable-memory-fudge: true` instead.


#### MPI-specific Resources

Expand Down
15 changes: 14 additions & 1 deletion snakemake_executor_plugin_slurm/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,18 @@ class ExecutorSettings(ExecutorSettingsBase):
},
)

disable_memory_fudge: bool = field(
default=False,
metadata={
"help": "Increase an explicit SLURM memory request for array jobs "
"to account for the encoded job payload. When no memory resource is "
"set, the executor adds a minimal --mem request. Disable this on "
"clusters whose memory allocation is derived from other resources.",
"env_var": False,
"required": False,
},
)

logdir: Optional[Path] = field(
default=None,
metadata={
Expand Down Expand Up @@ -911,7 +923,8 @@ def run_array_jobs(self, jobs: List[JobExecutorInterface]):
# add memory fudge factor to the base call,
# to account for the extra memory needed by the
# jobstep process to hold and parse the array execs payload.
call = apply_mem_fudge(call, array_execs_payload)
if not self.workflow.executor_settings.disable_memory_fudge:
call = apply_mem_fudge(call, array_execs_payload)

use_script_submission = (
self.workflow.executor_settings.pass_command_as_script
Expand Down
28 changes: 28 additions & 0 deletions tests/test_array_jobs.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ def _make_executor_stub(array_jobs=None, array_limit=100):
array_limit=array_limit,
status_attempts=1,
init_seconds_before_status_checks=40,
disable_memory_fudge=False,
keep_successful_logs=False,
requeue=False,
no_requeue=False,
Expand Down Expand Up @@ -131,6 +132,11 @@ def test_array_limit_default_is_1000(self):
settings = ExecutorSettings()
assert settings.array_limit == 1000

def test_disable_memory_fudge_defaults_to_false(self):
"""Existing array memory behavior remains enabled by default."""
settings = ExecutorSettings()
assert settings.disable_memory_fudge is False

def test_array_jobs_none_yields_empty_set_on_executor(self):
"""Executor with array_jobs=None initialises self.array_jobs as empty set."""
executor = _make_executor_stub(array_jobs=None)
Expand Down Expand Up @@ -396,6 +402,28 @@ def test_array_execs_task_1_absent_tasks_2_plus_present(
assert "2" in array_execs
assert "3" in array_execs

def test_memory_fudge_can_be_disabled(self, tmp_path, mock_popen_success):
executor = self._build_executor(tmp_path)
executor.workflow.executor_settings.disable_memory_fudge = True
jobs = self._make_jobs(n=2)

executor.run_array_jobs(jobs)

popen_call_str = mock_popen_success.call_args_list[0][0][0]
assert "--mem " not in popen_call_str
assert "--mem-per-cpu " not in popen_call_str

def test_memory_fudge_remains_enabled_by_default(
self, tmp_path, mock_popen_success
):
executor = self._build_executor(tmp_path)
jobs = self._make_jobs(n=2)

executor.run_array_jobs(jobs)

popen_call_str = mock_popen_success.call_args_list[0][0][0]
assert "--mem 1" in popen_call_str

def test_array_execs_omits_first_task_of_each_chunk(self, tmp_path):
"""For each chunk, first task uses base exec command and is absent from map."""
executor = self._build_executor(tmp_path, array_limit=3)
Expand Down
30 changes: 30 additions & 0 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,31 @@
executor functionality.
"""

from argparse import ArgumentParser
from unittest.mock import MagicMock, patch
import uuid

import pytest

from snakemake_executor_plugin_slurm import Executor, ExecutorSettings
from snakemake_interface_common.exceptions import WorkflowError
from snakemake_interface_common.plugin_registry.plugin import PluginBase


class _SlurmSettingsPlugin(PluginBase[ExecutorSettings]):
"""Minimal plugin wrapper for exercising the settings resolution path."""

@property
def name(self) -> str:
return "slurm"

@property
def cli_prefix(self) -> str:
return "slurm"

@property
def settings_cls(self):
return ExecutorSettings


def _make_executor(jobname_prefix: str):
Expand Down Expand Up @@ -50,6 +68,18 @@ def test_jobname_prefix_validation():
executor.__post_init__(test_mode=True)


def test_disable_memory_fudge_flag_resolves_from_cli():
"""CLI flag is a no-argument switch that sets disable_memory_fudge=True."""
plugin = _SlurmSettingsPlugin()
parser = ArgumentParser()
plugin.register_cli_args(parser, "executor")

args = parser.parse_args(["--slurm-disable-memory-fudge"])
settings = plugin.get_settings(args)

assert settings.disable_memory_fudge is True


def test_requeue_options_are_mutually_exclusive():
with pytest.raises(WorkflowError, match="mutually exclusive"):
ExecutorSettings(requeue=True, no_requeue=True)
Loading