Skip to content

Stop reading a backend config field that no longer exists - #22298

Open
shoumikhin wants to merge 5 commits into
pytorch:mainfrom
shoumikhin:llama-stale-mps
Open

Stop reading a backend config field that no longer exists#22298
shoumikhin wants to merge 5 commits into
pytorch:mainfrom
shoumikhin:llama-stale-mps

Conversation

@shoumikhin

@shoumikhin shoumikhin commented Aug 29, 2026

Copy link
Copy Markdown
Contributor

Summary

Exporting llama for Core ML fails before it starts:

AttributeError: 'BackendConfig' object has no attribute 'mps'

BackendConfig lost its mps field when the MPS backend was removed, but the Core ML branch in
export_llama_lib still reads llm_config.backend.mps.enabled:

elif llm_config.backend.coreml.enabled and not (
    llm_config.backend.vulkan.enabled
    or llm_config.backend.mps.enabled   # <- field no longer exists
    or llm_config.backend.qnn.enabled
):

Scope, stated precisely: or short-circuits, so this raises only when Vulkan is off. Core ML alone
and Core ML with QNN raise. Core ML with Vulkan does not, because the first term is already true.

This is what fails test-llama-runner-mac (fp32, coreml), which is red on main.

How it got in

The MPS removal cleaned this file completely: four backend.mps reads before, zero after. The read
came back in the change that moved Core ML to to_edge_transform_and_lower and added the dedicated
Core ML lowering helper. That change was written against an older main and merged after the
removal, so this is stale-branch merge skew rather than a missed spot.

That also means the dedicated Core ML lowering has never had a green run, because the helper and
the broken guard arrived together. This change is the first time it can execute. It lowers through
to_edge_transform_and_lower where the previous combined path used export_to_edge plus
to_backend, which keeps whole the ops Core ML implements itself, so the delegated graph changes
too. Expect the first real run of that job to find more than this.

What changes behaviour

The removed term never evaluated to False. It raised, which is the bug. So one outcome does
change: Core ML with QNN used to raise and now runs the combined lowering, which still lowers Core
ML while keeping the QNN partitioner.

Test plan

Two tests, one per repaired route:

revision Core ML alone Core ML with QNN
base raises AttributeError raises AttributeError
head reaches the Core ML lowering reaches the combined lowering

The QNN case matters on its own: without it, deleting the whole exclusion clause leaves the suite
green while silently dropping the Vulkan and QNN partitioners. I checked that mutation, and the QNN
test is what catches it.

The full macOS Core ML jobs ran on this branch through a ciflow/trunk tag and both passed,
including test-llama-runner-mac (fp32, coreml), which fails at the merge base. Worth knowing that
this job does not start from the pull request path filter alone; the tag route is what runs it.

Follow-ups, not fixed here

  • A Core ML export requesting an etrecord gets no file and no warning. Being fixed separately.
  • .lintrunner.toml has examples/**/*.py commented out of the mypy scope. The pinned mypy flags
    exactly this bug, so putting the directory in scope would have caught it in seconds, but that file
    alone reports hundreds of pre-existing errors, so it is a much larger change.

Copilot AI lite review requested due to automatic review settings August 29, 2026 05:13
@pytorch-bot

pytorch-bot Bot commented Aug 29, 2026

Copy link
Copy Markdown

🔗 Helpful Links

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

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

⏳ 7 Pending, 1 Unrelated Failure

As of commit 8cb4cf5 with merge base 9b558d9 (image):

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 Aug 29, 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.

@shoumikhin shoumikhin added the release notes: examples Changes to any of our example LLMs integrations, such as Llama3 and Llava label Aug 29, 2026
Copilot AI review requested due to automatic review settings August 29, 2026 11: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.

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 August 31, 2026 04:04

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

Good catch on the coverage gap. Fixed and pushed.

The tests pinned only the QNN half of the clause. I confirmed your mutation: dropping just the Vulkan
term left both green, and that sends a Core ML plus Vulkan export to the Core ML lowering, which
takes no Vulkan argument, so the partitioner is dropped in silence. A third case covers it. The
matrix is complete now:

drop vulkan term      the Vulkan case fails
drop qnn term         the QNN case fails
whole clause deleted  both combined cases fail
stale field restored  both Core ML cases fail

You were also right that the helper hid the Core ML flag. Deleting that line left the test named for
Core ML with QNN passing, so it never checked Core ML was on. Backends are named at each call site
now. That removed the getattr lookup as well, which was the same unchecked kind of lookup this
change deletes, and the f-string patch target is now patch.object.

The bare RuntimeError marker is gone too. Your kv cache experiment is the convincing part: a real
RuntimeError from inside the export was swallowed and the test reported only that the lowering was
never called. A private marker class shows the real cause.

Both combined cases now assert the backend flags reach the shared lowering, since a call count cannot
see them and preserving those partitioners is the whole point of the clause.

On the commit messages: agreed and they are rewritten. The transformers macOS job claim was wrong,
and the claim that the earlier note about the trunk path filter was wrong is itself wrong, since the
filter really does not cover this directory and a ciflow tag is a separate push route. The
description already had the correct version of both, and since this repository squashes with the
description, that is the text that lands.

Two I am leaving: the per-test export cost, and the note that the Core ML enable_state and
preserve_sdpa options are read by no exporter. The second is a real product gap this change makes
reachable for the first time, but it is not this change's to fix.

Exporting llama for Core ML fails before it starts:

    AttributeError: 'BackendConfig' object has no attribute 'mps'

`BackendConfig` lost its `mps` field when the MPS backend was removed, but the Core ML branch still
reads `llm_config.backend.mps.enabled` while deciding whether Core ML can be lowered on its own. The
read happens whether or not MPS was requested, which is why the whole path is dead rather than just
the MPS part of it.

This is what fails `test-llama-runner-mac (fp32, coreml)`.

Test plan:

The removed term never evaluated to False. Reading a field that is not there raises, so two routes
change from crashing to running: Core ML alone, and Core ML with QNN. Core ML with Vulkan is
unchanged, because the first term short-circuits before the bad read.

Enumerated all eight combinations of the three remaining flags with the field actually raising:
exactly those two differ, and no combination that worked before changes.

Against an installed wheel, the old expression raises the AttributeError above and the new one
returns True for a Core ML export, which is the branch it should take.
The fix had no test, so nothing stopped the field read returning.

Patching the lowering to raise a marker keeps this off macOS and away from coremltools: what is
asserted is that control reaches the lowering at all, which is exactly what the stale read prevented.

Test plan:

    with the fix     reaches the Core ML lowering
    without it       raises AttributeError before any lowering
The first test pinned only one of the two routes the fix repaired, and it stayed green when the whole
exclusion clause was deleted, which silently drops the Vulkan and QNN partitioners. So it guarded the
removed field read and not the routing the clause exists for.

Core ML with QNN is now covered too. It must fall through to the combined lowering, which still
lowers Core ML but keeps the QNN partitioner. That case raised before the fix and it fails on the
mutant, so between them the two tests pin both halves.

Both now use a patched lowering with an assertion on the call, which is how the rest of the tree
writes this, instead of a local marker exception and a stub.

Also removed a paragraph from the test docstring about when the macOS Core ML job runs. A comment
inside a test cannot be checked from the test and goes stale on its own, and the workflow's own path
filter does not cover this directory, so the sentence was misleading either way.

Test plan:

    both tests, fixed         pass
    stale field restored      the Core ML case fails with the AttributeError
    exclusion clause deleted  the QNN case fails
Review found the tests pinned only the QNN half. Dropping just the Vulkan term left both green, and
that mutation sends a Core ML plus Vulkan export to the Core ML lowering, which takes no Vulkan
argument, so the partitioner is dropped in silence. A third case covers it, and the mutation matrix
is now complete:

    drop vulkan term      the Vulkan case fails
    drop qnn term         the QNN case fails
    whole clause deleted  both combined cases fail
    stale field restored  both Core ML cases fail

The helper also turned Core ML on inside itself, so neither call site showed which backends its case
used, and deleting that hidden line left the test named for Core ML with QNN passing. Backends are
now named at each call.

Three smaller corrections that came with it. The helper resolved the backend field from a string
through getattr, which is the same unchecked lookup this change removes and is against the house
rule on dynamic attribute access. The patch target was built from an f-string, the only one in the
tree, now patch.object. And a bare RuntimeError was the marker, which swallowed real RuntimeErrors
from inside the export and reported only that the lowering was never called; a private marker class
shows the real cause instead.

Both combined cases now also assert the backend flags reach the shared lowering, since a call count
alone cannot see them and the exclusion clause exists to preserve exactly those partitioners.
Each routing case traced and lowered the whole model to check which branch it takes. Stubbing the
model preparation, which the routing decision does not depend on, takes peak memory from about
4.3 GB to 420 MB and the three cases from 7.1 to 3.5 seconds of processor time.

The mutation matrix is unchanged, so the cases still pin what they did before:

    drop vulkan term      the Vulkan case fails
    drop qnn term         the QNN case fails
    whole clause deleted  both combined cases fail
    stale field restored  both Core ML cases fail

Also pinned the KV cache off. With it on, the source transforms import the Qualcomm SDK, which
routing does not need and which is absent on most machines, so the Core ML with QNN case only avoided
it through a default nothing stated.
Copilot AI review requested due to automatic review settings August 31, 2026 15:30

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

Second pass, all four addressed.

The commit messages are actually rewritten now. I said last time they were, and they were not, so
thank you for the specifics. I verified your finding first: at the merge base all six
coreml_fp32_gpu cells of the transformers macOS job passed, including the model my message named, so
the message pointed at a job that was green. That claim, the "no outcome changes" line, the sentence
answering a review with no public existence, and the one calling the trunk path filter note wrong are
all gone. The history is a message-only rewrite, the tree is byte identical.

The tests no longer export a model to check a branch. Routing is decided before the model is
touched, so the preparation is stubbed. Peak memory goes from about 4.3 GB to 420 MB and the three
cases from 7.1 to 3.5 seconds of processor time, with the mutation matrix unchanged.

The KV cache is pinned off, with a line saying why. Your finding was right: with it on the source
transforms import the Qualcomm SDK, so the Core ML with QNN case was avoiding that only through a
default nothing stated.

The two unread Core ML options are a real bug and I filed it separately. enable_state and
preserve_sdpa are parsed, stored, assigned, and read by nothing, and the lfm2 Core ML config shipped
here sets both to true, so that config has been exporting without them silently. It is not this
change's to fix, but this change is what makes it observable, so it should not stay only in a review
thread. Tracked in #22352.

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: examples Changes to any of our example LLMs integrations, such as Llama3 and Llava

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants