规则依赖与DAG:Snakemake 如何构建有向无环图,dry-run 模式

规则之间的依赖关系,是 Snakemake 能自动编排流程的核心。说白了,就是告诉它「谁先跑,谁后跑」。Snakemake 不会傻傻地按你写规则的顺序执行——它只看文件依赖。你给每条规则定义好输入和输出,它就能自动画出一张有向无环图(DAG)。我刚开始用的时候也疑惑过:它怎么知道先跑哪个?其实逻辑很简单:谁的输出是别人的输入,谁就先跑,谁的文件还没准备好,就等着。

规则的输入输出与依赖链

每条规则都有 input 和 output。Snakemake 在启动时会扫描所有规则,建立从最终目标文件到最原始输入文件的依赖链。比如你做 FastQC 质控,规则可能是这样:

rule fastqc:
    input:
        "data/{sample}.fastq"
    output:
        "results/fastqc/{sample}_fastqc.html",
        "results/fastqc/{sample}_fastqc.zip"
    shell:
        "fastqc -o results/fastqc {input}"

接着你写一条汇总规则:

rule multiqc:
    input:
        expand("results/fastqc/{sample}_fastqc.html", sample=SAMPLES)
    output:
        "results/multiqc_report.html"
    shell:
        "multiqc results/fastqc -o results"

你看,multiqc 的输入依赖 fastqc 的输出文件。Snakemake 立刻知道:必须先跑 fastqc,再跑 multiqc。这就是最基础的依赖链。我管它叫「文件串联」。

DAG 是什么?为什么必须是 DAG?

有向无环图,就是每个节点(规则)只能沿着箭头方向走,不能绕回来形成环。如果 A 需要 B 的输出,B 又需要 A 的输出,那就成死循环了。Snakemake 会直接报错退出。我记得有一次写质控流程,不小心让两个规则输出了同一个文件名,结果依赖链绕回去了——Snakemake 弹了个大红字 Cycle detected。嗯,从那以后我养成了一个习惯:每个规则的输出文件名必须唯一,不交叉。

下面这张图展示了一个典型的质控 DAG:

Snakemake 质控流程 DAG 示例 原始 FASTQ 文件 FastQC 质控 Trimmomatic 修剪 MultiQC 汇总 修剪后 FastQC 可选 每个节点代表一条规则,箭头方向表示文件流动方向。无环结构保证流程可以并行执行。

这张图里,原始数据同时流向 FastQC 和 Trimmomatic,二者互不依赖,可以并行。修剪后再做一次 FastQC,最后 MultiQC 汇总所有报告。整个流程没有回头路,Snakemake 很满意。

dry-run 模式:跑之前先看看计划

我每次都喜欢先 dry-run 一次。不是因为不自信,而是因为 DAG 一旦复杂起来,你很难肉眼看出依赖关系对不对。dry-run 模式就是让 Snakemake 假装执行一遍,只输出要做什么,不改任何文件。

用法很简单:

snakemake -n -j 1

加上 --dryrun 也行,效果一样。控制台会输出类似的内容:

Building DAG of jobs...
Job stats:
job              count    min threads    max threads
-------------  -------  -------------  -------------
fastqc               2              1              1
multiqc              1              1              1
trimmomatic          2              1              1
total                5              1              1

This was a dry-run (flag -n). The order of jobs does not reflect the order of execution.

你看到了吗?它算出了每个规则要跑多少次(通配符展开后),以及需要多少线程。我遇到过一次惨痛经历:不加 -n 直接跑了一个大流程,结果发现某个规则写错了输出路径,所有结果都覆盖到同一个文件上了。欲哭无泪。所以现在我只要写新规则,必先 snakemake -n 确认一下。

用 --dag 导出可视化的 DAG

光看文字不够过瘾?Snakemake 还能导出真正的图文件,配合 Graphviz 工具生成 PDF 或 PNG。

snakemake --dag | dot -Tpdf > dag.pdf

这行命令先让 Snakemake 输出 DOT 格式的 DAG 描述,然后用 dot 解析成 PDF。你想想看,如果一个流程有几十条规则,用这张图来排查依赖关系,比盯着 Snakefile 一行行找要高效得多。

小技巧: dry-run 加上 -p 参数(snakemake -n -p)会连每条规则要执行的 shell 命令也打印出来。我 debug 时经常用,能一眼看出通配符是否展开正确。

常见依赖问题与避坑

问题现象 原因 解决办法
Snakemake 报 "Cycle detected" 规则的输入/输出形成循环依赖 检查是否两个规则输出了相同文件,或通配符冲突导致文件名称重叠
dry-run 显示 job 数量为 0 目标文件已经全部存在,且没有设置 --force 用 snakemake -n -f 强制重新运行即可看到作业计划
DAG 中规则缺失某个节点 某个规则的输入文件没有对应的输出规则匹配 检查 Snakefile 是否遗漏了生成该输入文件的规则,或者通配符约束不匹配
我曾经踩过的坑: 在 dry-run 中看到规则数量是对的,但实际运行到一半报错 "MissingInputException"。原因是我的输入文件路径用了一个变量,结果该变量在不同规则里解析不一致。记住:dry-run 只检查规则是否存在,不检查输入文件是否真的在硬盘上。最好在 dry-run 之后,再用 snakemake -n -r 加上 --reason 看看每步的原因,同时确认路径正确性。

DAG 背后的拓扑排序

Snakemake 构建好 DAG 后,会做拓扑排序。说白了就是给所有规则排个执行顺序,保证每个规则执行时它的所有输入文件都已经准备好。如果 DAG 是分层的(比如多个样本并行),Snakemake 会把同一层里互不依赖的任务同时扔到多个核上跑。

我在项目中就遇到过这样的好处:一次做 50 个样本的质控,FastQC 和 Trimmomatic 互不依赖,Snakemake 自动就并行跑了,完全不用我手动写什么循环或并行脚本。你想想看,如果自己写 shell 脚本,多半要用 xargs -P 或者 wait 来控并发,而在 Snakemake 里,只要定义好依赖关系,它自己就把 DAG 排好序了。

dry-run 时 Snakemake 还会显示每步的大致顺序(虽然实际执行时是多线程并行,顺序不一定严格)。你可以用 --dag 输出的图来确认这个顺序是否合理。

核心原则: 依赖关系 = 文件关系。你不需要告诉 Snakemake「先做 A 再做 B」,你只需要让 B 的输入文件等于 A 的输出文件。剩下的交给 DAG。

最后提一句:Snakemake 的 DAG 是动态构建的。如果你用了 checkpoint 或 input_functions,DAG 可能会在运行过程中发生变化。不过对于标准质控流程,静态 DAG 已经足够。我建议初学者先把基础的依赖逻辑吃透,再碰那些高级特性。