Skip to content

Fix native crash in serial Nevière theta-search sweeps - #44

Merged
simonevadi merged 3 commits into
developfrom
fix/neviere-serial-blas-segfault
Sep 4, 2026
Merged

Fix native crash in serial Nevière theta-search sweeps#44
simonevadi merged 3 commits into
developfrom
fix/neviere-serial-blas-segfault

Conversation

@simonevadi

Copy link
Copy Markdown
Contributor

Context

A serial (MAX_WORKERS=1) p-polarised Mo/B4C second-order theta-search sweep
crashed with Segmentation fault (core dumped) on Linux/OpenBLAS, with no
Python traceback and no exception recorded in the checkpoint metadata. The run's
tracked-theta logic had followed a near-zero p-pol feature down to ~0.29 deg
grazing (well below the multilayer Bragg estimate) at ~1e-4 efficiency when it
died.

Root cause

The differential method issues thousands of tiny dense zgesv/zgemm calls
per photon-energy point
— one interface-response block per z-slice plus the
sub-block cascade. Under a many-threaded OpenBLAS that pattern is both slow
(thread dispatch dominates for ~100×100 matrices) and, on some OpenBLAS builds,
a hard SIGSEGV in threaded zgemm/getrf.

BatchSimulationRunner already pins OPENBLAS_NUM_THREADS / OMP_NUM_THREADS /
MKL_NUM_THREADS to 1 — but only in _worker_initializer, which runs solely
in spawned ProcessPoolExecutor workers. A serial run (_run_serial_cases) or
a direct grax.run_simulation call executes in the current process, where those
environment variables were read once at BLAS import and no longer take effect.
Not reproducible on macOS because NumPy there links Apple Accelerate, which is
not affected by this OpenBLAS bug class.

Changes

  • grax._threads.single_threaded_blas() — a context manager wrapping
    threadpoolctl.threadpool_limits(1, "blas"). A no-op where no controllable
    native library is loaded (e.g. Accelerate) and redundant inside a spawned
    worker.
  • run_simulation wraps only the Nevière solve in it. RCWA is left alone:
    its one large per-layer eigendecomposition genuinely benefits from threaded
    BLAS, and it runs the shared cascade far fewer times (one block per compressed
    layer, no RK4 sub-blocks), so it is not the pathological caller.
  • Non-finite hardening in the shared cascade code (_boundary_block_from_transfer,
    _cascade_boundary_pair): reject a non-finite slab transfer matrix or
    cascaded block with a ValueError before it reaches np.linalg.solve,
    which crashes rather than raising on some LAPACK builds. The cascade's
    existing post-check is escalated from a logger.warning to that same error so
    a bad block can never propagate into the next solve. This part applies to both
    solvers.
  • threadpoolctl promoted from an indirect dependency (via SciPy) to a
    direct one.
  • examples/simulation/neviere_grazing_stability/ — sweeps a coated Mo/B4C
    grating from a Bragg angle down to 0.01 deg in p-polarization and asserts
    every diffraction efficiency stays finite; exits cleanly.
  • Regression tests in tests/unit/test_neviere.py (BLAS pin applied for
    neviere and not for rcwa; non-finite cascade input → ValueError;
    small-angle p-pol order-2 stays finite).

Also folded in on this branch (were sitting locally on develop, unpushed):

  • dynamic_ncols=True on the tqdm progress bars so they re-fit on terminal
    resize, plus the DummyProgress test-double signature fix that change needed.

Is RCWA affected?

The threaded-BLAS crash class could in principle touch RCWA, since it shares
_cascade_boundary_pair — but RCWA drives the cascade orders of magnitude less
and was not the reported failure, and blanket single-threading RCWA would
regress its eigensolve. So the thread pin is scoped to Nevière; the non-finite
guard, which is the other half of the fix, does cover RCWA.

Verification

  • Full tests/unit + tests/smoke: 486 passed, 5 skipped.
  • New neviere_grazing_stability example runs clean locally (Accelerate). Needs
    a confirmation run on the Linux/OpenBLAS box that originally crashed:
    python examples/simulation/neviere_grazing_stability/neviere_grazing_stability.py
    then re-run the real 2_mo_b4c_blaze_study.py.

Not addressed here

The tracked-theta logic locking onto a ~1e-4 p-pol feature at 0.29 deg, far from
the ~0.88 deg Bragg estimate, looks like a separate robustness question in
theta_search_sweep.py's continuity-vs-Bragg fallback.

🤖 Generated with Claude Code

simonevadi and others added 3 commits September 4, 2026 12:02
Pass dynamic_ncols=True to the three tqdm bars so a mid-run terminal
resize re-fits the bar instead of keeping the width measured at start-up.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The progress-bar change in afb1c79 passes dynamic_ncols=True to the tqdm
constructor. The DummyProgress fakes in the batch-runner and theta-search
progress tests took a fixed (total, desc, unit) signature and raised
TypeError. Accept and ignore extra keyword arguments.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ascades

A serial (max_workers=1) p-polarised Mo/B4C second-order theta-search sweep
segfaulted on Linux/OpenBLAS with no Python traceback, during a run whose
tracked-theta logic had wandered down to ~0.29 deg grazing (well below the
Bragg estimate) at ~1e-4 efficiency.

Root cause is the differential method's access pattern under a threaded BLAS:
it issues thousands of tiny dense zgesv/zgemm calls per photon-energy point
(one interface-response block per z-slice, plus the sub-block cascade), and a
many-threaded OpenBLAS both wastes its time on dispatch and, on some builds,
crashes under that churn. BatchSimulationRunner already sets
OPENBLAS_NUM_THREADS=1 in its spawned workers, but a serial run or a direct
run_simulation call executes in the current process, where the env var was
read once at BLAS import and no longer takes effect.

- New grax._threads.single_threaded_blas() context manager
  (threadpoolctl.threadpool_limits(1, "blas")); no-op when no controllable
  native library is loaded (e.g. NumPy on Apple Accelerate).
- run_simulation wraps only the Neviere solve in it. RCWA, whose single large
  eigensolve does benefit from threads, is untouched.
- threadpoolctl promoted from indirect (via SciPy) to a direct dependency.
- _boundary_block_from_transfer and _cascade_boundary_pair now raise ValueError
  on non-finite input/output instead of feeding it to np.linalg.solve, which
  crashes rather than raising on some LAPACK builds. The cascade's existing
  post-check is escalated from a warning to that same error.
- New examples/simulation/neviere_grazing_stability/ sweeps a coated Mo/B4C
  grating from a Bragg angle down to 0.01 deg in p and asserts finiteness.
- Regression tests in tests/unit/test_neviere.py.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@simonevadi
simonevadi merged commit 8434ed0 into develop Sep 4, 2026
1 check passed
@simonevadi
simonevadi deleted the fix/neviere-serial-blas-segfault branch September 4, 2026 12:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant