Skip to content

Ship the quantized kernels as their own library - #21642

Open
shoumikhin wants to merge 25 commits into
gh/shoumikhin/92/headfrom
gh/shoumikhin/93/head
Open

Ship the quantized kernels as their own library#21642
shoumikhin wants to merge 25 commits into
gh/shoumikhin/92/headfrom
gh/shoumikhin/93/head

Conversation

@shoumikhin

@shoumikhin shoumikhin commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

A quantized model uses smaller numbers than a normal one, so the tensors take less memory.
Running one needs the quantized operator kernels.

The only copy the wheel shipped is the one torch loads to export a model, which a C++ application
cannot use. Such an application links the runtime, loads a quantized model, and the model fails at
run time with a missing operator, which looks like a model problem rather than a packaging one.

Build the quantized kernels as their own shared library and name it as a CMake component, the same
way the other kernel sets are named.

find_package(executorch REQUIRED COMPONENTS kernels_quantized)
target_link_libraries(my_app PRIVATE executorch::runtime
                                     executorch::kernels_quantized)

The wheel now ships lib/libexecutorch_kernels_quantized.so.

Note that the wheel also ships a second copy of these kernels, inside the library torch loads when
you export a model. That copy is built into the plugin rather than resolved from the shared library,
so a process holding both registers the same operators twice, and the runtime treats that as fatal:

Re-registering quantized_decomposed::add.out

This affects only a process that does both, for example an application that embeds a Python
interpreter. A plain C++ application can link the component freely.

Because of that, this is the one component EXECUTORCH_LIBRARIES does not include, so an
application that links whatever the package offers cannot end up in that position without asking.
A consumer that wants the quantized kernels names the component, or on CMake older than 3.28, where
no component targets exist, links EXECUTORCH_QUANTIZED_KERNELS_LIBRARY as well. That variable is
now populated on both CMake routes, so a consumer that adopts the older-CMake recipe and later
upgrades keeps the library on their link line instead of silently losing it.

Built the wheel, installed it into a clean environment, and:

  • exported a quantized model and ran it from Python, matching eager PyTorch to within the
    quantization step (measured worst difference 0.0048 against a tolerance of 0.02).
  • built a C++ application that links executorch::kernels_quantized, ran the same program, and got
    the same output as Python, byte for byte.
  • confirmed the Python extension does not depend on the run-time copy, and that a process holding
    the shipped library and the export plugin aborts in either load order.
  • checked every shipped library the same way, to establish that this is the only pair that
    collides: the CPU kernels, the delegate, the thread pool, the profiler and the runtime all
    coexist with both the extension and the export plugin.
  • an application linking only EXECUTORCH_LIBRARIES does not depend on the quantized library while
    still depending on the CPU kernels, on CMake 3.28 and on real CMake 3.24. A new check asserts
    this, and it fails on the previous behaviour.
  • EXECUTORCH_QUANTIZED_KERNELS_LIBRARY resolves to the shipped library on both the modern-CMake
    route (as the imported target) and the pre-3.28 route (as a file path).
  • a missing quantized library now fails the checks instead of skipping them. The preset that builds
    the wheel enables these kernels unconditionally, so their absence is a regression rather than a
    configuration to tolerate, and both the ownership table and the C++ check previously treated it as
    an acceptable state and reported coverage they had not run.

Ran on Linux x86_64 and aarch64.

@pytorch-bot

pytorch-bot Bot commented Aug 7, 2026

Copy link
Copy Markdown

🔗 Helpful Links

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

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

❌ 41 New Failures, 3 Unrelated Failures, 39 Unclassified Failures

As of commit 707e35f with merge base ed65b12 (image):

NEW FAILURES - The following jobs have failed:

UNCLASSIFIED FAILURES - DrCI could not classify the following jobs because the workflow did not run on the merge base. The failures may be pre-existing on trunk or introduced by this PR:

FLAKY - The following jobs failed but were likely due to flakiness present on trunk:

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 Aug 7, 2026
@shoumikhin shoumikhin added ciflow/periodic ciflow/trunk ciflow/binaries ciflow/binaries/all Release PRs with this label will build wheels for all python versions ciflow/nightly ciflow/cuda labels Aug 7, 2026
[ghstack-poisoned]
[ghstack-poisoned]
[ghstack-poisoned]
[ghstack-poisoned]
[ghstack-poisoned]
[ghstack-poisoned]
[ghstack-poisoned]
[ghstack-poisoned]
[ghstack-poisoned]
[ghstack-poisoned]
[ghstack-poisoned]
Comment thread docs/source/using-executorch-cpp.md Outdated
Comment on lines +125 to +126
`executorch.kernels.quantized` loads a plugin that carries its own copy of the same
kernels, and the runtime stops when the same operator is registered twice:

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.

can we make this as clean as other .so files i.e. registered only once, and not part of the cpython extension lib?

Comment on lines +339 to +345
library torch loads at export time, because each side registers into a table the
other never reads. Loading both into one process does abort on the second
registration, so what this check enforces for that component is one owner among the
runtime libraries, not the absence of the export copy. Excusing every component
would disarm the check where duplication is a real fault: two of these libraries
defined the backend registry symbols in one released wheel and not in the release
before it, so the duplication this catches does happen.

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.

yeah .. this.

with torch.no_grad():
expected = model(*example)

if mode == "quantized":

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.

nice!

@digantdesai digantdesai 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.

ok with duplicate symbols but we should see if we can do something about it.

[ghstack-poisoned]
@shoumikhin

Copy link
Copy Markdown
Contributor Author

can we make this as clean as other .so files i.e. registered only once, and not part of the
cpython extension lib?

Good question, and I dug into it rather than guessing. The short answer is that the duplicate is
real, but it is not in the cpython extension, and removing it is a bigger change than this PR.

Measured on the installed wheel, there are two separate libraries that both register the same
21 quantized operators:

kernels/quantized/libquantized_ops_aot_lib.so   21 quantized_decomposed:: entries
lib/libexecutorch_kernels_quantized.so          21 quantized_decomposed:: entries

They are not two copies of the same thing by accident. They serve different sides:

  • libquantized_ops_aot_lib.so links torch and registers into the PyTorch dispatcher. It exists
    so quantized operators can run at export time, which is what executorch.kernels.quantized
    loads.
  • libexecutorch_kernels_quantized.so is the runtime component this PR ships, registering into the
    ExecuTorch operator table for a C++ application.

So the kernels are already out of the cpython extension, which is the part your comment asks about.
What remains is that a single process which both imports the export-time plugin and links the
runtime component ends up registering the same operator names twice, and the runtime stops on the
second registration.

Making that "registered only once" means having the export-time plugin reuse the runtime component
instead of carrying its own kernels. That is a real cleanup and I think the right end state, but it
changes the AOT side, which no part of this stack touches, and it would need its own testing on the
export path. I would rather do it as a separate change than widen this one.

What I have done here is make the conflict impossible to hit by accident: the component is held out
of ${EXECUTORCH_LIBRARIES} so you never get it implicitly, and the docs say plainly that you name
it only when you want quantized operators without loading the Python plugin in the same process.

[ghstack-poisoned]
[ghstack-poisoned]
[ghstack-poisoned]
[ghstack-poisoned]
[ghstack-poisoned]
[ghstack-poisoned]
[ghstack-poisoned]
[ghstack-poisoned]
[ghstack-poisoned]
[ghstack-poisoned]
[ghstack-poisoned]
[ghstack-poisoned]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ciflow/binaries/all Release PRs with this label will build wheels for all python versions ciflow/binaries ciflow/cuda ciflow/nightly ciflow/periodic ciflow/trunk CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants