動機
自然科学系の多くの分野で要求されるデータ処理には、以下のような特徴が一般的だと思います。
- 互いに依存関係のある複数の処理を順番に行う
- コードを試行錯誤しながら反復的に実行する。出力された図や数値を人間が見てコードを修正し、必要なステップを再実行する必要がしばしば生じる
- 処理間で渡されるデータの形式が決まっておらず、ファイル数も多くなりがち
個人的には2番目の特徴が厄介で、コードの実行漏れやコードとデータの乖離を防ぐためにこれを自動化したいと前々から思っていました。
特徴1,2だけを見ると、愚直にMakefileにファイルの依存関係を書いて利用すれば解決できそうですが(参照に挙げたGNU Make for Reproducible Data Analysisがこの例)、3つ目の特徴によりファイルの依存関係をMakefileに書いてしまうと、頻繁にMakefileを修正する必要があり効率的ではありません。
そこで、以下のような対案を実装したところ使い勝手が良かったので共有します。
- やりとりされるデータファイルではなく、処理そのものに着目した依存関係をMakefileに記述する
サンプル (Makefile)
※コピペするとインデントがスペースになるので注意。タブに直してください。
# ================================
# STEP_ALL : スペース区切りで全ステップ名。数字以外も使用可能
# src_[step] : 実行判断に利用されるソースファイルや実行プログラム
# dep_[step] : 依存するステップ名
# out_[step] : 出力ファイル(ワイルドカード可)。実行には関係ないが、make cleanで消去できるので書いておくと便利。
# cmd_[step] : 実行コマンド
# ================================
STEP_ALL = 1 2
src_1 = src1.sh # echo xxx > out1.txt
dep_1 =
out_1 = out1.txt
cmd_1 = sh src1.sh
src_2 = src2.sh # cat out1.txt >> out2.txt
dep_2 = 1
out_2 = out2.txt
cmd_2 = sh src2.sh
# ===============================================
# 以下、利用時に編集の必要なし
# ===============================================
VPATH = .make
all: $(STEP_ALL)
# 依存関係のテンプレート
define rule_comm
$1: $(src_$1) $(dep_$1)
@echo "STEP $1:"
$(cmd_$1)
@mkdir -p $(VPATH)
@touch $(VPATH)/$1
endef
# 各ステップ分の依存関係を作成する
$(foreach i, $(STEP_ALL), $(eval $(call rule_comm,$i)))
clean:
rm -rf $(foreach i,$(STEP_ALL),$(out_$i))
@rm -rf $(VPATH)
.PHONY: all clean
簡単なサンプルとして2ステップのものを掲載しましたが、より実用的な依存関係の例としてはこんな感じでしょうか。(src_, cmd_, out_も各ステップ分追加する必要があります。)
STEP_ALL = compile1 exec1 plot1 compile2 exec2 plot2 latex
dep_compile1 =
dep_exec1 = compile1
dep_plot1 = exec1
dep_compile2 =
dep_exec2 = compile2
dep_plot2 = exec2
dep_latex = plot1 plot2
サンプルコードの解説
カレントディレクトリに隠しディレクトリ.makeを作成し、その中に各ステップの実行時刻を変更時刻(mtime)に持つ空ファイルを作成します。Makeが src_* の更新及び dep_* の新規実行を監視して必要最低限のコマンドを実行してくれます。
実行時にMakeに引数を渡してやれば、任意のステップのみを実行することも可能です。
$ make # 全ステップと必要な依存関係が更新される
$ make plot1 # plot1とその依存関係のみ更新される
GNU Make 3.81で動作確認をしています。
課題
上記サンプルの問題点と対策案
- スパコンなどで利用されるバッチ型ジョブスケジューラとの連携
- sleep等を利用しつつジョブを監視し、終了時に続きを実行する
- ジョブを投げた時点でmakeを終了。ジョブの末尾でmakeを起動するようにしておいて再帰的に実行する(すでに終了しているプロセスは自動的にスキップして続きから実行される)
- スケジューラが要るような大きなジョブは(安易に投げられない=あまりご利益がないので)別管理にする
参照
makeを使ったデータ処理。
http://www.slideshare.net/HirofumiSaito/osc2015-tokyo-spring20150228
GNU Make for Reproducible Data Analysis
http://zmjones.com/make/