From f446cc061ee4d717ccc73e2c588ec8966dadbdfc Mon Sep 17 00:00:00 2001 From: shoumikhin Date: Mon, 3 Aug 2026 22:23:09 -0700 Subject: [PATCH 1/5] Update [ghstack-poisoned] --- .ci/scripts/wheel/cuda_arch_list.sh | 78 +++++++++++++++++++ .ci/scripts/wheel/envvar_linux.sh | 15 ++++ .../workflows/build-wheels-aarch64-linux.yml | 7 +- .github/workflows/build-wheels-linux.yml | 7 +- setup.py | 36 ++++++++- 5 files changed, 140 insertions(+), 3 deletions(-) create mode 100644 .ci/scripts/wheel/cuda_arch_list.sh diff --git a/.ci/scripts/wheel/cuda_arch_list.sh b/.ci/scripts/wheel/cuda_arch_list.sh new file mode 100644 index 00000000000..46d16266397 --- /dev/null +++ b/.ci/scripts/wheel/cuda_arch_list.sh @@ -0,0 +1,78 @@ +#!/usr/bin/env bash +# Copyright (c) Meta Platforms, Inc. and affiliates. +# All rights reserved. +# +# This source code is licensed under the BSD-style license found in the +# LICENSE file in the root directory of this source tree. + +# GPU architectures to compile device code for, chosen per release row rather than detected +# from the build machine. +# +# Without this, the build compiles for whichever GPU the builder happens to have. The wheel +# then installs on every machine the row claims and fails when a model runs on a different +# generation. Detection is the right default for a local build and the wrong one for a +# published artifact. +# +# The value is published as TORCH_CUDA_ARCH_LIST rather than CMAKE_CUDA_ARCHITECTURES. +# PyTorch's own CMake explicitly rejects the latter and overrides it with OFF, so setting it +# alone silently reduces the build to a single detected architecture. + +# Data center and desktop parts on the CUDA 13 trains: Ampere, Hopper, Blackwell data center, +# and Blackwell desktop. +_cuda_arch_x86_64_cu130="8.0 9.0 10.0 12.0" +_cuda_arch_x86_64_cu132="${_cuda_arch_x86_64_cu130}" + +# Server-class ARM plus the Jetson modules whose CUDA train matches: Hopper for Grace-Hopper, +# Blackwell data center for GB200, and Thor. +_cuda_arch_aarch64_cu130="9.0 10.0 11.0" +_cuda_arch_aarch64_cu132="${_cuda_arch_aarch64_cu130}" + +# The older CUDA train, where Orin is the target. +_cuda_arch_aarch64_cu126="8.7" +_cuda_arch_x86_64_cu126="8.0 9.0" + +# The architectures for the current row, space separated in the dotted form PyTorch expects. +# Empty when the row is unknown, which leaves the build detecting as before. +executorch_cuda_arch_list() { + local machine + machine="$(uname -m)" + local train="${DESIRED_CUDA:-}" + if [ -z "${train}" ]; then + return 0 + fi + # DESIRED_CUDA arrives as cu130; some callers pass 13.0 instead. + train="${train#cu}" + train="${train//./}" + + case "${machine}" in + aarch64 | arm64) + case "${train}" in + 126) printf '%s' "${_cuda_arch_aarch64_cu126}" ;; + 130) printf '%s' "${_cuda_arch_aarch64_cu130}" ;; + 132) printf '%s' "${_cuda_arch_aarch64_cu132}" ;; + esac + ;; + x86_64) + case "${train}" in + 126) printf '%s' "${_cuda_arch_x86_64_cu126}" ;; + 130) printf '%s' "${_cuda_arch_x86_64_cu130}" ;; + 132) printf '%s' "${_cuda_arch_x86_64_cu132}" ;; + esac + ;; + esac +} + +# The same architectures in CMake's own form, for targets outside PyTorch's CMake. +executorch_cuda_cmake_arch_list() { + local dotted + dotted="$(executorch_cuda_arch_list)" + if [ -z "${dotted}" ]; then + return 0 + fi + local out="" entry + for entry in ${dotted}; do + entry="${entry//./}" + out="${out:+${out};}${entry}-real" + done + printf '%s' "${out}" +} diff --git a/.ci/scripts/wheel/envvar_linux.sh b/.ci/scripts/wheel/envvar_linux.sh index 3b24b3f7188..431e376c86e 100755 --- a/.ci/scripts/wheel/envvar_linux.sh +++ b/.ci/scripts/wheel/envvar_linux.sh @@ -9,3 +9,18 @@ # any variables so that subprocesses will see them. source "${GITHUB_WORKSPACE}/${REPOSITORY}/.ci/scripts/wheel/envvar_base.sh" + +# Compile device code for the GPU architectures this release row claims, rather than for +# whichever GPU the builder happens to have. A wheel built with detection alone installs on +# every machine the row covers and then fails when a model runs on a different generation. +source "${GITHUB_WORKSPACE}/${REPOSITORY}/.ci/scripts/wheel/cuda_arch_list.sh" +_executorch_cuda_arch="$(executorch_cuda_arch_list)" +if [ -n "${_executorch_cuda_arch}" ]; then + # PyTorch's CMake rejects CMAKE_CUDA_ARCHITECTURES and overrides it with OFF, which leaves + # the build compiling for one detected architecture, so the list has to go through the + # variable PyTorch reads. Both are set: targets that go through PyTorch's CMake honour the + # first, and any that do not honour the second. + export TORCH_CUDA_ARCH_LIST="${_executorch_cuda_arch}" + export CMAKE_ARGS="${CMAKE_ARGS} -DCMAKE_CUDA_ARCHITECTURES=$(executorch_cuda_cmake_arch_list)" + echo "CUDA architectures for this row: ${_executorch_cuda_arch}" +fi diff --git a/.github/workflows/build-wheels-aarch64-linux.yml b/.github/workflows/build-wheels-aarch64-linux.yml index 8adf4268228..fc01fa0294b 100644 --- a/.github/workflows/build-wheels-aarch64-linux.yml +++ b/.github/workflows/build-wheels-aarch64-linux.yml @@ -30,7 +30,12 @@ jobs: os: linux-aarch64 test-infra-repository: pytorch/test-infra test-infra-ref: main - with-cuda: disabled + # CUDA enabled so the accelerator rows are built and published. The generator + # emits one cell per supported CUDA train, and each cell carries its own local + # version label so the CPU and accelerator artifacts stay distinguishable. A plain + # `pip install executorch` continues to resolve the CPU wheel from the default + # index; an accelerator wheel requires pointing at the matching index. + with-cuda: enable with-rocm: disabled python-versions: '["3.10", "3.11", "3.12", "3.13", "3.14"]' diff --git a/.github/workflows/build-wheels-linux.yml b/.github/workflows/build-wheels-linux.yml index 7428b68a773..49a80148ec6 100644 --- a/.github/workflows/build-wheels-linux.yml +++ b/.github/workflows/build-wheels-linux.yml @@ -30,7 +30,12 @@ jobs: os: linux test-infra-repository: pytorch/test-infra test-infra-ref: main - with-cuda: disabled + # CUDA enabled so the accelerator rows are built and published. The generator + # emits one cell per supported CUDA train, and each cell carries its own local + # version label so the CPU and accelerator artifacts stay distinguishable. A plain + # `pip install executorch` continues to resolve the CPU wheel from the default + # index; an accelerator wheel requires pointing at the matching index. + with-cuda: enable with-rocm: disabled python-versions: '["3.10", "3.11", "3.12", "3.13", "3.14"]' diff --git a/setup.py b/setup.py index 58258c1491d..ea978af1592 100644 --- a/setup.py +++ b/setup.py @@ -108,6 +108,40 @@ def _is_minimal_build() -> bool: return _is_env_flag_enabled("EXECUTORCH_BUILD_MINIMAL") +def _cuda_train() -> str: + """The CUDA train this wheel is being built for, as a bare number like "130". + + Read from the wheel build environment rather than detected from an installed compiler. + A CPU wheel built on a machine that happens to have a CUDA toolkit must not declare CUDA + dependencies, and detection cannot tell the two cases apart. + """ + train = os.environ.get("DESIRED_CUDA", "").strip().lower() + if not train: + return "" + train = train.removeprefix("cu").replace(".", "") + return train if train.isdigit() else "" + + +def _cuda_dependencies() -> List[str]: + """Runtime libraries a CUDA wheel needs but does not bundle. + + Empty for a CPU wheel, and empty for a build whose CUDA train is unknown, so the CPU rows + are unaffected. The major decides the package suffix, matching how these are published. + """ + train = _cuda_train() + if not train or not _is_env_flag_enabled("EXECUTORCH_BUILD_CUDA"): + return [] + major = train[:2] if train.startswith("13") else train[:2] + suffix = f"cu{major}" + # Only what the delegate and its shim actually link. A shorter list keeps a CUDA install + # from pulling in libraries nothing in this wheel references. + return [ + f"nvidia-cuda-runtime-{suffix}; platform_system == 'Linux'", + f"nvidia-curand-{suffix}; platform_system == 'Linux'", + f"nvidia-cublas-{suffix}; platform_system == 'Linux'", + ] + + def _minimal_cmake_flags() -> List[str]: return [ "-DEXECUTORCH_BUILD_COREML=OFF", @@ -1205,7 +1239,7 @@ def run(self): # noqa C901 setup_kwargs["packages"] = _minimal_packages() setup_kwargs["install_requires"] = _minimal_dependencies() else: - setup_kwargs["install_requires"] = _base_dependencies() + setup_kwargs["install_requires"] = _base_dependencies() + _cuda_dependencies() setup( From a0fb40fab40c946c2ea854485846cdc1153d2fe0 Mon Sep 17 00:00:00 2001 From: shoumikhin Date: Tue, 4 Aug 2026 00:00:08 -0700 Subject: [PATCH 2/5] Update [ghstack-poisoned] --- .ci/scripts/wheel/cuda_arch_list.sh | 7 +++-- setup.py | 41 ++++++++++++++++++++--------- 2 files changed, 33 insertions(+), 15 deletions(-) diff --git a/.ci/scripts/wheel/cuda_arch_list.sh b/.ci/scripts/wheel/cuda_arch_list.sh index 46d16266397..f7b72089410 100644 --- a/.ci/scripts/wheel/cuda_arch_list.sh +++ b/.ci/scripts/wheel/cuda_arch_list.sh @@ -36,11 +36,14 @@ _cuda_arch_x86_64_cu126="8.0 9.0" executorch_cuda_arch_list() { local machine machine="$(uname -m)" - local train="${DESIRED_CUDA:-}" + # The wheel build exports the row's CUDA train as CU_VERSION. DESIRED_CUDA is the name of + # the matrix field, not of the variable, so reading only that one leaves every row falling + # back to detecting the builder's GPU. + local train="${CU_VERSION:-${DESIRED_CUDA:-}}" if [ -z "${train}" ]; then return 0 fi - # DESIRED_CUDA arrives as cu130; some callers pass 13.0 instead. + # The value arrives as cu130; some callers pass 13.0 instead. train="${train#cu}" train="${train//./}" diff --git a/setup.py b/setup.py index ea978af1592..ae9360d1e93 100644 --- a/setup.py +++ b/setup.py @@ -114,32 +114,47 @@ def _cuda_train() -> str: Read from the wheel build environment rather than detected from an installed compiler. A CPU wheel built on a machine that happens to have a CUDA toolkit must not declare CUDA dependencies, and detection cannot tell the two cases apart. + + The wheel build exports this as CU_VERSION; DESIRED_CUDA is the matrix field name and is + accepted only so local invocations keep working. """ - train = os.environ.get("DESIRED_CUDA", "").strip().lower() + train = ( + (os.environ.get("CU_VERSION") or os.environ.get("DESIRED_CUDA") or "") + .strip() + .lower() + ) if not train: return "" train = train.removeprefix("cu").replace(".", "") return train if train.isdigit() else "" +# The published project names for the CUDA runtime components this wheel links but does not +# bundle, keyed by CUDA major. These are not derivable from a suffix rule: the CUDA 12 wheels +# carry a "-cu12" suffix while the CUDA 13 wheels are published under unsuffixed names, and the +# suffixed CUDA 13 projects are placeholders that ship no binaries. A train with no entry gets +# no declared dependencies, which is safer than requesting a name that may not exist. +_CUDA_RUNTIME_PACKAGES = { + "12": ("nvidia-cuda-runtime-cu12", "nvidia-curand-cu12", "nvidia-cublas-cu12"), + "13": ("nvidia-cuda-runtime", "nvidia-curand", "nvidia-cublas"), +} + + def _cuda_dependencies() -> List[str]: """Runtime libraries a CUDA wheel needs but does not bundle. - Empty for a CPU wheel, and empty for a build whose CUDA train is unknown, so the CPU rows - are unaffected. The major decides the package suffix, matching how these are published. + Empty for a CPU wheel, and empty for a build whose CUDA train is unknown or unmapped, so + the CPU rows are unaffected. """ train = _cuda_train() if not train or not _is_env_flag_enabled("EXECUTORCH_BUILD_CUDA"): return [] - major = train[:2] if train.startswith("13") else train[:2] - suffix = f"cu{major}" + packages = _CUDA_RUNTIME_PACKAGES.get(train[:2]) + if not packages: + return [] # Only what the delegate and its shim actually link. A shorter list keeps a CUDA install # from pulling in libraries nothing in this wheel references. - return [ - f"nvidia-cuda-runtime-{suffix}; platform_system == 'Linux'", - f"nvidia-curand-{suffix}; platform_system == 'Linux'", - f"nvidia-cublas-{suffix}; platform_system == 'Linux'", - ] + return [f"{name}; platform_system == 'Linux'" for name in packages] def _minimal_cmake_flags() -> List[str]: @@ -686,9 +701,9 @@ def __init__( modpath: The dotted path of the python module that maps to the extension. """ - assert "/" not in modpath, ( - f"modpath must be a dotted python module path: saw '{modpath}'" - ) + assert ( + "/" not in modpath + ), f"modpath must be a dotted python module path: saw '{modpath}'" full_src = src if src_dir is None and _is_windows(): src_dir = "%BUILD_TYPE%/" From 930237346ecdf2d0d87b4b99ee82f5ef40c32428 Mon Sep 17 00:00:00 2001 From: shoumikhin Date: Tue, 4 Aug 2026 00:16:10 -0700 Subject: [PATCH 3/5] Update [ghstack-poisoned] --- .ci/scripts/wheel/cuda_arch_list.sh | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/.ci/scripts/wheel/cuda_arch_list.sh b/.ci/scripts/wheel/cuda_arch_list.sh index f7b72089410..27db86188e3 100644 --- a/.ci/scripts/wheel/cuda_arch_list.sh +++ b/.ci/scripts/wheel/cuda_arch_list.sh @@ -31,6 +31,15 @@ _cuda_arch_aarch64_cu132="${_cuda_arch_aarch64_cu130}" _cuda_arch_aarch64_cu126="8.7" _cuda_arch_x86_64_cu126="8.0 9.0" +# A CUDA train with no architecture list would otherwise leave the build detecting the +# builder's GPU, which is the failure this file exists to prevent. Adding a train to the release +# matrix without adding its architectures should fail loudly. +_executorch_unknown_train() { + echo "cuda_arch_list.sh: no GPU architecture list for CUDA train '$1' on $(uname -m)." >&2 + echo "Add one before building this row, or the wheel ships device code for one GPU only." >&2 + return 64 +} + # The architectures for the current row, space separated in the dotted form PyTorch expects. # Empty when the row is unknown, which leaves the build detecting as before. executorch_cuda_arch_list() { @@ -40,9 +49,10 @@ executorch_cuda_arch_list() { # the matrix field, not of the variable, so reading only that one leaves every row falling # back to detecting the builder's GPU. local train="${CU_VERSION:-${DESIRED_CUDA:-}}" - if [ -z "${train}" ]; then - return 0 - fi + # A CPU row names no CUDA train and needs no architectures, so it is not an error. + case "${train}" in + "" | cpu | CPU | none | NONE) return 0 ;; + esac # The value arrives as cu130; some callers pass 13.0 instead. train="${train#cu}" train="${train//./}" @@ -53,6 +63,7 @@ executorch_cuda_arch_list() { 126) printf '%s' "${_cuda_arch_aarch64_cu126}" ;; 130) printf '%s' "${_cuda_arch_aarch64_cu130}" ;; 132) printf '%s' "${_cuda_arch_aarch64_cu132}" ;; + *) _executorch_unknown_train "${train}" ;; esac ;; x86_64) @@ -60,8 +71,10 @@ executorch_cuda_arch_list() { 126) printf '%s' "${_cuda_arch_x86_64_cu126}" ;; 130) printf '%s' "${_cuda_arch_x86_64_cu130}" ;; 132) printf '%s' "${_cuda_arch_x86_64_cu132}" ;; + *) _executorch_unknown_train "${train}" ;; esac ;; + *) _executorch_unknown_train "${train}" ;; esac } From 29d8d2c00da1f76b46bb3b7b9afdaaf202dff7cf Mon Sep 17 00:00:00 2001 From: shoumikhin Date: Tue, 4 Aug 2026 08:48:17 -0700 Subject: [PATCH 4/5] Update [ghstack-poisoned] --- setup.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index d6058843287..028fd79c49a 100644 --- a/setup.py +++ b/setup.py @@ -147,8 +147,12 @@ def _cuda_dependencies() -> List[str]: the CPU rows are unaffected. """ train = _cuda_train() - if not train or not _is_env_flag_enabled("EXECUTORCH_BUILD_CUDA"): + if not train: return [] + # The CUDA train alone decides this. An earlier version also required + # EXECUTORCH_BUILD_CUDA, but that reaches the build as a CMake argument rather than an + # environment variable, so the condition was never true and a CUDA wheel shipped with no + # declared CUDA dependencies at all. packages = _CUDA_RUNTIME_PACKAGES.get(train[:2]) if not packages: return [] From 78974a16093027024e635a2d2bf06b9ce7fe57bc Mon Sep 17 00:00:00 2001 From: shoumikhin Date: Tue, 4 Aug 2026 20:59:01 -0700 Subject: [PATCH 5/5] Update [ghstack-poisoned] --- .ci/scripts/wheel/cuda_arch_list.sh | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/.ci/scripts/wheel/cuda_arch_list.sh b/.ci/scripts/wheel/cuda_arch_list.sh index 27db86188e3..989b838cf47 100644 --- a/.ci/scripts/wheel/cuda_arch_list.sh +++ b/.ci/scripts/wheel/cuda_arch_list.sh @@ -27,8 +27,12 @@ _cuda_arch_x86_64_cu132="${_cuda_arch_x86_64_cu130}" _cuda_arch_aarch64_cu130="9.0 10.0 11.0" _cuda_arch_aarch64_cu132="${_cuda_arch_aarch64_cu130}" -# The older CUDA train, where Orin is the target. -_cuda_arch_aarch64_cu126="8.7" +# The older CUDA train on generic ARM. These are the server parts that train supports, matching +# what the x86_64 row of the same train claims. Jetson is deliberately not here: a Jetson-only +# architecture such as Orin's 8.7 in a generic manylinux wheel would advertise a device the row +# cannot otherwise serve, since a Jetson also needs the pinned CUDA, TensorRT and PyTorch from its +# own software release rather than the ones a generic wheel resolves. +_cuda_arch_aarch64_cu126="8.0 9.0" _cuda_arch_x86_64_cu126="8.0 9.0" # A CUDA train with no architecture list would otherwise leave the build detecting the