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
,该映射包含约1500万个元素李>.map()
- 在具有24个内核的机器上工作,并在
多处理.Pool()中使用默认值
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)