简短答案
池的块大小算法是一种启发式方法。它为您试图塞入Pool方法中的所有可想象的问题场景提供了一个简单的解决方案。因此,无法针对任何特定情况对其进行优化。
与天真的方法相比,该算法将可迭代对象任意划分为大约四倍的块。更大的块意味着更多的开销,但增加了调度灵活性。这个答案将如何显示,平均导致更高的工人利用率,但不能保证每种情况下的总体计算时间都较短。
您可能会想:“很高兴知道,但是知道这如何帮助我解决具体的多处理问题?” 好吧,事实并非如此。更为诚实的简短答案是:“没有简短答案”,“多处理很复杂”和“取决于”。观察到的症状可能具有不同的根源,即使对于类似的情况也是如此。
该答案试图为您提供基本概念,以帮助您更清晰地了解Pool的调度黑匣子。它还尝试为您提供一些基本的工具,用于识别和避免与数据块大小相关的潜在悬崖。
目录
第一部分
- 定义
- 并行化目标
- 并行化方案
- 大于1的风险
- 普尔的块大小算法
量化算法效率
6.1模型
6.2平行时间表
6.3效率
6.3.1绝对分配效率(ADE)
6.3.2相对分配效率(RDE)
第二部分
- 朴素vs游泳池的块大小算法
- 现实检查
- 结论
首先必须澄清一些重要术语。
1.定义
块
此处的块是iterable
池方法调用中指定的-argument的份额。该答案的主题是如何计算块大小以及它可以产生什么影响。
任务
下图显示了工作进程在数据上的任务物理表示。
该图显示了对的示例调用pool.map()
,该调用沿代码行显示,取自该multiprocessing.pool.worker
函数,其中从inqueue
get读取的任务被解压缩。worker
是MainThread
pool-worker流程中基本的主要功能。该func
池中法规定-argument将只匹配func
-variable内的worker
单呼的方法,如功能全 apply_async
和imap
同chunksize=1
。对于带有chunksize
-parameter的其余池方法,处理功能func
将是映射器功能(mapstar
或starmapstar
)。此函数将用户指定的func
-parameter映射到可迭代的传输块的每个元素上(->“ map-tasks”)。这花费的时间定义了任务也作为工作单位。
塔塞尔
虽然“任务”一词在整个块的整个处理过程中的用法与内部的代码相匹配multiprocessing.pool
,但没有指示应如何以块的一个元素作为参数对用户指定的单个调用func
提及。为避免命名冲突(maxtasksperchild
Pool的__init__
-method的-parameter )引起的混乱,此答案将任务中的单个工作单元称为taskel。
甲taskel(从任务+ EL EMENT)是一种内工作的最小单位的任务。它是func
使用Pool
-method的-parameter指定的函数的单次执行,该函数使用从传输的chunk的单个元素获得的参数调用。一个任务由taskels。chunksize
并行化开销(PO)
PO由Python内部的开销和进程间通信(IPC)的开销组成。Python中每个任务的开销包含打包和解压缩任务及其结果所需的代码。IPC开销具有必要的线程同步以及不同地址空间之间的数据复制(需要两个复制步骤:父->队列->子)。IPC开销的大小取决于OS,硬件和数据大小,这使得很难概括影响。
2.并行化目标
使用多处理时,我们的总体目标(显然)是最大程度地减少所有任务的总处理时间。为了达到这个总体目标,我们的技术目标需要优化硬件资源的利用率。
实现技术目标的一些重要子目标是:
- 最小化并行化开销(最著名但并非唯一的IPC)
- 所有CPU核心的高利用率
- 限制内存使用量,以防止OS进行过多的分页(乱码)
首先,任务必须在计算上足够繁重(密集),以赚回我们必须为并行化支付的订单。PO的相关性随每个任务的绝对计算时间的增加而降低。或者,换句话说,每个任务的绝对计算时间越长,对降低PO的需求就越不相关。如果您的计算将花费每个任务小时数,则IPC开销相比而言可以忽略不计。这里的主要考虑是在分配所有任务之后防止空闲的工作进程。保持所有内核都处于加载状态,这意味着我们将尽可能并行化。
3.并行化方案
哪些因素决定了multiprocessing.Pool.map()之类的方法的最佳块大小参数
问题的主要因素是单个任务之间的计算时间可能会有所不同。顾名思义,最佳块大小的选择取决于每个任务的计算时间的变异系数(CV)。
从这种变化的程度来看,在规模上有两种极端情况:
- 所有任务都需要完全相同的计算时间。
- 一个Taskel可能需要几秒钟或几天才能完成。
为了更好地记忆,我将这些场景称为:
- 密集场景
- 广泛的场景
密集场景
在密集场景中,希望一次分发所有任务组,以将必要的IPC和上下文切换保持在最低水平。这意味着我们只想创建尽可能多的块,以及尽可能多的工作进程。如上所述,PO的权重随每个任务的计算时间缩短而增加。
为了获得最大吞吐量,我们还希望所有工作进程都忙,直到处理完所有任务(没有空闲的工作进程)。为了这个目标,分布式块应该大小相等或相近。
广泛的场景
宽场景的主要示例是优化问题,结果要么快速收敛,要么计算可能要花费数小时甚至数天。通常,在这种情况下,无法预料任务将包含“轻任务组”和“重任务组”的混合形式,因此不建议一次在任务批中分配太多任务组。一次分配尽可能少的任务,意味着增加调度灵活性。这是达到我们所有内核的高利用率子目标所必需的。
如果Pool
默认情况下将针对密集场景完全优化方法,则它们将为靠近广域场景的每个问题逐步创建次最佳时序。
4.大量风险> 1
考虑一下广泛场景可迭代的简化伪代码示例,我们希望将其传递给池方法:
good_luck_iterable = [60, 60, 86400, 60, 86400, 60, 60, 84600]
代替实际值,我们假装以秒为单位查看所需的计算时间,为简单起见,仅计算1分钟或1天。我们假设该池具有四个工作进程(在四个内核上),chunksize
并设置为2
。因为将保留订单,所以发送给工人的块将是:
[(60, 60), (86400, 60), (86400, 60), (60, 84600)]
因为我们有足够的工作人员,并且计算时间足够长,所以可以说,每个工作人员进程首先都会获得一个要处理的块。(对于快速完成的任务,不一定是这种情况)。进一步我们可以说,整个处理将花费大约86400 + 60秒,因为在这种人工场景中,这是一个块的最高总计算时间,并且我们仅分配块一次。
现在考虑这个可迭代的对象,与之前的可迭代对象相比,它只有一个元素切换其位置:
bad_luck_iterable = [60, 60, 86400, 86400, 60, 60, 60, 84600]
...以及相应的块:
[(60, 60), (86400, 86400), (60, 60), (60, 84600)]
不幸的是,我们的可迭代排序几乎使我们的总处理时间翻了一番(86400 + 86400)!收到恶性(86400,86400)大块的工人正在阻止任务中的第二个繁重任务,无法分发给已经完成(60,60)大块的空转工人之一。如果我们着手,我们显然不会冒这样令人不快的结果的风险chunksize=1
。
这是更大块的风险。有了更大的块大小,我们就可以牺牲较少的开销来交换调度灵活性,并且在上述情况下,这是很糟糕的。
我们将在第6章中看到。量化算法效率时,较大的块大小也会导致密集场景的次优结果。
5. Pool的块大小算法
在下面的源代码中,您会找到该算法的略微修改版本。如您所见,我切除了下部并将其包装到一个用于在chunksize
外部计算参数的函数中。我还替换4
了一个factor
参数并将len()
呼叫外包。
def calc_chunksize(n_workers, len_iterable, factor=4):
"""Calculate chunksize argument for Pool-methods.
Resembles source-code within `multiprocessing.pool.Pool._map_async`.
"""
chunksize, extra = divmod(len_iterable, n_workers * factor)
if extra:
chunksize += 1
return chunksize
为确保所有人都在同一页面上,请divmod
执行以下操作:
divmod(x, y)
是返回的内置函数(x//y, x%y)
。
x // y
是底数除法,从中返回向下的商x / y
,
x % y
而是模运算,从中返回余数x / y
。因此,例如divmod(10, 3)
return (3, 1)
。
现在,当你看chunksize, extra = divmod(len_iterable, n_workers * 4)
,你会发现n_workers
这里是除数y
在x / y
和乘法4
,而无需进一步调整通过if extra: chunksize +=1
以后,导致初始CHUNKSIZE至少四倍小(len_iterable >= n_workers * 4
),那将是比其他。
要查看乘以4
对中间chunksize结果的影响,请考虑以下函数:
def compare_chunksizes(len_iterable, n_workers=4):
"""Calculate naive chunksize, Pool's stage-1 chunksize and the chunksize
for Pool's complete algorithm. Return chunksizes and the real factors by
which naive chunksizes are bigger.
"""
cs_naive = len_iterable // n_workers or 1
cs_pool1 = len_iterable // (n_workers * 4) or 1
cs_pool2 = calc_chunksize(n_workers, len_iterable)
real_factor_pool1 = cs_naive / cs_pool1
real_factor_pool2 = cs_naive / cs_pool2
return cs_naive, cs_pool1, cs_pool2, real_factor_pool1, real_factor_pool2
上面的函数计算cs_naive
Pool的chunksize-algorithm(cs_pool1
)的天真chunksize()和第一步块大小,以及完整的Pool-algorithm(cs_pool2
)的chunksize 。此外,它还计算了实际因子 rf_pool1 = cs_naive / cs_pool1
和rf_pool2 = cs_naive / cs_pool2
,它们告诉我们天真的计算出的块大小比Pool的内部版本大多少倍。
在下面,您可以看到使用此函数的输出创建的两个图形。左图仅显示n_workers=4
直到可迭代长度为止的块大小500
。右图显示的值rf_pool1
。对于可迭代的长度16
,实际因数变为>=4
,len_iterable >= n_workers * 4
并且它的最大值是7
可迭代的长度28-31
。这4
与算法收敛到更长的可迭代项的原始因子有很大的偏差。这里的“较长”是相对的,取决于指定工人的数量。
记住CHUNKSIZEcs_pool1
仍然缺乏extra
从其余端口-divmod
包含在cs_pool2
从完整的算法。
该算法继续:
if extra:
chunksize += 1
现在例,有是一个余数(一个extra
从divmod操作),通过增加1 CHUNKSIZE显然不能为每一个任务的工作了。毕竟,如果可以的话,就不会有剩余。
你怎么可以在下面的图中看到的,“额外处理”具有这样的效果,即真正的因素对于rf_pool2
现在走向收敛4
从下方 4
和偏差是有点顺畅。标准偏差n_workers=4
和len_iterable=500
从下降0.5233
了rf_pool1
到0.4115
了rf_pool2
。
最终,增加chunksize
1的效果是,最后传输的任务的大小仅为len_iterable % chunksize or chunksize
。
然而,对于生成的块数(),可以观察到额外处理的效果越有趣,我们将如何在以后看到,因此也就更加有意义。对于足够长的可迭代对象,Pool完成的chunksize-algorithm(在下图中)将使chunk的数量稳定在。相反,幼稚算法(经过最初的打p)在迭代器和迭代器的长度增长时保持交替。n_chunks
n_pool2
n_chunks == n_workers * 4
n_chunks == n_workers
n_chunks == n_workers + 1
在下面,您将找到两个针对Pool的增强信息功能和朴素的chunksize-algorithm。下一章将需要这些功能的输出。
from collections import namedtuple
Chunkinfo = namedtuple(
'Chunkinfo', ['n_workers', 'len_iterable', 'n_chunks',
'chunksize', 'last_chunk']
)
def calc_chunksize_info(n_workers, len_iterable, factor=4):
"""Calculate chunksize numbers."""
chunksize, extra = divmod(len_iterable, n_workers * factor)
if extra:
chunksize += 1
n_chunks = len_iterable // chunksize + (len_iterable % chunksize > 0)
last_chunk = len_iterable % chunksize or chunksize
return Chunkinfo(
n_workers, len_iterable, n_chunks, chunksize, last_chunk
)
不要被可能意想不到的外观所迷惑calc_naive_chunksize_info
。在extra
从divmod
没有用于计算CHUNKSIZE。
def calc_naive_chunksize_info(n_workers, len_iterable):
"""Calculate naive chunksize numbers."""
chunksize, extra = divmod(len_iterable, n_workers)
if chunksize == 0:
chunksize = 1
n_chunks = extra
last_chunk = chunksize
else:
n_chunks = len_iterable // chunksize + (len_iterable % chunksize > 0)
last_chunk = len_iterable % chunksize or chunksize
return Chunkinfo(
n_workers, len_iterable, n_chunks, chunksize, last_chunk
)
6.量化算法效率
现在,在我们看到Pool
的chunksize-algorithm的输出与朴素算法的输出相比看起来有何不同之后...
- 如何判断Pool的方法是否确实有所改善?
- 那到底是什么呢?
如在前面的章节中,对于较长的iterables(一个更大的数taskels的)中所示,游泳池的CHUNKSIZE算法大致划分成可迭代四倍多比幼稚方法块。较小的块意味着更多的任务,而更多的任务则意味着更多的并行化开销(PO),这必须权衡成本以提高调度灵活性的好处(请参阅“块大小的风险> 1”)。
由于相当明显的原因,Pool的基本块大小算法无法为我们权衡针对PO的调度灵活性。IPC开销取决于操作系统,硬件和数据大小。该算法无法知道我们在什么硬件上运行代码,也无法知道Taskel将花费多长时间来完成。这是一种启发式功能,可为所有可能的情况提供基本功能。这意味着不能针对任何特定情况对其进行优化。如前所述,随着每个任务的计算时间增加(负相关),PO也变得越来越不受关注。
当您回顾第2章中的并行化目标时,有一个要点是:
前面提到的一些东西,Pool的chunksize-algorithm可以尝试改善的是最小化空闲的工作进程以及cpu-core的利用率。
multiprocessing.Pool
人们想知道未使用的内核/在您希望所有工作进程都忙的情况下使工作进程闲置,从而对SO提出了一个重复的问题。尽管这可能有很多原因,但在工人数不等于数的除数的情况下,即使使用密集场景(每个任务的计算时间相等),我们经常可以观察到在计算结束时使工人进程空转。块()。n_chunks % n_workers > 0
现在的问题是:
在实践中,我们如何才能将对块大小的理解转化为某种东西,从而使我们能够解释观察到的工人利用率,甚至在这方面比较不同算法的效率?
6.1模型
为了在这里获得更深入的见识,我们需要一种并行计算的抽象形式,该形式可以将过于复杂的现实简化到可管理的复杂程度,同时在定义的边界内保留重要性。这样的抽象称为模型。如果要收集数据,则这种“并行化模型”(PM)的实现会像实际计算一样生成工人映射的元数据(时间戳)。由模型生成的元数据允许在一定约束下预测并行计算的指标。
此处定义的PM中的两个子模型之一是分布模型(DM)。的DM解释工作单元(taskels)如何原子被分布在平行工人和时间,当没有其他因素比相应CHUNKSIZE算法,工人的数量,输入可迭代(数taskels的)和它们的计算的持续时间被认为是。这意味着不包括任何形式的间接费用。
为了获得完整的PM,DM用开销模型(OM)扩展,该模型表示各种形式的并行开销(PO)。这种模型需要针对每个节点分别进行校准(硬件,操作系统相关性)。如何开销多种形式在中表示OM是开放等多个OMs的具有不同程度的复杂性可能存在。实施的OM需要达到哪种精度水平,取决于特定计算的PO总权重。较短的任务舵会导致PO的重量增加,进而需要更精确的OM如果我们试图预测 并行化效率(PE)。
6.2平行时间表(PS)
的并行调度是并行计算,其中x轴表示时间,y轴的二维表示代表平行工人的池。工人的数量和总的计算时间标志着矩形的延伸,在该矩形中绘制了较小的矩形。这些较小的矩形代表工作的原子单位(任务组)。
在下面,您可以找到PS的可视化图像,该PS绘制了Dense Scenario中Pool的chunksize-algorithm的DM中的数据。
- X轴被划分为相等的时间单位,其中每个单位代表Taskel所需的计算时间。
- y轴分为池使用的工作进程数。
- 此处的taskel显示为最小的青色矩形,放在匿名工作进程的时间轴(日程表)中。
- 一项任务是在工作人员时间轴中以相同色调连续突出显示的一个或多个任务。
- 空转时间单位用红色的瓷砖表示。
- 并行计划分为几部分。最后一部分是尾部。
下图显示了组成部分的名称。
在包括OM的完整PM中,空转份额不仅限于尾部,还包括任务之间甚至任务板之间的空间。
6.3效率
上面介绍的模型可以量化工人的利用率。我们可以区分:
- 分配效率(DE) -借助DM(或针对Dense Scenario的简化方法)进行计算。
- 并行效率(PE) -借助校准的PM(预测)进行计算或根据实际计算的元数据进行计算。
重要的是要注意,对于给定的并行化问题,计算出的效率不会自动与更快的整体计算相关联。在这种情况下,对工人的利用只能区分已经开始但尚未完成的任务组的工人和没有这样的“开放”任务组的工人。这意味着,有可能空转时一taskel的时间跨度没有注册。
上面提到的所有效率基本上都是通过计算“繁忙共享/并行计划”划分的商来获得的。DE和PE之间的区别在于,忙于共享在开销扩展的PM的整体“并行计划”中占较小的比例。
该答案将仅进一步讨论一种用于计算密集场景的DE的简单方法。这足以比较不同的块大小算法,因为...
- ... DM是PM的一部分,它会随着所采用的不同块大小算法而变化。
- ...每个任务的计算持续时间相等的密集场景描述了一个“稳定状态”,对于这些状态,这些时间跨度不属于等式。任何其他情况都将导致随机结果,因为任务组的排序很重要。
6.3.1绝对分配效率(ADE)
一般而言,可以通过将忙碌份额除以并行计划的全部潜力来计算基本效率:
绝对分配效率(ADE) =繁忙共享/并行计划
对于Dense Scenario,简化的计算代码如下所示:
def calc_ade(n_workers, len_iterable, n_chunks, chunksize, last_chunk):
"""Calculate Absolute Distribution Efficiency (ADE).
`len_iterable` is not used, but contained to keep a consistent signature
with `calc_rde`.
"""
if n_workers == 1:
return 1
potential = (
((n_chunks // n_workers + (n_chunks % n_workers > 1)) * chunksize)
+ (n_chunks % n_workers == 1) * last_chunk
) * n_workers
n_full_chunks = n_chunks - (chunksize > last_chunk)
taskels_in_regular_chunks = n_full_chunks * chunksize
real = taskels_in_regular_chunks + (chunksize > last_chunk) * last_chunk
ade = real / potential
return ade
如果没有空转分享,忙分享会等,以并行计划,因此我们得到一个ADE的100%。在我们的简化模型中,这是一个场景,其中所有可用进程将在处理所有任务所需的整个时间中都处于繁忙状态。换句话说,整个工作实际上可以并行化到100%。
但是,为什么在这里我继续将PE称为绝对 PE?
为了理解这一点,我们必须考虑chunksize(cs)的可能情况,以确保最大的调度灵活性(也可以有Highlanders的数量。巧合?):
__________________________________ 〜一个〜 __________________________________
例如,如果我们有4个工作进程和37个Taskel,则即使没有,也将有空闲工人,甚至是chunksize=1
,因为n_workers=4
不是37的除数。除以37/4的余数是1。剩下的单个Taskel必须是由唯一的工人处理,其余三个空闲。
同样,仍然会有一个闲置的工人和39个任务小组,您将如何看到下图。
当你比较上并行计划用于chunksize=1
与以下版本chunksize=3
,你会发现,上并行调度较小,在时间轴X轴短。现在应该变得明显,如何做大chunksizes竟然也可以导致增加整体的计算时间,即使是密集方案。
但是,为什么不仅仅使用x轴的长度进行效率计算呢?
因为此模型中不包含开销。两种块大小都将有所不同,因此x轴并不是真正可直接比较的。开销仍然可能导致更长的总计算时间,如下面图例2所示。
6.3.2相对分配效率(RDE)
该ADE值不包含的信息,如果一个更好的taskels的分布是可能的CHUNKSIZE设置为1,更好的还在这儿意味着较小的怠速分享。
为了获得调整为最大可能DE的DE值,我们必须将考虑的ADE除以我们获得的ADE。chunksize=1
相对分配效率(RDE) = ADE_cs_x / ADE_cs_1
这是在代码中的外观:
def calc_rde(n_workers, len_iterable, n_chunks, chunksize, last_chunk):
"""Calculate Relative Distribution Efficiency (RDE)."""
ade_cs1 = calc_ade(
n_workers, len_iterable, n_chunks=len_iterable,
chunksize=1, last_chunk=1
)
ade = calc_ade(n_workers, len_iterable, n_chunks, chunksize, last_chunk)
rde = ade / ade_cs1
return rde
RDE,在这里如何定义,本质上是一个关于并行调度表尾部的故事。RDE受尾部中包含的最大有效块大小影响。(此尾可以是x轴长度的chunksize
或last_chunk
)。这具有这样的结果,即RDE自然收敛到100%(偶数)为各种各样的“尾看起来”像示于下图。
低RDE ...
- 是优化潜力的有力暗示。
- 对于更长的可迭代对象,自然会变得不太可能,因为整体“并行计划”的相对尾部会缩小。
请在此处找到此答案的第二部分。
4
是任意的,块大小的整个计算是一种启发式方法。相关因素是您的实际处理时间可能会变化多少。在这里还需要更多一些,直到我有时间回答时(如果仍然需要)。