mcptoolshop-interlock 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- interlock/__init__.py +4 -0
- interlock/__main__.py +4 -0
- interlock/adapters/__init__.py +1 -0
- interlock/adapters/base.py +167 -0
- interlock/adapters/offrig_calibrate.py +91 -0
- interlock/adapters/ollama.py +559 -0
- interlock/adapters/ollama_cases.py +313 -0
- interlock/adapters/openvino.py +1289 -0
- interlock/adapters/record_fixtures.py +403 -0
- interlock/adapters/registry.py +37 -0
- interlock/cli.py +468 -0
- interlock/devices.py +201 -0
- interlock/families.py +53 -0
- interlock/guards.py +96 -0
- interlock/harness/__init__.py +1 -0
- interlock/harness/choice.py +85 -0
- interlock/harness/count_budget.py +100 -0
- interlock/harness/e4b.py +2620 -0
- interlock/harness/e4b_stats.py +213 -0
- interlock/harness/e5.py +560 -0
- interlock/harness/environment.py +161 -0
- interlock/harness/fill.py +2597 -0
- interlock/harness/lifecycle.py +307 -0
- interlock/harness/lock.py +41 -0
- interlock/harness/marker.py +113 -0
- interlock/harness/ollama_serve.py +364 -0
- interlock/harness/receipts.py +411 -0
- interlock/harness/runner.py +2221 -0
- interlock/harness/spec.py +469 -0
- interlock/harness/steady.py +230 -0
- interlock/harness/storage.py +166 -0
- interlock/harness/timing.py +175 -0
- interlock/harness/watch.py +756 -0
- interlock/importers/__init__.py +1 -0
- interlock/importers/aspire_judges.py +249 -0
- interlock/importers/assemble.py +303 -0
- interlock/importers/calibration.py +257 -0
- interlock/importers/common.py +142 -0
- interlock/importers/gaps.py +242 -0
- interlock/importers/ladder.py +109 -0
- interlock/importers/nli.py +103 -0
- interlock/importers/offers.py +131 -0
- interlock/importers/pins.py +32 -0
- interlock/importers/probe.py +158 -0
- interlock/ollama_supervisor.py +1715 -0
- interlock/receipt.py +339 -0
- interlock/router/__init__.py +5 -0
- interlock/router/policy.py +785 -0
- interlock/schema/route-decision.v1.json +101 -0
- interlock/schema/routing-row.schema.json +184 -0
- interlock/scope.py +52 -0
- interlock/tasks.py +41 -0
- mcptoolshop_interlock-0.1.0.dist-info/METADATA +211 -0
- mcptoolshop_interlock-0.1.0.dist-info/RECORD +57 -0
- mcptoolshop_interlock-0.1.0.dist-info/WHEEL +4 -0
- mcptoolshop_interlock-0.1.0.dist-info/entry_points.txt +2 -0
- mcptoolshop_interlock-0.1.0.dist-info/licenses/LICENSE +21 -0
interlock/__init__.py
ADDED
interlock/__main__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Device adapters. The harness depends on ``base``, not on a particular engine."""
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
"""The adapter interface.
|
|
2
|
+
|
|
3
|
+
An adapter runs one measured step and unloads the model it loaded. The harness
|
|
4
|
+
talks only to this shape. Kimi's OpenVINO adapter plugs in by adding
|
|
5
|
+
``interlock.adapters.openvino`` with a function named ``adapter`` that returns
|
|
6
|
+
an object with ``run`` and ``unload``. The harness imports that module only
|
|
7
|
+
when a step names the OpenVINO engine. It does not fall back to another device.
|
|
8
|
+
|
|
9
|
+
``run`` returns a result. A failed step has ``ok`` false and does not raise,
|
|
10
|
+
so the harness can stop the sequence and still unload.
|
|
11
|
+
|
|
12
|
+
A paired arm names a companion. The harness calls ``start_session`` on that
|
|
13
|
+
companion's adapter, runs the measured step, then calls ``stop_session``.
|
|
14
|
+
The companion keeps running between those two calls. ``stop_session`` ends
|
|
15
|
+
that load. It is not ``unload``.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import threading
|
|
21
|
+
import time
|
|
22
|
+
from contextlib import contextmanager
|
|
23
|
+
from dataclasses import dataclass
|
|
24
|
+
from typing import Protocol
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class AdapterError(Exception):
|
|
28
|
+
"""The adapter refused the step before measuring it."""
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class NativeBinaryRejected(AdapterError):
|
|
32
|
+
"""The device refused the compiled native binary at execute time.
|
|
33
|
+
|
|
34
|
+
Raised only at the execute site, after this run's compile succeeded, with
|
|
35
|
+
the Level Zero signature ``ZE_RESULT_ERROR_INVALID_NATIVE_BINARY``
|
|
36
|
+
(``0x7800000f``). ``compile_s`` carries that compile's time, so the
|
|
37
|
+
harness's capability row can record that the compile finished. Nothing is
|
|
38
|
+
retried.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
def __init__(self, message: str, *, compile_s: float | None = None) -> None:
|
|
42
|
+
super().__init__(message)
|
|
43
|
+
self.compile_s = compile_s
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
COMPILER_MESSAGE_MARKER = "[NPU_VCL] Compiler returned msg"
|
|
47
|
+
COMPILE_REJECTED_CODE = "0x78000007"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def compile_rejected_error(error: str | None) -> bool:
|
|
51
|
+
"""The NPU compiler refused the graph (ruling on #36).
|
|
52
|
+
|
|
53
|
+
Both markers must be present: the compiler's own message line and the Level
|
|
54
|
+
Zero code ``0x78000007`` (``ZE_RESULT_ERROR_INVALID_NULL_POINTER``). Either
|
|
55
|
+
alone is some other failure and stays an abort.
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
if not isinstance(error, str) or not error:
|
|
59
|
+
return False
|
|
60
|
+
return COMPILER_MESSAGE_MARKER in error and COMPILE_REJECTED_CODE in error.lower()
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class CompileRejected(AdapterError):
|
|
64
|
+
"""The NPU compiler refused the graph, raised inside ``compile_model``.
|
|
65
|
+
|
|
66
|
+
Raised only at the compile site, with the signature in
|
|
67
|
+
:func:`compile_rejected_error`. ``failed_s`` is the time from the start of
|
|
68
|
+
the build to the failure: for embed that is the compile; for generate it
|
|
69
|
+
also includes the pipeline load. The harness reads the subtype to tell a compile
|
|
70
|
+
refusal from an execute failure. Nothing is retried.
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
def __init__(self, message: str, *, failed_s: float | None = None) -> None:
|
|
74
|
+
super().__init__(message)
|
|
75
|
+
self.failed_s = failed_s
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class BuildRefused(AdapterError):
|
|
79
|
+
"""The build guard refused a pipeline or model build for lack of memory headroom.
|
|
80
|
+
|
|
81
|
+
Raised before the build starts, so nothing was compiled. The harness marks the
|
|
82
|
+
block's unfinished arms not run (reason ``memory headroom``); it is not a
|
|
83
|
+
capability outcome and never reaches the routing table.
|
|
84
|
+
"""
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class CompileTracker:
|
|
88
|
+
"""Which native compile is running, and since when.
|
|
89
|
+
|
|
90
|
+
A native ``compile_model`` cannot be interrupted from Python, so the
|
|
91
|
+
harness watches it from another thread. An adapter brackets each compile
|
|
92
|
+
with :meth:`compiling`; the watcher reads :meth:`active`. Thread-safe. A
|
|
93
|
+
compile that never returns stays ``active`` until the process ends.
|
|
94
|
+
"""
|
|
95
|
+
|
|
96
|
+
def __init__(self, clock=None) -> None:
|
|
97
|
+
self.clock = clock or time.monotonic
|
|
98
|
+
self._lock = threading.Lock()
|
|
99
|
+
self._active: tuple[str, float] | None = None
|
|
100
|
+
|
|
101
|
+
@contextmanager
|
|
102
|
+
def compiling(self, label: str):
|
|
103
|
+
with self._lock:
|
|
104
|
+
self._active = (label, self.clock())
|
|
105
|
+
try:
|
|
106
|
+
yield
|
|
107
|
+
finally:
|
|
108
|
+
with self._lock:
|
|
109
|
+
self._active = None
|
|
110
|
+
|
|
111
|
+
def active(self) -> tuple[str, float] | None:
|
|
112
|
+
"""``(label, started)`` of the running compile, or ``None``."""
|
|
113
|
+
|
|
114
|
+
with self._lock:
|
|
115
|
+
return self._active
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
@dataclass(frozen=True)
|
|
119
|
+
class StepRequest:
|
|
120
|
+
step_id: str
|
|
121
|
+
task: str
|
|
122
|
+
model: str
|
|
123
|
+
device: str
|
|
124
|
+
engine: str
|
|
125
|
+
settings: dict
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
@dataclass(frozen=True)
|
|
129
|
+
class StepResult:
|
|
130
|
+
"""One measured step.
|
|
131
|
+
|
|
132
|
+
``latency_ms`` is the step latency and does not include compile time.
|
|
133
|
+
``compile_ms`` and ``first_call_ms`` are reported on a cold step, separate
|
|
134
|
+
from ``latency_ms``. ``throughput`` for a generate step is server-reported
|
|
135
|
+
decode (``eval_count / eval_duration``), not wall clock. ``prompt_tok_s``
|
|
136
|
+
is ``prompt_eval_count / prompt_eval_duration``. ``prompt_eval_duration_ns``
|
|
137
|
+
is that duration; fill-card reports it as time to first token, because
|
|
138
|
+
Ollama does not send a separate one. ``output`` is the measured
|
|
139
|
+
vectors or the generation. ``resolved`` carries values learned at run time,
|
|
140
|
+
such as ``num_ctx`` and ``quant``. ``sensors`` may include ``gpu_temp_c``,
|
|
141
|
+
``sm_clock_mhz``, ``power_w``, ``shared_mib`` and ``dedicated_mib``.
|
|
142
|
+
"""
|
|
143
|
+
|
|
144
|
+
ok: bool
|
|
145
|
+
latency_ms: float | None = None
|
|
146
|
+
throughput: float | None = None
|
|
147
|
+
throughput_unit: str | None = None
|
|
148
|
+
error: str | None = None
|
|
149
|
+
compile_ms: float | None = None
|
|
150
|
+
first_call_ms: float | None = None
|
|
151
|
+
prompt_tok_s: float | None = None
|
|
152
|
+
resolved: dict | None = None
|
|
153
|
+
output: object | None = None
|
|
154
|
+
sensors: dict | None = None
|
|
155
|
+
prompt_eval_duration_ns: int | None = None
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
class Adapter(Protocol):
|
|
159
|
+
def run(self, request: StepRequest) -> StepResult:
|
|
160
|
+
"""Measure one step. A failed measurement returns ``ok`` false."""
|
|
161
|
+
|
|
162
|
+
def unload(self, model: str, device: str | None = None) -> None:
|
|
163
|
+
"""Drop the model. A real Ollama unload uses keep_alive 0.
|
|
164
|
+
|
|
165
|
+
``device`` selects the endpoint. An adapter that serves more than one
|
|
166
|
+
device must not unload through a stored default.
|
|
167
|
+
"""
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"""Plan an ``offrig verify calibrate`` invocation.
|
|
2
|
+
|
|
3
|
+
The command is the one at offrig ``751fb1d`` (``crates/offrig-cli/src/verify_cmd.rs``,
|
|
4
|
+
``CalibrateArgs``). ``--project`` is a scratch directory, because opening a project
|
|
5
|
+
migrates its store. The URL stays on loopback. ``gpu_cost_hr`` stays 0.
|
|
6
|
+
|
|
7
|
+
Nothing here launches offrig. A caller that wants a real run passes an executor.
|
|
8
|
+
Dry-run and the tests do not.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from interlock.adapters.base import AdapterError, StepRequest, StepResult
|
|
14
|
+
|
|
15
|
+
SCRATCH_ROOT = "E:/AI/switchyard-scratch"
|
|
16
|
+
LOOPBACK = frozenset({"http://127.0.0.1:11434", "http://127.0.0.1:11490"})
|
|
17
|
+
THINKING = frozenset({"off", "on", "low", "medium", "high"})
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def scratch_project(experiment_id: str) -> str:
|
|
21
|
+
if not isinstance(experiment_id, str) or not experiment_id.strip():
|
|
22
|
+
raise AdapterError("a scratch project needs an experiment id")
|
|
23
|
+
name = experiment_id.strip()
|
|
24
|
+
if any(part in name for part in ("/", "\\", "..")) or name.startswith("."):
|
|
25
|
+
raise AdapterError(f"{experiment_id} is not a single scratch name")
|
|
26
|
+
return f"{SCRATCH_ROOT}/{name}"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def plan_argv(spec: dict) -> list[str]:
|
|
30
|
+
"""Argv for ``offrig verify calibrate``. This does not run it."""
|
|
31
|
+
|
|
32
|
+
model = spec.get("model")
|
|
33
|
+
if not isinstance(model, str) or not model:
|
|
34
|
+
raise AdapterError("calibrate needs a model")
|
|
35
|
+
lowered = model.lower()
|
|
36
|
+
if ":cloud" in lowered or lowered.endswith("-cloud"):
|
|
37
|
+
raise AdapterError(f"cloud model tags are refused ({model})")
|
|
38
|
+
url = spec.get("url", "http://127.0.0.1:11434")
|
|
39
|
+
if url not in LOOPBACK:
|
|
40
|
+
raise AdapterError("calibrate stays on loopback Ollama")
|
|
41
|
+
if spec.get("gpu_cost_hr", 0) != 0:
|
|
42
|
+
raise AdapterError("gpu_cost_hr must be 0; this adapter does not price a rental")
|
|
43
|
+
gold = spec.get("gold")
|
|
44
|
+
if not isinstance(gold, str) or not gold or ".." in gold.replace("\\", "/").split("/"):
|
|
45
|
+
raise AdapterError("calibrate needs a gold path inside the repo")
|
|
46
|
+
think = spec.get("think", "off")
|
|
47
|
+
if think not in THINKING:
|
|
48
|
+
raise AdapterError(f"think {think!r} is not one offrig accepts")
|
|
49
|
+
num_predict = spec.get("num_predict", 128)
|
|
50
|
+
if not isinstance(num_predict, int) or num_predict < 1:
|
|
51
|
+
raise AdapterError("num_predict must be a positive integer")
|
|
52
|
+
return [
|
|
53
|
+
"offrig",
|
|
54
|
+
"verify",
|
|
55
|
+
"calibrate",
|
|
56
|
+
"--project",
|
|
57
|
+
scratch_project(spec["id"]),
|
|
58
|
+
"--model",
|
|
59
|
+
model,
|
|
60
|
+
"--url",
|
|
61
|
+
url,
|
|
62
|
+
"--think",
|
|
63
|
+
think,
|
|
64
|
+
"--gpu-cost-hr",
|
|
65
|
+
"0",
|
|
66
|
+
"--num-predict",
|
|
67
|
+
str(num_predict),
|
|
68
|
+
gold,
|
|
69
|
+
]
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class OffrigCalibrateAdapter:
|
|
73
|
+
"""Builds the argv. Launches only when an executor was passed in."""
|
|
74
|
+
|
|
75
|
+
def __init__(self, *, executor=None):
|
|
76
|
+
self.executor = executor
|
|
77
|
+
|
|
78
|
+
def run(self, request: StepRequest) -> StepResult:
|
|
79
|
+
spec = dict(request.settings)
|
|
80
|
+
spec.setdefault("id", request.step_id)
|
|
81
|
+
spec.setdefault("model", request.model)
|
|
82
|
+
argv = plan_argv(spec)
|
|
83
|
+
if self.executor is None:
|
|
84
|
+
raise AdapterError("offrig verify calibrate is not launched without an executor")
|
|
85
|
+
code = self.executor(argv)
|
|
86
|
+
if code != 0:
|
|
87
|
+
return StepResult(ok=False, error=f"offrig exited {code}")
|
|
88
|
+
return StepResult(ok=True)
|
|
89
|
+
|
|
90
|
+
def unload(self, model: str, device: str | None = None) -> None:
|
|
91
|
+
return None
|