在Julia

时间:2015-05-05 07:56:08

标签: performance parallel-processing julia hpc

我有一个很大的矢量字符串向量: 大约有50,000个字符串向量, 每个包含2-15个长度为1-20个字符的字符串。

MyScoringOperation是一个函数,它对字符串向量(基准面)进行操作,并返回一个10100得分的数组(如Float64s)。运行MyScoringOperation大约需要0.01秒(取决于基准面的长度)

function MyScoringOperation(state:State, datum::Vector{String})
      ...
      score::Vector{Float64} #Size of score = 10000

我有什么相当于嵌套循环。 外循环通常会运行500次迭代

data::Vector{Vector{String}} = loaddata()
for ii in 1:500 
    score_total = zeros(10100)
    for datum in data
         score_total+=MyScoringOperation(datum)
    end
end

在一台计算机上,在3000(而不是50,000)的小测试用例中,每个外环需要100-300秒。

我有3个功能强大的服务器,安装了Julia 3.9(可以轻松获得3个,然后在下一个规模上可以获得数百个)。

我有@parallel的基本经验,但似乎花了很多时间复制常量(它或多或少挂在较小的测试用例上)

看起来像:

data::Vector{Vector{String}} = loaddata()
state = init_state()
for ii in 1:500 

    score_total = @parallel(+) for datum in data
         MyScoringOperation(state, datum)
    end
    state = update(state, score_total)
end

我对@parallel实现方式的理解是:

每个 ii

  1. data分区为每个工作人员的夹头
  2. 将该夹头发送给每个工人
  3. 在那里处理所有过程
  4. 主程序在结果到达时对结果进行求和。
  5. 我想删除第2步, 这样,而不是向每个工人发送一大块数据, 我只是向每个工作人员发送一系列索引,然后从他们自己的data副本中查找。甚至更好,只给每个人自己的块,并让它们每次都重复使用(节省大量的RAM)。

    剖析支持了我对@parellel功能的看法。 对于类似范围的问题(甚至更小的数据), 非并行版本运行0.09秒, 并行运行 分析器显示几乎所有时间都花费了185秒。 Profiler显示,其中几乎100%用于与网络IO进行交互。

1 个答案:

答案 0 :(得分:4)

这应该让你开始:

function get_chunks(data::Vector, nchunks::Int)
    base_len, remainder = divrem(length(data),nchunks)
    chunk_len = fill(base_len,nchunks)
    chunk_len[1:remainder]+=1 #remained will always be less than nchunks
    function _it() 
        for ii in 1:nchunks
            chunk_start = sum(chunk_len[1:ii-1])+1
            chunk_end = chunk_start + chunk_len[ii] -1
            chunk = data[chunk_start: chunk_end]
            produce(chunk)
        end
    end
    Task(_it)
end

function r_chunk_data(data::Vector)
    all_chuncks = get_chunks(data, nworkers()) |> collect;
    remote_chunks = [put!(RemoteRef(pid)::RemoteRef, all_chuncks[ii]) for (ii,pid) in enumerate(workers())]
    #Have to add the type annotation sas otherwise it thinks that, RemoteRef(pid) might return a RemoteValue
end



function fetch_reduce(red_acc::Function, rem_results::Vector{RemoteRef})
    total = nothing 
    #TODO: consider strongly wrapping total in a lock, when in 0.4, so that it is garenteed safe 
    @sync for rr in rem_results
        function gather(rr)
            res=fetch(rr)
            if total===nothing
                total=res
            else 
                total=red_acc(total,res)
            end
        end
        @async gather(rr)
    end
    total
end

function prechunked_mapreduce(r_chunks::Vector{RemoteRef}, map_fun::Function, red_acc::Function)
    rem_results = map(r_chunks) do rchunk
        function do_mapred()
            @assert r_chunk.where==myid()
            @pipe r_chunk |> fetch |> map(map_fun,_) |> reduce(red_acc, _)
        end
        remotecall(r_chunk.where,do_mapred)
    end
    @pipe rem_results|> convert(Vector{RemoteRef},_) |> fetch_reduce(red_acc, _)
end

rchunk_data将数据分成块(由get_chunks方法定义)并将这些块分别发送给不同的工作者,并将它们存储在RemoteRefs中。 RemoteRefs是对其他进程(以及可能的计算机)的内存的引用,

prechunked_map_reduce对某种地图进行缩小以使每个工作人员首先在其每个卡盘元素上运行map_fun,然后使用{减少其卡盘中的所有元素{1}}(减少累加器函数)。最后,每个工作人员返回结果,然后使用red_acc使用red_acc将所有结果合并在一起,以便我们可以添加先完成的第一个结果。

fetch_reduce是非阻塞获取和减少操作。我相信它没有竞争条件,但这可能是因为fetch_reduce@async中的实现细节。当朱莉娅0.4出来时,很容易锁定它,使其显然没有竞争条件。

这段代码并没有真正的战斗力。我不相信 您还可能希望查看chuck大小可调,以便您可以向更快的工作人员看到更多数据(如果有更好的网络或更快的cpu)

您需要将代码重新表达为map-reduce问题,这看起来并不太难。

用以下方法测试:

@sync

当分布在8名工人(没有他们与发射器在同一台机器上)时,花了大约0.03秒

vs仅在本地运行:

data = [float([eye(100),eye(100)])[:] for _ in 1:3000] #480Mb
chunk_data(:data, data)
@time prechunked_mapreduce(:data, mean, (+))

耗时约0.06秒。