Bug Description
solve_loop_thread_limits guards against concurrent solve loops within one process, and its RuntimeError message recommends running simulations in parallel "with separate processes, for example with the multiprocessing module." On Linux with Python 3.13 and earlier, following that advice with the default fork start method crashes without ever reaching a Ptera Software error message. The first parallel=True kernel launch initializes Numba's GNU OpenMP threading layer, whose pthread_atfork child handler aborts every subsequently forked child during the fork() call itself, printing only "Terminating: fork() called from a process already using GNU OpenMP, this is unsafe." A fork-based ProcessPoolExecutor then surfaces this as an opaque BrokenProcessPool. The failure is also sneakily configuration-dependent: it only occurs when at least one solve has already run in the parent process, which is exactly what a warm-up run to prime the Numba cache does.
Location(s): pterasoftware/_functions.py
Expected Behavior
The fork hazard should be intercepted by the existing solve loop guard machinery and raised as a RuntimeError with corrected guidance, matching how the thread-level hazard is handled today. Numba's abort happens in the forked child at the C level, before any Python code runs there, and Python's os.register_at_fork before-hooks cannot veto a fork (exceptions raised in them are reported as unraisable and the fork proceeds), so the only workable interception point is the parent, before the fork. When the first solve loop in a process exits, check numba.threading_layer(), and if it is "omp", wrap os.fork and os.forkpty with functions that raise the guard's RuntimeError. The fork start method of multiprocessing reaches the kernel through the os.fork attribute, so the wrapper surfaces as a catchable exception at the caller's site (verified empirically with a fork-based ProcessPoolExecutor). The wrapper has no false positives, because once the OpenMP pool is live, Numba aborts every child created through os.fork anyway, even one that would immediately exec. It also has no collateral damage: subprocess does not go through os.fork, and the threading layer gate keeps the patch off platforms whose default start method is already spawn. The expanded message should state that fork-method multiprocessing cannot be used after a solve has run in the process, and should recommend the spawn or forkserver start method (for example, mp_context=multiprocessing.get_context("spawn") for ProcessPoolExecutor) or creating the worker processes before the first solve.
Additional Context
Python 3.14 changes the default multiprocessing start method on Linux away from fork, so this failure mode disappears there, but Ptera Software's supported range caps at Python 3.13, so every supported version is affected. Hardening the kernels themselves (for example, requesting a fork-safe Numba threading layer) was considered and deliberately deferred; this issue covers only the guard expansion and the corrected guidance language.
Bug Description
solve_loop_thread_limitsguards against concurrent solve loops within one process, and itsRuntimeErrormessage recommends running simulations in parallel "with separate processes, for example with the multiprocessing module." On Linux with Python 3.13 and earlier, following that advice with the default fork start method crashes without ever reaching a Ptera Software error message. The firstparallel=Truekernel launch initializes Numba's GNU OpenMP threading layer, whosepthread_atforkchild handler aborts every subsequently forked child during thefork()call itself, printing only "Terminating: fork() called from a process already using GNU OpenMP, this is unsafe." A fork-basedProcessPoolExecutorthen surfaces this as an opaqueBrokenProcessPool. The failure is also sneakily configuration-dependent: it only occurs when at least one solve has already run in the parent process, which is exactly what a warm-up run to prime the Numba cache does.Location(s):
pterasoftware/_functions.pyExpected Behavior
The fork hazard should be intercepted by the existing solve loop guard machinery and raised as a
RuntimeErrorwith corrected guidance, matching how the thread-level hazard is handled today. Numba's abort happens in the forked child at the C level, before any Python code runs there, and Python'sos.register_at_forkbefore-hooks cannot veto a fork (exceptions raised in them are reported as unraisable and the fork proceeds), so the only workable interception point is the parent, before the fork. When the first solve loop in a process exits, checknumba.threading_layer(), and if it is"omp", wrapos.forkandos.forkptywith functions that raise the guard'sRuntimeError. The fork start method ofmultiprocessingreaches the kernel through theos.forkattribute, so the wrapper surfaces as a catchable exception at the caller's site (verified empirically with a fork-basedProcessPoolExecutor). The wrapper has no false positives, because once the OpenMP pool is live, Numba aborts every child created throughos.forkanyway, even one that would immediately exec. It also has no collateral damage:subprocessdoes not go throughos.fork, and the threading layer gate keeps the patch off platforms whose default start method is already spawn. The expanded message should state that fork-method multiprocessing cannot be used after a solve has run in the process, and should recommend the spawn or forkserver start method (for example,mp_context=multiprocessing.get_context("spawn")forProcessPoolExecutor) or creating the worker processes before the first solve.Additional Context
Python 3.14 changes the default multiprocessing start method on Linux away from fork, so this failure mode disappears there, but Ptera Software's supported range caps at Python 3.13, so every supported version is affected. Hardening the kernels themselves (for example, requesting a fork-safe Numba threading layer) was considered and deliberately deferred; this issue covers only the guard expansion and the corrected guidance language.