Skip to content

[Enzyme-JAX] Add Fortran support via fortran_call - #3821

Open
wsmoses wants to merge 5 commits into
mainfrom
fortran-support
Open

wsmoses wants to merge 5 commits into
mainfrom
fortran-support

Conversation

@wsmoses

@wsmoses wsmoses commented Nov 3, 2026

Copy link
Copy Markdown
Member

Overview

This PR adds native Fortran support to Enzyme-JAX via a new fortran_call API, enabling direct execution, JIT compilation, and automatic differentiation (forward-mode JVP, reverse-mode VJP/grad) of Fortran routines in JAX.

Key Changes

  1. In-Process Flang Compilation (flang_compile.h, flang_compile.cc):

    • Compiles Fortran source code directly to LLVM IR in-memory using LLVM Flang (Fortran::frontend::CompilerInstance, CompilerInvocation).
    • Runs Flang's MLIR FIR-to-LLVM lowering pipeline (createMLIRToLLVMPassPipeline).
    • Bootstraps standard intrinsic module headers (iso_c_binding, iso_fortran_env, ieee_arithmetic, etc.) from an in-memory virtual filesystem.
    • Inspects FIR function signatures to extract parameter kinds (raw pointers, value arguments, box/descriptors), shapes, element types, and intent.
    • Demangles lowered symbol names (e.g. FIR _QPfoo to foo_) to match LLVM IR export definitions.
  2. Fortran ABI & Calling Convention Adapter (clang_compile.cc):

    • Synthesizes an inline C++ wrapper conforming to Enzyme's call contract:
      • Directly passes scalars by-value when marked with VALUE.
      • Passes raw pointers (void*) for standard dummy arrays/scalars.
      • Constructs standard ISO Fortran CFI_cdesc_t descriptors for assumed-shape arrays, allocatables, and pointer dummy arguments.
      • Reverses array dimensions and byte strides so JAX row-major C-contiguous memory maps directly to Fortran column-major layout with zero memory copy.
      • Wraps non-differentiable integers and booleans with enzyme_const so Enzyme AD focuses strictly on differentiable floating-point tensors.
  3. Static Flang Runtime Linking (enzyme_call.cc, BUILD):

    • Embeds static @llvm-project//flang-rt:runtime into enzyme_call via flang_rt_archive.inc.
    • Registers an LLJIT StaticLibraryDefinitionGenerator with the archive so all standard Fortran runtime routines (I/O, descriptors, math intrinsics) resolve seamlessly inside the JIT execution session.
  4. Python Frontend & AD Integration (primitives.py, __init__.py):

    • Exports fortran_call from enzyme_ad.jax.
    • Supports subroutines (multiple outputs) and scalar-returning functions.
    • Enhances enzyme_jvp and enzyme_vjp to handle non-differentiable outputs (float0 / ad.Zero) and multiple outputs cleanly.
  5. Comprehensive Test Suite (test/fortran_test.py, test/BUILD):

    • Tests pure scalar functions (VALUE and reference arguments).
    • Tests vector and matrix operations (scale, add, GEMV/GEMM).
    • Tests assumed-shape arrays using Fortran 90 modules and array descriptors.
    • Tests subroutines with multiple return arrays and non-differentiable configuration inputs (dimensions, scaling factors).
    • Tests both forward-mode (jax.jvp) and reverse-mode (jax.grad / jax.vjp) automatic differentiation with numerical finite difference validation.
    • Tests integration with jax.jit.

Verification

  • Bazel tests pass:
    bazel test --config=public_cache --repo_env=CC=/usr/bin/clang-18 --test_env=JAX_PLATFORMS=cpu --test_output=errors //test:fortran_test //test:test //test:testffi
    • //test:fortran_test: PASSED
    • //test:test: PASSED
    • //test:testffi: PASSED
  • buildifier checked and clean.
  • git clang-format clean on all touched lines.
  • ASCII encoding clean (ord(c) < 128 on all files).

Add native Fortran support to Enzyme-JAX through fortran_call, enabling seamless execution, JIT compilation, and automatic differentiation (forward-mode JVP, reverse-mode VJP/grad) of Fortran routines directly within JAX.

Key changes:
- In-process Flang compilation: Compile Fortran source directly to LLVM IR using LLVM Flang frontend (Fortran::frontend::CompilerInstance / CompilerInvocation) and FIR pass pipeline (createMLIRToLLVMPassPipeline), with in-memory standard intrinsic module headers (iso_c_binding, iso_fortran_env, ieee_arithmetic, etc.).
- ABI and calling convention mapping: Analyze FIR signatures to automatically handle:
  * Raw pointers (void*) for standard dummy arrays/references.
  * Pass-by-value (VALUE attribute) scalar parameters.
  * Standard ISO Fortran descriptors (CFI_cdesc_t) for assumed-shape arrays, allocatables, and pointer dummy arguments.
  * Reverse multi-dimensional array bounds and byte strides to map row-major JAX arrays to Fortran column-major layout seamlessly with zero-copy.
  * Subroutines (multiple outputs) and scalar-returning functions.
- Static Flang runtime integration: Bundle and link static @llvm-project//flang-rt:runtime via LLJIT StaticLibraryDefinitionGenerator so Fortran runtime symbols (I/O, descriptors, intrinsics) resolve during kernel execution.
- Python API: Export fortran_call in enzyme_ad.jax, backed by Language::FORTRAN (lang=3). Enhance JVP/VJP rules for multi-output primitives and non-differentiable integers/booleans.
- Comprehensive test suite: Unit tests in test/fortran_test.py covering scalar operations, vector/matrix math, assumed-shape descriptors, subroutines, functions, non-differentiable inputs, JIT compilation, and forward/reverse automatic differentiation.
… for libc++

Fix CI failures:
- Reformat primitives.py and test/fortran_test.py with black==26.10.1 to pass Python-Black lint workflow.
- Add patches/llvm_flang_features.patch and wire into workspace.bzl to include <unordered_map>, <string>, and <array> in flang/include/flang/Support/Fortran-features.h when building with libc++ on macOS / Darwin.
Guard Microsoft CRT-specific Bessel functions (_j0, _j1, _jn, _y0, _y1, _yn) with #if defined(_MSC_VER) instead of #ifdef _WIN32 in flang/lib/Evaluate/intrinsics-library.cpp, fixing compilation errors when building for Windows with MinGW toolchains.
@codecov

codecov Bot commented Nov 3, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 30.79%. Comparing base (8f13d33) to head (0f8ad4e).
⚠️ Report is 24 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #3821      +/-   ##
==========================================
+ Coverage   29.15%   30.79%   +1.64%     
==========================================
  Files         249      249              
  Lines       54687    55898    +1211     
==========================================
+ Hits        15942    17214    +1272     
+ Misses      38745    38684      -61     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@joewallwork joewallwork left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems like it will be very handy, thanks @wsmoses. It would've been nice to have a heads-up that you were going to do this, since we had already been working on it, but thanks for making me aware of this PR.

I can't claim to understand the internals of your implementation, since I'm not familiar with Enzyme-JAX. However, I've shared some feedback on the Fortran code and tests.

It would be good to follow this up by adding some docs and an example to the Fortran Guide on the main Enzyme website.

Comment thread test/fortran_test.py Outdated

class FortranCallTest(absltest.TestCase):
def test_explicit_shape_and_layout_f32_f64(self):
for dtype, kind in ((jnp.float32, "4"), (jnp.float64, "8")):

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we not use a map {jnp.float32: "4", jnp.float64: "8"}, since this is a direct correspondence?

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Better still, how about adding use, intrinsic :: iso_fortran_env, only: {kind} and using the map {jnp.float32: "real32", jnp.float64: "real64"}?

Comment thread test/fortran_test.py Outdated
Comment on lines +70 to +75
module my_fortran_mod
implicit none
contains
subroutine poly_2d(y, x)
real(8), intent(out) :: y(:, :)
real(8), intent(in) :: x(:, :)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again, how about

Suggested change
module my_fortran_mod
implicit none
contains
subroutine poly_2d(y, x)
real(8), intent(out) :: y(:, :)
real(8), intent(in) :: x(:, :)
module my_fortran_mod
use, intrinsic :: iso_fortran_env, only: real64
implicit none
contains
subroutine poly_2d(y, x)
real(real64), intent(out) :: y(:, :)
real(real64), intent(in) :: x(:, :)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah okay, I see that you have a different test for iso_fortran_env further down.

Comment thread test/fortran_test.py Outdated
x = jnp.array([[1.0, -2.0, 0.5], [3.0, 1.5, -1.0]], dtype=jnp.float64)
out_shape = [jax.core.ShapedArray(x.shape, jnp.float64)]

for proc_name in ("my_fortran_mod::poly_2d", "poly_2d"):

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the loop doing here? Testing that you can access the subroutine directly or from the module?

Comment thread test/fortran_test.py Outdated
Comment on lines +117 to +118
real(8) function weighted_sum(x, y) result(res)
real(8), intent(in) :: x(:), y(:)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As above, although I'm not sure whether you would be able to have a use statement here, given you have an orphan function. Ideally, this would be put inside a module, although I suppose Fortran codes in the wild don't necessarily follow best practices so we should test handling such procedures.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah okay, I see that you have a different test for iso_fortran_env further down.

Comment thread test/fortran_test.py Outdated
use iso_c_binding, only: c_int, c_double
implicit none
contains
subroutine axpy_c(y, a, x, n) bind(C, name="custom_axpy_c")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where is the C file for the custom_axpy_c implementation?

Comment thread test/fortran_test.py Outdated
Comment on lines +285 to +303
complex_source = """
subroutine cscale(y, z, alpha)
complex(8), intent(out) :: y(2)
complex(8), intent(in) :: z(2), alpha
y = alpha * z
end subroutine cscale
"""
z = jnp.array([1.0 + 2.0j, -3.0 + 0.5j], dtype=jnp.complex128)
alpha = jnp.array(0.5 - 1.5j, dtype=jnp.complex128)
(cy,) = jax.jit(
lambda z_, a_: fortran_call(
z_,
a_,
out_shapes=[jax.core.ShapedArray((2,), jnp.complex128)],
source=complex_source,
fn="cscale",
)
)(z, alpha)
np.testing.assert_allclose(cy, alpha * z, rtol=1e-12)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why isn't this a separate test? Or at least handled by a parameter

Comment thread test/fortran_test.py Outdated
)(z, alpha)
np.testing.assert_allclose(cy, alpha * z, rtol=1e-12)

def test_fixed_form_and_error_reporting(self):

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why aren't these broken down into separate tests?

@wsmoses

wsmoses commented Nov 4, 2026

Copy link
Copy Markdown
Member Author

This seems like it will be very handy, thanks @wsmoses. It would've been nice to have a heads-up that you were going to do this, since we had already been working on it, but thanks for making me aware of this PR.

I can't claim to understand the internals of your implementation, since I'm not familiar with Enzyme-JAX. However, I've shared some feedback on the Fortran code and tests.

It would be good to follow this up by adding some docs and an example to the Fortran Guide on the main Enzyme website.

Ah sorry, this is entirely AI generated. Per our call Monday where you mentioned wanting to look into jax<->Fortran, I mentioned that this repo was the right place to connect things [having already the connection and most of the infra from C++].

Please feel free to completely take this PR, I just figure some starter code [that builds and hopefully does something reasonable] would help get you bootstrapped on

@joewallwork

Copy link
Copy Markdown

Ah sorry, this is entirely AI generated. Per our call Monday where you mentioned wanting to look into jax<->Fortran, I mentioned that this repo was the right place to connect things [having already the connection and most of the infra from C++].

Please feel free to completely take this PR, I just figure some starter code [that builds and hopefully does something reasonable] would help get you bootstrapped on

No worries. This repo does seem like the right place to do this stuff so thanks for the help getting something set up. I'll chat with @Mikolaj-A-Kowalski in our standup tomorrow about how we want to proceed.

This branch has not been deployed

No deployments
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