规则依赖与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:
这张图里,原始数据同时流向 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 一行行找要高效得多。
-p 参数(snakemake -n -p)会连每条规则要执行的 shell 命令也打印出来。我 debug 时经常用,能一眼看出通配符是否展开正确。
常见依赖问题与避坑
| 问题现象 | 原因 | 解决办法 |
|---|---|---|
| Snakemake 报 "Cycle detected" | 规则的输入/输出形成循环依赖 | 检查是否两个规则输出了相同文件,或通配符冲突导致文件名称重叠 |
| dry-run 显示 job 数量为 0 | 目标文件已经全部存在,且没有设置 --force |
用 snakemake -n -f 强制重新运行即可看到作业计划 |
| DAG 中规则缺失某个节点 | 某个规则的输入文件没有对应的输出规则匹配 | 检查 Snakefile 是否遗漏了生成该输入文件的规则,或者通配符约束不匹配 |
snakemake -n -r 加上 --reason 看看每步的原因,同时确认路径正确性。
DAG 背后的拓扑排序
Snakemake 构建好 DAG 后,会做拓扑排序。说白了就是给所有规则排个执行顺序,保证每个规则执行时它的所有输入文件都已经准备好。如果 DAG 是分层的(比如多个样本并行),Snakemake 会把同一层里互不依赖的任务同时扔到多个核上跑。
我在项目中就遇到过这样的好处:一次做 50 个样本的质控,FastQC 和 Trimmomatic 互不依赖,Snakemake 自动就并行跑了,完全不用我手动写什么循环或并行脚本。你想想看,如果自己写 shell 脚本,多半要用 xargs -P 或者 wait 来控并发,而在 Snakemake 里,只要定义好依赖关系,它自己就把 DAG 排好序了。
dry-run 时 Snakemake 还会显示每步的大致顺序(虽然实际执行时是多线程并行,顺序不一定严格)。你可以用 --dag 输出的图来确认这个顺序是否合理。
最后提一句:Snakemake 的 DAG 是动态构建的。如果你用了 checkpoint 或 input_functions,DAG 可能会在运行过程中发生变化。不过对于标准质控流程,静态 DAG 已经足够。我建议初学者先把基础的依赖逻辑吃透,再碰那些高级特性。