Skip to content

CUDA EP as a standalone plugin package (onnxruntime-ep-cuda) — explores #196 follow-up - #197

Draft
hmaarrfk wants to merge 56 commits into
conda-forge:mainfrom
hmaarrfk:rattler-build-ep-plugin
Draft

CUDA EP as a standalone plugin package (onnxruntime-ep-cuda) — explores #196 follow-up#197
hmaarrfk wants to merge 56 commits into
conda-forge:mainfrom
hmaarrfk:rattler-build-ep-plugin

Conversation

@hmaarrfk

@hmaarrfk hmaarrfk commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

Exploratory follow-up to #196, implementing @cbourjau's suggestion (comment): use upstream's new execution provider plugin system to decouple the expensive CUDA build from Python.

It would virtually remove the need for staged outputs, I think. We would compile one Python-independent cuda package per platform, and N fairly cheap Python-dependent frontends that depend on the former.

This branch contains everything from #196 plus the plugin reorganization. If #196 merges first this rebases to just the plugin commits.

Upstream mechanics (onnxruntime 1.28.0, docs/cuda_plugin_ep/)

  • onnxruntime_BUILD_CUDA_EP_AS_PLUGIN=ON builds onnxruntime_providers_cuda_plugin — a standalone shared module with the canonical provider filename (libonnxruntime_providers_cuda.so / onnxruntime_providers_cuda.dll) registered under the canonical name CUDAExecutionProvider. The legacy in-tree CUDA provider is not built in this mode.
  • The plugin is Python-independent (links the internal static libs + cudart/cublas/cublasLt/driver only) and loads into any onnxruntime core ≥ 1.24.4 via load-time API version negotiation (plugin-ep-cuda/MIN_ONNXRUNTIME_VERSION).
  • cuDNN and cuFFT are build-time header dependencies but loaded lazily at runtime — the plugin has no hard link on either.
  • Upstream itself distributes this as separate onnxruntime-ep-cuda12 / onnxruntime-ep-cuda13 wheels.

Recipe reorganization

  • New output onnxruntime-ep-cuda (one per platform × CUDA version, no Python dimension): configures with BUILD_CUDA_EP_AS_PLUGIN=ON, ENABLE_PYTHON=OFF, builds only the plugin target, ships just the provider library (lib/ on unix, Library/bin/ on win, matching upstream's install rule).
  • CUDA disappears from the staged outputs: onnxruntime-build/-cpp/python become CPU-only. The CUDA python matrix (previously 5 python versions × 2 CUDA versions × 3 platforms of full CUDA rebuilds) is gone.
  • Usage until deeper integration lands:
    import onnxruntime as ort
    ort.register_execution_provider_library(
        "CUDAExecutionProvider", f"{os.environ['CONDA_PREFIX']}/lib/libonnxruntime_providers_cuda.so")
    sess = ort.InferenceSession("model.onnx", providers=[("CUDAExecutionProvider", {}), "CPUExecutionProvider"])
  • run_constraints: onnxruntime >=1.24.4 documents the minimum core; __cuda run dep avoids needless downloads.

Exploratory CI restriction (revert before merge)

Per-recipe EXPLORATORY skips limit the matrix to the 6 linux/win CUDA jobs — the CPU/osx/novec outputs are byte-identical to #196 and already green there. Removing the top-level exploratory skips restores them.

Open questions for review

  1. Auto-registration: the CPU python wheel doesn't carry cuda-plugin-ep=1 build info, so the bundled auto-registration path is inert. Options: patch onnxruntime/__init__.py to also probe $CONDA_PREFIX/lib, ship a small onnxruntime-ep-cuda-python shim (upstream's onnxruntime_ep_cuda module), or document explicit registration.
  2. cuDNN as hard run dep: kept for now (via cudnn host run-exports) since most users want Conv; upstream's design treats it as optional. Could be relaxed to run_constrained.
  3. Package naming: onnxruntime-ep-cuda (one name, CUDA version via variant/build string) vs upstream's onnxruntime-ep-cuda12/13 split names.
  4. Whether -cpp CUDA consumers need anything beyond Env::RegisterExecutionProviderLibrary (the C++ path works with the plugin as-is).
  5. CPU python frontends still use the staged megabuild from Migrate to rattler-build with staged outputs (refresh of #171 for 1.28.0) #196 — with CUDA gone it's much cheaper, but per-python full CPU rebuilds would be the alternative if we want to drop staging entirely.

CC @cbourjau — this is the shape you suggested; keen on your take on the open questions.

cbourjau added 30 commits August 8, 2026 20:41
(cherry picked from commit bbb7162)
(cherry picked from commit 1729bf1)
(cherry picked from commit a603d7a)
(cherry picked from commit 013a296)
(cherry picked from commit b9e50c1)
(cherry picked from commit b4ddaab)
(cherry picked from commit 0ceeae9)
(cherry picked from commit 8178088)
(cherry picked from commit 0c0eb26)
[cherry picked from commit 256790e; recipe changes only, rerender output dropped]
[cherry picked from commit d3457f3; recipe changes only, rerender output dropped]
(cherry picked from commit 0f79f21)
(cherry picked from commit 2332847)
(cherry picked from commit 9724cd6)
(cherry picked from commit de3e786)
(cherry picked from commit 68ded51)
(cherry picked from commit 9417bea)
[cherry picked from commit 666a846; recipe changes only, rerender output dropped]
(cherry picked from commit 85eb668)
(cherry picked from commit af69e8d)
(cherry picked from commit 542e19a)
cbourjau and others added 24 commits August 8, 2026 20:42
(cherry picked from commit 6e420d1)
(cherry picked from commit 953701e)
[cherry picked from commit add924b; recipe changes only, rerender output dropped]
(cherry picked from commit 9dcca6c)
(cherry picked from commit ae48510)
(cherry picked from commit dd1d48d)
(cherry picked from commit 72e183d)
(cherry picked from commit 9407828)
(cherry picked from commit 8f432b4)
(cherry picked from commit 66edfee)
<details><summary>Claude's draft</summary>

Adapt the cherry-picked rattler-build migration (conda-forge#171 by @cbourjau) to the
current 1.28.0 feedstock state:

- Bump version to 1.28.0 with the matching sha256 and add patches 0006/0007
  (onnx-less test guards introduced for 1.28.0).
- Re-enable the -novec variants that were skipped while debugging conda-forge#171; this
  required templating the staging output name in the `inherit:` keys.
- Port CoreML execution provider support for osx-arm64 (conda-forge#182):
  onnxruntime_USE_COREML=ON, nlohmann_json host dep, provider test,
  BSD-3-Clause license and vendored license files.
- Port Jetson Thor support (conda-forge#190): add 110-real to the CUDA 13.0 arch list.
- Port nvcc_threads=2 (Windows CUDA 13.0 OOM mitigation).
- Pin cmake <4 (vendored protobuf 3.21 does not build with CMake 4).
- Match main's 1.28.0 runtime deps: drop sympy, add __cuda run dep on the
  python output.
- Add onnxruntime_test --help entry-point test.

Resume this Claude session:
```
cd /home/mark/git/feedstock/onnxruntime-feedstock
claude --resume bceefe2e-0d08-4ad8-9b4c-3518466f1ccc
```
</details>
…026.08.08.17.43.08

Other tools:
- conda-build 26.7.0
- rattler-build 0.72.2
- rattler-build-conda-compat 1.4.19
<details><summary>Claude's draft</summary>

The top-level recipe name is a placeholder (onnxruntime-dummy), so the
conda-smithy linter needs an explicit extra.feedstock-name: onnxruntime.

Resume this Claude session:
```
cd /home/mark/git/feedstock/onnxruntime-feedstock
claude --resume bceefe2e-0d08-4ad8-9b4c-3518466f1ccc
```
</details>
<details><summary>Claude's draft</summary>

All CI jobs failed at cmake configure with "CMAKE_CXX_STANDARD must be at
least 20. It is 17." — 1.28.0 enforces C++20 via
onnxruntime_language_standard_versions.cmake.

The C++17 choice in conda-forge#171 was a workaround for C++20 module-scanning issues
at 1.24.4; upstream 1.28.0 now sets CMAKE_CXX_SCAN_FOR_MODULES=OFF itself
(cmake/CMakeLists.txt:21), so C++20 is safe. This also matches main's
build.py invocation which passed CMAKE_CXX_STANDARD=20.

run_cpp_test.bat stays at /std:c++17: consuming the public headers only
needs C++17 and main is green with that combination.

Resume this Claude session:
```
cd /home/mark/git/feedstock/onnxruntime-feedstock
claude --resume bceefe2e-0d08-4ad8-9b4c-3518466f1ccc
```
</details>
<details><summary>Claude's draft</summary>

Both Windows CUDA jobs failed at configure with "Could not find compiler
set in environment variable CC: cl.exe" while the Windows CPU job (which
skips the CUDA branch) proceeded fine.

Cause: on Windows, nushell exposes the search path as a *list* named
`Path`. The CUDA branch did

    $env.PATH = $"($build_lib_prefix)/bin;($env.PATH)"

which interpolates that list into a string ("[C:\..., C:\...]") under the
separate key `PATH`, shadowing the real `Path` in child processes — so
cmake could no longer find cl.exe. Prepend to the `Path` list instead.

Resume this Claude session:
```
cd /home/mark/git/feedstock/onnxruntime-feedstock
claude --resume bceefe2e-0d08-4ad8-9b4c-3518466f1ccc
```
</details>
<details><summary>Claude's draft</summary>

The Windows CUDA 12.9 job now compiles (PATH fix worked) but fails on every
.cu file targeting sm_100a/sm_120a: CCCL's clusterlaunchcontrol.h inline asm
uses long2, and long is 32-bit under MSVC, producing "asm operand type
size(4) does not match constraint 'l'".

main already carries the answer in bld.bat: SM 100+ is broken with CUDA 12.9
on Windows (fixed in 13.0), and SM 110 (Thor) is Linux-only. My 1.28.0
adaptation had wrongly applied the Linux arch lists to all platforms. Split
the arch lists per platform to match main exactly:

- win 12.9: 70..90-real (no 100/120)
- win 13.0: 75..100-real,120 (no 110)
- linux unchanged

Resume this Claude session:
```
cd /home/mark/git/feedstock/onnxruntime-feedstock
claude --resume bceefe2e-0d08-4ad8-9b4c-3518466f1ccc
```
</details>
<details><summary>Claude's draft</summary>

The osx_arm64 jobs failed compiling onnxruntime_autoep_test: the vendored
googletest's gtest-printers.h formats std::chrono::time_point via
std::format, whose floating-point path needs std::to_chars — gated by
libc++ availability to macOS 13.4+, while MACOSX_DEPLOYMENT_TARGET is 11.0.

main never hits this because its meta.yaml keeps gmock in host, so
onnxruntime's FetchContent FIND_PACKAGE_ARGS resolves conda-forge's
gtest/gmock instead of vendoring one. Restore that dep, scoped to osx —
Linux and Windows are green with the vendored copy, and the megabuild
prefers a minimal host env to keep the shared cmake cache stable.

Added to both the staging and python stages to keep their dependency
sets identical (cache-sharing requirement).

Resume this Claude session:
```
cd /home/mark/git/feedstock/onnxruntime-feedstock
claude --resume bceefe2e-0d08-4ad8-9b4c-3518466f1ccc
```
</details>
<details><summary>Claude's draft</summary>

The osx jobs now compile, pass all 10 ctest suites, and install — but fail
at packaging with rattler-build's "No license files were copied". That
error fires when ANY itemized license path matches nothing (the detailed
missing-path list goes to a log level the CI does not surface), and the
set of vendored _deps differs per platform (CoreML adds coremltools/fp16/
psimd on osx; deps provided from conda-forge, e.g. nlohmann_json/gtest on
osx, may not be vendored at all).

Replace the itemized _deps license list with globs:

    build-ci/Release/_deps/*/LICENSE*
    build-ci/Release/_deps/*/COPYING*

This ships the license of every dep actually vendored into the build on
each platform — including several that are compiled in but were previously
not shipped (mp11, date, dlpack, kleidiai, cutlass on cuda) — and cannot
break when the vendored set changes.

Resume this Claude session:
```
cd /home/mark/git/feedstock/onnxruntime-feedstock
claude --resume bceefe2e-0d08-4ad8-9b4c-3518466f1ccc
```
</details>
<details><summary>Claude's draft</summary>

Replace the license globs with an explicit list, conditioned on exactly the
variants that vendor each dependency. The per-variant sets were read from
the packaging logs of the passing glob-based CI run:

- base (all variants): LICENSE + abseil, date, dlpack, eigen (COPYING.MPL2
  only, EIGEN_MPL2_ONLY=ON), flatbuffers, gsl, onnx, protobuf,
  pytorch_cpuinfo, re2, safeint
- nlohmann_json: not osx (conda-forge host dep there, not vendored)
- googletest: only native non-CUDA builds (tests off ⇒ never fetched), and
  not osx (conda-forge gtest/gmock there)
- kleidiai: ARM targets (linux-aarch64, osx-arm64)
- microsoft_wil: win
- coremltools/fp16/psimd: osx (CoreML EP)

Verified with rattler-build --render-only that each variant's rendered list
matches what its CI packaging log actually copied. Not itemized: cutlass /
cudnn_frontend on CUDA builds (parity with main, which never shipped them);
can be added once a CUDA packaging log confirms the paths.

Resume this Claude session:
```
cd /home/mark/git/feedstock/onnxruntime-feedstock
claude --resume bceefe2e-0d08-4ad8-9b4c-3518466f1ccc
```
</details>
<details><summary>Claude's draft</summary>

Implements @cbourjau's suggestion from conda-forge#196: onnxruntime 1.28.0 ships an
execution provider plugin system (onnxruntime_BUILD_CUDA_EP_AS_PLUGIN=ON,
see docs/cuda_plugin_ep/ upstream) that builds the CUDA EP as a standalone,
Python-independent shared library loading into any onnxruntime core
>= 1.24.4 via the EP plugin API with load-time API version negotiation.

Recipe reorganization:
- New output onnxruntime-ep-cuda (mirrors upstream's onnxruntime-ep-cuda12/13
  wheel naming), built only on CUDA variants: configures with
  onnxruntime_BUILD_CUDA_EP_AS_PLUGIN=ON, ENABLE_PYTHON=OFF, builds only the
  onnxruntime_providers_cuda_plugin target, and ships just
  libonnxruntime_providers_cuda.so / onnxruntime_providers_cuda.dll.
- The staged onnxruntime-build / -cpp / python outputs become CPU-only
  (skip: cuda_enabled). The CUDA python builds disappear entirely — GPU
  support comes from installing onnxruntime-ep-cuda next to the CPU package
  and registering via onnxruntime.register_execution_provider_library.
- cuDNN and cuFFT are needed at build time (headers) but loaded lazily at
  runtime by the plugin; host deps kept conservative for now.
- run_constraints document the 1.24.4 minimum core version.

EXPLORATORY skips (marked in the recipe, to be removed before merge): only
the 6 linux/win CUDA jobs build while iterating, since the CPU/osx/novec
outputs are unchanged from conda-forge#196 and already green.

Resume this Claude session:
```
cd /home/mark/git/feedstock/onnxruntime-feedstock
claude --resume bceefe2e-0d08-4ad8-9b4c-3518466f1ccc
```
</details>
…026.08.09.00.32.18

Other tools:
- conda-build 26.7.0
- rattler-build 0.72.2
- rattler-build-conda-compat 1.4.19
@conda-forge-admin

conda-forge-admin commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

Hi! This is the friendly automated conda-forge-linting service.

I just wanted to let you know that I linted all conda-recipes in your PR (recipe/recipe.yaml) and found it was in an excellent condition.

I do have some suggestions for making it better though...

For recipe/recipe.yaml:

  • ℹ️ License is not an SPDX identifier (or a custom LicenseRef) nor an SPDX license expression.

Documentation on acceptable licenses can be found conda-forge.org > Docs > Maintainer Documentation > Contributing packages > SPDX Identifiers and Expressions.

This message was generated by GitHub Actions workflow run https://github.com/conda-forge/conda-forge-webservices/actions/runs/31313606262. Examine the logs at this URL for more detail.

<details><summary>Claude's draft</summary>

The plugin builds compiled fine (~1100 targets) but failed at license
packaging: with onnxruntime_ENABLE_PYTHON=OFF the plugin-only configure
never fetches dlpack, so the unconditional dlpack license entry matched
nothing (rattler-build then reports only "No license files were copied").

Condition dlpack on non-CUDA variants, and itemize the two deps the plugin
build newly vendors — cutlass and cudnn_frontend (filenames verified
against the exact archives pinned in upstream cmake/deps.txt, both
LICENSE.txt at the root). Verified the rendered CUDA list matches the
failed job's actual '-- Fetch' set one for one; kleidiai on aarch64 and
microsoft_wil on win (unconditional on WIN32) were confirmed still fetched
in plugin mode.

Resume this Claude session:
```
cd /home/mark/git/feedstock/onnxruntime-feedstock
claude --resume bceefe2e-0d08-4ad8-9b4c-3518466f1ccc
```
</details>
@cbourjau

cbourjau commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

I think we should start with a clean slate here. A lot has changed upstream. We should be able to do a clean rattler-build migration without any staging outputs when using the EP-plugin.

@hmaarrfk

hmaarrfk commented Aug 9, 2026

Copy link
Copy Markdown
Contributor Author

I think we should start with a clean slate here. A lot has changed upstream. We should be able to do a clean rattler-build migration without any staging outputs when using the EP-plugin.

You know I’m using 100% Claude.

do you prefer to use an AI yourself?

I’ve had a lot of success with conda-forge stuff and Claude.

I’m mostly trying to find the good ideas In your cleanups before trying to cleanup the whole shared library situation ;)

ill give Claude your prompt, but I just don’t want you to think I’m adding too much human value to this particular effort

(I do stand by our previous discussion regarding unvendorinf. That was before I learned how to use AI lol)

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.

3 participants