Python多处理:理解“chunksize”背后的逻辑`

Python多处理:理解“chunksize”背后的逻辑`,python,python-3.x,parallel-processing,multiprocessing,python-multiprocessing,Python,Python 3.x,Parallel Processing,Multiprocessing,Python Multiprocessing,哪些因素决定了像multiprocessing.Pool.map()这样的方法的最佳chunksize参数?.map()方法似乎对其默认的chunksize使用了一种任意的启发式方法(解释如下);这种选择的动机是什么?是否有一种基于特定情况/设置的更为深思熟虑的方法 比如说,我是: 将一个iterable传递给.map(),该映射包含约1500万个元素 在具有24个内核的机器上工作,并在多处理.Pool()中使用默认值 我天真的想法是给24名员工每人一块大小相等的块,即15_000_000/

哪些因素决定了像
multiprocessing.Pool.map()
这样的方法的最佳
chunksize
参数?
.map()
方法似乎对其默认的chunksize使用了一种任意的启发式方法(解释如下);这种选择的动机是什么?是否有一种基于特定情况/设置的更为深思熟虑的方法

比如说,我是:

  • 将一个
    iterable
    传递给
    .map()
    ,该映射包含约1500万个元素
  • 在具有24个内核的机器上工作,并在
    多处理.Pool()中使用默认值
我天真的想法是给24名员工每人一块大小相等的块,即
15_000_000/24
或625000。大批量生产应在充分利用所有员工的同时减少营业额/管理费用。但这似乎忽略了给每个工人大批量生产的一些潜在不利因素。这是一张不完整的照片吗?我错过了什么


我的部分问题源于if
chunksize=None
的默认逻辑:
.map()
.starmap()
调用,如下所示:

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
# 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
divmod(len(iterable)、len(self.\u pool)*4)背后的逻辑是什么?这意味着chunksize将更接近
15_000_000/(24*4)==156_250
。将len(self.\u pool)
乘以4的目的是什么

这使得生成的chunksize比我上面的“NaiveLogic”小4倍,后者只需将iterable的长度除以
池中的工作线程数即可

最后,还有
.imap()
上Python文档中的这一点进一步激发了我的好奇心:

chunksize
参数与
map()使用的参数相同
方法。对于非常长的iterables,为
chunksize
使用较大的值可以 使作业完成速度比使用默认值1快得多



相关的回答很有帮助,但有点太高了:。

我认为你缺少的部分是,你天真的估计假设每个工作单元都需要相同的时间,在这种情况下,你的策略是最好的。但是,如果某些作业比其他作业完成得更快,则某些内核可能会闲置,等待缓慢的作业完成

因此,通过将块分成4倍多的块,如果一个块提前完成,那么该核心可以开始下一个块(而其他核心继续处理其较慢的块)

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

Pool的chunksize算法是一种启发式算法。它为您试图填充到Pool方法中的所有可想象的问题场景提供了一个简单的解决方案。因此,它无法针对任何特定场景进行优化

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

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

这个答案试图为您提供一些基本概念,帮助您更清楚地了解池的调度黑匣子。它还试图为您提供一些基本的工具,帮助您识别和避免与块大小相关的潜在悬崖

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
    )

目录

第一部分

  • 定义
  • 并行化目标
  • 并行化场景
  • Chunksize>1的风险
  • 池的Chunksize算法
  • 量化算法效率

    6.1模型

    6.2平行时间表

    6.3效率

    6.3.1绝对分配效率(ADE)

    6.3.2相对分配效率(RDE)

  • Naive vs.Pool的Chunksize算法
  • 现实检查
  • 结论
  • 有必要先澄清一些重要术语


    1.定义

    这里的块是池方法调用中指定的
    iterable
    -参数的一部分。chunksize是如何计算的,以及它会产生什么影响,这是本答案的主题


    任务

    任务在辅助进程中的数据物理表示如下图所示

    此图显示了对
    pool.map()
    的调用示例,该调用沿着一行代码显示,取自
    多处理.pool.worker
    函数,其中从
    inqueue
    读取的任务将被解包
    worker
    是池工作进程的
    main线程中的底层主函数。pool方法中指定的
    func
    -参数将仅与
    worker
    -函数中的
    func
    -变量相匹配,用于
    apply\u async
    imap
    chunksize=1
    等单次调用方法。对于具有
    chunksize
    -参数的池方法的其余部分,处理函数
    func
    将是映射器函数(
    mapstar
    starmapstar
    )。此函数将用户指定的
    func
    -参数映射到iterable(-->“映射”的已传输块的每个元素上-
    bad_luck_iterable = [60, 60, 86400, 86400, 60, 60, 60, 84600]
    
    [(60, 60), (86400, 86400), (60, 60), (60, 84600)]
    
    # 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
    
    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
    
    if extra:
        chunksize += 1
    
    # 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
        )
    
    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
        )
    
    # 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
    
    # 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
    
    @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
    
    ...
    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)