Skip to content

Commit fb9e693

Browse files
authored
allow customization of all SBATCH header fields (#202)
* add sbatch headers * add tests * update generate_slurm_header * update test * add `slurm.yml`
1 parent 37c22a8 commit fb9e693

4 files changed

Lines changed: 468 additions & 21 deletions

File tree

example/slurm.yml

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
general:
2+
max_concurrent: 4 # How many should be executed at the same time
3+
ncores: 96
4+
execution: slurm
5+
# arbitrary #SBATCH directives, added on top of/overriding the above
6+
# if not defined, SLURM will use it's default, keep in mind
7+
# this will change by system
8+
slurm_header:
9+
partition: small
10+
nodes: 1
11+
account: project_XXXXXX
12+
mem-per-cpu: 1500
13+
time: "24:00:00"
14+
qos: standard
15+
exclusive: true # renders as `#SBATCH --exclusive`
16+
mol_suffixes: [_r_u, _l_u, _x_u]
17+
input_list: docking/input_list.txt
18+
work_dir: ../bm-goes-here
19+
20+
scenarios:
21+
- name: true-interface
22+
workflow:
23+
topoaa:
24+
autohis: true
25+
rigidbody:
26+
sampling: 5
27+
ambig_fname: _ti.tbl
28+
unambig_fname: _unambig.tbl
29+
ligand_top_fname: _ligand.top
30+
ligand_param_fname: _ligand.param
31+
seletop:
32+
select: 2
33+
flexref:
34+
ambig_fname: _ti.tbl
35+
unambig_fname: _unambig.tbl
36+
ligand_top_fname: _ligand.top
37+
ligand_param_fname: _ligand.param
38+
emref:
39+
caprieval:
40+
reference_fname: _ref.pdb

src/input.rs

Lines changed: 228 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,8 @@ impl Input {
191191
anyhow::bail!("partition must be non-empty when defined");
192192
}
193193

194+
self.validate_slurm_header()?;
195+
194196
match &self.general.execution {
195197
Execution::Local => {
196198
validate_haddock3()?;
@@ -211,6 +213,31 @@ impl Input {
211213

212214
Ok(())
213215
}
216+
217+
/// Validates `general.slurm_header` entries: keys must be recognized `sbatch`
218+
/// long-option names (see `SBATCH_FIELDS`), and values must be scalars.
219+
fn validate_slurm_header(&self) -> Result<()> {
220+
let Some(slurm_header) = &self.general.slurm_header else {
221+
return Ok(());
222+
};
223+
224+
for (key, value) in slurm_header {
225+
if !SBATCH_FIELDS.contains(&key.as_str()) {
226+
bail!(
227+
"unknown slurm_header field '{key}': not a recognized sbatch option \
228+
(see docs/reference.md or https://slurm.schedmd.com/sbatch.html for valid fields)"
229+
);
230+
}
231+
232+
if matches!(value, Value::Sequence(_) | Value::Mapping(_)) {
233+
bail!(
234+
"slurm_header field '{key}' must be a string, number, or boolean, not a list/mapping"
235+
);
236+
}
237+
}
238+
239+
Ok(())
240+
}
214241
}
215242

216243
#[derive(Debug, Deserialize, Serialize, Clone)]
@@ -227,8 +254,122 @@ pub struct General {
227254
pub preprocess: Option<bool>,
228255
pub postprocess: Option<bool>,
229256
pub gen_archive: Option<bool>,
257+
// SBATCH optional header customization
258+
pub slurm_header: Option<IndexMap<String, Value>>,
230259
}
231260

261+
/// Canonical `sbatch` long-option names accepted in `general.slurm_header`.
262+
/// Sourced from https://slurm.schedmd.com/sbatch.html.
263+
///
264+
/// Note that some options that don't make sense to be sense embedded
265+
/// in a job script header are intentionally excluded.
266+
pub(crate) const SBATCH_FIELDS: &[&str] = &[
267+
"account",
268+
"acctg-freq",
269+
"array",
270+
"batch",
271+
"bb",
272+
"bbf",
273+
"begin",
274+
"chdir",
275+
"cluster-constraint",
276+
"clusters",
277+
"comment",
278+
"consolidate-segments",
279+
"constraint",
280+
"container",
281+
"container-id",
282+
"container-type",
283+
"contiguous",
284+
"core-spec",
285+
"cores-per-socket",
286+
"cpu-freq",
287+
"cpus-per-gpu",
288+
"cpus-per-task",
289+
"deadline",
290+
"delay-boot",
291+
"dependency",
292+
"distribution",
293+
"error",
294+
"exclude",
295+
"exclusive",
296+
"export",
297+
"export-file",
298+
"extra",
299+
"extra-node-info",
300+
"get-user-env",
301+
"gid",
302+
"gpu-bind",
303+
"gpu-freq",
304+
"gpus",
305+
"gpus-per-node",
306+
"gpus-per-socket",
307+
"gpus-per-task",
308+
"gres",
309+
"gres-flags",
310+
"hint",
311+
"hold",
312+
"ignore-pbs",
313+
"input",
314+
"job-name",
315+
"kill-on-invalid-dep",
316+
"licenses",
317+
"mail-type",
318+
"mail-user",
319+
"mcs-label",
320+
"mem",
321+
"mem-bind",
322+
"mem-per-cpu",
323+
"mem-per-gpu",
324+
"mem-update",
325+
"mincpus",
326+
"network",
327+
"nice",
328+
"no-kill",
329+
"no-requeue",
330+
"nodefile",
331+
"nodelist",
332+
"nodes",
333+
"ntasks",
334+
"ntasks-per-core",
335+
"ntasks-per-gpu",
336+
"ntasks-per-node",
337+
"ntasks-per-socket",
338+
"oom-kill-step",
339+
"open-mode",
340+
"output",
341+
"overcommit",
342+
"oversubscribe",
343+
"parsable",
344+
"partition",
345+
"prefer",
346+
"priority",
347+
"profile",
348+
"propagate",
349+
"qos",
350+
"reboot",
351+
"requeue",
352+
"reservation",
353+
"resources",
354+
"resv-ports",
355+
"segment",
356+
"signal",
357+
"sockets-per-node",
358+
"spread-job",
359+
"spread-segments",
360+
"stepmgr",
361+
"switches",
362+
"thread-spec",
363+
"threads-per-core",
364+
"time",
365+
"time-min",
366+
"tmp",
367+
"tres-bind",
368+
"uid",
369+
"wait",
370+
"wckey",
371+
];
372+
232373
#[derive(Debug, Deserialize, Serialize, Clone)]
233374
#[serde(rename_all = "lowercase")]
234375
pub enum Execution {
@@ -343,6 +484,7 @@ mod tests {
343484
preprocess: None,
344485
postprocess: None,
345486
gen_archive: None,
487+
slurm_header: None,
346488
},
347489
scenarios: vec![],
348490
};
@@ -367,6 +509,7 @@ mod tests {
367509
preprocess: None,
368510
postprocess: None,
369511
gen_archive: None,
512+
slurm_header: None,
370513
},
371514
scenarios: vec![],
372515
};
@@ -391,6 +534,7 @@ mod tests {
391534
preprocess: None,
392535
postprocess: None,
393536
gen_archive: None,
537+
slurm_header: None,
394538
},
395539
scenarios: vec![],
396540
};
@@ -415,6 +559,7 @@ mod tests {
415559
preprocess: None,
416560
postprocess: None,
417561
gen_archive: None,
562+
slurm_header: None,
418563
},
419564
scenarios: vec![],
420565
};
@@ -440,6 +585,7 @@ mod tests {
440585
preprocess: None,
441586
postprocess: None,
442587
gen_archive: None,
588+
slurm_header: None,
443589
},
444590
scenarios: vec![],
445591
};
@@ -464,6 +610,7 @@ mod tests {
464610
preprocess: None,
465611
postprocess: None,
466612
gen_archive: None,
613+
slurm_header: None,
467614
},
468615
scenarios: vec![],
469616
};
@@ -487,6 +634,7 @@ mod tests {
487634
preprocess: None,
488635
postprocess: None,
489636
gen_archive: None,
637+
slurm_header: None,
490638
},
491639
scenarios: vec![],
492640
};
@@ -499,6 +647,85 @@ mod tests {
499647
);
500648
}
501649

650+
fn make_input_with_slurm_header(slurm_header: IndexMap<String, Value>) -> Input {
651+
Input {
652+
general: General {
653+
mol_suffixes: vec!["_r".to_string(), "_l".to_string()],
654+
input_list: "test.txt".to_string(),
655+
work_dir: PathBuf::from("/tmp"),
656+
max_concurrent: 1,
657+
ncores: 1,
658+
execution: Execution::Local,
659+
partition: None,
660+
preprocess: None,
661+
postprocess: None,
662+
gen_archive: None,
663+
slurm_header: Some(slurm_header),
664+
},
665+
scenarios: vec![],
666+
}
667+
}
668+
669+
#[test]
670+
fn test_validate_slurm_header_unknown_field_is_error() {
671+
let mut slurm_header = IndexMap::new();
672+
slurm_header.insert("partiton".to_string(), Value::String("gpu".to_string()));
673+
let input = make_input_with_slurm_header(slurm_header);
674+
675+
let result = input.validate_general();
676+
assert!(result.is_err());
677+
assert!(
678+
result.unwrap_err().to_string().contains("partiton"),
679+
"error should mention the offending key"
680+
);
681+
}
682+
683+
#[test]
684+
fn test_validate_slurm_header_known_field_is_ok() {
685+
let mut slurm_header = IndexMap::new();
686+
slurm_header.insert("nodes".to_string(), Value::Number(2.into()));
687+
slurm_header.insert("account".to_string(), Value::String("proj".to_string()));
688+
let input = make_input_with_slurm_header(slurm_header);
689+
690+
assert!(input.validate_slurm_header().is_ok());
691+
}
692+
693+
#[test]
694+
fn test_validate_slurm_header_sequence_value_is_error() {
695+
let mut slurm_header = IndexMap::new();
696+
slurm_header.insert(
697+
"nodes".to_string(),
698+
Value::Sequence(vec![Value::Number(1.into())]),
699+
);
700+
let input = make_input_with_slurm_header(slurm_header);
701+
702+
let result = input.validate_general();
703+
assert!(result.is_err());
704+
}
705+
706+
#[test]
707+
fn test_validate_slurm_header_mapping_value_is_error() {
708+
let mut slurm_header = IndexMap::new();
709+
let mut mapping = serde_yaml::Mapping::new();
710+
mapping.insert(Value::String("a".to_string()), Value::Bool(true));
711+
slurm_header.insert("nodes".to_string(), Value::Mapping(mapping));
712+
let input = make_input_with_slurm_header(slurm_header);
713+
714+
let result = input.validate_general();
715+
assert!(result.is_err());
716+
}
717+
718+
#[test]
719+
fn test_validate_slurm_header_null_and_empty_values_are_ok() {
720+
let mut slurm_header = IndexMap::new();
721+
slurm_header.insert("partition".to_string(), Value::Null);
722+
slurm_header.insert("qos".to_string(), Value::String(String::new()));
723+
slurm_header.insert("exclusive".to_string(), Value::Bool(false));
724+
let input = make_input_with_slurm_header(slurm_header);
725+
726+
assert!(input.validate_slurm_header().is_ok());
727+
}
728+
502729
#[test]
503730
fn test_input_deserialize_unknown_top_level_field() {
504731
let yaml = r#"
@@ -713,6 +940,7 @@ scenarios:
713940
preprocess: None,
714941
postprocess: None,
715942
gen_archive: None,
943+
slurm_header: None,
716944
},
717945
scenarios: vec![Scenario {
718946
name: "test".to_string(),

0 commit comments

Comments
 (0)