Skip to content

Stop importing the SDK downloader to load the Qualcomm package - #22394

Closed
shoumikhin wants to merge 3 commits into
pytorch:mainfrom
shoumikhin:qnn-lazy-scripts-import
Closed

Stop importing the SDK downloader to load the Qualcomm package#22394
shoumikhin wants to merge 3 commits into
pytorch:mainfrom
shoumikhin:qnn-lazy-scripts-import

Conversation

@shoumikhin

@shoumikhin shoumikhin commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Summary

Deferring the Qualcomm SDK setup left six modules importing the package itself:

from executorch.backends.qualcomm import setup_qnn_sdk

That loads the package __init__, which imported the SDK downloader from the sibling scripts
directory at module level. That directory is not part of the installed package in every build of this
backend, and where it is missing all six modules fail to import:

ModuleNotFoundError: No module named 'executorch.backends.qualcomm.scripts'

Anything importing one of those six then fails too, which surfaces as a test module that cannot be
collected rather than as a test failure.

Before the setup was deferred, nothing inside the package imported the package __init__, so the
missing directory never mattered. That is why this was not caught earlier: an installed source tree or
wheel has scripts on disk, so the import resolves there.

The change

Nothing about loading the package needs the downloader. It is used by one branch of setup, the one
that fetches a prebuilt SDK, and that branch cannot be reached when QNN_SDK_ROOT is already set or
when no SDK is published for the platform. So it is imported inside that branch instead.

Three details worth stating, because each one was a wrong turn first:

  • is_linux_x86 is defined here now rather than resolved lazily. Setup asks it before the branch
    that needs a downloader, so leaving it lazy pulled the downloader back in for every caller. The
    answer is a short platform test.
  • install_qnn_sdk and QNN_ZIP_URL resolve through a module level __getattr__, so they stay
    ordinary module attributes. A function local import is simpler but makes them unpatchable, which
    silently broke four existing tests when I tried it.
  • When the downloader is genuinely absent, setup raises a RuntimeError naming QNN_SDK_ROOT rather
    than letting a missing module escape, since a packaging detail is not something the caller can act
    on. That rewrap is limited to the downloader module by name: the downloader imports requests, and
    a missing dependency inside it has to keep reporting itself instead of being blamed on packaging.

Test plan

case result
package import, scripts present ok
package import, scripts absent ok
QNN_SDK_ROOT set, scripts absent early return, downloader never imported
no QNN_SDK_ROOT, scripts absent, Linux x86 RuntimeError naming QNN_SDK_ROOT
scripts present, requests absent ModuleNotFoundError naming requests

The third row is the case that broke. The new test covers it by removing the module from
sys.modules, reloading, then forcing that path and calling setup_qnn_sdk(). It fails on the
previous revision with the ModuleNotFoundError above.

The last two rows were each verified by hiding the relevant piece and reading the error the caller
gets. The full test file passes with scripts present and with it absent.

Not covered

No Qualcomm device, so compiling a model for QNN end to end is not exercised here. What is tested is
which modules can be imported and what setup does in each of the states above.

Deferring the SDK setup meant six modules now do this at import:

    from executorch.backends.qualcomm import setup_qnn_sdk

which loads the package `__init__`, which imported the downloader from the sibling `scripts`
directory. That directory is not part of the installed package everywhere the backend is built, so
on those builds every one of those six modules fails to import:

    ModuleNotFoundError: No module named 'executorch.backends.qualcomm.scripts'

Before this backend deferred its setup nothing imported the package `__init__` from inside the
package, so the missing directory never mattered. That is why it went unnoticed.

Nothing about loading the package needs the downloader. It is used in one branch of setup, the one
that fetches a prebuilt SDK, and that branch is unreachable when `QNN_SDK_ROOT` is already set or
the platform has no published SDK. So it is now imported inside that branch.

`is_linux_x86` moved here rather than being resolved lazily, because setup asks it before the
branch that needs a downloader, and the answer is a two line platform test. `install_qnn_sdk` and
`QNN_ZIP_URL` resolve through a module level `__getattr__`, so they are still ordinary module
attributes that callers and tests can replace.

Test plan:

    package import, scripts present   ok
    package import, scripts absent    ok, and setup_qnn_sdk() completes
    QNN_SDK_ROOT set, scripts absent  early return, downloader never imported

The new test covers the third case, which is the one that broke. It fails on the previous revision
with the ModuleNotFoundError above.
Copilot AI lite review requested due to automatic review settings September 1, 2026 04:30
@pytorch-bot

pytorch-bot Bot commented Sep 1, 2026

Copy link
Copy Markdown

🔗 Helpful Links

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

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

❌ 3 New Failures, 1 Unrelated Failure

As of commit feb8b0b with merge base 90452c6 (image):

NEW FAILURES - The following jobs have failed:

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.

@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 1, 2026
@shoumikhin shoumikhin added the release notes: qualcomm Changes to the Qualcomm backend delegate label Sep 1, 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.

Pull request overview

This PR removes an import-time dependency on the Qualcomm backend’s sibling scripts/ directory by deferring the SDK downloader import until it’s actually needed, preventing ModuleNotFoundError in builds that don’t package backends/qualcomm/scripts.

Changes:

  • Replace module-level imports of the downloader with a lazy __getattr__ for install_qnn_sdk / QNN_ZIP_URL.
  • Define is_linux_x86() in backends/qualcomm/__init__.py so platform gating can run without the downloader.
  • Add a regression test intended to ensure importing the package works even when the downloader module is unavailable.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

File Description
backends/qualcomm/init.py Makes downloader symbols lazily resolved to avoid import-time failures when scripts/ isn’t packaged.
backends/qualcomm/tests/test_import_side_effects.py Adds a test aimed at validating import behavior when the downloader modules are forced-missing.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread backends/qualcomm/tests/test_import_side_effects.py
Comment thread backends/qualcomm/__init__.py Outdated
Two review points, both right.

The new test only checked that attributes existed after reloading with the downloader missing. It
never called `setup_qnn_sdk()`, so it did not cover the case the change is about: a Linux x86 host
with a preinstalled SDK, where setup takes its early return. It now forces that path and calls
setup, which is what would raise if the downloader were imported again.

And on a build with no downloader, where `QNN_SDK_ROOT` happens to be unset on Linux x86, the lazy
attribute leaked a `ModuleNotFoundError` about a missing sibling directory. That tells the caller
nothing they can act on. It is now a `RuntimeError` naming the variable to set, chained to the
original so the cause is still visible.
Copilot AI review requested due to automatic review settings September 1, 2026 04:45
@shoumikhin

Copy link
Copy Markdown
Contributor Author

Both of these were right, thanks. Fixed and pushed.

The test did not exercise the case. You are correct that it only checked attributes existed after
the reload. It now forces the path that actually broke, a Linux x86 host with QNN_SDK_ROOT set,
and calls setup_qnn_sdk(), which is what would raise if the downloader were imported again. I
confirmed it still fails on the previous revision with the same ModuleNotFoundError.

The leaked import error. Also right, and I reproduced it: on a build with no downloader where
QNN_SDK_ROOT is unset on Linux x86, the caller got
No module named 'executorch.backends.qualcomm.scripts', which tells them nothing to act on. It is
now a RuntimeError naming the variable to set, chained to the original so the cause is still
visible:

This build cannot download a QNN SDK. Set QNN_SDK_ROOT to an existing installation:
       export QNN_SDK_ROOT=/path/to/qualcomm/sdk

On the three red unittest rows here: those are an infrastructure failure, not this change. The jobs
die during dependency install with Credentials could not be loaded and
sccache: error: Server startup failed, which breaks the pytorch_tokenizers wheel build. ExecuTorch
itself is never installed, so nothing in this change runs. The same three rows fail the same way on an
unrelated pull request of mine that touches only a C++ file and two documentation pages.

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.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

Comment thread backends/qualcomm/__init__.py
The catch was too wide. The downloader imports `requests`, so on a build where that is not
installed the missing dependency was reported as "This build cannot download a QNN SDK", pointing
the reader at packaging instead of at the real cause. Reproduced by hiding `requests`.

Narrowed to the downloader module itself, by name. A `ModuleNotFoundError` from anything else is
re-raised untouched:

    requests missing      ModuleNotFoundError naming requests
    downloader missing    RuntimeError naming QNN_SDK_ROOT
Copilot AI review requested due to automatic review settings September 1, 2026 05:03
@shoumikhin

Copy link
Copy Markdown
Contributor Author

Right again, and I reproduced it before fixing. With the downloader present but requests not
installed, the missing dependency came back as "This build cannot download a QNN SDK", which sends the
reader at packaging instead of the real cause.

The catch now only rewraps when the absent module is the downloader itself, checked by name. Anything
else is re-raised untouched:

requests missing      ModuleNotFoundError naming requests
downloader missing    RuntimeError naming QNN_SDK_ROOT

Both verified by hiding each one in turn.

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.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

Comment on lines +94 to +95
if not (error.name or "").startswith(f"{__name__}.scripts"):
raise
@shoumikhin

Copy link
Copy Markdown
Contributor Author

Closing this in favour of #22392, which fixes the same problem and one more.

This change keeps setup_qnn_sdk and disable_mkldnn_on_amd defined in the package's
__init__.py. That file is the thing that goes missing: a build which assembles a package
from a file list can leave it out and put an empty file there instead, so everything defined
in it disappears and callers still fail:

ImportError: cannot import name 'setup_qnn_sdk' from 'executorch.backends.qualcomm'

I checked both changes against an installed wheel in four packaging shapes, with
__init__.py emptied and with the scripts directory removed. This one is clean in two of
the four. #22392 is clean in all four, because it takes the helpers out of __init__.py
altogether.

Two ideas from here are worth keeping, and both are already in #22392: answering the
platform question locally with platform.system() and platform.machine(), so a machine
that could never download does not have to import the downloader to find that out, and
turning an absent downloader into an error that names QNN_SDK_ROOT instead of reporting a
missing module.

Nothing outside the package imports install_qnn_sdk, QNN_ZIP_URL or is_linux_x86 from
the package root, so making them private in #22392 breaks no callers.

@shoumikhin shoumikhin closed this Sep 1, 2026
@shoumikhin

Copy link
Copy Markdown
Contributor Author

Closing this as redundant. I branched it before #22392 merged and did not check, so it re-solves a
problem that is already fixed on main, and by a worse route.

#22392 removes the seven module level setup_qnn_sdk() calls outright. This one kept them and made the
package import survive a missing downloader instead. Removing the calls is the better fix: it also
handles the case where a build ships an empty __init__.py, where nothing defined in that file exists
at all, which no amount of lazy importing inside it can help with.

Everything worth keeping here is already on main:

  • the downloader imported inside the install branch rather than at module level
  • the platform test answered without it
  • a RuntimeError naming QNN_SDK_ROOT when the downloader is genuinely absent, narrowed by module
    name so a missing dependency inside the downloader still reports itself

I verified main handles the case this was opened for: with the scripts directory removed, the package
imports and setup_qnn_sdk() completes.

One thing from #22392 is still open and is not covered by either change: disable_mkldnn_on_amd() now
has no caller. It exists to avoid a crash on some AMD hosts, so it needs a call site on the compile
path. Worth its own change rather than reviving this one.

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: qualcomm Changes to the Qualcomm backend delegate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants