这大概是程序串行改并行最简单粗暴的方法

今天给大家安利一个神器:GNU Parallel 。 如果你也做计算,你也用Unix,但还没有听说过它的话, 那一定要去学一学,我跟你说,Parallel——–赞!!!

并行计算的困惑

单核的时代早就结束不知道多长时间了,计算机硬件的发展已经在多核、多处理机的方向上一骑绝尘。 它发展的太快,或者说人们对于多核适应的太慢,以致于二者已经产生了很大的脱节。虽说大多数程序员都 或多或少能够用某种编程语言的并发库(比如openmp或者是Java的并发库)来编写并行程序了, 但不得不说,我们的思维方式更多时候还是停留在串行上,当然一方面串行的逻辑能够自然地为人所理解, 再一方面就是受到我们在学校所接受的计算机教育的影响,一般来说大学里教给我们的编程知识, 都是以计算机只有一个运算核心来作为假定的, 比如,在学校学到的C、C++、Java这些语言虽然都有并行的解决方案,但这些语言从 根本上来说,都不是为了并发而设计的,一般来说只有少数人会出于兴趣去学习Erlang、Scala这样的 更加适合并发的语言。再比如说微机系统、体系结构等一系列有关于计算机系统架构的课程中,虽然并行架构 都会有所提及,但重点都还是放在传统的串行架构上。当然了,学习嘛,由浅入深,一开始先学简单的内容 循序渐进,才更能让我们理解,所以这也怪不得学校。但这种脱节,在我们走入生产环境真正要干活的时候, 就会给我们带来很多的问题。

根据我肤浅的认识,这种问题主要是以下两个方面的:

  1. 并发程序难以编写。首先并发编程天生是困难的,先不说很多问题本身就是不可能并行的,就可并行的 问题来说,在编写并行程序的时候,不同于串行程序,除了着力于解决问题本身之外,还要考虑各个进程、 线程之间的协作、通讯问题,程序编写的复杂程度自然就上升了一个层次。 再者,就如之前说的,大多数时候,由于各种原因,我们使用编程语言都是以单核为基础设计出来的, 相比于串行程序的编写,并行写出来往往会显得非常蹩脚、不自然,因而维护和扩展的难度也会增加不少。
  2. 历史遗留问题。虽说现在的程序一般都是支持并行的,比如说生物信息计算常用的samtools、bedtools 这些东西,你给它指定一个线程数作为参数,就会自动地将计算分配到几个线程上去。 但单核时代在历史上的确是持续了相当长的一段时间,自然会遗留下来大量的程序宝库, 这些程序都是经过很多大神们设计、优化过的,一时半会也不可能将其抛弃掉, 就比如说Unix中的cut、uniq、join、sed、awk等等这些系统自带命令行工具,通过管道串联、输出重定向, 平常用来处理文本、表格非常好用,但也很可惜这些都是串行程序,数据量一大就显得有些无能为力了。

自动化并行计算

对于上面的问题,一种比较理想的解决办法是通过一个管理程序自动对计算任务进行分割与分配。 对于一个可分解的任务,我们只需要通过简单的接口将相关信息目标告诉主控程序,然后由它自动地对计算任务进行 切分,然后为每个子任务开启一个进程,分配到不同的计算单元上去进行计算,并且管理程序还能够在整个计算过程 中动态监控每个核心的状态,最大化利用CPU资源。 例如,利用4个cpu来完成32个独立的计算任务,应该怎么做呢?一种方案是每个核心负责8个任务,那么所有CPU的整体 利用情况可能是这样的: 可以看到,由于每个核心上的计算不是同时完成的,所以造成了较多的CPU时间的浪费,但如果计算是动态分配的,那 么情况就会变成这样: 可以看到被浪费掉的CPU时间大大减少了! 这样一来,我们就能够合理地利用上多个CPU来进行并行计算了, 并且通过这种方式能够轻松地将并行编程化解为串行编程,节省大量用在调试程序并行接口上的时间, 这是多么美好啊!这也就是Parallel要做的事情。

GNU Parallel

说了这么多废话,差不多该进入主题了,下面正式开始介绍今天的主角了。

安装

首先安装非常简单,如果你没有root权限(有root就直接apt了,怕不是废话…), 可以通过如下命令安装:

1
(wget -O - pi.dk/3 || curl pi.dk/3/ || fetch -o - http://pi.dk/3) | bash

基本使用

之前说了 Parallel 所做的事情就是对计算任务进行合理的分配,所以,首先你要告诉它都有哪些计算任务。 Parallel 通过一些特殊的记号对命令行进行解释来做到这一点,有点像 xargs,但要更灵活一些。举一个简单的栗子: 比如,我的目录里现在有几个fastq文件,我现在要把它们进行压缩,你可能会 gzip ./*.fq 这样来跑, 但这样不能发挥我们计算机的多核优势,这时我就可以通过如下方式对它进行并行化:

1
$ ls *.fq | parallel gzip {}

这里将ls命令产生的结果通过管道传递给parallel,然后parallel动态地根据cpu核心的使用情况来开启gzip进程, 并将参数传递给它。这里的{}符号是参数的占位符,在运行过程中parallel会用管道中的数据行去替换它。 比如这里的{}会被ls产生的.fq文件的文件名替换掉,ls产生的每一行都会被当做一个参数来初始化一个gzip进程。 在命令在运行的时候,如果你打开系统资源监控器,会看到计算机上所有的核心都在进行工作, 加入你不想使用所有的计算资源,你可以通过 -P 选项来指定在同一时间内运行的最大进程数。

1
$ ls *.fq | parallel -P 2 gzip {}

这样一来在运行的时候就最多只有两个核心会被占用。

参数传递

从上面的例子可以看到,parallel可以通过管道和占位符相结合的方式来传递命令行参数,{}是最简单与基本的占位符, 表达对管道内输入行的完整替换,为了方便地对参数进行修改与组合,parallel还引入了其他种类的占位符。 比如:{.}代表去掉后缀后的输入行、{/}代表输入行去掉路径后的文件名,{//}则代表输入行的路径。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
$ mkdir txts
$ touch txts/{1..5}.txt
$ ls txts/*.txt
txts/1.txt txts/2.txt txts/3.txt txts/4.txt txts/5.txt
$ ls txts/*.txt | parallel echo {}
txts/1.txt
txts/2.txt
txts/3.txt
txts/4.txt
txts/5.txt
$ ls txts/*.txt | parallel echo {.}
txts/1
txts/2
txts/3
txts/4
txts/5
$ ls txts/*.txt | parallel echo {/}
1.txt
2.txt
3.txt
4.txt
5.txt
$ ls txts/*.txt | parallel echo {//}
txts
txts
txts
txts
txts

除此之外,还有几种占位符,但显然这玩意儿不是很方便记忆,还是用的时候man parallel查询一下比较好。

多源输入

上面举的例子里面命令行的参数仅仅来源于标准输入这一个来源,此外参数源还可以是字符串或者文件。 :::用来指定来自于字符串的参数,::::用来指定来自于文件的参数,二者均可重复, 每次重复产生一个新的源,可以通过{n}占位符来取得第n个源中的输入。 :::::::二者之间可以组合使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
$ parallel echo {} ::: $(seq 1 5)
1
2
3
4
5
$ seq 1 5 > list.txt
$ parallel echo {} :::: list.txt
1
2
3
4
5
$ parallel echo {1} {2} ::: $(seq 1 2) ::: $(seq 3 4)
1 3
1 4
2 3
2 4
$ parallel echo {1} {2} ::: $(seq 1 2) :::: <(seq 3 4)
1 3
1 4
2 3
2 4

对文件进行分块并行

从输入的角度来看,所有上面的例子里面所展示的‘并行’都是文件级别的,算是‘粒度’比较大的。 然而很多时候我们程序的输入只是一个单独的文件,比如一个十几G的fastq文件。这种情况下,当然一种可行的 办法是先对这个大文件进行分割(split命令),然后再用上面的方法进行并行计算。但这并不高效, 也不便利。 这时可以通过parallel 加上--pipe选项,来让它帮我们自动对单一的输入进行分割,进行并行计算。

比如要对一个比较大的fasta进行blastp,

1
cat 1gb.fasta | parallel --block 100k --recstart '>' --pipe blastp -evalue 0.01 -outfmt 6 -db db.fa -query - > results

这里parallel加上了 --pipe 选项后,标准输入就用于输入带分割的文件而不是像之前那样的文件列表了, 这里的--block 100k 表示分割的块的大小为100kB,同时为了确保单一的fasta记录没有被拆分到不同的块中, --recstart '>'指定了记录的开始标记为’>’字符。这样原来的一个fasta会分到多个blastp进程中去跑, 所有进程的输出都会被重定向到result中去。

你可能会发现,这样并行之后可能就会有另一个问题,由于几个进程是并行的,所以输出的顺序会和输入顺序不一致。 在一些情景下,这样的结果不是我们所希望的。这时可以加上参数--fifo来保证输出的顺序与输入保持一致。

更多的栗子这里就不放了,生信分析上应用的一些栗子可以在这个帖子里找到, 都是非常实用的SAO操作,强烈推荐!

多机并行

多机并行不用说都知道是挺麻烦的事情,我所了解到的解决方案除了Spark、Hadoop之流(不是很了解,不敢多说), 一种是基于MPI(Message Passing Interface) 编写可以多机间进行通讯的程序,再有就是生信狗们几乎每天都会用到的PBS (Portable Batch System)系统。 MPI当然是最灵活的,但相对比较底层,自己需要干的事情太多,一般是不会用的。 PBS其实算是比较好的,提供了运算任务在集群上的自动化控制、调度机制。对于大型的、多人的集群管理是比较好的。 但对于仅限于少数人使用的几台机器组成的小型集群来说也许是有点重了,使用不方便,配置起来也有点麻烦。

parallel也提供了一种多机并行的机制,使用起来几乎就和单机的parallel没太大区别。从上面的帖子里直接copy过来个 例子来说吧。还是刚才的blast命令,怎么让它在多台机器上运行呢?

1
cat 1gb.fasta | parallel -S :,server1,server2 --block 100k --recstart '>' --pipe blastp -evalue 0.01 -outfmt 6 -db db.fa -query - > result

这里-S参数指定了要使用的机器列表,这里指定了:本地机器、server1和server2。parallel是以ssh方式登录远程机器的, 所以为了让它知道server1和server2的相关信息,需要事先把server1和server2的信息存在~/.ssh/config这个文件里面。

结语

上面介绍了parallel这个对于使用命令行的人来说可以说是极其好用的工具, 可能你看完还是不知道怎么用,嘛~命令行嘛,熟能生巧,多敲几次,多翻翻man page自然就会掌握的, 希望本文能为你带来帮助,谢谢!