Skip to content

Instantly share code, notes, and snippets.

@rgommers
Created August 22, 2026 20:47
Show Gist options
  • Select an option

  • Save rgommers/42e30301bec51c55143cd9c31fb0dfa6 to your computer and use it in GitHub Desktop.

Select an option

Save rgommers/42e30301bec51c55143cd9c31fb0dfa6 to your computer and use it in GitHub Desktop.
A summary of issues surfaced by the experiment of rewriting scipy.linalg's f2py wrappers to Cython (see https://github.com/scipy/scipy/compare/main...rgommers:scipy:f2py-removal)

Pre-existing fblas/flapack wrapper issues surfaced by the f2py-removal experiment

Audit of the f2py-removal branch (46 commits, branched off main at 986b8b3035, Mar 2026), asking: which of the problems found during that effort are defects in the existing wrappers and surrounding scipy code — i.e. still present in main today and never upstreamed?

Sources: all 46 commit messages/bodies, f2py_removal_plan.md, f2py_removal_session_summary.md, the diff against the merge base (the diff against current main is misleading — main has moved on by ~5 months), and verification of each candidate against current main sources. Nothing was executed; all findings are from source reading.

Bottom line

The branch is not a repository of unreported f2py-wrapper bugs. 42 of its 46 commits fix defects in the new pyf parser / Cython generator built for the experiment: missing pre-call pivot increments, Python-3 b'V'[0] bytes indexing, the lwork == -1 sentinel clash, _lwork scalar/array confusion, callback arity, output ordering, and so on. Every segfault chased during that effort — integrate Radau/BDF, lobpcg, test_batch, gejsv, cheevd — root-caused to the new generator, not to the .pyf.src specs.

What genuinely belongs to main is the short list in sections A and B below. Sections C–E record constraints, disproven suspicions, and loose ends, mostly so they aren't re-investigated.

Scope note: main no longer builds BLAS with f2py — fblas*.pyf.src is gone and _fblas / _fblas_64 are C++ (scipy/linalg/src/blas/*.cpp). LAPACK is still f2py (flapack*.pyf.src plus flapack_64.pyf.src), so only the LAPACK-side findings are live.


A. Defects in main's .pyf.src specs

A1. intnet(in) typo in both gtsvx variants

scipy/linalg/flapack_gen_tri.pyf.src:136 and :173:

<ftype2>  intnet(in), dimension(n) :: d     ! line 136  (real gtsvx)
<ftype2c> intnet(in), dimension(n) :: d     ! line 173  (complex gtsvx)

intnet is not an f2py attribute; crackfortran drops it silently, so d falls back to f2py's default (input) intent and observable behaviour is currently unchanged. Still worth fixing: the declared intent is not the applied one, and this is exactly the failure mode that bit the reimplementation (arrays with no effective intent got no dtype conversion, so garbage was passed to LAPACK). These are the only two attribute typos in ~1770 declarations across all .pyf.src files (verified by an attribute-token scan of every declaration line).

A2. Dependency-spec hygiene (all in main, all benign today)

  • flapack_gen_tri.pyf.src:146 and :183 — integer intent(hide) :: ldx = MAX(1, shape(b, 0)) has no depend(b), unlike the adjacent ldb, which does. f2py infers the dependency; a non-f2py generator does not, which is what forced the branch's "defer hidden args that reference output array shapes" workaround.
  • flapack_other.pyf.src:27 (gejsv) — depend(a), check(m>=n), depend(n): depend() twice.
  • flapack_gen_banded.pyf.src:124 and :163 — same duplicate-depend pattern.
  • flapack_other.pyf.src:2239 — integer intent(in,hide) :: ldc = max(1,shape(c,0)), no depend(c).

B. Defects in the Python layer around the wrappers (still in main)

B1. cho_factor returns a 0-d ndarray instead of a bool

scipy/linalg/_decomp_cholesky.py:210-212:

batch_shape = a.shape[:-2]
ret_lower = np.tile(lower, reps=batch_shape)

np.tile always returns an ndarray, so for ordinary unbatched input cho_factor(A)[1] is array(False) rather than False, contradicting the documented (array, bool) return. Low impact, trivial fix (np.tile(...) if batch_shape else lower).

B2. Batch-shaped lower array is passed straight into potrs

scipy/linalg/_decomp_cholesky.py:285-288. In batch mode cho_factor returns lower tiled to batch_shape. _cho_solve is decorated with @_apply_over_batch(('c', 2), ('b', '1|2')), so lower is an "other" argument and the entire batch-shaped boolean array — not a per-slice scalar — is handed to potrs(..., lower=lower) for every slice.

This works only because f2py's scalar converter silently accepts an ndarray where an integer is expected. Any non-f2py wrapper rejects it; on the branch it broke 12 cho_solve batch tests and had to be papered over with bool(np.asarray(lower).flat[0]) inside _cho_solve.

The cleaner fix, identified during that session but never implemented, is in the batch framework: inherently-scalar flags (lower, trans, …) should stay scalar instead of being broadcast to the batch shape. This is the one item here that materially blocks moving the LAPACK wrappers off f2py.

C. f2py-specific behaviours that main relies on (constraints, not bugs)

Each of these cost real debugging time and will resurface in any replacement:

  1. Silent ndarray → int-scalar coercion for scalar flags — see B2.
  2. Read-only inputs to in-place post-processing. _apply_over_batch passes broadcast_to views; f2py works on its own copies, so main is fine, but a wrapper that decrements pivot arrays in place hits read-only buffers.
  3. 0-D and 1-D inputs auto-reshaped for 2-D parameters. scipy/linalg/tests/test_lapack.py:1954 calls gejsv(1.) with a scalar and asserts u.shape == (1, 1); other tests pass a 1-D b and expect a 1-D result back. Both are f2py conveniences that the tests encode as API contract.
  4. check() directives are load-bearing for the test suite. ~36 test_fblas tests (test_x_bad_size, test_stride_assert, …) assert that invalid input raises, originally with f2py's module-level error type. main has relaxed these to ValueError as part of the C++ BLAS port; the LAPACK half still depends on f2py-generated checks.
  5. Wrappers used as class attributes. test_fblas.py does blas_func = fblas.saxpy at class scope. f2py fortran objects and C PyCFunctions don't implement the descriptor protocol, but Cython def functions do and silently become bound methods (self lands in the first argument). Any Cython-based replacement needs staticmethod() in that file.

D. Suspicions that turned out to be false

  • sspr2/dspr2/chpr2/zhpr2 and stbsv "missing from the pyf files." They are present — fblas_l2.pyf.src:317, subroutine <prefix><s,s,h,h>pr2. The branch's parser lost them because template expansion emits end subroutine<prefix>foo with no space and the regex used \b.
  • Four cython_blas utility routines "missing from f2py." Confirmed never exposed by f2py either.
  • sgttrs argument order. An attempted reorder (b before ipiv) was wrong and was reverted; the pyf order matches Fortran.
  • tzrzf_lwork requires m <= n; tgsen_lwork/trsen_lwork "need arrays". These came from the branch's own generic _lwork smoke calls, not from scipy's tests — test_lapack.py:1122 and :1198 correctly use m, n = 10, 15. Not a scipy defect.

E. Loose ends from the experiment (branch-only, not upstream bugs)

  • test_gttrf_gttrs segfaults under pytest on the branch while passing in a direct interpreter; concluded to be latent corruption from some other generated wrapper. Never root-caused.
  • ILP64 variants (_pyblas_64/_pylapack_64) were never implemented.
  • _cython_signature_generator.py still imports numpy.f2py.crackfortran (dev-only tool, also true in main).

Suggested upstream follow-ups

  1. intnet → intent in flapack_gen_tri.pyf.src:136,173. One-liner, no behaviour change.
  2. cho_factor scalar lower (B1), plus a test asserting an unbatched cho_factor(A)[1] is a Python bool.
  3. Batch framework scalar passthrough (B2) — the substantive one. Worth doing on its own merits, and a prerequisite for taking the LAPACK wrappers off f2py.
  4. Optionally fold the A2 hygiene items into the same .pyf.src cleanup.

Verification for any of the above: pixi run build, then pixi run -e test spin python --no-build -- -m pytest scipy/linalg/tests/test_lapack.py -k gtsvx and scipy/linalg/tests/test_batch.py -k cho.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment