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.
Files changed (57) hide show
  1. interlock/__init__.py +4 -0
  2. interlock/__main__.py +4 -0
  3. interlock/adapters/__init__.py +1 -0
  4. interlock/adapters/base.py +167 -0
  5. interlock/adapters/offrig_calibrate.py +91 -0
  6. interlock/adapters/ollama.py +559 -0
  7. interlock/adapters/ollama_cases.py +313 -0
  8. interlock/adapters/openvino.py +1289 -0
  9. interlock/adapters/record_fixtures.py +403 -0
  10. interlock/adapters/registry.py +37 -0
  11. interlock/cli.py +468 -0
  12. interlock/devices.py +201 -0
  13. interlock/families.py +53 -0
  14. interlock/guards.py +96 -0
  15. interlock/harness/__init__.py +1 -0
  16. interlock/harness/choice.py +85 -0
  17. interlock/harness/count_budget.py +100 -0
  18. interlock/harness/e4b.py +2620 -0
  19. interlock/harness/e4b_stats.py +213 -0
  20. interlock/harness/e5.py +560 -0
  21. interlock/harness/environment.py +161 -0
  22. interlock/harness/fill.py +2597 -0
  23. interlock/harness/lifecycle.py +307 -0
  24. interlock/harness/lock.py +41 -0
  25. interlock/harness/marker.py +113 -0
  26. interlock/harness/ollama_serve.py +364 -0
  27. interlock/harness/receipts.py +411 -0
  28. interlock/harness/runner.py +2221 -0
  29. interlock/harness/spec.py +469 -0
  30. interlock/harness/steady.py +230 -0
  31. interlock/harness/storage.py +166 -0
  32. interlock/harness/timing.py +175 -0
  33. interlock/harness/watch.py +756 -0
  34. interlock/importers/__init__.py +1 -0
  35. interlock/importers/aspire_judges.py +249 -0
  36. interlock/importers/assemble.py +303 -0
  37. interlock/importers/calibration.py +257 -0
  38. interlock/importers/common.py +142 -0
  39. interlock/importers/gaps.py +242 -0
  40. interlock/importers/ladder.py +109 -0
  41. interlock/importers/nli.py +103 -0
  42. interlock/importers/offers.py +131 -0
  43. interlock/importers/pins.py +32 -0
  44. interlock/importers/probe.py +158 -0
  45. interlock/ollama_supervisor.py +1715 -0
  46. interlock/receipt.py +339 -0
  47. interlock/router/__init__.py +5 -0
  48. interlock/router/policy.py +785 -0
  49. interlock/schema/route-decision.v1.json +101 -0
  50. interlock/schema/routing-row.schema.json +184 -0
  51. interlock/scope.py +52 -0
  52. interlock/tasks.py +41 -0
  53. mcptoolshop_interlock-0.1.0.dist-info/METADATA +211 -0
  54. mcptoolshop_interlock-0.1.0.dist-info/RECORD +57 -0
  55. mcptoolshop_interlock-0.1.0.dist-info/WHEEL +4 -0
  56. mcptoolshop_interlock-0.1.0.dist-info/entry_points.txt +2 -0
  57. mcptoolshop_interlock-0.1.0.dist-info/licenses/LICENSE +21 -0
interlock/__init__.py ADDED
@@ -0,0 +1,4 @@
1
+ """interlock routes a job to a model, an engine and a device, from receipts."""
2
+
3
+ __version__ = "0.1.0"
4
+ __status__ = "prototype"
interlock/__main__.py ADDED
@@ -0,0 +1,4 @@
1
+ from interlock.cli import main
2
+
3
+ if __name__ == "__main__":
4
+ raise SystemExit(main())
@@ -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