Skip to content

Commit 52f5e83

Browse files
committed
Add --throttle option to babs init for SLURM array job throttling
1 parent 5a9f9bc commit 52f5e83

File tree

6 files changed

+94
-0
lines changed

6 files changed

+94
-0
lines changed

babs/bootstrap.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ def babs_bootstrap(
3737
container_name,
3838
container_config,
3939
initial_inclusion_df=None,
40+
throttle=None,
4041
):
4142
"""
4243
Bootstrap a babs project: initialize datalad-tracked RIAs, generate scripts to be used, etc
@@ -57,6 +58,11 @@ def babs_bootstrap(
5758
of how to run the BIDS App container
5859
initial_inclusion_df: pd.DataFrame
5960
initial inclusion dataframe of subjects/sessions to analyze
61+
throttle: int or None, optional
62+
Optional throttle value for SLURM array jobs. This limits the number of
63+
simultaneously running array tasks. The value will be added to the array
64+
specification as `%<throttle>`. Example: `10` will result in
65+
`--array=1-${max_array}%10`.
6066
"""
6167
if op.exists(self.project_root):
6268
raise FileExistsError(
@@ -79,6 +85,9 @@ def babs_bootstrap(
7985

8086
os.makedirs(self.project_root)
8187

88+
# Store throttle value for job submission template
89+
self.throttle = throttle
90+
8291
# validate `processing_level`:
8392
self.processing_level = validate_processing_level(processing_level)
8493

babs/cli.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,14 @@ def _parse_init():
118118
" Please refer to section below 'What if ``babs init`` fails?' for details.",
119119
# ^^ in `babs init.rst`, pointed to below section for more
120120
)
121+
parser.add_argument(
122+
'--throttle',
123+
type=int,
124+
help='Optional throttle value for SLURM array jobs. '
125+
'This limits the number of simultaneously running array tasks. '
126+
'The value will be added to the array specification as ``%%<throttle>``. '
127+
'Example: ``--throttle 10`` will result in ``--array=1-${max_array}%%10``.',
128+
)
121129

122130
return parser
123131

@@ -148,6 +156,7 @@ def babs_init_main(
148156
processing_level: str,
149157
queue: str,
150158
keep_if_failed: bool,
159+
throttle: int | None = None,
151160
):
152161
"""This is the core function of babs init.
153162
@@ -175,6 +184,11 @@ def babs_init_main(
175184
sge or slurm
176185
keep_if_failed: bool
177186
If `babs init` failed with error, whether to keep the created BABS project.
187+
throttle: int or None, optional
188+
Optional throttle value for SLURM array jobs. This limits the number of
189+
simultaneously running array tasks. The value will be added to the array
190+
specification as `%<throttle>`. Example: `10` will result in
191+
`--array=1-${max_array}%10`.
178192
"""
179193

180194
from babs import BABSBootstrap
@@ -188,6 +202,7 @@ def babs_init_main(
188202
container_name,
189203
container_config,
190204
list_sub_file,
205+
throttle=throttle,
191206
)
192207
except Exception as exc:
193208
print('\n`babs init` failed! Below is the error message:')

babs/container.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,9 @@ def generate_job_submit_template(self, yaml_path, babs, system, test=False):
274274
array_args = '--array=1'
275275
else: # need max_array for for `submit_job_template.yaml`
276276
array_args = '--array=1-${max_array}'
277+
# Add throttle if specified
278+
if hasattr(babs, 'throttle') and babs.throttle is not None:
279+
array_args += f'%{babs.throttle}'
277280

278281
# Render the template
279282
env = Environment(

docs/babs-init.rst

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,14 @@ a SLURM cluster:
104104
--queue slurm \
105105
/path/to/a/folder/holding/BABS/project/my_BABS_project
106106
107+
.. note::
108+
**Throttling SLURM array jobs**: If you want to limit the number of simultaneously running
109+
array tasks, you can add the ``--throttle`` option. For example, ``--throttle 10`` will
110+
limit SLURM to run at most 10 array tasks at the same time. This is useful when you have
111+
many jobs but want to avoid overwhelming the cluster or hitting resource limits.
112+
The throttle value will be added to the array specification as ``%<throttle>``,
113+
e.g., ``--array=1-${max_array}%10``.
114+
107115

108116
*********
109117
Debugging

docs/walkthrough.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -354,6 +354,12 @@ and results and provenance are saved. An example command of ``babs init`` is as
354354
--queue slurm \
355355
"${HOME}/babs_demo/my_BABS_project"
356356
357+
.. note::
358+
**Optional: Throttling array jobs**: If you have many jobs and want to limit how many
359+
run simultaneously, you can add ``--throttle <number>`` to the command above.
360+
For example, ``--throttle 10`` will limit SLURM to run at most 10 array tasks at once.
361+
This helps avoid overwhelming the cluster or hitting resource limits when submitting
362+
large numbers of array tasks.
357363

358364
Here you will create a BABS project called ``my_BABS_project`` in directory ``~/babs_demo``.
359365
The input dataset is specified in the yaml file and no longer specified in the command line.

tests/test_base.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
"""Test the check_setup functionality."""
22

3+
import os
34
import os.path as op
45
import random
6+
import re
57
from pathlib import Path
68
from unittest.mock import MagicMock
79

@@ -202,3 +204,54 @@ def test_key_info_full(babs_project_sessionlevel):
202204
babs_proj.wtf_key_info(flag_output_ria_only=False)
203205
assert babs_proj.output_ria_data_dir is not None
204206
assert babs_proj.analysis_dataset_id is not None
207+
208+
209+
@pytest.mark.parametrize(
210+
('throttle_value', 'expected_in_template'),
211+
[(10, True), (None, False)],
212+
)
213+
def test_throttle_in_job_template(
214+
tmp_path_factory,
215+
templateflow_home,
216+
simbids_container_ds,
217+
bids_data_singlesession,
218+
throttle_value,
219+
expected_in_template,
220+
):
221+
"""Test that throttle value is correctly included in job submission template."""
222+
from conftest import get_config_simbids_path, update_yaml_for_run
223+
224+
from babs.bootstrap import BABSBootstrap
225+
226+
os.environ['TEMPLATEFLOW_HOME'] = str(templateflow_home)
227+
228+
project_base = tmp_path_factory.mktemp('project')
229+
project_root = project_base / f'my_babs_project_{throttle_value or "none"}'
230+
container_config = update_yaml_for_run(
231+
project_base,
232+
get_config_simbids_path().name,
233+
{'BIDS': bids_data_singlesession},
234+
)
235+
236+
babs_bootstrap = BABSBootstrap(project_root=project_root)
237+
babs_bootstrap.babs_bootstrap(
238+
processing_level='subject',
239+
queue='slurm',
240+
container_ds=simbids_container_ds,
241+
container_name='simbids-0-0-3',
242+
container_config=container_config,
243+
initial_inclusion_df=None,
244+
throttle=throttle_value,
245+
)
246+
247+
assert babs_bootstrap.throttle == throttle_value
248+
249+
template_path = op.join(babs_bootstrap.analysis_path, 'code', 'submit_job_template.yaml')
250+
with open(template_path) as f:
251+
cmd_template = yaml.safe_load(f)['cmd_template']
252+
253+
assert '--array=1-${max_array}' in cmd_template
254+
if expected_in_template:
255+
assert f'%{throttle_value}' in cmd_template
256+
else:
257+
assert not re.search(r'%\d+', cmd_template)

0 commit comments

Comments
 (0)