Fix native crash in serial Nevière theta-search sweeps - #44
Merged
Conversation
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Context
A serial (
MAX_WORKERS=1) p-polarised Mo/B4C second-order theta-search sweepcrashed with
Segmentation fault (core dumped)on Linux/OpenBLAS, with noPython 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/zgemmcallsper 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
SIGSEGVin threadedzgemm/getrf.BatchSimulationRunneralready pinsOPENBLAS_NUM_THREADS/OMP_NUM_THREADS/MKL_NUM_THREADSto1— but only in_worker_initializer, which runs solelyin spawned
ProcessPoolExecutorworkers. A serial run (_run_serial_cases) ora direct
grax.run_simulationcall executes in the current process, where thoseenvironment 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 wrappingthreadpoolctl.threadpool_limits(1, "blas"). A no-op where no controllablenative library is loaded (e.g. Accelerate) and redundant inside a spawned
worker.
run_simulationwraps 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.
_boundary_block_from_transfer,_cascade_boundary_pair): reject a non-finite slab transfer matrix orcascaded block with a
ValueErrorbefore it reachesnp.linalg.solve,which crashes rather than raising on some LAPACK builds. The cascade's
existing post-check is escalated from a
logger.warningto that same error soa bad block can never propagate into the next solve. This part applies to both
solvers.
threadpoolctlpromoted from an indirect dependency (via SciPy) to adirect one.
examples/simulation/neviere_grazing_stability/— sweeps a coated Mo/B4Cgrating from a Bragg angle down to 0.01 deg in p-polarization and asserts
every diffraction efficiency stays finite; exits cleanly.
tests/unit/test_neviere.py(BLAS pin applied forneviereand not forrcwa; 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=Trueon thetqdmprogress bars so they re-fit on terminalresize, plus the
DummyProgresstest-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 lessand 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
tests/unit+tests/smoke: 486 passed, 5 skipped.neviere_grazing_stabilityexample runs clean locally (Accelerate). Needsa confirmation run on the Linux/OpenBLAS box that originally crashed:
python examples/simulation/neviere_grazing_stability/neviere_grazing_stability.pythen 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