这是我的python-3.6代码,用于模拟1D反射的随机游走,使用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
请您参考如下方法:
一种改善我对joblib的使用的方法?joblib会有所帮助,但只会有所帮助,但如果这样做的成本少于有效的提速,那么只有可以从分布式执行中受益的代码才能在某些资源池中分配。
调整joblib的预加载和批处理大小参数仅在待分发的代码获得性能优化后才有意义。
如下所示,对此所做的一些努力显示了~ 8x的核心加速,即实现了每一个随机游走[SERIAL]的纯~ 217,000 [us]运行时间(据报道,不是每个项目的~ 1,640,000 [us])以上 )。
只有在此之后,才可能有一些艰苦的工作来进行与群集资源有关的优化(避免性能破坏),以进行绝对定义的意图,以组织上述定义的400次重复的分布式工作流。
当且仅当以下情况才有意义:
尽可能避免CPU匮乏,以及
如果不必支付分布式工作计划的任何额外费用。
关于性能保存或丢失的地方可能是一个漫长而重要的故事执行摘要Gene AMDAHL博士的论点几乎没有更好的回报:
上面定义的任务的内部结构主要是[SERIAL]:
由于PRNG设计的缘故,随机数发生主要是纯[SERIAL]过程
在预先计算的醉酒水手步骤的一维向量上的1E6迭代器是纯[SERIAL]
是的,“工作”范围(同一过程的400次重复)可以很容易地转换为“ just”-[CONCURRENT](不是true- [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 |
+-----------------------------------------------------------------+
< 1.45 [s]?为什么呢怎么样 ?这就是关于...的全部故事(由于HPC的努力,它可能会在1 [s]以下变得更好)
Gene AMDAHL博士的论证,即使是他最初的,附加的,不可知的开销形式,在他引人注目的报告中也表明,
[SERIAL]和
[PARALLEL]工作块的任何组成都将主要受益于增加数量的收益为
[PARALLEL]-部分使用的处理单元数量(又称递减收益定律,即使对于无限数量的处理器,也趋向于渐近限制的加速速度),而为
[SERIAL]-部分引入的任何改进将继续增加加速(以纯线性方式)。让我在这里跳过不利影响(也影响加速,有些以类似的纯线性方式,但在不利的意义上是附加的开销,这些将在下面讨论)。
步骤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数量
joblib生成的进程,使用子进程而不是线程backend调用
multiprocessing)都会使您付出一定的代价。总是...
鉴于,
您的代码执行将获得附加的
[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
对于这种工作包,最佳的演示目的加速将通过未解释的,无GIL,线程后端,
multiprocessing.Pool生成的Cython编码的代码包,
cdef -ed和
nogil随机游走的大约
= ~ 217,000 [us]以下,并带有
[SERIAL]个步骤。以免让他们挨饿。但是,在这种简化的情况下,所有过早优化警告均应有效,并且应采用适当的工程实践来获得专业级的结果。
某些工具可能会帮助您看到,通过任何相应的高级语言语法构造器元素(或并发/并行化#pragma masquerades),您可以看到,实际组装了多少指令,以“嗅觉”这些附加处理,在最终的代码执行期间将支付的费用:
给定这些附加处理成本,“少量”-(薄)数量的工作“同时”在“公正”内部-同时执行(请注意,并非自动进行真正的[并行]调度),这些附加成本可能使您支付的费用比分拆付款所支付的更多。
阻止者:
任何附加的通信/同步都可能进一步破坏理论上的加速代码执行流程。常见的阻止因素包括锁,不使用线程后端时避免使用GIL,信号量,套接字通讯,共享等。
对于精心设计的随机源,从这样的“设备”获得的任何抽奖也必须集中重新同步,以保持这种随机性的质量。这可能会在幕后引起更多麻烦(在具有某些经过权威认证的随机源的系统上,这是一个常见问题)。
下一步?
最好阅读 more details on Amdahl's Law,当代重新制定的版本,其中以“开销限制”模式添加了设置和终止开销,并且 practical evaluation of realistic speedup limitations考虑了处理的原子性
下一步:测量代码的净持续时间成本,您将间接获得体内系统执行过程中安装和终止开销的附加成本。
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中不可行吗?
好吧,最近的评论似乎给尝试帮助并带来原因的人们带来了某些不便,这些原因说明了按原样发布代码的原因。以这种精确的方式定义处理策略不是我们的选择,是吗?
所以,
再次。
鉴于最初的决定是
使用
1E6 +
python-3.6 +
joblib.Parallel()仪器,只需Alea Iacta Est ...
对于{R | MATLAB |朱莉娅| }}并不意味着它在GIL生成的
joblib.delayed()较少的生态系统中将以相同的方式工作。
总是要为
joblib.Parallel()生成的工作支付的第一个成本是重建当前python解释器状态的整个1:1副本的成本。假设当前状态包含的对象实例比精简的MCVE代码(如对@aath的
python2.x MCVE脚本所展示的)更多,那么,整个,多次复制的内存图像首先,必须在SLURM管理的群集占用空间上复制+传输+重构到所有分布式处理节点上,所有这些都会增加(非生产性)开销时间。如有疑问,请在python解释器的状态中添加几个GB大小的numpy数组,并将针对各个持续时间计算得出的已测量时间戳记放入第一个和最后一个数组单元格中,最后放入
joblib.Parallel()中。总体执行时间将增加,因为最初的1:1复制和返回路径都必须在该处来回移动大量数据(再次,有关实例化相关附加成本的详细信息,张贴在这里的许多地方,包括用于对各个附加费用进行系统基准测试的模板)。
正是出于这个原因,我们建议类型O / P确实测量有效的计算时间(基本成本/收益参数的“收益”部分),这在平凡化的实验中都很便宜,显示“远程”执行的有效计算有效载荷内的有用工作的规模,总和和实际比例(请参阅上面建议的代码修改,返回的值使
return ( m, aFatArray )告诉他们在高效工作包计算期间花费的“全职工作”的实际“净”计算成本,一旦这些成本最终到达并激活到相应的“远程”代码执行生态系统中(这里,以原始python解释器的
W[:][1]生成的二进制全尺寸副本的形式),而在执行主代码的开始和结束之间的时间流显示了实际费用-此处一次性支出的时间量,即包括所有“远程”过程实例+所有重新预期工作包-分发+所有“远程”过程终止。
对于那些还没有花时间阅读与随机性相关的问题的读者的最后一句话:
任何好的做法都应该避免隐藏在
“共享随机性”。更好地使用单独配置的PRNG源。如果有兴趣或需要可验证的PRNG稳健性,请随时阅读有关此 in a discussion here的更多信息
