From 8546d505ef711815e4550aaf0623163347f33dc4 Mon Sep 17 00:00:00 2001 From: AkshatRai07 Date: Wed, 16 Sep 2026 11:20:47 +0530 Subject: [PATCH 1/6] Add EKO Oxidation --- _gsocblogs/2026/blog_NNPDF_AkshatRai.md | 304 ++++++++++++++++++++++++ images/blog_authors/AkshatRai.jpg | Bin 0 -> 38426 bytes 2 files changed, 304 insertions(+) create mode 100644 _gsocblogs/2026/blog_NNPDF_AkshatRai.md create mode 100644 images/blog_authors/AkshatRai.jpg diff --git a/_gsocblogs/2026/blog_NNPDF_AkshatRai.md b/_gsocblogs/2026/blog_NNPDF_AkshatRai.md new file mode 100644 index 000000000..6d4bc92f6 --- /dev/null +++ b/_gsocblogs/2026/blog_NNPDF_AkshatRai.md @@ -0,0 +1,304 @@ +--- +project: NNPDF +title: EKO Oxidation +author: Akshat Rai +photo: blog_authors/AkshatRai.jpg +date: 16.09.2026 +year: 2026 +layout: blog_post +logo: nnpdf.png +intro: | + This project aimed to modernize EKO (Evolution Kernel Operators) by bridging its Python codebase with a Rust backend. Over the summer, I implemented a robust cross-language interoperability layer, developed two dedicated interface crates for C-ABI and Python bindings, backed by automated CI/CD workflows that deploy directly to PyPI and GitHub Releases. +--- + +
+NNPDF Logo +
+ +## Mentors: Felix Hekhorn and Juan Manuel Cruz Martinez + +## [Project Link (NNPDF Collaboration) - EKO](https://github.com/NNPDF/eko) + +### The short version + +EKO (Evolution Kernel Operators) is a Python library that solves the DGLAP equations used to extract Parton Distribution Functions from collider data. It precomputes solution operators independently of the actual PDF, which turns a repeated integro-differential problem into a one-off integration followed by fast linear algebra. That integration is expensive, and getting more expensive as EKO adds higher perturbative orders and QED effects, so the project has been "oxidizing": moving the hot loop from pure Python into Rust, one piece at a time, while `scipy` and `numba` keep the rest running. + +Coming in, the Rust port already existed in prototype form. My job was to turn it into a documented architecture, an interface that doesn't leak memory and is quicker than the legacy code, a public C ABI and a PyO3 binding for consumers who aren't `eko` itself, and a release pipeline that gets all of that onto PyPI, GitHub Releases, and crates.io. Along the way I tried to simplify the core call chain, benchmarked the attempt properly, and found it was worse. + +### Mapping the architecture + +The first task ([#516](https://github.com/NNPDF/eko/issues/516)) was to document what the current Python/Rust split actually does before changing any of it. [PR #521](https://github.com/NNPDF/eko/pull/521) walked the user entry point (`runner.solve`) down through `Operator` and `OperatorMatrixElement`, the parallel loop over target x-grid points, the inner loop over source basis functions and flavour labels, and finally into `scipy.integrate.quad` itself, comparing the pure-Python/Numba `quad_ker` path against the Rust `rust_quad_ker` entry point. + +A companion PR, [#529](https://github.com/NNPDF/eko/pull/529), added `performance.md` alongside it: raw benchmark numbers and the commands used to produce them, kept separate from the narrative document so the two could be updated independently. + +### Publishing what already existed + +While the two crates (`ekore` and `eko`) were already publishing successfully, there was a problem with `eko` reaching PyPI ([#517](https://github.com/NNPDF/eko/issues/517)). [PR #522](https://github.com/NNPDF/eko/pull/522) fixed this by updating `maturin.yml` to call `bump-versions.py` and replacing the deprecated Maturin publish action with a modern build-and-upload step. I confirmed the pipeline end-to-end using a throwaway PyPI project (`akshat-eko-rs-test`). + +### The experiment that didn't work + +This is the part of the summer worth being specific about, because it's a negative result with real numbers behind it, not just an abandoned idea. + +The architecture that ships today looks like this for the Rust-enabled path: + +```text +scipy.integrate.quad(LowLevelCallable(rust_quad_ker, &cfg), 0.5, 1-ε) + ↓ scipy's C backend calls the function pointer directly, no Python overhead +rust_quad_ker(u, *args) [crates/eko/src/lib.rs] + ↓ +ekore (Rust) → anomalous dimensions + ↓ callback +cb_quad_ker_qcd / cb_quad_ker_qed [Numba] + ↓ +kernels/singlet.py, non_singlet.py → evolution operator matrix + ↓ +f64 returned to scipy +``` + +`scipy → Rust → Numba → Rust → scipy`. Rust owns the entry point and the anomalous-dimension computation, then hands off to Numba for the evolution operator matrix, which hasn't been ported yet. + +While working on the kernel split, I tried inverting this: keep the `LowLevelCallable` entry point in Numba instead of Rust, using `nb.cfunc`, and call into Rust only for the anomalous dimensions via `ctypes`: + +```text +scipy.integrate.quad(LowLevelCallable(quad_ker_llc, &cfg), 0.5, 1-ε) + ↓ +quad_ker_llc(u, *args) [nb.cfunc] + ↓ ctypes call into Rust +ekors.qcd_gamma_singlet / qcd_gamma_ns + ↓ +ekore (Rust) → anomalous dimensions + ↓ back to Numba +kernels/singlet.py, non_singlet.py → evolution operator matrix + ↓ +f64 returned to scipy +``` + +After struggling with complex `ctypes` type coercion across the boundary, I used a `#[pyfunction]` stand-in to benchmark the approach (`poe lha -m nnlo and sv`). + +The results were stark compared to the master branch (`rs → nb → rs`): + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
MetricMasterInverted (nb → rs → nb)Ratio
Wall clock11:1620:24~1.8× worse
Peak RSS616 MB1953 MB~3.2× worse
Cost per quad call~1.5 ms~5 ms~3.3× worse
+ +Both regressions stemmed from Numba's compilation model: + +* **Memory:** `ctypes` pointers prevent the use of `cache=True`. Instead of writing artifacts to disk and releasing them, Numba was forced to keep the entire compiled call graph resident in memory. +* **Time:** The inverted path pays a heavy Numba-to-Rust transition tax on *every* `quad` node evaluation. In contrast, the master path does the expensive setup in Rust exactly once beforehand. + +Because the `Numba → Rust` pattern proved unviable for this workload, the PR was not merged. However, the reasoning and metrics were permanently documented in [architecture.md](https://github.com/NNPDF/eko/blob/master/extras/architecture.md) for future reference. + +### ekore_capi: a stable C-ABI + +To make `ekore` accessible to standard high-energy physics tooling (C, C++, and eventually Fortran) ([Issue #519](https://github.com/NNPDF/eko/issues/519)), [PR #537](https://github.com/NNPDF/eko/pull/537) introduced the `ekore_capi` crate. Before exposing this C interface, several prerequisite PRs were merged to harden the Rust codebase. This groundwork included ensuring benchmarks ran in `--release` mode, removing hidden heap allocations in favor of fixed caller-provided buffers, strictly scoping visibility (`pub(crate)`), centralizing workspace dependencies, updating MSRV, and bolstering unit tests and literature citations. + +The `ekore_capi` crate relies on a few key FFI conventions: + +* **Safe Layouts:** Complex numbers cross the boundary as a `#[repr(C)] ComplexF64` struct to avoid relying on C99 `double complex` layouts. +* **Opaque Pointers:** The Mellin-space harmonic cache is managed via `cache_new` and `cache_delete`. +* **Caller Allocation:** Computations use `_result_len` helpers so callers can safely pre-allocate correctly sized buffers. +* **Automation:** `cargo-c` and `cbindgen` handle header and `pkg-config` generation automatically. + +Finally, [PR #553](https://github.com/NNPDF/eko/pull/553) added `install-capi.sh`, a POSIX shell installer for pre-built binaries. + +### ekore_py: direct PyO3 bindings + +While `ekore_capi` supports C, C++, and Fortran, Python users seeking only the standalone physics functions lacked direct access. [PR #554](https://github.com/NNPDF/eko/pull/554) solved this by introducing `ekore_py`, which exposes `ekore`'s anomalous dimensions and operator matrix elements as native Python functions via PyO3 (packaged on PyPI as `ekore-rs`, imported as `ekore_rs`). This serves as a general-purpose physics library and is deliberately distinct from the internal `eko-rs` bridge package. + +Most of the crate's logic resides in `macros.rs`, which uses a single macro to generate the `#[pyfunction]` boilerplate for the various anomalous dimension and OME wrappers. + +### The libome dead end + +The project's task list required preparation for a third-party C++ contribution. To test this before any real external attempt, [PR #562](https://github.com/NNPDF/eko/pull/562) evaluated whether `ekore` could successfully call out to an external C++ library. I wrote a mock C++ library (`extras/gsoc/libome`) to stand in for the real `libome`, compiled it via a Rust `build.rs` script using the `cc` crate, and consumed it from a new `as3.rs` file. + +This demonstrated calling an external C++ library from `ekore` does not incur meaningful performance costs. It also demonstrated two legitimate ways to wire the connection. The first method is to vendor the real library as a submodule, build it with the `cmake` crate, and explicitly link its dependencies like GSL. The second method is to link directly against a pre-built `.so` file. In this latter case, Rust does not even need the C++ headers because `rustc` reads the function signatures directly from its own `unsafe extern "C"` declarations and `#[repr(C)]` structs. + +However, there's an issue. Currently the real [libome](https://gitlab.com/libome/libome) operates in x-space, while the `ekore` OME machinery operates in Mellin N-space. Calling the real library would require a dedicated transform layer that was out of scope for the summer. Rather than merging a mock that solved the wrong problem, [PR #562](https://github.com/NNPDF/eko/pull/562) was closed and unmerged. We intentionally preserved the branch as a starting point for future developers. Finally, [PR #567](https://github.com/NNPDF/eko/pull/567) officially documented this mismatch in `architecture.md` so the constraint is recorded rather than rediscovered. + +### Fixing the release pipeline + +The final stretch of the summer focused entirely on ensuring the release pipeline functioned correctly across all registries. + +[PR #556](https://github.com/NNPDF/eko/pull/556) fixed macOS specific C-ABI linking errors, removed static library generation, and enabled dynamic versioning so `eko` wheel names tracked the correct release. [PR #561](https://github.com/NNPDF/eko/pull/561) standardized workflow names to a `-.yml` convention, restricted release triggers to semver tags, and added `workflow_dispatch` inputs for manual retries. + +Testing these changes against real registries in [PR #569](https://github.com/NNPDF/eko/pull/569) required several fixes before succeeding. `eko` failed due to a dynamic versioning bug, `ekore-rs` published without a description, and `eko-rs` failed due to a misdirected trusted publisher entry. Fixing these got all three packages live. During this process, Felix also pointed out a risk: while manual tag inputs help retry broken releases, they introduce the danger of accidentally attaching new code to an old tag. + +To secure the pipeline against that risk, [PR #573](https://github.com/NNPDF/eko/pull/573) added explicit tag validation, which does multiple checks to ensure the `workflow_dispatch` workflow is not overriding anything. It also closed out the remaining gaps by adding proper PyPI descriptions, bypassing dynamic versioning during manual workflows, and updating GitHub action versions across the board. + +### What got merged + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ContributionPR/IssueStatus
Architecture documentation (architecture.md)#521Merged
Fixed crate/package publishing (crates.io, PyPI)#522Merged
Inverted Numba → Rust → Numba call chain (benchmarked, rejected)#526Not merged, documented in architecture.md
Performance benchmarking document#529Merged
Split integration kernel from the Operator class#531Merged
eko-rs workflow fix#540Merged
--release build flag fix#541Merged
Removed mallocs, single-buffer integration#542Merged
Tightened ekore visibility, split constants.rs#543Merged
Centralised workspace dependencies#544Merged
MSRV bump to 1.85.0 / Edition 2024#545Merged
Tests for spacelike.rs#546Merged
Restored literature references#550Merged
ekore_capi crate addition#537Merged
install-capi.sh installer#553Merged
ekore_py crate addition, published as ekore-rs#554Merged
macOS release fixes, dynamic versioning#556Merged
Updated architecture.md with new crates#559Merged
Full GitHub workflow refactor#561Merged
libome external C++ interop prototype#562Not merged, branch kept as reference
Documented libome mismatch in architecture.md#567Merged
Live PyPI release fixes (eko, eko-rs, ekore-rs)#569Merged
Package descriptions, tag validation#573Merged
+ +**Left for later:** Fortran test coverage for `ekore_capi` was explicitly postponed to [#566](https://github.com/NNPDF/eko/issues/566). The C ABI supports it architecturally, but no automated Fortran suite exists yet. + +### Final Architecture + +
+Final Arghitecture +
+ +### What I'd tell next year's contributor + +**Benchmark architectural intuition.** Inverting the `Numba`/`Rust` call chain seemed simpler but performed three times worse across all metrics. Catching this early with a `poe lha` benchmark prevented a permanent regression. + +**Numba's caching model struggles with raw pointers.** Both major bugs I encountered (the `select_singlet_element` typing error and the `ctypes` memory blowup) stemmed from unintuitive cache invalidation. When integrating FFI into a Numba codebase, test caching behavior in isolation first. + +**Scrutinize installer scripts like application code.** The initial `install-capi.sh` draft contained classic POSIX traps: a `read` failure under a pipe, shell injection from unquoted paths, and unescaped `&` characters in `sed`. These vulnerabilities often hide perfectly within the happy path. + +**Document closed PRs.** The `libome` PR was closed unmerged, but preserving the branch and formally documenting the domain mismatch in `architecture.md` ensures future developers will not have to rediscover the same dead end. + +### Thanks + +A huge thank you to Felix Hekhorn and Juan Cruz-Martinez for bringing me onto this project and for their exceptional mentorship over the past few months. Their detailed feedback improved the code we shipped and profoundly shaped how I approach software design, performance profiling, and open-source development. I also want to express my sincere appreciation to the NNPDF team, the HEP Software Foundation, and GSoC team for making this summer project possible. I have learned an immense amount during this time, and I am deeply grateful for the experience and the opportunity to work alongside such talented developers. I am excited to stay involved and continue contributing to the repository well beyond the end of the program. diff --git a/images/blog_authors/AkshatRai.jpg b/images/blog_authors/AkshatRai.jpg new file mode 100644 index 0000000000000000000000000000000000000000..bbf2e4f423e71522118791ee0c0da29b00fc5948 GIT binary patch literal 38426 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 literal 0 HcmV?d00001 From cc89174d6c7b868223cb20a3a0b4856c5c7bc2ba Mon Sep 17 00:00:00 2001 From: AkshatRai07 Date: Wed, 16 Sep 2026 11:48:18 +0530 Subject: [PATCH 2/6] Fix typos and wordings --- _gsocblogs/2026/blog_NNPDF_AkshatRai.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/_gsocblogs/2026/blog_NNPDF_AkshatRai.md b/_gsocblogs/2026/blog_NNPDF_AkshatRai.md index 6d4bc92f6..1f5ee6eb9 100644 --- a/_gsocblogs/2026/blog_NNPDF_AkshatRai.md +++ b/_gsocblogs/2026/blog_NNPDF_AkshatRai.md @@ -286,12 +286,12 @@ To secure the pipeline against that risk, [PR #573](https://github.com/NNPDF/eko ### Final Architecture
-Final Arghitecture +Final Architecture
### What I'd tell next year's contributor -**Benchmark architectural intuition.** Inverting the `Numba`/`Rust` call chain seemed simpler but performed three times worse across all metrics. Catching this early with a `poe lha` benchmark prevented a permanent regression. +**Benchmark architectural intuition.** Inverting the `Numba`/`Rust` call chain seemed simpler but performed worse across all metrics: about 1.8× slower wall-clock time and over 3× worse peak memory and per-call cost. Catching this early with a `poe lha` benchmark prevented a permanent regression. **Numba's caching model struggles with raw pointers.** Both major bugs I encountered (the `select_singlet_element` typing error and the `ctypes` memory blowup) stemmed from unintuitive cache invalidation. When integrating FFI into a Numba codebase, test caching behavior in isolation first. From e88c783d183f70fbffa82f2a0530e7bfbd04aaa6 Mon Sep 17 00:00:00 2001 From: AkshatRai07 Date: Wed, 16 Sep 2026 14:48:28 +0530 Subject: [PATCH 3/6] Update table header --- _gsocblogs/2026/blog_NNPDF_AkshatRai.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/_gsocblogs/2026/blog_NNPDF_AkshatRai.md b/_gsocblogs/2026/blog_NNPDF_AkshatRai.md index 1f5ee6eb9..f37f1f433 100644 --- a/_gsocblogs/2026/blog_NNPDF_AkshatRai.md +++ b/_gsocblogs/2026/blog_NNPDF_AkshatRai.md @@ -158,7 +158,7 @@ To secure the pipeline against that risk, [PR #573](https://github.com/NNPDF/eko Contribution - PR/Issue + PR Status From 0d1c4f324c73d2be72fc9e6e8d290ceddc732b3c Mon Sep 17 00:00:00 2001 From: AkshatRai07 Date: Wed, 16 Sep 2026 14:49:26 +0530 Subject: [PATCH 4/6] Change libome text --- _gsocblogs/2026/blog_NNPDF_AkshatRai.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/_gsocblogs/2026/blog_NNPDF_AkshatRai.md b/_gsocblogs/2026/blog_NNPDF_AkshatRai.md index f37f1f433..5cba5f7a4 100644 --- a/_gsocblogs/2026/blog_NNPDF_AkshatRai.md +++ b/_gsocblogs/2026/blog_NNPDF_AkshatRai.md @@ -134,13 +134,13 @@ While `ekore_capi` supports C, C++, and Fortran, Python users seeking only the s Most of the crate's logic resides in `macros.rs`, which uses a single macro to generate the `#[pyfunction]` boilerplate for the various anomalous dimension and OME wrappers. -### The libome dead end +### The libome exploration The project's task list required preparation for a third-party C++ contribution. To test this before any real external attempt, [PR #562](https://github.com/NNPDF/eko/pull/562) evaluated whether `ekore` could successfully call out to an external C++ library. I wrote a mock C++ library (`extras/gsoc/libome`) to stand in for the real `libome`, compiled it via a Rust `build.rs` script using the `cc` crate, and consumed it from a new `as3.rs` file. This demonstrated calling an external C++ library from `ekore` does not incur meaningful performance costs. It also demonstrated two legitimate ways to wire the connection. The first method is to vendor the real library as a submodule, build it with the `cmake` crate, and explicitly link its dependencies like GSL. The second method is to link directly against a pre-built `.so` file. In this latter case, Rust does not even need the C++ headers because `rustc` reads the function signatures directly from its own `unsafe extern "C"` declarations and `#[repr(C)]` structs. -However, there's an issue. Currently the real [libome](https://gitlab.com/libome/libome) operates in x-space, while the `ekore` OME machinery operates in Mellin N-space. Calling the real library would require a dedicated transform layer that was out of scope for the summer. Rather than merging a mock that solved the wrong problem, [PR #562](https://github.com/NNPDF/eko/pull/562) was closed and unmerged. We intentionally preserved the branch as a starting point for future developers. Finally, [PR #567](https://github.com/NNPDF/eko/pull/567) officially documented this mismatch in `architecture.md` so the constraint is recorded rather than rediscovered. +However, integrating the real library is currently on pause. The real [libome](https://gitlab.com/libome/libome) operates in x-space, while the `ekore` OME machinery operates in Mellin N-space. Bridging this gap requires a dedicated transform layer and help from a third party, which currently has an indefinite timescale. The mock integration code itself is fine as is, but rather than merging it while we wait, [PR #562](https://github.com/NNPDF/eko/pull/562) was closed. We intentionally preserved the branch as a fully functional starting point for future developers once the third-party work is ready. Finally, [PR #567](https://github.com/NNPDF/eko/pull/567) officially documented this mismatch in `architecture.md` so the constraint is recorded rather than rediscovered. ### Fixing the release pipeline From 649bedddbceac3d1f1e566886ea7d169adffa69e Mon Sep 17 00:00:00 2001 From: AkshatRai07 Date: Wed, 16 Sep 2026 15:02:03 +0530 Subject: [PATCH 5/6] Update architecture diagram --- _gsocblogs/2026/blog_NNPDF_AkshatRai.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/_gsocblogs/2026/blog_NNPDF_AkshatRai.md b/_gsocblogs/2026/blog_NNPDF_AkshatRai.md index 5cba5f7a4..e0e1d58c2 100644 --- a/_gsocblogs/2026/blog_NNPDF_AkshatRai.md +++ b/_gsocblogs/2026/blog_NNPDF_AkshatRai.md @@ -286,7 +286,7 @@ To secure the pipeline against that risk, [PR #573](https://github.com/NNPDF/eko ### Final Architecture
-Final Architecture +Final Architecture
### What I'd tell next year's contributor From 29798651f2630fa129925b4a0a68c12243cc8432 Mon Sep 17 00:00:00 2001 From: AkshatRai07 Date: Fri, 18 Sep 2026 14:00:37 +0530 Subject: [PATCH 6/6] Made text more positive --- _gsocblogs/2026/blog_NNPDF_AkshatRai.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/_gsocblogs/2026/blog_NNPDF_AkshatRai.md b/_gsocblogs/2026/blog_NNPDF_AkshatRai.md index e0e1d58c2..062c7087a 100644 --- a/_gsocblogs/2026/blog_NNPDF_AkshatRai.md +++ b/_gsocblogs/2026/blog_NNPDF_AkshatRai.md @@ -23,7 +23,7 @@ intro: | EKO (Evolution Kernel Operators) is a Python library that solves the DGLAP equations used to extract Parton Distribution Functions from collider data. It precomputes solution operators independently of the actual PDF, which turns a repeated integro-differential problem into a one-off integration followed by fast linear algebra. That integration is expensive, and getting more expensive as EKO adds higher perturbative orders and QED effects, so the project has been "oxidizing": moving the hot loop from pure Python into Rust, one piece at a time, while `scipy` and `numba` keep the rest running. -Coming in, the Rust port already existed in prototype form. My job was to turn it into a documented architecture, an interface that doesn't leak memory and is quicker than the legacy code, a public C ABI and a PyO3 binding for consumers who aren't `eko` itself, and a release pipeline that gets all of that onto PyPI, GitHub Releases, and crates.io. Along the way I tried to simplify the core call chain, benchmarked the attempt properly, and found it was worse. +Coming in, the Rust port already existed in prototype form. My job was to turn it into a documented architecture, an interface that doesn't leak memory and is quicker than the legacy code, a public C ABI and a PyO3 binding for consumers who aren't `eko` itself, and a release pipeline that gets all of that onto PyPI, GitHub Releases, and crates.io. Along the way, I also explored simplifying the core call chain, benchmarked the attempt thoroughly, and documented the negative result to establish clear constraints and validate the existing architecture. ### Mapping the architecture @@ -35,7 +35,7 @@ A companion PR, [#529](https://github.com/NNPDF/eko/pull/529), added `performanc While the two crates (`ekore` and `eko`) were already publishing successfully, there was a problem with `eko` reaching PyPI ([#517](https://github.com/NNPDF/eko/issues/517)). [PR #522](https://github.com/NNPDF/eko/pull/522) fixed this by updating `maturin.yml` to call `bump-versions.py` and replacing the deprecated Maturin publish action with a modern build-and-upload step. I confirmed the pipeline end-to-end using a throwaway PyPI project (`akshat-eko-rs-test`). -### The experiment that didn't work +### Negative attempt to improve performance This is the part of the summer worth being specific about, because it's a negative result with real numbers behind it, not just an abandoned idea.