python joblib和随机游走-[CONCURRENT]-进程调度的性能

S. *_*nch 1 python parallel-processing performance parallelism-amdahl joblib

这是我的python-3.6代码,用于模拟1D反射的随机游走,使用该joblib模块K在Linux集群计算机上的工作人员之间同时生成400个实现。

但是,我注意到for的运行时K=3比for差K=1,并且for的运行时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      
Run Code Online (Sandbox Code Playgroud)

use*_*197 5

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

joblib 可以提供帮助,但会提供帮助,但是如果这样做的成本少于有效的加速,那么只有可以从分布式执行中受益的代码才可以在某些资源池中分配。

joblib仅在待分发的代码获得性能优化之后,才对t的预加载和批处理大小参数进行调整才有意义。

如下所示,对此所做的一些努力显示了实现每一个随机游走(而不是如上所述的每个项目)的纯运行时间的核心速度~ 8x[SERIAL]~ 217,000 [us]~ 1,640,000 [us]

只有在此之后,才可能进行一些与群集资源相关的优化(避免性能损失的工作),以达到绝对定义的意图,以组织上述定义的400次重复的分布式工作流程。

当且仅当以下情况才有意义:

  • 尽可能避免CPU匮乏,以及
  • 如果不必支付分布式工作计划的任何额外费用。

关于性能保存或丢失的地方,也许是一个漫长而重要的故事。
执行摘要Gene AMDAHL博士的论点

很难
获得更好的回报:

上面定义的任务的内部结构很重要[SERIAL]

  • 由于PRNG设计,随机数发生主要是一个[SERIAL]过程
  • 1E6预先计算的醉酒水手步骤的一维向量上的迭代器是纯[SERIAL]

是的,“外部”工作范围(同一过程的400次重复)可以很容易地转换为“公正” [CONCURRENT](不是真正的)[PARALLEL],即使教授和想要的大师试图告诉您)过程调度也可以,但这样做的附加成本要比线性增加的运行时间还要差,并且鉴于该[SERIAL]零件的性能未经过重新设计,因此这种努力的净效果很容易破坏最初的良好意图(上述QED,为张贴的运行时间长大,毕业于10:52,对K == 1对〜13分钟甚至少数K-s)。


简短的测试证明,使用标准的python工具后,整个任务可以在纯[SERIAL]< 1.45 [s]石头(而不是报告的12到13分钟在纯石头般古老的台式设备(某些缓存中)下运行。计算的效果是可能的,但更多的是作为偶然的副作用,而不是针对HPC集群特定性能的有意HPC驱动的代码重构):

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                               |
+-----------------------------------------------------------------+
Run Code Online (Sandbox Code Playgroud)

< 1.45 [s]
为什么呢 怎么样 ?这就是有关...的全部故事
(由于HPC的努力,它可能会在1 [s]以下进一步发展)


Gene AMDAHL博士的论证,即使是他最初的,附加的,不可知的开销形式,在他的引人注目的报告中也表明,工作的任何组成[SERIAL][PARALLEL]工作块都将受益于越来越多的处理单元使用的有限的收益。[PARALLEL]-part(又称递减收益定律,即使对于无限数量的处理器,也要达到渐近地受限的加速),[SERIAL]-part 引入的任何改进将继续以递增的方式(以纯线性方式)增加加速。让我在这里跳过不利影响(也影响加速,有些以类似的纯线性方式,但在不利的意义上是附加的开销,这些将在下面讨论)。

步骤1:
修复代码,以使有用。

给定上面的代码,根本没有随机游走。

为什么呢

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

因此,
按原样的代码会生成相当昂贵的先验已知常量列表。完全没有随机性。该死的整数取整的python舍入。 :o)

>>> [ op( np.random.binomial( 1, 1./3., 1E9 ) ) for op in ( sum, min, max, len ) ]
[333338430, 0, 1, 1000000000]
Run Code Online (Sandbox Code Playgroud)

所以这是固定的。


第2步:
了解开销(最好还包括任何隐藏的性能障碍)

任何实例化分布式进程的尝试(对于每个K -amount joblib-spreaded进程的指令,multiprocessing使用子进程调用a ,而不是线程化backend)都会使您付出一定的代价。总是...

鉴于此,
您的代码执行将获得其他附加[SERIAL]代码,这些附加代码必须运行,然后才能开始发生任何……只是理论上的…… ( 1 / n_jobs )分裂效果

仔细研究“ usefull”工作:

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
Run Code Online (Sandbox Code Playgroud)

对于这种工作包,将通过未解释的,无GIL,线程后端,multiprocessing.Pool产生了Cython的代码包以及cdef带有nogil指令的ed 来演示最佳的演示目的加速。可以预期这样的代码执行将在大约= ~ 217,000 [us]每一个纯[SERIAL]随机游走下,使用1E6steps进行,这时开始有意义的是通过一些预加载调整来利用代码执行节点池,以免它们饿死。但是,在这种简化的情况下所有过早优化警告均应有效,并且应采用适当的工程实践来获得专业级的结果。

某些工具可能会帮助您看到,通过任何高级语言语法构造器元素(或并发/并行化#pragma masquerades),您可以看到,实际组装了多少指令,以“嗅觉”这些附加处理,在最终的代码执行期间将支付的费用:

在此处输入图片说明

给定这些附加处理成本,“少量”-(薄)数量的工作“同时”在“公正”内部-同时执行(请注意,并非自动进行真正的[并行]调度),这些附加成本可能使您支付的费用比分拆付款所支付的更多

阻止者:

任何附加的通信/同步都可能进一步破坏理论上的加速代码执行流程。常见的阻止因素包括锁,不使用线程后端时避免使用GIL,信号量,套接字通讯,共享等。

对于精心设计的随机源,从这样的“设备”获得的任何抽奖也必须集中重新同步,以保持这种随机性的质量。这可能会在幕后引起更多麻烦(在具有某些经过权威认证的随机源的系统上,这是一个常见问题)。


下一步?

阅读更多关于阿姆达尔定律的详细信息最好是当代重新制定的版本,在“开销限制”模式下增加了设置和终止开销,并且考虑了处理的原子性,以实际评估实际的加速限制

下一步:测量代码的净持续时间成本,您将间接获得体内系统执行过程中安装和终止开销的附加成本。

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
Run Code Online (Sandbox Code Playgroud)

对于课堂教程,我已经使用R,Matlab,Julia和Stata中的特殊模块成功并行化了我的随机步行代码。(通过“成功”,我的意思是很清楚地知道,在相同的时间间隔内,20个工作人员所做的工作至少是1个工作人员的至少10倍。)这样的内部并行化在Python中不可行吗?

好吧,最近的评论似乎给尝试帮助并带来原因的人们带来了某些不便,这些原因说明了按原样发布代码的原因。以这种精确的方式定义处理策略不是我们的选择,是吗?

所以,
再次。
鉴于最初的决定是
使用python-3.6+ joblib.Parallel()+ joblib.delayed()工具,只需Alea Iacta Est ...

对于{R | MATLAB | 朱莉娅| }} 并不意味着它在GIL步进,较少joblib.Parallel()生成的生态系统中将以相同的方式工作。

始终会为joblib.Parallel()生成的工作支付的第一个成本是重建当前python解释器状态的整个1:1副本的成本。鉴于当前状态包含的对象实例比精简后的MCVE代码(如 MCVE脚本所展示,如@rth所展示的)更多,整个,多次复制的内存图像首先,必须在SLURM管理的群集占用空间上复制+传输+重构到所有分布式处理节点上,所有这些都会增加(非生产性)开销时间。如有疑问,请在python解释器的状态中添加一些GB大小的numpy-arrays,并将针对各个持续时间计算得出的时间戳记放入第一个和最后一个数组单元中,最后return ( m, aFatArray )。总体执行时间将增加,因为最初的1:1复制和返回路径都必须在该处来回移动大量数据(再次,有关实例化相关附加成本的详细信息,张贴在这里的许多地方,包括用于对各个附加费用进行系统基准测试的模板)。

正是出于这个原因,我们建议类型O / P确实测量有效的计算时间(基本成本/收益参数的“收益”部分),这在琐碎的实验中都很便宜,显示“远程”执行的有效计算有效载荷内的有用工作的规模,总和和实际比例(请参阅上面建议的代码修改,返回的值W[:][1]将告诉实际值在有效工作包计算期间花费的“ 有用工作 ”的“净”计算成本,一旦最终到达并被激活到相应的“远程”代码执行生态系统(此处为的形式joblib.Parallel()-生成原始python解释器的二进制全尺寸副本),而在主代码执行的开始和结束之间的时间流显示了实际的花费-此处一次性支出的时间量,即包括所有“远程”-流程说明+所有相应的工作包-分发+所有“远程”-流程终止。


对于那些
还没有花时间阅读
与随机性相关的问题的读者的最后一句话:

任何好的做法都应该避免隐藏在“共享随机性”后面的阻塞逻辑。更好地使用单独配置的PRNG源。如果有兴趣或需要可验证的PRNG稳健性,请随​​时在此处的讨论中阅读更多内容