diff --git a/.ci/docker/build.sh b/.ci/docker/build.sh index 7c4a80044e4..e6122c498ce 100755 --- a/.ci/docker/build.sh +++ b/.ci/docker/build.sh @@ -92,6 +92,18 @@ esac TORCH_VERSION=$(cat ci_commit_pins/pytorch.txt) BUILD_DOCS=1 +# Pull channel + spec/url helpers out of torch_pin.py so install_pytorch.sh +# (which runs inside the docker build, where torch_pin.py isn't available) +# can decide between wheel install (test/release) and source build (nightly). +# Self-hosted runners often have python3 but not the unversioned python alias. +PYTHON_BIN=$(command -v python3 || command -v python) +TORCH_PIN_HELPERS=$(cd ../.. && "$PYTHON_BIN" -c "from torch_pin import CHANNEL, torch_spec, torchaudio_spec, torchvision_spec, torch_index_url_base; print(CHANNEL); print(torch_spec()); print(torchaudio_spec()); print(torchvision_spec()); print(torch_index_url_base())") +TORCH_CHANNEL=$(echo "${TORCH_PIN_HELPERS}" | sed -n '1p') +TORCH_SPEC=$(echo "${TORCH_PIN_HELPERS}" | sed -n '2p') +TORCHAUDIO_SPEC=$(echo "${TORCH_PIN_HELPERS}" | sed -n '3p') +TORCHVISION_SPEC=$(echo "${TORCH_PIN_HELPERS}" | sed -n '4p') +TORCH_INDEX_URL=$(echo "${TORCH_PIN_HELPERS}" | sed -n '5p') + # Copy requirements-lintrunner.txt from root to here cp ../../requirements-lintrunner.txt ./ @@ -104,6 +116,11 @@ docker build \ --build-arg "PYTHON_VERSION=${PYTHON_VERSION}" \ --build-arg "MINICONDA_VERSION=${MINICONDA_VERSION}" \ --build-arg "TORCH_VERSION=${TORCH_VERSION}" \ + --build-arg "TORCH_CHANNEL=${TORCH_CHANNEL}" \ + --build-arg "TORCH_SPEC=${TORCH_SPEC}" \ + --build-arg "TORCHAUDIO_SPEC=${TORCHAUDIO_SPEC}" \ + --build-arg "TORCHVISION_SPEC=${TORCHVISION_SPEC}" \ + --build-arg "TORCH_INDEX_URL=${TORCH_INDEX_URL}" \ --build-arg "BUCK2_VERSION=${BUCK2_VERSION}" \ --build-arg "LINTRUNNER=${LINTRUNNER:-}" \ --build-arg "BUILD_DOCS=${BUILD_DOCS}" \ diff --git a/.ci/docker/ci_commit_pins/pytorch.txt b/.ci/docker/ci_commit_pins/pytorch.txt index f6e39a63b92..0932a9ef6b8 100644 --- a/.ci/docker/ci_commit_pins/pytorch.txt +++ b/.ci/docker/ci_commit_pins/pytorch.txt @@ -1 +1 @@ -release/2.11 \ No newline at end of file +release/2.11 diff --git a/.ci/docker/common/install_pytorch.sh b/.ci/docker/common/install_pytorch.sh index 548a24f885d..ddf2f21baa9 100755 --- a/.ci/docker/common/install_pytorch.sh +++ b/.ci/docker/common/install_pytorch.sh @@ -17,6 +17,24 @@ install_domains() { } install_pytorch_and_domains() { + if [ "${TORCH_CHANNEL}" != "nightly" ]; then + # Test/release: install the published wheels directly. The specs and URL + # are passed in as docker build args (computed from torch_pin.py by + # .ci/docker/build.sh). RC wheels at /whl/test/ get re-uploaded under the + # same version, so use --no-cache-dir there to avoid stale cache hits. + local cache_flag="" + if [ "${TORCH_CHANNEL}" = "test" ]; then + cache_flag="--no-cache-dir" + fi + pip_install --force-reinstall ${cache_flag} \ + "${TORCH_SPEC}" "${TORCHVISION_SPEC}" "${TORCHAUDIO_SPEC}" \ + --index-url "${TORCH_INDEX_URL}/cpu" + return + fi + + # Nightly: build pytorch from source against the pinned SHA in pytorch.txt + # so we catch upstream regressions, then install audio/vision from the + # commits that pytorch itself pins. git clone https://github.com/pytorch/pytorch.git # Fetch the target commit @@ -27,14 +45,19 @@ install_pytorch_and_domains() { chown -R ci-user . export _GLIBCXX_USE_CXX11_ABI=1 + # PyTorch's FindARM.cmake hard-fails when the SVE+BF16 compile probe + # doesn't pass — gcc-11 in this image is too old to accept the combined + # NEON/SVE/bfloat16 intrinsics the probe exercises. Executorch's aarch64 + # runtime targets (phones, embedded) don't use SVE, so bypass the check. + export BUILD_IGNORE_SVE_UNAVAILABLE=1 # Then build and install PyTorch conda_run python setup.py bdist_wheel pip_install "$(echo dist/*.whl)" - # Grab the pinned audio and vision commits from PyTorch - TORCHAUDIO_VERSION=release/2.11 + # Defer to PyTorch's own pinned audio/vision commits. + TORCHAUDIO_VERSION=$(cat .github/ci_commit_pins/audio.txt) export TORCHAUDIO_VERSION - TORCHVISION_VERSION=release/0.26 + TORCHVISION_VERSION=$(cat .github/ci_commit_pins/vision.txt) export TORCHVISION_VERSION install_domains diff --git a/.ci/docker/ubuntu/Dockerfile b/.ci/docker/ubuntu/Dockerfile index 0e2d7e48eb9..98268d49675 100644 --- a/.ci/docker/ubuntu/Dockerfile +++ b/.ci/docker/ubuntu/Dockerfile @@ -64,6 +64,11 @@ ENV SCCACHE_S3_KEY_PREFIX executorch ENV SCCACHE_REGION us-east-1 ARG TORCH_VERSION +ARG TORCH_CHANNEL +ARG TORCH_SPEC +ARG TORCHAUDIO_SPEC +ARG TORCHVISION_SPEC +ARG TORCH_INDEX_URL ARG SKIP_PYTORCH COPY ./common/install_pytorch.sh install_pytorch.sh COPY ./common/utils.sh utils.sh diff --git a/.ci/scripts/test_model_e2e.sh b/.ci/scripts/test_model_e2e.sh index 8b8783d0db8..7205bb6d49c 100755 --- a/.ci/scripts/test_model_e2e.sh +++ b/.ci/scripts/test_model_e2e.sh @@ -260,7 +260,10 @@ if [ "$AUDIO_URL" != "" ]; then elif [[ "$MODEL_NAME" == *whisper* ]] || [ "$MODEL_NAME" = "voxtral_realtime" ]; then conda install -y -c conda-forge "ffmpeg<8" pip install datasets soundfile - pip install torchcodec==0.11.0 --extra-index-url https://download.pytorch.org/whl/test/cpu + # We pushd'd into EXECUTORCH_ROOT above, so torch_pin is importable here. + TORCHCODEC_PKG=$(python -c "from torch_pin import torchcodec_spec; print(torchcodec_spec())") + TORCHCODEC_INDEX=$(python -c "from torch_pin import torch_index_url_base; print(torch_index_url_base())") + pip install "$TORCHCODEC_PKG" --extra-index-url "${TORCHCODEC_INDEX}/cpu" python -c "from datasets import load_dataset;import soundfile as sf;sample = load_dataset('distil-whisper/librispeech_long', 'clean', split='validation')[0]['audio'];sf.write('${MODEL_DIR}/$AUDIO_FILE', sample['array'][:sample['sampling_rate']*30], sample['sampling_rate'])" fi diff --git a/.ci/scripts/test_wheel_package_qnn.sh b/.ci/scripts/test_wheel_package_qnn.sh index 763bd8733c1..f44fafadb58 100644 --- a/.ci/scripts/test_wheel_package_qnn.sh +++ b/.ci/scripts/test_wheel_package_qnn.sh @@ -150,25 +150,26 @@ run_core_tests () { echo "=== [$LABEL] Installing wheel & deps ===" "$PIPBIN" install --upgrade pip "$PIPBIN" install "$WHEEL_FILE" - TORCH_VERSION=$( + # runpy.run_path uses a relative path, so the caller must run this script + # from the executorch repo root (where torch_pin.py lives). + TORCH_SPEC=$( "$PYBIN" - <<'PY' import runpy module_vars = runpy.run_path("torch_pin.py") -print(module_vars["TORCH_VERSION"]) +print(module_vars["torch_spec"]()) PY ) + TORCH_INDEX=$( + "$PYBIN" - <<'PY' +import runpy +module_vars = runpy.run_path("torch_pin.py") +print(module_vars["torch_index_url_base"]()) +PY +) + echo "=== [$LABEL] Install $TORCH_SPEC from ${TORCH_INDEX}/cpu ===" -# NIGHTLY_VERSION=$( -# "$PYBIN" - <<'PY' -# import runpy -# module_vars = runpy.run_path("torch_pin.py") -# print(module_vars["NIGHTLY_VERSION"]) -# PY -# ) - echo "=== [$LABEL] Install torch==${TORCH_VERSION} ===" - - # Install torch based on the pinned PyTorch version, preferring the PyTorch test index - "$PIPBIN" install torch=="${TORCH_VERSION}" --extra-index-url "https://download.pytorch.org/whl/test" + # Install torch based on the pinned PyTorch version from the channel index. + "$PIPBIN" install "$TORCH_SPEC" --index-url "${TORCH_INDEX}/cpu" "$PIPBIN" install wheel # Install torchao based on the pinned commit from third-party/ao submodule diff --git a/.ci/scripts/tests/test_torch_pin.py b/.ci/scripts/tests/test_torch_pin.py new file mode 100644 index 00000000000..6c475aeaa05 --- /dev/null +++ b/.ci/scripts/tests/test_torch_pin.py @@ -0,0 +1,54 @@ +import importlib + +import pytest + + +@pytest.fixture +def pin(): + """Yield a fresh import of torch_pin so tests can mutate CHANNEL safely.""" + import torch_pin + + yield torch_pin + importlib.reload(torch_pin) + + +@pytest.mark.parametrize( + "channel, expected_torch, expected_url", + [ + ( + "nightly", + "torch=={TORCH_VERSION}.{NIGHTLY_VERSION}", + "https://download.pytorch.org/whl/nightly", + ), + ("test", "torch=={TORCH_VERSION}", "https://download.pytorch.org/whl/test"), + ("release", "torch=={TORCH_VERSION}", "https://download.pytorch.org/whl"), + ], +) +def test_channel_resolution(pin, channel, expected_torch, expected_url): + pin.CHANNEL = channel + expected = expected_torch.format( + TORCH_VERSION=pin.TORCH_VERSION, NIGHTLY_VERSION=pin.NIGHTLY_VERSION + ) + assert pin.torch_spec() == expected + assert pin.torch_index_url_base() == expected_url + + +def test_all_specs_share_nightly_suffix(pin): + pin.CHANNEL = "nightly" + suffix = f".{pin.NIGHTLY_VERSION}" + assert pin.torch_spec().endswith(suffix) + assert pin.torchaudio_spec().endswith(suffix) + assert pin.torchcodec_spec().endswith(suffix) + assert pin.torchvision_spec().endswith(suffix) + + +def test_specs_drop_suffix_off_nightly(pin): + pin.CHANNEL = "test" + assert pin.torch_spec() == f"torch=={pin.TORCH_VERSION}" + assert pin.torchaudio_spec() == f"torchaudio=={pin.TORCHAUDIO_VERSION}" + assert pin.torchcodec_spec() == f"torchcodec=={pin.TORCHCODEC_VERSION}" + assert pin.torchvision_spec() == f"torchvision=={pin.TORCHVISION_VERSION}" + + +def test_torch_branch_derived_from_version(pin): + assert pin.torch_branch() == f"release/{pin.TORCH_VERSION.rsplit('.', 1)[0]}" diff --git a/.ci/scripts/utils.sh b/.ci/scripts/utils.sh index 86e54b478ef..12e7f3d2067 100644 --- a/.ci/scripts/utils.sh +++ b/.ci/scripts/utils.sh @@ -53,7 +53,7 @@ dedupe_macos_loader_path_rpaths() { pushd .. torch_lib_dir=$(python -c "import importlib.util; print(importlib.util.find_spec('torch').submodule_search_locations[0])")/lib popd - + if [[ -z "${torch_lib_dir}" || ! -d "${torch_lib_dir}" ]]; then return fi @@ -89,6 +89,30 @@ install_domains() { } install_pytorch_and_domains() { + # CWD is the executorch repo root, where torch_pin.py lives. + TORCH_CHANNEL=$(python -c "from torch_pin import CHANNEL; print(CHANNEL)") + + if [ "${TORCH_CHANNEL}" != "nightly" ]; then + # Test/release: install the published wheels directly from torch_pin.py's + # channel index, skipping the source-build path entirely. RC wheels at + # /whl/test/ get re-uploaded under the same version, so use --no-cache-dir + # there to avoid stale cache hits. + local torch_spec=$(python -c "from torch_pin import torch_spec; print(torch_spec())") + local torchvision_spec=$(python -c "from torch_pin import torchvision_spec; print(torchvision_spec())") + local torchaudio_spec=$(python -c "from torch_pin import torchaudio_spec; print(torchaudio_spec())") + local torch_index_url=$(python -c "from torch_pin import torch_index_url_base; print(torch_index_url_base())") + local cache_flag="" + if [ "${TORCH_CHANNEL}" = "test" ]; then + cache_flag="--no-cache-dir" + fi + pip install --force-reinstall ${cache_flag} \ + "${torch_spec}" "${torchvision_spec}" "${torchaudio_spec}" \ + --index-url "${torch_index_url}/cpu" + return + fi + + # Nightly: source-build pytorch from the pinned SHA so CI catches upstream + # regressions; pytorch's own audio/vision pins drive those installs. pushd .ci/docker || return TORCH_VERSION=$(cat ci_commit_pins/pytorch.txt) popd || return @@ -140,10 +164,10 @@ install_pytorch_and_domains() { fi dedupe_macos_loader_path_rpaths - # Grab the pinned audio and vision commits from PyTorch - TORCHAUDIO_VERSION=release/2.11 + # We're on the nightly path here; defer to PyTorch's own pinned commits. + TORCHAUDIO_VERSION=$(cat .github/ci_commit_pins/audio.txt) export TORCHAUDIO_VERSION - TORCHVISION_VERSION=release/0.26 + TORCHVISION_VERSION=$(cat .github/ci_commit_pins/vision.txt) export TORCHVISION_VERSION install_domains @@ -218,17 +242,21 @@ download_stories_model_artifacts() { } do_not_use_nightly_on_ci() { - # An assert to make sure that we are not using PyTorch nightly on CI to prevent - # regression as documented in https://github.com/pytorch/executorch/pull/6564 - TORCH_VERSION=$(pip list | grep -w 'torch ' | awk -F ' ' {'print $2'} | tr -d '\n') + # Sanity check that prevents accidentally landing a PR that pins to PyTorch + # nightly without exercising the source-build path (see #6564). + # + # For CHANNEL=nightly, CI source-builds pytorch from the SHA in pytorch.txt, + # so the installed torch shows up as e.g. 2.13.0a0+gitc8a648d — assert that. + # For CHANNEL=test/release, we install published wheels by design (e.g. + # 2.11.0), so the +git assertion doesn't apply. + TORCH_CHANNEL=$(python -c "from torch_pin import CHANNEL; print(CHANNEL)") + if [ "${TORCH_CHANNEL}" != "nightly" ]; then + return 0 + fi - # The version of PyTorch building from source looks like 2.6.0a0+gitc8a648d that - # includes the commit while nightly (2.6.0.dev20241019+cpu) or release (2.6.0) - # won't have that. Note that we couldn't check for the exact commit from the pin - # ci_commit_pins/pytorch.txt here because the value will be different when running - # this on PyTorch CI + TORCH_VERSION=$(pip list | grep -w 'torch ' | awk -F ' ' {'print $2'} | tr -d '\n') if [[ "${TORCH_VERSION}" != *"+git"* ]]; then - echo "Unexpected torch version. Expected binary built from source, got ${TORCH_VERSION}" + echo "Unexpected torch version. Expected binary built from source for CHANNEL=nightly, got ${TORCH_VERSION}" exit 1 fi } diff --git a/.github/scripts/update_pytorch_pin.py b/.github/scripts/update_pytorch_pin.py index dbc48552d9b..9f2698917b2 100644 --- a/.github/scripts/update_pytorch_pin.py +++ b/.github/scripts/update_pytorch_pin.py @@ -8,6 +8,12 @@ import urllib.request from pathlib import Path +# torch_pin.py lives at the repo root. Locate it relative to this script so +# the import works regardless of where the script is invoked from. +_REPO_ROOT = Path(__file__).resolve().parents[2] +sys.path.insert(0, str(_REPO_ROOT)) +from torch_pin import CHANNEL, NIGHTLY_VERSION, torch_branch + def parse_nightly_version(nightly_version): """ @@ -27,23 +33,6 @@ def parse_nightly_version(nightly_version): return f"{year}-{month}-{day}" -def get_torch_nightly_version(): - """ - Read NIGHTLY_VERSION from torch_pin.py. - - Returns: - NIGHTLY_VERSION string - """ - with open("torch_pin.py", "r") as f: - content = f.read() - - match = re.search(r'NIGHTLY_VERSION\s*=\s*["\']([^"\']+)["\']', content) - if not match: - raise ValueError("Could not find NIGHTLY_VERSION in torch_pin.py") - - return match.group(1) - - def get_commit_hash_for_nightly(date_str): """ Fetch commit hash from PyTorch nightly branch for a given date. @@ -91,17 +80,16 @@ def extract_hash_from_title(title): return match.group(1) -def update_pytorch_pin(commit_hash): +def update_pytorch_pin(ref): """ - Update .ci/docker/ci_commit_pins/pytorch.txt with the new commit hash. + Update .ci/docker/ci_commit_pins/pytorch.txt with the new ref. Args: - commit_hash: Commit hash to write + ref: Either a commit SHA (nightly) or a branch name (test/release). """ - pin_file = ".ci/docker/ci_commit_pins/pytorch.txt" - with open(pin_file, "w") as f: - f.write(f"{commit_hash}\n") - print(f"Updated {pin_file} with commit hash: {commit_hash}") + pin_file = _REPO_ROOT / ".ci/docker/ci_commit_pins/pytorch.txt" + pin_file.write_text(f"{ref}\n") + print(f"Updated {pin_file} with ref: {ref}") def should_skip_file(filename): @@ -118,18 +106,20 @@ def should_skip_file(filename): return filename in skip_files -def fetch_file_content(commit_hash, file_path): +def fetch_file_content(ref, file_path): """ Fetch file content from GitHub API. Args: - commit_hash: Commit hash to fetch from + ref: Commit SHA or branch name to fetch from file_path: File path in the repository Returns: File content as bytes """ - api_url = f"https://api.github.com/repos/pytorch/pytorch/contents/{file_path}?ref={commit_hash}" + api_url = ( + f"https://api.github.com/repos/pytorch/pytorch/contents/{file_path}?ref={ref}" + ) req = urllib.request.Request(api_url) req.add_header("Accept", "application/vnd.github.v3+json") @@ -146,7 +136,7 @@ def fetch_file_content(commit_hash, file_path): raise -def sync_directory(et_dir, pt_path, commit_hash): +def sync_directory(et_dir, pt_path, ref): """ Sync files from PyTorch to ExecuTorch using GitHub API. Only syncs files that already exist in ExecuTorch - does not add new files. @@ -154,7 +144,7 @@ def sync_directory(et_dir, pt_path, commit_hash): Args: et_dir: ExecuTorch directory path pt_path: PyTorch directory path in the repository (e.g., "c10") - commit_hash: Commit hash to fetch from + ref: Commit SHA or branch name to fetch from Returns: Number of files grafted @@ -181,12 +171,12 @@ def sync_directory(et_dir, pt_path, commit_hash): # Fetch content from PyTorch and compare try: - pt_content = fetch_file_content(commit_hash, pt_file_path) + pt_content = fetch_file_content(ref, pt_file_path) et_content = et_file.read_bytes() if pt_content != et_content: print(f"āš ļø Difference detected in {rel_path}") - print(f"šŸ“‹ Grafting from PyTorch commit {commit_hash}...") + print(f"šŸ“‹ Grafting from PyTorch ref {ref}...") et_file.write_bytes(pt_content) print(f"āœ… Grafted {et_file}") @@ -201,37 +191,34 @@ def sync_directory(et_dir, pt_path, commit_hash): return files_grafted -def sync_c10_directories(commit_hash): +def sync_c10_directories(ref): """ Sync c10 and torch/headeronly directories from PyTorch to ExecuTorch using GitHub API. Args: - commit_hash: PyTorch commit hash to sync from + ref: PyTorch commit SHA or branch name to sync from Returns: Total number of files grafted """ print("\nšŸ”„ Syncing c10 directories from PyTorch via GitHub API...") - # Get repository root - repo_root = Path.cwd() - # Define directory pairs to sync (from check_c10_sync.sh) # Format: (executorch_dir, pytorch_path_in_repo) dir_pairs = [ ( - repo_root / "runtime/core/portable_type/c10/c10", + _REPO_ROOT / "runtime/core/portable_type/c10/c10", "c10", ), ( - repo_root / "runtime/core/portable_type/c10/torch/headeronly", + _REPO_ROOT / "runtime/core/portable_type/c10/torch/headeronly", "torch/headeronly", ), ] total_grafted = 0 for et_dir, pt_path in dir_pairs: - files_grafted = sync_directory(et_dir, pt_path, commit_hash) + files_grafted = sync_directory(et_dir, pt_path, ref) total_grafted += files_grafted if total_grafted > 0: @@ -244,27 +231,26 @@ def sync_c10_directories(commit_hash): def main(): try: - # Read NIGHTLY_VERSION from torch_pin.py - nightly_version = get_torch_nightly_version() - print(f"Found NIGHTLY_VERSION: {nightly_version}") - - # Parse to date string - date_str = parse_nightly_version(nightly_version) - print(f"Parsed date: {date_str}") - - # Fetch commit hash from PyTorch nightly branch - commit_hash = get_commit_hash_for_nightly(date_str) - print(f"Found commit hash: {commit_hash}") + print(f"CHANNEL: {CHANNEL}") + if CHANNEL == "nightly": + # Nightly pins to an immutable SHA looked up by date. + print(f"Found NIGHTLY_VERSION: {NIGHTLY_VERSION}") + date_str = parse_nightly_version(NIGHTLY_VERSION) + print(f"Parsed date: {date_str}") + pin_ref = get_commit_hash_for_nightly(date_str) + else: + # For test/release, pin to the branch name so CI picks up + # cherry-picks / security patches as they land on the branch. + pin_ref = torch_branch() + print(f"Pin ref: {pin_ref}") # Update the pin file - update_pytorch_pin(commit_hash) + update_pytorch_pin(pin_ref) - # Sync c10 directories from PyTorch - sync_c10_directories(commit_hash) + # Sync c10 directories from PyTorch (ref param accepts branches too) + sync_c10_directories(pin_ref) - print( - "\nāœ… Successfully updated PyTorch commit pin and synced c10 directories!" - ) + print("\nāœ… Successfully updated PyTorch pin and synced c10 directories!") except Exception as e: print(f"Error: {e}", file=sys.stderr) diff --git a/.github/workflows/weekly-pytorch-pin-bump.yml b/.github/workflows/weekly-pytorch-pin-bump.yml index 30579c77701..ba8f48505d5 100644 --- a/.github/workflows/weekly-pytorch-pin-bump.yml +++ b/.github/workflows/weekly-pytorch-pin-bump.yml @@ -22,29 +22,46 @@ jobs: with: python-version: '3.11' + - name: Check torch_pin channel + id: channel + run: | + CHANNEL=$(python -c "from torch_pin import CHANNEL; print(CHANNEL)") + echo "channel=${CHANNEL}" >> "$GITHUB_OUTPUT" + if [ "${CHANNEL}" != "nightly" ]; then + echo "torch_pin.py CHANNEL is '${CHANNEL}'; weekly nightly bump only runs when CHANNEL == 'nightly'." + fi + - name: Determine nightly version + if: steps.channel.outputs.channel == 'nightly' id: nightly run: | NIGHTLY_DATE=$(date -u -d 'yesterday' '+%Y%m%d') NIGHTLY_VERSION="dev${NIGHTLY_DATE}" echo "version=${NIGHTLY_VERSION}" >> "$GITHUB_OUTPUT" - - name: Read current TORCH_VERSION - id: torch - run: | - TORCH_VERSION=$(python -c "exec(open('torch_pin.py').read()); print(TORCH_VERSION)") - echo "version=${TORCH_VERSION}" >> "$GITHUB_OUTPUT" - - name: Update torch_pin.py with new NIGHTLY_VERSION + if: steps.channel.outputs.channel == 'nightly' + env: + NIGHTLY_VERSION: ${{ steps.nightly.outputs.version }} run: | - printf 'TORCH_VERSION = "%s"\nNIGHTLY_VERSION = "%s"\n' \ - "${{ steps.torch.outputs.version }}" \ - "${{ steps.nightly.outputs.version }}" > torch_pin.py + python - <<'PY' + import os, pathlib, re + p = pathlib.Path('torch_pin.py') + p.write_text(re.sub( + r'^NIGHTLY_VERSION\s*=\s*".*"$', + f'NIGHTLY_VERSION = "{os.environ["NIGHTLY_VERSION"]}"', + p.read_text(), + count=1, + flags=re.MULTILINE, + )) + PY - name: Run pin bump script + if: steps.channel.outputs.channel == 'nightly' run: python .github/scripts/update_pytorch_pin.py - name: Create branch and PR + if: steps.channel.outputs.channel == 'nightly' env: GH_TOKEN: ${{ secrets.UPDATEBOT_TOKEN }} run: | diff --git a/examples/models/moshi/mimi/install_requirements.sh b/examples/models/moshi/mimi/install_requirements.sh index 9fc12f64bc9..eb8fe96ed05 100755 --- a/examples/models/moshi/mimi/install_requirements.sh +++ b/examples/models/moshi/mimi/install_requirements.sh @@ -7,10 +7,16 @@ set -x +SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) +# torch_pin lives at the executorch repo root. +cd "$SCRIPT_DIR/../../../.." + +TORCHCODEC_PKG=$(python -c "from torch_pin import torchcodec_spec; print(torchcodec_spec())") +TORCHCODEC_INDEX=$(python -c "from torch_pin import torch_index_url_base; print(torch_index_url_base())") + sudo apt install ffmpeg -y -pip install torchcodec==0.11.0 --extra-index-url https://download.pytorch.org/whl/test/cpu +pip install "$TORCHCODEC_PKG" --extra-index-url "${TORCHCODEC_INDEX}/cpu" pip install moshi==0.2.11 pip install bitsandbytes soundfile einops # Run llama2/install requirements for torchao deps -SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) bash "$SCRIPT_DIR"/../../llama/install_requirements.sh diff --git a/install_executorch.py b/install_executorch.py index ed2bb409bad..56dc1ee1add 100644 --- a/install_executorch.py +++ b/install_executorch.py @@ -174,7 +174,11 @@ def _parse_args() -> argparse.Namespace: parser.add_argument( "--use-pt-pinned-commit", action="store_true", - help="build from the pinned PyTorch commit instead of nightly", + help="install plain `torch` (whatever pip resolves by default; CI " + "uses this when torch is already built from source against the " + "pinned ref in pytorch.txt). Without this flag, install the specific " + "pinned version from the channel selected in torch_pin.py " + "(nightly / test / release).", ) parser.add_argument( "--editable", @@ -205,13 +209,14 @@ def main(args): return check_and_update_submodules() - # This option is used in CI to make sure that PyTorch build from the pinned commit - # is used instead of nightly. CI jobs wouldn't be able to catch regression from the - # latest PT commit otherwise - use_pytorch_nightly = not args.use_pt_pinned_commit + # By default install the specific pinned version from the channel selected + # in torch_pin.py. With --use-pt-pinned-commit, install plain `torch` (pip's + # default resolution); CI uses this when torch is already built from source + # against the pinned ref in pytorch.txt. + install_pinned_version = not args.use_pt_pinned_commit # Step 1: Install core dependencies first - install_requirements(use_pytorch_nightly) + install_requirements(install_pinned_version) # Step 2: Install core package cmd = ( @@ -232,7 +237,7 @@ def main(args): # Step 3: Extra (optional) packages that is only useful for running examples. if not args.minimal: - install_optional_example_requirements(use_pytorch_nightly) + install_optional_example_requirements(install_pinned_version) if __name__ == "__main__": diff --git a/install_requirements.py b/install_requirements.py index b30068cbdb8..1e8ab5c2d6f 100644 --- a/install_requirements.py +++ b/install_requirements.py @@ -12,9 +12,18 @@ from install_utils import determine_torch_url, is_intel_mac_os, python_is_compatible -# The pip repository that hosts nightly torch packages. -# This will be dynamically set based on CUDA availability and CUDA backend enabled/disabled. -TORCH_URL_BASE = "https://download.pytorch.org/whl/test" +from torch_pin import ( + CHANNEL, + torch_index_url_base, + torch_spec, + torchaudio_spec, + torchvision_spec, +) + +# Only RC wheels at /whl/test/ get re-uploaded under the same version, so +# pip's local cache can serve stale content. Nightly and release wheels are +# immutable per their identifier. +_NO_CACHE_DIR_FLAG = ["--no-cache-dir"] if CHANNEL == "test" else [] # Since ExecuTorch often uses main-branch features of pytorch, only the nightly # pip versions will have the required features. @@ -23,17 +32,18 @@ # NIGHTLY_VERSION, you should re-run this script to install the necessary # package versions. # -# NOTE: If you're changing, make the corresponding change in .ci/docker/ci_commit_pins/pytorch.txt -# by picking the hash from the same date in -# https://hud.pytorch.org/hud/pytorch/pytorch/nightly/ @lint-ignore +# NOTE: If you change torch_pin.py, the pre-commit hook runs +# .github/scripts/update_pytorch_pin.py to refresh +# .ci/docker/ci_commit_pins/pytorch.txt and the c10 grafted headers. +# If you bypass the hook, run that script manually. # # NOTE: If you're changing, make the corresponding supported CUDA versions in # SUPPORTED_CUDA_VERSIONS in install_utils.py if needed. -def install_requirements(use_pytorch_nightly): - # Skip pip install on Intel macOS if using nightly. - if use_pytorch_nightly and is_intel_mac_os(): +def install_requirements(install_pinned_version): + # No prebuilt wheels are available for Intel macOS, regardless of channel. + if install_pinned_version and is_intel_mac_os(): print( "ERROR: Prebuilt PyTorch wheels are no longer available for Intel-based macOS.\n" "Please build from source by following https://docs.pytorch.org/executorch/main/using-executorch-building-from-source.html", @@ -42,25 +52,26 @@ def install_requirements(use_pytorch_nightly): sys.exit(1) # Determine the appropriate PyTorch URL based on CUDA delegate status - torch_url = determine_torch_url(TORCH_URL_BASE) + torch_url = determine_torch_url(torch_index_url_base()) # pip packages needed by exir. TORCH_PACKAGE = [ - # Setting use_pytorch_nightly to false to test the pinned PyTorch commit. Note - # that we don't need to set any version number there because they have already - # been installed on CI before this step, so pip won't reinstall them - ("torch==2.11.0" if use_pytorch_nightly else "torch"), + # Default: install the specific pinned version from the channel selected + # in torch_pin.py. With --use-pt-pinned-commit, pass plain "torch" and + # let pip resolve its default (CI's source-build is already installed). + (torch_spec() if install_pinned_version else "torch"), ] # Install the requirements for core ExecuTorch package. - # `--extra-index-url` tells pip to look for package - # versions on the provided URL if they aren't available on the default URL. + # `--extra-index-url` tells pip to look for package versions on the + # provided URL if they aren't available on the default URL. subprocess.run( [ sys.executable, "-m", "pip", "install", + *_NO_CACHE_DIR_FLAG, "-r", "requirements-dev.txt", *TORCH_PACKAGE, @@ -106,14 +117,14 @@ def install_requirements(use_pytorch_nightly): ) -def install_optional_example_requirements(use_pytorch_nightly): +def install_optional_example_requirements(install_pinned_version): # Determine the appropriate PyTorch URL based on CUDA delegate status - torch_url = determine_torch_url(TORCH_URL_BASE) + torch_url = determine_torch_url(torch_index_url_base()) print("Installing torch domain libraries") DOMAIN_LIBRARIES = [ - ("torchvision==0.26.0" if use_pytorch_nightly else "torchvision"), - ("torchaudio==2.11.0" if use_pytorch_nightly else "torchaudio"), + (torchvision_spec() if install_pinned_version else "torchvision"), + (torchaudio_spec() if install_pinned_version else "torchaudio"), ] # Then install domain libraries subprocess.run( @@ -122,6 +133,7 @@ def install_optional_example_requirements(use_pytorch_nightly): "-m", "pip", "install", + *_NO_CACHE_DIR_FLAG, *DOMAIN_LIBRARIES, "--extra-index-url", torch_url, @@ -152,7 +164,11 @@ def main(args): parser.add_argument( "--use-pt-pinned-commit", action="store_true", - help="build from the pinned PyTorch commit instead of nightly", + help="install plain `torch` (whatever pip resolves by default; CI " + "uses this when torch is already built from source against the " + "pinned ref in pytorch.txt). Without this flag, install the specific " + "pinned version from the channel selected in torch_pin.py " + "(nightly / test / release).", ) parser.add_argument( "--example", @@ -160,10 +176,10 @@ def main(args): help="Also installs required packages for running example scripts.", ) args = parser.parse_args(args) - use_pytorch_nightly = not bool(args.use_pt_pinned_commit) - install_requirements(use_pytorch_nightly) + install_pinned_version = not bool(args.use_pt_pinned_commit) + install_requirements(install_pinned_version) if args.example: - install_optional_example_requirements(use_pytorch_nightly) + install_optional_example_requirements(install_pinned_version) if __name__ == "__main__": diff --git a/torch_pin.py b/torch_pin.py index 3575d9a376d..856a67c1990 100644 --- a/torch_pin.py +++ b/torch_pin.py @@ -1,2 +1,59 @@ +# CHANNEL selects the wheel source for torch and its domain libraries. +# "nightly" — dev builds from /whl/nightly. NIGHTLY_VERSION is appended to +# every package spec, and CI source-builds pytorch from the +# pinned SHA in pytorch.txt to catch upstream regressions. +# "test" — release candidates from /whl/test. +# "release" — stable releases from /whl. +# For "test" and "release", NIGHTLY_VERSION is ignored and CI installs the +# published wheels directly (no source build). +# +# Example — pinning to a 2.12 release candidate when nightly is broken: +# 1. Set CHANNEL = "test". +# 2. Set the four version constants to the RC's major.minor.patch +# (look up matching versions on https://download.pytorch.org/whl/test/). +# 3. Re-run install_requirements.sh; commit. The pre-commit hook calls +# .github/scripts/update_pytorch_pin.py, which writes torch_branch() +# (e.g. "release/2.12") into .ci/docker/ci_commit_pins/pytorch.txt and +# re-syncs grafted c10 headers. +CHANNEL = "test" + TORCH_VERSION = "2.11.0" -# NIGHTLY_VERSION = "dev20260318" Temporarily pinning to stable release candidate. Revert https://github.com/pytorch/executorch/pull/18287 +TORCHAUDIO_VERSION = "2.11.0" +TORCHCODEC_VERSION = "0.11.0" +TORCHVISION_VERSION = "0.26.0" + +NIGHTLY_VERSION = "dev20260318" + + +def _spec(name: str, version: str) -> str: + if CHANNEL == "nightly": + return f"{name}=={version}.{NIGHTLY_VERSION}" + return f"{name}=={version}" + + +def torch_spec() -> str: + return _spec("torch", TORCH_VERSION) + + +def torchaudio_spec() -> str: + return _spec("torchaudio", TORCHAUDIO_VERSION) + + +def torchcodec_spec() -> str: + return _spec("torchcodec", TORCHCODEC_VERSION) + + +def torchvision_spec() -> str: + return _spec("torchvision", TORCHVISION_VERSION) + + +def torch_index_url_base() -> str: + if CHANNEL == "release": + return "https://download.pytorch.org/whl" + return f"https://download.pytorch.org/whl/{CHANNEL}" + + +def torch_branch() -> str: + # PyTorch uses "release/M.N" branches; derive from the pinned version. + # Used by update_pytorch_pin.py to write into pytorch.txt for test/release. + return f"release/{TORCH_VERSION.rsplit('.', 1)[0]}"