Nextflow 进程详解:输入、输出、脚本、标签、资源分配

进程(process)是 Nextflow 的原子单位。
说白了,你的每个计算步骤——数据清洗、特征提取、模型训练——都会封装进一个进程中。

我刚开始用 Nextflow 的时候,总觉得进程就是写个脚本跑一跑。
后来项目复杂度上去了才发现,输入/输出的声明方式、资源分配的策略,直接决定了管线能不能稳定跑完。

这一节咱们就逐一拆解进程的五个核心要素。

输入:管线的「入口」

每个进程都需要指定 input 块。
它告诉 Nextflow:这个进程要消费什么数据。

我个人习惯用 set 输入组合多个值。比如同时传一个文件和一个参数:

process FEATURE_EXTRACT {
    input:
    path fasta_file
    val cutoff
}

为什么这么写?
因为有时输入的通道不一定是文件,可能是列表或值。
path 用来处理文件,val 用来处理普通值。

建议:能用 path 就别用 file。path 会与 Nextflow 的暂存系统更好配合,避免意外把同一个文件复制多次。

这里有几种常用的输入声明方式,供你参考:

输入类型关键字适用场景
文件path读取 FASTA、CSV、模型文件等
标量值val整数、字符串、参数列表
环境变量env传递 PATH、TOKEN 等系统变量
stdinstdin管道进程之间的流式传数据

输出:结果的「通道」

输出声明看起来有点吓人,但核心逻辑很简单——告诉 Nextflow 这个进程会产生什么文件。

我见过很多新手在 output 块里漏写文件,结果后续进程一直报「找不到文件」。
嗯,这里一定要注意:你声明了才能传出去。

process COUNT_KMERS {
    input:
    path reads
    output:
    path "counts.tsv", emit: kmer_table
    script:
    """
    kmer_counter --input $reads --output counts.tsv
    """
}

emit: kmer_table 这个标记非常实用。
它给输出通道取了个名字,后续进程可以直接引用:

workflow {
    COUNT_KMERS(reads_ch)
    NORMALIZE(kmer_table)  // 直接用名字引用,不用管变量名
}

我在项目中遇到过一种情况:输出文件不确定 —— 有时输出 3 个,有时输出 10 个。
这时候可以用 path "*" 来通配匹配:

output:
path "*.json"
注意:通配符能匹配多个文件,但会让下游进程处理起来更复杂。不建议在关键路径上滥用。

脚本:核心执行逻辑

script 块就是写实际命令的地方。
你可以写 bash、Python、R——只要是 shell 能跑的,Nextflow 都能接管。

我建议把脚本写得尽量干净。不要在里面嵌入复杂的流程控制,否则调试日志会乱成一团。

script:
"""
feature_engineer.py \
    --input $input_fasta \
    --method pca \
    --output $output_file
"""

为什么用三引号?
因为 Nextflow 的 script 块接受多行字符串,用单引号会导致字符串里的变量不会被解析。
用双引号则可以让 $input_fasta 这种变量被替换。

有一种特殊情况——你想在原样字符串里保留 $,比如传给 shell 的变量:

script:
"""
echo "Shell 变量:\$HOME"
"""

转义一下就好,我当年吃了不少这个坑。

标签:模块化与复用

label 是 Nextflow 的一个小功能,但非常实用。
它可以给进程打上语义标签,比如 "gpu"、"heavy"、"fast"。

为什么要用标签?
因为你可以基于标签做资源路由、日志分类、甚至条件执行。

process DEEP_LEARN {
    label 'gpu'
    label 'training'
    ...
}

我曾经历过一个管线,后期要加入一个新特性,结果发现旧进程的参数全用 old_ 前缀标注着。
如果用标签,甚至可以一键筛选出所有 'deprecated' 进程进行替换。

最佳实践:给每个进程至少打一个标签。后期做资源监控、日志聚合时,标签就是金钥匙。

资源分配:让计算恰到好处

Nextflow 默认不会给你分配很多资源。
如果你不声明,它可能只用 1 个 CPU 和 1 GB 内存跑你的模型——当然会慢得让人怀疑人生。

资源分配主要通过 cpus、memory、time 这几个指令来控制:

process HEAVY_TRAIN {
    cpus 8
    memory '32 GB'
    time '2h'

    script:
    """
    python train.py --cpus 8
    """
}

注意:你声明了 cpus 8,但 Python 代码并不知道。
你需要在脚本里也把 --cpus 8 传进去,否则它只会用一个核。

我曾经用 cpus 1 跑一个需要 16 核的任务,结果跑了三天才跑完。后来加上了资源声明,几个小时就搞定了。
嗯,这东西看似简单,却是管线性能的分水岭。

资源分配的常见参数我列成表格,方便你查:

参数作用单位建议值
cpusCPU 核数整数根据计算节点配置,通常 2~32
memory内存上限GB / MB数据量和算法决定,谨慎设置
time最长执行时间h / m / s防止死循环或数据错误导致的无限等待
disk临时存储GB需要大 I/O 时设置
queue指定队列名称字符串用于区分 GPU 队列、高内存队列

本章核心结构

下面这张 SVG 图把进程的五个要素串了起来。你看一眼,就能理解它们之间的关系。

进程核心要素 进程 process 输入 (input) path / val / env 输出 (output) path / emit 脚本 (script) bash / python / R 标签 (label) gpu / heavy / fast 资源分配 cpus / memory / time 五个要素共同定义了一个进程的行为: 输入 → 脚本执行 → 输出,标签和资源分配控制环境

综合示例

把上面这些要素组合起来,就是一个完整的进程:

process TRAIN_MODEL {
    label 'gpu'
    label 'training'

    cpus 4
    memory '16 GB'
    time '3h'

    input:
    path train_data
    path config_file
    val epochs

    output:
    path "model.pt", emit: trained_model
    path "training.log"

    script:
    """
    python train.py \
        --data $train_data \
        --config $config_file \
        --epochs $epochs \
        --cpus ${task.cpus}
    """
}

这里用到了 ${task.cpus},它自动读取你声明的 cpus 4。
这样一来,资源声明和脚本里的参数就对齐了。不用手动重复写死数值。

小技巧:task 对象里还有 task.memory、task.time 等属性,都能在脚本里引用,保持一致性。

避坑指南

最后总结几个我踩过的坑,希望能帮你绕过:

  • 输入输出文件名字必须一致:脚本里生成的文件名,必须与 output 块里声明的完全一致。大小写、扩展名都不能错。
  • 不要硬编码资源值:用 task.cpus 和 task.memory 代替,方便后期调整。
  • 标签不是注释:标签有实际作用——可以用在 withLabel 选择器里做条件执行。
  • time 别设得太紧:留 20%~30% 的余量。特别是数据量不可控时,容易超时失败。

进程是 Nextflow 的基石。
输入输出定义清晰、脚本干净、资源分配合理,管线就跑得稳、跑得快。

我个人觉得,花时间把这些基础打磨好,比后面堆砌复杂功能划算得多。