コマンドを並列に実行するGNU parallelがとても便利
最近のコンピュータは複数のCPUコアを持っているので並列にコマンドを実行することができます。 たくさんの同じようなファイルに同じ処理を実行することは、私のやっているバイオインフォマティクスではよくあります。
しかし自分で並列に実行するスクリプトを書くことはそれほど簡単ではなく、ログや実行結果の確認など煩雑な処理を書かなければいけません。 この記事では、そうした処理を簡単にするGNU parallelというツールを紹介します。
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_tutorial
やman parallel
に目を通すことをオススメします。
私はまだ実際に実行して確認していませんが、GNU parallelにはSSH越しに複数のノードで並列にジョブを実行させる機能やパイプライン処理で中間のボトルネックになっている処理だけ並列化するなどの機能もあるようです。
これらの機能は確認し次第、ブログに追記していこうと思います。