はじめに
少し前の投稿のように,便利にマルチスレッドを使っていたのですが,基本的にPlots.jl
はスレッドセーフではありません。
業務で大量にグラフを描画する必要があるので,やむを得ずマルチプロセスも試してみることにします。
といっても,それぞれ別々のファイルからデータを読み込んでグラフにし,別々の画像ファイルに出力するので, それぞれの処理に依存関係が全くありません。
なので,簡単にマルチプロセスが実現できると思い試してみました。 その試してみたことのノリが分かるような例を作ってみました。
内容
処理の部分を次のfuncs.jlような例を作ってみました。
基本的にダミーで,ただ待つ処理を間に入れているような感じです。
# funcs.jl using Plots """ read_dummy(filename) ファイルを読んでいるフリをする関数 """ function read_dummy(filename::AbstractString) @debug "dumy" filename sleep(1) # ファイルを読んでるフリ rand(Float64, (1000,)) end """ calc_dummy!(data) `data`を計算して加工するフリをする関数 """ function calc_dummy!(data::Vector{Float64}) sleep(2) # いろいろな計算をしているフリ data .= 1.0 .- data nothing end """ plot_dummy(data) プロットをするフリ?をする関数 """ function plot_dummy(data::Vector{Float64}) sleep(1) # 時間がかかる小芝居 plot(data, size=(1024, 512)) end
で先ほどのfuncs.jlの関数を使って順次処理を進めます。
次のようなコードを書きました。
# example_sp.jl using ProgressMeter include("./funcs.jl") datafilenames = "data" .* string.(1:20, pad=3) .* ".csv" pngfilenames = "output" .* string.(1:20, pad=3) .* ".png" @showprogress for i in eachindex(datafilenames) data = read_dummy(datafilenames[i]) calc_dummy!(data) plt = plot_dummy(data) savefig(plt, pngfilenames[i]) end
そして上のコードを実行させます。ProgressMeterの結果表示から
ループの部分だけの処理で1分25秒かかっています。
julia> import Pkg; Pkg.activate("/home/ujimushi/blog/test/mp/") Activating project at `~/blog/test/mp` julia> include("example_sp.jl") Progress: 100%|███████████████████████████████████████| Time: 0:01:25 julia>
次にマルチプロセス版を実装してみます。先達の記事, Juliaで超単純にマルチプロセスや, マルチプロセスのはまりどころ を参考にしながら実装していきます。
@everywhereをつけると各プロセスで定義することができるようです。また,
addprocs(...)の後に@everywhereを実行するのも先達の記事の通りにしました。
@everywhere inlcude(...)とできるのが楽チンです。ただ,定義する量が多いと初期化プロセスに時間がかかります。
今回はPlots.jlのモジュール読み込みに時間かがかっているようでした。また,事前にパッケージモードで
モジュールをアップデートしておかないと初期化プロセスのプリコンパイルが長くなってしまうので要注意です。
@distributedマクロは forとの間に2入力引数の関数の引数をとって,それぞれの結果について後処理用の関数として使えますが,
関数の引数を省略すると,ただのfor文のような感じで使えます。
今回は演算結果を利用しないので関数の引数を無しにして利用しています。
# example_mp.jl using Distributed addprocs(6, exeflags="--project") using ProgressMeter @everywhere include("./funcs.jl") @everywhere datafilenames = "data" .* string.(1:20, pad=3) .* ".csv" @everywhere pngfilenames = "output" .* string.(1:20, pad=3) .* ".png" @showprogress @distributed for i in eachindex(datafilenames) data = read_dummy(datafilenames[i]) calc_dummy!(data) plt = plot_dummy(data) savefig(plt, pngfilenames[i]) end
そして実行させてみます。ループの部分は20秒です。 全体の処理時間が短くなっています。
julia> import Pkg; Pkg.activate("/home/ujimushi/blog/test/mp/") Activating project at `~/blog/test/mp` julia> include("example_mp.jl") Progress: 100%|███████████████████████████████████████| Time: 0:00:20 julia>
ただ,これだけの記事では分かりづらいですが,Plots.jlを各プロセスで読み込むのにかなり時間がかかっています。
しかし,業務では3000件ぐらいの時系列データ(各1件当たり複数のデータ)のグラフを描かせており オーバーヘッドがそれほど気にならないぐらい全体の時間がかかるものだったので,非常に助かりました。
それぞれの処理に依存関係がある時はこう単純ではないのですが,コア数が多いCPUでは 有効に処理時間を削減できそうです。
その他
その他に利用しやすいのはmap関数をマルチプロセス化したpmap関数で,
result_map = @showprogress pmap(src_vectors) do src ... end
のような感じで処理状況を確認しながら演算することができます。
filterっぽい結果を返すなら
flags = @showprogress pmap(src_vectors) do src ... trueなら残す。falseなら削除する判定文 end src_filtered = src_vectors[flags]
とかで実現できますね。
なお,マルチスレッドの時もそうですが,for文のところでzip関数を使うとエラーになるとか 色々注意点はありそうなのですが,その辺りは体育系なので体当たりで 学んでいくといったところです。
エラーで学ぶタイプの自分にとっては動的スクリプト言語はありがたいです。 また,引数の型の指定も可能で,エディタからの支援も効きやすいのも嬉しいところです。