Resolve adapters by what is mounted, and say why each kind is not loaded
runtime.py resolved adapters at 7 call sites and only ever looked in the
adapter library, so an adapter attached to a cartridge (cartridge.py:230
copies it into <cartridge>/adapters/{kind}/) was never loaded by anything.
Resolution now lives in adapters.py: it reads the mounted cartridge set,
prefers a mounted cartridge's copy over the library, and answers every one
of the six kinds with a state the facts support — loaded, available,
deferred (base_model mismatch or missing weights, both models named in the
reason), missing — plus a shadowed row for each adapter that lost the
choice. A mounted cartridge is the scenario selection; neither manifest
carries a scenario field, so none was invented.
Two bugs the new tests caught: the preference sort lost reverse=True, so a
library adapter beat a mounted cartridge's copy, and the load branch had
never been executed by a test. Stub weights now prove peft is asked to
attach, that the returned model is put in eval mode, and that the endpoint
reports loaded until unload clears it.
Faces: GET /api/runtime/adapters, `ely-eye adapters`, and a log line naming
the reason whenever a generation runs without an adapter.
Gates: 222 passed; ruff clean; mypy cold cache on the 7 touched files gives
the same 2 errors as HEAD's runtime.py, and pyright gives the same 2 as
HEAD — both are qwen_vl_utils typing artifacts (process_vision_info returns
2 or 3 values depending on return_video_kwargs; the product takes 2).
This commit is contained in:
1 parent
9f0f7993e5
commit
2fbb4af96b
8 files changed
+585
-86
No files matched your search
@@ -0,0 +1,227 @@
|
||||
"""Which adapter the runtime loads, and the reason every other kind does not."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from datetime import UTC, datetime
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
from unittest import mock
|
||||
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from ely_eye import adapters, api
|
||||
from ely_eye.cartridge_store import set_mounts
|
||||
from ely_eye.config import Settings
|
||||
from ely_eye.db import Database
|
||||
from ely_eye.runtime import QwenRuntime
|
||||
from ely_eye.schemas import CartridgeManifest, TrainingAdapterKind
|
||||
|
||||
CORE = TrainingAdapterKind.hme_core_lora
|
||||
MODEL = "Qwen/Qwen3.5-9B"
|
||||
|
||||
|
||||
def _manifest(kind: TrainingAdapterKind, base_model: str, adapter_id: str) -> dict[str, Any]:
|
||||
return {
|
||||
"adapter_id": adapter_id,
|
||||
"kind": kind.value,
|
||||
"created_at": "2026-05-01T00:00:00+00:00",
|
||||
"base_model": base_model,
|
||||
"dataset_path": "PRD.md",
|
||||
"dataset_sha256": "d" * 64,
|
||||
"sample_count": 8,
|
||||
"max_steps": 16,
|
||||
"max_length": 512,
|
||||
"rank": 8,
|
||||
"alpha": 16,
|
||||
"target_modules": ["q_proj"],
|
||||
"trainable_params": 1_000,
|
||||
"total_params": 10_000,
|
||||
"optimizer": "adamw_8bit",
|
||||
"learning_rate": 0.0002,
|
||||
"gradient_accumulation_steps": 1,
|
||||
"bf16": True,
|
||||
"final_loss": 0.01,
|
||||
"artifacts": {},
|
||||
"sha256": "a" * 64,
|
||||
}
|
||||
|
||||
|
||||
def _write_adapter(
|
||||
root: Path,
|
||||
kind: TrainingAdapterKind = CORE,
|
||||
base_model: str = MODEL,
|
||||
adapter_id: str = "adapter-1",
|
||||
weights: bool = True,
|
||||
) -> Path:
|
||||
root.mkdir(parents=True, exist_ok=True)
|
||||
manifest = _manifest(kind, base_model, adapter_id)
|
||||
(root / "adapter_manifest.json").write_text(json.dumps(manifest), encoding="utf-8")
|
||||
if weights:
|
||||
(root / "adapter_model.safetensors").write_bytes(b"stub weights")
|
||||
return root
|
||||
|
||||
|
||||
def _settings(tmp_path: Path) -> Settings:
|
||||
settings = Settings(home=tmp_path / "home", model_id=MODEL)
|
||||
settings.ensure_dirs()
|
||||
return settings
|
||||
|
||||
|
||||
def _cartridge(settings: Settings, database: Database, cartridge_id: str) -> Path:
|
||||
root = settings.home / "cartridges" / cartridge_id
|
||||
root.mkdir(parents=True, exist_ok=True)
|
||||
now = datetime.now(UTC)
|
||||
manifest = CartridgeManifest(
|
||||
cartridge_id=cartridge_id,
|
||||
name="home",
|
||||
created_at=now,
|
||||
model_base=settings.model_id,
|
||||
source_count=0,
|
||||
atom_count=0,
|
||||
token_equivalent=0,
|
||||
artifacts={},
|
||||
dna=None,
|
||||
)
|
||||
database.write_cartridge(cartridge_id, "home", root, manifest.model_dump_json(), None, 0, now)
|
||||
set_mounts(database, [cartridge_id], now)
|
||||
return root
|
||||
|
||||
|
||||
class _FakeModel:
|
||||
def __init__(self) -> None:
|
||||
self.evalled = False
|
||||
|
||||
def eval(self) -> None:
|
||||
self.evalled = True
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _no_stale_loads():
|
||||
adapters.clear_loads()
|
||||
yield
|
||||
adapters.clear_loads()
|
||||
|
||||
|
||||
def test_a_library_adapter_is_selected_for_its_kind(tmp_path: Path) -> None:
|
||||
settings = _settings(tmp_path)
|
||||
chosen = _write_adapter(settings.adapters_dir / "core")
|
||||
source, state, reason = adapters.resolve_library_adapter(settings)
|
||||
assert source is not None and source.path == chosen
|
||||
assert state == "available"
|
||||
assert reason == "selected from the adapter library"
|
||||
|
||||
|
||||
def test_an_adapter_on_another_base_model_is_deferred_naming_both_models(tmp_path: Path) -> None:
|
||||
settings = _settings(tmp_path)
|
||||
_write_adapter(settings.adapters_dir / "router", kind=TrainingAdapterKind.hme_router, base_model="Qwen/Qwen3.5-0.8B")
|
||||
source, state, reason = adapters.resolve_library_adapter(settings, TrainingAdapterKind.hme_router)
|
||||
assert source is None
|
||||
assert state == "deferred"
|
||||
assert "trained on Qwen/Qwen3.5-0.8B" in reason and f"runtime holds {MODEL}" in reason
|
||||
|
||||
|
||||
def test_a_manifest_without_weights_is_deferred_naming_the_missing_file(tmp_path: Path) -> None:
|
||||
settings = _settings(tmp_path)
|
||||
_write_adapter(settings.adapters_dir / "core", weights=False)
|
||||
source, state, reason = adapters.resolve_library_adapter(settings)
|
||||
assert source is None and state == "deferred"
|
||||
assert "adapter_model.safetensors" in reason
|
||||
|
||||
|
||||
def test_the_mounted_cartridge_copy_wins_over_the_library(tmp_path: Path) -> None:
|
||||
settings = _settings(tmp_path)
|
||||
database = Database(settings)
|
||||
_write_adapter(settings.adapters_dir / "core", adapter_id="library-copy")
|
||||
cartridge_root = _cartridge(settings, database, "cart-home")
|
||||
cartridge_copy = _write_adapter(cartridge_root / "adapters" / "hme_core_lora", adapter_id="cartridge-copy")
|
||||
sources, mounted = adapters.discover(settings, database)
|
||||
assert mounted == ["cart-home"]
|
||||
source, state, reason = adapters.resolve(sources, settings, CORE)
|
||||
assert source is not None and source.path == cartridge_copy
|
||||
assert state == "available" and "mounted cartridge cart-home" in reason
|
||||
report = adapters.plan(settings, database)
|
||||
row = next(item for item in report.adapters if item.kind == CORE)
|
||||
assert row.adapter_id == "cartridge-copy" and row.cartridge_id == "cart-home"
|
||||
|
||||
|
||||
def test_the_endpoint_lists_every_kind_with_a_reason(tmp_path: Path, monkeypatch) -> None:
|
||||
settings = _settings(tmp_path)
|
||||
_write_adapter(settings.adapters_dir / "core")
|
||||
monkeypatch.setattr(api, "get_settings", lambda: settings)
|
||||
client = TestClient(api.create_app())
|
||||
body = client.get("/api/runtime/adapters").json()
|
||||
assert body["model_id"] == MODEL and body["mounted"] == []
|
||||
by_kind = {row["kind"]: row for row in body["adapters"]}
|
||||
assert set(by_kind) == {kind.value for kind in TrainingAdapterKind}
|
||||
assert by_kind[CORE.value]["state"] == "available"
|
||||
for kind in TrainingAdapterKind:
|
||||
if kind == CORE:
|
||||
continue
|
||||
assert by_kind[kind.value]["state"] == "missing"
|
||||
assert by_kind[kind.value]["reason"], f"{kind.value} carries no reason"
|
||||
|
||||
|
||||
def test_stub_weights_prove_the_load_branch_runs(tmp_path: Path, monkeypatch) -> None:
|
||||
settings = _settings(tmp_path)
|
||||
chosen = _write_adapter(settings.adapters_dir / "core")
|
||||
attached: list[tuple[Any, Path]] = []
|
||||
|
||||
def fake_attach(model: Any, path: Path) -> _FakeModel:
|
||||
attached.append((model, path))
|
||||
return _FakeModel()
|
||||
|
||||
monkeypatch.setattr(adapters, "load_into_model", fake_attach)
|
||||
monkeypatch.setattr(QwenRuntime, "_load_base_model", lambda self: (_FakeModel(), object()))
|
||||
runtime = QwenRuntime(settings)
|
||||
model, _processor = runtime._load_transformers()
|
||||
assert attached == [(mock.ANY, chosen)], "peft was never asked to attach the stub adapter"
|
||||
assert model.evalled is True
|
||||
assert [record.adapter_id for record in adapters.load_records()] == ["adapter-1"]
|
||||
row = next(item for item in adapters.plan(settings, Database(settings)).adapters if item.kind == CORE)
|
||||
assert row.state == "loaded" and "attached to the live model" in row.reason
|
||||
|
||||
|
||||
def test_unload_clears_the_loaded_state(tmp_path: Path, monkeypatch) -> None:
|
||||
settings = _settings(tmp_path)
|
||||
_write_adapter(settings.adapters_dir / "core")
|
||||
monkeypatch.setattr(adapters, "load_into_model", lambda model, path: _FakeModel())
|
||||
monkeypatch.setattr(QwenRuntime, "_load_base_model", lambda self: (_FakeModel(), object()))
|
||||
runtime = QwenRuntime(settings)
|
||||
runtime._load_transformers()
|
||||
assert adapters.load_records(), "nothing was recorded as loaded"
|
||||
runtime.unload()
|
||||
assert adapters.load_records() == []
|
||||
row = next(item for item in adapters.plan(settings, Database(settings)).adapters if item.kind == CORE)
|
||||
assert row.state == "available"
|
||||
|
||||
|
||||
def test_the_loser_of_the_choice_is_reported_as_shadowed(tmp_path: Path) -> None:
|
||||
settings = _settings(tmp_path)
|
||||
_write_adapter(settings.adapters_dir / "first", adapter_id="written_first")
|
||||
_write_adapter(settings.adapters_dir / "second", adapter_id="written_second")
|
||||
report = adapters.plan(settings, Database(settings))
|
||||
losers = [row for row in report.adapters if row.shadowed]
|
||||
assert [row.adapter_id for row in losers] == ["written_first"]
|
||||
assert "another adapter of this kind wins" in losers[0].reason
|
||||
winner = next(row for row in report.adapters if row.adapter_id == "written_second")
|
||||
assert winner.shadowed is False and winner.state == "available"
|
||||
|
||||
|
||||
def test_an_unreadable_manifest_names_the_file(tmp_path: Path) -> None:
|
||||
settings = _settings(tmp_path)
|
||||
broken = settings.adapters_dir / "broken"
|
||||
broken.mkdir(parents=True)
|
||||
(broken / "adapter_manifest.json").write_text("{not json", encoding="utf-8")
|
||||
with pytest.raises(ValueError, match="the adapter manifest is not a valid AdapterManifest"):
|
||||
adapters.library_sources(settings)
|
||||
|
||||
|
||||
def test_a_pinned_adapter_dir_without_a_manifest_says_the_path_is_missing(tmp_path: Path) -> None:
|
||||
settings = Settings(home=tmp_path / "home", model_id=MODEL, adapter_dir=tmp_path / "empty")
|
||||
settings.ensure_dirs()
|
||||
assert adapters.library_sources(settings) == []
|
||||
available, detail = QwenRuntime(settings)._transformers_status()
|
||||
assert available is False
|
||||
assert detail == f"Configured adapter path is missing: {tmp_path / 'empty'}"
|
||||
Reference in new issue
Block a user