python joblib&随机漫步 - [CONCURRENT] - 进程调度的一种表现

时间:2018-06-14 15:30:32

标签: python performance parallel-processing joblib parallelism-amdahl

这是我的用于模拟一维反射随机游走的python-3.6代码,使用joblib模块在​​Linux集群机器上的 K 工作者之间同时生成400个实现

但是,我注意到K=3的运行时比K=1的运行时更差,K=5的运行时更差!

任何人都可以看到一种方法来改善我对joblib的使用吗?

from math import sqrt
import numpy as np
import joblib as jl
import os

K = int(os.environ['SLURM_CPUS_PER_TASK'])

def f(j):
    N = 10**6
    p = 1/3
    np.random.seed(None)
    X = 2*np.random.binomial(1,p,N)-1   # X = 1 with probability p
    s = 0                               # X =-1 with probability 1-p
    m = 0 
    for t in range(0,N):
        s = max(0,s+X[t])
        m = max(m,s)
    return m

pool = jl.Parallel(n_jobs=K)
W = np.asarray(pool(jl.delayed(f)(j) for j in range(0,400)))
W      

2 个答案:

答案 0 :(得分:5)

  

一种改进我对joblib的使用的方法?

joblib 可以帮助并将提供帮助,但只有可以从分布式执行中受益的代码,如果执行成本分散在某些资源池中所以不是一个有效的加速。

调整joblib的预加载和批量大小参数只有在待分发的代码获得性能优化后才开始有意义。

如下所示,对此的一些努力已经显示~ 8x 的核心 - 加速实现仍然纯粹的[SERIAL]运行时间 {{每个随机漫步1}} (而不是每个项目~ 217,000 [us],如上所述)。

在此之后,可能会出现一些关于群集资源相关优化(性能破坏避免工作)的更难的工作,因为ab-definitio假定有意组织上述400次重复的分布式工作流程。

当且仅当:

时才有意义
  • 如果可能,以避免CPU饥饿,
  • 如果不必支付分布式作业调度的任何过多的附加成本。

也许这是一个很长但很重要的故事,关于表演保存或丢失的地方
执行摘要

几乎没有对Gene AMDAHL博士的论点给予更好的奖励:

上面定义的任务的内部结构非常~ 1,640,000 [us]

    由于PRNG-design,
  • 随机数生成主要是纯 - [SERIAL] 过程
  • [SERIAL]迭代器在预制计划的醉酒水手步骤的一维矢量上面是纯粹的 - 1E6 < / LI>

是的,&#34;外部&#34; -scope-of-work(同一过程的 400 重复)可以轻松转换为&#34;只是&#34; - [SERIAL](不是真正的 - [CONCURRENT],即使是教授和想要的大师试图告诉你)进程安排,但这样做的附加成本会比线性增加到运行时更糟糕并且考虑到[PARALLEL]部分没有重新设计性能,这种努力的净效应很容易破坏最初的良好意图(上面的QED,因为已发布的运行时间从{{ 1}} [SERIAL]朝向约13分钟,即使是少量10:52 - s)。

一个简短的测试证明,在使用标准python工具之后,K == 1 下以纯粹的K方式运行(而不是报告的<强大的>〜12 - 13分钟)甚至在一个相当石器时代的古老桌面设备上(一些缓存计算效果是可能的,但更多的是偶然的副作用,而不是故意的HPC驱动的代码重构对于HPC群集特定性能):

[SERIAL]

< 1.45 [s]?为什么?怎么样 ?这就是关于...... 的全部故事(适当的HPC努力可以使其在1 [s]之下更好)

博士。基因AMDAHL的论点,即使是他最初的附加开销不可知形式,在他引用得很好的报告中也表明,任何u@amd64FX:~$ lstopo --of ascii +-----------------------------------------------------------------+ | Machine (7969MB) | | | | +------------------------------------------------------------+ | | | Package P#0 | | | | | | | | +--------------------------------------------------------+ | | | | | L3 (8192KB) | | | | | +--------------------------------------------------------+ | | | | | | | | +--------------------------+ +--------------------------+ | | | | | L2 (2048KB) | | L2 (2048KB) | | | | | +--------------------------+ +--------------------------+ | | | | | | | | +--------------------------+ +--------------------------+ | | | | | L1i (64KB) | | L1i (64KB) | | | | | +--------------------------+ +--------------------------+ | | | | | | | | +------------++------------+ +------------++------------+ | | | | | L1d (16KB) || L1d (16KB) | | L1d (16KB) || L1d (16KB) | | | | | +------------++------------+ +------------++------------+ | | | | | | | | +------------++------------+ +------------++------------+ | | | | | Core P#0 || Core P#1 | | Core P#2 || Core P#3 | | | | | | || | | || | | | | | | +--------+ || +--------+ | | +--------+ || +--------+ | | | | | | | PU P#0 | || | PU P#1 | | | | PU P#2 | || | PU P#3 | | | | | | | +--------+ || +--------+ | | +--------+ || +--------+ | | | | | +------------++------------+ +------------++------------+ | | | +------------------------------------------------------------+ | | | +-----------------------------------------------------------------+ +-----------------------------------------------------------------+ | Host: amd64FX | | Date: Fri 15 Jun 2018 07:08:44 AM | +-----------------------------------------------------------------+ < 1.45 [s]工作块的组合都会有主要是受益于[SERIAL] - 部分(称为减少收益的法则,即使是无限数量的处理器的渐近有限的加速)所使用的处理单元数量增加的有限受益,任何为 [PARALLEL] -part引入的改进将继续以额外的方式增加加速(以纯线性方式)。让我跳过这里的不利影响(也影响加速,有些是类似的纯线性方式,但在不利的意义上 - 附加开销 - 因为这些将在下面讨论)。

步骤1:
修复代码,以便做出有用的事情。

根据上面的代码,没有随机漫步。

为什么?

[PARALLEL]

所以,
代码as-is产生一个相当昂贵的先验已知常量列表。 完全没有随机性。 诅咒整数除法的蟒蛇四舍五入。 [SERIAL]

>>> [ op( np.random.binomial( 1, 1 /3,  1E9 ) ) for op in ( sum, min, max, len ) ]
[0, 0, 0, 1000000000]

所以这是固定的。

第2步:
了解开销(最好还有任何隐藏的性能阻滞器)

尝试实例化分布式流程(为每个指示的:o) - 数量 >>> [ op( np.random.binomial( 1, 1./3., 1E9 ) ) for op in ( sum, min, max, len ) ] [333338430, 0, 1, 1000000000] -spawned进程,调用<强> K 与子流程,而不是线程,后端)使您支付成本。总是...

鉴于此,
您的代码执行将获得额外的 joblib -add-on代码,必须在任何之前运行...仍然只是理论上的...... multiprocessing分裂 - 效果可能会开始发生。

仔细研究&#34;有用的&#34;工作:

[SERIAL]

对于这种类型的工作包,最好的演示目的加速可以从非解释的,无GIL,线程后端( 1 / n_jobs ) - 生成的Cython-ised代码包中证明,< strong> def f( j ): # T0 #pass; np.random.seed( None ) # + ~ 250 [us] prnGEN = np.random.RandomState() # + ~ 230 [us] # = 2 * np.random.binomial( 1, 1./3., 1E6 ) - 1 # + ~ 465,000 [us] X = prnGEN.binomial( 1, 1./3., 1E6 ) # + ~ 393,000 X*= 2 # + ~ 2.940 X-= 1 # + ~ 2.940 s = 0; m = 0 # + ~ 3 [us] for t in range( 0, int( 1E6 ) ): # ( py3+ does not allocate range() but works as an xrange()-generator s = max( 0, s + X[t] ) # + ~ 15 [us] cache-line friendly consecutive { hit | miss }-rulez here, heavily ... m = max( m, s ) # + ~ 5 [us] return m # = ~ 2,150,000 [us] @ i5/2.67 GHz # # = ~ 1,002,250 [us] @ amd/3.6 GHz -ed,带有 multiprocessing.Pool 指令。可能期望这样的代码执行大约cdef每个纯nogil随机漫步,= ~ 217,000 [us],当它开始有意义利用池时代码执行节点的一些预加载调整,以免让他们饿死。然而,所有过早优化警告在这个简化的背景下应该是有效的,并且适当的工程实践将用于实现专业级的结果。

有些工具可以帮助您看到,通过任何相应的高级语言语法构造函数元素(或并发/并行化#pragma伪装)来降低程序集级别,实际添加了多少指令,以及#34;气味&# 34;这些附加处理成本将在最终的代码执行期间支付:

enter image description here

鉴于这些附加处理成本,&#34;小&#34 ;-(瘦) - 工作量&#34;内部&#34; &#34;只是&#34; - 同时执行(当心,不能自动实现真正的[PARALLEL] - 可能),这些附加费用可能会让您付出的收费超过您的收入通过分裂。

阻挡者:

任何附加通信/同步都可能进一步破坏理论上的Speedup代码执行流程。如果不使用线程后端,信号量,套接字通信,共享等,则可以避免锁定,GIL是常见的阻止程序。

对于精心设计的随机来源,任何来自这种&#34;设备的抽奖&#34;还必须集中重新同步,以保持这种随机性的质量。这可能会导致幕后的额外麻烦(在具有一些权威认证的随机源的系统上的常见问题)。

下一步?

最好阅读more details on Amdahl's Law,当代重新制定的版本,其中在Overhead-strict模式中添加了设置+终止开销,并且practical evaluation of realistic speedup limitations <还考虑了处理的原子性/ p>

下一步:衡量代码的净持续时间成本,并间接获得您的体内系统执行中设置+终止开销的附加成本。

[SERIAL]
  

对于课堂教程,我使用R,Matlab,Julia和amp;中的特殊模块成功并行化了我的随机漫游代码。 Stata的。 (通过&#34;成功&#34;,我的意思是很清楚,20名工作人员在同一时间间隔内完成的工作量至少是1名工作人员的10倍。)这样的内部并行化在Python中不可行吗?

嗯,最后的评论似乎给那些试图提供帮助的人带来了一些不便,他们提出了原因,为什么发布的代码按原样运作。我们不是以这种方式定义处理策略的选择,是吗?

所以,
再次,
鉴于最初的决定是为了 使用 1E6 + def f( j ): ts = time.time() #------------------------------------------------------<clock>-ed SECTION N = 10**6 p = 1./3. np.random.seed( None ) # RandomState coordination ... X = 2 * np.random.binomial( 1, p, N ) - 1 # X = 1 with probability p s = 0 # X =-1 with probability 1-p m = 0 for t in range( 0, N ): s = max( 0, s + X[t] ) m = max( m, s ) #------------------------------------------------------<clock>-ed SECTION return ( m, time.time() - ts ) # tuple + python-3.6 工具,只需 Alea Iacta Est ...

{R |可能有用(如引用) MATLAB |朱莉娅| Stata}只是并不意味着它在GIL步骤中的工作方式相同, joblib.Parallel() - 生成的生态系统就越少。

第一个费用总是支付joblib.delayed() - 生成的作业是重建当前python-interpreter状态的整个1:1副本的成本。鉴于当前状态包含更多的对象实例,而不是精简的MCVE代码(正如@rth演示的 MCVE脚本所示),整个,多次复制的内存-image首先必须被复制+传输+重新构建到所有分布式处理节点上,与SLURM管理的集群足迹相对应,这些都会增加(非生产性)开销时间。如果有疑问,请在python解释器的状态中添加几个GB大小的numpy-arrays,并将测量的时间戳用于相应的持续时间计算到第一个和最后一个数组单元格中,最后 joblib.Parallel() 。总体执行时间会跳得更高,因为初始的1:1副本和返回路径都必须在那里移回更大量的数据(同样,有关实例化相关的附加成本的详细信息,已经有在这里的许多地方发布,包括系统基准测试相应附加成本的模板。)

确切地说,这就是建议那种O / P确实可以衡量有效计算时间量的原因(&#34;福利&#34;基本成本/收益论证的一部分) ),在平凡的实验中都很便宜,它将显示在远程&#34; - 执行的高效计算有效载荷内的有用工作的规模,总和和实际比例(s) )(参考上面提出的代码修改,返回值,以便joblib.Parallel()告诉&#34; 有用工作的实际&#34; net&#34;计算成本>&#34;在高效工作包计算时间内花费,一旦它们最终到达并激活到相应的远程代码执行生态系统(这里,以return ( m, aFatArray ) - 生成的原始python解释器的二进制全尺寸副本)而主代码执行的开始和结束之间的时间流显示实际费用 - 这里是lumpsum花费的金额时间,即包括所有&#34;远程&#34; -process-instantiation(s)+所有相应的工作包 - 分发+所有&#34;远程&#34; -process-termination

对于那些没有花时间阅读有关随机性相关问题的人的最后评论:

任何好的做法都应该避免隐藏在后面的阻塞逻辑 &#34;共享随机性&#34 ;.更好地使用单独配置的PRNG源。如果您有兴趣或需要可认证的PRNG稳健性,请随​​时阅读in a discussion here

答案 1 :(得分:2)

@user3666197 wrote a very nice answer about overhead, with a lot of bolded text ;)但是,我想引起你的注意,当你用K = 1运行你的代码时,你只做一次随机游走。当K = 3或5时,您同时执行3或5次随机游走(似乎)。因此,您需要将K = 1的运行时间乘以3或5,以获得所需的运行时间,从而完成相同的工作。我想这个运行时间会比你的大得多。

嗯,提供一个有用的答案,而不仅仅是一个注释(OP在评论中是正确的)。似乎 multiprocessing 模块是更好的选择。 这是你的代码

from math import sqrt
import numpy as np

from multiprocessing import Pool
import os

K = int(os.environ['NTASK'])


def f(j):
    N = 10**6
    p = 1./3.
    np.random.seed(None)
    X = 2*np.random.binomial(1,p,N)-1   # X = 1 with probability p
    s = 0                               # X =-1 with probability 1-p
    m = 0 
    for t in range(0,N):
        s = max(0,s+X[t])
        m = max(m,s)
    return m

pool = Pool(processes=K) 
print pool.map(f, xrange(40))

和表现

$ time NTASK=1 python stof.py                                                                                                
[21, 19, 17, 17, 18, 16, 17, 17, 19, 19, 17, 16, 18, 16, 19, 22, 20, 18, 16, 17, 17, 16, 18, 18, 17, 17, 19, 17, 19, 19, 16, 16, 18, 17, 18, 18, 19, 20, 16, 19]

real    0m30.367s
user    0m30.064s
sys     0m 0.420s
$ time NTASK=2 python stof.py                                                                                                
[18, 16, 16, 17, 19, 17, 21, 18, 19, 21, 17, 16, 15, 25, 19, 16, 20, 17, 15, 19, 17, 16, 20, 17, 16, 16, 16, 16, 17, 23, 17, 16, 17, 17, 19, 16, 17, 16, 19, 18]

real    0m13.428s
user    0m26.184s
sys     0m 0.348s
$ time NTASK=3 python stof.py 
[18, 17, 16, 19, 17, 18, 20, 17, 21, 16, 16, 16, 16, 17, 22, 18, 17, 15, 17, 19, 18, 16, 15, 16, 16, 24, 20, 16, 16, 16, 22, 19, 17, 18, 18, 16, 16, 19, 17, 18]

real    0m11.946s
user    0m29.424s
sys     0m 0.308s
$ time NTASK=4 python stof.py
[16, 19, 17, 16, 19, 17, 17, 16, 18, 22, 16, 21, 16, 18, 15, 16, 20, 17, 22, 17, 16, 17, 17, 20, 22, 21, 17, 17, 16, 17, 19, 16, 19, 16, 16, 18, 25, 21, 19, 18]

real    0m 8.206s
user    0m26.580s
sys     0m 0.360s