Skip to content

Commit ce65e3f

Browse files
committed
Drop the ExecuTorch runtime from the Torch-TensorRT runtime wheel
The torch-tensorrt-executorch-runtime wheel shipped a full ExecuTorch Python runtime alongside the TensorRT delegate. This ships only the delegate: a single shared library that registers TensorRTBackend with the ExecuTorch runtime that the executorch distribution already provides, rather than bundling a second copy of that runtime. Shipping a second copy is also what made the old wheel prone to a libstdc++ clash, because two C++ runtimes could end up in one process. The native build produces just the delegate library, its RUNPATH points at the executorch package the delegate links against, and setup.py packages the one shared object. The runtime dependency stays commented out in the top-level setup.py because the delegate wheel is not published to any index yet, so the docs and the load-time and save-time errors direct users to build it from py/torch-tensorrt-executorch-runtime/README.md. The delegate links the C++ runtime dynamically, the way every other shared object in the process already does. The build toolchain is newer than the libstdc++ on a user's machine, so an optimized build emits out-of-line calls into the newer runtime, for example std::string::_M_replace_cold. Naming stdc++ as a link library puts the reference after the objects, where the toolchain's own libstdc++.so linker script resolves it: the old, stable symbols bind dynamically to the system libstdc++.so.6 and only the newer helpers are pulled statically from the toolchain's companion archive. The delegate ends up needing no C++ runtime version above what the ExecuTorch it loads beside already needs. A static C++ runtime is deliberately avoided: this library is loaded next to libtorch and ExecuTorch, and a private libstdc++ would give it its own exception type_info and locale state, which breaks exceptions and dynamic_cast across the boundary. The build guard checks the shape: the delegate keeps a dynamic libstdc++ dependency, has no unversioned C++ runtime symbol left undefined, and requires no symbol version above the paired runtime. The wheel is tagged py3-none rather than per-interpreter, because the delegate is a plain shared object with no Python ABI and one build serves every CPython. test_api.py checks the shipped layout: the delegate resolves through the loader in the layout that ships, the wheel's RUNPATH is compared whole against the one the build asks for, the symbol versions and the C++ runtime dependency are compared against the runtime the delegate links, and the wheel's own metadata is checked. The reachability scans that assert the import and static-C++ checks run in CI parse each language's grammar rather than matching text, and none of them execute the workflow they inspect. The wheel exposes no runtime API at all. Loading and running a program belongs to ExecuTorch, which already ships Runtime, Program and Method, so the Python wrapper this wheel used to carry is gone along with the load(format="executorch") entry point that reached it. That wrapper duplicated ExecuTorch's own classes down to the line that keeps the file buffer alive, and its CPU copy of top-level inputs quietly defeated programs exported for device-resident inputs. A consumer now imports this package and uses executorch.runtime directly. Registration happens on import, so there is nothing to call. ExecuTorch's own delegates register because they are linked into its pybindings extension, and loading that extension pulls them in; a delegate in a separate wheel cannot join that link and ExecuTorch has no discovery hook for out-of-tree backends, so this package performs the equivalent step itself. A load it cannot complete raises from the import rather than being swallowed, because the diagnosis here names the real cause, a CPU-only ExecuTorch wheel or an ABI mismatch, which a later "backend not available" cannot. TORCH_TENSORRT_SKIP_DELEGATE_REGISTRATION=1 imports the module without the side effect, for tooling that wants the metadata only. The wheel now follows the layout ExecuTorch uses for its own backends, so the TensorRT delegate is an out-of-tree sibling of them rather than a Python-only artifact. The shared library moves to lib/, next to where executorch keeps libexecutorch_backend_cuda.so and friends, and the wheel ships a CMake package under share/cmake so a C++ app can link it: find_package(executorch REQUIRED COMPONENTS backend_cuda) find_package(torchtrt_executorch REQUIRED) target_link_libraries(app PRIVATE executorch::runtime torchtrt::executorch_backend) Before this the shared library was reachable only from Python, even though it is a drop-in sibling of ExecuTorch's backends: same naming, same soname convention, register_backend imported rather than defined. What was missing was the discovery layer, so the only way for C++ to get the delegate was add_subdirectory against a source checkout of this repository. The imported target links with --no-as-needed, bracketed by push-state and pop-state. Nothing in a consumer references a symbol the delegate defines, so the default would drop the dependency and the backend would never register: the app would build, load the program, and fail with an unregistered backend. That is the shared-library counterpart of the --whole-archive the in-repo source build needs for the same reason. No headers ship, because a consumer calls no Torch-TensorRT code; registration happens in the library's static initializer and the rest is ExecuTorch's runtime API. Moving the library under lib/ also moves what $ORIGIN means, so the delegate's own RUNPATH gains a level: $ORIGIN/../../executorch/lib rather than $ORIGIN/../executorch/lib, and likewise for tensorrt_libs and nvidia/cu13/lib. Without that the entries resolve inside the package directory instead of site-packages, the delegate cannot find libexecutorch.so, libcudart or libnvinfer, and a C++ consumer fails to link it with undefined references to cudaMemcpyAsync and friends. The depth and the install location are one decision, so the test that reads the declaration now rejects the single-level form it used to require. The CMake package installs to lib/cmake/torchtrt_executorch, which is where ExecuTorch puts its own: find_package resolves executorch from site-packages/executorch/lib/cmake/executorch, so following that layout rather than share/ means a consumer points CMAKE_PREFIX_PATH at the two package roots and both resolve the same way. The walk that locates the package root now looks for the delegate itself instead of for a directory named lib, because the config now lives inside lib/ and stopping at the first lib/ it meets would set IMPORTED_LOCATION to that directory. The CMake package test now configures the package with real CMake and asks for the imported target back, because a string search over the config cannot tell a working package from a broken one: inserting return() after cmake_minimum_required makes the config define nothing and every string assertion still passes. The README command locates ExecuTorch through its distribution metadata, since it is a namespace package whose __file__ is None, so the documented one-liner raised TypeError before CMake ran. The delegate builds for CUDA 12 as well as CUDA 13, because torch-tensorrt publishes both channels. Three places assumed one major. The version check now accepts either, since a minor bump inside a major does not change the ABI the delegate links. The RUNPATH carries both layout directories, because the two majors package their runtime differently: the CUDA 13 wheels install nvidia/cu13/lib while the CUDA 12 wheels install nvidia/cuda_runtime/lib. The artifact check maps the CUDA runtime the delegate asks for to the directory that carries it and fails when the RUNPATH has no matching entry, which is the case that would link cleanly and then find nothing at load time. The symbol version ceiling is compared against the manylinux platform the wheel ships under rather than against the ExecuTorch distribution beside it, and that platform is passed in per architecture because the two rows use different builder images. A symbol version requirement is a floor on the host, not a ceiling a library imposes on its neighbours: two libraries in one process may need different versions, and the loader only needs the host to satisfy the highest. Comparing against the sibling rejected the delegate wherever TensorRT itself was built with a newer toolchain than ExecuTorch, which is the case on aarch64 today, and by that rule the check would reject TensorRT too. The delegate is built for aarch64 as well as x86_64, matching the architectures the torch-tensorrt wheel it pairs with already ships. The native build already selected the right TensorRT per architecture; what was missing is that the only caller generated an x86_64 matrix, and the architecture input defaults to x86_64, so nothing ever asked for the other rows. The aarch64 workflow now calls the same build against its own matrix, ordered after the job that uploads the wheel it downloads, and deliberately outside that workflow's gate so a delegate failure cannot block pull requests that have nothing to do with the delegate. The check that the downloaded wheel carries the C++ runtime looks for the library instead of importing the compiler package. That import reaches torch.cuda.get_device_capability() while deciding whether it is running on Tegra, so it needs a GPU, and the aarch64 builder has none. The symbol version cases in the guard's own test pass the manylinux tag, without which the ceiling is skipped and every one of them passes for the wrong reason. Three of them asserted the old rule, that a version above the ExecuTorch distribution's own is a rejection, and now expect the artifact to be accepted: all three sit below what the platform guarantees, and the host provides the C++ runtime rather than the sibling wheel. The export and the reference runner run only where a GPU is present. Both compile and execute a TensorRT engine, and the aarch64 builders are CPU-only instances, which is why the wheel's own aarch64 lanes build without running their tests. The delegate is still built and checked on aarch64; its runtime behaviour stays covered by the x86_64 rows, which have a GPU. Keyed on whether the device is usable rather than on the architecture, so a GPU runner never skips it. The delegate follows the main wheel's CUDA versions, CUDA 12.6 included. Both read the same matrix filter, so the rows agree by construction: 25 rows, with cu126 on x86_64 only and the Arm rows on CUDA 13, which is what the main wheel publishes. The TensorRT distribution is resolved from the CUDA the build actually uses rather than hardcoded, so a cu126 row declares tensorrt-cu12 and a cu13 row declares tensorrt-cu13. The RUNPATH and the ELF guard already carried both CUDA layouts, nvidia/cuda_runtime/lib for 12 and nvidia/cu13/lib for 13, so no packaging change was needed for the new rows. Test plan: Ran the real filter from this branch against a full three-CUDA, five-Python, two-architecture input and compared it against main's filter on the same input: both return the same 25 rows. Added a test that asserts cu126 is present on x86_64, absent on aarch64, and that both CUDA 13 rows survive on each architecture. It fails when cu126 is removed from the x86 list. Replaced the test that asserted a hardcoded tensorrt-cu13, which would have kept passing while a cu126 row declared the wrong dependency; the replacement fails when the resolution is hardcoded again. The release lane installs ExecuTorch too, so it is a pin site and now names the pinned nightly from the nightly channel. It arrived with the CUDA 12.6 rows naming a stale release off the default index, which resolves no ExecuTorch at all, and the pin checks caught it. A URL is no longer mistaken for a comment. The trailing-comment strip cut at the first "//", so any line carrying an index URL was truncated before its requirement and the site was reported as missing rather than as wrong. It now skips a "//" that follows a colon, which is a scheme rather than a comment. The channel variable that install reaches through is asserted to stay in scope. The URL is built from CU_VERSION, which the reusable build workflow exports from the matrix row; if that export is renamed the URL collapses to a channel that does not exist, pip falls back to the default index, and the install resolves the wrong ExecuTorch without failing.
1 parent 4d4033c commit ce65e3f

25 files changed

Lines changed: 4324 additions & 863 deletions

File tree

.github/workflows/ci-sbsa.yml

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,30 @@ jobs:
114114
name-prefix: "RTX Python-only SBSA "
115115
raw-matrix: ${{ needs.generate-matrix.outputs.matrix }}
116116

117+
executorch-runtime-build:
118+
# Ordered after `build`, not just after the matrix: this job downloads the torch-tensorrt wheel
119+
# that `build` uploads, and starting earlier fails with "Artifact not found for name:
120+
# pytorch_tensorrt__...". The x86_64 caller waits on its own wheel job for the same reason.
121+
needs: [decide, generate-matrix, build]
122+
# Same lane gating as the standard SBSA channels, plus the rtx exclusion the x86_64 caller
123+
# applies: the delegate links stock TensorRT, not TensorRT-RTX.
124+
if: >-
125+
!cancelled() &&
126+
needs.build.result == 'success' &&
127+
(needs.decide.outputs.lane == 'full' || needs.decide.outputs.lane == 'nightly') &&
128+
needs.decide.outputs.backend != 'rtx'
129+
uses: ./.github/workflows/executorch-build-linux.yml
130+
with:
131+
repository: pytorch/tensorrt
132+
ref: ""
133+
test-infra-repository: pytorch/test-infra
134+
test-infra-ref: main
135+
build-matrix: ${{ needs.generate-matrix.outputs.matrix }}
136+
architecture: aarch64
117137
gate:
138+
# Deliberately not depending on executorch-runtime-build. The gate fails the whole workflow on
139+
# any failure among its needs, so putting the delegate here would let it block every unrelated
140+
# pull request that touches SBSA. It reports on its own check instead.
118141
needs: [decide, build, python-only, build-rtx, python-only-rtx]
119142
if: always()
120143
runs-on: ubuntu-latest

.github/workflows/executorch-build-linux.yml

Lines changed: 197 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@ on:
2323
description: "Test infra reference to use"
2424
default: ""
2525
type: string
26+
architecture:
27+
description: "x86_64 or aarch64. Must match the os the caller generated its matrix for."
28+
default: "x86_64"
29+
type: string
2630

2731
jobs:
2832
filter-matrix:
@@ -49,6 +53,7 @@ jobs:
4953
uses: ./.github/workflows/linux-test.yml
5054
with:
5155
job-name: executorch-runtime-build
56+
architecture: ${{ inputs.architecture }}
5257
repository: ${{ inputs.repository }}
5358
ref: ${{ inputs.ref }}
5459
test-infra-repository: ${{ inputs.test-infra-repository }}
@@ -85,15 +90,37 @@ jobs:
8590
# CU_VERSION selects the row's own channel, which is what keeps the runtime the
8691
# delegate links to the same CUDA build as the rest of the job.
8792
EXECUTORCH_INDEX_URL="https://download.pytorch.org/whl/nightly/${CU_VERSION}"
88-
python -m pip install pyyaml --extra-index-url "${EXECUTORCH_INDEX_URL}" "executorch==1.5.0.dev20260901"
93+
python -m pip install pyyaml patchelf --extra-index-url "${EXECUTORCH_INDEX_URL}" "executorch==1.5.0.dev20260901"
8994
export TORCH_TENSORRT_EXECUTORCH_RUNTIME_VERSION="$(python -c 'import importlib.metadata; print(importlib.metadata.version("torch-tensorrt"))')"
9095
9196
# The downloaded wheel has to carry the C++ runtime. A wheel built with
9297
# PYTHON_ONLY=1 has no libtorchtrt.so, which leaves
9398
# ENABLED_FEATURES.torch_tensorrt_runtime False, and the export near the
9499
# end of this script then fails after the Bazel builds have already run.
95100
# Check it here so the wrong wheel is reported as the wrong wheel.
96-
python -c 'from torch_tensorrt._features import ENABLED_FEATURES as f; assert f.torch_tensorrt_runtime, f'
101+
#
102+
# Checked by looking for the library rather than by importing
103+
# torch_tensorrt._features, which reaches torch.cuda.get_device_capability()
104+
# while deciding whether it is on Tegra and so needs a GPU. The aarch64
105+
# builder has none, and the presence of the file is what this cares about.
106+
# Matched exactly, not with a trailing wildcard: a glob of libtorchtrt.so*
107+
# also matches a renamed neighbour, so the check passed against a wheel whose
108+
# real library had been moved aside.
109+
python - <<'PY'
110+
import importlib.util
111+
import pathlib
112+
import sys
113+
114+
spec = importlib.util.find_spec("torch_tensorrt")
115+
assert spec is not None and spec.origin, "torch_tensorrt is not importable"
116+
lib = pathlib.Path(spec.origin).parent / "lib" / "libtorchtrt.so"
117+
if not lib.is_file():
118+
sys.exit(
119+
"the downloaded torch-tensorrt wheel ships no libtorchtrt.so, so it is a "
120+
"python-only build and the delegate cannot link its C++ runtime"
121+
)
122+
print(f"torch-tensorrt C++ runtime present: {lib.name}")
123+
PY
97124
98125
# Bazel truncates a failing action's output at 1 MB by default and then
99126
# prints nothing but the size, which hid the real error behind
@@ -105,6 +132,159 @@ jobs:
105132
106133
# Build the no-compile-for-users Python runtime wheel.
107134
python -m pip wheel --no-build-isolation --no-deps --wheel-dir dist py/torch-tensorrt-executorch-runtime
135+
# The point of this wheel is that it carries the TensorRT delegate and leaves the
136+
# ExecuTorch runtime to the executorch wheel. Nothing else here would notice it
137+
# quietly going back to shipping a second copy of that runtime, and the wheel would
138+
# still work, so assert the contents rather than the behavior.
139+
python - "$(ls dist/torch_tensorrt_executorch_runtime-*.whl)" <<'PY'
140+
import ast, pathlib, re, sys, zipfile
141+
142+
import yaml
143+
# Read the name out of setup.py rather than repeating it, so renaming the delegate
144+
# keeps this honest instead of quietly turning the assertion below into a no-op.
145+
# Parsed rather than imported: importing setup.py would run setup().
146+
source = pathlib.Path("py/torch-tensorrt-executorch-runtime/setup.py").read_text()
147+
library, = [
148+
node.value.value
149+
for node in ast.parse(source).body
150+
if isinstance(node, ast.Assign)
151+
and any(getattr(t, "id", None) == "DELEGATE_LIBRARY" for t in node.targets)
152+
]
153+
names = zipfile.ZipFile(sys.argv[1]).namelist()
154+
# ".so" and ".so.<version>" both: libnvinfer.so.10 and libstdc++.so.6 are shared objects
155+
# the wheel must not carry, and a suffix test for ".so" alone does not see them.
156+
objects = sorted(n for n in names if re.search(r"\.so(\.\d+)*$", n))
157+
print("shared libraries in the wheel:", objects)
158+
# sys.exit rather than assert throughout: under PYTHONOPTIMIZE or python -O every assert
159+
# is compiled out, and this step would print its success message over a bad wheel.
160+
def reject(message):
161+
sys.exit(f"FATAL: {message}")
162+
if len(objects) != 1:
163+
reject(f"expected exactly the delegate, got {objects}")
164+
# The full expected path, not a suffix: the wheel must ship the ExecuTorch delegate name
165+
# inside the package directory, so neither a setuptools-mangled name nor a stray copy in
166+
# some other package satisfies this. Under lib/, the same place ExecuTorch keeps its own
167+
# backends, so one location serves both the Python loader and the CMake package.
168+
expected = "torch_tensorrt_executorch_runtime/lib/" + library
169+
if objects[0] != expected:
170+
reject(f"expected {expected}, got {objects}")
171+
# A C++ consumer links this wheel through its CMake package, the same way it links
172+
# ExecuTorch's own backends. Without these two files the delegate is reachable from Python
173+
# only, and the shared library above is unusable outside this repo's source tree.
174+
for required in (
175+
"torch_tensorrt_executorch_runtime/lib/cmake/torchtrt_executorch/torchtrt_executorch-config.cmake",
176+
"torch_tensorrt_executorch_runtime/lib/cmake/torchtrt_executorch/torchtrt_executorch-config-version.cmake",
177+
):
178+
if required not in names:
179+
reject(f"the wheel ships no CMake package: {required} is missing")
180+
forbidden = [n for n in names if any(
181+
part in n for part in ("_portable_lib", "libexecutorch.so", "libextension_cuda", "libaoti_cuda_shims"))]
182+
if forbidden:
183+
reject(f"the wheel ships ExecuTorch runtime components: {forbidden}")
184+
# The platform tag, because the payload is a Linux ELF object. Dropping
185+
# distclass=PlatformDistribution from setup.py yields a py3-none-any wheel that pip would
186+
# install into purelib on Windows or macOS, and the content checks above all still pass.
187+
tag = pathlib.Path(sys.argv[1]).name.rsplit("-", 1)[-1][: -len(".whl")]
188+
# Every platform in a compressed tag, not a substring search: "win_amd64.linux_fake"
189+
# contains "linux" and expands to a tag pip will install on Windows.
190+
platforms = tag.split(".")
191+
alien = [
192+
p for p in platforms
193+
if not re.fullmatch(r"(many|musl)?linux[0-9_.]*_(x86_64|aarch64|i686)", p)
194+
]
195+
if alien or not platforms:
196+
reject(f"wheel is tagged {tag}, which would install on non-Linux platforms: {alien}")
197+
wheel_metadata = next(
198+
(n for n in names if re.fullmatch(r"[^/]+\.dist-info/WHEEL", n)), None)
199+
if wheel_metadata is None:
200+
reject("wheel carries no dist-info/WHEEL to check Root-Is-Purelib against")
201+
purelib = zipfile.ZipFile(sys.argv[1]).read(wheel_metadata).decode()
202+
if "Root-Is-Purelib: false" not in purelib:
203+
reject(f"wheel declares itself pure python:\n{purelib}")
204+
# Requires-Dist, which is where the build environment leaks into the artifact: setup.py
205+
# derives the executorch pin from whatever is installed, so a wheel built beside the wrong
206+
# version requires that version, and every content check above still passes. A local label
207+
# is the same class of problem -- it binds the wheel to one CUDA train, which is why every
208+
# requirement is stripped of one with public_version().
209+
core_metadata = next(
210+
(n for n in names if re.fullmatch(r"[^/]+\.dist-info/METADATA", n)), None)
211+
if core_metadata is None:
212+
reject("wheel carries no dist-info/METADATA to check Requires-Dist against")
213+
metadata = zipfile.ZipFile(sys.argv[1]).read(core_metadata).decode()
214+
requires = [
215+
r.strip()
216+
for r in re.findall(r"^Requires-Dist:\s*(.+)$", metadata, re.MULTILINE)
217+
]
218+
pinned = yaml.safe_load(
219+
open("dev_dep_versions.yml"))["__executorch_version__"]
220+
if f"executorch=={pinned}" not in requires:
221+
reject(
222+
f"wheel requires {[r for r in requires if 'executorch' in r]} but the "
223+
f"repository pins executorch=={pinned}:\n" + "\n".join(requires))
224+
# setup.py derives torch-tensorrt, torch, tensorrt-cu13 and nvidia-cuda-runtime from
225+
# importlib.metadata the same way it derives executorch, so a wheel built beside the wrong
226+
# version of any of them requires that version and every content check above still passes.
227+
# torch-tensorrt is the requirement that binds this runtime wheel to the producer that
228+
# emitted the program; torch is the framework both were built against. None has a repository
229+
# pin to compare a value against -- their version is whatever CI installed -- but the
230+
# derivation can still be checked for shape: each must be present and pinned with an exact
231+
# "==", so a requirement that went missing or loosened to a range is caught here rather than
232+
# shipping. executorch is checked above against the repository pin, which is stricter.
233+
for distribution in (
234+
"torch-tensorrt",
235+
"torch",
236+
"tensorrt-cu13",
237+
"nvidia-cuda-runtime",
238+
):
239+
matched = [
240+
r for r in requires
241+
if re.match(rf"{re.escape(distribution)}\s*(==|[<>!~ ;]|$)", r)
242+
]
243+
if not matched:
244+
reject(
245+
f"wheel does not require {distribution}, which setup.py derives from the "
246+
f"build environment:\n" + "\n".join(requires))
247+
if not any(
248+
re.match(rf"{re.escape(distribution)}==[^;]+", r) for r in matched
249+
):
250+
reject(
251+
f"wheel requires {matched} without an exact == pin, so it does not bind "
252+
f"{distribution} to the version it was built beside:\n" + "\n".join(requires))
253+
labelled = [r for r in requires if "+" in r.split(";")[0]]
254+
if labelled:
255+
reject(f"these requirements carry a local version label: {labelled}")
256+
print(
257+
f"the wheel ships only {objects[0]}, tagged {tag}, requiring "
258+
f"executorch=={pinned}")
259+
PY
260+
261+
# Every dependency actually resolves from where the wheel puts it. The ELF guard that runs
262+
# at link time compares the whole RUNPATH against what the build asked for and checks one
263+
# ExecuTorch symbol, but it cannot resolve anything: in a Bazel output tree the sibling
264+
# distributions do not exist yet, so it reasons about the artifact's own metadata rather
265+
# than loading it. Here the installed layout exists, so ask the loader instead of inferring:
266+
# a pin bump that drops some other ExecuTorch export reaches an undefined symbol at import
267+
# time that the link-time guard cannot see. -r resolves data and function symbols, which is
268+
# what turns an undefined ExecuTorch symbol into a failure rather than a silent success.
269+
# env -u LD_LIBRARY_PATH, because the test lane exports the CUDA site-packages directory
270+
# onto LD_LIBRARY_PATH (install-torch-tensorrt.sh), which puts nvidia/cu13/lib on the load
271+
# path and lets a missing $ORIGIN/../nvidia/cu13/lib RUNPATH entry resolve here while it
272+
# would not on a user's process. Clearing it makes ldd -r see what the user would.
273+
python -m pip install --no-deps "$(ls dist/torch_tensorrt_executorch_runtime-*.whl)"
274+
# Skip registration for this one call. Importing the package now LOADS the delegate, so a
275+
# pin bump that dropped a symbol would abort here with the package's own error and the
276+
# ldd -r guard below, which exists to diagnose exactly that, would never run. Resolving
277+
# the path is all this needs, and the plain import further down still proves the side
278+
# effect works.
279+
delegate="$(TORCH_TENSORRT_SKIP_DELEGATE_REGISTRATION=1 python -c 'import torch_tensorrt_executorch_runtime as m; print(m._delegate_path())')"
280+
echo "resolving ${delegate} from its installed location"
281+
if env -u LD_LIBRARY_PATH ldd -r "${delegate}" 2>&1 | grep -E "not found|undefined symbol" ; then
282+
echo "FATAL: the installed delegate has unresolved dependencies, so importing it fails." >&2
283+
echo "Either a RUNPATH entry is missing or the pinned ExecuTorch no longer provides a symbol it uses." >&2
284+
exit 1
285+
fi
286+
python -c "import torch_tensorrt_executorch_runtime"
287+
108288
# this is to build the libtorchtrt.tar.gz
109289
bazel build //:libtorchtrt --compilation_mode opt --config=linux
110290
# Run the ExecuTorch backend C++ unit tests, which are otherwise only
@@ -142,7 +322,18 @@ jobs:
142322
python -m venv "${RUNNER_TEMP}/range-check-venv"
143323
# pin-check: range-ok
144324
"${RUNNER_TEMP}/range-check-venv/bin/python" -m pip install --no-deps --extra-index-url "${EXECUTORCH_INDEX_URL}" "executorch>=1.5.0.dev20260901,<1.6"
145-
python examples/torchtrt_executorch_example/export_static_shape.py \
146-
--model_path="${RUNNER_TEMP}/torchtrt-reference-runner.pte"
147-
.github/scripts/verify-executorch-reference-runner.sh \
148-
"${RUNNER_TEMP}/torchtrt-reference-runner.pte"
325+
# The export compiles a TensorRT engine and the reference runner executes it, so both need a
326+
# GPU. The aarch64 builders have none: every arm64 runner label is a CPU-only Graviton
327+
# instance, which is why the wheel's own SBSA lanes pass run-tests false and build without
328+
# testing. Skipped rather than failed, so the aarch64 rows still produce and check an
329+
# artifact; the delegate's runtime behaviour stays covered by the x86_64 rows, which run
330+
# this on a GPU runner.
331+
if python -c 'import torch, sys; sys.exit(0 if torch.cuda.is_available() else 1)'; then
332+
python examples/torchtrt_executorch_example/export_static_shape.py \
333+
--model_path="${RUNNER_TEMP}/torchtrt-reference-runner.pte"
334+
.github/scripts/verify-executorch-reference-runner.sh \
335+
"${RUNNER_TEMP}/torchtrt-reference-runner.pte"
336+
else
337+
echo "No GPU on this runner, so the export and reference runner are skipped." >&2
338+
echo "The delegate itself was built and checked above." >&2
339+
fi

0 commit comments

Comments
 (0)