Skip to content

Add Fortran interface files and tests for compact and MKL-style APIs - #48

Open
ivan-pi wants to merge 11 commits into
mainfrom
claude/stoic-mendel-0vmfic
Open

ivan-pi wants to merge 11 commits into
mainfrom
claude/stoic-mendel-0vmfic

Conversation

@ivan-pi

@ivan-pi ivan-pi commented Sep 13, 2026

Copy link
Copy Markdown
Owner

Summary

This PR adds comprehensive Fortran interface support for both the portable C API (cqr_compact.h) and the MKL-style API (cqr_mkl_ext.h). The interfaces are provided as dual-form include files that read identically as fixed-form and free-form Fortran source, enabling Fortran users to call the library's batched QR, Cholesky, LDL^T, triangular solve, and least-squares solve routines.

Key Changes

  • Fortran Interface Files:

    • include/cqr_compact.fi: Portable C API interfaces with explicit interleave width V
    • include/cqr_mkl_ext.fi: MKL-style API interfaces (LP64, MKL_INT as c_int)
    • include/cqr_mkl_ext_ilp64.fi: ILP64 variant with 64-bit MKL_INT (c_long_long)
    • include/cqr_mkl_enums.fi: Shared MKL enumerators (layout, uplo, side, trans, diag, pack format)
  • Dual-Form Layout Convention:

    • All .fi files use a portable column layout readable as both fixed-form (72-column) and free-form Fortran
    • Statements occupy columns 7–72; continuation lines use column-73 & (free-form marker) continued by column-6 & (fixed-form marker)
    • Comprehensive documentation in each file explaining the layout and usage
  • Precision-Generic Interfaces:

    • Each routine pair (double/single) is grouped under a generic name (e.g., cqr_mkl_geqrf_compact)
    • Generic resolution selects the specific based on the kind of real actual arguments
    • Generics require rank-1 actuals; specific names accept any rank via sequence association
  • Test Suite:

    • tests/test_cqr_fortran_compact.F90: Free-form test of portable API
    • tests/test_cqr_fortran_compact_fixed.f: Fixed-form proof that .fi files read as fixed-form
    • tests/test_cqr_fortran_mkl.F90: Free-form test of MKL-style API (LP64 and ILP64 variants)
    • tests/test_cqr_fortran_mkl_fixed.F: Fixed-form proof for MKL-style API
    • tests/test_cqr_fortran_util.inc: Shared scaffolding (fill, pack, check utilities)
    • tests/check_fi_twins.py: Verification that LP64 and ILP64 interface files differ only in integer kinds
  • Build Integration:

    • New CMake option -DCQR_BUILD_FORTRAN_TESTS=ON (opt-in, requires Fortran compiler)
    • Fortran tests compiled once per work precision (single/double)
    • Fixed-form tests compiled at standard 72-column line length to verify dual-form layout
    • CI workflow added for gfortran testing with both LP64 and ILP64 MKL builds
  • Documentation:

    • Updated README.md and .claude/CLAUDE.md with Fortran interface information
    • Each interface file includes detailed comments on usage, workspace contracts, and the dual-form convention

Implementation Details

  • Workspace Contract: Like MKL's own compact routines, the Fortran interfaces skip argument checking. Each routine sizes its work via lwork = -1 query; undersized or shared work arrays cause silent overruns (documented in cqr_mkl_ext.h).

  • Enumerator Mapping: MKL enumerators (C int enums) are transcribed as Fortran enum, bind(c) blocks with identical values from MKL 2020.0.4 and oneMKL 2026.1.

  • ILP64 Support:

https://claude.ai/code/session_01PEJMMExeRTHLcTZ9dopKWq

Two Fortran INCLUDE files under include/, one per public header:
cqr_compact.fi (the portable C API, integer(c_int) functions returning
the LAPACK-style info) and cqr_mkl_ext.fi (the MKL-style subroutines,
with the MKL_LAYOUT / MKL_UPLO / MKL_SIDE / MKL_TRANSPOSE / MKL_DIAG /
MKL_COMPACT_PACK enumerators transcribed from mkl_types.h as
enum, bind(c), and integer(c_int) for LP64 MKL_INT). Specific names
only, every interface body bind(c) with its own iso_c_binding use
statement and implicit none.

Each file reads identically as fixed-form and free-form source:
statements in columns 7-72, a continued line ends with '&' in column
73 (past fixed form's statement field, a continuation in free form)
and its continuation carries '&' in column 6 (a continuation in fixed
form, stripped in free form). Fixed-form includers must stay at the
standard 72-column line length.

The tests call every entry point of both APIs from free-form sources
(.f90) and include the .fi files from fixed-form ones (.f) -- compiling
both is what enforces the dual-form layout. The compact buffers are
built with RESHAPE alone: the first reshape splits the batch index
into (lane, group) and PAD fills the partial final group (identity
matrices for A, zero columns for B), the second one's ORDER permutes
the lane innermost, which is the compact layout; nmat = 3 leaves a
padding lane at every width, so the padded-group convention is
exercised. The MKL-style tests pack by hand and so link only
cqr_mkl_ext, no MKL.

New opt-in CMake option CQR_BUILD_FORTRAN_TESTS (needs a Fortran
compiler; tests carry the "fortran" CTest label) and a CI job running
gfortran against MKL ON and OFF. The session-start hook now also
installs gfortran.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PEJMMExeRTHLcTZ9dopKWq
Following MKL's own _lp64/_ilp64 interface-file convention (the
mkl_*_omp_offload_{lp64,ilp64}.f90 pattern of newer oneMKL includes),
the ILP64 selection is the includer's: include exactly one of
cqr_mkl_ext.fi (LP64, the default MKLCompact_INTERFACE) and
cqr_mkl_ext_ilp64.fi (an ilp64 library build). In the ILP64 file the
MKL_INT dummies and info are integer(c_long_long); the enum arguments
stay integer(c_int), since a C enum does not widen under ILP64. The
two files differ only in the header comment and those integer kinds,
and must be edited in step; both file headers say so. The portable
API (cqr_compact.fi) uses plain C int and needs no variant.

The MKL Fortran tests become preprocessed sources (.F90/.F): one
CQR_ILP64 macro, defined by CMake when MKLCompact_INTERFACE=ilp64,
switches the include file and the integer kind of every MKL_INT
actual argument, so the same tests run against either build of the
library. CI's fortran-interface job gains the ilp64 leg.

The full suite (C++ tests included) passes under
-DMKLCompact_INTERFACE=ilp64 with both gcc and clang.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PEJMMExeRTHLcTZ9dopKWq
The free-form test programs no longer carry a test_double/test_single
pair. Each declares generic interfaces over the include file's
specific names (interface geqrf_compact / procedure sgeqrf_compact,
dgeqrf_compact -- the pattern a caller who wants generic entry points
would write) and one test body in the work precision wp, which the
CQR_SINGLE preprocessor guard sets to c_float (default c_double).
CMake compiles each source twice, into _d and _s executables, so both
sets of entry points stay covered; the interleave width and tolerance
follow wp (storage_size / epsilon). The fixed-form includers keep one
precision: their job is only the dual-form layout.

Generic resolution requires the actual arguments' rank to match the
assumed-size dummies (sequence association does not relax rank there,
unlike a call to a specific), so the compact buffers in the tests are
rank-1, with the RESHAPE packing flattening its permuted 4-D image.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PEJMMExeRTHLcTZ9dopKWq
The include files now group each d/s pair of interface bodies inside
a named generic interface block (interface geqrf_compact ... the two
bodies ... end interface), so including a .fi declares both the
specific names and a precision-generic one -- geqrf_compact,
cqr_mkl_geqrf_compact, ... -- resolved by the kind of the real actual
arguments. The bodies are unchanged; a generic interface block that
contains them declares the specifics exactly as the plain block did.
The dual-form layout is untouched (the new lines need no
continuation), and F77 compatibility was never at stake: the names
and the interface blocks are already Fortran 90, only the *source
form* of the includer is free.

One caveat, now documented in the headers: generic resolution
matches rank against the assumed-size dummies (sequence association
applies only to a call to a specific name), so the compact buffers
must be rank-1 when calling through the generics.

The free-form tests drop their local generic blocks and call the
shipped generics, which puts the generic declarations themselves
under test in every precision/interface leg. The ILP64 twin is
re-derived and still differs from cqr_mkl_ext.fi only in its header
and the MKL_INT integer kinds.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PEJMMExeRTHLcTZ9dopKWq
The PAD argument of RESHAPE is placed along the ORDER-permuted
traversal, not in result element order, so the two-stage (and
three-stage) reshape idiom collapses to a single call:

    packed = reshape(dense, shape(packed), pad=pad, order=[2,3,1,4])

ORDER interleaves the lanes -- which is the compact layout -- and
PAD's copies fill the padding lanes of the partial final group along
the same walk (identity matrices for A, zero columns for B); verified
against the two-stage result before switching. The unpack helper is
gone too: the padding lanes solve identity systems with zero
right-hand sides, so the computed compact solution compares directly
against pack_c of the exact x, padding included.

The compact buffers get their natural shape back, (V, rows, cols,
ngroups). Rank-changing sequence association does cover such an array
passed to the assumed-size dummies -- but only in a call to a
specific name: generic resolution is TKR-exact, and gfortran rejects
both a whole higher-rank array and a starting-array-element actual
through the generics (sequence association applies only once a
specific has been chosen). The calls therefore go through rank-1
pointer views, p(1:size(a)) => a; the interface-file headers, README
and CLAUDE.md now state the rule precisely.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PEJMMExeRTHLcTZ9dopKWq
The portable API carries V, the leading dimensions and the extents as
arguments, so its .fi can declare the array dummies in the layout's
own shape instead of flat assumed-size: matrices ap(v, ldap, ncols, *)
with the group count ceil(nm/v) left assumed (the caller pads a
partial final group up to a multiple of v), tau taup(v, min(m, n), *).
The named extents document the tuned column-major (and side = 'L')
case; other layout or side choices swap in-matrix extents of the
caller's array, which is harmless -- an assumed-size dummy's extents
never have to agree with the actual's, only the rank (for generic
resolution) and storage size matter. Naturally shaped
(V, rows, cols, ngroups) arrays now go through the generics as they
are, and the portable test drops its rank-1 pointer views; a flat
buffer still associates in a call to a specific name.

The MKL-style interfaces stay rank-1 assumed-size: there V is a
runtime function of the format enumerator, not a dummy, so the
compact shape is not expressible -- cqr_mkl_ext.fi's header now says
so, and its generics keep taking rank-1 actuals (the ILP64 twin's
declarations are untouched, keeping the two in step).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PEJMMExeRTHLcTZ9dopKWq
Follow LAPACK's own convention for flag-dependent extents -- TRSM
declares A(LDA,*) precisely because A's column count depends on SIDE
-- lifted by the lane dimension: matrices are ap(v, ldap, *), the
assumed extent absorbing the columns of every group, and tau is
taup(v, min(m, n), *) with the groups assumed. Only the strides are
named, and strides do not move with the layout or side flags, so
every named extent is exact for every flag choice; the previous
declarations named the column-major / side='L' column count, which
read as a documented shape but was wrong for layout 'R' or side 'R'.

The array dummies are uniformly rank-3 as a result, so the portable
generics take rank-3 actuals: a rank-4 (V, ld, cols, groups) batch
goes through a rank-3 pointer view folding (columns, groups) into
the assumed extent, p(1:v, 1:ld, 1:nc*ng) => a, which is what the
portable test now does (tau needs no view -- it is (V, k, groups)
already). Specific-name calls still accept any rank by sequence
association. The MKL-style interfaces are unchanged, rank-1
assumed-size, V being a runtime function of format there.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PEJMMExeRTHLcTZ9dopKWq
The (v, ldap, *) declarations were rank-3 against a conceptually 4-D
batch: neither the storage truth (a rank-1 interleaved sequence) nor
the conceptual one. A fully honest shaped declaration would need
rank 4 with merge()-computed, flag-dependent extents in a public
dual-form header -- not worth it. So the compact buffers go back to
plain assumed-size ap(*), and the header now records why: their
in-memory extents depend on the layout and side flags, so no fixed
shape is right for every call (LAPACK's TRSM declares A(LDA,*) for
the same reason).

Both APIs now share one rule -- generics take rank-1 actuals, a flat
buffer or a rank-1 pointer view p(1:size(a)) => a; specifics accept
any rank by sequence association -- and the moot lp64/mkl asymmetry
note leaves cqr_mkl_ext.fi. The portable test keeps its naturally
shaped buffers and single-reshape packing, calling through rank-1
views again; declaration bodies in cqr_compact.fi are back to
byte-identical with the pre-shape state, only the header text moved.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PEJMMExeRTHLcTZ9dopKWq
Interface files:
- The mkl_types.h enumerators are transcribed once, in the new
  cqr_mkl_enums.fi, and pulled into both MKL-style interface files
  through a nested INCLUDE -- the one hand-copied, silently-driftable
  region of the lp64/ilp64 twins is now single-sourced.
- tests/check_fi_twins.py (the fortran_fi_twins CTest) enforces the
  twins' invariant by canonicalizing both files (continuations joined,
  comments dropped, c_long_long mapped to c_int, use-lists and
  integer-value declarations compared as name sets) and diffing:
  "nothing else may differ" is machine-checked instead of prose.
- Split declaration lines merged: trsm's five character flags fit one
  line, geqrf's ap and work share intent(inout), and the ILP64 file
  groups each body's integer dummies as one c_int (enums) line plus
  one c_long_long (MKL_INT) line -- the line a reader scans to see
  what widened.

Tests:
- The byte-identical scaffolding of the two free-form tests (check,
  fill, pack_c) moves to tests/test_cqr_fortran_util.inc, INCLUDEd
  after CONTAINS so wp and the batch parameters host-associate; fill
  now also hands out the pack paddings. pack_c takes assumed-shape
  arguments, dropping the ncol dummy whose kind differed between the
  two tests.
- The MKL test now uses the lwork = -1 queries it asserts: each
  routine's workspace is allocated from its own query into a
  1-element probe, one buffer per routine, instead of one shared
  hand-sized array -- the exact pattern the workspace contract
  forbids no longer appears in the tree's only Fortran exemplar. The
  format-error check passes a 1-element placeholder (the buffer is
  never touched).
- The fixed-form tests index their buffers as (lane, row, column)
  through the specifics' sequence association instead of hand-
  linearized offsets, use an epsilon-based tolerance, and their
  comments no longer claim exactness while testing to a tolerance.
- CMake pins the fixed-form targets at the 72-column line length the
  dual-form layout requires (GNU/Intel/LLVMFlang flags) instead of
  relying on compiler defaults, and registers each test target on the
  list where it is created. Stale .f90 cross-references fixed.

Skipped by choice: sharing between the two fixed-form smoke tests
(self-containment is the property they prove), hoisting a reset()
helper (costs block-local readability), and width/format constants in
the .fi files (new public API surface, a separate decision).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PEJMMExeRTHLcTZ9dopKWq
Merge main (which added ?potrs_compact, ?posv_compact and
?orgqr_compact to both C APIs) and extend the Fortran layer to match,
so the .fi files again cover every public C entry point:

- cqr_compact.fi: orgqr_compact, potrs_compact and posv_compact
  generic blocks (potrs/posv mirror sytrsnp/sysvnp; orgqr takes the
  geqrf factor and tau, in place).
- cqr_mkl_ext.fi: the cqr_mkl_* counterparts (orgqr with the usual
  work/lwork pair); cqr_mkl_ext_ilp64.fi re-derived, and the
  fortran_fi_twins check confirms the twins still differ only in
  their MKL_INT kinds (193 canonical statements).
- Both free-form tests: potrf + potrs solved against the known x and
  the fused posv checked bit-identical to the two calls (the same
  pattern as the sysvnp block, per the documented guarantee); orgqr's
  explicit Q checked against ormqr('N') applied to a packed identity
  batch, with orgqr's own lwork = -1 query sizing its workspace in
  the MKL test.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PEJMMExeRTHLcTZ9dopKWq
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.

2 participants