Skip to content

[BUG] Forking after a solve aborts at the C level instead of raising the solve loop guard's error #229

Description

@camUrban

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions