Skip to content

Build the CUDA backend outside fbcode, without the LLM cache - #24817

Open
mattiasbrand wants to merge 2 commits into
pytorch:mainfrom
mattiasbrand:export-D129654471
Open

mattiasbrand wants to merge 2 commits into
pytorch:mainfrom
mattiasbrand:export-D129654471

Conversation

@mattiasbrand

@mattiasbrand mattiasbrand commented Nov 9, 2026 •

Copy link
Copy Markdown

Summary:
The CUDA backend's C++ runtime builds only in fbcode, and only with the LLM extension's KV cache linked in. So a C++ application that runs an exported vision or audio model on an NVIDIA GPU can't use it from any other Buck cell, cross-compiled aarch64 targets such as Jetson included. To run the same AOTInductor-compiled kernels it has to link LibTorch instead. XNNPACK, Vulkan and QNN already define their C++ targets in both cells; this does the same for CUDA.

  • Targets in both cells. The aoti/slim, extension/cuda and cuda/runtime C++ targets are now defined outside fbcode too. There every CUDA target carries platforms = [CXX] and compatible_with = ["ovr_config//cuda:has_cuda"], so CPU-only builds, including wildcard builds of these packages, skip them instead of failing to find cuda_runtime.h. SlimTensor storage picks up its CUDA deps through a select. The Python AOTI targets stay fbcode-only.
  • Public headers. Headers that dependents include (slim_tensor.h, storage.h, runtime_api.h, the shim and factory headers) move from headers to exported_headers, and runtime_api's CUDA dependency becomes an exported external dep. Outside fbcode headers are private to the target, so nothing could compile against them. fbcode already exports both, so nothing changes there.
  • LLM cache gated as in CMake. CMake builds cuda_kv_cache.cpp and cuda_kv_pool.cpp only with EXECUTORCH_BUILD_EXTENSION_LLM. Buck now does the same: outside fbcode the backend builds without those sources, their headers and the kv_cache dependency. Only fbcode defines the macro, because only fbcode builds the LLM extension. cuda_kv_cache.h stays an LLM-only header. cuda_backend.cpp includes it, and uses the off-graph KV cache, only when the macro is defined. requires_offgraph_kv_storage reads the program's constants, not the cache, so it moves to cuda_delegate_handle.h and runs in every build. A backend built with the LLM cache still asks for a cache under llm_cache_registry_key. One built without it refuses a program whose KV cache lives off-graph with NotSupported, and ignores a cache key passed for any other program.
  • Linking. A compiled AOTI library binds its aoti_torch_* calls to the shims in the host executable. CMake makes that work with a PUBLIC --export-dynamic on aoti_cuda_backend. Outside fbcode, cuda_backend now exports the same flag on Linux, so an executable that links it statically can load a model without adding the flag itself.
  • Build flags. The nvcc architecture flags stay fbcode-only; outside fbcode the CUDA toolchain picks the architecture. Outside fbcode the shims also undefine CPU_CAPABILITY_AVX2 and CPU_CAPABILITY_AVX512 for CUDA sources: c10 exports them on x86, and Half.h then pulls host AVX2 vec_half code into nvcc compiles. cuda-lazy maps to libcudart, and the shims link curand and thrust for rand.cu and sort.cu.

fbcode builds keep the same sources, flags and LLM cache. In a CMake build with the LLM extension, behavior is unchanged. In a CMake build without it, a cache key passed for a program that doesn't keep its KV cache off-graph is now ignored rather than rejected, as a backend with the cache already ignores it for such a program.

Differential Revision: D129654471

@pytorch-bot

pytorch-bot Bot commented Nov 9, 2026 •

Copy link
Copy Markdown

🔗 Helpful Links

🧪 See artifacts and rendered test results at hud.pytorch.org/pr/pytorch/executorch/24817

Note: Links to docs will display an error until the docs builds have been completed.

❌ You can merge normally! (4 Unrelated Failures), 4 Unclassified Failures

As of commit 2658663 with merge base 618abb8 (image):

UNCLASSIFIED FAILURES - DrCI could not classify the following jobs because the workflow did not run on the merge base. The failures may be pre-existing on trunk or introduced by this PR:

BROKEN TRUNK - The following jobs failed but were present on the merge base:

👉 Rebase onto the `viable/strict` branch to avoid these failures

This comment was automatically generated by Dr. CI and updates every 15 minutes.

@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. label Nov 9, 2026
@meta-codesync

meta-codesync Bot commented Nov 9, 2026

Copy link
Copy Markdown
Contributor

@mattiasbrand has exported this pull request. If you are a Meta employee, you can view the originating Diff in D129654471.

@github-actions

github-actions Bot commented Nov 9, 2026

Copy link
Copy Markdown

This PR needs a release notes: label

If your change should be included in the release notes (i.e. would users of this library care about this change?), please use a label starting with release notes:. This helps us keep track and include your important work in the next release notes.

To add a label, you can comment to pytorchbot, for example
@pytorchbot label "release notes: none"

For more information, see
https://github.com/pytorch/pytorch/wiki/PyTorch-AutoLabel-Bot#why-categorize-for-release-notes-and-how-does-it-work.

mattiasbrand added a commit to mattiasbrand/executorch that referenced this pull request Nov 10, 2026
…ytorch#24817)

Summary:
Pull Request resolved: pytorch#24817

The CUDA backend's C++ runtime builds only in fbcode, and only with the LLM extension's KV cache linked in. So a C++ application that runs an exported vision or audio model on an NVIDIA GPU can't use it from any other Buck cell, cross-compiled aarch64 targets such as Jetson included. To run the same AOTInductor-compiled kernels it has to link LibTorch instead. XNNPACK, Vulkan and QNN already define their C++ targets in both cells; this does the same for CUDA.

- **Targets in both cells.** The `aoti/slim`, `extension/cuda` and `cuda/runtime` C++ targets are now defined outside fbcode too. There they carry `compatible_with = ["ovr_config//cuda:has_cuda"]`, so CPU-only builds skip them instead of failing to compile, and SlimTensor `storage` picks up its CUDA deps through a `select`. The Python AOTI targets stay fbcode-only.
- **Public headers.** Headers that dependents include (`slim_tensor.h`, `storage.h`, `runtime_api.h`, the shim and factory headers) move from `headers` to `exported_headers`, and `runtime_api`'s CUDA dependency becomes an exported external dep. Outside fbcode `headers` are private to the target, so nothing could compile against them. fbcode already exports both, so nothing changes there.
- **LLM cache optional, as in CMake.** CMake already builds `cuda_kv_cache.cpp` and `cuda_kv_pool.cpp` only with `EXECUTORCH_BUILD_EXTENSION_LLM`. Buck now does the same outside fbcode: the cache code in `cuda_backend.cpp` and `cuda_kv_cache.h` is behind that define, and the define is exported so that dependents see the same declarations as the backend. CMake now exports it too (`PUBLIC` instead of `PRIVATE` on `aoti_cuda_backend`), since the header's declarations depend on it; `test_cuda_kv_cache` and `cuda_batching` include that header. fbcode keeps the cache. A backend built without it still refuses a program whose KV cache lives off-graph, because `requires_offgraph_kv_storage` reads that from the program's constants and sits outside the gate.
- **Build flags.** The `nvcc` architecture flags stay fbcode-only. Outside fbcode the shims undefine `CPU_CAPABILITY_AVX2` and `CPU_CAPABILITY_AVX512` for CUDA sources: c10 exports them on x86, and `Half.h` then pulls host AVX2 `vec_half` code into nvcc compiles. `cuda-lazy` maps to `libcudart`, and the shims link `curand` and `thrust` for `rand.cu` and `sort.cu`.

fbcode builds keep the same sources and flags, and the CMake build is untouched.

Differential Revision: D129654471
@Gasoonjia

Copy link
Copy Markdown
Contributor

@claude /pr-review

@claude

claude Bot commented Nov 10, 2026 •

Copy link
Copy Markdown

Claude finished @Gasoonjia's task in 44m 22s —— View job


Request Changes: this builds the CUDA backend in the xplat cell, but most of the newly exposed CUDA targets lack the has_cuda guard the description promises, and Buck doesn't propagate the --export-dynamic flag that statically linked consumers (the Jetson case) need to load AOTI .so files.

Detailed review

PR Review: #24817

Summary

This PR defines the aoti/slim, extension/cuda and cuda/runtime C++ targets in the xplat cell and builds the off-graph KV cache only in fbcode. The diff also contains the stacked base commit f2674fb (#24643: AOTI shim linkage and cuda_lib). Two issues block approval: only 3 of the newly exposed CUDA targets get a CPU-only guard, and xplat binaries have to add --export-dynamic themselves. There are also smaller problems in the non-LLM init path, in where the LLM gate sits, and in test coverage. "Test CUDA Builds" was still queued when this review was written.

Infrastructure

  • Most newly exposed CUDA targets have no has_cuda guard (major). The description says these targets "carry compatible_with = ["ovr_config//cuda:has_cuda"]" outside fbcode. In fact cuda_kwargs is applied only to cuda_platform, runtime_shims and cuda_backend (backends/cuda/runtime/targets.bzl:39,96,163). These targets are now defined in xplat with neither compatible_with nor platforms:

    • extension/cuda:runtime_api, :caller_stream and :cuda_allocator (extension/cuda/targets.bzl:12,29,60). The extension/cuda/BUCK comment this PR deletes said that defining caller_stream in xplat "produces a target that cannot compile (cuda_runtime.h not found)".
    • backends/aoti/slim/cuda:guard (backends/aoti/slim/cuda/targets.bzl:7) and backends/aoti/slim/c10/cuda:exception (backends/aoti/slim/c10/cuda/targets.bzl:7).
    • The deprecated stub backends/cuda/runtime:cuda_allocator (backends/cuda/runtime/targets.bzl:102).

    The select in storage (backends/aoti/slim/core/targets.bzl:27) protects only storage itself. A CPU-only, Android or Apple build over these packages, such as an xplat/executorch/... wildcard, will still try to compile them. The exception would be if the xplat cuda-lazy/libcudart target is already restricted to has_cuda, which I can't check from OSS. Give these targets the same compatible_with/platforms as the others, or correct the description. Fix this →

  • --export-dynamic doesn't travel with the shims in Buck (major). The AOTInductor .so looks up aoti_torch_* in the host process, so a static consumer needs link_whole and also an exported dynamic symbol table.

    • CMake attaches the flag as a PUBLIC link option on aoti_common (backends/aoti/CMakeLists.txt:51-54), aoti_cuda_shims (backends/cuda/CMakeLists.txt:295-296) and aoti_cuda_backend (backends/cuda/CMakeLists.txt:406-407).
    • In Buck, common_shims_slim (backends/aoti/targets.bzl:70-73) and cuda_backend set only link_whole.

    So every xplat cxx_binary that links cuda_backend has to know to add -Wl,--export-dynamic, including the Jetson binary this PR exists for. Without it, dlopen of the AOTI .so fails with undefined symbol: aoti_torch_…. The PR's own consumers already add the flag by hand (extension/pybindings/test/BUCK:103-104, backends/aoti/tests/targets.bzl:48-51). Add exported_linker_flags = ["-Wl,--export-dynamic"] to common_shims_slim or cuda_backend, following runtime/executor/targets.bzl:119 and kernels/optimized/lib_defs.bzl:265. At minimum, document the requirement on cuda_backend. Fix this →

  • cuda_lib may stop //extension/pybindings from loading in OSS Buck (lower confidence). OSS evaluates fbcode_target: exir/backend/test/BUCK:355 defines test_delegate_map_builder only through it, and .ci/scripts/unittest-buck2.sh:49 builds that target. The new cuda_lib (extension/pybindings/BUCK:74-82) is therefore instantiated in OSS too, and it would fail like this:

    1. use_cuda = True adds ("cuda", None, "cuda-lazy") to external_deps (shim_et/xplat/executorch/extension/pybindings/pybindings.bzl:88).
    2. _resolve_external_dep looks it up with a bare _EXTERNAL_DEPS[name] (shim_et/xplat/executorch/build/env_interface.bzl:69), and the table has no such key.
    3. That error would fail evaluation of the whole package, including portable_lib, _C and aten_lib.

    No OSS CI job loads this package, and I couldn't run buck2 here because the shim submodule isn't checked out. Please check with buck2 uquery //extension/pybindings/.... If it fails, skip cuda_lib when runtime.is_oss. Fix this →

  • No CUDA arch flags outside fbcode (low confidence, question). get_nvcc_arch_args() is passed only in fbcode (backends/cuda/runtime/targets.bzl:15-19), so xplat builds target whatever arch the toolchain defaults to. The int4 matmul kernels only have device code for __CUDA_ARCH__ >= 800 (backends/cuda/runtime/shims/int4mm.cuh:181-1130). If the xplat has_cuda toolchain defaults below sm_80, every _weight_int4pack_mm launch fails at ET_CUDA_KERNEL_LAUNCH_CHECK (int4mm.cuh:1020), even on an sm_87 Jetson Orin. The other kernels would still JIT from PTX. Which arch does the xplat toolchain target?

API Design

  • cuda_lib is PUBLIC but can't share a binary with LibTorch (minor). Nothing at extension/pybindings/BUCK:74-82 says that cuda_lib defines the same aoti_torch_* symbols as LibTorch. Only the test docstring mentions it (extension/pybindings/test/test_pybindings_cuda_lib.py:9-11). With visibility = ["PUBLIC"], a binary that depends on both torch and cuda_lib gets duplicate or interposed aoti_torch_* symbols. Add a comment at the target and narrow its visibility to the known consumers. Fix this →

Testing

  • The new configurations aren't exercised (minor):

    • Every Buck test in the touched packages sits behind if not is_fbcode: return (e.g. backends/cuda/runtime/targets.bzl:166). The xplat has_cuda build is therefore never compiled or linked.
    • The CMake KV-cache tests run only with EXECUTORCH_BUILD_EXTENSION_LLM. Nothing covers the new non-LLM paths in cuda_backend.cpp, including the promise in the description that a non-LLM backend "still refuses a program whose KV cache lives off-graph".

    test_common_shims_slim_link and one off-graph refusal test could run outside fbcode under the same compatible_with.

Code Quality

  • The non-LLM init path drops validation, and the LLM error message lost its hint (minor). Both come from the changes to backends/cuda/runtime/cuda_backend.cpp:

    • Both builds now use the new message at lines 593-598. In an LLM-enabled build (fbcode, or CMake with LLM on), it no longer names the llm_cache_registry_key option the caller didn't pass. It suggests instead that the backend lacks EXECUTORCH_BUILD_EXTENSION_LLM. Pick the message with #if.
    • In non-LLM builds, CMake included, resolve_cuda_cache_key (lines 114-169, called at 417-421) is now compiled out. The old #else that returned NotSupported when a cache was supplied is also gone. Malformed, duplicate or unmapped cache-key specs are therefore accepted silently, and a supplied key is ignored. This contradicts "the CMake build is untouched". Keep a small #else that rejects a supplied key, or call the change out in the description.

    Fix this →

  • The LLM gate misses two exported declarations (minor). Outside fbcode, cuda_kv_cache.cpp and cuda_kv_pool.cpp aren't built (backends/cuda/runtime/targets.bzl:118). Their declarations are still exported:

    • attach_offgraph_kv_cache is declared just above the new #if (backends/cuda/runtime/cuda_kv_cache.h:194-201).
    • cuda_kv_pool.h is still exported (targets.bzl:123), and its constructor and destructor are defined out of line (cuda_kv_pool.h:55,66).

    A dependent that uses either compiles cleanly and then fails to link. Move the #if above the declaration, and export cuda_kv_pool.h only in fbcode. Fix this →

  • The LLM cache isn't actually optional in Buck (minor, design question). The title and the "LLM cache optional, as in CMake" bullet suggest a switch. In fact the cell decides (backends/cuda/runtime/targets.bzl:118,134,143): an xplat LLM runner, such as one on Jetson, can never get the off-graph KV cache, and fbcode can't opt out. If that is intended, say so in the title and description. Otherwise, gate it on a config or select.

Recommendation

Request Changes

The point of the PR is to let a Buck cell outside fbcode link the CUDA backend. That cell currently gets six CUDA targets with no CPU-only guard, which can break wildcard and CPU builds. It also gets a link requirement that every consumer has to rediscover. Please fix those two, and confirm or rule out the OSS pybindings load failure. The remaining items can be follow-ups.


  • Gather context (diff, commits, changed files): the PR diff covers 2 commits, the stacked base f2674fb (Fix CUDA AOTI shim linkage in Buck (#24643) #24643) and 251ee69
  • Run independent reviews (3 reviewers)
  • Consolidate findings
  • Fact-check findings
  • Post final review

Comment thread backends/cuda/runtime/cuda_kv_cache.h Outdated
#include <executorch/backends/cuda/runtime/backend_options.h>
#include <executorch/backends/cuda/runtime/cuda_delegate_handle.h>
#include <executorch/extension/cuda/runtime_api.h>
#if defined(EXECUTORCH_BUILD_EXTENSION_LLM)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

EXECUTORCH_BUILD_EXTENSION_LLM is must-have for cuda_kv_cache and our cmake is alreaady guard it.

I think we should:

  1. follow the same way in buck target
  2. In the places including cuda_kv_cache.h like cuda_backend.h, we use EXECUTORCH_BUILD_EXTENSION_LLM marco to guard it.

Summary:
Pull Request resolved: pytorch#24643

Make the Buck CUDA runtime link its AOTI shims the way the CMake build already does.

A compiled AOTInductor shared library does not contain the tensor runtime. When the CUDA backend loads it, the library resolves its `aoti_torch_*` shim calls from the host process by name. Nothing in the host code calls most of these functions directly, so a static link without whole-archive drops them from `common_shims_slim` and loading fails with unresolved symbols. CMake already links `aoti_common_shims_slim` with `--whole-archive`; this sets `link_whole = True` on the Buck target to match, along with `supports_python_dlopen = True` like the other force-linked AOTI and CUDA targets. An executable that loads an AOTI library also has to export these symbols, as CMake does with `-Wl,--export-dynamic`. That includes a Buck `python_binary` that uses the Python bindings, because the extension links into its native executable.

`backends/aoti/tests:test_common_shims_slim_link` looks every shim up by name in a statically linked test binary that never calls them. It fails for every shim when `link_whole` is removed. `test_common_shims_slim` cannot catch this, because it calls the shims directly and that keeps them linked.

`backends/cuda/runtime/shims/tensor_attribute.cpp` defined `aoti_torch_get_device_type` and `aoti_torch_device_type_cuda` for ETensor, which the CUDA backend no longer uses. `common_shims_slim` defines the same `extern "C"` symbols for SlimTensor, so once both are force-linked they collide. CMake already leaves this file out of the CUDA shim sources. This deletes the file and its header and removes them from the Buck target.

`executorch_pybindings(use_cuda = True)` compiles code that calls the CUDA runtime directly but did not declare the CUDA dependency, so such a target could not link. The macro now adds it when `use_cuda` is set, together with `//executorch/extension/cuda:cuda_allocator`, which `pybindings_cuda.h` includes and calls. Previously that dependency was only met indirectly through the AOTI CUDA backend.

`//executorch/extension/pybindings:cuda_lib` builds these bindings with the AOTI CUDA backend. LibTorch defines the same `aoti_torch_*` symbols with a different tensor handle ABI, so it cannot link into the same binary; the target says so, and is visible only to the pybindings packages. OSS Buck has no CUDA backend targets and no mapping for the CUDA dependency, so `executorch_pybindings(use_cuda = True)` defines nothing there; the CMake build links CUDA into `portable_lib` instead. `extension/pybindings/test:test_pybindings_cuda_lib` exports a CUDA-delegated program with external weights, runs it through `cuda_lib` in a separate LibTorch-free binary, and compares the output with eager PyTorch. Without `link_whole`, or without `-Wl,--export-dynamic` on that binary, it fails with `undefined symbol: aoti_torch_dtype_float32`. `pytest.ini` and `pytest-windows.ini` leave it out of the pytest runs, because it needs a GPU and the Buck-built runner.

Differential Revision: D129530257
Summary:
The CUDA backend's C++ runtime builds only in fbcode, and only with the LLM extension's KV cache linked in. So a C++ application that runs an exported vision or audio model on an NVIDIA GPU can't use it from any other Buck cell, cross-compiled aarch64 targets such as Jetson included. To run the same AOTInductor-compiled kernels it has to link LibTorch instead. XNNPACK, Vulkan and QNN already define their C++ targets in both cells; this does the same for CUDA.

- **Targets in both cells.** The `aoti/slim`, `extension/cuda` and `cuda/runtime` C++ targets are now defined outside fbcode too. There every CUDA target carries `platforms = [CXX]` and `compatible_with = ["ovr_config//cuda:has_cuda"]`, so CPU-only builds, including wildcard builds of these packages, skip them instead of failing to find `cuda_runtime.h`. SlimTensor `storage` picks up its CUDA deps through a `select`. The Python AOTI targets stay fbcode-only.
- **Public headers.** Headers that dependents include (`slim_tensor.h`, `storage.h`, `runtime_api.h`, the shim and factory headers) move from `headers` to `exported_headers`, and `runtime_api`'s CUDA dependency becomes an exported external dep. Outside fbcode `headers` are private to the target, so nothing could compile against them. fbcode already exports both, so nothing changes there.
- **LLM cache gated as in CMake.** CMake builds `cuda_kv_cache.cpp` and `cuda_kv_pool.cpp` only with `EXECUTORCH_BUILD_EXTENSION_LLM`. Buck now does the same: outside fbcode the backend builds without those sources, their headers and the `kv_cache` dependency. Only fbcode defines the macro, because only fbcode builds the LLM extension. `cuda_kv_cache.h` stays an LLM-only header. `cuda_backend.cpp` includes it, and uses the off-graph KV cache, only when the macro is defined. `requires_offgraph_kv_storage` reads the program's constants, not the cache, so it moves to `cuda_delegate_handle.h` and runs in every build. A backend built with the LLM cache still asks for a cache under `llm_cache_registry_key`. One built without it refuses a program whose KV cache lives off-graph with `NotSupported`, and ignores a cache key passed for any other program.
- **Linking.** A compiled AOTI library binds its `aoti_torch_*` calls to the shims in the host executable. CMake makes that work with a `PUBLIC` `--export-dynamic` on `aoti_cuda_backend`. Outside fbcode, `cuda_backend` now exports the same flag on Linux, so an executable that links it statically can load a model without adding the flag itself.
- **Build flags.** The `nvcc` architecture flags stay fbcode-only; outside fbcode the CUDA toolchain picks the architecture. Outside fbcode the shims also undefine `CPU_CAPABILITY_AVX2` and `CPU_CAPABILITY_AVX512` for CUDA sources: c10 exports them on x86, and `Half.h` then pulls host AVX2 `vec_half` code into nvcc compiles. `cuda-lazy` maps to `libcudart`, and the shims link `curand` and `thrust` for `rand.cu` and `sort.cu`.

fbcode builds keep the same sources, flags and LLM cache. In a CMake build with the LLM extension, behavior is unchanged. In a CMake build without it, a cache key passed for a program that doesn't keep its KV cache off-graph is now ignored rather than rejected, as a backend with the cache already ignores it for such a program.

Differential Revision: D129654471
@meta-codesync meta-codesync Bot changed the title Build the CUDA backend outside fbcode, with the LLM cache optional Build the CUDA backend outside fbcode, without the LLM cache Nov 10, 2026

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

CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. meta-exported

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants