哪些因素决定了chunksize
之类的方法的最佳multiprocessing.Pool.map()
参数? .map()
方法似乎对其默认的块大小使用了任意启发式(如下所述);是什么促使了这一选择,是否有基于某些特定情况/设置的更周到的方法?
示例-说我是:
iterable
传递到具有约1500万个元素的.map()
; multiprocessing.Pool()
中使用默认的processes = os.cpu_count()
。我天真的想法是给24名工人中的每人一个相等大小的块,即15_000_000 / 24
或625,000。大块应该在充分利用所有工人的同时减少营业额/间接费用。但这似乎没有为每个工人提供大批量生产的潜在弊端。这是一张不完整的图片,我想念什么?
我的问题的一部分源于if chunksize=None
的默认逻辑:.map()
和.starmap()
都调用.map_async()
,如下所示:
def _map_async(self, func, iterable, mapper, chunksize=None, callback=None,
error_callback=None):
# ... (materialize `iterable` to list if it's an iterator)
if chunksize is None:
chunksize, extra = divmod(len(iterable), len(self._pool) * 4) # ????
if extra:
chunksize += 1
if len(iterable) == 0:
chunksize = 0
divmod(len(iterable), len(self._pool) * 4)
背后的逻辑是什么?这意味着块大小将更接近15_000_000 / (24 * 4) == 156_250
。将len(self._pool)
乘以4的意图是什么?
这使所得的块大小比我上面的“幼稚逻辑”小4个 ,其中包括将可迭代的长度除以pool._pool
中的工人数量。
最后,.imap()
上的Python文档中也有snippet,这进一步激发了我的好奇心:
chunksize
参数与map()
使用的参数相同 方法。对于chunksize
使用大值的可迭代对象,可以使用很长时间 与使用默认值1相比,完成任务的速度快得多。
有用的相关答案,但有点太高了:Python multiprocessing: why are large chunksizes slower?。
答案 0 :(得分:18)
关于此答案
此答案是已接受的答案above的第二部分。
在进入细节之前,请考虑以下两个gif。对于一系列不同的iterable
长度,它们显示了两个比较的算法如何对传递的iterable
进行分块(届时将是一个序列),以及如何分配结果任务。工作人员的顺序是随机的,实际上,每个工作人员的分布式任务数量可能与此图像不同,对于轻型任务组和/或广泛场景中的任务组来说。如前所述,此处也不包括开销。但是,对于密集场景中传输数据大小可忽略的繁重任务,实际计算会得出非常相似的图景。
如“ 5。Pool的Chunksize算法”一章中所示,使用Pool的chunksize-algorithm,对于足够大的可迭代对象,块的数量将稳定在n_chunks == n_workers * 4
,同时保持在n_chunks == n_workers
和n_chunks == n_workers + 1
采用幼稚的方法。对于简单的算法,因为:n_chunks % n_workers == 1
是True
的{{1}},所以将创建一个新节,其中只雇用一个工人。
朴素的块大小算法:
您可能会认为您是在相同数量的工作人员中创建任务的,但这仅适用于
n_chunks == n_workers + 1
没有剩余的情况。如果有剩余的 ,则将有一个新部分,其中只有一个工作人员可以完成一个任务。届时,您的计算将不再是并行的。
在下面,您将看到类似于第5章中显示的图形,但显示的是节数而不是块数。对于Pool的完整块大小算法(len_iterable / n_workers
),n_pool2
将稳定在臭名昭著的硬编码因子n_sections
上。对于朴素的算法,4
将在一到两个之间交替出现。
对于Pool的块大小算法,通过前面提到的额外处理将其稳定在n_sections
上,可防止在此处创建新部分并保持空闲共享 >仅限一名工人提供足够长的可迭代项。不仅如此,该算法还会使空闲共享的相对大小不断缩小,从而使RDE值收敛到100%。
例如n_chunks = n_workers * 4
的“足够长”是n_workers=4
。对于等于或大于该值的可迭代项,空闲共享将仅限于一个工作者,该特征最初是由于首先在块大小算法中进行len_iterable=210
乘法而丢失的。< / p>
幼稚的chunksize-算法也收敛到100%,但它的速度较慢。会聚效果完全取决于以下事实:在有两个部分的情况下,尾巴的相对部分会收缩。只有一名受雇工人的尾巴仅限于x轴长度4
,这可能是n_workers - 1
的最大余数。
天真和Pool的chunksize-algorithm的实际RDE值有何不同?
在下面,您将找到两个热图,其中显示了所有可迭代长度(最多5000),所有2到100的工人的 RDE 值。 色阶从0.5到1(50%-100%)。您会在左侧的热图中注意到更多朴素算法的暗区域(较低的RDE值)。相比之下,Pool右边的chunksize-algorithm则绘制出更多阳光照耀的画面。
左下角暗角与右上角亮角的对角线梯度再次显示了依赖于工人数量的“长迭代”。
每种算法有多糟糕?
使用Pool的chunksize-algorithm, RDE 值为81.25%,这是上面指定的worker范围和可迭代长度的最小值:
使用朴素的chunksize算法,情况可能会变得更糟。计算出的最低 RDE 为50.72%。在这种情况下,几乎只有一个工人在运行一半的计算时间!因此,请注意Knights Landing的骄傲所有者。 ;)
在前面的章节中,我们考虑了纯数学分布问题的简化模型,去除了细节问题,这些细节首先使多重处理成为棘手的话题。为了更好地理解单独的分布模型(DM) 可以在多大程度上有助于解释实际观察到的工人利用率,我们现在来看看由 real 计算得出的并行调度。
以下各图均处理了一个简单的,cpu绑定的伪函数的并行执行,该伪函数使用各种参数调用,因此我们可以观察绘制的并行调度如何随输入值的变化而变化。此函数中的“工作”仅包括范围对象上的迭代。因为我们传入了大量数字,这已经足以使核心繁忙。可选地,该函数需要一些taskel唯一的额外len_iterable / n_workers
,而这些data
会原样返回。由于每个Taskel都包含完全相同的工作量,因此我们仍在这里处理密集场景。
该函数由包装器装饰,该包装器以ns分辨率(Python 3.7+)获取时间戳。时间戳用于计算任务组的时间跨度,因此可以绘制经验性的并行计划。
@stamp_taskel
def busy_foo(i, it, data=None):
"""Dummy function for CPU-bound work."""
for _ in range(int(it)):
pass
return i, data
def stamp_taskel(func):
"""Decorator for taking timestamps on start and end of decorated
function execution.
"""
@wraps(func)
def wrapper(*args, **kwargs):
start_time = time_ns()
result = func(*args, **kwargs)
end_time = time_ns()
return (current_process().name, (start_time, end_time)), result
return wrapper
Pool的starmap方法也以仅对starmap调用本身计时的方式进行修饰。此调用的“开始”和“结束”确定所产生的并行计划的x轴上的最小值和最大值。
我们将在具有以下规格的机器上观察在四个工作进程上对40个任务组的计算: Python 3.7.1,Ubuntu 18.04.2,Intel®Core™i7-2600K CPU @ 3.40GHz×8
将要改变的输入值是for循环中的迭代次数 (30k,30M,600M)和其他发送数据大小(每个任务,numpy-ndarray:0 MiB,50 MiB)。
...
N_WORKERS = 4
LEN_ITERABLE = 40
ITERATIONS = 30e3 # 30e6, 600e6
DATA_MiB = 0 # 50
iterable = [
# extra created data per taskel
(i, ITERATIONS, np.arange(int(DATA_MiB * 2**20 / 8))) # taskel args
for i in range(LEN_ITERABLE)
]
with Pool(N_WORKERS) as pool:
results = pool.starmap(busy_foo, iterable)
以下所示的运行是经过手工挑选的,具有相同的块顺序,因此与“分配模型”中的“并行计划”相比,您可以更好地发现差异,但是请不要忘记工人获得任务的顺序不是确定性的。
重申一下,分布模型“预测”了并行调度,就像我们在6.2章中已经看到的那样:
我们在这里的第一次跑步很短,而任务车非常“轻”。整个pool.starmap()
通话总共只花了14.5毫秒。
您会注意到,与 DM 相反,空转不仅仅局限于尾部,而是在任务之间甚至任务板之间进行。那是因为我们这里的实际日程安排自然包括各种开销。在这里空转意味着任务栏的所有外部。 之前未提到的任务包可能没有 real 闲置 。
您还可以看到,并非所有工作人员都同时完成任务。这是由于所有工作人员都在一个共享的inqueue
上受够了,一次只能读取一个工作人员。 outqueue
同样适用。一旦传输了非边际大小的数据,这可能会导致更大的麻烦,我们稍后会看到。
此外,您可以看到,尽管每个任务组都包含相同的工作量,但任务组的实际测量时间差异很大。分配给worker-3和worker-4的任务需要比前两个worker处理的任务更多的时间。对于本次运行,我怀疑是由于turbo boost当时在worker-3 / 4的内核上不再可用,因此他们以较低的时钟速率处理任务。
整个计算非常轻巧,以至于硬件或操作系统引入的混乱因素会大大扭曲 PS 。该计算是“随风而逝”, DM -预测即使在理论上合适的情况下也没有什么意义。
for循环中的迭代次数从30,000增加到3,000万,产生了一个真正的并行调度,与 DM 提供的数据预测的调度非常接近,欢呼!现在,每个Taskel的计算量都足够大,足以在开始时和中间之间将闲置部分边缘化,从而仅显示 DM 预测的大闲置份额。
保留30M迭代,但另外,每个Taskel来回发送50 MiB会使图像再次偏斜。在这里,排队效果很明显。工人4需要等待的时间比工人1更长。现在想象一下有70个工人的时间表!
如果任务组在计算上非常轻巧,但提供了大量的数据作为有效负载,则单个共享队列的瓶颈可能会阻止向池中添加更多工作人员的任何其他好处,即使它们由物理核心支持。在这种情况下,Worker-1可以完成其第一个任务,甚至在Worker-40完成其第一个任务之前就等待一个新任务。
现在应该变得很清楚,为什么Pool
中的计算时间并不总是随着工作者的数量线性减少。沿 发送相对大量的数据会导致出现以下情况:大部分时间都花在等待将数据复制到工作人员的地址空间上,并且一次只能喂养一个工作人员。
在这里,我们再次发送了50 MiB,但是迭代次数从30M增加到600M,这使总计算时间从10 s增加到152 s。再次绘制的并行计划 与预测的计划非常接近,通过数据复制产生的开销被边缘化了。
讨论的乘以4
的乘法增加了调度灵活性,但也利用了taskel分布的不均匀性。如果没有这种乘法,则即使对于短暂的可迭代对象(对于具有密集场景的 DM 而言),闲置份额也将仅限于单个工人。 Pool的chunksize算法需要输入可迭代项具有一定大小才能恢复该特征。
正如这个答案所希望显示的那样,与天真的方法相比,Pool的chunksize-algorithm平均导致更好的核心利用率,至少对于一般情况而言,并且不考虑长开销。朴素的算法在这里的分布效率(DE)可以低至约51%,而Pool的块大小算法的分布效率低至约81%。 DE 但是不像IPC那样包含并行化开销(PO)。第8章已经表明, DE 在密集化方案中仍具有很好的预测能力,而开销却被边缘化了。
尽管Pool的chunksize-algorithm与朴素的方法相比具有更高的 DE ,但它不能为每个输入星座图提供最佳的taskel分布。静态分块算法无法优化(包括开销)并行效率(PE),没有内在的理由为什么它不能总是提供100%的相对分布效率(RDE),这意味着,与chunksize=1
相同的 DE 。一个简单的chunksize-algorithm仅由基本数学组成,并且可以以任何方式自由“切片”。
与Pool实施“等分块”算法不同,“均分块”算法将为每个len_iterable
/ {{ 1}}组合。偶数大小的算法在Pool的源代码中实现起来会稍微复杂一些,但是可以通过将任务打包在外部来在现有算法的基础上进行调制(如果在此处将Q / A放在该怎么做)。
答案 1 :(得分:5)
我认为您所缺少的部分是您的幼稚估计假设每个工作单元花费相同的时间,在这种情况下,您的策略将是最好的。但是,如果某些作业比其他作业更快完成,那么某些内核可能会变得空闲,等待缓慢的作业完成。
因此,通过将这些块分解成4倍以上的块,然后,如果一个块提前完成,则该内核可以启动下一个块(而其他内核继续运行其较慢的块)。
我不知道他们为什么要精确选择因子4,但这将是在最小化映射代码的开销(需要尽可能大的块)与平衡花费不同时间量的块(这需要花费4个时间)之间的权衡可能的最小块)。