Skip to content

Stop shipping unusable MKL search paths in the Linux wheel - #22541

Merged
shoumikhin merged 1 commit into
mainfrom
fix-mkl-runpath-leak
Sep 4, 2026
Merged

Stop shipping unusable MKL search paths in the Linux wheel#22541
shoumikhin merged 1 commit into
mainfrom
fix-mkl-runpath-leak

Conversation

@shoumikhin

@shoumikhin shoumikhin commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Fixes #21611

The problem, in plain terms

Five shared libraries in the Linux wheel search three directories that exist on nobody's machine:

/lib/intel64
/lib/intel64_win
/lib/win-x64

Two of them name a Windows layout, in a Linux wheel.

They come from PyTorch. Its exported CMake package creates a caffe2::mkl imported target with a hardcoded list of link directories, and linking torch brings them in even though this project never asks for MKL:

${MKL_ROOT}/lib  ${MKL_ROOT}/lib/intel64  ${MKL_ROOT}/lib/intel64_win  ${MKL_ROOT}/lib/win-x64

MKL_ROOT resolves to nothing here, so what the linker records is left anchored at the filesystem root. Packaging copies the built libraries out of the build tree rather than installing them, so whatever the linker recorded ships as is. The bare /lib form does not survive, because CMake filters its own implicit link directories out of the link line, which is why three entries appear rather than four.

This only affects Linux x86_64. The aarch64 nightly records no absolute entries at all, since MKL is not found there, and the macOS and Windows wheels record none either.

Why it is worth fixing

Nothing needs those directories. No shipped library names an MKL or OpenMP runtime among its dependencies, so nothing resolves through them.

They are not merely untidy either. They sit ahead of the relative entries packaging appends, and the loader searches in order, so a user who happens to have a matching directory resolves a library from there instead of from the one the wheel installed. That is the same shadowing the release check already rejects a CUDA toolkit prefix for.

What changed

Two small pieces.

Packaging now drops these entries, in the same place the other unusable ones are already dropped. The match is deliberately narrow: only the exact /lib/<arch> form that an empty prefix produces. A real MKL installation spells the same arch directory below a prefix, as /opt/intel/mkl/lib/intel64, and that is a directory the environment genuinely provides, so it is kept.

The release check that rejects absolute search paths listed these three as allowed. That is why they shipped while a check whose whole purpose is rejecting absolute paths reported the wheel clean. It now rejects the empty prefix form specifically, and still accepts a real installation's prefixed directory, so the two halves agree rather than contradict each other.

Test plan

New unit tests in .ci/scripts/tests/test_runtime_path_filter.py. They read the functions out of setup.py, so they exercise the code that ships rather than a copy, and they run on every pull request through the existing unit test job. They need no wheel build, which is what the previous check could not manage: it could only see this after a full build, and only on the platform that built one.

pytest .ci/scripts/tests/test_runtime_path_filter.py
27 passed

They cover both directions, because a filter that satisfies either one alone is wrong:

  • the three unresolved entries are dropped
  • a real MKL installation and ordinary system directories are kept
  • every relative entry on a shipped library survives, so a library can still find its siblings
  • the absolute torch directory is kept when it is a library's only route to torch, which is what stops this becoming a blanket rule that breaks importing

I checked by mutation that each part of the fix is load bearing:

what I broke tests that failed
removed the packaging filter 4
removed the release check's maths rejection 3
removed its build directory branch 2
removed its catch-all rejection 1
severed the call that applies it to a shipped library 1
reordered its guards so a build directory is accepted 1
narrowed what it accepts, refusing a real install 3
made the packaging filter reject an ordinary system directory 1
added an arch to the shared constant, untaught to the check 2

The release check's per-entry decision is a small module level function, so the unit test calls the same code the wheel check runs and compares the reason it returns. Asserting only that a path was rejected was not enough: the check rejects every absolute path it does not recognise, so an unknown arch passed for the wrong reason and two of its three branches could each be deleted on their own.

I also measured the published artifacts directly, reading the recorded search paths out of every shared library in each wheel:

wheel libraries with a search path unusable entries
1.4.1 release, CPython 3.12 6 0
current nightly, CPU, CPython 3.12 18 15
current nightly, CUDA, CPython 3.13 21 15
current nightly, CPU, aarch64 16 0

The 1.4.1 release predates the code changed here. Both nightly rows show the same 15 entries across the same five libraries, which is what this removes. Reading those same libraries' declared dependencies shows no MKL or OpenMP runtime in any of them.

Scope

This covers the first of the two things the issue suggests: not shipping a runtime search path a user cannot use. It does not do the second, which is to resolve MKL through something the wheel or its declared dependencies own. That one belongs upstream, since the directory list is set in PyTorch's own CMake package, and its own comment there marks it as a hack. Nothing in this wheel links MKL, so there is no dependency here left to redirect.

What I did not verify

No wheel was built for this. The change is to the filter those builds already run, and the tests exercise it directly. The link line that emits the paths is unchanged; the entries are dropped at packaging, the same way the CUDA toolkit and torch directories already are.

Copilot AI lite review requested due to automatic review settings September 3, 2026 23:01
@shoumikhin shoumikhin added the release notes: build Changes related to build, including dependency upgrades, build flags, optimizations, etc. label Sep 3, 2026
@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. label Sep 3, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@pytorch-bot

pytorch-bot Bot commented Sep 3, 2026

Copy link
Copy Markdown

🔗 Helpful Links

🧪 See artifacts and rendered test results at hud.pytorch.org/pr/pytorch/executorch/22541

Note: Links to docs will display an error until the docs builds have been completed.

✅ You can merge normally! (1 Unrelated Failure)

As of commit ce0cd12 with merge base c65b23a (image):

BROKEN TRUNK - The following job failed but were present on the merge base:

👉 Rebase onto the `viable/strict` branch to avoid these failures

This comment was automatically generated by Dr. CI and updates every 15 minutes.

@kirklandsign

Copy link
Copy Markdown
Contributor

@claude review

@claude

claude Bot commented Sep 3, 2026

Copy link
Copy Markdown

Claude finished @kirklandsign's task in 3m 20s —— View job


Review: Stop shipping unusable MKL search paths in the Linux wheel

  • Read setup.py changes
  • Read .ci/scripts/wheel/test_shared_libraries.py changes
  • Read new .ci/scripts/tests/test_runtime_path_filter.py
  • Provide review feedback

This is a clean, well-scoped fix. The diagnosis matches the code, the match is narrow enough to keep a real MKL install, and both halves of the fix are enforced. I traced the two code paths and the tests and have no blocking concerns — a few small notes below.


What works well

  • The predicate is correctly anchored. _is_unresolved_math_library_directory (setup.py:606) requires exactly ('/', 'lib', <arch>), so /opt/intel/mkl/lib/intel64 and $ORIGIN/lib/intel64 both fall through. The entry.rstrip("/") normalization matches how the rest of the file (_is_cuda_toolkit_directory, and the /torch/lib suffix check at setup.py:1499) treats a trailing separator, so the two spellings are classified alike.

  • Ordering matters and is handled. The new drop sits alongside the CUDA-toolkit and torch-lib drops in _is_usable_runtime_path, ahead of the relative hops appended in _strip_absolute_runtime_paths (setup.py:1608). Since the loader searches in order, dropping these before the hops is what removes the shadowing — same rationale the CUDA prefix already uses.

  • Removing the exemption is the right call, and it's self-enforcing. With allowed_absolute narrowed to ("/torch/lib",), a CPU wheel built without patchelf (the best-effort path at setup.py:1551) keeps the MKL entries and now fails test_no_absolute_runtime_paths rather than shipping clean. That's the intended coupling: packaging drops them, the check refuses to hide them.

  • Testing the shipped code via AST is a genuinely good idea. Compiling the helpers out of setup.py without running setup() means the test exercises what ships, and the missing assertion in _setup_namespace guards against the extraction silently checking nothing if a name is renamed.

Minor notes (non-blocking)

  1. has_relative_torch_route is re-implemented in the test, not extracted. _filtered in the test (test_runtime_path_filter.py:100) restates the same any(... endswith("/torch/lib")) logic that lives in _strip_absolute_runtime_paths (setup.py:1592). The predicate under test (_is_usable_runtime_path) is faithfully imported, but this input computation is a copy, so it could drift if the production route-detection ever changes. Small risk given how stable that line is; worth a one-line comment noting it mirrors setup.py, or extracting that into a tiny helper you can also compile in.

  2. Double-slash edge is not covered (theoretical). If a prefix ever resolved to / you'd get //lib/intel64, whose POSIX PurePosixPath.parts[0] is '//', so the predicate returns False and the entry would survive. The observed artifacts are all exactly /lib/<arch>, so this is not a real case today — noting it only for completeness.

  3. Stale line reference in a comment. The comment in test_shared_libraries.py:2078 (and the mirror at setup.py) points at "setup.py:1300" for "what packaging decides to strip", but 1299 is the _strip_absolute_runtime_paths call; the actual filter is _is_usable_runtime_path at 1448. Trivial, but line-number references in comments tend to rot.

Verification

I could not run pytest .ci/scripts/tests/test_runtime_path_filter.py in this environment (sandbox denied the command), so I relied on reading the code paths. The PR reports 17 passing and — more importantly — states the tests were confirmed to fail when either half of the fix is reverted, which is the property that matters for a regression guard.


Overall: correct, narrowly targeted, and the test design (bidirectional coverage + exercising shipped code) is a notable improvement over the build-only check it supplements. LGTM with the minor notes above.

Copilot AI review requested due to automatic review settings September 3, 2026 23:46

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Copilot AI review requested due to automatic review settings September 4, 2026 00:17

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@shoumikhin

Copy link
Copy Markdown
Contributor Author

@claude review

@claude

claude Bot commented Sep 4, 2026

Copy link
Copy Markdown

Claude finished @shoumikhin's task in 1m 37s —— View job


Review: Stop shipping unusable MKL search paths in the Linux wheel

  • Read setup.py changes
  • Read .ci/scripts/wheel/test_shared_libraries.py changes
  • Read new .ci/scripts/tests/test_runtime_path_filter.py
  • Post review

I traced both code paths and the test harness. This is a clean, well-scoped fix, and the two minor notes from the earlier review round have since been addressed. LGTM.


What works well

  • The predicate is correctly anchored. _is_unresolved_math_library_directory (setup.py:606) requires exactly ('/', 'lib', <arch>) after rstrip("/"), so a real install (/opt/intel/mkl/lib/intel64), a system dir (/usr/lib/intel64), a relative hop ($ORIGIN/lib/intel64), and a deeper path (/lib/intel64/extra) all fall through. An empty prefix is the whole signature, and only that form is dropped.

  • Ordering is the actual bug, and it's handled. The new drop sits in _is_usable_runtime_path (setup.py:1497) ahead of the relative hops appended in _strip_absolute_runtime_paths (setup.py:1608). Since the loader searches in order, dropping these before the hops is what removes the shadowing — the same rationale the CUDA-toolkit and torch-lib drops already use. The rstrip("/") normalization matches how the rest of the file classifies a trailing separator.

  • The two halves are now made to agree, and it's self-enforcing. Narrowing allowed_absolute and adding the explicit unresolved_math_directories rejection (test_shared_libraries.py:2098) means a CPU wheel built without patchelf (the best-effort path at setup.py:1551) keeps the MKL entries and now fails test_no_absolute_runtime_paths rather than shipping clean. Packaging drops them; the check refuses to hide them.

  • Testing the shipped code via AST is the right design. Compiling the helpers out of setup.py (test_runtime_path_filter.py:52) without running setup() exercises what ships, and the missing assertion guards against a silent no-op if a name is renamed. The bidirectional coverage (drop the unresolved entries / keep a real install + keep the sole torch route + keep every relative hop) is what makes this a real regression guard rather than a filter that satisfies one direction and breaks the other.

Addressed since the last review

  • has_relative_torch_route is now extracted, not restated. _relative_torch_route_predicate (test_runtime_path_filter.py:143) lifts the expression out of setup.py by AST and rebinds its loop variable, with an assertion that fails loudly if the expression stops iterating found. This closes the drift risk the previous round flagged — the third argument to the filter is now derived from shipped code rather than a copy.
  • The stale setup.py:1300 line reference is gone from the test_shared_libraries.py comment; it now reads "the same way packaging decides what to strip" without a rotting line number.

Minor notes (non-blocking)

  1. Stale line reference in a pre-existing comment. _names_a_build_directory at test_shared_libraries.py:1993 still points at "setup.py:1288" for the build-directory classifier, but that classifier is the part in ("pip-out", "cmake-out") … block at setup.py:1486. This line is outside this PR's diff, so it's not a regression here — just noting it since the PR touched the neighborhood and line-number references tend to rot.

  2. Double-slash edge remains theoretical and uncovered. If a prefix ever resolved to / you'd get //lib/intel64, whose PurePosixPath.parts[0] is '//', so the predicate returns False and the entry survives. Every observed artifact is exactly /lib/<arch>, so this isn't a real case today — mentioned only for completeness.

Verification

I could not run pytest .ci/scripts/tests/test_runtime_path_filter.py here (sandbox denied it), so I relied on reading the code paths and tracing the predicate by hand across the parametrized cases. The PR reports 19 passing and — the property that matters — that the tests were confirmed to fail when either half of the fix is reverted or when the accepted set is narrowed to refuse a real install.

Overall: correct, narrowly targeted, and the earlier feedback is folded in. LGTM.
fix-mkl-runpath-leak

Copilot AI review requested due to automatic review settings September 4, 2026 02:19

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Copilot AI review requested due to automatic review settings September 4, 2026 04:54

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Five shared libraries in the Linux x86_64 wheel search three directories that
exist on nobody's machine:

    /lib/intel64
    /lib/intel64_win
    /lib/win-x64

Two of them name a Windows layout, in a Linux wheel.

They come from PyTorch. Its exported CMake package creates a caffe2::mkl imported
target, and linking torch brings it in even though this project never asks for
MKL. That target carries a hardcoded list of four link directories below
MKL_ROOT, which resolves to nothing here, so what the linker records is left
anchored at the filesystem root. The bare MKL_ROOT/lib does not survive, because
CMake filters its own implicit link directories out of the link line, which is
why three entries appear rather than four. Packaging copies the built libraries out of the build
tree rather than installing them, so whatever the linker recorded ships as is.

Nothing needs them. No shipped library names an MKL or OpenMP runtime among its
dependencies, so nothing resolves through those directories. They are not merely
untidy either: they sit ahead of the relative entries packaging appends, and the
loader searches in order, so a user who happens to have a matching directory
resolves a library from there instead of from the one the wheel installed. That
is the same shadowing the release check already rejects a CUDA toolkit prefix
for.

The fix drops them where the other unusable entries are already dropped, and
matches only the exact /lib/<arch> form that an empty prefix produces. A real
MKL installation spells the same arch directory below a prefix, as
/opt/intel/mkl/lib/intel64, and that one is a directory the environment
genuinely provides, so it is kept.

The release check that rejects absolute search paths listed these three as
allowed, which is why they shipped while a check whose whole purpose is
rejecting absolute paths reported the wheel clean. It now rejects the empty
prefix form specifically, while still accepting a real installation's prefixed
directory, so it agrees with what packaging does rather than contradicting it.

Test plan:

New unit tests in .ci/scripts/tests/test_runtime_path_filter.py, which read the
functions out of setup.py so they exercise what ships. They run on every pull
request through the existing unit test job, and need no wheel build, which is
what the previous check could not manage.

  pytest .ci/scripts/tests/test_runtime_path_filter.py
  23 passed

They cover both directions, since a filter that satisfies either alone is wrong.
The three unresolved entries are dropped, a real MKL installation and ordinary
system directories are kept, every relative entry on a shipped library survives,
and the absolute torch directory is kept when it is a library's only route to
torch, which is what stops this becoming a blanket rule that breaks importing.

Confirmed by mutation that each part of the fix is load bearing. Nine mutations,
each failing at least one test: removing the packaging filter, removing any one
of the release check's three rejecting branches, severing the call that applies
the check to a shipped library, reordering its guards so a build directory is
accepted, widening the packaging predicate, narrowing what the check accepts so a
real installation is refused, making the packaging filter reject an ordinary
system directory, and adding an arch to the shared constant without teaching the
check about it.

The release check's per-entry decision is a module level function, so the unit
test calls the same code the wheel check runs and compares the reason it returns.
Asserting only that a path was rejected was not enough: the check rejects every
absolute path it does not recognise, so an unknown arch passed for the wrong
reason and two of its three branches could each be deleted alone.

Also measured the published artifacts directly, reading the recorded search
paths out of every shared library in each wheel:

  1.4.1 release, CPython 3.12   6 libraries with a search path, 0 unusable
  current nightly, CPU 3.12    18 libraries with a search path, 15 unusable
  current nightly, CUDA 3.13   21 libraries with a search path, 15 unusable

The 1.4.1 release predates the code changed here. Both nightly rows show the
same 15 entries across the same five libraries, which is what this removes.
Reading the same libraries' declared dependencies shows no MKL or OpenMP runtime
in any of them.

Not verified: no wheel was built for this, since the change is to the filter
those builds already run and the tests exercise it directly. The link line that
emits the paths is unchanged; they are dropped at packaging, the same way the
CUDA toolkit and torch directories already are.
@shoumikhin
shoumikhin merged commit 5410b1a into main Sep 4, 2026
369 of 370 checks passed
@shoumikhin
shoumikhin deleted the fix-mkl-runpath-leak branch September 4, 2026 06:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. release notes: build Changes related to build, including dependency upgrades, build flags, optimizations, etc.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Linux wheel ships build-machine absolute paths in _portable_lib RUNPATH

5 participants