Skip to content

Vendor Eigen instead of letting lwtnn configure its own - #353

Open
nsmith- wants to merge 5 commits into
masterfrom
fix-eigen-gmp-350
Open

Vendor Eigen instead of letting lwtnn configure its own#353
nsmith- wants to merge 5 commits into
masterfrom
fix-eigen-gmp-350

Conversation

@nsmith-

@nsmith- nsmith- commented Jul 31, 2026

Copy link
Copy Markdown
Collaborator

Note

This description is agent-generated text, written by Claude Opus 5 via Claude Code.

Closes #350.

The failure

Building 2.9.0 from source dies while CMake configures the bundled lwtnn:

CMake Error: The following variables are used in this project, but they are set to NOTFOUND.
GMP_LIBRARIES (ADVANCED)
    linked by target "mpreal_support" in directory .../lwtnn/externals/src/Eigen/unsupported/test

lwtnn's BUILTIN_EIGEN acquires Eigen with ExternalProject_Add, which runs a
full CMake configure of Eigen — including its unsupported/test suite. Where MPFR
is present but GMP's development headers are not, Eigen's mpreal_support target
ends up linking GMP_LIBRARIES-NOTFOUND and CMake treats that as fatal. CERN's
GitLab CI runners are in exactly that state.

This became everyone's problem in 2.9.0, when add_subdirectory(lwtnn) started
running unconditionally. And as the reporter notes, our set(BUILD_TESTING OFF)
genuinely cannot reach it: the ExternalProject configures in a separate CMake
invocation with its own cache.

The fix

Eigen is header-only — nothing links against it, the templates are instantiated
into lwtnn's objects. So there is no reason to configure Eigen's build system at
all. This vendors Eigen as a submodule alongside pybind11/rapidjson/xxhash and
points lwtnn's own FindEigen3.cmake at the headers, which short-circuits on a
pre-set EIGEN3_INCLUDE_DIR. Eigen's CMake project never runs, so GMP and MPFR
never enter the picture — for anyone, not just those who can pass a flag.

Also fixes the thing the reporter couldn't work around: -DEIGEN3_INCLUDE_DIR=/usr/include/eigen3
now builds against a system Eigen.

Only eigen/Eigen/** ships in the sdist (337 files, ~1 MB compressed); tests,
docs, demos and unsupported/ are excluded. lwtnn's only Eigen include is
<Eigen/Dense>. As a bonus this removes one of the two network fetches a source
build performs — Boost's ExternalProject stays, but it has a no-op
CONFIGURE_COMMAND and so was never implicated here.

Why the test change comes along

Vendoring pins Eigen 3.4.0 rather than the 3.3.7 lwtnn downloads, and the summation
order differs enough to move the lwtnn fixture by two ULP:

Eigen electron_fastsim_sf
3.3.7 0.95186825355646787 (bit-exact to the assertion)
3.4.0 0.95186825355646760

assert sf == 0.95186825355646787 was already too strict to survive a platform
change: #348 disabled the entire test suite on manylinux_aarch64 three
commits ago because that build differed by one ULP. Comparing with
pytest.approx(..., rel=1e-12) covers both, and lets the aarch64 wheels be tested
again — this PR touches pyproject.toml, so wheels.yml will actually exercise
that here.

3.4.0 rather than 3.3.7 also seems the better bet for the reporter's toolchain:
3.3.7 predates gcc 15 by six years, and they build with gcc 15.2.0.

Testing

  • cmake configure + build against the vendored copy, and pip install . — clean
  • full test suite (minus cvmfs) passes against Eigen 3.4.0
  • rebuilt with -DEIGEN3_INCLUDE_DIR=<eigen-3.3.7> to confirm the ULP shift is the
    Eigen version and nothing else, and that the override path works
  • sdist built and inspected: 337 eigen/ entries, all under eigen/Eigen/

nsmith- added 4 commits July 31, 2026 10:04
lwtnn's BUILTIN_EIGEN downloads Eigen and configures Eigen's CMake project,
which drags in its unsupported/ test suite. On any system that has MPFR but
no GMP development headers -- CERN's GitLab CI runners among them -- that
dies at configure time with

  GMP_LIBRARIES (ADVANCED)
      linked by target "mpreal_support" in directory .../unsupported/test

and takes the whole wheel build with it. It first bit in 2.9.0, where lwtnn
started being built unconditionally. Our set(BUILD_TESTING OFF) cannot reach
it, since the ExternalProject configures with its own cache.

Eigen is header only, so vendor it as a submodule and point lwtnn's bundled
FindEigen3 at those headers instead; Eigen's own CMake project never runs.
Only eigen/Eigen/** is shipped in the sdist, and -DEIGEN3_INCLUDE_DIR=...
builds against a system copy.

Vendoring pins 3.4.0 rather than the 3.3.7 lwtnn downloaded, which moves the
lwtnn test fixture by two ULP. That assertion was already exact-float
fragile -- #348 disabled the entire aarch64 test suite over a one ULP
difference there -- so compare approximately and test aarch64 wheels again.

Closes #350

Assisted-by: Claude Opus 5 <noreply@anthropic.com>
The wheel builds run the suite on every architecture we ship, but an approximate
assertion only ever says pass or fail, so we never learn how much float spread is
actually out there -- which is how #348 ended up skipping all of aarch64 in
response to a single ULP.

Route the three approximate assertions through a fixture that records the ULP
distance from the reference and print a summary at the end of the run, so each
cibuildwheel job's log carries a per-architecture record:

  ========================= float assertion ULP report =========================
  arm64 darwin python 3.13.12
  test_lwtnn_example     2 ulp  0.9518682535564676 vs 0.9518682535564679
  test_ndpoly            3 ulp  1.0801881480751712 vs 1.0801881480751705
  test_tformula          4 ulp  1.2512381067949132 vs 1.251238106794914

Purely informational; the assertions themselves are unchanged. Worth noting that
the two pre-existing ones are already 3 and 4 ULP out on macOS arm64, so exact
comparison was never what was being tested there.

Assisted-by: Claude Opus 5 <noreply@anthropic.com>
The report is only useful if you can read it without opening a log, so collect
it as markdown and append it to the job summary, where it lands just under
cibuildwheel's wheel table as one row per interpreter and assertion.

It cannot be written to GITHUB_STEP_SUMMARY from the test run itself, which is
the obvious approach: cibuildwheel builds its own summary and commits it with
write_text() after every build and test has finished, so anything written there
earlier is discarded. The tests write to CORRECTIONLIB_ULP_REPORT instead and a
following workflow step appends that, which also survives cibuildwheel failing.

Linux tests run in a container, so the path has to be passed in with
environment-pass and then resolved against the /host mount to reach the runner's
filesystem; the other platforms see the runner environment directly.

Assisted-by: Claude Opus 5 <noreply@anthropic.com>

Copilot AI 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.

Pull request overview

This PR addresses source-build failures introduced in v2.9.0 by preventing lwtnn from configuring/building Eigen via its ExternalProject path, instead pointing lwtnn at a vendored (or user-specified system) Eigen header tree. It also relaxes float assertions and adds per-architecture ULP reporting so wheel CI can run tests across architectures without brittle bit-exact expectations.

Changes:

  • Vendor Eigen headers and force lwtnn to use EIGEN3_INCLUDE_DIR rather than BUILTIN_EIGEN.
  • Replace strict float equality assertions with a shared ulp_report fixture that asserts via pytest.approx while recording ULP distances.
  • Append an ULP report into the GitHub Actions job summary for wheel builds.

Reviewed changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
CMakeLists.txt Forces lwtnn to use a vendored/system Eigen include dir and disables lwtnn’s builtin Eigen ExternalProject.
pyproject.toml Adjusts sdist contents to ship only Eigen headers needed for builds; re-enables aarch64 testing and passes ULP report env into Linux containers.
tests/conftest.py Adds the ulp_report fixture and end-of-run reporting (terminal + optional markdown).
tests/test_lwtnn.py Makes lwtnn numeric assertion tolerant and records ULP spread.
tests/test_ndpolyfit.py Uses ulp_report for float comparisons.
tests/test_core.py Uses ulp_report for a numerically sensitive TFormula assertion.
.github/workflows/wheels.yml Collects and appends the ULP report into the job summary after cibuildwheel finishes.
.gitmodules Adds Eigen as a submodule.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread tests/conftest.py
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
@github-actions

github-actions Bot commented Jul 31, 2026

Copy link
Copy Markdown

CVMFS benchmarks

Top 25 slowest-loading corrections, sorted by mean time:

Benchmark Mean (ms) Stddev (ms) Rounds
test_load[JME/Run2-2017-UL-NanoAODv9/latest/jet_jerc.json.gz] 995.055 6.181 5
test_load[JME/Run2-2017-UL-NanoAODv9/latest/fatJet_jerc.json.gz] 833.223 1.962 5
test_load[JME/Run3-24Prompt-Winter24-NanoAODv14/latest/jet_jerc.json.gz] 698.086 6.714 5
test_load[JME/Run3-24CDEReprocessingFGHIPrompt-Summer24-NanoAODv15/latest/jet_jerc.json.gz] 484.139 1.166 5
test_load[JME/Run3-24CDEReprocessingFGHIPrompt-Summer24-NanoAODv15/latest/fatJet_jerc.json.gz] 482.898 1.762 5
test_load[JME/Run3-25Prompt-Winter25-NanoAODv15/latest/jet_jerc.json.gz] 411.754 2.170 5
test_load[JME/Run3-25Prompt-Winter25-NanoAODv15/latest/fatJet_jerc.json.gz] 409.187 1.188 5
test_load[JME/Run3-25Prompt-Summer24-NanoAODv15/latest/jet_jerc.json.gz] 350.757 3.459 5
test_load[JME/Run3-25Prompt-Summer24-NanoAODv15/latest/fatJet_jerc.json.gz] 348.862 1.825 5
test_load[JME/Run2-2018-UL-NanoAODv9/latest/jet_jerc.json.gz] 336.100 1.487 5
test_load[JME/Run3-23CSep23-Summer23-NanoAODv12/latest/fatJet_jerc.json.gz] 333.928 2.258 5
test_load[JME/Run3-23CSep23-Summer23-NanoAODv12/latest/jet_jerc.json.gz] 333.308 1.732 5
test_load[JME/Run3-26Prompt-Summer24-NanoAODv15/latest/jet_jerc.json.gz] 330.396 1.072 5
test_load[JME/Run3-26Prompt-Summer24-NanoAODv15/latest/fatJet_jerc.json.gz] 329.718 0.667 5
test_load[JME/Run2-2016preVFP-UL-NanoAODv9/latest/jet_jerc.json.gz] 271.859 0.527 5
test_load[JME/Run2-2018-UL-NanoAODv9/latest/fatJet_jerc.json.gz] 261.159 1.154 5
test_load[JME/Run3-22EFGSep23-Summer22EE-NanoAODv12/latest/fatJet_jerc.json.gz] 258.867 1.894 5
test_load[JME/Run3-22EFGSep23-Summer22EE-NanoAODv12/latest/jet_jerc.json.gz] 258.098 0.226 5
test_load[JME/Run2-2016postVFP-UL-NanoAODv9/latest/jet_jerc.json.gz] 252.233 3.150 5
test_load[JME/Run3-23DSep23-Summer23BPix-NanoAODv12/latest/fatJet_jerc.json.gz] 242.902 1.177 5
test_load[JME/Run3-23DSep23-Summer23BPix-NanoAODv12/latest/jet_jerc.json.gz] 242.655 0.844 5
test_load[JME/Run2-2016preVFP-UL-NanoAODv9/latest/fatJet_jerc.json.gz] 233.487 0.490 5
test_load[JME/Run2-2017-UL-NanoAODv15/latest/fatJet_jerc.json.gz] 230.827 1.152 5
test_load[JME/Run2-2017-UL-NanoAODv15/latest/jet_jerc.json.gz] 230.513 0.615 5
test_load[JME/Run2-2016postVFP-UL-NanoAODv9/latest/fatJet_jerc.json.gz] 221.958 1.714 5

@nsmith-

nsmith- commented Jul 31, 2026

Copy link
Copy Markdown
Collaborator Author

@mads-hb let me know if this is a satisfactory fix for you

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

Labels

None yet

Development

Successfully merging this pull request may close these issues.

Corectionlib v2.9.0 source build fails on systems without GMP dev headers (e.g. CERN Gitlab CI runners)

2 participants