From 76f47d4a26b4ebed44d343e975a3f033883e6d07 Mon Sep 17 00:00:00 2001 From: shoumikhin Date: Mon, 3 Aug 2026 21:35:21 -0700 Subject: [PATCH 1/7] Update [ghstack-poisoned] --- .ci/scripts/wheel/cuda_arch_list.sh | 60 +++++++++++++++++++ .ci/scripts/wheel/envvar_linux.sh | 10 ++++ .../workflows/build-wheels-aarch64-linux.yml | 7 ++- .github/workflows/build-wheels-linux.yml | 7 ++- setup.py | 36 ++++++++++- 5 files changed, 117 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..d69f2535d7c --- /dev/null +++ b/.ci/scripts/wheel/cuda_arch_list.sh @@ -0,0 +1,60 @@ +#!/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, CMake 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 GPU +# generation. Detection is the right default for a local build and the wrong one for a +# published artifact. +# +# CUDA_ARCH_LIST_ names the architectures for that CUDA train. DESIRED_CUDA is +# supplied by the wheel build as cu126, cu130, and so on. + +# Data center and desktop parts on the CUDA 13 trains: Ampere, Hopper, Blackwell data center, +# and Blackwell desktop. +_cuda_arch_x86_64_cu130="80-real;90-real;100-real;120-real" +_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="90-real;100-real;110-real" +_cuda_arch_aarch64_cu132="${_cuda_arch_aarch64_cu130}" + +# The older CUDA train, where Orin is the target. +_cuda_arch_aarch64_cu126="87-real" +_cuda_arch_x86_64_cu126="80-real;90-real" + +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 +} diff --git a/.ci/scripts/wheel/envvar_linux.sh b/.ci/scripts/wheel/envvar_linux.sh index 3b24b3f7188..1d4c972f4d6 100755 --- a/.ci/scripts/wheel/envvar_linux.sh +++ b/.ci/scripts/wheel/envvar_linux.sh @@ -9,3 +9,13 @@ # 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 + export CMAKE_ARGS="${CMAKE_ARGS} -DCMAKE_CUDA_ARCHITECTURES=${_executorch_cuda_arch}" + 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 d84247675332835524fa153281102ce43b7d189a Mon Sep 17 00:00:00 2001 From: shoumikhin Date: Mon, 3 Aug 2026 21:35:26 -0700 Subject: [PATCH 2/7] Update [ghstack-poisoned] --- .ci/scripts/wheel/test_cpp_sdk.py | 99 ++++++++++++++++++++++++++++--- 1 file changed, 91 insertions(+), 8 deletions(-) diff --git a/.ci/scripts/wheel/test_cpp_sdk.py b/.ci/scripts/wheel/test_cpp_sdk.py index 8b0b0d772e5..678aec2bdae 100644 --- a/.ci/scripts/wheel/test_cpp_sdk.py +++ b/.ci/scripts/wheel/test_cpp_sdk.py @@ -351,10 +351,7 @@ def test_shipped_libraries_load() -> None: # list instead would hide a genuinely under-linked symbol on the same wheel. # The leading-underscore forms matter too: nvcc emits host stubs such as # __cudaRegisterFatBinary for every compiled .cu file. - and not ( - skip_undefined - and re.search(_CUDA_SYMBOL, line) - ) + and not (skip_undefined and re.search(_CUDA_SYMBOL, line)) ] if undefined: unresolved[str(library.relative_to(package_dir))] = undefined[:5] @@ -1087,9 +1084,17 @@ def test_documented_example_compiles(work_dir: Path) -> None: build_dir = work_dir / "documented-build" configure = subprocess.run( - ["cmake", "-S", str(source_dir), "-B", str(build_dir), - f"-DCMAKE_PREFIX_PATH={_installed_package_dir()}"], - capture_output=True, text=True, check=False, + [ + "cmake", + "-S", + str(source_dir), + "-B", + str(build_dir), + f"-DCMAKE_PREFIX_PATH={_installed_package_dir()}", + ], + capture_output=True, + text=True, + check=False, ) assert configure.returncode == 0, ( "the documented example does not configure against the installed wheel: " @@ -1097,7 +1102,9 @@ def test_documented_example_compiles(work_dir: Path) -> None: ) build = subprocess.run( ["cmake", "--build", str(build_dir)], - capture_output=True, text=True, check=False, + capture_output=True, + text=True, + check=False, ) assert build.returncode == 0, ( "the documented example does not compile against the installed wheel: " @@ -1149,6 +1156,81 @@ def test_python_extension_links_shared_runtime() -> None: ) +def _requested_cuda_architectures() -> list: + """GPU architectures this wheel was built to support, from the build environment. + + Empty when the build did not name any, which is the case for a CPU wheel and for a local + build that used detection. + """ + raw = os.environ.get("CMAKE_CUDA_ARCHITECTURES", "") + if not raw: + match = re.search( + r"-DCMAKE_CUDA_ARCHITECTURES=([^\s]+)", os.environ.get("CMAKE_ARGS", "") + ) + raw = match.group(1) if match else "" + if not raw: + return [] + found = [] + for entry in raw.replace(",", ";").split(";"): + number = re.match(r"(\d+)", entry.strip()) + # A "-virtual" entry asks for a portable format rather than compiled code for that + # architecture, so it is not expected to appear as device code. + if number and "virtual" not in entry: + found.append(number.group(1)) + return found + + +def test_device_code_covers_claimed_architectures() -> None: + """Every GPU architecture the row claims must be present as compiled device code. + + A wheel whose device code covers only the build machine's GPU installs on all the hardware + the row promises and then fails when a model runs. That failure appears late and looks like + a model problem rather than a packaging one, so it is worth catching here. + """ + claimed = _requested_cuda_architectures() + if not claimed: + print("- this build named no GPU architectures, skipping the device code check") + return + if shutil.which("cuobjdump") is None: + print("- cuobjdump is not available, skipping the device code check") + return + + package_dir = _installed_package_dir() + libraries = [ + library + for library in _shipped_shared_objects(package_dir) + if "cuda" in library.name or "aoti" in library.name + ] + if not libraries: + print( + "- this wheel ships no accelerator libraries, skipping the device code check" + ) + return + + present = set() + for library in libraries: + result = subprocess.run( + ["cuobjdump", "--list-elf", str(library)], + capture_output=True, + text=True, + check=False, + ) + if result.returncode != 0: + continue + present.update(re.findall(r"sm_(\d+)", result.stdout)) + + missing = sorted(set(claimed) - present, key=int) + assert not missing, ( + f"this row claims GPU architectures {sorted(claimed, key=int)} but the shipped " + f"libraries only carry device code for {sorted(present, key=int)}; a model would " + f"fail on hardware needing {missing}" + ) + print( + f"\u2713 device code covers every claimed GPU architecture: " + f"{sorted(claimed, key=int)}" + ) + + def run_tests(work_dir: Path) -> None: test_shipped_libraries_load() test_shipped_libraries_resolve_without_build_tree() @@ -1162,6 +1244,7 @@ def run_tests(work_dir: Path) -> None: test_single_kernel_registration() test_single_xnnpack_delegate() test_single_cuda_delegate() + test_device_code_covers_claimed_architectures() test_cpp_consumer(work_dir) test_documented_example_compiles(work_dir) test_component_targets_link(work_dir) From b140d21d198787f45d12a924eb5d6dec8b7216e4 Mon Sep 17 00:00:00 2001 From: shoumikhin Date: Mon, 3 Aug 2026 23:34:40 -0700 Subject: [PATCH 3/7] 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 9ec50809348d28f88e7f889d44b0edaef623f210 Mon Sep 17 00:00:00 2001 From: shoumikhin Date: Mon, 3 Aug 2026 23:38:21 -0700 Subject: [PATCH 4/7] 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 81ee8658b925882b8ead0e95520eaf000587cbf3 Mon Sep 17 00:00:00 2001 From: shoumikhin Date: Tue, 4 Aug 2026 00:07:44 -0700 Subject: [PATCH 5/7] Update [ghstack-poisoned] --- .ci/scripts/wheel/test_cpp_sdk.py | 87 +++++++++++++++++++++++++------ 1 file changed, 70 insertions(+), 17 deletions(-) diff --git a/.ci/scripts/wheel/test_cpp_sdk.py b/.ci/scripts/wheel/test_cpp_sdk.py index 678aec2bdae..19c0df20934 100644 --- a/.ci/scripts/wheel/test_cpp_sdk.py +++ b/.ci/scripts/wheel/test_cpp_sdk.py @@ -1180,19 +1180,50 @@ def _requested_cuda_architectures() -> list: return found +def _is_accelerator_row() -> bool: + """Whether this build is an accelerator row rather than a CPU wheel. + + Taken from the build variable that names the row's CUDA train, which is the same signal + the rest of the wheel build uses. A CPU row leaves it unset or names no CUDA train. + """ + train = ( + os.environ.get("CU_VERSION") or os.environ.get("DESIRED_CUDA") or "" + ).strip() + return bool(train) and train.lower() not in {"cpu", "none"} + + def test_device_code_covers_claimed_architectures() -> None: """Every GPU architecture the row claims must be present as compiled device code. - A wheel whose device code covers only the build machine's GPU installs on all the hardware - the row promises and then fails when a model runs. That failure appears late and looks like - a model problem rather than a packaging one, so it is worth catching here. + A wheel whose device code covers only the build machine's GPU installs on all the + hardware the row promises and then fails when a model runs. That failure appears late and + looks like a model problem rather than a packaging one, so it is caught here. + + On an accelerator row this fails closed. A missing architecture claim, a missing + inspection tool, a missing library, or a failed inspection each leave the shipped device + code unverified, so each blocks rather than passes. """ + accelerator_row = _is_accelerator_row() claimed = _requested_cuda_architectures() + if not claimed: - print("- this build named no GPU architectures, skipping the device code check") + assert not accelerator_row, ( + "this is an accelerator row but the build named no GPU architectures, so its " + "device code is whatever the build machine's GPU happened to be; the row's " + "architecture list has to reach the build" + ) + print("- this is a CPU wheel, so there is no device code to audit") return - if shutil.which("cuobjdump") is None: - print("- cuobjdump is not available, skipping the device code check") + + have_cuobjdump = shutil.which("cuobjdump") is not None + assert have_cuobjdump or not accelerator_row, ( + "this is an accelerator row but cuobjdump is not available, so the shipped device " + "code cannot be audited" + ) + if not have_cuobjdump: + print( + "- cuobjdump is not available and this is not an accelerator row, skipping" + ) return package_dir = _installed_package_dir() @@ -1201,13 +1232,18 @@ def test_device_code_covers_claimed_architectures() -> None: for library in _shipped_shared_objects(package_dir) if "cuda" in library.name or "aoti" in library.name ] + assert ( + libraries or not accelerator_row + ), "this is an accelerator row but the wheel ships no accelerator libraries" if not libraries: print( - "- this wheel ships no accelerator libraries, skipping the device code check" + "- this wheel ships no accelerator libraries and is not an accelerator row" ) return - present = set() + # Each library is checked on its own. Unioning architectures across libraries would let + # one library's device code stand in for another's, which is the case worth catching. + audited = 0 for library in libraries: result = subprocess.run( ["cuobjdump", "--list-elf", str(library)], @@ -1215,19 +1251,36 @@ def test_device_code_covers_claimed_architectures() -> None: text=True, check=False, ) + combined = result.stdout + result.stderr + # A library with no device code is a legitimate case, for example the delegate itself, + # which links the runtime but carries no kernels. cuobjdump reports that with a + # non-zero status, so it has to be told apart from a real inspection failure. + if "does not contain device code" in combined: + continue + assert result.returncode == 0 or not accelerator_row, ( + f"could not inspect {library.name} on an accelerator row: " + f"{result.stderr.strip()[:200]}" + ) if result.returncode != 0: continue - present.update(re.findall(r"sm_(\d+)", result.stdout)) + present = set(re.findall(r"sm_(\d+)", result.stdout)) + if not present: + continue + missing = sorted(set(claimed) - present, key=int) + assert not missing, ( + f"{library.name} claims GPU architectures {sorted(claimed, key=int)} but only " + f"carries device code for {sorted(present, key=int)}; a model would fail on " + f"hardware needing {missing}" + ) + audited += 1 - missing = sorted(set(claimed) - present, key=int) - assert not missing, ( - f"this row claims GPU architectures {sorted(claimed, key=int)} but the shipped " - f"libraries only carry device code for {sorted(present, key=int)}; a model would " - f"fail on hardware needing {missing}" - ) + assert ( + audited or not accelerator_row + ), "this is an accelerator row but no library carried device code to audit" print( - f"\u2713 device code covers every claimed GPU architecture: " - f"{sorted(claimed, key=int)}" + f"\u2713 device code covers every claimed GPU architecture " + f"{sorted(claimed, key=int)} in {audited} " + f"librar{'y' if audited == 1 else 'ies'}" ) From 0e8ba0c5188138705bc8b7752290917fa4f052cd Mon Sep 17 00:00:00 2001 From: shoumikhin Date: Tue, 4 Aug 2026 09:09:50 -0700 Subject: [PATCH 6/7] Update [ghstack-poisoned] --- .ci/scripts/wheel/test_cpp_sdk.py | 18 +++++------------- 1 file changed, 5 insertions(+), 13 deletions(-) diff --git a/.ci/scripts/wheel/test_cpp_sdk.py b/.ci/scripts/wheel/test_cpp_sdk.py index 1487877dfae..b620d3551fa 100644 --- a/.ci/scripts/wheel/test_cpp_sdk.py +++ b/.ci/scripts/wheel/test_cpp_sdk.py @@ -1255,19 +1255,11 @@ def test_device_code_covers_claimed_architectures() -> None: return package_dir = _installed_package_dir() - libraries = [ - library - for library in _shipped_shared_objects(package_dir) - if "cuda" in library.name or "aoti" in library.name - ] - assert ( - libraries or not accelerator_row - ), "this is an accelerator row but the wheel ships no accelerator libraries" - if not libraries: - print( - "- this wheel ships no accelerator libraries and is not an accelerator row" - ) - return + # Every shipped library is inspected rather than a subset chosen by file name. A + # library carrying device code under an unexpected name would otherwise be skipped, + # which is the failure this check exists to catch. + libraries = _shipped_shared_objects(package_dir) + assert libraries, f"no shared libraries found under {package_dir}" # Each library is checked on its own. Unioning architectures across libraries would let # one library's device code stand in for another's, which is the case worth catching. From 5cd6c09b53d94066abfb573dfd536701c1487057 Mon Sep 17 00:00:00 2001 From: shoumikhin Date: Tue, 4 Aug 2026 16:46:57 -0700 Subject: [PATCH 7/7] Update [ghstack-poisoned] --- .ci/scripts/wheel/test_cpp_sdk.py | 22 +++++++++++++++------- setup.py | 23 +++++++++++++++++------ 2 files changed, 32 insertions(+), 13 deletions(-) diff --git a/.ci/scripts/wheel/test_cpp_sdk.py b/.ci/scripts/wheel/test_cpp_sdk.py index a899ea754c8..a6727362595 100644 --- a/.ci/scripts/wheel/test_cpp_sdk.py +++ b/.ci/scripts/wheel/test_cpp_sdk.py @@ -1272,7 +1272,7 @@ def test_device_code_covers_claimed_architectures() -> None: # Each library is checked on its own. Unioning architectures across libraries would let # one library's device code stand in for another's, which is the case worth catching. - audited = 0 + audited_names: List[str] = [] for library in libraries: result = subprocess.run( [inspector, "--list-elf", str(library)], @@ -1301,15 +1301,23 @@ def test_device_code_covers_claimed_architectures() -> None: f"carries device code for {sorted(present, key=int)}; a model would fail on " f"hardware needing {missing}" ) - audited += 1 + audited_names.append(library.name) - assert ( - audited or not accelerator_row - ), "this is an accelerator row but no library carried device code to audit" + # A count is not enough. The library that carries the compiled kernels has to be the one + # audited, or a build that produced only forward-compatible intermediate code there would + # still pass on the strength of some other library's coverage. + assert audited_names or not accelerator_row, ( + "this is an accelerator row but no library carried device code to audit" + ) + if accelerator_row: + assert any("shim" in name for name in audited_names), ( + "no shim library carried device code; the row claims " + f"{sorted(claimed, key=int)} but only {sorted(audited_names)} were audited" + ) print( f"\u2713 device code covers every claimed GPU architecture " - f"{sorted(claimed, key=int)} in {audited} " - f"librar{'y' if audited == 1 else 'ies'}" + f"{sorted(claimed, key=int)} in {len(audited_names)} " + f"librar{'y' if len(audited_names) == 1 else 'ies'}" ) diff --git a/setup.py b/setup.py index 273f846b91b..e0cbc871f54 100644 --- a/setup.py +++ b/setup.py @@ -419,8 +419,8 @@ def get_dynamic_lib_name(name: str) -> str: def _write_cmake_version_file(destination: str) -> None: """Generate the CMake package version file next to the package config. - Read from version.txt so the version CMake reports is the same one the wheel and - the runtime SONAME use. + Takes the version the wheel publishes so a single artifact reports one identity + everywhere: to pip, to CMake, and in the runtime SONAME. Written by hand rather than from CMake's own template because the rule here is deliberately stricter than any stock one: a request above the package version is @@ -428,14 +428,16 @@ def _write_cmake_version_file(destination: str) -> None: assumed to work with a later one, so accepting a higher request would let a consumer match a package that does not satisfy it. """ - root = os.path.dirname(os.path.abspath(__file__)) - with open(os.path.join(root, "version.txt")) as handle: - version = handle.read().strip() + # The same version the wheel publishes, including any BUILD_VERSION override, so one + # artifact cannot report one identity to pip and a different one to CMake. + version = Version.string() # A pre-release suffix is not a CMake version component, so keep the numeric # prefix and let compatibility be decided on the major. numeric = re.match(r"\d+(?:\.\d+){0,2}", version) numeric = numeric.group(0) if numeric else "0.0.0" major = numeric.split(".")[0] + # Only an exclusive bound at the immediately following major means "any {major}.x". + next_major = str(int(major) + 1) contents = f"""\ set(PACKAGE_VERSION "{numeric}") @@ -465,10 +467,19 @@ def _write_cmake_version_file(destination: str) -> None: # Same major rule as below: a different major means a different shared runtime. set(PACKAGE_VERSION_UNSUITABLE TRUE) elseif(PACKAGE_FIND_VERSION_MAX_MAJOR - AND NOT PACKAGE_FIND_VERSION_MAX_MAJOR STREQUAL "{major}") + AND NOT PACKAGE_FIND_VERSION_MAX_MAJOR STREQUAL "{major}" + AND NOT (PACKAGE_FIND_VERSION_RANGE_MAX STREQUAL "EXCLUDE" + AND PACKAGE_FIND_VERSION_MAX_MAJOR EQUAL {next_major} + AND PACKAGE_FIND_VERSION_MAX_MINOR EQUAL 0 + AND PACKAGE_FIND_VERSION_MAX_PATCH EQUAL 0)) # Both endpoints have to share the major, the way CMake's own template requires. # Checking only the lower one accepts a range such as 1.0...3.0 against a 1.x # runtime, which tells a consumer that majors 2 and 3 are satisfied too. + # + # The exception is an exclusive bound exactly at the next major, as in 1.0...<2.0. + # That is the idiomatic way to ask for "any 1.x", and it excludes major 2 rather + # than reaching into it, so refusing it would reject the very request this rule + # exists to describe. set(PACKAGE_VERSION_UNSUITABLE TRUE) else() set(PACKAGE_VERSION_COMPATIBLE TRUE)