ADR 0009 - LAPACKE translation layer: raw-Fortran driver implementations
1. Context
The BLAS device backends call Fortran LAPACK through raw FFI. LAPACK is
column-major only; routines need workspace arrays with per-routine sizing
rules; and callers hand the driver tier row-major buffers. The C ecosystem
solved the same problems in LAPACKE - a two-tier lapacke_xxx /
lapacke_xxx_work interface whose _work tier bridges row-major arguments
into Fortran and whose high tier sizes and allocates the workspace.
LAPACKE is not universally available: only a few vendored BLAS libraries
export it, and even OpenBLAS - the most commonly used - cannot be assumed to
carry the LAPACKE symbols (commonly distributed builds, e.g. Debian or conda,
ship CBLAS and the raw Fortran LAPACK without LAPACKE).
rstsr-blas-traits/driver_impl/ therefore holds two implementations
of one driver API - driver_impl/lapacke/, a thin passthrough to a vendor
LAPACKE, and driver_impl/lapack/, which re-expresses the same LAPACKE logic
in Rust over the raw Fortran entry points. Translation, not invention, is what
makes the raw-Fortran tree reviewable and deterministic: an agent producing
driver code must land on the same implementation every time.
2. Decision
2.1 1. Scope
The LAPACKE translation layer is the tree
rstsr-blas-traits/driver_impl/lapack/**, one file per LAPACK routine. Each
file is a direct translation of that routine's LAPACKE source
(LAPACKE/src/lapacke_x*_work.c plus lapacke_x*.c), with
driver_impl/lapacke/** as its passthrough sibling for the vendors that
export LAPACKE. The layer is raw FFI: it receives pointers, blas_int
dimensions, and one FlagOrder, and returns a blas_int. It knows nothing of
rstsr tensors, layout preference, or error types - those belong to the LAPACK
interface layer above it (ADR-0010), which reaches it through the
XxxDriverAPI trait.
The tree is compiled into every vendor crate by directory symlink
(driver_impl/lapack/ into all five; driver_impl/lapacke/ into MKL and
OpenBLAS only).
2.2 2. Two tiers in one file
Each file mirrors LAPACKE's split:
pub unsafe fn xxx_work(order, ..) -> blas_int- thelapacke_xxx_workequivalent; carries the layout branch.- the
XxxDriverAPIimplementation - thelapacke_xxxhigh-level equivalent: query, allocate, compute. The trait definitions live insrc/lapack_*and are fixed.
2.3 3. Layout branch
The _work tier branches on the call's single order (the Rust counterpart
of LAPACKE's matrix_layout):
- ColMajor - call the Fortran entry point directly with the caller's lds.
- RowMajor - derive the output-shape table (
nrows_u,ncols_u,nrows_vt,ncols_vt) and template ldsMAX(1, rows); check the incoming lds; short-circuit thelwork = -1query; allocate column-major temporaries; transpose the input in, call, transpose the outputs back.
Both calls (query and compute) route through xxx_work, the query issued with
the lds the compute call will use.
2.4 4. Translation conventions
duplicate_itemsupplies the four dtypes per routine (realf32/f64, complexComplex<f32>/Complex<f64>);numsupplies the complex arithmetic traits.- The driver trait is generic over
T: BlasFloat, so one implementation serves bothTandComplex<T>whenever the two Fortran argument lists are parallel - complex real-scalar outputs (s,superb) areT::Real. Routines whose real and complex Fortran signatures genuinely differ (real?geevreturnswr/wi; complex returnsw) do not fit one signature and need an explicit, per-routine decision. blas_intis the FFI integer throughout (i32, ori64underilp64).
2.5 5. Workspace protocol
Query (lwork = -1) where LAPACKE queries (gesvd, getri, dsysv, syev, sygv;
double query lwork+liwork for syevd/sygvd); documented fixed formulas where
LAPACK exposes no query (gesdd iwork = MAX(1, 8·min(m,n)), allocated before
the work query; complex gesvd rwork = MAX(1, 5·min(m,n)); the complex gesdd
lrwork formula). The gesvd superb superdiagonal is copied out after the
compute call (work[i+1] real, rwork[i] complex, for i < min(m,n)−1).
Hardening, all constant-budget:
MAX(1, ·)around every formula term;lwork == 0 → 1after each read-back.- Checked integer conversion of the query value, with a one-ulp growth for
f32-mantissa results - LAPACKE's truncating(lapack_int)cast can round those down. - No fudge factors (scipy's
1.01is vendor-specific and rejected). - Complex routines needing rwork at query time allocate it before the query.
2.6 6. Error channel
The thunk returns raw LAPACK info. Negative values carry the routine's
Fortran argument numbering (verified against the reference-LAPACK source),
so the reported argument index can be checked directly against the Fortran
documentation. LAPACKE numbers negative info one higher because it inserts
matrix_layout as argument 1; the lapack/ tree has no such argument and
does not apply that adjustment - its row-major leading-dimension checks are
written directly in Fortran indices.
Allocation and bridge failures reuse LAPACKE's sentinel values -1010 (work)
and -1011 (transpose), matching LAPACK_WORK_MEMORY_ERROR /
LAPACK_TRANSPOSE_MEMORY_ERROR; real LAPACK info magnitudes never reach 1010.
The interface layer decodes them (ADR-0010).
2.7 7. Divergences from LAPACKE
- No NaN pre-check. LAPACKE's high tier optionally scans its inputs for NaNs and returns early; this tree performs no scan (numpy precedent).
- Hardened query read-back (§5), where LAPACKE truncates.
- Negative-
infonumbering (§6), unshifted from LAPACKE's. - Full-matrix bridges only - no triangle-only transpose variants.
- 2-D only at this tier.
lapack/ and lapacke/ remain two implementations of one driver API: they
produce the same outputs and the same info for every input LAPACK itself
handles, and differ only in the places listed above.
3. Considered
- Bridge the layouts in the interface layer and keep the driver column-major. Rejected: it duplicates LAPACKE's row-major logic (shape tables, template lds, per-output transpose conditions) at every call site and forfeits the per-branch query short-circuit; the translation layer exists to hold that logic once, per routine.
- Depend on a vectorized LAPACKE everywhere. Not possible: most vendored BLAS libraries do not export LAPACKE.
- Triangle-only transpose bridges (a
dsy_transanalogue) or the scipy Cholesky uplo-flip. Rejected for uniformity: one full-matrix bridge shape everywhere, at O(N²) copy against O(N³) compute. - Adopt LAPACKE's negative-
infoshift. Rejected: it exists only because LAPACKE insertsmatrix_layoutas argument 1; without that argument the Fortran indices are the ones a reader can look up.
4. Consequences
- A routine is reviewed against its LAPACKE source line by line, with the
lapacke/passthrough as the behavioural oracle. - The row-major path performs transposed copies; the threading policy for those copies is not decided here and is the likeliest point of refactor.
- A routine whose real and complex Fortran signatures are not parallel cannot share an implementation and needs an explicit decision.