Skip to content

fix(backends): release() deregisters via a new remove_adapter() inverse verb - #1554

Merged
planetf1 merged 5 commits into
generative-computing:mainfrom
planetf1:issue-1528
Aug 24, 2026
Merged

fix(backends): release() deregisters via a new remove_adapter() inverse verb#1554
planetf1 merged 5 commits into
generative-computing:mainfrom
planetf1:issue-1528

Conversation

@planetf1

@planetf1 planetf1 commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

Pull Request

Issue

Fixes #1528.

Description

LocalFileBinding.release() unloaded adapter weights but left the adapter registered with the backend. That permanently reserved its qualified name, so a fresh binding for the same capability could not be prepared later in the same process.

This PR adds remove_adapter() as the inverse of registration and calls it during LocalFileBinding.release(). A released binding remains terminal; a newly created binding can now register the released name safely.

Where this fits

This is Phase 2 work for Epic #929. It resolves the lifecycle decision needed by #1465 and is independent of the embedded-adapter work in #1142 and telemetry work in #1466. Shim removal remains with #1144.

What changed

  • Add the AdapterMixin.remove_adapter() backend verb and the LocalHFBackend implementation.
  • Deregister local-file adapters during release, while rejecting removal of still-loaded adapters.
  • Clear removed registration state and snapshot adapter-registry iteration to keep concurrent reads safe.
  • Add regression coverage for re-registration, terminal bindings, and registry safety.

Caveat

Backends that do not implement remove_adapter() retain the previous release behaviour and emit a warning rather than failing teardown.

Testing

Focused adapter and Hugging Face tests, the non-qualitative suite, Ruff, mypy, and the documentation quality gate pass. Required GitHub checks pass.

Testing

  • Tests added to the respective file if code was changed
  • New code has 100% coverage if code was added
  • Ensure existing tests and github automation passes (a maintainer will kick off the github automation when the rest of the PR is populated)

Attribution

  • AI coding assistants used

Adding a new component, requirement, sampling strategy, or tool?

If your PR adds or modifies one of the types below, check the matching box. A checklist of type-specific review items will be posted as a comment.

  • Component
  • Requirement
  • Sampling Strategy
  • Tool

NOTE: Please ensure you have an issue that has been acknowledged by a core contributor and routed you to open a pull request against this repository. Otherwise, please open an issue before continuing with this pull request.

…se verb

Settles generative-computing#1528's contract question: release() stays terminal for the
binding instance itself (bind_backend()/prepare() still reject reuse),
but a released qualified_name no longer stays claimed for the backend's
lifetime. add_adapter() previously had no inverse, so unload_peft_adapter
reversed the load but left the registration in LocalHFBackend's
_added_adapters — burning the name for any later binding.

Adds remove_adapter() as a reality-specific AdapterMixin verb (mirrors
load_peft_adapter/unload_peft_adapter: default NotImplementedError,
overridden by LocalHFBackend to pop the entry from _added_adapters).
LocalFileBinding.release() now calls it alongside unload_peft_adapter, so
a fresh LocalFileBinding for the same capability can register under the
same qualified_name once the original is released. list_adapters() and
the strong reference to the released binding both correct themselves as
a result, with no change to list_adapters()'s own contract.

Assisted-by: Claude Code
Signed-off-by: Nigel Jones <jonesn@uk.ibm.com>
@github-actions github-actions Bot added the bug Something isn't working label Aug 18, 2026
…ontract

Independent review of PR generative-computing#1554 (3 reviewers) found remove_adapter() was an
incomplete inverse of add_adapter() and unsafe against still-loaded names:

- remove_adapter() only popped the registry entry, leaving the removed
  object's .backend/.path pointing at the old backend. Re-adding the same
  object silently no-op'd via add_adapter()'s "already added" guard.
  Fix: pop and clear both.
- remove_adapter() never checked _loaded_adapters. Freeing a name that was
  still loaded let a later load_peft_adapter() call hit PEFT's swallowed
  "already exists" error and silently keep running on the old adapter's
  weights under a new binding's identity. Fix: raise ValueError if still
  loaded; release() already unloads first, so the sanctioned path is
  unaffected.
- _added_adapters was insert-only before remove_adapter() added the first
  runtime deletion from it. Two generation-path sites (_find_adapter,
  resolve_adapter) iterated it via a live dict view outside the lock
  release() holds while deleting, racing to "dictionary changed size during
  iteration". Fix: snapshot into a list before iterating at both sites.
- release() called remove_adapter() unconditionally; a PEFT-reality backend
  that hasn't overridden it would have release() raise NotImplementedError
  mid-teardown and strand the binding permanently. Fix: catch and log,
  degrading to the pre-generative-computing#1528 behaviour (name stays claimed) instead of
  breaking release().

Also: corrected the reworded prepare() error (a blocking adapter is only
known to be registered, not "active"); strengthened the test double in
test_local_file_binding.py to mirror add_adapter()'s "already added" guard,
not just the duplicate-name one; added remove_adapter() assertions to the
idempotent-release and never-prepared-noop tests; fixed the AdapterMixin
docstring's stale reality-specific-verb count.

Assisted-by: Claude Code
Signed-off-by: Nigel Jones <jonesn@uk.ibm.com>
…ents

No behavioural change. Docstrings/comments referenced "generative-computing#1554 review finding
N" — a self-reference to the PR they live in, redundant now that the fixes
are just part of this PR's own history rather than a response to someone
else's review.

Assisted-by: Claude Code
Signed-off-by: Nigel Jones <jonesn@uk.ibm.com>
Fold the independent review of the generative-computing#1528 registry work into the PR:

- prepare(): drop "and has not been released" from the refusal
  message — false on the degradation path, where release() ran
  but the backend predates remove_adapter()
- release(): the degradation warning now states the observable
  fact (remove_adapter() raised NotImplementedError) instead of
  asserting the backend lacks the verb — a real implementation
  can raise it internally
- prepare(): note the lock mismatch between add_adapter()'s
  duplicate check (binding lifecycle lock only) and release()'s
  pop (generation lock) — a transient, retryable refusal
- _find_adapter(): record the snapshot-lookup invariant (one
  entry type per qualified name; generation consumes only shims;
  remove_adapter() is public) for generative-computing#1465
- LocalHFBackend.remove_adapter(): docstring points binding users
  at binding.release()
- tests: deterministic regression test for the resolve_adapter
  snapshot (companion to the _find_adapter one); drop drifting
  line-number citations; correct the fake-backend fidelity claim
  and the degradation-test history sentence

Assisted-by: Zed
Signed-off-by: Nigel Jones <jonesn@uk.ibm.com>
release() is terminal, so a later activate()/deactivate() does not
"keep working" — both paths raise the same prepare()-required error.
The docstring now states the actual contrast: release() keeps the
binding's lifecycle state coherent with the registry, while a direct
call leaves the binding in a state where the prepare()-required error
hides the real cause (an external remove_adapter()); prepare()
self-heals that state.

Assisted-by: Zed
Signed-off-by: Nigel Jones <jonesn@uk.ibm.com>
@planetf1
planetf1 marked this pull request as ready for review August 19, 2026 13:43
@planetf1
planetf1 requested a review from a team as a code owner August 19, 2026 13:43

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

One nit from claude, otherwise LGTM

mellea/backends/adapters/_core.py line 551–552 — 
release() catches only NotImplementedError from remove_adapter().
LocalHFBackend.remove_adapter() can also raise ValueError (still-loaded).
On the normal path unload_peft_adapter() runs first
so this can't fire, but if a subclass's unload_peft_adapter()
were ever a no-op while weights remained loaded, that ValueError
would propagate mid-teardown and leave _released False
(the same failure mode the NotImplementedError catch was added to prevent).
This is defensible as-is — the current code is correct under the documented invariant.

@planetf1

Copy link
Copy Markdown
Contributor Author

One nit from claude, otherwise LGTM

Thanks for the feedback. I think it makes sense to leave as is

@planetf1
planetf1 added this pull request to the merge queue Aug 24, 2026
Merged via the queue into generative-computing:main with commit 0aeae49 Aug 24, 2026
13 checks passed
@planetf1
planetf1 deleted the issue-1528 branch August 24, 2026 08:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

refactor(backends): release() leaves the adapter registered; is a binding reusable after release? (Epic #929)

2 participants