Stop the adapter report from claiming things that are not true
Independent review of the adapter resolution work found six defects, all real and all fixed here. A kind whose only candidates were rejected had no winning path, so its row carried path=None and the rejected candidate fell into the shadow loop: the report said "another adapter of this kind wins for the current mount" while nothing won, and the real deferred reason was buried. A shadow row now requires a winner and names it. "loaded" was backed by a process-global record, so a fresh runtime with no model still claimed a live adapter. The record is now held by the instance that attached it, and the report keeps a weak reference: while the model lives the row says loaded, once it is gone the row says available and explains that the model is gone. The sort key had lost the manifest's cartridge tag, which changed which adapter loads on any machine holding a tagged library copy, and neither the commit message nor the checklist said so. The tag is back as the middle key. Proof checks resolved through the library while generation resolved through the mount, so one proof payload could name two different copies. Both now resolve the way generation does. loaded_at was written and never read, so it is gone. The checklist claim that peft is lazily imported in one place was true only for generation and is now scoped that way. The four generation paths and status() had no test coverage, so "no function was lost" rested on reading. One test now walks the transformers prompt path end to end with stub weights and asserts the generation answer carries the adapter it attached. pytest -q: 225 passed. ruff clean. mypy on the three touched source files: 2 errors, the identical 2 on a stashed HEAD tree, net zero. pyright: 2 errors, both the known qwen_vl_utils tuple artifact, unchanged from HEAD.
This commit is contained in:
1 parent
2fbb4af96b
commit
2d1cfdadc7
5 files changed
+128
-38
No files matched your search
@@ -3,8 +3,8 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import weakref
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
@@ -34,16 +34,16 @@ class AdapterSource:
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class LoadRecord:
|
||||
"""An adapter this process actually attached to a live model."""
|
||||
"""An adapter this process attached to a model."""
|
||||
|
||||
kind: TrainingAdapterKind
|
||||
adapter_id: str
|
||||
path: str
|
||||
sha256: str
|
||||
loaded_at: str
|
||||
|
||||
|
||||
_LOADED: dict[str, LoadRecord] = {}
|
||||
_LIVE: dict[str, weakref.ReferenceType[object]] = {}
|
||||
|
||||
|
||||
def read_manifest(path: Path) -> AdapterManifest:
|
||||
@@ -143,36 +143,41 @@ def resolve(
|
||||
return chosen, "available", "selected from the adapter library"
|
||||
|
||||
|
||||
def resolve_library_adapter(
|
||||
settings: Settings,
|
||||
kind: TrainingAdapterKind = TrainingAdapterKind.hme_core_lora,
|
||||
) -> tuple[AdapterSource | None, AdapterState, str]:
|
||||
"""Resolution without a database: the library only, no mounted cartridges."""
|
||||
return resolve(library_sources(settings), settings, kind)
|
||||
|
||||
|
||||
def _preference(source: AdapterSource) -> tuple[bool, float]:
|
||||
def _preference(source: AdapterSource) -> tuple[bool, bool, float]:
|
||||
weights = source.path / WEIGHTS_FILE
|
||||
return (source.origin == "cartridge", weights.stat().st_mtime if weights.exists() else 0.0)
|
||||
return (
|
||||
source.origin == "cartridge",
|
||||
source.manifest.cartridge_id is not None,
|
||||
weights.stat().st_mtime if weights.exists() else 0.0,
|
||||
)
|
||||
|
||||
|
||||
def load_records() -> list[LoadRecord]:
|
||||
return list(_LOADED.values())
|
||||
|
||||
|
||||
def live_kinds() -> set[str]:
|
||||
"""Kinds whose attached model is still alive, not merely attached once."""
|
||||
alive = {kind for kind, reference in _LIVE.items() if reference() is not None}
|
||||
for kind in [kind for kind, reference in _LIVE.items() if reference() is None]:
|
||||
del _LIVE[kind]
|
||||
return alive
|
||||
|
||||
|
||||
def clear_loads() -> None:
|
||||
_LOADED.clear()
|
||||
_LIVE.clear()
|
||||
|
||||
|
||||
def record_load(source: AdapterSource) -> LoadRecord:
|
||||
def record_load(source: AdapterSource, model: Any) -> LoadRecord:
|
||||
record = LoadRecord(
|
||||
kind=source.manifest.kind,
|
||||
adapter_id=source.manifest.adapter_id,
|
||||
path=str(source.path),
|
||||
sha256=source.manifest.sha256,
|
||||
loaded_at=datetime.now().astimezone().isoformat(),
|
||||
)
|
||||
_LOADED[record.kind.value] = record
|
||||
_LIVE[record.kind.value] = weakref.ref(model)
|
||||
logger.info("loaded adapter %s (%s) from %s", record.adapter_id, record.kind.value, record.path)
|
||||
return record
|
||||
|
||||
@@ -191,12 +196,18 @@ def plan(settings: Settings, database: Database) -> AdapterLoadReport:
|
||||
"""One row per adapter kind, plus a row for every adapter that lost the choice."""
|
||||
sources, mounted = discover(settings, database)
|
||||
rows: list[AdapterChoice] = []
|
||||
winners: dict[str, AdapterSource | None] = {}
|
||||
alive = live_kinds()
|
||||
for kind in TrainingAdapterKind:
|
||||
source, state, reason = resolve(sources, settings, kind)
|
||||
winners[kind.value] = source
|
||||
record = next((item for item in load_records() if item.kind == kind), None)
|
||||
if record is not None:
|
||||
reason = f"attached to the live model from {record.path}"
|
||||
state = "loaded"
|
||||
if kind.value in alive:
|
||||
state, reason = "loaded", f"attached to the live model from {record.path}"
|
||||
else:
|
||||
state = "available"
|
||||
reason = f"attached earlier in this process from {record.path}; that model is gone"
|
||||
rows.append(
|
||||
AdapterChoice(
|
||||
kind=kind,
|
||||
@@ -209,15 +220,15 @@ def plan(settings: Settings, database: Database) -> AdapterLoadReport:
|
||||
cartridge_id=source.cartridge_id if source else None,
|
||||
)
|
||||
)
|
||||
chosen_paths = {row.path for row in rows if row.path}
|
||||
for source in sources:
|
||||
if str(source.path) in chosen_paths:
|
||||
winner = winners.get(source.manifest.kind.value)
|
||||
if winner is None or source.path == winner.path:
|
||||
continue
|
||||
rows.append(
|
||||
AdapterChoice(
|
||||
kind=source.manifest.kind,
|
||||
state="deferred",
|
||||
reason="another adapter of this kind wins for the current mount",
|
||||
reason=f"shadowed by {winner.manifest.adapter_id} for the current mount",
|
||||
path=str(source.path),
|
||||
adapter_id=source.manifest.adapter_id,
|
||||
base_model=source.manifest.base_model,
|
||||
|
||||
@@ -19,6 +19,7 @@ from ..evaluation_schemas import (
|
||||
from ..evidence_graph import EvidenceGraphService
|
||||
from ..profiles import runtime_profile_report
|
||||
from ..runtime import QwenRuntime
|
||||
from ..runtime_schemas import AdapterState
|
||||
from ..schemas import (
|
||||
RuntimeProfile,
|
||||
TrainingAdapterKind,
|
||||
@@ -40,6 +41,16 @@ from .training_trace import (
|
||||
)
|
||||
|
||||
|
||||
def _generation_adapter(
|
||||
settings: Settings,
|
||||
database: Database,
|
||||
kind: TrainingAdapterKind = TrainingAdapterKind.hme_core_lora,
|
||||
) -> tuple[adapters.AdapterSource | None, AdapterState, str]:
|
||||
"""Resolve the way generation does, mounted cartridges included, so proof and generation name one copy."""
|
||||
sources, _mounted = adapters.discover(settings, database)
|
||||
return adapters.resolve(sources, settings, kind)
|
||||
|
||||
|
||||
class RuntimeChecks:
|
||||
"""Runtime adapter binding, gold-adapter generation, launch profiles, and the removed module check."""
|
||||
|
||||
@@ -59,7 +70,7 @@ class RuntimeChecks:
|
||||
return deferred
|
||||
runtime = QwenRuntime(self.settings)
|
||||
status = runtime.status()
|
||||
adapter, _adapter_state, _adapter_reason = adapters.resolve_library_adapter(self.settings)
|
||||
adapter, _adapter_state, _adapter_reason = _generation_adapter(self.settings, self.db)
|
||||
weight_path = adapter.path / "adapter_model.safetensors" if adapter else None
|
||||
payload = {
|
||||
"runtime_status": status.model_dump(mode="json"),
|
||||
@@ -134,7 +145,7 @@ class RuntimeChecks:
|
||||
)
|
||||
payload["generation_question"] = generation_question
|
||||
status = runtime.status()
|
||||
adapter, _adapter_state, _adapter_reason = adapters.resolve_library_adapter(proof_settings)
|
||||
adapter, _adapter_state, _adapter_reason = _generation_adapter(proof_settings, self.db)
|
||||
generation = runtime.generate(generation_question, context)
|
||||
answer, citations, citation_report = Verifier().verify_answer(generation.text, context)
|
||||
claim_id = EvidenceGraphService(proof_settings, self.db).record_answer(
|
||||
|
||||
@@ -42,6 +42,7 @@ class QwenRuntime:
|
||||
self.database = Database(self.settings)
|
||||
self._processor: Any | None = None
|
||||
self._model: Any | None = None
|
||||
self._attached: adapters.LoadRecord | None = None
|
||||
|
||||
def _adapter_choice(self) -> tuple[adapters.AdapterSource | None, str]:
|
||||
"""The adapter this runtime would attach, plus the reason if it would not attach one."""
|
||||
@@ -50,7 +51,8 @@ class QwenRuntime:
|
||||
return source, reason
|
||||
|
||||
def _loaded_adapter(self) -> adapters.LoadRecord | None:
|
||||
return next((item for item in adapters.load_records() if item.kind == TrainingAdapterKind.hme_core_lora), None)
|
||||
"""What this instance attached; another instance's model is not this one's claim."""
|
||||
return self._attached
|
||||
|
||||
def _generation_adapter_fields(self) -> dict[str, str | None]:
|
||||
record = self._loaded_adapter()
|
||||
@@ -314,7 +316,7 @@ class QwenRuntime:
|
||||
logger.info("generating without an adapter: %s", reason)
|
||||
else:
|
||||
model = adapters.load_into_model(model, source.path)
|
||||
adapters.record_load(source)
|
||||
self._attached = adapters.record_load(source, model)
|
||||
model.eval()
|
||||
self._model, self._processor = model, processor
|
||||
return model, processor
|
||||
@@ -345,7 +347,7 @@ class QwenRuntime:
|
||||
def unload(self) -> None:
|
||||
self._model = None
|
||||
self._processor = None
|
||||
adapters.clear_loads()
|
||||
self._attached = None
|
||||
try:
|
||||
import torch
|
||||
|
||||
|
||||
Reference in new issue
Block a user