2

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

但是,我注意到 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      
4

2 回答 2

7

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

joblib 可以帮助并且将帮助,但如果这样做的成本低于有效加速,则只有可以从分布式执行中受益的代码拆分到一些资源池中。

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

如下所示,在这方面的一些努力已经显示了实现每个随机游走(而不是每个项目,如上所述)的仍然纯运行时间的核心加速。~ 8x[SERIAL]~ 217,000 [us]~ 1,640,000 [us]

只有在此之后,可能会在与集群资源相关的优化(性能破坏避免工作)方面进行一些更艰巨的工作,以实现 ab-definitio 假设的意图来组织上述定义的 400 次重复的分布式工作流。

当且仅当:

  • 尽可能避免 CPU 饥饿,以及
  • 如果不必支付分布式作业调度的任何额外附加成本。

关于性能在哪里得到保存或丢失,也许是一个漫长但重要的故事
执行摘要Gene AMDAHL 博士的论点

几乎没有

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

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

是的,“外部”-工作范围(同一过程的400次重复)可以很容易地转换为“公正”- [CONCURRENT](不是真的- [PARALLEL],即使教授和想成为大师试图告诉你)过程调度,但是这样做的附加成本比线性添加到运行时更糟,并且考虑到该[SERIAL]部分没有重新设计性能,这种努力的净效果很容易破坏最初的良好意图(上面的 QED,随着发布的运行时间的增长,从10:52K == 1, for到 ~ 13 分钟(即使是少量的K-s )。


一个简短的测试证明,在使用标准 python 工具之后,整个任务可以在(而不是报告的~ 12 - 13 分钟)下以纯[SERIAL]方式< 1.45 [s]运行,甚至在相当石器时代的古老桌面设备上(一些缓存计算效果是可能的,但更多的是意外的副作用,而不是针对 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                               |
+-----------------------------------------------------------------+

< 1.45 [s]?
为什么 ?如何 ?这就是关于......的全部故事
(由于 HPC 的努力可以使其进一步低于 1 [s])


Gene AMDAHL 博士的论点,即使在他最初的、附加开销不可知论的形式中,在他被广泛引用的报告中也表明,任何工作组合[SERIAL][PARALLEL]工作块都将从增加数量的处理单元中获得主要有限的收益用于[PARALLEL]-part (又名收益递减定律,即使是无限数量的处理器也可以实现渐近受限的加速),为 -part 引入的任何改进[SERIAL]都将继续增加加速(以纯线性方式)。让我在这里跳过不利影响(也影响加速,一些以类似的纯线性方式,但在不利的意义上 - 附加开销 - 这些将在下面讨论)。

Step No.1:
修复代码,做一些有用的东西。

鉴于上面的代码,根本没有随机游走。

为什么 ?

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

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

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

所以这是固定的。


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

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

鉴于此,
您的代码执行将获得额外[SERIAL]的 -add-on 代码,必须在任何......仍然只是理论上的...... ( 1 / n_jobs )split -效果可能开始发生之前运行。

仔细看看“有用”的工作:

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

对于这种工作包,最好的演示目的加速将通过非解释、无 GIL、线程后端、multiprocessing.Pool生成的 Cython 代码包cdefnogil指令来证明。当通过一些预加载调整来利用代码执行节点池开始有意义时,可能会期望在大约= ~ 217,000 [us]每一个纯[SERIAL]随机游走1E6下执行这种代码执行,以免让它们挨饿。然而,在这种简化的情况下,所有过早优化警告都是应有的和有效的,并且将使用适当的工程实践来获得专业级的结果。

一些工具可以帮助您查看,到汇编级别,实际添加了多少指令,由任何相应的高级语言语法构造器元素(或并发/并行化#pragma masquerades)“闻”这些附加处理 -在最终的代码执行期间将支付的费用:

在此处输入图像描述

考虑到这些附加处理成本,“刚刚”并发执行的“内部”工作量“小”(薄)(请注意,不会自动进行真正的[并行]调度),这些附加成本可能会使您支付的费用比拆分时收到的要多。

拦截器:

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

对于精心设计的随机源,从这种“设备”中提取的任何数据也必须集中重新同步,以保持这种随机性的质量。这可能会在幕后造成额外的麻烦(在具有一些权威认证的随机源的系统上的常见问题)。


下一步?

阅读有关Amdahl 定律的更多详细信息,这是当代重新制定的最佳版本,其中在 Overhead-strict 模式中添加了设置和终止开销,并且还考虑了处理的原子性,以对实际加速限制进行实际评估

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

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

对于课堂教程,我已经使用 R、Matlab、Julia 和 Stata 中的特殊模块成功地并行化了我的随机游走代码。(通过“成功”,我的意思是很明显,在同一时间间隔内,20 名工人的工作量至少是 1 名工人的 10 倍。)这种内部并行化在 Python 中不可行吗?

好吧,最后一条评论似乎给人们带来了某种不便,他们试图提供帮助并提出了理由,为什么发布的代码按原样工作,正如所观察到的那样。以这种方式定义处理策略不是我们的选择,是吗?

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

什么可能对 { R | 有用(如引用)MATLAB | 朱莉娅 | Stata }并不意味着它会在 GIL-stepped、较少joblib.Parallel()产生的生态系统中以相同的方式工作。

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

正是这就是建议那种 O/P 确实测量计算时间的有效量(基本成本/收益论点的“收益”部分)的原因,这在琐碎的实验中都很便宜,这将显示在“远程”执行的高效计算有效载荷中有用工作的规模、总和和实际比例(参考上面提出的代码修改,它返回值以便W[:][1]告诉实际一旦这些最终到达并激活到各自的“远程”代码执行生态系统(这里,在的形式joblib.Parallel()- 生成原始 python 解释器的二进制全尺寸副本),而主代码执行开始和结束之间的时间流显示实际费用 - 这里是一次性花费的时间量,即包括所有“远程” -流程实例化+所有相应的工作包-分发+所有“远程”流程终止。


最后,对于那些
没有花时间阅读
随机源相关问题的人来说:

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

于 2018-06-14T16:03:28.510 回答
3

@user3666197 写了一个关于开销的非常好的答案,其中包含很多粗体文本;)但是,我想提请您注意,当您以 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
于 2018-06-14T16:27:30.133 回答