Skip to content

enzyme: opt-in Clang/Enzyme build option + AD smoke test#6

Draft
krystophny wants to merge 11 commits into
build-clang21from
enzyme-build-option
Draft

enzyme: opt-in Clang/Enzyme build option + AD smoke test#6
krystophny wants to merge 11 commits into
build-clang21from
enzyme-build-option

Conversation

@krystophny

Copy link
Copy Markdown
Member

What

Add an opt-in Clang/Enzyme build path and a toolchain smoke test:

  • VMECPP_ENABLE_ENZYME CMake option, off by default. When on, it requires
    a Clang compiler and -DVMECPP_ENZYME_PLUGIN=/path/to/ClangEnzyme-NN.so, and
    builds the enzyme_smoke test target.
  • common/enzyme/enzyme.h: thin declarations of the Enzyme autodiff intrinsics
    and the activity markers, with the allocation constraint that shapes every
    differentiable kernel in this stack documented in place.
  • common/enzyme/enzyme_smoke_test.cc: differentiates a scalar objective over
    Eigen::Map'd caller buffers and checks reverse- and forward-mode gradients
    against the closed form and central finite differences.

Why

Stacked on #5. Enzyme differentiates LLVM IR through a Clang plugin, so the
differentiable VMEC++ work needs a Clang build and a way to attach the plugin.
This patch adds that switch and a test that fails loudly if the plugin is not
attached or if Enzyme cannot differentiate the Eigen::Map buffer pattern the
later kernels depend on. It adds no production code; with the option off, the
build is byte-for-byte the previous one.

The smoke test also pins down the one hard Enzyme constraint for this codebase:
Enzyme's allocation analysis does not track Eigen's aligned allocator, so a
dynamic-size Eigen heap temporary crossing the differentiated call aborts with
"freeing without malloc". Differentiable kernels therefore operate on
caller-owned buffers via Eigen::Map. The test exercises exactly that pattern.

Verification

Configure and build with Clang 21.1.8 and the ClangEnzyme-21 plugin:

-- Enzyme plugin: .../ClangEnzyme-21.so
cfg=0
build=0 (0 errors)

ctest:

    Start 1: enzyme_smoke
1/1 Test #1: enzyme_smoke .....................   Passed    0.00 sec
100% tests passed, 0 tests failed out of 1

Smoke test output (reverse and forward both exact, agree with finite
differences):

enzyme smoke test (n=8)
  max|reverse - analytic| = 0.000e+00
  max|forward - analytic| = 0.000e+00
  max|reverse - finite-diff| = 2.646e-08
PASS

No regression with the option off. A default configure (GCC, no Enzyme flags)
succeeds and emits no enzyme_smoke target:

cfg=0  enzyme target present? 0

Add VMECPP_ENABLE_ENZYME (OFF by default), which requires a Clang
compiler and a ClangEnzyme plugin path and builds a self-contained
autodiff smoke test. The test differentiates a scalar objective written
over Eigen::Map'd caller buffers and checks reverse- and forward-mode
Enzyme gradients against the closed form and central finite differences.

enzyme.h documents the intrinsic ABI and the allocation constraint that
shapes the differentiable kernels: Enzyme cannot track Eigen's aligned
allocator, so differentiable paths use Eigen::Map over caller-owned
buffers and avoid heap expression temporaries.

With the option off the build is unchanged.
krystophny and others added 10 commits June 14, 2026 19:19
The 'Compare benchmark result' step uses github-action-benchmark with
comment-on-alert and the GITHUB_TOKEN, which is read-only for pull requests from
forks -> 'Resource not accessible by integration'. Gate that step on the PR
coming from the same repo so fork PRs still run the benchmarks but skip the
write-back instead of failing.
The pinned vmec-0.0.6 cp310 wheel was f90wrapped against numpy 1.x. Under
the numpy 2.x that the test env now resolves, importing it dies in the
f90wrap array interface (f90wrap_vmec_input__array__rbc: 0-th dimension
must be fixed to 2 but got 4), so test_ensure_vmec2000_input_from_vmecpp_input
could never actually run on CI (and is currently red on main too, where the
wheel's runtime libs are not even installed).

Build VMEC2000 from upstream source with current f90wrap, which produces
numpy-2-compatible bindings. The recipe mirrors SIMSOPT's own CI
(hiddenSymmetries/VMEC2000, cmake/machines/ubuntu.json). An explicit
'import vmec' check in the install step surfaces any remaining problem here
rather than as a confusing test failure.
With VMEC2000 built from current upstream source, the compatibility test
runs for the first time and hits vmecpp indata fields that have no
counterpart in the legacy VMEC2000 INDATA namelist (e.g.
free_boundary_method), which raised AttributeError. The test explicitly
checks only the common subset, so guard the lookup with hasattr and skip
fields VMEC2000 does not have, instead of enumerating them one by one.
…mit pin

Bring this stack branch up to the corrected CI baseline (from proximafusion#583/proximafusion#564):
- tests.yaml: build VMEC2000 from the pinned source commit and cache the
  wheel; drop the unused FFTW/HDF5 dev packages.
- benchmarks.yaml: skip the result upload on fork PRs (read-only token).
- test_simsopt_compat.py: skip vmecpp-only INDATA fields.
- CMakeLists: pin abseil to the 20260107.1 commit hash, not the tag.
Move the Enzyme autodiff smoke test into the bazel test framework, which
owns every other C++ test in this repository, and drop the separate CMake
ctest path that nothing in CI exercised.

- vmecpp/common/enzyme/BUILD.bazel: an `enzyme` header library plus an
  `enzyme_smoke_test` cc_test. The test is tagged `manual` so the default
  GCC `bazel test //...` skips it (the Enzyme intrinsics only resolve under
  Clang with the plugin attached) and never tries to compile it with GCC.
- .bazelrc: a `--config=enzyme` that sets -O2 so the Enzyme optimization
  pass fires. Select Clang with CC/CXX and pass the plugin path the way
  -DVMECPP_ENZYME_PLUGIN did under CMake:
    CC=clang CXX=clang++ bazel test --config=enzyme \
      --copt=-fplugin=/path/to/ClangEnzyme-NN.so \
      //vmecpp/common/enzyme:enzyme_smoke_test
- CMakeLists.txt: remove the VMECPP_ENABLE_ENZYME option and the ctest
  registration it only existed to drive.
Add a GitHub Actions job that gives the Enzyme autodiff smoke test actual CI
coverage. It mirrors the EnzymeAD upstream recipe: install Clang/LLVM 21 from
apt.llvm.org, build a pinned ClangEnzyme-21 plugin (v0.0.264, the version this
stack is developed against) against the installed LLVM and Clang, then run the
bazel target under --config=enzyme with the plugin attached. The plugin build
is cached on the pinned ref so only the first run pays for it.

This is what the enzyme test needed beyond the bazel move: the default GCC
test_bazel job skips the manual-tagged target, so without a Clang/Enzyme job
nothing exercised it.
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