Skip to content

Ship the thread pool as its own library so a process has one pool - #21522

Open
shoumikhin wants to merge 4 commits into
gh/shoumikhin/78/headfrom
gh/shoumikhin/79/head
Open

Ship the thread pool as its own library so a process has one pool#21522
shoumikhin wants to merge 4 commits into
gh/shoumikhin/78/headfrom
gh/shoumikhin/79/head

Conversation

@shoumikhin

Copy link
Copy Markdown
Contributor

The thread pool that spreads CPU work across cores is meant to be a
process-wide singleton, but it is held in a function-local static inside a
static library, so every component that links it gets a private copy. The
wheel ships two of them today: the pybindings extension and the training
extension each define get_threadpool. Loading both into one interpreter
gives the process two pools, and because each sizes itself to the machine's
core count independently, they oversubscribe the CPU and contend with each
other.

Build the thread pool as a shared library for the wheel and let both
extensions resolve it, so there is one pool per process again. cpuinfo and
pthreadpool are forced static, so they are bundled inside this library
rather than left for each consumer to supply, and the runtime is resolved
from the shared runtime instead of embedding a second copy of the core.

This is gated on the existing EXECUTORCH_BUILD_SHARED option, so only the
Linux wheel changes; iOS, Android, and embedded builds keep linking the
static library exactly as before.

Test plan:

The wheel smoke test now asserts that exactly one shipped library defines
the thread pool accessor, alongside the existing single-backend-registry
assertion, so a future change that reintroduces a second pool fails in CI
rather than silently degrading performance. The new assertion was confirmed
to fail against a wheel built before this change, where it correctly reports
the two definers by name.

Built the wheel from a clean checkout and verified against a fresh virtual
environment with a normal dependency-resolving install:

  • nm -DC across every shipped shared object shows exactly one definition of
    get_threadpool, in the new library; the extensions import it rather than
    defining their own.
  • readelf -d shows the pybindings extension records the versioned thread
    pool library in DT_NEEDED, with a relocatable RUNPATH so it resolves the
    runtime as a sibling without LD_LIBRARY_PATH.
  • The single-backend-registry assertion still holds with the new library
    loaded.
  • import executorch, the registered backend list, and .pte execution
    through the Python bindings are unchanged, with outputs matching eager
    PyTorch.
  • With EXECUTORCH_BUILD_SHARED off, the thread pool is still a static
    library and no new shared object is produced, so every build that does not
    opt in is unaffected.

[ghstack-poisoned]
@pytorch-bot

pytorch-bot Bot commented Jul 31, 2026

Copy link
Copy Markdown

🔗 Helpful Links

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

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

❌ 5 Cancelled Jobs, 7 Pending, 1 Unrelated Failure, 3 Unclassified Failures

As of commit e058bf3 with merge base d632341 (image):

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:

CANCELLED JOBS - The following jobs were cancelled. Please retry:

FLAKY - The following job failed but was likely due to flakiness present on trunk:

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 Jul 31, 2026
[ghstack-poisoned]
shoumikhin added a commit that referenced this pull request Jul 31, 2026
The thread pool that spreads CPU work across cores is meant to be a
process-wide singleton, but it is held in a function-local static inside a
static library, so every component that links it gets a private copy. The
wheel ships two of them today: the pybindings extension and the training
extension each define `get_threadpool`. Loading both into one interpreter
gives the process two pools, and because each sizes itself to the machine's
core count independently, they oversubscribe the CPU and contend with each
other.

Build the thread pool as a shared library for the wheel and let both
extensions resolve it, so there is one pool per process again. `cpuinfo` and
`pthreadpool` are forced static, so they are bundled inside this library
rather than left for each consumer to supply, and the runtime is resolved
from the shared runtime instead of embedding a second copy of the core.

This is gated on the existing `EXECUTORCH_BUILD_SHARED` option, so only the
Linux wheel changes; iOS, Android, and embedded builds keep linking the
static library exactly as before.

Test plan:

The wheel smoke test now asserts that exactly one shipped library defines
the thread pool accessor, alongside the existing single-backend-registry
assertion, so a future change that reintroduces a second pool fails in CI
rather than silently degrading performance. The new assertion was confirmed
to fail against a wheel built before this change, where it correctly reports
the two definers by name.

Built the wheel from a clean checkout and verified against a fresh virtual
environment with a normal dependency-resolving install:

- `nm -DC` across every shipped shared object shows exactly one definition of
  `get_threadpool`, in the new library; the extensions import it rather than
  defining their own.
- `readelf -d` shows the pybindings extension records the versioned thread
  pool library in `DT_NEEDED`, with a relocatable RUNPATH so it resolves the
  runtime as a sibling without `LD_LIBRARY_PATH`.
- The single-backend-registry assertion still holds with the new library
  loaded.
- `import executorch`, the registered backend list, and `.pte` execution
  through the Python bindings are unchanged, with outputs matching eager
  PyTorch.
- With `EXECUTORCH_BUILD_SHARED` off, the thread pool is still a static
  library and no new shared object is produced, so every build that does not
  opt in is unaffected.


ghstack-source-id: dfdb2bf
ghstack-comment-id: 5146636071
Pull-Request: #21522
[ghstack-poisoned]
[ghstack-poisoned]
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.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant