当前位置:K88软件开发文章中心编程语言非主流编程语言Julia → 文章内容

Julia 并行计算

减小字体 增大字体 作者:佚名  来源:网上搜集  发布时间:2019-1-15 16:28:21

由 陈 创建, 最后一次修改 2016-08-12 并行计算Julia 提供了一个基于消息传递的多处理器环境,能够同时在多处理器上使用独立的内存空间运行程序。Julia 的消息传递与 MPI [1] 等环境不同。Julia 中的通信是“单边”的,即程序员只需要管理双处理器运算中的一个处理器即可。Julia 中的并行编程基于两个原语:remote references 和 remote calls 。remote reference 对象,用于从任意的处理器,查阅指定处理器上存储的对象。 remote call 请求,用于一个处理器对另一个(也有可能是同一个)处理器调用某个函数处理某些参数。remote call 返回 remote reference 对象。 remote call 是立即返回的;调用它的处理器继续执行下一步操作,而 remote call 继续在某处执行。可以对 remotereference 调用 wait ,以等待 remote call 执行完毕,然后通过 fetch 获取结果的完整值。使用 put 可将值存储到 remote reference 。通过 julia -p n 启动,可以在本地机器上提供 n 个处理器。一般 n 等于机器上 CPU 内核个数:$ ./julia -p 2julia> r = remotecall(2, rand, 2, 2)RemoteRef(2,1,5)julia> fetch(r)2x2 Float64 Array: 0.60401 0.501111 0.174572 0.157411julia> s = @spawnat 2 1 .+ fetch(r)RemoteRef(2,1,7)julia> fetch(s)2x2 Float64 Array: 1.60401 1.50111 1.17457 1.15741remote_call 的第一个参数是要进行这个运算的处理器索引值。Julia 中大部分并行编程不查询特定的处理器或可用处理器的个数,但可认为 remote_call 是个为精细控制所提供的低级接口。第二个参数是要调用的函数,剩下的参数是该函数的参数。此例中,我们先让处理器 2 构造一个 2x2 的随机矩阵,然后我们在结果上加 1 。两个计算的结果保存在两个 remote reference 中,即 r 和 s 。 @spawnat 宏在由第一个参数指明的处理器上,计算第二个参数中的表达式。remote_call_fetch 函数可以立即获取要在远端计算的值。它等价于 fetch(remote_call(...)) ,但比之更高效:julia> remotecall_fetch(2, getindex, r, 1, 1)0.10824216411304866getindex(r,1,1) :ref:等价于 <man-array-indexing> r[1,1] ,因此,这个调用获取 remote reference 对象 r 的第一个元素。remote_call 语法不太方便。 @spawn 宏简化了这件事儿,它对表达式而非函数进行操作,并自动选取在哪儿进行计算:julia> r = @spawn rand(2,2)RemoteRef(1,1,0)julia> s = @spawn 1 .+ fetch(r)RemoteRef(1,1,1)julia> fetch(s)1.10824216411304866 1.137982338779231161.12376292706355074 1.18750497916607167注意,此处用 1 .+ fetch(r) 而不是 1 .+ r 。这是因为我们不知道代码在何处运行,而 fetch 会将需要的 r 移到做加法的处理器上。此例中, @spawn 很聪明,它知道在有 r 对象的处理器上进行计算,因而 fetch 将不做任何操作。( @spawn 不是内置函数,而是 Julia 定义的 :ref:宏 <man-macros> )所有执行程序代码的处理器上,都必须能获得程序代码。例如,输入: julia> function rand2(dims...) return 2*rand(dims...) endjulia> rand2(2,2)2x2 Float64 Array: 0.153756 0.368514 1.15119 0.918912julia> @spawn rand2(2,2)RemoteRef(1,1,1)julia> @spawn rand2(2,2)RemoteRef(2,1,2)julia> exception on 2: in anonymous: rand2 not defined 进程 1 知道 rand2 函数,但进程 2 不知道。 require 函数自动在当前所有可用的处理器上载入源文件,使所有的处理器都能运行代码: julia> require("myfile")在集群中,文件(及递归载入的任何文件)的内容会被发送到整个网络。可以使用 @everywhere 宏在所有处理器上执行命令: julia> @everywhere id = myid()julia> remotecall_fetch(2, ()->id)2@everywhere include("defs.jl")文件也可以在多个进程启动时预加载,并且一个驱动脚本可以用于驱动计算: julia -p <n> -L file1.jl -L file2.jl driver.jl每个进程都有一个关联的标识符。这个过程提供的 Julia 提示总是有一个 id 值为 1 ,就如上面例子中 julia 进程会运行驱动脚本一样。这个被默认用作平行操作的进程被称为 workers。当只有一个进程的时候,进程 1 就被当做一个 worker。否则,worker 就是指除了进程 1 之外的所有进程。Julia 内置有对于两种集群的支持: 如上文所示,一个本地集群指定使用 —p 选项。一个集群生成机器使用 --machinefile 选项。它使用一个无密码的 ssh 登来在指定的机器上启动 julia 工作进程(以相同的路径作为当前主机)。函数 addprocs,rmprocs,workers,当然还有其他的在一个集群中可用的以可编程的方式进行添加,删除和查询的函数。其他类型的集群可以通过编写自己的自定义 ClusterManager。请参阅 ClusterManagers 部分。数据移动并行计算中,消息传递和数据移动是最大的开销。减少这两者的数量,对性能至关重要。fetch 是显式的数据移动操作,它直接要求将对象移动到当前机器。 @spawn (及相关宏)也进行数据移动,但不是显式的,因而被称为隐式数据移动操作。对比如下两种构造随机矩阵并计算其平方的方法: : # method 1 A = rand(1000,1000) Bref = @spawn A^2 ... fetch(Bref) # method 2 Bref = @spawn rand(1000,1000)^2 ... fetch(Bref)方法 1 中,本地构造了一个随机矩阵,然后将其传递给做平方计算的处理器。方法 2 中,在同一处理器构造随机矩阵并进行平方计算。因此,方法 2 比方法 1 移动的数据少得多。并行映射和循环大部分并行计算不需要移动数据。最常见的是蒙特卡罗仿真。下例使用 @spawn 在两个处理器上仿真投硬币。先在 count_heads.jl 中写如下函数: function count_heads(n) c::Int = 0 for i=1:n c += randbool() end c end在两台机器上做仿真,最后将结果加起来: require("count_heads") a = @spawn count_heads(100000000) b = @spawn count_heads(100000000) fetch(a)+fetch(b)在多处理器上独立地进行迭代运算,然后用一些函数把它们的结果综合起来。综合的过程称为 约简 。上例中,我们显式调用了两个 @spawn 语句,它将并行计算限制在两个处理器上。要在任意个数

[1] [2] [3]  下一页


Julia 并行计算