Raquel Manzano
04/16/2026, 9:39 AMMath.min(p.max_cpus as int, s.max_total_cpus as int) being computed, which again I am unsure if this is the best way; 3) I am not even sure if inferring things from a parameter such as project (I am trying to infer the priority level of the account because then the specs change) is good practice at all. Many thanks.
/*
* Cambridge CSD3 institutional profile
*
* Partitions:
* - icelake : 76 CPUs, 256 GiB RAM
* - icelake-himem : 76 CPUs, 512 GiB RAM
* - sapphire : 112 CPUs, 512 GB RAM
*
* Service levels:
* - inferred from params.project (e.g. MYGROUP-SL2-CPU)
* - params.service_level can override
* - sl1/sl2 -> 36h
* - sl3 -> 12h
*/
// ---- Resolve partition ----
def partition_specs = [
'icelake' : [ max_cpus: 76, max_memory: 256.GB ],
'icelake-himem' : [ max_cpus: 76, max_memory: 512.GB ],
'sapphire' : [ max_cpus: 112, max_memory: 512.GB ]
]
def selected_partition = params.partition ?: 'icelake'
def p = partition_specs[selected_partition]
if (!p) {
throw new IllegalArgumentException(
"Unsupported partition '${selected_partition}'. " +
"Supported: ${partition_specs.keySet().join(', ')}"
)
}
// ---- Infer service level from project ----
def service_specs = [
'sl1' : [ max_time: 36.h, max_total_cpus: 4256 ],
'sl2' : [ max_time: 36.h, max_total_cpus: 4256 ],
'sl3' : [ max_time: 12.h, max_total_cpus: 448 ]
]
def inferred_service = 'sl2'
if (params.project) {
def project_upper = params.project.toUpperCase()
if (project_upper.contains('-SL3-')) inferred_service = 'sl3'
else if (project_upper.contains('-SL2-')) inferred_service = 'sl2'
else if (project_upper.contains('-SL1-')) inferred_service = 'sl1'
}
// Manual override wins
def selected_service = (params.service_level ?: inferred_service).toLowerCase()
def s = service_specs[selected_service]
if (!s) {
throw new IllegalArgumentException(
"Unsupported service_level '${selected_service}'. " +
"Supported: ${service_specs.keySet().join(', ')}"
)
}
// ---- Params ----
params {
config_profile_description = 'Cambridge HPC CSD3 cluster profile.'
config_profile_contact = 'Raquel Manzano (<mailto:rm889@cam.ac.uk|rm889@cam.ac.uk>) and Andries van Tonder (<mailto:ajv37@cam.ac.uk|ajv37@cam.ac.uk>)'
config_profile_url = '<https://docs.hpc.cam.ac.uk/hpc>'
partition = selected_partition
project = null
service_level = selected_service
max_memory = p.max_memory
max_cpus = Math.min(p.max_cpus as int, s.max_total_cpus as int)
max_time = s.max_time
}
// ---- Singularity options ----
singularity {
enabled = true
autoMounts = true
}
// ---- JAVA limits for nextflow ----
// Note that we recommend running nextflow with `srun` + screen/tmux
// or via `sbatch` for large or long-running workflows
env {
NXF_JVM_ARGS = '-Xms2g -Xmx4g'
}
process {
// Hard caps from selected partition + service level
resourceLimits = [
memory: params.max_memory,
cpus: params.max_cpus,
time: params.max_time
]
// Module commands provided by HPC
beforeScript = '''
. /etc/profile.d/modules.sh
module purge
module load rhel8/default-icl
'''
executor = 'slurm'
queue = params.partition
// Only pass account; queue already defines partition
clusterOptions = params.project ? "-A ${params.project}" : ''
cache = 'lenient'
scratch = true
errorStrategy = { task.exitStatus in [143, 137, 104, 134, 139, 140] ? 'retry' : 'finish' }
maxErrors = '-1'
maxRetries = 2
withLabel: process_single {
cpus = { 1 * task.attempt }
memory = { 4.GB * task.attempt }
time = { 4.h * task.attempt }
}
withLabel: process_low {
cpus = { 2 * task.attempt }
memory = { 8.GB * task.attempt }
time = { 8.h * task.attempt }
}
withLabel: process_medium {
cpus = { 4 * task.attempt }
memory = { 16.GB * task.attempt }
time = { 12.h * task.attempt }
}
withLabel: process_high {
cpus = { 8 * task.attempt }
memory = { 32.GB * task.attempt }
time = { 24.h * task.attempt }
}
withLabel: process_long {
time = { 36.h }
}
withLabel: process_high_memory {
memory = { 128.GB * task.attempt }
}
}
// ---- Executor tuning: necessary to not overload SLURM ----
executor {
name = 'slurm'
queueSize = 200
pollInterval = '5 min'
queueStatInterval = '5 min'
submitRateLimit = '10 sec'
exitReadTimeout = '30 min'
}Maxime Garcia
04/16/2026, 9:59 AMMaxime Garcia
04/16/2026, 10:03 AMthrow IllegalArgumentException, but rather a System.err.println.pontus
04/16/2026, 10:05 AMpontus
04/16/2026, 10:06 AMpontus
04/16/2026, 10:10 AMpontus
04/16/2026, 10:13 AMwithLabel would normally be defined in the pipelines (where hopefully the developer have some idea about the data and tools and what values might make sense), unless you feel you have good reasons for defining them, I'd remove them.Maxime Garcia
04/16/2026, 10:15 AMnextflow lint conf/cambridge.config
nextflow run <http://configtest.nf|configtest.nf> -profile cambridge
with the latest edge nextflow to be surepontus
04/16/2026, 10:17 AMpontus
04/16/2026, 10:18 AMenv scope doesn't work - when nextflow sees that it has already started...James Fellows Yates
04/16/2026, 4:59 PMmax_resources each time. You just need to set the maximum limits of the biggest parition, then select teh queue depending on the specific task resource requests, I would assume (even if seems the requests are simply just memoryu/cpus, no fancy GPU stuff)Raquel Manzano
04/17/2026, 8:34 AMhostname in principle but would involve some lines of code which I am not sure if the config syntax would be happy with. If I set the config to the partition with the biggest resources, woudn’t I create the risk of a job asking for too much mem/cpu in a partition with less resources?James Fellows Yates
04/17/2026, 8:35 AMf I set the config to the partition with the biggest resources, woudn’t I create the risk of a job asking for too much mem/cpu in a partition with less resources?No, because the other part I mentioned that the
queue closure should always make sure it goes to the right partition (unless I have missed a more complicated detail about the way the cluster works)James Fellows Yates
04/17/2026, 8:36 AMqueue should redirect that to the a sufficietn partition (so to say)Raquel Manzano
04/17/2026, 8:37 AMJames Fellows Yates
04/17/2026, 8:37 AMRaquel Manzano
04/17/2026, 8:37 AMRaquel Manzano
04/17/2026, 8:37 AMJames Fellows Yates
04/17/2026, 8:38 AMqueue = { task.cpu ? 48 & task.memory > 128 ? 'partition1' : 'partition2' }?James Fellows Yates
04/17/2026, 8:38 AMpontus
04/17/2026, 8:38 AMJames Fellows Yates
04/17/2026, 8:39 AMRaquel Manzano
04/17/2026, 8:39 AMRaquel Manzano
04/17/2026, 8:40 AMRaquel Manzano
04/17/2026, 8:40 AMpontus
04/17/2026, 8:41 AMRaquel Manzano
04/17/2026, 8:41 AMpontus
04/17/2026, 8:42 AMRaquel Manzano
04/17/2026, 8:42 AMRaquel Manzano
04/17/2026, 8:43 AMJames Fellows Yates
04/17/2026, 8:44 AM* - icelake : 76 CPUs, 256 GiB RAM
* - icelake-himem : 76 CPUs, 512 GiB RAM
* - sapphire : 112 CPUs, 512 GB RAM
Do you mean here that icelake and icelake-himem are pretty much the same except for the himem bit?pontus
04/17/2026, 8:45 AMpontus
04/17/2026, 8:47 AMThomas Danhorn
04/21/2026, 8:37 AM--partition parameters, so you should be able to set queue = "icelake,icelake-himem,sapphire" and thus accomplish what the sysadmin probably should have done in the first place -- make all nodes available to any given process and let SLURM pick. The point of partitions is not to group various nodes (as seems to be the attempt here), but to group classes of processes, e.g. low-resource & quick, low-resource and long-running, high-resource & quick, etc. The idea is that if you pick the correct queue, you can get a quick, low-resource job in, even if there are a bunch of jobs ahead of you, as long as they need more resources than that queue provides. BTW, a node can be used in several queues.
But if I try most of the jobs will go to the biggest partition and not sure if any will go to the other one 🤔SLURM will see if each partition allows the request of the job, and among those that do it will pick the one where the job would start soonest. If one partition fills up, another will become more favorable to the scheduler. (As mentioned, in a more well thought-out setup, you wouldn't have so many queues that all would allow a job.) For the
resourceLimits you just pick the maximum of any partition. The only thing you have to be careful about here is that you don't allow the pipeline to make a request that no partition can fulfill (e.g. if you have one with lots of RAM and another with long runtimes, but none with both, you'll have to decide between them.) Not sure you have such a scenario here, but if you do, you can redefine the relevant slot (time, memory, cpus) in resourceLimits from some baseline to something higher (if necessary while limiting another slot) based on e.g. process label.
I am not sure what the whole "service level" thing is about -- maybe I'm blind (it happens occasionally), but I could not find where that this actually used to influence SLURM in any way. Is this meant to be the --qos parameter of sbatch? If so, you would have to set that in clusterOptions. E.g., I have the following in my config:
process {
executor = 'slurm'
clusterOptions = { task.memory <= 240.GB ? (task.time > 24.h ? '--qos=long'
: '--qos=normal')
: '--qos=mem' }
// other stuff here ...
}
(I'm too lazy to translate your situation, but I think you get the point.)
Lastly, I have some concern about using params. The config per-se does not care if a parameter exist or is possible, but the pipeline you are running might, and if it validates all parameters and crashes upon encountering one it does not recognize (e.g. because that one was specifically designed to be only used in the config), that's a problem, because you can't use that parameter with that pipeline. (The other thing I don't like is that any help function of the pipeline would not know about config params, and that makes it harder for the user to even figure out that they exist and when they should be used.)
If you really need user input to decide something, I would just make different configs to begin with, and user can pick the correct one via -profile. I'm not sure why you would need this here (e.g. if the user really wanted or needed to override the queue or --qos within clusterOptions, that could be done in a custom config, but ideally the config should pick the appropriate setting based on the requested resources).
In short, your config may (and possibly should) set params (e.g. config_profile_description & friends, for really old nf-core pipeline versions also params.max_memory & Co.), but it should never expect and read params. E.g., I find it very elegant that you are using parmas.max_* to set the resourceLimits, because one of those will work with a (nf-core) pipeline of pretty much any age, but unfortunately it might crash newer (or non-nf-core) pipelines when you set a parameter (like --max_memory) they don't recognize. (Many pipelines ignore unknown parameters or just issue a warning, but there is no guarantee that all of them will, and ideally your -profile is not limited to nf-core but works for all Nextflow pipelines.)Raquel Manzano
04/22/2026, 12:29 PMRaquel Manzano
04/27/2026, 10:55 AMsbatch: error: Batch job submission failed: Multiple partition job request not supported when a partition is set in the association, which is probably related to their specific config of slurm. Is the dynamic queue worth it then? Seqera AI spitted this for me:
queue = { selectCSD3Queue(task) }
def selectCSD3Queue(task) {
/*
* Dynamically select CSD3 partition based on resource requirements
*
* Decision logic:
* 1. Jobs >76 cores → sapphire (112 cores/node)
* 2. Jobs >6.0 GB/core → icelake-himem (6.8 GB/core)
* 3. Jobs >3.4 GB/core → sapphire (4.6 GB/core, efficient)
* 4. Default → icelake (3.4 GB/core, most available)
*/
def requestedMemoryGB = task.memory ? task.memory.toGiga() : 3.4
def requestedCpus = task.cpus ?: 1
def memoryPerCoreGB = requestedMemoryGB / requestedCpus
// High CPU count jobs need sapphire (112 cores vs 76)
if (requestedCpus > 76) {
return 'sapphire'
}
// Very high memory per core needs icelake-himem
if (memoryPerCoreGB > 6.0) {
return 'icelake-himem'
}
// Medium memory per core: sapphire is more efficient than icelake-himem
if (memoryPerCoreGB > 3.4 && requestedCpus <= 112) {
return 'sapphire'
}
// Default: standard icelake partition
return 'icelake'
}
but maybe I am overthinking this and should set everything to the default queue which should be icelakepontus
04/27/2026, 10:59 AMicelake available to all?Raquel Manzano
04/27/2026, 12:51 PMRaquel Manzano
04/27/2026, 12:51 PMJames Fellows Yates
04/27/2026, 12:52 PMJames Fellows Yates
04/27/2026, 12:52 PMRaquel Manzano
04/27/2026, 1:37 PMJames Fellows Yates
04/27/2026, 2:42 PMJames Fellows Yates
04/27/2026, 2:43 PMJames Fellows Yates
04/27/2026, 2:50 PMpontus
04/27/2026, 2:51 PMpontus
04/27/2026, 2:53 PMRaquel Manzano
04/27/2026, 2:56 PMpontus
04/27/2026, 2:57 PMpontus
04/27/2026, 3:00 PMRaquel Manzano
04/27/2026, 3:46 PMpontus
04/27/2026, 3:47 PMresourceLimits.Raquel Manzano
04/27/2026, 4:07 PMRaquel Manzano
04/27/2026, 4:07 PMJames Fellows Yates
04/27/2026, 4:10 PMpontus
04/27/2026, 4:13 PMprocess {
resourceLimits = [
time: {some code}.call(),
cpus: maybeisstaticsonocode,
memory: {some other code maybe}.call()
]
}Thomas Danhorn
04/27/2026, 4:37 PMresourceLimits determined (in part) based on the "service level" in the draft config. It's a bit convoluted, but it should work, except my concerns about params.
What the defaults should be (for partition and SL) is something to discuss with the sysadmins. I would be inclined to go with the most basic and let users ask for more if they need.
Regarding the use of params, as mentioned I don't like using that for configs, because if a pipeline validates params and croaks on ones that it does not recognize, that's a deal breaker. I would carefully think about that and maybe do some testing.
But if you don't use params, how else can you capture user input? I see these possibilities:
• Set an environment variable and check that in the config.
• Have a different config for e.g. each SL and/or partition, so you would chose a different -profile. With lots of combinations, this may not be very practical, though.
If the sysadmins are helpful, they could put the "correct" setting of the environment variable(s) into each user's .bashrc (or somewhere equivalent) -- of course power users can override that.
My best guess about the partitions: They bought over the years different sets of nodes, and each set is basically its own cluster, except that all of them are accessible via the same head node(s). Maybe there are good technical reasons to keep them separate, but to me that seems like a questionable decision and I would have combined them into one partition. If you set one as default, that one will be hammered, while the others will be underutilized. But that's not you problem -- the sysadmins have to figure that out. If they are open to discussion, I would talk to them and try to accommodate their suggestions.Raquel Manzano
04/27/2026, 7:14 PMsapphire, if you want even more then have to go for the himem partitions but that is basically it, you have to play around with the queue depending on how much is being used. icelake is the biggest one and that’s normally the preferred choice but sapphire even though is smaller the jobs run faster because is newer but then you might have to wait longer for your jobs to start. That’s why is basically up to the user and the reason why I want to give a choice (exactly what Thomas said, I don’t want icelake queue being overflowed with nextflow jobs).Raquel Manzano
04/27/2026, 7:15 PMJames Fellows Yates
04/27/2026, 7:37 PMThomas Danhorn
04/27/2026, 7:46 PMpontus
04/27/2026, 7:54 PMpontus
04/27/2026, 7:58 PMicelake). But it's something only the people on the system will knowRaquel Manzano
04/29/2026, 7:14 AMRaquel Manzano
04/29/2026, 7:14 AMparam but still have flexibilitypontus
04/29/2026, 7:17 AMRaquel Manzano
04/29/2026, 7:17 AMgroupname-sl2-somethingpontus
04/29/2026, 7:17 AMRaquel Manzano
04/29/2026, 7:18 AMparam it is for the partitionRaquel Manzano
04/29/2026, 7:31 AMpontus
04/29/2026, 7:33 AMRaquel Manzano
04/29/2026, 7:44 AMnextflow lint and nextflow run <http://configtest.nf|configtest.nf> and all was okay.
// nf-core/configs: Cambridge CSD3 cluster profile
// Available partitions:
// - icelake : 76 CPUs, 256 GB RAM
// - icelake-himem : 76 CPUs, 512 GB RAM
// - sapphire : 112 CPUs, 512 GB RAM
//
// The profile defaults to the broadly available `icelake` partition but allows
// users to override it with `--partition`. Walltime is inferred from the SLURM
// account name when possible: projects containing `-SL3-` use a 12 h cap,
// otherwise the profile assumes the common SL1/SL2 36 h limit.
params {
config_profile_description = 'Cambridge HPC CSD3 cluster profile.'
config_profile_contact = 'Raquel Manzano (<mailto:rm889@cam.ac.uk|rm889@cam.ac.uk>) and Andries van Tonder (<mailto:ajv37@cam.ac.uk|ajv37@cam.ac.uk>)'
config_profile_url = '<https://docs.hpc.cam.ac.uk/hpc>'
partition = 'icelake'
project = null
// Compatibility with nf-core schema validation across pipeline versions.
schema_ignore_params = 'partition,project,max_memory,max_cpus,max_time,validationSchemaIgnoreParams'
validationSchemaIgnoreParams = 'partition,project,max_memory,max_cpus,max_time,schema_ignore_params,validationSchemaIgnoreParams'
}
validation {
ignoreParams = ['partition', 'project', 'max_memory', 'max_cpus', 'max_time', 'schema_ignore_params', 'validationSchemaIgnoreParams']
}
params.max_memory = {
if (params.partition == 'icelake') {
return 256.GB // this is a whole node
}
if (params.partition == 'icelake-himem' || params.partition == 'sapphire') {
return 512.GB // this is a whole node
}
System.err.println("ERROR: cambridge params.partition must be one of 'icelake', 'icelake-himem', or 'sapphire' (got '${params.partition}').")
System.exit(1)
}.call()
params.max_cpus = {
if (params.partition == 'sapphire') {
return 112
}
if (params.partition == 'icelake' || params.partition == 'icelake-himem') {
return 76
}
System.err.println("ERROR: cambridge params.partition must be one of 'icelake', 'icelake-himem', or 'sapphire' (got '${params.partition}').")
System.exit(1)
}.call()
params.max_time = {
def project_name = params.project?.toUpperCase()
return project_name?.contains('-SL3-') ? 12.h : 36.h
}.call()
singularity {
enabled = true
autoMounts = true
}
process {
resourceLimits = [
memory: params.max_memory,
cpus: params.max_cpus,
time: params.max_time
]
executor = 'slurm'
queue = params.partition
clusterOptions = { params.project ? "--account=${params.project}" : '' }
}James Fellows Yates
04/29/2026, 7:57 AMRaquel Manzano
04/29/2026, 7:58 AMRaquel Manzano
04/29/2026, 7:59 AMpontus
04/29/2026, 8:02 AMRaquel Manzano
04/29/2026, 8:03 AMpontus
04/29/2026, 8:12 AMparams.csd_time = { params.project?.toUpperCase().contains('-SL3-')? 12.h : 36.h }.call()
params.csd_parts = [
icelake: [ memory: 256, cpus: 76, time: params.csd_time],
"icelake-himem": [ memory: 512, cpus: 76, time: params.csd_time],
sapphire: [ memory: 512, cpus: 112, time: params.csd_time],
process {
resourceLimits = params.csd_parts[params.partition]
executor = 'slurm'
queue = params.partition
clusterOptions = { params.project ? "--account=${params.project}" : '' }.call()Raquel Manzano
04/29/2026, 8:22 AMRaquel Manzano
04/29/2026, 8:35 AMpontus
04/29/2026, 8:38 AMRaquel Manzano
04/29/2026, 8:46 AMRaquel Manzano
04/29/2026, 9:04 AMJames Fellows Yates
04/29/2026, 9:04 AMpontus
04/29/2026, 9:04 AMRaquel Manzano
04/29/2026, 9:05 AMRaquel Manzano
04/29/2026, 9:05 AMJames Fellows Yates
04/29/2026, 9:05 AMJames Fellows Yates
04/29/2026, 9:05 AMRaquel Manzano
04/29/2026, 9:07 AMRaquel Manzano
04/29/2026, 9:07 AMJames Fellows Yates
04/29/2026, 9:08 AMJames Fellows Yates
04/29/2026, 9:08 AMRaquel Manzano
04/29/2026, 9:13 AMpre-commit run I had to change this hook to mszostok/codeowners-validator:v0.7.4 . I am not sure if this is because I am doing it locally so I did not push this change but letting you know just in caseJames Fellows Yates
04/29/2026, 9:13 AMmashehu
04/29/2026, 9:15 AMJames Fellows Yates
04/29/2026, 9:16 AMRaquel Manzano
04/29/2026, 10:13 AMNXF_SYNTAX_PARSER=v2 nextflow lint and all seems okay and went through the new syntax docs. I think I cover everything but let me know if you notice anything. I also sent the config to the hpc admins for their feedback. If everyone happy I will do the merge