Skip to content

Commit 3499d2d

Browse files
committed
Add EBS disk support to Seqera executor [ci fast]
- Add disk configuration options to MachineRequirementOpts (diskType, diskThroughputMiBps, diskIops, diskEncrypted) - Extend DiskResource with cloud-specific properties (iops, throughput, encrypted, filesystem, mountPath) - Add MapperUtil.toDiskRequirement() to map disk settings to sched API - Update sched-client to 0.14.0-SNAPSHOT for DiskRequirement support - Document EBS disk configuration in executor.md and config.md - Default to gp3 volumes with 325 MiB/s throughput (Fusion recommended) Signed-off-by: Paolo Di Tommaso <paolo.ditommaso@gmail.com>
1 parent c42de21 commit 3499d2d

File tree

8 files changed

+335
-7
lines changed

8 files changed

+335
-7
lines changed

docs/executor.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -441,6 +441,24 @@ Resource requests and other job characteristics can be controlled via the follow
441441
- {ref}`process-memory`
442442
- {ref}`process-time`
443443

444+
### EBS disk support
445+
446+
When the {ref}`process-disk` directive is specified, the Seqera executor provisions an EBS volume that is attached to the task container. By default, a gp3 volume with 325 MiB/s throughput is used (Fusion recommended settings).
447+
448+
You can customize the EBS volume configuration using the `seqera.machineRequirement` options:
449+
450+
```groovy
451+
seqera {
452+
machineRequirement {
453+
diskType = 'ebs/io1' // Use provisioned IOPS SSD
454+
diskIops = 10000 // Required for io1/io2
455+
diskEncrypted = true // Enable KMS encryption
456+
}
457+
}
458+
```
459+
460+
Supported volume types: `ebs/gp3` (default), `ebs/gp2`, `ebs/io1`, `ebs/io2`, `ebs/st1`, `ebs/sc1`.
461+
444462
See the {ref}`seqera scope <config-seqera>` for the available configuration options.
445463

446464
(slurm-executor)=

docs/reference/config.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1520,6 +1520,18 @@ The following settings are available:
15201520
`seqera.machineRequirement.machineFamilies`
15211521
: List of acceptable EC2 instance families, e.g. `['m5', 'c5', 'r5']`.
15221522

1523+
`seqera.machineRequirement.diskType`
1524+
: The EBS volume type for task scratch disk. Supported types: `'ebs/gp3'` (default), `'ebs/gp2'`, `'ebs/io1'`, `'ebs/io2'`, `'ebs/st1'`, `'ebs/sc1'`.
1525+
1526+
`seqera.machineRequirement.diskThroughputMiBps`
1527+
: The throughput in MiB/s for gp3 volumes (125-1000). Default: `325` (Fusion recommended).
1528+
1529+
`seqera.machineRequirement.diskIops`
1530+
: The IOPS for io1/io2/gp3 volumes. Required for io1/io2 volume types.
1531+
1532+
`seqera.machineRequirement.diskEncrypted`
1533+
: Enable KMS encryption for the EBS volume (default: `false`).
1534+
15231535
`seqera.retryPolicy.delay`
15241536
: The initial delay when a failing HTTP request is retried (default: `'450ms'`).
15251537

modules/nextflow/src/main/groovy/nextflow/executor/res/DiskResource.groovy

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,10 @@ import groovy.transform.ToString
2222
import nextflow.util.MemoryUnit
2323

2424
/**
25-
* Models disk resource request
26-
*
25+
* Models disk resource request with support for cloud-specific options.
26+
*
2727
* @author Ben Sherman <bentshermann@gmail.com>
28+
* @author Paolo Di Tommaso <paolo.ditommaso@gmail.com>
2829
*/
2930
@ToString(includeNames = true, includePackage = false)
3031
@CompileStatic
@@ -33,6 +34,11 @@ class DiskResource {
3334

3435
final MemoryUnit request
3536
final String type
37+
final Integer iops
38+
final Integer throughput
39+
final Boolean encrypted
40+
final String filesystem
41+
final String mountPath
3642

3743
DiskResource( value ) {
3844
this(request: value)
@@ -43,10 +49,28 @@ class DiskResource {
4349

4450
if( opts.type )
4551
this.type = opts.type as String
52+
if( opts.iops )
53+
this.iops = opts.iops as Integer
54+
if( opts.throughput )
55+
this.throughput = opts.throughput as Integer
56+
if( opts.encrypted != null )
57+
this.encrypted = opts.encrypted as Boolean
58+
if( opts.filesystem )
59+
this.filesystem = opts.filesystem as String
60+
if( opts.mountPath )
61+
this.mountPath = opts.mountPath as String
4662
}
4763

4864
DiskResource withRequest(MemoryUnit value) {
49-
return new DiskResource(request: value, type: this.type)
65+
return new DiskResource(
66+
request: value,
67+
type: this.type,
68+
iops: this.iops,
69+
throughput: this.throughput,
70+
encrypted: this.encrypted,
71+
filesystem: this.filesystem,
72+
mountPath: this.mountPath
73+
)
5074
}
5175

5276
private static MemoryUnit toMemoryUnit( value ) {

plugins/nf-seqera/build.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ dependencies {
4646
compileOnly project(':nextflow')
4747
compileOnly 'org.slf4j:slf4j-api:2.0.17'
4848
compileOnly 'org.pf4j:pf4j:3.12.0'
49-
api 'io.seqera:sched-client:0.13.0-SNAPSHOT'
49+
api 'io.seqera:sched-client:0.14.0-SNAPSHOT'
5050

5151
testImplementation(testFixtures(project(":nextflow")))
5252
testImplementation "org.apache.groovy:groovy:4.0.29"

plugins/nf-seqera/src/main/io/seqera/config/MachineRequirementOpts.groovy

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,33 @@ class MachineRequirementOpts implements ConfigScope {
5555
""")
5656
final List<String> machineFamilies
5757

58+
@ConfigOption
59+
@Description("""
60+
The EBS volume type for task scratch disk (e.g., `ebs/gp3`, `ebs/io1`).
61+
Default: `ebs/gp3`.
62+
""")
63+
final String diskType
64+
65+
@ConfigOption
66+
@Description("""
67+
The throughput in MiB/s for gp3 volumes (125-1000).
68+
Default: 325 (Fusion recommended).
69+
""")
70+
final Integer diskThroughputMiBps
71+
72+
@ConfigOption
73+
@Description("""
74+
The IOPS for io1/io2/gp3 volumes. Required for io1/io2.
75+
""")
76+
final Integer diskIops
77+
78+
@ConfigOption
79+
@Description("""
80+
Enable KMS encryption for the EBS volume.
81+
Default: false.
82+
""")
83+
final Boolean diskEncrypted
84+
5885
/* required by config scope -- do not remove */
5986
MachineRequirementOpts() {}
6087

@@ -63,6 +90,10 @@ class MachineRequirementOpts implements ConfigScope {
6390
this.provisioning = opts.provisioning as String
6491
this.maxSpotAttempts = opts.maxSpotAttempts as Integer
6592
this.machineFamilies = opts.machineFamilies as List<String>
93+
this.diskType = opts.diskType as String
94+
this.diskThroughputMiBps = opts.diskThroughputMiBps as Integer
95+
this.diskIops = opts.diskIops as Integer
96+
this.diskEncrypted = opts.diskEncrypted as Boolean
6697
}
6798

6899
String getArch() {
@@ -80,4 +111,20 @@ class MachineRequirementOpts implements ConfigScope {
80111
List<String> getMachineFamilies() {
81112
return machineFamilies
82113
}
114+
115+
String getDiskType() {
116+
return diskType
117+
}
118+
119+
Integer getDiskThroughputMiBps() {
120+
return diskThroughputMiBps
121+
}
122+
123+
Integer getDiskIops() {
124+
return diskIops
125+
}
126+
127+
Boolean getDiskEncrypted() {
128+
return diskEncrypted
129+
}
83130
}

plugins/nf-seqera/src/main/io/seqera/executor/SeqeraTaskHandler.groovy

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import groovy.transform.CompileStatic
2323
import groovy.transform.PackageScope
2424
import groovy.util.logging.Slf4j
2525
import io.seqera.sched.api.schema.v1a1.AcceleratorType
26+
import io.seqera.sched.api.schema.v1a1.DiskRequirement
2627
import io.seqera.sched.api.schema.v1a1.GetTaskLogsResponse
2728
import io.seqera.sched.api.schema.v1a1.ResourceRequirement
2829
import io.seqera.sched.api.schema.v1a1.Task
@@ -102,10 +103,11 @@ class SeqeraTaskHandler extends TaskHandler implements FusionAwareTask {
102103
if( accelerator.type )
103104
resourceReq.acceleratorName(accelerator.type)
104105
}
105-
// build machine requirement merging config settings with task arch
106+
// build machine requirement merging config settings with task arch and disk
106107
final machineReq = MapperUtil.toMachineRequirement(
107108
executor.getSeqeraConfig().machineRequirement,
108-
task.getContainerPlatform()
109+
task.getContainerPlatform(),
110+
task.config.getDisk()
109111
)
110112
final schedTask = new Task()
111113
.name(task.lazyName())

plugins/nf-seqera/src/main/io/seqera/util/MapperUtil.groovy

Lines changed: 71 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,12 @@ package io.seqera.util
1919

2020
import groovy.transform.CompileStatic
2121
import io.seqera.config.MachineRequirementOpts
22+
import io.seqera.sched.api.schema.v1a1.DiskRequirement
2223
import io.seqera.sched.api.schema.v1a1.MachineRequirement
2324
import io.seqera.sched.api.schema.v1a1.PriceModel as SchedPriceModel
2425
import io.seqera.sched.api.schema.v1a1.ProvisioningModel
2526
import nextflow.cloud.types.PriceModel
27+
import nextflow.util.MemoryUnit
2628

2729
/**
2830
* Utility class to map Nextflow config objects to Sched API schema objects.
@@ -32,6 +34,22 @@ import nextflow.cloud.types.PriceModel
3234
@CompileStatic
3335
class MapperUtil {
3436

37+
/** Default EBS volume type - gp3 provides good balance of price and performance */
38+
static final String DEFAULT_DISK_TYPE = 'ebs/gp3'
39+
40+
/** Default throughput in MiB/s - Fusion recommended setting for optimal I/O */
41+
static final int DEFAULT_DISK_THROUGHPUT_MIBPS = 325
42+
43+
/** Supported EBS volume types */
44+
static final Set<String> SUPPORTED_DISK_TYPES = Set.of(
45+
'ebs/gp3', // General purpose SSD (default)
46+
'ebs/gp2', // General purpose SSD (legacy)
47+
'ebs/io1', // Provisioned IOPS SSD
48+
'ebs/io2', // Provisioned IOPS SSD (higher durability)
49+
'ebs/st1', // Throughput optimized HDD
50+
'ebs/sc1' // Cold HDD
51+
)
52+
3553
/**
3654
* Maps MachineRequirementOpts to MachineRequirement API object.
3755
*
@@ -59,18 +77,70 @@ class MapperUtil {
5977
* @return the MachineRequirement API object, or null if no settings
6078
*/
6179
static MachineRequirement toMachineRequirement(MachineRequirementOpts opts, String taskArch) {
80+
return toMachineRequirement(opts, taskArch, null)
81+
}
82+
83+
/**
84+
* Maps MachineRequirementOpts to MachineRequirement API object, merging with task arch and disk.
85+
* Task arch overrides config arch if specified.
86+
*
87+
* @param opts the config options (can be null)
88+
* @param taskArch the task container platform/arch (can be null)
89+
* @param diskSize the disk size from task config (can be null)
90+
* @return the MachineRequirement API object, or null if no settings
91+
*/
92+
static MachineRequirement toMachineRequirement(MachineRequirementOpts opts, String taskArch, MemoryUnit diskSize) {
6293
final arch = taskArch ?: opts?.arch
6394
final provisioning = opts?.provisioning
6495
final maxSpotAttempts = opts?.maxSpotAttempts
6596
final machineFamilies = opts?.machineFamilies
97+
final diskReq = toDiskRequirement(diskSize, opts)
6698
// return null if no settings
67-
if (!arch && !provisioning && !maxSpotAttempts && !machineFamilies)
99+
if (!arch && !provisioning && !maxSpotAttempts && !machineFamilies && !diskReq)
68100
return null
69101
new MachineRequirement()
70102
.arch(arch)
71103
.provisioning(toProvisioningModel(provisioning))
72104
.maxSpotAttempts(maxSpotAttempts)
73105
.machineFamilies(machineFamilies)
106+
.disk(diskReq)
107+
}
108+
109+
/**
110+
* Maps a disk size to DiskRequirement API object.
111+
* Uses config options if provided, otherwise defaults to Fusion recommended settings:
112+
* EBS gp3 volume with 325 MiB/s throughput.
113+
*
114+
* @param diskSize the disk size (can be null)
115+
* @param opts the machine requirement options with disk settings (can be null)
116+
* @return the DiskRequirement API object, or null if diskSize is null or zero
117+
*/
118+
static DiskRequirement toDiskRequirement(MemoryUnit diskSize, MachineRequirementOpts opts=null) {
119+
if (!diskSize || diskSize.toGiga() <= 0)
120+
return null
121+
// Use config values or Fusion recommended defaults
122+
final type = opts?.diskType ?: DEFAULT_DISK_TYPE
123+
// Validate disk type is supported
124+
if (!SUPPORTED_DISK_TYPES.contains(type)) {
125+
throw new IllegalArgumentException("Invalid disk type: ${type}. Supported types: ${SUPPORTED_DISK_TYPES.join(', ')}")
126+
}
127+
final throughput = opts?.diskThroughputMiBps ?: DEFAULT_DISK_THROUGHPUT_MIBPS
128+
final iops = opts?.diskIops
129+
final encrypted = opts?.diskEncrypted ?: false
130+
131+
def req = new DiskRequirement()
132+
.sizeGiB(diskSize.toGiga() as Integer)
133+
.type(type)
134+
.encrypted(encrypted)
135+
// Only set throughput for gp3 volumes
136+
if (type == DEFAULT_DISK_TYPE) {
137+
req.throughputMiBps(throughput)
138+
}
139+
// Set IOPS if provided
140+
if (iops) {
141+
req.iops(iops)
142+
}
143+
return req
74144
}
75145

76146
/**

0 commit comments

Comments
 (0)