chipwright 0.2.4__tar.gz → 0.2.6__tar.gz
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.
- {chipwright-0.2.4 → chipwright-0.2.6}/PKG-INFO +1 -1
- {chipwright-0.2.4 → chipwright-0.2.6}/chipwright/__init__.py +1 -1
- {chipwright-0.2.4 → chipwright-0.2.6}/chipwright/cli.py +21 -6
- {chipwright-0.2.4 → chipwright-0.2.6}/chipwright/data/index.yaml +24 -0
- {chipwright-0.2.4 → chipwright-0.2.6}/chipwright/recipe.py +54 -1
- {chipwright-0.2.4 → chipwright-0.2.6}/chipwright/registry.py +2 -0
- {chipwright-0.2.4 → chipwright-0.2.6}/chipwright/resolver.py +134 -6
- chipwright-0.2.6/chipwright/tests/test_precision_registry.py +68 -0
- chipwright-0.2.6/chipwright/tests/test_run_render.py +47 -0
- chipwright-0.2.6/chipwright/tests/test_runtime_support.py +261 -0
- {chipwright-0.2.4 → chipwright-0.2.6}/chipwright.egg-info/PKG-INFO +1 -1
- {chipwright-0.2.4 → chipwright-0.2.6}/chipwright.egg-info/SOURCES.txt +3 -0
- {chipwright-0.2.4 → chipwright-0.2.6}/pyproject.toml +1 -1
- {chipwright-0.2.4 → chipwright-0.2.6}/LICENSE +0 -0
- {chipwright-0.2.4 → chipwright-0.2.6}/README.md +0 -0
- {chipwright-0.2.4 → chipwright-0.2.6}/chipwright/adapt.py +0 -0
- {chipwright-0.2.4 → chipwright-0.2.6}/chipwright/build.py +0 -0
- {chipwright-0.2.4 → chipwright-0.2.6}/chipwright/cache.py +0 -0
- {chipwright-0.2.4 → chipwright-0.2.6}/chipwright/config.py +0 -0
- {chipwright-0.2.4 → chipwright-0.2.6}/chipwright/data/behavior_prompts.yaml +0 -0
- {chipwright-0.2.4 → chipwright-0.2.6}/chipwright/device.py +0 -0
- {chipwright-0.2.4 → chipwright-0.2.6}/chipwright/device_adb.py +0 -0
- {chipwright-0.2.4 → chipwright-0.2.6}/chipwright/env.py +0 -0
- {chipwright-0.2.4 → chipwright-0.2.6}/chipwright/errors.py +0 -0
- {chipwright-0.2.4 → chipwright-0.2.6}/chipwright/jsonout.py +0 -0
- {chipwright-0.2.4 → chipwright-0.2.6}/chipwright/llm_reference.py +0 -0
- {chipwright-0.2.4 → chipwright-0.2.6}/chipwright/modalities/__init__.py +0 -0
- {chipwright-0.2.4 → chipwright-0.2.6}/chipwright/modalities/asr_encoder.py +0 -0
- {chipwright-0.2.4 → chipwright-0.2.6}/chipwright/modalities/cv.py +0 -0
- {chipwright-0.2.4 → chipwright-0.2.6}/chipwright/modalities/llm.py +0 -0
- {chipwright-0.2.4 → chipwright-0.2.6}/chipwright/replay.py +0 -0
- {chipwright-0.2.4 → chipwright-0.2.6}/chipwright/run.py +0 -0
- {chipwright-0.2.4 → chipwright-0.2.6}/chipwright/sdk.py +0 -0
- {chipwright-0.2.4 → chipwright-0.2.6}/chipwright/tags.py +0 -0
- {chipwright-0.2.4 → chipwright-0.2.6}/chipwright/task_eval.py +0 -0
- {chipwright-0.2.4 → chipwright-0.2.6}/chipwright/tests/conftest.py +0 -0
- {chipwright-0.2.4 → chipwright-0.2.6}/chipwright/tests/test_adapt.py +0 -0
- {chipwright-0.2.4 → chipwright-0.2.6}/chipwright/tests/test_build.py +0 -0
- {chipwright-0.2.4 → chipwright-0.2.6}/chipwright/tests/test_cache.py +0 -0
- {chipwright-0.2.4 → chipwright-0.2.6}/chipwright/tests/test_cli_ux.py +0 -0
- {chipwright-0.2.4 → chipwright-0.2.6}/chipwright/tests/test_device_adb.py +0 -0
- {chipwright-0.2.4 → chipwright-0.2.6}/chipwright/tests/test_device_ctx.py +0 -0
- {chipwright-0.2.4 → chipwright-0.2.6}/chipwright/tests/test_emit_record.py +0 -0
- {chipwright-0.2.4 → chipwright-0.2.6}/chipwright/tests/test_env.py +0 -0
- {chipwright-0.2.4 → chipwright-0.2.6}/chipwright/tests/test_env_adb.py +0 -0
- {chipwright-0.2.4 → chipwright-0.2.6}/chipwright/tests/test_json_cli.py +0 -0
- {chipwright-0.2.4 → chipwright-0.2.6}/chipwright/tests/test_llm_modality.py +0 -0
- {chipwright-0.2.4 → chipwright-0.2.6}/chipwright/tests/test_packaging.py +0 -0
- {chipwright-0.2.4 → chipwright-0.2.6}/chipwright/tests/test_precision_support.py +0 -0
- {chipwright-0.2.4 → chipwright-0.2.6}/chipwright/tests/test_registry.py +0 -0
- {chipwright-0.2.4 → chipwright-0.2.6}/chipwright/tests/test_replay.py +0 -0
- {chipwright-0.2.4 → chipwright-0.2.6}/chipwright/tests/test_resolver.py +0 -0
- {chipwright-0.2.4 → chipwright-0.2.6}/chipwright/tests/test_run_llm.py +0 -0
- {chipwright-0.2.4 → chipwright-0.2.6}/chipwright/tests/test_security.py +0 -0
- {chipwright-0.2.4 → chipwright-0.2.6}/chipwright/tests/test_tags.py +0 -0
- {chipwright-0.2.4 → chipwright-0.2.6}/chipwright/tests/test_task_eval.py +0 -0
- {chipwright-0.2.4 → chipwright-0.2.6}/chipwright/tests/test_verify.py +0 -0
- {chipwright-0.2.4 → chipwright-0.2.6}/chipwright/tests/test_verify_llm.py +0 -0
- {chipwright-0.2.4 → chipwright-0.2.6}/chipwright/verify.py +0 -0
- {chipwright-0.2.4 → chipwright-0.2.6}/chipwright/verify_llm.py +0 -0
- {chipwright-0.2.4 → chipwright-0.2.6}/chipwright.egg-info/dependency_links.txt +0 -0
- {chipwright-0.2.4 → chipwright-0.2.6}/chipwright.egg-info/entry_points.txt +0 -0
- {chipwright-0.2.4 → chipwright-0.2.6}/chipwright.egg-info/requires.txt +0 -0
- {chipwright-0.2.4 → chipwright-0.2.6}/chipwright.egg-info/top_level.txt +0 -0
- {chipwright-0.2.4 → chipwright-0.2.6}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: chipwright
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.6
|
|
4
4
|
Summary: A verified package manager for Qualcomm NPUs — resolve the artifact that loads on your exact chip, run it, and prove it against a CPU reference.
|
|
5
5
|
License-Expression: MIT
|
|
6
6
|
Project-URL: Homepage, https://github.com/nameissakthi25/chipwright
|
|
@@ -303,6 +303,26 @@ def cmd_verify(args):
|
|
|
303
303
|
return _replay(reg, args.model)
|
|
304
304
|
|
|
305
305
|
|
|
306
|
+
def _render_run_report(rep, d):
|
|
307
|
+
"""Print a run's verification report (human door) and return its exit code. Shape-safe: the LLM
|
|
308
|
+
generative gate reports a `tripwire` (token-match), CV/ASR a cosine — the human door must not diverge
|
|
309
|
+
from the already-shape-safe `--json` projection (Principle IV, and never a KeyError traceback)."""
|
|
310
|
+
mark = f"{GREEN}✔ VERIFIED{RST}" if rep["pass"] else f"{RED}✗ MISMATCH{RST}"
|
|
311
|
+
if "tripwire" in rep: # LLM generative gate (token-match), not a cosine
|
|
312
|
+
tw = rep["tripwire"]
|
|
313
|
+
thr = (tw.get("thresholds") or {}).get("token_match")
|
|
314
|
+
print(f"\n{mark} generative gate token_match = {float(tw.get('token_match', 0.0)):.5f}"
|
|
315
|
+
+ (f" (threshold {thr})" if thr is not None else ""))
|
|
316
|
+
if rep.get("behavior"):
|
|
317
|
+
print(f" behavioral probe: {rep['behavior']}")
|
|
318
|
+
else:
|
|
319
|
+
print(f"\n{mark} NPU vs CPU-reference cosine_min = {rep['cosine_min']:.5f} "
|
|
320
|
+
f"(threshold {rep['threshold']})")
|
|
321
|
+
for name, c in rep["per_output"].items():
|
|
322
|
+
print(f" {name}: {c:.5f}")
|
|
323
|
+
return jsonout.exit_code_for(d) if rep["pass"] else jsonout.EXIT_VERIFY_MISMATCH
|
|
324
|
+
|
|
325
|
+
|
|
306
326
|
def cmd_run(args):
|
|
307
327
|
reg = Registry(args.registry)
|
|
308
328
|
if args.model not in reg.models():
|
|
@@ -333,12 +353,7 @@ def cmd_run(args):
|
|
|
333
353
|
except Exception as e:
|
|
334
354
|
print(f"{RED}run failed:{RST} {e}")
|
|
335
355
|
return jsonout.EXIT_UNEXPECTED
|
|
336
|
-
|
|
337
|
-
print(f"\n{mark} NPU vs CPU-reference cosine_min = {rep['cosine_min']:.5f} "
|
|
338
|
-
f"(threshold {rep['threshold']})")
|
|
339
|
-
for name, c in rep["per_output"].items():
|
|
340
|
-
print(f" {name}: {c:.5f}")
|
|
341
|
-
return jsonout.exit_code_for(d) if rep["pass"] else jsonout.EXIT_VERIFY_MISMATCH
|
|
356
|
+
return _render_run_report(rep, d)
|
|
342
357
|
|
|
343
358
|
|
|
344
359
|
def _run_json(args, reg, target) -> int:
|
|
@@ -242,6 +242,12 @@ models:
|
|
|
242
242
|
metric: cosine_min
|
|
243
243
|
value: 0.9999
|
|
244
244
|
note: 4x super-resolution; w8a8 upscaled image is near-lossless vs float
|
|
245
|
+
npu_output:
|
|
246
|
+
- name: upscaled_image
|
|
247
|
+
url: https://huggingface.co/nameissakthi/chipwright-cv/resolve/main/quicksrnet-small/npu_upscaled_image.npy
|
|
248
|
+
sha256: 8a4f01ec0b69e815cca4ef75bf50db87b3922dca2eb921f0328286516339c293
|
|
249
|
+
dtype: float32
|
|
250
|
+
shape: [1, 3, 512, 512]
|
|
245
251
|
latency_ms:
|
|
246
252
|
note: 128x128 -> 512x512; DLC compiled to context on-board
|
|
247
253
|
- model: fcn-resnet50
|
|
@@ -280,6 +286,12 @@ models:
|
|
|
280
286
|
metric: cosine_min
|
|
281
287
|
value: 0.998
|
|
282
288
|
note: per-pixel semantic mask; w8a8 near-lossless vs float
|
|
289
|
+
npu_output:
|
|
290
|
+
- name: mask
|
|
291
|
+
url: https://huggingface.co/nameissakthi/chipwright-cv/resolve/main/fcn-resnet50/npu_mask.npy
|
|
292
|
+
sha256: 39e8e3b0dc4ea9f81037242bc403755c16b9c08914652092e5028b1807b9b1dc
|
|
293
|
+
dtype: float32
|
|
294
|
+
shape: [1, 520, 520]
|
|
283
295
|
latency_ms:
|
|
284
296
|
note: 520x520; DLC compiled to context on-board (spills to DDR)
|
|
285
297
|
- model: mobilenet-cv-built
|
|
@@ -346,6 +358,12 @@ models:
|
|
|
346
358
|
backend: onnxruntime-cpu
|
|
347
359
|
metric: depth_cosine
|
|
348
360
|
value: 0.99996
|
|
361
|
+
npu_output:
|
|
362
|
+
- name: depth_estimates
|
|
363
|
+
url: https://huggingface.co/nameissakthi/chipwright-cv/resolve/main/depth-anything-v2/npu_depth_estimates.npy
|
|
364
|
+
sha256: 2ad552242a1152056d358b6b80a6967862c132a240d142fe59916051110dd8f8
|
|
365
|
+
dtype: float32
|
|
366
|
+
shape: [1, 1, 518, 518]
|
|
349
367
|
- model: movenet
|
|
350
368
|
source: qualcomm/MoveNet
|
|
351
369
|
license: apache-2.0
|
|
@@ -381,3 +399,9 @@ models:
|
|
|
381
399
|
backend: onnxruntime-cpu
|
|
382
400
|
metric: keypoint_cosine
|
|
383
401
|
value: 0.91607
|
|
402
|
+
npu_output:
|
|
403
|
+
- name: kpt_with_conf
|
|
404
|
+
url: https://huggingface.co/nameissakthi/chipwright-cv/resolve/main/movenet/npu_kpt_with_conf.npy
|
|
405
|
+
sha256: 37148303f83b4acbb1fee4de088921f33259d5fea7ab58b06703b28fccf39bca
|
|
406
|
+
dtype: float32
|
|
407
|
+
shape: [1, 1, 17, 3]
|
|
@@ -23,6 +23,13 @@ RUNS_ON = ("npu", "host")
|
|
|
23
23
|
# through to the resolver's refusal verbatim.
|
|
24
24
|
_PRECISION_KINDS = ("supported", "blocked", "unsupported")
|
|
25
25
|
|
|
26
|
+
# Runtime-support statuses a recipe may declare (spec 004, FR-RT04). The runtime axis is the precision
|
|
27
|
+
# axis a second time — the same {key: "supported" | "<kind>:<reason>"} matrix the resolver already refuses
|
|
28
|
+
# on — only one level deeper, because a runtime's support is *per arch*: `{runtime: {arch: status}}`. A
|
|
29
|
+
# status is the bare word "supported", or "deprecated:<successor>" (runnable, but deprecating upstream —
|
|
30
|
+
# resolve warns), or "unsupported:<reason>" (the board's arch cannot host it — resolve refuses by name).
|
|
31
|
+
_RUNTIME_KINDS = ("supported", "deprecated", "unsupported")
|
|
32
|
+
|
|
26
33
|
|
|
27
34
|
@dataclass
|
|
28
35
|
class Source:
|
|
@@ -86,6 +93,7 @@ class Recipe:
|
|
|
86
93
|
host: Host = field(default_factory=Host)
|
|
87
94
|
graphs: List[Graph] = field(default_factory=list)
|
|
88
95
|
precision_support: Dict[str, str] = field(default_factory=dict) # {precision: status} — LLM, FR-L06
|
|
96
|
+
runtime_support: Dict[str, Dict[str, str]] = field(default_factory=dict) # {runtime: {arch: status}} — FR-RT04
|
|
89
97
|
|
|
90
98
|
@property
|
|
91
99
|
def quant_tag(self) -> str:
|
|
@@ -101,6 +109,15 @@ class Recipe:
|
|
|
101
109
|
"""The precisions this recipe declares runnable through the pipeline."""
|
|
102
110
|
return [p for p, s in self.precision_support.items() if s == "supported"]
|
|
103
111
|
|
|
112
|
+
def runtime_status(self, runtime: str, arch: str) -> Optional[str]:
|
|
113
|
+
"""The declared status for `runtime` on `arch` ('supported'/'deprecated:<s>'/'unsupported:<r>'), or None."""
|
|
114
|
+
return self.runtime_support.get(runtime, {}).get(arch)
|
|
115
|
+
|
|
116
|
+
def runnable_runtimes(self, arch: str) -> List[str]:
|
|
117
|
+
"""The runtimes this recipe declares runnable on `arch` — 'supported' OR 'deprecated' (not 'unsupported')."""
|
|
118
|
+
return sorted(rt for rt, am in self.runtime_support.items()
|
|
119
|
+
if str(am.get(arch, "")).split(":", 1)[0] in ("supported", "deprecated"))
|
|
120
|
+
|
|
104
121
|
def adaptation(self, kind: str) -> Optional[Adaptation]:
|
|
105
122
|
"""The declared adaptation of a given kind, or None (e.g. recipe.adaptation('erf_to_tanh'))."""
|
|
106
123
|
return next((a for a in self.adaptations if a.kind == kind), None)
|
|
@@ -153,6 +170,41 @@ def _parse_precision_support(items: Any) -> Dict[str, str]:
|
|
|
153
170
|
return out
|
|
154
171
|
|
|
155
172
|
|
|
173
|
+
def _parse_runtime_support(items: Any) -> Dict[str, Dict[str, str]]:
|
|
174
|
+
"""Parse + validate `runtime_support:` — a nested map {runtime: {arch: status}}. Raises ValueError if malformed.
|
|
175
|
+
|
|
176
|
+
A near-clone of `_parse_precision_support`, one level deeper (runtime -> arch -> status) because a
|
|
177
|
+
runtime's support is per-arch. A status must be "supported" or "<deprecated|unsupported>:<tail>" with a
|
|
178
|
+
non-empty tail (the successor for `deprecated`, the reason for `unsupported`) — so a recipe cannot smuggle
|
|
179
|
+
in a vague "no": a limitation is named or it is not a valid recipe (the same strictness as the tag grammar).
|
|
180
|
+
"""
|
|
181
|
+
if not items:
|
|
182
|
+
return {}
|
|
183
|
+
if not isinstance(items, dict):
|
|
184
|
+
raise ValueError(f"runtime_support must be a mapping {{runtime: {{arch: status}}}}, got {type(items).__name__}")
|
|
185
|
+
out: Dict[str, Dict[str, str]] = {}
|
|
186
|
+
for runtime, archmap in items.items():
|
|
187
|
+
if not isinstance(archmap, dict):
|
|
188
|
+
raise ValueError(
|
|
189
|
+
f"runtime_support[{runtime!r}] must be a mapping {{arch: status}}, got {type(archmap).__name__}")
|
|
190
|
+
per_arch: Dict[str, str] = {}
|
|
191
|
+
for arch, status in archmap.items():
|
|
192
|
+
status = str(status)
|
|
193
|
+
head = status.split(":", 1)[0]
|
|
194
|
+
if head not in _RUNTIME_KINDS:
|
|
195
|
+
raise ValueError(
|
|
196
|
+
f"runtime_support[{runtime!r}][{arch!r}] = {status!r}: status must start with one of "
|
|
197
|
+
f"{_RUNTIME_KINDS} (e.g. 'supported' or 'unsupported:htp-backend-excludes-v68')")
|
|
198
|
+
if head != "supported" and (":" not in status or not status.split(":", 1)[1].strip()):
|
|
199
|
+
raise ValueError(
|
|
200
|
+
f"runtime_support[{runtime!r}][{arch!r}] = {status!r}: a {head!r} status must name a "
|
|
201
|
+
f"reason/successor after the colon (e.g. 'deprecated:geniex-qairt' or "
|
|
202
|
+
f"'unsupported:htp-backend-excludes-v68'); only 'supported' stands alone")
|
|
203
|
+
per_arch[str(arch)] = status
|
|
204
|
+
out[str(runtime)] = per_arch
|
|
205
|
+
return out
|
|
206
|
+
|
|
207
|
+
|
|
156
208
|
def from_dict(doc: Dict[str, Any]) -> Recipe:
|
|
157
209
|
"""Build a Recipe from an already-parsed YAML mapping. Pure — no I/O. Raises ValueError if malformed."""
|
|
158
210
|
if not isinstance(doc, dict):
|
|
@@ -195,7 +247,8 @@ def from_dict(doc: Dict[str, Any]) -> Recipe:
|
|
|
195
247
|
return Recipe(name=str(doc["name"]), version=str(doc["version"]), route=route,
|
|
196
248
|
source=source, target=target, adaptations=_parse_adaptations(doc.get("adaptations")),
|
|
197
249
|
quant=quant, host=host, graphs=graphs,
|
|
198
|
-
precision_support=_parse_precision_support(doc.get("precision_support"))
|
|
250
|
+
precision_support=_parse_precision_support(doc.get("precision_support")),
|
|
251
|
+
runtime_support=_parse_runtime_support(doc.get("runtime_support")))
|
|
199
252
|
|
|
200
253
|
|
|
201
254
|
def load(path: str) -> Recipe:
|
|
@@ -57,6 +57,8 @@ class Registry:
|
|
|
57
57
|
url=v.get("url"), sha256=v.get("sha256"),
|
|
58
58
|
local=v.get("local"), run=v.get("run"),
|
|
59
59
|
recipe=v.get("recipe"), verified=v.get("verified"),
|
|
60
|
+
runtime=v.get("runtime"), runtime_support=v.get("runtime_support"),
|
|
61
|
+
precision_support=v.get("precision_support"),
|
|
60
62
|
))
|
|
61
63
|
self._models[model] = vs
|
|
62
64
|
|
|
@@ -44,11 +44,19 @@ class Variant:
|
|
|
44
44
|
recipe: Optional[str] = None # present => can be built when no artifact matches
|
|
45
45
|
verified: Optional[dict] = None # the verification record (loads / cosine / task / latency)
|
|
46
46
|
precision_support: Optional[dict] = None # LLM: {precision: "supported"|"blocked:<r>"|"unsupported:<r>"}
|
|
47
|
+
runtime: Optional[str] = None # the inference runtime this variant binds to (None => classic genie)
|
|
48
|
+
runtime_support: Optional[dict] = None # {runtime: {arch: "supported"|"deprecated:<s>"|"unsupported:<r>"}} — FR-RT04
|
|
47
49
|
|
|
48
50
|
@property
|
|
49
51
|
def has_artifact(self) -> bool:
|
|
50
52
|
return bool(self.url or self.local)
|
|
51
53
|
|
|
54
|
+
@property
|
|
55
|
+
def effective_runtime(self) -> str:
|
|
56
|
+
"""The runtime this variant runs on. Historically every variant silently assumed classic Genie
|
|
57
|
+
(spec 004), so an unset runtime IS `genie` — the deprecating-but-default floor."""
|
|
58
|
+
return self.runtime or "genie"
|
|
59
|
+
|
|
52
60
|
@property
|
|
53
61
|
def has_recipe(self) -> bool:
|
|
54
62
|
return bool(self.recipe)
|
|
@@ -91,6 +99,80 @@ def precision_refusal(precision: Optional[str], support: Optional[dict]) -> Opti
|
|
|
91
99
|
return str(status)
|
|
92
100
|
|
|
93
101
|
|
|
102
|
+
# --- runtime axis (spec 004) ------------------------------------------------------------------
|
|
103
|
+
# The same shape as precision, one axis over. A model+board can be served by different inference runtimes
|
|
104
|
+
# ({genie, geniex-qairt, llamacpp-htp}) with genuinely different arch + precision support (classic Genie is
|
|
105
|
+
# w4a16-only and deprecating; the llama.cpp HTP backend does Q4_0/Q8_0 on v73+ but NOT v68). So `resolve`
|
|
106
|
+
# must (a) refuse a runtime the board's arch cannot host, BY NAME (`axis=runtime`), and (b) among the
|
|
107
|
+
# runtimes the board CAN host, pick the one matching the caller's priority. The support table is per-arch:
|
|
108
|
+
# `{runtime: {arch: status}}`, validated in recipe.py (`_parse_runtime_support`).
|
|
109
|
+
#
|
|
110
|
+
# Priority -> the order runtimes are preferred in, among those runnable on the board's arch. Grounded in the
|
|
111
|
+
# v73 measurement: llamacpp-htp Q8_0 is the quality/prefill win (WikiText PPL 11.66, near-lossless, the
|
|
112
|
+
# >4-bit integer route Genie structurally lacks) but regresses decode (14.4 vs 22.5 tok/s); genie w4a16 is
|
|
113
|
+
# the decode win and the default floor. So quality-first prefers llamacpp-htp, decode-latency-first prefers
|
|
114
|
+
# genie, and the no-priority default is the conservative floor (genie) with a warn to choose explicitly.
|
|
115
|
+
_RUNTIME_PRIORITY = {
|
|
116
|
+
"quality": ("llamacpp-htp", "geniex-qairt", "genie"),
|
|
117
|
+
"decode-latency": ("genie", "geniex-qairt", "llamacpp-htp"),
|
|
118
|
+
}
|
|
119
|
+
_DEFAULT_RUNTIME_ORDER = ("genie", "geniex-qairt", "llamacpp-htp")
|
|
120
|
+
_RUNNABLE = ("supported", "deprecated") # 'unsupported' is the only hard refusal; 'deprecated' still runs
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def runtime_refusal(runtime: Optional[str], arch: str, support: Optional[dict]) -> Optional[str]:
|
|
124
|
+
"""Return the named reason to refuse `runtime` on `arch`, or None if it is runnable / not constrained.
|
|
125
|
+
|
|
126
|
+
Pure and tiny, mirroring `precision_refusal`. A runtime absent from the matrix (or an arch the matrix
|
|
127
|
+
doesn't declare for it) is *not* refused here — the matrix only constrains what it declares. Only an
|
|
128
|
+
explicit `unsupported:<reason>` is a refusal; a `deprecated:<successor>` status still runs (resolve warns).
|
|
129
|
+
"""
|
|
130
|
+
if not runtime or not support:
|
|
131
|
+
return None
|
|
132
|
+
archmap = support.get(runtime)
|
|
133
|
+
if not isinstance(archmap, dict):
|
|
134
|
+
return None
|
|
135
|
+
status = archmap.get(arch)
|
|
136
|
+
if status is None or str(status).split(":", 1)[0] != "unsupported":
|
|
137
|
+
return None
|
|
138
|
+
return str(status)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def runnable_runtimes(arch: str, support: Optional[dict]) -> List[str]:
|
|
142
|
+
"""The runtimes the matrix declares runnable on `arch` (status 'supported' or 'deprecated')."""
|
|
143
|
+
if not support:
|
|
144
|
+
return []
|
|
145
|
+
return sorted(rt for rt, am in support.items()
|
|
146
|
+
if isinstance(am, dict) and str(am.get(arch, "")).split(":", 1)[0] in _RUNNABLE)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def pick_runtime(runtimes: List[str], priority: Optional[str]):
|
|
150
|
+
"""Choose one runtime from `runtimes` (all runnable on the board's arch) by `priority`.
|
|
151
|
+
|
|
152
|
+
Returns (chosen, note). `note` records the trade-off (which runtime was chosen over which) so the
|
|
153
|
+
Decision can carry it — tested-is-a-record, now across runtimes.
|
|
154
|
+
"""
|
|
155
|
+
order = _RUNTIME_PRIORITY.get(priority)
|
|
156
|
+
if order:
|
|
157
|
+
for rt in order:
|
|
158
|
+
if rt in runtimes:
|
|
159
|
+
others = [r for r in runtimes if r != rt]
|
|
160
|
+
return rt, f"priority={priority}: chose runtime {rt} over {others}"
|
|
161
|
+
# no (or unknown) priority -> the conservative default floor, and say how to choose
|
|
162
|
+
for rt in _DEFAULT_RUNTIME_ORDER:
|
|
163
|
+
if rt in runtimes:
|
|
164
|
+
others = [r for r in runtimes if r != rt]
|
|
165
|
+
return rt, (f"multiple runtimes available {sorted(runtimes)}; defaulted to {rt} "
|
|
166
|
+
f"(pass priority=quality|decode-latency to choose)" + (f"; alternatives {others}" if others else ""))
|
|
167
|
+
rt = sorted(runtimes)[0] # only unrecognised runtimes present
|
|
168
|
+
return rt, f"multiple runtimes {sorted(runtimes)}; defaulted to {rt}"
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def _merge_warn(*parts: Optional[str]) -> Optional[str]:
|
|
172
|
+
"""Join the non-empty warning fragments, or None."""
|
|
173
|
+
return " | ".join(p for p in parts if p) or None
|
|
174
|
+
|
|
175
|
+
|
|
94
176
|
# --- tiny semver range check (enough for QAIRT x.y.z) -----------------------------------------
|
|
95
177
|
def _ver(v: str):
|
|
96
178
|
return tuple(int(x) for x in (v.split("-")[0].split("+")[0].split(".") + ["0", "0"])[:3])
|
|
@@ -114,11 +196,31 @@ def _in_range(point: str, rng: str) -> bool:
|
|
|
114
196
|
|
|
115
197
|
def resolve(target: TargetKey, variants: List[Variant],
|
|
116
198
|
want_quant: Optional[str] = None, want_shape: Optional[str] = None,
|
|
117
|
-
precision_support: Optional[dict] = None
|
|
199
|
+
precision_support: Optional[dict] = None,
|
|
200
|
+
want_runtime: Optional[str] = None, priority: Optional[str] = None,
|
|
201
|
+
runtime_support: Optional[dict] = None) -> Decision:
|
|
118
202
|
"""Return the single Decision for this board. Pure function — easy to test without a board."""
|
|
119
203
|
if not variants:
|
|
120
204
|
return Decision(Outcome.FAIL, reason="no variants registered for this model", axis="model")
|
|
121
205
|
|
|
206
|
+
# The runtime-support matrix may be passed explicitly (e.g. straight from the recipe) or carried on the
|
|
207
|
+
# variants (the registry populates it). Resolve it once so both the refusal and the selection use it.
|
|
208
|
+
rsupport = runtime_support
|
|
209
|
+
if rsupport is None:
|
|
210
|
+
rsupport = next((v.runtime_support for v in variants if v.runtime_support), None)
|
|
211
|
+
|
|
212
|
+
# Axis 0b (runtime refusal): mirror the precision refusal one axis over. If the caller names a runtime
|
|
213
|
+
# the board's arch cannot host, refuse up front and NAME it — listing the runtimes that ARE runnable on
|
|
214
|
+
# this arch — before arch/SDK matching, because the runtime is unavailable on this silicon regardless of
|
|
215
|
+
# which artifact is in hand (FAIL-names-the-axis, FR-RT03 / SC-RT02).
|
|
216
|
+
if want_runtime:
|
|
217
|
+
reason = runtime_refusal(want_runtime, target.htp_arch, rsupport)
|
|
218
|
+
if reason:
|
|
219
|
+
ok = runnable_runtimes(target.htp_arch, rsupport)
|
|
220
|
+
return Decision(Outcome.FAIL, axis="runtime",
|
|
221
|
+
reason=f"runtime {want_runtime} is not available on {target.htp_arch}: {reason} "
|
|
222
|
+
f"(supported on {target.htp_arch}: {ok})")
|
|
223
|
+
|
|
122
224
|
# Axis 0 (LLM precision-support): if the caller asked for a precision the model declares as
|
|
123
225
|
# blocked/unsupported, refuse up front and name the reason — before arch/SDK matching, because the
|
|
124
226
|
# precision is unavailable through the pipeline regardless of which board is in hand. The matrix may
|
|
@@ -156,6 +258,31 @@ def resolve(target: TargetKey, variants: List[Variant],
|
|
|
156
258
|
reason=f"no shape {want_shape} for {target.htp_arch}; have {have}")
|
|
157
259
|
cand = c2
|
|
158
260
|
|
|
261
|
+
# Runtime axis (spec 004): pick WHICH runtime among the candidates runnable on this board's arch.
|
|
262
|
+
# If the caller named a runtime, narrow to it (its refusal was already handled above). Otherwise, only
|
|
263
|
+
# when the candidates span more than one runtime do we choose by priority — a single-runtime model
|
|
264
|
+
# resolves exactly as before, so nothing that pre-dates the runtime axis changes behaviour.
|
|
265
|
+
runtime_warning = None
|
|
266
|
+
if want_runtime:
|
|
267
|
+
c2 = [v for v in cand if v.effective_runtime == want_runtime]
|
|
268
|
+
if not c2:
|
|
269
|
+
have = sorted({v.effective_runtime for v in cand})
|
|
270
|
+
return Decision(Outcome.FAIL, axis="runtime",
|
|
271
|
+
reason=f"runtime {want_runtime} has no variant for {target.htp_arch}; have {have}")
|
|
272
|
+
cand = c2
|
|
273
|
+
else:
|
|
274
|
+
runtimes_here = sorted({v.effective_runtime for v in cand})
|
|
275
|
+
if len(runtimes_here) > 1:
|
|
276
|
+
chosen, runtime_warning = pick_runtime(runtimes_here, priority)
|
|
277
|
+
cand = [v for v in cand if v.effective_runtime == chosen]
|
|
278
|
+
# Warn if the chosen runtime is marked deprecated on this arch (its successor is named in the matrix).
|
|
279
|
+
if rsupport and cand:
|
|
280
|
+
dep = rsupport.get(cand[0].effective_runtime, {})
|
|
281
|
+
dep = dep.get(target.htp_arch, "") if isinstance(dep, dict) else ""
|
|
282
|
+
if str(dep).split(":", 1)[0] == "deprecated":
|
|
283
|
+
runtime_warning = _merge_warn(runtime_warning,
|
|
284
|
+
f"runtime {cand[0].effective_runtime} on {target.htp_arch} is {dep}")
|
|
285
|
+
|
|
159
286
|
built = [v for v in cand if v.has_artifact]
|
|
160
287
|
|
|
161
288
|
# Axis: SDK range. If we don't know the board's QAIRT (offline), we can't check the point —
|
|
@@ -164,17 +291,18 @@ def resolve(target: TargetKey, variants: List[Variant],
|
|
|
164
291
|
if target.qairt is None:
|
|
165
292
|
v = built[0]
|
|
166
293
|
return Decision(Outcome.USE_WITH_WARN, variant=v,
|
|
167
|
-
warning="board QAIRT unknown (offline) — SDK compatibility unverified",
|
|
294
|
+
warning=_merge_warn("board QAIRT unknown (offline) — SDK compatibility unverified",
|
|
295
|
+
runtime_warning),
|
|
168
296
|
reason="artifact matches arch/quant/shape")
|
|
169
297
|
in_range = [v for v in built if _in_range(target.qairt, v.sdk_compatible)]
|
|
170
298
|
if in_range:
|
|
171
299
|
exact = [v for v in in_range if _ver(v.sdk_tested) == _ver(target.qairt)]
|
|
172
300
|
if exact:
|
|
173
|
-
return Decision(Outcome.USE, variant=exact[0], reason="exact match")
|
|
301
|
+
return Decision(Outcome.USE, variant=exact[0], reason="exact match", warning=runtime_warning)
|
|
174
302
|
v = in_range[0]
|
|
175
303
|
return Decision(Outcome.USE_WITH_WARN, variant=v,
|
|
176
|
-
warning=(f"board QAIRT {target.qairt} is in range {v.sdk_compatible} "
|
|
177
|
-
|
|
304
|
+
warning=_merge_warn(f"board QAIRT {target.qairt} is in range {v.sdk_compatible} "
|
|
305
|
+
f"but the artifact was tested at {v.sdk_tested}", runtime_warning),
|
|
178
306
|
reason="in-range, outside tested point")
|
|
179
307
|
# artifact exists but SDK is out of every range -> fall through to recipe/fail on the SDK axis
|
|
180
308
|
sdk_axis = True
|
|
@@ -184,7 +312,7 @@ def resolve(target: TargetKey, variants: List[Variant],
|
|
|
184
312
|
# Axis 3: no usable artifact, but a recipe can build one for this target.
|
|
185
313
|
buildable = [v for v in cand if v.has_recipe]
|
|
186
314
|
if buildable:
|
|
187
|
-
return Decision(Outcome.BUILD, variant=buildable[0],
|
|
315
|
+
return Decision(Outcome.BUILD, variant=buildable[0], warning=runtime_warning,
|
|
188
316
|
reason=("no artifact matches the board SDK; a recipe can build one"
|
|
189
317
|
if sdk_axis else "no built artifact, but a recipe exists"))
|
|
190
318
|
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""Registry→resolve wiring for the LLM precision-support axis (FR-L06, Principle II).
|
|
2
|
+
|
|
3
|
+
Analyzer finding (0.2.5): the precision-support matrix was parsed by the recipe and enforced by the
|
|
4
|
+
resolver, but `Registry._load` never carried `precision_support` onto the Variant and no catalogue entry
|
|
5
|
+
declared one — so the Axis-0 "refuse by name" could never fire through the real registry→resolve path,
|
|
6
|
+
yet the suite was green (it only exercised hand-built Variants / an explicit kwarg). This test drives the
|
|
7
|
+
REGISTRY path, so it would have failed before the fix (registry.py now threads precision_support).
|
|
8
|
+
"""
|
|
9
|
+
import os
|
|
10
|
+
import sys
|
|
11
|
+
|
|
12
|
+
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
|
|
13
|
+
|
|
14
|
+
from chipwright.registry import Registry
|
|
15
|
+
from chipwright.resolver import resolve, Outcome
|
|
16
|
+
from chipwright.env import TargetKey
|
|
17
|
+
|
|
18
|
+
_FIXTURE = """registry_schema: 1
|
|
19
|
+
models:
|
|
20
|
+
- model: demo-llm
|
|
21
|
+
modality: llm
|
|
22
|
+
version: 1.0.0
|
|
23
|
+
variants:
|
|
24
|
+
- arch: v73
|
|
25
|
+
quant: w4a16
|
|
26
|
+
shape: ctx4096
|
|
27
|
+
sdk_tested: "2.37.1"
|
|
28
|
+
sdk_compatible: ">=2.34,<2.40"
|
|
29
|
+
ext: qnnctx
|
|
30
|
+
precision_support:
|
|
31
|
+
w4a16: supported
|
|
32
|
+
w8a16: "blocked:genie-kv-cache-quant-mismatch"
|
|
33
|
+
fp16: "unsupported:genie-engine-is-integer-only"
|
|
34
|
+
recipe: recipes/demo-llm/1.0.0.yaml
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _registry(tmp_path):
|
|
39
|
+
p = tmp_path / "prec_index.yaml"
|
|
40
|
+
p.write_text(_FIXTURE)
|
|
41
|
+
return Registry(index=str(p))
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def test_registry_carries_precision_support_onto_variants(tmp_path):
|
|
45
|
+
vs = _registry(tmp_path).variants("demo-llm")
|
|
46
|
+
assert vs and vs[0].precision_support is not None, "registry must thread precision_support onto the Variant"
|
|
47
|
+
assert vs[0].precision_support["w8a16"].startswith("blocked:")
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def test_resolve_refuses_blocked_precision_through_the_registry_path(tmp_path):
|
|
51
|
+
"""The Axis-0 refusal fires from a registry-loaded variant — not only a hand-built one."""
|
|
52
|
+
vs = _registry(tmp_path).variants("demo-llm")
|
|
53
|
+
d = resolve(TargetKey(htp_arch="v73", qairt="2.37.1"), vs, want_quant="w8a16")
|
|
54
|
+
assert d.outcome == Outcome.FAIL and d.axis == "precision", d
|
|
55
|
+
assert "blocked:genie-kv-cache-quant-mismatch" in d.reason
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def test_resolve_refuses_unsupported_precision_through_the_registry_path(tmp_path):
|
|
59
|
+
vs = _registry(tmp_path).variants("demo-llm")
|
|
60
|
+
d = resolve(TargetKey(htp_arch="v73", qairt="2.37.1"), vs, want_quant="fp16")
|
|
61
|
+
assert d.outcome == Outcome.FAIL and d.axis == "precision"
|
|
62
|
+
assert "unsupported:genie-engine-is-integer-only" in d.reason
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def test_supported_precision_is_not_refused_through_the_registry_path(tmp_path):
|
|
66
|
+
vs = _registry(tmp_path).variants("demo-llm")
|
|
67
|
+
d = resolve(TargetKey(htp_arch="v73", qairt="2.37.1"), vs, want_quant="w4a16")
|
|
68
|
+
assert d.axis != "precision", d # w4a16 is supported → no precision refusal
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""The human run-report renderer is shape-safe (analyzer finding, 0.2.5).
|
|
2
|
+
|
|
3
|
+
`run_variant` returns a cosine-shaped report for CV/ASR but a tripwire-shaped one for the LLM generative
|
|
4
|
+
gate (`{tripwire, latency, pass}` — no `cosine_min`/`per_output`). The human door used to index the cosine
|
|
5
|
+
keys unconditionally → a bare KeyError traceback on an LLM success, while the `--json` door was already
|
|
6
|
+
shape-safe. These lock both shapes through the single `_render_run_report` helper (Principle IV: the two
|
|
7
|
+
front doors don't diverge).
|
|
8
|
+
"""
|
|
9
|
+
import os
|
|
10
|
+
import sys
|
|
11
|
+
|
|
12
|
+
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
|
|
13
|
+
|
|
14
|
+
from chipwright import cli, jsonout
|
|
15
|
+
from chipwright.resolver import Decision, Outcome
|
|
16
|
+
|
|
17
|
+
_USE = Decision(Outcome.USE)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def test_llm_tripwire_report_renders_without_crash(capsys):
|
|
21
|
+
rep = {"pass": True, "latency": {},
|
|
22
|
+
"tripwire": {"token_match": 0.991, "thresholds": {"token_match": 0.98}}}
|
|
23
|
+
code = cli._render_run_report(rep, _USE)
|
|
24
|
+
out = capsys.readouterr().out
|
|
25
|
+
assert "generative gate" in out and "token_match = 0.99100" in out
|
|
26
|
+
assert "Traceback" not in out
|
|
27
|
+
assert code == 0 # USE pass → 0
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def test_llm_tripwire_fail_exits_verify_mismatch(capsys):
|
|
31
|
+
rep = {"pass": False, "tripwire": {"token_match": 0.50, "thresholds": {"token_match": 0.98}}}
|
|
32
|
+
assert cli._render_run_report(rep, _USE) == jsonout.EXIT_VERIFY_MISMATCH
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def test_llm_behavior_block_is_shown_when_present(capsys):
|
|
36
|
+
rep = {"pass": True, "tripwire": {"token_match": 0.99, "thresholds": {"token_match": 0.98}},
|
|
37
|
+
"behavior": {"refusal_retention": 1.0, "benign_answer_rate": 0.96}}
|
|
38
|
+
cli._render_run_report(rep, _USE)
|
|
39
|
+
assert "behavioral probe" in capsys.readouterr().out
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def test_cosine_report_still_renders(capsys):
|
|
43
|
+
rep = {"pass": True, "cosine_min": 0.95585, "threshold": 0.95, "per_output": {"class_logits": 0.95585}}
|
|
44
|
+
code = cli._render_run_report(rep, _USE)
|
|
45
|
+
out = capsys.readouterr().out
|
|
46
|
+
assert "NPU vs CPU-reference" in out and "class_logits: 0.95585" in out
|
|
47
|
+
assert code == 0
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
"""Runtime axis (spec 004, FR-RT01/03/04/05, SC-RT01/02/04) — the OFFLINE subset.
|
|
2
|
+
|
|
3
|
+
The runtime axis is the precision axis a second time: a per-key "supported"|"<kind>:<reason>" matrix the
|
|
4
|
+
resolver refuses on, one level deeper (`{runtime: {arch: status}}`) because a runtime's support is per-arch.
|
|
5
|
+
Everything here is pure and board-free — the recipe parse/validate, the resolver priority + the axis=runtime
|
|
6
|
+
refusal, and the runtime-variant record threaded through the registry. Also runnable as
|
|
7
|
+
`python3 test_runtime_support.py`. (The on-device adapter run + dispatch check are Track 2 — they need silicon.)
|
|
8
|
+
"""
|
|
9
|
+
import os
|
|
10
|
+
import sys
|
|
11
|
+
|
|
12
|
+
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
|
|
13
|
+
|
|
14
|
+
from chipwright.env import TargetKey
|
|
15
|
+
from chipwright.recipe import from_dict
|
|
16
|
+
from chipwright.registry import Registry
|
|
17
|
+
from chipwright.resolver import (Outcome, Variant, resolve, runnable_runtimes,
|
|
18
|
+
runtime_refusal)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
# --- recipe schema: runtime_support parses + validates (T-RT01) -----------------------------------
|
|
22
|
+
_BASE = {"name": "qwen2.5-1.5b", "version": "1.0.0",
|
|
23
|
+
"source": {"format": "onnx", "sha256": "abc"},
|
|
24
|
+
"target": {"arch": "v73", "quant": "w4a16", "shape": "ctx4096"}}
|
|
25
|
+
|
|
26
|
+
_MATRIX = {
|
|
27
|
+
"genie": {"v68": "deprecated:geniex-qairt", "v73": "deprecated:geniex-qairt"},
|
|
28
|
+
"geniex-qairt": {"v73": "supported"},
|
|
29
|
+
"llamacpp-htp": {"v68": "unsupported:htp-backend-excludes-v68", "v73": "supported"},
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _recipe(**extra):
|
|
34
|
+
doc = dict(_BASE)
|
|
35
|
+
doc.update(extra)
|
|
36
|
+
return from_dict(doc)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def test_recipe_parses_runtime_matrix():
|
|
40
|
+
r = _recipe(runtime_support=_MATRIX)
|
|
41
|
+
assert r.runtime_status("llamacpp-htp", "v73") == "supported"
|
|
42
|
+
assert r.runtime_status("llamacpp-htp", "v68") == "unsupported:htp-backend-excludes-v68"
|
|
43
|
+
assert r.runtime_status("genie", "v73") == "deprecated:geniex-qairt"
|
|
44
|
+
# 'deprecated' still RUNS (so it's runnable), 'unsupported' does not
|
|
45
|
+
assert r.runnable_runtimes("v73") == ["genie", "geniex-qairt", "llamacpp-htp"]
|
|
46
|
+
assert r.runnable_runtimes("v68") == ["genie"]
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def test_recipe_without_matrix_is_empty_not_error():
|
|
50
|
+
r = _recipe()
|
|
51
|
+
assert r.runtime_support == {} and r.runnable_runtimes("v73") == []
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def test_recipe_rejects_unknown_status_kind():
|
|
55
|
+
for bad in ({"llamacpp-htp": {"v73": "maybe"}}, {"genie": {"v73": "gone:x"}}):
|
|
56
|
+
try:
|
|
57
|
+
_recipe(runtime_support=bad)
|
|
58
|
+
except ValueError:
|
|
59
|
+
continue
|
|
60
|
+
raise AssertionError(f"expected ValueError for {bad!r}")
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def test_recipe_rejects_deprecated_or_unsupported_without_reason():
|
|
64
|
+
for bad in ({"genie": {"v73": "deprecated"}},
|
|
65
|
+
{"llamacpp-htp": {"v68": "unsupported"}},
|
|
66
|
+
{"llamacpp-htp": {"v68": "unsupported: "}}):
|
|
67
|
+
try:
|
|
68
|
+
_recipe(runtime_support=bad)
|
|
69
|
+
except ValueError:
|
|
70
|
+
continue
|
|
71
|
+
raise AssertionError(f"a deprecated/unsupported status must NAME a reason/successor: {bad!r}")
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def test_recipe_rejects_non_mapping_arch_entry():
|
|
75
|
+
try:
|
|
76
|
+
_recipe(runtime_support={"genie": "supported"}) # must be {arch: status}, not a bare status
|
|
77
|
+
except ValueError:
|
|
78
|
+
return
|
|
79
|
+
raise AssertionError("expected ValueError for a non-mapping runtime entry")
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
# --- the pure refusal helpers --------------------------------------------------------------------
|
|
83
|
+
def test_runtime_refusal_helper():
|
|
84
|
+
assert runtime_refusal("llamacpp-htp", "v68", _MATRIX) == "unsupported:htp-backend-excludes-v68"
|
|
85
|
+
assert runtime_refusal("llamacpp-htp", "v73", _MATRIX) is None # supported -> no refusal
|
|
86
|
+
assert runtime_refusal("genie", "v73", _MATRIX) is None # deprecated still RUNS -> no refusal
|
|
87
|
+
assert runtime_refusal("llamacpp-htp", "v73", None) is None # no matrix -> nothing to refuse
|
|
88
|
+
assert runtime_refusal("nonesuch", "v73", _MATRIX) is None # undeclared -> not refused here
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def test_runnable_runtimes_helper():
|
|
92
|
+
assert runnable_runtimes("v68", _MATRIX) == ["genie"] # llamacpp unsupported; genie deprecated-but-runs
|
|
93
|
+
assert runnable_runtimes("v73", _MATRIX) == ["genie", "geniex-qairt", "llamacpp-htp"]
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
# --- resolver: priority picks the runtime (SC-RT01) ----------------------------------------------
|
|
97
|
+
def _genie(arch="v73", **kw):
|
|
98
|
+
base = dict(model="qwen2.5-1.5b", version="1.0.0", arch=arch, quant="w4a16", shape="ctx4096",
|
|
99
|
+
sdk_tested="2.37.1", sdk_compatible=">=2.34,<2.40", runtime="genie",
|
|
100
|
+
runtime_support=_MATRIX, url="u", sha256="s")
|
|
101
|
+
base.update(kw)
|
|
102
|
+
return Variant(**base)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _htp(arch="v73", **kw):
|
|
106
|
+
base = dict(model="qwen2.5-1.5b", version="1.0.0", arch=arch, quant="Q8_0", shape="ctx4096",
|
|
107
|
+
sdk_tested="2.37.1", sdk_compatible=">=2.34,<2.40", runtime="llamacpp-htp",
|
|
108
|
+
ext="gguf", runtime_support=_MATRIX, local="/tmp/q.gguf")
|
|
109
|
+
base.update(kw)
|
|
110
|
+
return Variant(**base)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def test_priority_quality_picks_llamacpp_htp():
|
|
114
|
+
t = TargetKey(htp_arch="v73", qairt="2.37.1")
|
|
115
|
+
d = resolve(t, [_genie(), _htp()], priority="quality")
|
|
116
|
+
assert d.ok and d.variant.effective_runtime == "llamacpp-htp" and d.variant.quant == "Q8_0"
|
|
117
|
+
assert "priority=quality" in (d.warning or "") # the trade-off is recorded on the Decision
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def test_priority_decode_latency_picks_genie():
|
|
121
|
+
t = TargetKey(htp_arch="v73", qairt="2.37.1")
|
|
122
|
+
d = resolve(t, [_genie(), _htp()], priority="decode-latency")
|
|
123
|
+
assert d.ok and d.variant.effective_runtime == "genie" and d.variant.quant == "w4a16"
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def test_default_picks_genie_floor_and_warns_to_choose():
|
|
127
|
+
t = TargetKey(htp_arch="v73", qairt="2.37.1")
|
|
128
|
+
d = resolve(t, [_genie(), _htp()]) # no priority
|
|
129
|
+
assert d.ok and d.variant.effective_runtime == "genie"
|
|
130
|
+
assert "priority=quality|decode-latency" in (d.warning or "") # tells the caller how to choose
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def test_deprecated_runtime_is_flagged_with_successor():
|
|
134
|
+
t = TargetKey(htp_arch="v73", qairt="2.37.1")
|
|
135
|
+
d = resolve(t, [_genie(), _htp()], want_runtime="genie")
|
|
136
|
+
assert d.ok and "deprecated:geniex-qairt" in (d.warning or "") # resolve warns on a deprecated runtime
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def test_single_runtime_model_is_unchanged_by_the_axis():
|
|
140
|
+
# a model with one runtime resolves exactly as before — no priority machinery engages
|
|
141
|
+
t = TargetKey(htp_arch="v73", qairt="2.37.1")
|
|
142
|
+
d = resolve(t, [_htp()])
|
|
143
|
+
assert d.ok and d.variant.effective_runtime == "llamacpp-htp"
|
|
144
|
+
assert "priority" not in (d.warning or "")
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
# --- resolver: refuse a runtime the board can't host, BY NAME (SC-RT02) --------------------------
|
|
148
|
+
def test_resolve_refuses_llamacpp_htp_on_v68_by_name():
|
|
149
|
+
t = TargetKey(htp_arch="v68", qairt="2.37.1")
|
|
150
|
+
d = resolve(t, [_genie(arch="v68")], want_runtime="llamacpp-htp")
|
|
151
|
+
assert d.outcome is Outcome.FAIL and d.axis == "runtime"
|
|
152
|
+
# names the runtime, the arch, the reason, and lists what IS runnable there
|
|
153
|
+
for token in ("llamacpp-htp", "v68", "htp-backend-excludes-v68", "genie"):
|
|
154
|
+
assert token in d.reason, token
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def test_resolve_refusal_reads_matrix_off_variants_or_explicit():
|
|
158
|
+
t = TargetKey(htp_arch="v68", qairt="2.37.1")
|
|
159
|
+
# matrix passed explicitly (e.g. straight from the recipe) also drives the refusal
|
|
160
|
+
v = _genie(arch="v68", runtime_support=None)
|
|
161
|
+
d = resolve(t, [v], want_runtime="llamacpp-htp", runtime_support=_MATRIX)
|
|
162
|
+
assert d.outcome is Outcome.FAIL and d.axis == "runtime"
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def test_supported_runtime_on_arch_is_not_refused():
|
|
166
|
+
t = TargetKey(htp_arch="v73", qairt="2.37.1")
|
|
167
|
+
d = resolve(t, [_genie(), _htp()], want_runtime="llamacpp-htp")
|
|
168
|
+
assert d.ok and d.variant.effective_runtime == "llamacpp-htp"
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
# --- the runtime-variant RECORD carries runtime+precision+PPL+decode/prefill (SC-RT04) ----------
|
|
172
|
+
# A self-contained fixture registry (mirrors registry/index.dev.yaml, which is the author-local overlay)
|
|
173
|
+
# so the test is green in any checkout AND exercises registry._load threading runtime + runtime_support.
|
|
174
|
+
_FIXTURE = """registry_schema: 1
|
|
175
|
+
models:
|
|
176
|
+
- model: qwen2.5-1.5b-instruct
|
|
177
|
+
modality: llm
|
|
178
|
+
version: 1.0.0
|
|
179
|
+
variants:
|
|
180
|
+
- arch: v73
|
|
181
|
+
quant: w4a16
|
|
182
|
+
shape: ctx4096
|
|
183
|
+
sdk_tested: "2.37.1"
|
|
184
|
+
sdk_compatible: ">=2.34,<2.40"
|
|
185
|
+
ext: qnnctx
|
|
186
|
+
runtime: genie
|
|
187
|
+
runtime_support:
|
|
188
|
+
genie: {v73: "deprecated:geniex-qairt"}
|
|
189
|
+
llamacpp-htp: {v68: "unsupported:htp-backend-excludes-v68", v73: "supported"}
|
|
190
|
+
recipe: recipes/qwen2.5-1.5b/1.0.0.yaml
|
|
191
|
+
- arch: v73
|
|
192
|
+
quant: Q8_0
|
|
193
|
+
shape: ctx4096
|
|
194
|
+
sdk_tested: "2.37.1"
|
|
195
|
+
sdk_compatible: ">=2.34,<2.40"
|
|
196
|
+
ext: gguf
|
|
197
|
+
runtime: llamacpp-htp
|
|
198
|
+
runtime_support:
|
|
199
|
+
genie: {v73: "deprecated:geniex-qairt"}
|
|
200
|
+
llamacpp-htp: {v68: "unsupported:htp-backend-excludes-v68", v73: "supported"}
|
|
201
|
+
local: /tmp/qwen2.5-1.5b-instruct-q8_0.gguf
|
|
202
|
+
verified:
|
|
203
|
+
"on": "QCS8550 (Hexagon v73, soc_id 603)"
|
|
204
|
+
runtime: llamacpp-htp
|
|
205
|
+
precision: Q8_0
|
|
206
|
+
loads: true
|
|
207
|
+
quality: {metric: wikitext_ppl, value: 11.66, reference_f16: 11.64}
|
|
208
|
+
latency: {decode_tok_s: 14.4, prefill_speedup_vs_cpu: 1.74, prefill_tok_s: 266.7}
|
|
209
|
+
deferred: {genie_v73_comparison: "no w4a16-Genie-on-v73 bundle built"}
|
|
210
|
+
"""
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def _fixture_registry(tmp_path):
|
|
214
|
+
p = tmp_path / "runtime_index.yaml"
|
|
215
|
+
p.write_text(_FIXTURE)
|
|
216
|
+
return Registry(index=str(p))
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def test_registry_threads_runtime_and_matrix_onto_variants(tmp_path):
|
|
220
|
+
vs = _fixture_registry(tmp_path).variants("qwen2.5-1.5b-instruct")
|
|
221
|
+
htp = next(v for v in vs if v.effective_runtime == "llamacpp-htp")
|
|
222
|
+
assert htp.runtime == "llamacpp-htp" and htp.quant == "Q8_0" and htp.ext == "gguf"
|
|
223
|
+
assert isinstance(htp.runtime_support, dict)
|
|
224
|
+
assert htp.runtime_support["llamacpp-htp"]["v68"] == "unsupported:htp-backend-excludes-v68"
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def test_record_carries_runtime_precision_ppl_and_latency(tmp_path):
|
|
228
|
+
vs = _fixture_registry(tmp_path).variants("qwen2.5-1.5b-instruct")
|
|
229
|
+
htp = next(v for v in vs if v.effective_runtime == "llamacpp-htp")
|
|
230
|
+
rec = htp.verified
|
|
231
|
+
assert rec["runtime"] == "llamacpp-htp" # which runtime
|
|
232
|
+
assert rec["precision"] == "Q8_0" # its precision
|
|
233
|
+
assert rec["quality"]["value"] == 11.66 # on-device quality (PPL)
|
|
234
|
+
assert rec["quality"]["reference_f16"] == 11.64
|
|
235
|
+
assert rec["latency"]["decode_tok_s"] == 14.4 # decode tok/s
|
|
236
|
+
assert rec["latency"]["prefill_speedup_vs_cpu"] == 1.74 # prefill (dispatch proof)
|
|
237
|
+
assert "deferred" in rec # the Genie-v73 head-to-head is recorded as deferred
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def test_resolve_over_fixture_registry_quality_vs_v68_refusal(tmp_path):
|
|
241
|
+
vs = _fixture_registry(tmp_path).variants("qwen2.5-1.5b-instruct")
|
|
242
|
+
# SC-RT01 on the registry: v73 + quality -> the Q8_0 HTP variant
|
|
243
|
+
d = resolve(TargetKey(htp_arch="v73", qairt="2.37.1"), vs, priority="quality")
|
|
244
|
+
assert d.ok and d.variant.effective_runtime == "llamacpp-htp"
|
|
245
|
+
# SC-RT02 on the registry: v68 + llamacpp-htp -> FAIL axis=runtime
|
|
246
|
+
d = resolve(TargetKey(htp_arch="v68", qairt="2.37.1"), vs, want_runtime="llamacpp-htp")
|
|
247
|
+
assert d.outcome is Outcome.FAIL and d.axis == "runtime"
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
if __name__ == "__main__":
|
|
251
|
+
import tempfile
|
|
252
|
+
from pathlib import Path
|
|
253
|
+
fns = [f for n, f in sorted(globals().items()) if n.startswith("test_")]
|
|
254
|
+
for f in fns:
|
|
255
|
+
if "tmp_path" in f.__code__.co_varnames[:f.__code__.co_argcount]:
|
|
256
|
+
with tempfile.TemporaryDirectory() as d:
|
|
257
|
+
f(Path(d))
|
|
258
|
+
else:
|
|
259
|
+
f()
|
|
260
|
+
print("ok", f.__name__)
|
|
261
|
+
print(f"\n{len(fns)} passed")
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: chipwright
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.6
|
|
4
4
|
Summary: A verified package manager for Qualcomm NPUs — resolve the artifact that loads on your exact chip, run it, and prove it against a CPU reference.
|
|
5
5
|
License-Expression: MIT
|
|
6
6
|
Project-URL: Homepage, https://github.com/nameissakthi25/chipwright
|
|
@@ -48,11 +48,14 @@ chipwright/tests/test_env_adb.py
|
|
|
48
48
|
chipwright/tests/test_json_cli.py
|
|
49
49
|
chipwright/tests/test_llm_modality.py
|
|
50
50
|
chipwright/tests/test_packaging.py
|
|
51
|
+
chipwright/tests/test_precision_registry.py
|
|
51
52
|
chipwright/tests/test_precision_support.py
|
|
52
53
|
chipwright/tests/test_registry.py
|
|
53
54
|
chipwright/tests/test_replay.py
|
|
54
55
|
chipwright/tests/test_resolver.py
|
|
55
56
|
chipwright/tests/test_run_llm.py
|
|
57
|
+
chipwright/tests/test_run_render.py
|
|
58
|
+
chipwright/tests/test_runtime_support.py
|
|
56
59
|
chipwright/tests/test_security.py
|
|
57
60
|
chipwright/tests/test_tags.py
|
|
58
61
|
chipwright/tests/test_task_eval.py
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "chipwright"
|
|
7
|
-
version = "0.2.
|
|
7
|
+
version = "0.2.6"
|
|
8
8
|
description = "A verified package manager for Qualcomm NPUs — resolve the artifact that loads on your exact chip, run it, and prove it against a CPU reference."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|