Python多处理:了解“块大小”背后的逻辑


84

哪些因素决定了此类chunksize方法的最佳论点multiprocessing.Pool.map()?该.map()方法似乎对其默认的块大小使用了任意启发式(如下所述);是什么激发了这种选择,并且基于某些特定的情况/设置,是否有一种更周到的方法?

示例-说我是:

  • 传递iterable给的.map()元素大约有1500万;
  • 24个核的机器上工作,使用默认processes = os.cpu_count()multiprocessing.Pool()

我天真的想法是给24名工人中的每人一个相等大小的块,即15_000_000 / 24625,000。大块应该在充分利用所有工人的同时减少营业额/间接费用。但这似乎没有为每个工人提供大批量生产的潜在弊端。这是一张不完整的图片,我想念什么?


我的问题的一部分源于if的默认逻辑chunksize=None:both.map().starmap()call .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)什么?这意味着chunksize将更接近15_000_000 / (24 * 4) == 156_250。乘以len(self._pool)4的意图是什么?

这使得生成的块大小比我上面的“幼稚逻辑”4倍,其中包括将可迭代的长度除以中的工作者数pool._pool

最后,Python文档中的以下代码片段.imap()也进一步激发了我的好奇心:

chunksize参数与该map() 方法使用的参数相同。对于很长的iterables使用一个较大的值chunksize可以使工作完成多少不是使用默认值1速度更快。


相关的回答很有帮助,但有点太高级了:Python多重处理:为什么大的块大小比较慢?


1
4是任意的,块大小的整个计算是一种启发式方法。相关因素是您的实际处理时间可能会变化多少。在这里还需要更多一些,直到我有时间回答时(如果仍然需要)。
Darkonaut

你检查过这个问题了吗?
安德鲁·纳吉

1
谢谢@AndrewNaguib,我实际上并没有偶然发现过那个东西
Brad Solomon

1
只是让您知道:我没有忘记这个问题。实际上,自您提出要求的那一天起,我就在研究圣经方面的规范答案(大量有用的代码片段和精美的图形)。赏金计划仍要过早1-2周才能完成,但我相信我将能够在截止日期之前足够接近目标。
Darkonaut

@BradSolomon不客气:)。它能回答您的问题吗?
安德鲁·纳吉布

Answers:


193

简短答案

池的块大小算法是一种启发式方法。它为您试图塞入Pool方法中的所有可想象的问题场景提供了一个简单的解决方案。因此,无法针对任何特定情况对其进行优化。

与天真的方法相比,该算法将可迭代对象任意划分为大约四倍的块。更大的块意味着更多的开销,但增加了调度灵活性。这个答案将如何显示,平均导致更高的工人利用率,但不能保证每种情况下的总体计算时间都较短。

您可能会想:“很高兴知道,但是知道这如何帮助我解决具体的多处理问题?” 好吧,事实并非如此。更为诚实的简短答案是:“没有简短答案”,“多处理很复杂”和“取决于”。观察到的症状可能具有不同的根源,即使对于类似的情况也是如此。

该答案试图为您提供基本概念,以帮助您更清晰地了解Pool的调度黑匣子。它还尝试为您提供一些基本的工具,用于识别和避免与数据块大小相关的潜在悬崖。


目录

第一部分

  1. 定义
  2. 并行化目标
  3. 并行化方案
  4. 大于1的风险
  5. 普尔的块大小算法
  6. 量化算法效率

    6.1模型

    6.2平行时间表

    6.3效率

    6.3.1绝对分配效率(ADE)

    6.3.2相对分配效率(RDE)

第二部分

  1. 朴素vs游泳池的块大小算法
  2. 现实检查
  3. 结论

首先必须澄清一些重要术语。


1.定义


此处的块是iterable池方法调用中指定的-argument的份额。该答案的主题是如何计算块大小以及它可以产生什么影响。


任务

下图显示了工作进程在数据上的任务物理表示。

图0

该图显示了对的示例调用pool.map(),该调用沿代码行显示,取自该multiprocessing.pool.worker函数,其中从inqueueget读取的任务被解压缩。workerMainThreadpool-worker流程中基本的主要功能。该func池中法规定-argument将只匹配func-variable内的worker单呼的方法,如功能全 apply_asyncimapchunksize=1。对于带有chunksize-parameter的其余池方法,处理功能func将是映射器功能(mapstarstarmapstar)。此函数将用户指定的func-parameter映射到可迭代的传输块的每个元素上(->“ map-tasks”)。这花费的时间定义了任务也作为工作单位


塔塞尔

虽然“任务”一词在整个块的整个处理过程中的用法与内部的代码相匹配multiprocessing.pool,但没有指示应如何以块的一个元素作为参数对用户指定的单个调用func提及。为避免命名冲突(maxtasksperchildPool的__init__-method的-parameter )引起的混乱,此答案将任务中的单个工作单元称为taskel

taskel(从任务+ EL EMENT)是一种内工作的最小单位的任务。它是func使用Pool-method的-parameter指定的函数的单次执行,该函数使用从传输的chunk的单个元素获得的参数调用。一个任务taskelschunksize


并行化开销(PO)

PO由Python内部的开销和进程间通信(IPC)的开销组成。Python中每个任务的开销包含打包和解压缩任务及其结果所需的代码。IPC开销具有必要的线程同步以及不同地址空间之间的数据复制(需要两个复制步骤:父->队列->子)。IPC开销的大小取决于OS,硬件和数据大小,这使得很难概括影响。


2.并行化目标

使用多处理时,我们的总体目标(显然)是最大程度地减少所有任务的总处理时间。为了达到这个总体目标,我们的技术目标需要优化硬件资源的利用率

实现技术目标的一些重要子目标是:

  • 最小化并行化开销(最著名但并非唯一的IPC
  • 所有CPU核心的高利用率
  • 限制内存使用量,以防止OS进行过多的分页(乱码

首先,任务必须在计算上足够繁重(密集),以赚回我们必须为并行化支付的订单。PO的相关性随每个任务的绝对计算时间的增加而降低。或者,换句话说,每个任务的绝对计算时间越长,对降低PO的需求就越不相关。如果您的计算将花费每个任务小时数,则IPC开销相比而言可以忽略不计。这里的主要考虑是在分配所有任务之后防止空闲的工作进程。保持所有内核都处于加载状态,这意味着我们将尽可能并行化。


3.并行化方案

哪些因素决定了multiprocessing.Pool.map()之类的方法的最佳块大小参数

问题的主要因素是单个任务之间的计算时间可能会有所不同。顾名思义,最佳块大小的选择取决于每个任务的计算时间的变异系数CV)。

从这种变化的程度来看,在规模上有两种极端情况:

  1. 所有任务都需要完全相同的计算时间。
  2. 一个Taskel可能需要几秒钟或几天才能完成。

为了更好地记忆,我将这些场景称为:

  1. 密集场景
  2. 广泛的场景


密集场景

密集场景中,希望一次分发所有任务组,以将必要的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()呼叫外包。

# mp_utils.py

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 / yx % y而是模运算,从中返回余数x / y。因此,例如divmod(10, 3)return (3, 1)

现在,当你看chunksize, extra = divmod(len_iterable, n_workers * 4),你会发现n_workers这里是除数yx / 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  # naive approach
    cs_pool1 = len_iterable // (n_workers * 4) or 1  # incomplete pool algo.
    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_naivePool的chunksize-algorithm(cs_pool1)的天真chunksize()和第一步块大小,以及完整的Pool-algorithm(cs_pool2)的chunksize 。此外,它还计算了实际因子 rf_pool1 = cs_naive / cs_pool1rf_pool2 = cs_naive / cs_pool2,它们告诉我们天真的计算出的块大小比Pool的内部版本大多少倍。

在下面,您可以看到使用此函数的输出创建的两个图形。左图仅显示n_workers=4直到可迭代长度为止的块大小500。右图显示的值rf_pool1。对于可迭代的长度16,实际因数变为>=4len_iterable >= n_workers * 4并且它的最大值是7可迭代的长度28-31。这4与算法收敛到更长的可迭代项的原始因子有很大的偏差。这里的“较长”是相对的,取决于指定工人的数量。

图1

记住CHUNKSIZEcs_pool1仍然缺乏extra从其余端口-divmod包含在cs_pool2从完整的算法。

该算法继续:

if extra:
    chunksize += 1

现在例,有一个余数(一个extra从divmod操作),通过增加1 CHUNKSIZE显然不能为每一个任务的工作了。毕竟,如果可以的话,就不会有剩余。

你怎么可以在下面的图中看到的,“额外处理”具有这样的效果,即真正的因素对于rf_pool2现在走向收敛4下方 4和偏差是有点顺畅。标准偏差n_workers=4len_iterable=500从下降0.5233rf_pool10.4115rf_pool2

图2

最终,增加chunksize1的效果是,最后传输的任务的大小仅为len_iterable % chunksize or chunksize

然而,对于生成的块数(),可以观察到额外处理的效果越有趣,我们将如何在以后看到,因此也就更加有意义。对于足够长的可迭代对象,Pool完成的chunksize-algorithm(在下图中)将使chunk的数量稳定在。相反,幼稚算法(经过最初的打p)在迭代器和迭代器的长度增长时保持交替。n_chunksn_pool2n_chunks == n_workers * 4n_chunks == n_workersn_chunks == n_workers + 1

图3

在下面,您将找到两个针对Pool的增强信息功能和朴素的chunksize-algorithm。下一章将需要这些功能的输出。

# mp_utils.py

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
    # `+ (len_iterable % chunksize > 0)` exploits that `True == 1`
    n_chunks = len_iterable // chunksize + (len_iterable % chunksize > 0)
    # exploit `0 == False`
    last_chunk = len_iterable % chunksize or chunksize

    return Chunkinfo(
        n_workers, len_iterable, n_chunks, chunksize, last_chunk
    )

不要被可能意想不到的外观所迷惑calc_naive_chunksize_info。在extradivmod没有用于计算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章中的并行化目标时,有一个要点是:

  • 所有CPU核心的高利用率

前面提到的一些东西,Pool的chunksize-algorithm可以尝试改善的是最小化空闲的工作进程以及cpu-core利用率

multiprocessing.Pool人们想知道未使用的内核/在您希望所有工作进程都忙的情况下使工作进程闲置,从而对SO提出了一个重复的问题。尽管这可能有很多原因,但在工人数不等于数的除数的情况下,即使使用密集场景(每个任务的计算时间相等),我们经常可以观察到在计算结束时使工人进程空转。块()。n_chunks % n_workers > 0

现在的问题是:

在实践中,我们如何才能将对块大小的理解转化为某种东西,从而使我们能够解释观察到的工人利用率,甚至在这方面比较不同算法的效率?


6.1模型

为了在这里获得更深入的见识,我们需要一种并行计算的抽象形式,该形式可以将过于复杂的现实简化到可管理的复杂程度,同时在定义的边界内保留重要性。这样的抽象称为模型。如果要收集数据,则这种“并行化模型”(PM)的实现会像实际计算一样生成工人映射的元数据(时间戳)。由模型生成的元数据允许在一定约束下预测并行计算的指标。

图4

此处定义的PM中的两个子模型之一是分布模型(DM)。的DM解释工作单元(taskels)如何原子被分布在平行工人和时间,当没有其他因素比相应CHUNKSIZE算法,工人的数量,输入可迭代(数taskels的)和它们的计算的持续时间被认为是。这意味着包括任何形式的间接费用。

为了获得完整的PMDM开销模型(OM)扩展,该模型表示各种形式的并行开销(PO)。这种模型需要针对每个节点分别进行校准(硬件,操作系统相关性)。如何开销多种形式在中表示OM是开放等多个OMs的具有不同程度的复杂性可能存在。实施的OM需要达到哪种精度水平,取决于特定计算的PO总权重。较短的任务舵会导致PO的重量增加,进而需要更精确的OM如果我们试图预测 并行化效率(PE)


6.2平行时间表(PS)

并行调度是并行计算,其中x轴表示时间,y轴的二维表示代表平行工人的池。工人的数量和总的计算时间标志着矩形的延伸,在该矩形中绘制了较小的矩形。这些较小的矩形代表工作的原子单位(任务组)。

在下面,您可以找到PS的可视化图像,该PS绘制了Dense Scenario中Pool的chunksize-algorithm的DM中的数据。

图5

  • X轴被划分为相等的时间单位,其中每个单位代表Taskel所需的计算时间。
  • y轴分为池使用的工作进程数。
  • 此处的taskel显示为最小的青色矩形,放在匿名工作进程的时间轴(日程表)中。
  • 一项任务是在工作人员时间轴中以相同色调连续突出显示的一个或多个任务。
  • 空转时间单位用红色的瓷砖表示。
  • 并行计划分为几部分。最后一部分是尾部。

下图显示了组成部分的名称。

图6

在包括OM的完整PM中空转份额不仅限于尾部,还包括任务之间甚至任务板之间的空间。


6.3效率

上面介绍的模型可以量化工人的利用率。我们可以区分:

  • 分配效率(DE) -借助DM(或针对Dense Scenario的简化方法)进行计算。
  • 并行效率(PE) -借助校准的PM(预测)进行计算或根据实际计算的元数据进行计算。

重要的是要注意,对于给定的并行化问题,计算出的效率不会自动与更快的整体计算相关联。在这种情况下,对工人的利用只能区分已经开始但尚未完成的任务组的工人和没有这样的“开放”任务组的工人。这意味着,有可能空转一taskel的时间跨度没有注册。

上面提到的所有效率基本上都是通过计算“繁忙共享/并行计划”划分的商来获得的。DEPE之间的区别在于,忙于共享在开销扩展的PM的整体“并行计划”中占较小的比例。

该答案将仅进一步讨论一种用于计算密集场景的DE的简单方法。这足以比较不同的块大小算法,因为...

  1. ... DMPM的一部分,它会随着所采用的不同块大小算法而变化。
  2. ...每个任务的计算持续时间相等的密集场景描述了一个“稳定状态”,对于这些状态,这些时间跨度不属于等式。任何其他情况都将导致随机结果,因为任务组的排序很重要。

6.3.1绝对分配效率(ADE)

一般而言,可以通过将忙碌份额除以并行计划的全部潜力来计算基本效率:

绝对分配效率(ADE) =繁忙共享/并行计划

对于Dense Scenario,简化的计算代码如下所示:

# mp_utils.py

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个任务小组,您将如何看到下图。

图7

当你比较上并行计划用于chunksize=1与以下版本chunksize=3,你会发现,上并行调度较小,在时间轴X轴短。现在应该变得明显,如何做大chunksizes竟然也可以导致增加整体的计算时间,即使是密集方案

但是,为什么不仅仅使用x轴的长度进行效率计算呢?

因为此模型中不包含开销。两种块大小都将有所不同,因此x轴并不是真正可直接比较的。开销仍然可能导致更长的总计算时间,如下面图例2所示。

图8


6.3.2相对分配效率(RDE)

ADE值不包含的信息,如果一个更好的taskels的分布是可能的CHUNKSIZE设置为1,更好的还在这儿意味着较小的怠速分享

为了获得调整为最大可能DEDE值,我们必须将考虑的ADE除以我们获得的ADEchunksize=1

相对分配效率(RDE) = ADE_cs_x / ADE_cs_1

这是在代码中的外观:

# mp_utils.py

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轴长度的chunksizelast_chunk)。这具有这样的结果,即RDE自然收敛到100%(偶数)为各种各样的“尾看起来”像示于下图。

图9

RDE ...

  • 是优化潜力的有力暗示。
  • 对于更长的可迭代对象,自然会变得不太可能,因为整体“并行计划”的相对尾部会缩小。

在此处找到此答案的第二部分。


51
我在SO上看到的最史诗般的答案之一。
克里斯蒂安·朗

4
哦,这是您的简短答案:P
d_kennetz '19

1
但是forreal ..这是一个很好的答案。我为以后想更好地理解这一点的情况加注了这个问题。略读它已经教会了我很多东西!谢谢
d_kennetz

2
@ L.Iridium不客气!我确实在可能的情况下使用过matplotlib,否则... LibreOffice calc + Pinta(基本图像编辑)。是的,我知道...但是它以某种方式起作用。;)
Darkonaut

2
第一个答案是在SO上看到的目录。
tly_alex

51

关于这个答案

该答案是上面接受的答案的第二部分。


7.天真vs. Pool的块大小算法

在详细介绍之前,请考虑以下两个gif。对于不同iterable长度的范围,它们显示了两个比较的算法如何对传递的数据进行分块iterable(届时将是一个序列),以及如何分配结果任务。工作人员的顺序是随机的,实际上,每个工作人员的分布式任务数量可能与此图像不同,在轻型任务组和/或广泛场景中的任务组中。如前所述,此处也不包括开销。对于密集场景中足够笨重的任务组,传输数据大小可以忽略不计,但是实际计算得出的图像非常相似。

cs_4_50

cs_200_250

如第一章“显示5.池的CHUNKSIZE算法”,配有游泳池的CHUNKSIZE算法块的数量将在稳定n_chunks == n_workers * 4的足够大iterables,同时保持之间进行切换n_chunks == n_workers,并n_chunks == n_workers + 1与幼稚的做法。对于朴素的算法适用:因为n_chunks % n_workers == 1Truefor n_chunks == n_workers + 1,所以将创建一个新部分,其中只雇用一个工人。

朴素的块大小算法:

您可能会认为您是在相同数量的工作人员中创建任务的,但这仅适用于没有余数的情况len_iterable / n_workers。如果余数,就会有一个新的部分只有一个单个工人的任务。届时,您的计算将不再并行。

在下面,您会看到一个类似于第5章中显示的图,但显示的是部分的数目而不是块的数目。对于Pool的完整chunksize-algorithm(n_pool2),n_sections将稳定在臭名昭著的硬编码因子上4。对于朴素的算法,n_sections将在一与二之间交替。

图10

对于Pool的块大小算法,n_chunks = n_workers * 4通过前面提到的额外处理进行的稳定,阻止在此处创建新的部分,并将闲置份额限制为一个工人有足够长的可迭代时间。不仅如此,算法还会使Idling Share的相对大小不断缩小,从而导致RDE值收敛至100%。

“足够长”的n_workers=4len_iterable=210例如。对于等于或大于该值的可迭代项,空闲份额将限制为一个工作者,该特征最初是由于4在块大小算法中的-乘法运算而最初丢失的。

图11

天真的块大小算法也可以收敛到100%,但它的速度要慢得多。会聚效果仅取决于以下事实:在有两个部分的情况下,尾巴的相对部分会收缩。只有一名受雇工人的尾巴仅限于x轴长度n_workers - 1,可能的最大余数为len_iterable / n_workers

天真和Pool的chunksize-algorithm的实际RDE值有何不同?

在下面,您可以找到两个热图,其中显示了所有可重复长度(最多5000),从2到100的所有工人的RDE值。色标从0.5到1(50%-100%)。您会在左侧的热图中注意到更多朴素算法的暗区域(较低的RDE值)。相比之下,Pool右边的chunksize-algorithm则绘制出更加阳光明媚的画面。

图12

左下暗角与右上亮角的对角线梯度再次显示了依赖于工人数量的“长迭代”。

每种算法有多糟糕?

使用Pool的chunksize-algorithm,RDE值为81.25%是上面指定的worker范围和可迭代长度的最小值:

图13

有了幼稚的块大小算法,情况可能会变得更糟。此处计算出的最低RDE为50.72%。在这种情况下,几乎只有一个工人在运行一半的计算时间!因此,当心骑士降落的骄傲所有者。;)

图14


8.现实检查

在前面的章节中,我们考虑了纯数学分布问题的简化模型,去除了细节问题,这些细节首先使多重处理成为一个棘手的话题。为了更好地理解分布模型(DM)本身可以在多大程度上有助于解释实际观察到的工人利用率,我们现在来看看实际计算得出的并行调度。

设定

下图均处理了一个简单的,cpu绑定的伪函数的并行执行,该伪函数使用各种参数调用,因此我们可以观察绘制的并行调度如何随输入值的变化而变化。该函数内的“工作”仅包括范围对象上的迭代。因为我们传入了大量数字,这已经足以使核心繁忙。可选地,该函数需要一些taskel-unique附加项data,而这些附加项将保持不变。由于每个Taskel都包含完全相同的工作量,因此我们仍在这里处理密集场景。

该函数用包装器装饰,该包装器以ns-resolution(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)和附加发送的数据大小(每个taskel,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)

下面显示的运行是经过手工挑选的,具有相同的块顺序,因此与“分配模型”中的“并行计划”相比,您可以更好地发现差异,但是请不要忘记工人获得任务的顺序是不确定的。

DM预测

重申一下,分布模型“预测”了并行调度,就像我们在6.2章中已经看到的那样:

图15

第一次运行:每个任务集30k迭代和0 MiB数据

图16

我们在这里的第一次跑步很短,而任务车则非常“轻”。整个pool.starmap()通话总共只花了14.5毫秒。您会发现,与DM相反,空转不仅仅限于尾部,而是在任务之间甚至任务板之间进行。那是因为我们这里的实际日程安排自然包括各种开销。在这里空转意味着任务栏之外的所有内容。Taskel期间可能发生的实际空转未捕获,如前所述。

您还可以看到,并非所有工作人员都同时完成任务。这是由于以下事实:所有工作人员都被共享共享inqueue,一次只能读取一个工作人员。同样适用于outqueue。一旦传输非边际大小的数据,这可能会导致更大的麻烦,我们稍后将看到。

此外,您可以看到,尽管每个任务组都包含相同的工作量,但任务组的实际测量时间跨度却相差很大。分配给worker-3和worker-4的任务需要比前两个worker处理的任务更多的时间。对于本次运行,我怀疑这是由于当时工人3-3 / 4的内核上不再提供Turbo Boost,因此他们以较低的时钟速率处理任务。

整个计算非常轻巧,以致于硬件或操作系统引入的混乱因素会严重扭曲PS。该计算是“随风而去”,而DM预测即使在理论上合适的情况下也没有什么意义。

第二次运行:每个Taskel进行30M次迭代和0个MiB数据

图17

将for循环中的迭代次数从30,000增加到3,000万,将导致真正的并行调度,这与DM提供的数据所预测的调度非常接近!现在,每个任务卡的计算量足够大,足以在开始时和中间将边缘的空闲部分边缘化,从而仅显示DM预测的较大的空闲份额。

第三次运行:每个任务集30M迭代和50 MiB数据

图18

保留30M次迭代,但另外每任务任务发送50 MiB来回扭曲图像。在这里,排队效果很明显。工作者4需要等待的时间比工作者1更长。现在想象一下有70名工人的时间表!

如果任务组在计算上非常轻巧,但是却提供了大量的数据作为有效负载,则单个共享队列的瓶颈可能会阻止向池中添加更多工作器的任何其他好处,即使它们由物理核心支持也是如此。在这种情况下,Worker-1可以完成其第一个任务,甚至在Worker-40完成其第一个任务之前就等待一个新任务。

现在应该变得很明显了,为什么计算时间Pool并不总是随工作人员的数量线性减少。发送相对大量的数据可能导致以下情况:大部分时间都花在等待将数据复制到工作人员的地址空间上,并且一次只能馈送一个工作人员。

第4次运行:每个Taskel有600M迭代和50 MiB数据

图19

在这里,我们再次发送50 MiB,但是将迭代次数从30M增加到600M,这使总计算时间从10 s增加到152 s。再次绘制的并行计划与预测的并行计划非常接近,通过数据复制产生的开销被边缘化了。


9.结论

所讨论的乘法4增加了调度灵活性,但也利用了taskel分布中的不均匀性。没有这种乘积,即使对于短的可迭代对象,空闲份额也将限于单个工人(对于具有密集场景的DM)。池的块大小算法需要输入可迭代项一定大小才能恢复该特性。

正如该答案所希望显示的那样,与朴素的方法相比,Pool的chunksize-algorithm平均导致更好的核心利用率,至少对于一般情况而言,并且不考虑长开销。朴素的算法在这里的分布效率(DE)可以低至约51%,而Pool的块大小算法的分布效率低至约81%。但是,DE不像IPC那样包含并行化开销(PO)。第8章已经表明,对于开销很小的密集场景,DE仍具有很好的预测能力。

尽管相比于朴素的方法,Pool的chunksize-algorithm实现了更高的DE但它并没有为每个输入星座图提供最佳的taskel分布。尽管简单的静态分块算法无法优化(包括开销)并行效率(PE),但没有内在的理由无法始终提供100%的相对分配效率(RDE),这意味着与DE相同与chunksize=1。一个简单的chunksize-algorithm仅由基本数学组成,并且可以以任何方式自由“切片”。

与Pool实施“等分块”算法不同,“等分块”算法将为每个/组合提供100%的RDE。均匀大小的算法在Pool的源代码中实现起来会稍微复杂一些,但是可以通过仅将任务外部打包而在现有算法的基础上进行调制(以防我将Q / A放在怎么做)。len_iterablen_workers


6

我认为您所缺少的部分是您的幼稚估计假设每个工作单元花费相同的时间,在这种情况下,您的策略将是最好的。但是,如果某些作业比其他作业更快完成,则某些核心可能会变得空闲,以等待缓慢的作业完成。

因此,通过将这些块分解成4倍以上的块,然后,如果一个块提早完成,则该内核可以启动下一个块(而其他内核继续在其较慢的块上工作)。

我不知道他们为什么要精确选择因子4,但这将是在最小化映射代码的开销(需要最大的块)和平衡不同时间段的块(这需要最小的块)之间的权衡)。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.