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.
Files changed (65) hide show
  1. {chipwright-0.2.4 → chipwright-0.2.6}/PKG-INFO +1 -1
  2. {chipwright-0.2.4 → chipwright-0.2.6}/chipwright/__init__.py +1 -1
  3. {chipwright-0.2.4 → chipwright-0.2.6}/chipwright/cli.py +21 -6
  4. {chipwright-0.2.4 → chipwright-0.2.6}/chipwright/data/index.yaml +24 -0
  5. {chipwright-0.2.4 → chipwright-0.2.6}/chipwright/recipe.py +54 -1
  6. {chipwright-0.2.4 → chipwright-0.2.6}/chipwright/registry.py +2 -0
  7. {chipwright-0.2.4 → chipwright-0.2.6}/chipwright/resolver.py +134 -6
  8. chipwright-0.2.6/chipwright/tests/test_precision_registry.py +68 -0
  9. chipwright-0.2.6/chipwright/tests/test_run_render.py +47 -0
  10. chipwright-0.2.6/chipwright/tests/test_runtime_support.py +261 -0
  11. {chipwright-0.2.4 → chipwright-0.2.6}/chipwright.egg-info/PKG-INFO +1 -1
  12. {chipwright-0.2.4 → chipwright-0.2.6}/chipwright.egg-info/SOURCES.txt +3 -0
  13. {chipwright-0.2.4 → chipwright-0.2.6}/pyproject.toml +1 -1
  14. {chipwright-0.2.4 → chipwright-0.2.6}/LICENSE +0 -0
  15. {chipwright-0.2.4 → chipwright-0.2.6}/README.md +0 -0
  16. {chipwright-0.2.4 → chipwright-0.2.6}/chipwright/adapt.py +0 -0
  17. {chipwright-0.2.4 → chipwright-0.2.6}/chipwright/build.py +0 -0
  18. {chipwright-0.2.4 → chipwright-0.2.6}/chipwright/cache.py +0 -0
  19. {chipwright-0.2.4 → chipwright-0.2.6}/chipwright/config.py +0 -0
  20. {chipwright-0.2.4 → chipwright-0.2.6}/chipwright/data/behavior_prompts.yaml +0 -0
  21. {chipwright-0.2.4 → chipwright-0.2.6}/chipwright/device.py +0 -0
  22. {chipwright-0.2.4 → chipwright-0.2.6}/chipwright/device_adb.py +0 -0
  23. {chipwright-0.2.4 → chipwright-0.2.6}/chipwright/env.py +0 -0
  24. {chipwright-0.2.4 → chipwright-0.2.6}/chipwright/errors.py +0 -0
  25. {chipwright-0.2.4 → chipwright-0.2.6}/chipwright/jsonout.py +0 -0
  26. {chipwright-0.2.4 → chipwright-0.2.6}/chipwright/llm_reference.py +0 -0
  27. {chipwright-0.2.4 → chipwright-0.2.6}/chipwright/modalities/__init__.py +0 -0
  28. {chipwright-0.2.4 → chipwright-0.2.6}/chipwright/modalities/asr_encoder.py +0 -0
  29. {chipwright-0.2.4 → chipwright-0.2.6}/chipwright/modalities/cv.py +0 -0
  30. {chipwright-0.2.4 → chipwright-0.2.6}/chipwright/modalities/llm.py +0 -0
  31. {chipwright-0.2.4 → chipwright-0.2.6}/chipwright/replay.py +0 -0
  32. {chipwright-0.2.4 → chipwright-0.2.6}/chipwright/run.py +0 -0
  33. {chipwright-0.2.4 → chipwright-0.2.6}/chipwright/sdk.py +0 -0
  34. {chipwright-0.2.4 → chipwright-0.2.6}/chipwright/tags.py +0 -0
  35. {chipwright-0.2.4 → chipwright-0.2.6}/chipwright/task_eval.py +0 -0
  36. {chipwright-0.2.4 → chipwright-0.2.6}/chipwright/tests/conftest.py +0 -0
  37. {chipwright-0.2.4 → chipwright-0.2.6}/chipwright/tests/test_adapt.py +0 -0
  38. {chipwright-0.2.4 → chipwright-0.2.6}/chipwright/tests/test_build.py +0 -0
  39. {chipwright-0.2.4 → chipwright-0.2.6}/chipwright/tests/test_cache.py +0 -0
  40. {chipwright-0.2.4 → chipwright-0.2.6}/chipwright/tests/test_cli_ux.py +0 -0
  41. {chipwright-0.2.4 → chipwright-0.2.6}/chipwright/tests/test_device_adb.py +0 -0
  42. {chipwright-0.2.4 → chipwright-0.2.6}/chipwright/tests/test_device_ctx.py +0 -0
  43. {chipwright-0.2.4 → chipwright-0.2.6}/chipwright/tests/test_emit_record.py +0 -0
  44. {chipwright-0.2.4 → chipwright-0.2.6}/chipwright/tests/test_env.py +0 -0
  45. {chipwright-0.2.4 → chipwright-0.2.6}/chipwright/tests/test_env_adb.py +0 -0
  46. {chipwright-0.2.4 → chipwright-0.2.6}/chipwright/tests/test_json_cli.py +0 -0
  47. {chipwright-0.2.4 → chipwright-0.2.6}/chipwright/tests/test_llm_modality.py +0 -0
  48. {chipwright-0.2.4 → chipwright-0.2.6}/chipwright/tests/test_packaging.py +0 -0
  49. {chipwright-0.2.4 → chipwright-0.2.6}/chipwright/tests/test_precision_support.py +0 -0
  50. {chipwright-0.2.4 → chipwright-0.2.6}/chipwright/tests/test_registry.py +0 -0
  51. {chipwright-0.2.4 → chipwright-0.2.6}/chipwright/tests/test_replay.py +0 -0
  52. {chipwright-0.2.4 → chipwright-0.2.6}/chipwright/tests/test_resolver.py +0 -0
  53. {chipwright-0.2.4 → chipwright-0.2.6}/chipwright/tests/test_run_llm.py +0 -0
  54. {chipwright-0.2.4 → chipwright-0.2.6}/chipwright/tests/test_security.py +0 -0
  55. {chipwright-0.2.4 → chipwright-0.2.6}/chipwright/tests/test_tags.py +0 -0
  56. {chipwright-0.2.4 → chipwright-0.2.6}/chipwright/tests/test_task_eval.py +0 -0
  57. {chipwright-0.2.4 → chipwright-0.2.6}/chipwright/tests/test_verify.py +0 -0
  58. {chipwright-0.2.4 → chipwright-0.2.6}/chipwright/tests/test_verify_llm.py +0 -0
  59. {chipwright-0.2.4 → chipwright-0.2.6}/chipwright/verify.py +0 -0
  60. {chipwright-0.2.4 → chipwright-0.2.6}/chipwright/verify_llm.py +0 -0
  61. {chipwright-0.2.4 → chipwright-0.2.6}/chipwright.egg-info/dependency_links.txt +0 -0
  62. {chipwright-0.2.4 → chipwright-0.2.6}/chipwright.egg-info/entry_points.txt +0 -0
  63. {chipwright-0.2.4 → chipwright-0.2.6}/chipwright.egg-info/requires.txt +0 -0
  64. {chipwright-0.2.4 → chipwright-0.2.6}/chipwright.egg-info/top_level.txt +0 -0
  65. {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.4
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
@@ -8,4 +8,4 @@ from .resolver import Decision, Outcome, Variant, resolve
8
8
  from .tags import ArtifactTag
9
9
 
10
10
  __all__ = ["TargetKey", "probe", "Registry", "Decision", "Outcome", "Variant", "resolve", "ArtifactTag"]
11
- __version__ = "0.2.4"
11
+ __version__ = "0.2.6"
@@ -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
- mark = f"{GREEN}✔ VERIFIED{RST}" if rep["pass"] else f"{RED}✗ MISMATCH{RST}"
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) -> Decision:
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
- f"but the artifact was tested at {v.sdk_tested}"),
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.4
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.4"
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