Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
40707d4
Track a recent ExecuTorch pin instead of 1.4.1
shoumikhin Aug 23, 2026
4f4227a
Install the ExecuTorch the pin names, from the channel that has it
shoumikhin Aug 23, 2026
a1efbcb
Follow the row's CUDA version, and reach the channel from every example
shoumikhin Aug 23, 2026
eaccb3a
Keep the extra resolvable on Windows, and pin the channel docgen inst…
shoumikhin Aug 24, 2026
7f3213d
Put the index on the two printed installs, and fail the suite when se…
shoumikhin Aug 24, 2026
b2ae899
Assert the pin sites CI actually uses, and move the checks off the GP…
shoumikhin Aug 24, 2026
c83f799
Make the pin checks run in CI, and test the properties they assert
shoumikhin Aug 24, 2026
4c48401
Let the pairing check run on the one lane that installs ExecuTorch
shoumikhin Aug 24, 2026
48e5a5c
Make the pin guards read values instead of nearby text
shoumikhin Aug 24, 2026
e841cd1
Stop the lockfile check from going red repo-wide on the next refresh
shoumikhin Aug 24, 2026
265bcac
Stop a test executing Python read out of a workflow file
shoumikhin Aug 24, 2026
e19a9dc
Guard the nightly channel at the local-path install sites, and check …
shoumikhin Aug 24, 2026
581fbc9
Harden the ExecuTorch pin guards and the printed install commands
shoumikhin Aug 25, 2026
9f15f5c
Track the ExecuTorch pin and move it to new nightlies automatically
shoumikhin Aug 25, 2026
88accd6
Move the ExecuTorch pin to the 2026-09-01 nightly
shoumikhin Sep 1, 2026
5b3c664
Drop the ExecuTorch runtime from the Torch-TensorRT runtime wheel
shoumikhin Aug 25, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 16 additions & 1 deletion .github/scripts/install-torch-tensorrt.sh
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,24 @@ fi

# Install Torch-TensorRT
if [[ ${PLATFORM} == win32 ]]; then
# pin-check: no-nightly -- this glob also matches the Linux-only ExecuTorch runtime wheel, but
# ExecuTorch publishes no win32 nightly and the [executorch] extra is Linux-only, so this
# platform's plain torch-tensorrt install needs no nightly index.
python -m pip install ${RUNNER_ARTIFACT_DIR}/torch_tensorrt*.whl
else
python -m pip install /opt/torch-tensorrt-builds/torch_tensorrt*.whl --use-deprecated=legacy-resolver
# The nightly channel is needed because this glob also matches the ExecuTorch runtime wheel,
# whose install_requires names an ExecuTorch dev build that is published only there.
# Hardcoded rather than ${CHANNEL} like the lines above: a .dev wheel exists on no other
# channel, so deriving it would break this install on exactly the test and release runs the
# index was added for. It is an extra index, not a replacement, and torch is already
# force-reinstalled from ${INDEX_URL} above, so the pinned torch is not at risk from it.
# || exit 1 because line 1's `set -exou pipefail` is commented out and linux-test.yml
# concatenates this file ahead of the user script, so a failure here would otherwise be
# discarded and the job would die later with an unrelated-looking ImportError. Scoped to the
# line this change is responsible for; re-enabling set -e for the whole file is a
# pre-existing hazard worth a separate change.
python -m pip install /opt/torch-tensorrt-builds/torch_tensorrt*.whl --use-deprecated=legacy-resolver \
--extra-index-url "https://download.pytorch.org/whl/nightly/${CU_VERSION}" || exit 1
fi

echo -e "Running test script";
323 changes: 323 additions & 0 deletions .github/scripts/update_executorch_pin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,323 @@
#!/usr/bin/env python3
"""Move the ExecuTorch pin to the newest wheel published on an index.

The pin is two coupled facts, spread across the tree but sourced from
``dev_dep_versions.yml``: ``__executorch_version__`` selects the wheel the delegate is
built to sit beside, and ``__executorch_commit__`` selects the tree it compiles from.
They must name one ExecuTorch, so this script never guesses the commit: it reads it from
the chosen wheel's own ``executorch/version.py``, which is the same provenance
``tests/py/dynamo/executorch/test_executorch_pin.py`` checks the pins against.

The daily workflow runs this, then opens a pull request when the pin moved. The existing
pin guards and the executorch end-to-end lane run on that pull request, so "the newest
wheel that actually works" is decided by the same gate a human bump goes through, not
re-implemented here. A nightly that did not publish leaves the newest version unchanged,
so the run rewrites nothing and opens nothing.
"""

from __future__ import annotations

import argparse
import re
import subprocess
import sys
import tempfile
import zipfile
from pathlib import Path

from packaging.version import InvalidVersion, Version

_REPO_ROOT = Path(__file__).resolve().parents[2]
_VERSIONS_FILE = _REPO_ROOT / "dev_dep_versions.yml"

# A wheel version on the PyTorch index carries a local label naming its CUDA build, for
# example ``1.5.0.devYYYYMMDD+cu130``. The pin omits it so one pin serves every CUDA row.
_LOCAL_LABEL = re.compile(r"\+.*$")


def _run(cmd: list[str]) -> str:
return subprocess.run(cmd, check=True, capture_output=True, text=True).stdout


def read_pin(field: str) -> str:
"""Return a pinned value from ``dev_dep_versions.yml``."""
text = _VERSIONS_FILE.read_text(encoding="utf-8")
match = re.search(rf'^{field}:\s*"?([^"\s]+)"?\s*$', text, re.MULTILINE)
if match is None:
raise SystemExit(f"{field} is not set in {_VERSIONS_FILE.name}")
return match.group(1)


def available_versions(index_args: list[str]) -> list[str]:
"""Every ExecuTorch version the index offers.

``pip index versions`` prints one ``Available versions:`` line. Parsing that is stable
and needs no network code here; the workflow passes the index the same way every other
install in the tree does.
"""
out = _run(
[sys.executable, "-m", "pip", "index", "versions", "executorch", *index_args]
)
match = re.search(r"^\s*Available versions:\s*(.+)$", out, re.MULTILINE)
if match is None:
raise SystemExit("pip index versions printed no Available versions line")
return [v.strip() for v in match.group(1).split(",") if v.strip()]


def pick_target(versions: list[str], track: str) -> str:
"""The newest version on the wanted track.

``nightly`` takes the newest dated dev build; ``stable`` takes the newest final
release, ignoring dev builds and release candidates. Ordering is PEP 440, not string
order, so a newer dev date on the same line sorts above an older one correctly.
"""
parsed: list[tuple[Version, str]] = []
for raw in versions:
try:
version = Version(raw)
except InvalidVersion:
continue
# A nightly is a dated dev build. is_prerelease is also true for release
# candidates, and an rc sorts above every dev of the same line under PEP 440, so
# filtering on it would let the first rc on the index silently become the nightly
# pin. Match the dev segment itself instead.
if track == "nightly" and version.dev is None:
continue
if track == "stable" and (version.is_prerelease or version.is_devrelease):
continue
parsed.append((version, raw))
if not parsed:
raise SystemExit(f"no executorch version on the index matches track {track!r}")
newest = max(parsed, key=lambda pair: pair[0])[1]
return _LOCAL_LABEL.sub("", newest)


def wheel_git_version(version: str, index_args: list[str]) -> str:
"""The source commit the chosen wheel records for itself.

Every published wheel writes ``git_version`` into ``executorch/version.py``. Reading it
from the wheel is what keeps the two pins naming one ExecuTorch. A wheel built without
git provenance records ``None`` and must not become a pin, so that is an error, not a
guess.
"""
with tempfile.TemporaryDirectory() as tmp:
_run(
[
sys.executable,
"-m",
"pip",
"download",
"--no-deps",
"--only-binary=:all:",
"--dest",
tmp,
f"executorch=={version}",
*index_args,
]
)
wheels = list(Path(tmp).glob("executorch-*.whl"))
if not wheels:
raise SystemExit(
f"pip download produced no wheel for executorch=={version}"
)
with zipfile.ZipFile(wheels[0]) as archive:
source = archive.read("executorch/version.py").decode("utf-8")
match = re.search(r"""git_version[^=]*=\s*['"]([0-9a-f]{40})['"]""", source)
if match is None:
raise SystemExit(
f"executorch=={version} records no source commit, so the pin would name a "
"wheel whose provenance cannot be checked"
)
return match.group(1)


def _upper_bound(version: str) -> str:
"""The exclusive upper bound a range site pairs with the pin, next minor of its line.

``tests/py/dynamo/executorch/test_executorch_pin.py`` derives the same bound from the
same two fields, so a range this writes and the range the guard expects agree by the
same rule rather than by coincidence.
"""
major, minor = version.split(".")[:2]
return f"{major}.{int(minor) + 1}"


# The only files that carry the pin as a real pin. A tree-wide literal replace was safe while the
# pin was a dated dev string, because "1.5.0.dev20260825" appears nowhere else, but it corrupts the
# tree the moment the pin is a plain release like "1.4.1": that token also lives in unrelated
# requirements (for example pandocfilters>=1.4.1 in committed notebooks) and, worst, in uv.lock,
# whose entries are content addressed, so rewriting the version inside a wheel URL while its hash and
# size stay behind is a guaranteed install failure. So restrict the rewrite to the sites the guard in
# tests/py/dynamo/executorch/test_executorch_pin.py enumerates, which are the only sites that are
# actually pins. A new legitimate site must be added here and to that guard together.
_PIN_SITES = (
".github/workflows/executorch-build-linux.yml",
".github/workflows/executorch-test-linux.yml",
".github/workflows/release-linux-x86_64.yml",
"MODULE.bazel",
"docker/MODULE.bazel.docker",
"docker/MODULE.bazel.ngc",
"justfile",
"py/torch-tensorrt-executorch-runtime/README.md",
"py/torch-tensorrt-executorch-runtime/pyproject.toml",
"toolchains/ci_workspaces/MODULE.bazel.tmpl",
"examples/executorch_reference_runner/README.md",
)


def _pin_site_paths() -> list[Path]:
tracked = set(_run(["git", "-C", str(_REPO_ROOT), "ls-files"]).splitlines())
# An entry that is no longer tracked is a stale list, not an absent pin, so refuse rather than
# skip it. Skipping rewrites the other sites and returns success, and the workflow's gate is
# `git diff --quiet`, which detects change and not coherence, so the bot would open a pull
# request whose unrewritten site still names the old pin. The likely cause is a pin site being
# renamed without this list following it.
missing = sorted(name for name in _PIN_SITES if name not in tracked)
if missing:
raise SystemExit(
"these pin sites are not tracked by git, so the pin cannot be rewritten "
f"consistently: {missing}. Update _PIN_SITES, and the matching list in "
"tests/py/dynamo/executorch/test_executorch_pin.py, if a file moved."
)
paths = [_REPO_ROOT / name for name in _PIN_SITES]
# dev_dep_versions.yml is the source of truth and is rewritten too; it is not in _PIN_SITES
# because the guard reads it rather than counting it as a downstream pin site.
paths.append(_VERSIONS_FILE)
return paths


def write_pins(new_version: str, new_commit: str) -> bool:
"""Rewrite the pin to the new version and commit at the known pin sites.

The rewrite is restricted to the sites the guard enumerates (see _PIN_SITES), and within them it
matches only a requirement on the executorch distribution. It is NOT a tree-wide literal
replace, and it is not a bare version match either: a plain release like "1.4.1" appears in
unrelated requirements, in content-addressed uv.lock entries, and in other pins inside the pin
files themselves. Neither setup.py nor uv.lock is a pin site; they are left for their own tooling
to regenerate. Returns whether anything changed.
"""
old_version = read_pin("__executorch_version__")
old_commit = read_pin("__executorch_commit__")
if (new_version, new_commit) == (old_version, old_commit):
return False

# A single regex pass, so a freshly written new version cannot be matched again. A plain
# text.replace of the bare version doubles the tail when the old version is a prefix of the new
# one (1.5.0 -> 1.5.0.post1 would yield 1.5.0.post1.post1), because the second replace re-hits
# what the first just wrote. The trailing boundary avoids that, and the range is handled by the
# same alternation so its own version is not rewritten twice.
#
# Anchored on what names the pin, because a bare version is not distinctive enough to identify
# one. Downstream sites all spell it as a requirement on the executorch distribution, while the
# same files carry unrelated versions that are lexically identical: each MODULE.bazel has
# bazel_dep(name = "bazel_skylib", version = "1.7.1"), so a pin of 1.7.1 rewrote that too.
# Neither a leading nor a trailing character-class boundary helps there, since the character
# before the version is a quote, and restricting the rewrite to known pin files does not help
# either, because those are the very files holding the bystanders. dev_dep_versions.yml is the
# exception: it is the source of truth and states the version as a YAML key rather than a
# requirement, so it gets its own alternative.
#
# Every operator the guard in tests/py/dynamo/executorch/test_executorch_pin.py treats as a pin,
# with its optional spaces, not just the two spellings the tree happens to use today. A site the
# guard counts but this rewriter skips is the worst shape available: the bump leaves it on the old
# version, and the guard then fails the generated pull request as a pin mismatch rather than as an
# operator the rewriter cannot see.
#
# The name needs a left boundary of its own, or "my-executorch==<pin>" matches on its tail.
# Distribution names normalise hyphens and underscores together, so exclude both, plus a dot.
old_upper = _upper_bound(old_version)
new_upper = _upper_bound(new_version)
old_range_tail = f"{old_version},<{old_upper}"
version_token = re.compile(
r"(?P<lead>(?<![0-9A-Za-z._-])executorch ?(?:===|==|>=|<=|~=|!=|<|>) ?"
r"|__executorch_version__:\s*\"?)(?:"
+ re.escape(old_range_tail)
+ r"|"
+ re.escape(old_version)
+ r")(?![0-9A-Za-z.+_-])"
)

def _sub_version(match: re.Match[str]) -> str:
lead = match.group("lead")
if match.group(0).endswith(f",<{old_upper}"):
return f"{lead}{new_version},<{new_upper}"
return f"{lead}{new_version}"

changed = False
for path in _pin_site_paths():
try:
text = path.read_text(encoding="utf-8")
except (UnicodeDecodeError, FileNotFoundError):
continue
if old_version not in text and old_commit not in text:
continue
updated = version_token.sub(_sub_version, text)
updated = updated.replace(old_commit, new_commit)
if updated != text:
path.write_text(updated, encoding="utf-8")
changed = True

if read_pin("__executorch_version__") != new_version:
raise SystemExit(
"the version pin did not take; dev_dep_versions.yml is unchanged"
)
return changed


def _index_args(track: str, channel: str) -> list[str]:
if track == "nightly":
return [
"--pre",
"--index-url",
f"https://download.pytorch.org/whl/nightly/{channel}",
]
return []


def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--track", choices=("nightly", "stable"), default="nightly")
parser.add_argument(
"--channel",
default="cu130",
help="nightly CUDA channel to read versions and provenance from",
)
parser.add_argument(
"--allow-downgrade",
action="store_true",
help=(
"move the pin even when the target sorts below the current pin. Off by default so a "
"regressed index, or switching --track from nightly to stable, cannot silently walk "
"the pin backwards onto a version this delegate cannot build against."
),
)
args = parser.parse_args(argv)

index_args = _index_args(args.track, args.channel)
target = pick_target(available_versions(index_args), args.track)
current = read_pin("__executorch_version__")
if target == current:
print(f"executorch pin is already at the newest {args.track} version {current}")
return 0
# Never move backwards unless asked. pick_target returns the newest on the track, but "newest"
# regresses when the index drops the current line or when --track flips from nightly to stable,
# whose newest final release can sort below a dated nightly. The pin then lands on a version with
# no standalone libexecutorch.so and no CUDA build, which the delegate cannot link, and the run
# would still report success. Refuse it, and require an explicit opt-in for a deliberate re-pin.
if not args.allow_downgrade and Version(target) < Version(current):
print(
f"target {target} sorts below the current pin {current}; refusing to move the pin "
"backwards. Pass --allow-downgrade to override for a deliberate re-pin."
)
return 0

commit = wheel_git_version(target, index_args)
if write_pins(target, commit):
print(f"moved executorch pin {current} -> {target} (commit {commit})")
else:
print("nothing to write")
return 0


if __name__ == "__main__":
raise SystemExit(main())
23 changes: 23 additions & 0 deletions .github/workflows/ci-sbsa.yml
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,30 @@ jobs:
name-prefix: "RTX Python-only SBSA "
raw-matrix: ${{ needs.generate-matrix.outputs.matrix }}

executorch-runtime-build:
# Ordered after `build`, not just after the matrix: this job downloads the torch-tensorrt wheel
# that `build` uploads, and starting earlier fails with "Artifact not found for name:
# pytorch_tensorrt__...". The x86_64 caller waits on its own wheel job for the same reason.
needs: [decide, generate-matrix, build]
# Same lane gating as the standard SBSA channels, plus the rtx exclusion the x86_64 caller
# applies: the delegate links stock TensorRT, not TensorRT-RTX.
if: >-
!cancelled() &&
needs.build.result == 'success' &&
(needs.decide.outputs.lane == 'full' || needs.decide.outputs.lane == 'nightly') &&
needs.decide.outputs.backend != 'rtx'
uses: ./.github/workflows/executorch-build-linux.yml
with:
repository: pytorch/tensorrt
ref: ""
test-infra-repository: pytorch/test-infra
test-infra-ref: main
build-matrix: ${{ needs.generate-matrix.outputs.matrix }}
architecture: aarch64
gate:
# Deliberately not depending on executorch-runtime-build. The gate fails the whole workflow on
# any failure among its needs, so putting the delegate here would let it block every unrelated
# pull request that touches SBSA. It reports on its own check instead.
needs: [decide, build, python-only, build-rtx, python-only-rtx]
if: always()
runs-on: ubuntu-latest
Expand Down
7 changes: 6 additions & 1 deletion .github/workflows/docgen.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,12 @@ jobs:
run: echo "sha=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
- name: Build Python Package
run: |
python3 -m pip install --pre ".[executorch]" --extra-index-url https://download.pytorch.org/whl/nightly/cu130
# The pin exactly, not the range the extra expands to: --pre plus a nightly
# channel that gains a member daily would otherwise install whichever dev build
# is newest that morning while the delegate compiles from the pinned commit.
python3 -m pip install --pre ".[executorch]" \
"executorch==$(python3 -c 'import yaml;print(yaml.safe_load(open("dev_dep_versions.yml"))["__executorch_version__"])')" \
--extra-index-url https://download.pytorch.org/whl/nightly/cu130
- name: Install uv
run: |
curl -LsSf https://astral.sh/uv/install.sh | sh
Expand Down
Loading
Loading