Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions docs/docs/advanced/intrinsics.md
Original file line number Diff line number Diff line change
Expand Up @@ -375,9 +375,10 @@ not one of the built-in adapter functions — see
weights binding into a single, inspectable object. Both `LocalHFBackend` and
`OpenAIBackend` accept a composed `Adapter` directly via `add_adapter` —
dispatching on the weights binding's reality (`LocalFileBinding` for
LocalFile/PEFT, `EmbeddedBinding` for Embedded/Granite Switch) — alongside
the deprecated shim classes, which remain functional for now (Epic #929,
issue #1144). An `EmbeddedBinding` adapter additionally requires `add_adapter`'s
LocalFile/PEFT, `EmbeddedBinding` for Embedded/Granite Switch). The composed
`Adapter` is now the only registration path — the earlier shim classes have
been removed (Epic #929, issue #1144). An `EmbeddedBinding` adapter
additionally requires `add_adapter`'s
`config=` argument (the raw io.yaml mapping) — `add_adapter` raises `ValueError`
without it, since that reality's config cannot be cheaply re-derived later; see
below.
Expand Down
7 changes: 1 addition & 6 deletions docs/docs/advanced/lora-and-alora-adapters.md
Original file line number Diff line number Diff line change
Expand Up @@ -261,15 +261,10 @@ all. The check activates only where an aLoRA is actually the adapter in play tod
`ValueError` (chained from the `AloraActivationError`) instead -- consistent with
how a missing adapter is handled, and for the same reason: a yes/no LLMaJ answer
cannot be parsed by the adapter's JSON `output_to_bool`.
- **The deprecated `IntrinsicAdapter` shim**, but not on its first call: the shim's
PEFT weights load lazily inside generation, so the check has nothing to compare
against yet the first time. From the second call onward the weights are already
loaded and the check applies normally (Epic #929, issue #1144 tracks removing the
shim entirely).

A direct adapter-function call reaching this check has no fallback layer below it and
lets `AloraActivationError` propagate, so the caller sees a clear error instead of a
silently wrong score -- but today that only happens on one of the three paths above,
silently wrong score -- but today that only happens on one of the two paths above,
not on an unmodified default installation calling `core.check_certainty(...)` or
similar. Server-mediated and embedded adapter deployments activate outside Mellea and
are not covered by this check at all.
Expand Down
3 changes: 1 addition & 2 deletions docs/docs/integrations/openai.md
Original file line number Diff line number Diff line change
Expand Up @@ -361,8 +361,7 @@ backend = OpenAIBackend(
)

# Discover and register a single adapter from the model's Hugging Face repo —
# this composes an Adapter (Identity + IOContract + EmbeddedBinding) rather
# than returning the deprecated EmbeddedIntrinsicAdapter shim.
# this composes an Adapter (Identity + IOContract + EmbeddedBinding).
backend.register_embedded_adapter_model(
IBM_GRANITE_SWITCH_4_1_3B_PREVIEW.hf_model_name,
intrinsic_name="answerability",
Expand Down
4 changes: 2 additions & 2 deletions docs/docs/observability/metrics.md
Original file line number Diff line number Diff line change
Expand Up @@ -304,8 +304,8 @@ All sampling metrics include:
## Adapter function metrics

Mellea records metrics for adapter function calls (`core.requirement_check()`,
`rag.check_answerability()`, `Intrinsic`/`ALoraRequirement`, and the
deprecated shim classes alike) — no code changes are required beyond enabling
`rag.check_answerability()`, and `Intrinsic`/`ALoraRequirement`) — no code
changes are required beyond enabling
metrics as above. They are recorded by `AdapterFunctionMetricsPlugin`
(`mellea/telemetry/metrics_plugins.py`), which subscribes to the
`adapter_function_invocation_complete` and `adapter_function_phase_complete`
Expand Down
8 changes: 4 additions & 4 deletions docs/docs/reference/glossary.md
Original file line number Diff line number Diff line change
Expand Up @@ -572,10 +572,10 @@ runtime or uses adapter functions embedded in a local [Granite Switch](#granite-
checkpoint with `load_embedded_adapters=True`. `OpenAIBackend` also supports
embedded adapter functions when backed by a Granite Switch deployment.

> **Note:** The Python symbol `Intrinsic` (and related classes such as `IntrinsicAdapter`)
> will be renamed to `AdapterFunction` / `Adapter` in a future phase of Epic #929
> (#1136). Prefer the term **adapter function** in prose and documentation; use `Intrinsic`
> only when referencing the current Python API directly.
> **Note:** The Python symbol `Intrinsic` will be renamed to `AdapterFunction`
> in a future phase of Epic #929 (#1136). Prefer the term **adapter function**
> in prose and documentation; use `Intrinsic` only when referencing the current
> Python API directly.

See: [Adapter Functions](../advanced/intrinsics.md)

Expand Down
4 changes: 2 additions & 2 deletions docs/docs/tutorials/07-custom-adapter-function.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ description: "Compose your own trained LoRA/aLoRA adapter into Mellea without a

This tutorial walks through registering a custom, non-catalog adapter
function by composing an `Adapter` directly — the replacement for the
deprecated `CustomIntrinsicAdapter` shim (Epic #929, issue #1144).
removed `CustomIntrinsicAdapter` shim (Epic #929, issue #1144).

By the end you will have covered:

Expand Down Expand Up @@ -137,7 +137,7 @@ result = m.instruct(

## What replaced the old approach

The deprecated `CustomIntrinsicAdapter` shim did two things under the hood
The removed `CustomIntrinsicAdapter` shim did two things under the hood
that this tutorial's composed `Adapter` replaces without a class of its own:

1. **Adapter registration** — `LocalFileBinding` already accepts an arbitrary
Expand Down
3 changes: 1 addition & 2 deletions docs/examples/granite-switch/manual_adapter_loading.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,7 @@
the model repo at init), this example shows how to create an OpenAIBackend with
`load_embedded_adapters=False` and then manually load individual adapters using
`register_embedded_adapter_model(intrinsic_name=...)`, which composes an
`Adapter` (Identity + IOContract + EmbeddedBinding) instead of the deprecated
`EmbeddedIntrinsicAdapter` shim.
`Adapter` (Identity + IOContract + EmbeddedBinding).

This is useful when:
- You only need a subset of the model's embedded adapters.
Expand Down
4 changes: 0 additions & 4 deletions mellea/backends/adapters/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,6 @@
AdapterInput,
AdapterMixin,
AdapterType,
EmbeddedIntrinsicAdapter,
IntrinsicAdapter,
LocalHFAdapter,
fetch_intrinsic_metadata,
get_adapter_for_intrinsic,
Expand All @@ -39,10 +37,8 @@
"AloraActivationError",
"EmbeddedActivationRequest",
"EmbeddedBinding",
"EmbeddedIntrinsicAdapter",
"IOContract",
"Identity",
"IntrinsicAdapter",
"LocalFileBinding",
"LocalHFAdapter",
"ServerMediatedBinding",
Expand Down
57 changes: 47 additions & 10 deletions mellea/backends/adapters/_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
The existing :class:`~mellea.backends.adapters.adapter.Adapter` ABC in
`adapter.py` is not modified here. This module introduces a new
`Adapter` *dataclass* that is re-exported from
`mellea.backends.adapters`. Both coexist until shim removal in 4.1.
`mellea.backends.adapters`. Both coexist for now.
The old ABC is not part of the public `__init__.py` surface, so there is
no namespace collision on the public API.
"""
Expand Down Expand Up @@ -691,10 +691,25 @@ def hold_prepared(self, backend: "AdapterMixin") -> Iterator[None]:
def activate(self) -> None:
"""Selects already-loaded adapter weights for generation.

Resilient to the weights having been dropped from the backend's PEFT
model since `prepare()`: `prepare()` injects the weights once, but an
`unload_peft_adapter()` call (e.g. to free GPU memory, or as a test
teardown) deletes them from the PEFT model while leaving this binding's
registration intact. Re-injecting them here — rather than treating
`prepare()` as a one-time guarantee — lets a composed `Adapter` be
unloaded and later re-used on the same backend without a hard failure
(the resilience the removed `IntrinsicAdapter` shim gave by re-loading
on demand).

Raises:
RuntimeError: `prepare()` was not called first, or called but did
not complete (registered with the backend but the weights
load itself failed or hasn't been retried yet).
Exception: Whatever `load_peft_adapter()` raises when the reload
after an intervening unload fails (e.g. the checkpoint was
evicted). The half-injected PEFT state is cleaned up before
the exception propagates, so the next `activate()` retries the
load cleanly rather than silently serving zero-effect weights.
"""
backend = self.backend
if backend is None or not self._loaded:
Expand All @@ -706,7 +721,33 @@ def activate(self) -> None:
raise RuntimeError(
"LocalFileBinding.activate() requires prepare() to be called first."
)
backend.activate_peft_adapter(self.qualified_name)
try:
backend.activate_peft_adapter(self.qualified_name)
except ValueError:
# The weights were dropped from the PEFT model since prepare()
# (an intervening unload_peft_adapter), so set_adapter() cannot
Comment on lines +726 to +728

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.

I think the exception block needs to set _loaded to untrue. I think that will fix the below issue as well.

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.

Traced both suggested fixes and neither closes it:

  • Setting _loaded = False in this except block is too late: _check_alora_activation has already returned for this call (it bails at huggingface.py:534 when the name is missing from peft_config).
  • Resetting it in unload_peft_adapter makes the next call raise RuntimeError: the generation path never re-prepares (huggingface.py:1021, lock-order), and resolve_adapter returns a found adapter without re-preparing (adapter.py:991). That brings back the test_constraint_lora_* failures this reload was added to fix.

The weights need to be back before the guard runs. In _generate_from_intrinsic, when the aLoRA qualified name is missing from _loaded_adapters but still in _added_adapters, reload it under _generation_lock ahead of _check_alora_activation; this except then only covers a genuine race. Two side notes: the _alora_qualified_name_for_activation_check docstring (huggingface.py:457) still says registration-time prepare() makes the check safe, and the reload bills a full PEFT load into the phase="activate" metric (adapter.py:1144), which a pre-guard reload removes.

# find the name. The registration in `_added_adapters` survives
# an unload, so load_peft_adapter() re-injects it; retry once.
# Both verbs mutate PEFT model state the activation lock already
# held here guards, so no new lock is taken — re-loading under
# `_lifecycle_lock` (as prepare() does) would invert the adapter
# lock order against a concurrent release().
try:
backend.load_peft_adapter(self.qualified_name)
except BaseException:
# PEFT injects the adapter's modules and registers the name
# in `peft_config` *before* it reads the checkpoint, so a
# failed reload (e.g. an evicted cache dir) leaves the name
# half-registered: the next `set_adapter` would succeed on
# zero-effect modules and serve base-model output under the
# adapter's name with no error. unload_peft_adapter()'s
# best-effort cleanup reverses that injected-but-unloaded
# state, so the next activate() hits ValueError again and
# retries the load cleanly. `_loaded` is deliberately left
# True — the registration itself is intact (PR #1700).
backend.unload_peft_adapter(self.qualified_name)
raise
backend.activate_peft_adapter(self.qualified_name)
self._active = True

def deactivate(self) -> None:
Expand Down Expand Up @@ -1129,11 +1170,7 @@ class Adapter:
# (registration and the verbs key on the binding's `qualified_name`;
# `_find_adapter` scans on the identity) and both return `None` on a miss,
# so a disagreement surfaces as "adapter not found" far from its cause. It
# could not be enforced here because the deprecated shims carry a
# `_ShimWeightsBinding` with no `name`/`adapter_type` to compare at all
# (their identity tracks the configured name/type) — that no longer
# blocks the LocalFile/PEFT reality now that the shims retire in #1144, so
# `LocalHFBackend.add_adapter` enforces it there instead, at registration
# time. `EmbeddedBinding` has no `name`/`adapter_type` of its own, so the
# Embedded/Granite Switch reality still has nothing to cross-check
# against.
# is enforced not here but in `LocalHFBackend.add_adapter`, at registration
# time, so a construction that never reaches a backend isn't rejected.
# `EmbeddedBinding` has no `name`/`adapter_type` of its own, so the
# Embedded/Granite Switch reality has nothing to cross-check against.
Loading
Loading