Pymoo 并行化评估指南:用 Starmap 与 Joblib 为 ElementwiseProblem 加速昂贵适应度计算
【免费下载链接】scientific-agent-skillsTurn any AI agent into an AI Scientist. The #1 Agent Skills library for science, used by 190,000+ scientists worldwide. 165 ready-to-use validated skills plus 100+ scientific databases covering biology, chemistry, medicine, and drug discovery. Compatible with Cursor, Claude Code, Codex, Pi, Antigravity, and the open Agent Skills standard.项目地址: https://gitcode.com/GitHub_Trending/cl/scientific-agent-skills
Pymoo 是当前仓库 pymoo 技能 主打的单目标与多目标优化框架(当前稳定版本 0.6.1.6)。当你的自定义问题以ElementwiseProblem形式定义、且单次_evaluate调用成本很高(例如仿真、机器学习推理、外部求解器)时,Pymoo 默认一次只评估一个解,整个进化算法的速度会被逐个评估串行拖垮。本指南基于技能库参考文档 references/parallelization.md 展开,完整讲解elementwise_runner的两种注入方式——基于multiprocessing.Pool.starmap的StarmapParallelization(线程池 / 进程池)与基于 joblib 的JoblibParallelization,并给出可复制运行的完整代码、选择依据与常见陷阱。读完后你可以在 10 分钟内把串行评估的自定义优化问题改造成并行版本,直接用于真实工程场景。
一、为什么需要并行化:理解 ElementwiseProblem 的评估瓶颈
1.1 三种问题定义风格中,为什么是 ElementwiseProblem
Pymoo 支持三种问题定义风格(见 SKILL.md 的 Core Concepts 小节):
| 风格 | 评估方式 | 适用场景 |
|---|---|---|
Problem | 向量化——_evaluate一次性接收一批解的矩阵 | 纯数值计算、可批量矢量化的目标函数 |
ElementwiseProblem | 逐解评估——每次_evaluate只处理一个解 | 自定义问题、需要并行化的昂贵评估 |
FunctionalProblem | 用独立函数定义目标与约束,无需继承类 | 函数式定义的简单问题 |
关键区别在于:ElementwiseProblem的_evaluate(self, x, out, *args, **kwargs)中x是单个解(一维数组),而向量化Problem的_evaluate中X是整个批次(二维矩阵)。参考文档明确指出:
Pymoo evaluates one solution per
_evaluatecall forElementwiseProblem; pass a runner to evaluate multiple solutions concurrently.
也就是说,进化算法每一代都需要评估整个种群,而ElementwiseProblem会将这些评估逐个串行执行。当单次评估很昂贵时(一次 CFD 仿真几秒钟、一次模型推理几百毫秒),串行等待就是整个优化流程的最大瓶颈。并行化的目标正是把"一次一个解"变成"一次并发多个解"。
仓库中的自定义问题示例 scripts/custom_problem_example.py 展示了ElementwiseProblem的标准写法——在__init__中声明n_var、n_obj、n_ieq_constr、xl/xu,在_evaluate中填充out["F"](目标值)、out["G"](不等式约束,约定g(x) <= 0为可行)与out["H"](等式约束,约定h(x) = 0)。
1.2 何时该启用并行化
参考文档给出的判断标准非常明确:当_evaluate是瓶颈时。典型场景包括:
- 仿真模拟:有限元、流体力学、分子动力学等单次运行耗时秒级到分钟级;
- 机器学习推理:每次评估需要加载模型并对输入做一次前向传播;
- 外部求解器:评估需要调用第三方求解器、命令行工具或远程服务。
如果_evaluate只是(x ** 2).sum()这类纯算数运算(如示例脚本 scripts/single_objective_example.py 中的 Sphere 函数),并行化的线程调度、进程通信开销反而可能超过计算本身,收益趋近于零甚至为负。
二、启用并行化的三个硬性前提
参考文档在 Requirements 一节给出了必须满足的三个条件,缺一不可:
- 必须继承
ElementwiseProblem(而不是向量化的Problem)。并行 runner 的契约是"每个 worker 处理一个解",只有逐解评估的问题定义才符合这个模型; - 设置
elementwise_evaluation=True。这是ElementwiseProblem的默认值,因此一般不需要显式写出——只要你继承的是ElementwiseProblem,该选项自动生效; - 把 runner 通过
elementwise_runner参数传给问题构造函数。这是接入并行化的唯一入口。
三、Starmap 接口:一套代码,线程池 / 进程池通吃
StarmapParallelization位于pymoo.parallelization.starmap,它复用的是 Python 标准库multiprocessing.Pool.starmap的函数签名——因此任何提供starmap(func, iterable)接口的池对象(ThreadPool或Pool)都可以直接注入。
3.1 第一步:定义一个可注入 runner 的自定义问题
问题类需要把elementwise_runner透传给父类构造函数,这是参考文档给出的标准骨架:
from pymoo.core.problem import ElementwiseProblem class MyProblem(ElementwiseProblem): def __init__(self, elementwise_runner=None, **kwargs): super().__init__( n_var=10, n_obj=1, xl=-5, xu=5, elementwise_runner=elementwise_runner, **kwargs, ) def _evaluate(self, x, out, *args, **kwargs): out["F"] = (x ** 2).sum() # 替换为真正昂贵的评估逻辑这里的**kwargs透传很重要:它允许 pymoo 内部在需要时注入其他问题级参数,也让你不必在每次构造时重复声明维度信息。
3.2 线程池方案:适合 I/O 密集型评估
当评估的瓶颈是等待外部资源(网络请求、文件读取、数据库查询)而非 CPU 计算时,线程池即可获得良好并行度,且线程共享进程内存,无需序列化传递问题对象:
import multiprocessing from multiprocessing.pool import ThreadPool from pymoo.algorithms.soo.nonconvex.ga import GA from pymoo.core.problem import ElementwiseProblem from pymoo.optimize import minimize from pymoo.parallelization.starmap import StarmapParallelization # 线程池(共享内存;适合 I/O 密集型评估) n_threads = 4 pool = ThreadPool(n_threads) runner = StarmapParallelization(pool.starmap) problem = MyProblem(elementwise_runner=runner) result = minimize(problem, GA(), ("n_gen", 50), seed=1) pool.close()3.3 进程池方案:适合 CPU 密集型评估
当评估消耗大量 CPU(数值仿真、模型推理的计算部分)时,线程受 GIL 限制无法充分利用多核,应改用multiprocessing.Pool。每个进程拥有独立的内存空间,可以真正并行执行 Python 代码:
# 进程池(独立内存;适合 CPU 密集型评估) n_processes = 4 pool = multiprocessing.Pool(n_processes) runner = StarmapParallelization(pool.starmap) problem = MyProblem(elementwise_runner=runner) result = minimize(problem, GA(), ("n_gen", 50), seed=1) pool.close()minimize()的其余部分与串行版本完全一致:问题、算法(这里使用单目标非凸 GA)、终止条件("n_gen", 50)(最多 50 代)以及可复现随机种子seed=1。关于minimize()的统一接口与result对象(result.X决策变量、result.F目标值、result.G约束违反),可参见 quick_start_workflows.md 的 Workflow 1/4。
3.4 底层原理:starmap 契约
StarmapParallelization之所以叫 "Starmap",是因为它接受任意满足starmap(func, iterable)签名的池对象:池会将迭代器中的每一组参数解包(*args)后调用func。在 pymoo 的并行评估流程中,主进程把一批待评估的解分发给 runner,runner 通过pool.starmap将它们并发地交给各 worker 执行_evaluate,再把结果汇总回out字典。由于接口只依赖starmap这一个方法,你甚至可以用任何实现了该接口的自定义调度器(如基于 Dask 的 runner)替换内置实现——这也是 quick_start_workflows.md 中 Workflow 8 提到"threads, processes, or Dask"的依据。
四、Joblib 接口:更灵活的备选方案
JoblibParallelization位于pymoo.parallelization.joblib,是参考文档给出的第二种并行注入方式。它的优势在于可以使用 joblib 的完整能力:按任务自动选择loky/threading后端、内存映射(mmap_mode)共享大数组、以及更精细的任务批处理控制:
from joblib import Parallel, delayed from pymoo.parallelization.joblib import JoblibParallelization runner = JoblibParallelization( lambda func, X: Parallel(n_jobs=4)(delayed(func)(x) for x in X) ) problem = MyProblem(elementwise_runner=runner)这里的 lambda 定义了一个"接收函数与解集合、返回并行结果列表"的映射规则:Parallel(n_jobs=4)启动 4 个 worker,delayed(func)(x)把每个解x分派给func(即 pymoo 内部包装的评估函数)。调整n_jobs即可控制并行度。
joblib 不在 pymoo 的默认依赖里,需要单独安装(SKILL.md 的 compatibility 字段也将 joblib 标注为可选依赖):
uv pip install joblib完整环境安装(pymoo 本体)同样通过 uv 完成,可固定版本以获得可复现环境:
uv pip install pymoo uv pip install "pymoo==0.6.1.6"五、线程池 vs 进程池:如何选择
| 维度 | 线程池(ThreadPool) | 进程池(multiprocessing.Pool) |
|---|---|---|
| 内存模型 | 共享进程内存 | 每 worker 独立内存 |
| 适用负载 | I/O 密集型(等待外部资源) | CPU 密集型(数值计算、仿真) |
| 对象传递 | 无需序列化 | 问题定义必须可 pickle |
| Python 代码并行度 | 受 GIL 限制 | 真正的多核并行 |
| 启动开销 | 低 | 较高(进程 fork/spawn) |
选择要点:如果你的_evaluate大部分时间在等待(网络、磁盘、外部服务),线程池足够且更轻量;如果它在高强度计算 Python 代码,进程池才能榨干多核。两者的接入代码只有pool = ThreadPool(...)与pool = multiprocessing.Pool(...)一行之差,切换成本极低,建议针对实际负载做一次基准测试。
六、关键注意事项与常见坑
参考文档的 Notes 一节总结了四条必须遵守的规则,结合源码实现可以进一步展开:
6.1 每次运行结束都要关闭池
minimize()返回后,池对象仍然持有 worker 资源,必须显式调用pool.close()(以及需要时pool.join())释放,否则进程/线程会滞留,在长流程或循环调用中积累成资源泄漏。参考文档与 Workflow 8 的所有示例都在minimize()之后立即关闭池。
6.2 进程池要求问题定义可 pickle
multiprocessing.Pool通过 pickle 序列化把问题对象发给各 worker 进程,因此:
- 避免在类体内使用 lambda——lambda 无法被 pickle;
- 避免局部定义的类、闭包捕获不可序列化对象;
- 在 Linux 等使用 fork 启动的平台上,进程池从父进程复制内存快照,问题相对宽松;但在 Windows 等 spawn 平台上,模块顶层必须受
if __name__ == "__main__":保护,否则子进程会重复执行模块代码。
参考文档的表述是"Process pools require picklable problem definitions (avoid lambdas in class bodies)",即问题是worker 需要"看懂"的对象,务必保证它能被序列化往返一次。
6.3 并行收益取决于评估成本与开销之比
并行化不是免费的:线程调度、任务分发、结果收集(以及进程池的序列化)都是固定开销。只有当单次_evaluate的成本显著高于这些开销时,加速比才接近理想值(受 Amdahl 定律约束,并行部分占比越高、加速越明显)。这也呼应了参考文档"Use parallelization when_evaluateis the bottleneck"的判断——把昂贵仿真与纯算术(x ** 2).sum()一视同仁地并行化是没有意义的。
6.4 向量化问题不要用 runner,直接在_evaluate内分批
如果你的问题继承的是向量化Problem(_evaluate接收整个批次的矩阵),并行 runner 并不适用。参考文档明确指出这类情况应在_evaluate内部自行实现批处理(batching)——例如在单个函数内对矩阵行做for循环或利用 NumPy 广播,让向量化与并行化各归其位。
6.5 与整体优化流程的配合
并行化只改变"评估怎么执行",不改变算法的搜索逻辑。你依然可以:
- 用
seed=1固定随机种子保证可复现性(仓库测试 tests/pymoo/test_scripts.py 专门验证了同种子两次运行结果一致); - 用
("n_gen", 50)或get_termination("f_tol", tol=0.001)控制终止条件; - 为 GA 配置算子和种群参数,例如 scripts/single_objective_example.py 中的
GA(pop_size=100, sampling=FloatRandomSampling(), crossover=SBX(prob=0.9, eta=15), mutation=PM(eta=20), eliminate_duplicates=True); - 评估循环中每个候选解的
_evaluate仍按out["F"]、out["G"]、out["H"]返回目标与约束。
七、在技能库中的定位:Workflow 8 与配套资源
并行化是 quick_start_workflows.md 九个可运行工作流中的 Workflow 8(Parallel Evaluation),它的适用条件、线程池示例与本文一致,并明确指向本参考文档获取进程池、joblib 与 pickling 细节。本技能还提供五个可直接运行的可执行示例脚本:
python3 scripts/single_objective_example.py # 单目标优化(GA + Sphere) python3 scripts/multi_objective_example.py # 多目标优化(NSGA-II + ZDT1) python3 scripts/many_objective_example.py # 多目标优化(NSGA-III + DTLZ2) python3 scripts/custom_problem_example.py # 自定义问题(含约束) python3 scripts/decision_making_example.py # 多准则决策(PseudoWeights)这些脚本位于 skills/pymoo/scripts/ 目录,仓库测试 tests/pymoo/test_scripts.py 会对它们逐一执行验证(包括验证评估预算pop_size * n_gen、ZDT1 解析前沿f2 = 1 - sqrt(f1)、NSGA-II 结果集互不支配等性质)。把并行化的elementwise_runner接入上述任一ElementwiseProblem场景(如自定义问题示例 scripts/custom_problem_example.py 中的MyBiObjectiveProblem/ConstrainedProblem),即可在保持优化语义不变的前提下获得并发评估能力。
实践建议总结:先确认评估确实昂贵(这是前提);再确认问题是ElementwiseProblem且没有显式关闭elementwise_evaluation;然后按负载类型选择线程池或进程池,通过StarmapParallelization或JoblibParallelization注入elementwise_runner;最后别忘了在minimize()之后关闭池,并保证进程池场景下问题对象可 pickle。按此流程改造,即可为昂贵的仿真、推理与外部求解器类优化问题带来立竿见影的吞吐提升。
【免费下载链接】scientific-agent-skillsTurn any AI agent into an AI Scientist. The #1 Agent Skills library for science, used by 190,000+ scientists worldwide. 165 ready-to-use validated skills plus 100+ scientific databases covering biology, chemistry, medicine, and drug discovery. Compatible with Cursor, Claude Code, Codex, Pi, Antigravity, and the open Agent Skills standard.项目地址: https://gitcode.com/GitHub_Trending/cl/scientific-agent-skills
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考