読者です 読者をやめる 読者になる 読者になる

りんごがでている

何か役に立つことを書きます

コマンドを並列に実行するGNU parallelがとても便利

最近のコンピュータは複数のCPUコアを持っているので並列にコマンドを実行することができます。 たくさんの同じようなファイルに同じ処理を実行することは、私のやっているバイオインフォマティクスではよくあります。

しかし自分で並列に実行するスクリプトを書くことはそれほど簡単ではなく、ログや実行結果の確認など煩雑な処理を書かなければいけません。 この記事では、そうした処理を簡単にするGNU parallelというツールを紹介します。

f:id:bicycle1885:20140810143806p:plain

GNU parallel

UNIX系のOSではインストールはとても簡単です。MacでしたらHomebrewを使って、Linuxでは各ディストリビューションのパッケージマネージャからインストールできます。 詳しくはGNU parallelのウェブページを参照して下さい(http://www.gnu.org/software/parallel/)。

Homebrew:

brew install parallel

パッケージマネージャが使えない環境でも、ソースコードからビルドすることができます。 ダウンロードページ(http://ftp.gnu.org/gnu/parallel/)から、最新のparallel(parallel-latest.tar.bz2)をダウンロードして展開し、ビルドしましょう。 必要ならばconfigureに--prefixも渡せます。

Source:

./configure && make && make install

なお、最初にparallelコマンドを実行する際に引用のお願いメッセージが出ますが、これはparallel --bibtexを一度実行すれば抑えられます。

Hello, GNU parallel !

インストールが終わったら、早速GNU parallelを実行してみましょう。

~/t/sample $ parallel echo ::: hello world !
hello
!
world

GNU parallelのparallelコマンドは、実行するコマンドと、そのコマンドに適用するいくつかの引数を引数としてとります。 上の例では、echoが実行するコマンドで、:::の後に続く3つの引数hello, world, !がそれぞれechoコマンドに適用する引数です。 echoされたメッセージが元の引数の順番と一致していないことから分かる通り、echoコマンドは並列に実行されるため、各引数の実行順は不定になります。

:::複数つなげることで、引数の組み合わせの積をつくることもできます。

~/t/sample $ parallel echo ::: hello, bye, ::: Alice Bob Charlie ::: !
hello, Alice !
hello, Bob !
hello, Charlie !
bye, Alice !
bye, Bob !
bye, Charlie !

ファイルや標準入出力から引数を与える

以下のようなファイル名のリストを収めたファイル(list.txt)があるとします。

~/t/sample $ cat list.txt
foo.txt
bar.txt
baz.txt

-a <file>オプションで、ファイルの各行を引数としてコマンドを並列に走らせることができます。 list.txtの各ファイルを並列に圧縮するには以下のようにします。

~/t/sample $ ls
bar.txt  baz.txt  foo.txt  list.txt
~/t/sample $ parallel -a list.txt gzip
~/t/sample $ ls
bar.txt.gz baz.txt.gz foo.txt.gz list.txt

パイプで引数を渡すこともできます。その時は-a -とオプションを渡します。

~/t/sample $ cat list.txt | parallel -a - gzip
~/t/sample $ ls
bar.txt.gz baz.txt.gz foo.txt.gz list.txt

引数の区切り文字はデフォルトでは\n(newline)です。 -0(--null)オプションで区切り文字が\0(null)文字になります。

他にも、<::::を使って引数の納められたファイルを渡すことができます。

~/t/sample $ parallel gzip < list.txt
~/t/sample $ parallel gzip :::: list.txt

引数の置き換え

コマンドに渡す引数の位置が最後でない場合や、特別な処理をしたい場合に引数の置換え位置を指定するをする必要があります。 その際には{}を使います。

~/t/sample $ parallel 'find {} -name "README*"' ::: ~/vendor/julia ~/vendor/vector
/Users/kenta/vendor/vector//old-testsuite/microsuite/README
/Users/kenta/vendor/vector//README.md
/Users/kenta/vendor/julia//contrib/mac/app/README
/Users/kenta/vendor/julia//contrib/README.ackrc.txt
...

さらに、{.}でファイル名の拡張子を除いた引数にしたり、{/}でファイル名だけ取り出したりできます。

~/t/sample $ parallel 'echo {.}' ::: tmp/foo.txt.gz tmp/bar.txt.gz
tmp/foo.txt
tmp/bar.txt
~/t/sample $ parallel 'echo {/}' ::: tmp/foo.txt.gz tmp/bar.txt.gz
foo.txt.gz
bar.txt.gz

実行されるコマンドの確認

--dry-runをつかえば、引数がどのようにコマンドに適用されてどのようなコマンドが実際に走るかを確認できます。 実際に実行する前に一度確認すると良いでしょう。

~/t/sample $ cat list.txt | parallel -a - --dry-run gzip
gzip foo.txt
gzip bar.txt
gzip baz.txt

実行結果の取得

標準出力と標準エラーに吐かれた結果をファイルとして取得することも容易です。 --results <outputdir>とすることで出力を各引数毎にディレクトリに構造化して保存できます。

~/t/sample $ parallel --results results 'perl -E "say STDOUT \"stdout\"; say STDERR \"stderr\""' ::: A B C
stdout
stderr
stdout
stderr
stdout
stderr
~/t/sample $ tree results/
results/
└── 1
    ├── A
    │   ├── stderr
    │   └── stdout
    ├── B
    │   ├── stderr
    │   └── stdout
    └── C
        ├── stderr
        └── stdout

4 directories, 6 files
~/t/sample $ cat results/1/A/stderr
stderr
~/t/sample $ cat results/1/A/stdout
stdout

実行結果の確認

時間のかかるコマンドや引数が多い場合など、実行結果の確認が大変な場合があります。 そこで、--joblog <logfile>を使えばコマンドのexit statusが確認しやすくなります。

~/t/sample $ parallel --joblog joblog.txt 'sleep {}; exit {}' ::: 0 1 2 3
~/t/sample $ cat joblog.txt
Seq     Host    Starttime       JobRuntime      Send    Receive Exitval Signal  Command
1       :       1407646480.280       0.072      0       0       0       0       sleep 0; exit 0
2       :       1407646480.284       1.144      0       0       1       0       sleep 1; exit 1
3       :       1407646480.289       2.125      0       0       2       0       sleep 2; exit 2
4       :       1407646480.294       3.153      0       0       3       0       sleep 3; exit 3

一目見て分かる通り、実行時間や実際に実行されたコマンドなども記録されるため、問題が発生した時のトラブルシューティングに非常に役に立ちます。 また、終わったコマンドから順に追記されていくため、進捗状況の確認もできます。

並列ジョブの制御

ファイルのダウンロードなどを並列に行いたいが過剰に負荷を掛けたくない場合や、逆にCPU boundでない処理をCPUのコア数より多く並列させたいときなどに同時実行するジョブ数を指定できると便利です。 デフォルトでは、マシンのコア数分だけ並列させるため、共有サーバーなどではCPUを占拠してしまい迷惑になることも考えられます。 そのようなときは--jobs <N>(-j <N>)オプションを使って、個分だけ並列化させる必要があります。

以下のように6つの引数を1, 2, 3, 6並列でそれぞれ実行して実行時間を見てみましょう。

~/t/sample $ time parallel --jobs 1 'sleep {}' ::: 1 1 1 1 1 1
        7.06 real         0.26 user         0.10 sys
~/t/sample $ time parallel --jobs 2 'sleep {}' ::: 1 1 1 1 1 1
        3.58 real         0.27 user         0.11 sys
~/t/sample $ time parallel --jobs 3 'sleep {}' ::: 1 1 1 1 1 1
        2.44 real         0.28 user         0.11 sys
~/t/sample $ time parallel --jobs 6 'sleep {}' ::: 1 1 1 1 1 1
        1.30 real         0.30 user         0.12 sys

--jobs 50%などと書くことで全体の半分のCPUコアを使う指定もできます。

~/t/sample $ time parallel --jobs 50% 'sleep {}' ::: 1 1 1 1 1 1
        3.59 real         0.26 user         0.11 sys

また、メモリを沢山必要とする計算などでは、--noswapオプションを指定することで、メモリのスワップが発生している時には新しいジョブを実行しないようにすることもできます。

他の便利そうな機能

ここで紹介した機能はGNU parallelのごく一部で、他にも有用なオプションなどがたくさんあります。 一度、man parallel_tutorialman parallelに目を通すことをオススメします。 私はまだ実際に実行して確認していませんが、GNU parallelにはSSH越しに複数のノードで並列にジョブを実行させる機能やパイプライン処理で中間のボトルネックになっている処理だけ並列化するなどの機能もあるようです。 これらの機能は確認し次第、ブログに追記していこうと思います。