使用multiprocessing.Pool处理超大迭代器无法启动的问题求助
需要从70个元素中选取30个元素的子集并计算每个子集的成本,目标是最小化成本。单CPU运行耗时极长(数周甚至更久),计划用multiprocessing模块借助8-24个逻辑核加速,但遇到以下问题:
- 总组合数约为5.5e19(计算公式:
70!/(30!*(70-30)!)),即使单成本计算仅需1ms,总耗时仍不可接受。 - 使用
pool.map时,进程迟迟不启动calc_cost函数,似乎在预计算全部输入数据;但缩小迭代规模(如从70个元素选3个,共54740种组合)时,进程能正常启动。 - 最初用列表存储数据池,后改用
itertools.combinations迭代器避免预构建全量数据,但问题依旧。 - 希望了解问题原因,以及处理此类超大迭代器的最佳方案,最好能在评估部分组合后找到符合成本阈值的结果。
示例代码:
import itertools import math import multiprocessing list_of_elements = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'] # 从元素列表中选取4个元素的组合,作为数据池 data_pool = list(map(" ".join, itertools.combinations(list_of_elements, 4))) # 数据池共70个元素(8选4的组合数) size_of_data_pool = len(data_pool) # = 70 sel_size = 30 # 或改为3用于测试 # 待评估的总组合数 total_nr_of_combinations = math.factorial(size_of_data_pool) / (math.factorial(sel_size) * math.factorial(size_of_data_pool - sel_size)) # ~5.5e19 def calc_cost(subset): # 此处为成本计算逻辑 pass with multiprocessing.Pool() as pool: pool.map(calc_cost, itertools.combinations(data_pool, sel_size))
一、问题原因
multiprocessing.Pool.map内部会将传入的可迭代对象转换为列表后再分配任务,哪怕你传入的是itertools.combinations迭代器,它也会先尝试生成所有组合并存储到内存中。对于5.5e19级别的组合数,这不仅会直接耗尽内存,还会让主进程陷入无限的预计算循环,根本无法启动子进程执行calc_cost。
二、最佳处理方案
1. 放弃暴力枚举,改用启发式优化算法
5.5e19的组合数完全不可能暴力遍历,哪怕用百万核并行也做不到。必须换用启发式优化算法寻找近似最优解,常用选项包括:
- 遗传算法:模拟自然选择,通过交叉、变异迭代生成更优子集
- 模拟退火:允许暂时接受较差解,避免陷入局部最优
- 贪心算法:每次选择当前最优元素,快速得到可行解(但可能不是全局最优)
- 局部搜索:从初始解出发,通过微调(替换元素)寻找更优解
这类算法仅需评估数万到数百万个组合,就能得到满足需求的近似最优解,完全不需要遍历全部组合。
2. 若需部分枚举,改用imap或imap_unordered
如果你的成本阈值很低,希望找到第一个符合要求的解就停止,可以使用pool.imap或pool.imap_unordered——这两个方法不会预先生成所有组合,而是逐个生成任务并分配给子进程,能立即启动calc_cost执行。
修改后的示例代码:
with multiprocessing.Pool() as pool: # 使用imap_unordered,结果返回顺序不固定,但能更快得到第一个结果 for result in pool.imap_unordered(calc_cost, itertools.combinations(data_pool, sel_size)): if result <= your_cost_threshold: # your_cost_threshold为设定的成本阈值 print("找到符合要求的子集") pool.terminate() # 立即终止所有进程 break
注意:即使如此,5.5e19的组合数下,随机碰到符合阈值的解概率极低,除非阈值设置得非常宽松,否则这种方法依然不现实。
3. 拆分任务,手动生成组合块
如果一定要并行处理部分组合,可以手动将组合生成逻辑拆分为多个独立子任务,让每个子进程负责生成特定范围的组合,避免主进程预计算全量数据。比如通过数学方法拆分组合空间,让每个进程只生成自己负责的那部分组合。不过这种方法复杂度高,且依然无法解决总组合数过大的问题,仅适用于组合数在可处理范围内的场景。
内容的提问来源于stack exchange,提问作者Mike M.

