Skip to content

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

Description

@planetf1

Parent epic: #929
Phase: 2 (follow-up to #1141 / PR #1454)
Depends on: nothing
Blocks: nothing

Problem

LocalFileBinding.release() does not fully deregister the adapter, so its
qualified_name stays claimed for the backend's lifetime and no later binding can
register under it.

release() calls AdapterMixin.unload_peft_adapter, which removes the entry from
LocalHFBackend._loaded_adapters — but nothing removes it from _added_adapters,
the registered set that add_adapter populates and that add_adapter itself
checks for duplicates. There is no inverse verb for add_adapter on the mixin, so
the binding has nothing to call.

Observed, with a fake backend reproducing LocalHFBackend.add_adapter's guards:

after prepare : backend set = True | _added_adapters = ['answerability_lora']
after release : backend = None | _staged_backend = None | _added_adapters STILL = ['answerability_lora']
re-prepare without bind_backend  -> RuntimeError: requires bind_backend() to be called first
re-prepare after bind_backend    -> RuntimeError: Backend refused to register 'answerability_lora'

Two consequences:

  1. The name is burned. A different LocalFileBinding for the same capability
    can never be registered on that backend after the first one is released, even
    though the first is fully torn down from the model's point of view.
  2. release() overstates itself. Its docstring said "releases all resources"
    and feat(backends): LocalFileBinding implements verbs (PEFT/aLoRA path) + from_catalog() (Epic #929 Phase 2) #1141's acceptance criterion says "release() cleanly unregisters". Neither
    was accurate. PR feat(backends): LocalFileBinding implements verbs (PEFT/aLoRA path) + from_catalog() (Epic #929 Phase 2) #1454 corrects the docstring to say exactly what is and is not
    released, and points here.

The actual question: should re-registration be supported at all?

This issue is a question first, not an agreed fix. The merged WeightsBinding
contract (mellea/backends/adapters/_core.py, from #1134) says:

release() — terminal; releases all resources. The binding is not reusable
after release().

If "terminal" is meant strictly, then a released binding never being revivable is
correct, and the only defects are the wording and the confusing error. But
#1141's own text says the opposite — "prepare once per session (or explicit
release()+prepare() cycle)" — which the merged ABC forbids. That contradiction
has been flagged on #1141 and needs settling here.

So, in order:

  1. Decide the contract. Is a binding reusable after release()? The ABC says
    no; feat(backends): LocalFileBinding implements verbs (PEFT/aLoRA path) + from_catalog() (Epic #929 Phase 2) #1141 said yes. One of them is wrong.
  2. Only then decide the mechanism. If bindings stay terminal, the fix may be
    nothing more than what PR feat(backends): LocalFileBinding implements verbs (PEFT/aLoRA path) + from_catalog() (Epic #929 Phase 2) #1454 already did (accurate docstring, clearer error)
    plus a decision about whether a fresh binding should be able to claim a
    released name — which is the case that actually bites.
  3. If a released name must become re-claimable, add_adapter needs an inverse.
    That is a change to the mixin verb contract, which feat(backends): EmbeddedBinding implements apply_activation (Granite Switch path); remove render_controls + set_request_adapter (Epic #929 Phase 2) #1142 and add support for granite-switch / embedded adapters on our huggingface backend #1018 also build
    on, so it should not be done incidentally.

Why it matters for #1465

#1465 routes intrinsic generation through adapter_scope. If that work ends up
preparing a binding per call rather than once per session, it hits this
immediately — the second call cannot register. Worth resolving before #1465 picks a
lifetime for its bindings, even if the resolution is "terminal is correct, prepare
once".

Scope

Not in scope

Changing unload_peft_adapter's own semantics — it correctly manages the loaded
set. The gap is the absent registered-set counterpart.

References

Metadata

Metadata

Assignees

Labels

area/adapter-functionsGranite adapter functions: framework and adaptiers including RAG, Guardian, Corearea/backendsProvider-specific work: Ollama, HF, LiteLLM, OpenAI, Bedrock, vLLMp2Medium/low: minor bugs, niche features, polish, docs, tests, cleanup. Scoped, lower urgency.refactor

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions