diff --git a/.ci/scripts/wheel/cuda_arch_list.sh b/.ci/scripts/wheel/cuda_arch_list.sh new file mode 100644 index 00000000000..989b838cf47 --- /dev/null +++ b/.ci/scripts/wheel/cuda_arch_list.sh @@ -0,0 +1,98 @@ +#!/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 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 +# 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() { + local machine + machine="$(uname -m)" + # 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:-}}" + # 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//./}" + + 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}" ;; + *) _executorch_unknown_train "${train}" ;; + 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}" ;; + *) _executorch_unknown_train "${train}" ;; + esac + ;; + *) _executorch_unknown_train "${train}" ;; + 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 c21206d25f8..952267549ed 100644 --- a/setup.py +++ b/setup.py @@ -108,6 +108,59 @@ 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. + + 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("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 or unmapped, so + the CPU rows are unaffected. + """ + train = _cuda_train() + 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 [] + # 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"{name}; platform_system == 'Linux'" for name in packages] + + def _minimal_cmake_flags() -> List[str]: return [ "-DEXECUTORCH_BUILD_COREML=OFF", @@ -1236,7 +1289,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(