chipwright 0.2.6__tar.gz → 0.2.7__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 (69) hide show
  1. {chipwright-0.2.6 → chipwright-0.2.7}/PKG-INFO +1 -1
  2. {chipwright-0.2.6 → chipwright-0.2.7}/chipwright/__init__.py +1 -1
  3. {chipwright-0.2.6 → chipwright-0.2.7}/chipwright/cli.py +35 -7
  4. {chipwright-0.2.6 → chipwright-0.2.7}/chipwright/data/index.yaml +44 -0
  5. {chipwright-0.2.6 → chipwright-0.2.7}/chipwright/jsonout.py +7 -0
  6. {chipwright-0.2.6 → chipwright-0.2.7}/chipwright/resolver.py +11 -0
  7. {chipwright-0.2.6 → chipwright-0.2.7}/chipwright/run.py +41 -0
  8. chipwright-0.2.7/chipwright/runtimes/__init__.py +40 -0
  9. chipwright-0.2.7/chipwright/runtimes/llamacpp_htp.py +178 -0
  10. chipwright-0.2.7/chipwright/tests/test_llamacpp_htp.py +143 -0
  11. chipwright-0.2.7/chipwright/tests/test_runtime_run.py +103 -0
  12. {chipwright-0.2.6 → chipwright-0.2.7}/chipwright.egg-info/PKG-INFO +1 -1
  13. {chipwright-0.2.6 → chipwright-0.2.7}/chipwright.egg-info/SOURCES.txt +4 -0
  14. {chipwright-0.2.6 → chipwright-0.2.7}/pyproject.toml +2 -2
  15. {chipwright-0.2.6 → chipwright-0.2.7}/LICENSE +0 -0
  16. {chipwright-0.2.6 → chipwright-0.2.7}/README.md +0 -0
  17. {chipwright-0.2.6 → chipwright-0.2.7}/chipwright/adapt.py +0 -0
  18. {chipwright-0.2.6 → chipwright-0.2.7}/chipwright/build.py +0 -0
  19. {chipwright-0.2.6 → chipwright-0.2.7}/chipwright/cache.py +0 -0
  20. {chipwright-0.2.6 → chipwright-0.2.7}/chipwright/config.py +0 -0
  21. {chipwright-0.2.6 → chipwright-0.2.7}/chipwright/data/behavior_prompts.yaml +0 -0
  22. {chipwright-0.2.6 → chipwright-0.2.7}/chipwright/device.py +0 -0
  23. {chipwright-0.2.6 → chipwright-0.2.7}/chipwright/device_adb.py +0 -0
  24. {chipwright-0.2.6 → chipwright-0.2.7}/chipwright/env.py +0 -0
  25. {chipwright-0.2.6 → chipwright-0.2.7}/chipwright/errors.py +0 -0
  26. {chipwright-0.2.6 → chipwright-0.2.7}/chipwright/llm_reference.py +0 -0
  27. {chipwright-0.2.6 → chipwright-0.2.7}/chipwright/modalities/__init__.py +0 -0
  28. {chipwright-0.2.6 → chipwright-0.2.7}/chipwright/modalities/asr_encoder.py +0 -0
  29. {chipwright-0.2.6 → chipwright-0.2.7}/chipwright/modalities/cv.py +0 -0
  30. {chipwright-0.2.6 → chipwright-0.2.7}/chipwright/modalities/llm.py +0 -0
  31. {chipwright-0.2.6 → chipwright-0.2.7}/chipwright/recipe.py +0 -0
  32. {chipwright-0.2.6 → chipwright-0.2.7}/chipwright/registry.py +0 -0
  33. {chipwright-0.2.6 → chipwright-0.2.7}/chipwright/replay.py +0 -0
  34. {chipwright-0.2.6 → chipwright-0.2.7}/chipwright/sdk.py +0 -0
  35. {chipwright-0.2.6 → chipwright-0.2.7}/chipwright/tags.py +0 -0
  36. {chipwright-0.2.6 → chipwright-0.2.7}/chipwright/task_eval.py +0 -0
  37. {chipwright-0.2.6 → chipwright-0.2.7}/chipwright/tests/conftest.py +0 -0
  38. {chipwright-0.2.6 → chipwright-0.2.7}/chipwright/tests/test_adapt.py +0 -0
  39. {chipwright-0.2.6 → chipwright-0.2.7}/chipwright/tests/test_build.py +0 -0
  40. {chipwright-0.2.6 → chipwright-0.2.7}/chipwright/tests/test_cache.py +0 -0
  41. {chipwright-0.2.6 → chipwright-0.2.7}/chipwright/tests/test_cli_ux.py +0 -0
  42. {chipwright-0.2.6 → chipwright-0.2.7}/chipwright/tests/test_device_adb.py +0 -0
  43. {chipwright-0.2.6 → chipwright-0.2.7}/chipwright/tests/test_device_ctx.py +0 -0
  44. {chipwright-0.2.6 → chipwright-0.2.7}/chipwright/tests/test_emit_record.py +0 -0
  45. {chipwright-0.2.6 → chipwright-0.2.7}/chipwright/tests/test_env.py +0 -0
  46. {chipwright-0.2.6 → chipwright-0.2.7}/chipwright/tests/test_env_adb.py +0 -0
  47. {chipwright-0.2.6 → chipwright-0.2.7}/chipwright/tests/test_json_cli.py +0 -0
  48. {chipwright-0.2.6 → chipwright-0.2.7}/chipwright/tests/test_llm_modality.py +0 -0
  49. {chipwright-0.2.6 → chipwright-0.2.7}/chipwright/tests/test_packaging.py +0 -0
  50. {chipwright-0.2.6 → chipwright-0.2.7}/chipwright/tests/test_precision_registry.py +0 -0
  51. {chipwright-0.2.6 → chipwright-0.2.7}/chipwright/tests/test_precision_support.py +0 -0
  52. {chipwright-0.2.6 → chipwright-0.2.7}/chipwright/tests/test_registry.py +0 -0
  53. {chipwright-0.2.6 → chipwright-0.2.7}/chipwright/tests/test_replay.py +0 -0
  54. {chipwright-0.2.6 → chipwright-0.2.7}/chipwright/tests/test_resolver.py +0 -0
  55. {chipwright-0.2.6 → chipwright-0.2.7}/chipwright/tests/test_run_llm.py +0 -0
  56. {chipwright-0.2.6 → chipwright-0.2.7}/chipwright/tests/test_run_render.py +0 -0
  57. {chipwright-0.2.6 → chipwright-0.2.7}/chipwright/tests/test_runtime_support.py +0 -0
  58. {chipwright-0.2.6 → chipwright-0.2.7}/chipwright/tests/test_security.py +0 -0
  59. {chipwright-0.2.6 → chipwright-0.2.7}/chipwright/tests/test_tags.py +0 -0
  60. {chipwright-0.2.6 → chipwright-0.2.7}/chipwright/tests/test_task_eval.py +0 -0
  61. {chipwright-0.2.6 → chipwright-0.2.7}/chipwright/tests/test_verify.py +0 -0
  62. {chipwright-0.2.6 → chipwright-0.2.7}/chipwright/tests/test_verify_llm.py +0 -0
  63. {chipwright-0.2.6 → chipwright-0.2.7}/chipwright/verify.py +0 -0
  64. {chipwright-0.2.6 → chipwright-0.2.7}/chipwright/verify_llm.py +0 -0
  65. {chipwright-0.2.6 → chipwright-0.2.7}/chipwright.egg-info/dependency_links.txt +0 -0
  66. {chipwright-0.2.6 → chipwright-0.2.7}/chipwright.egg-info/entry_points.txt +0 -0
  67. {chipwright-0.2.6 → chipwright-0.2.7}/chipwright.egg-info/requires.txt +0 -0
  68. {chipwright-0.2.6 → chipwright-0.2.7}/chipwright.egg-info/top_level.txt +0 -0
  69. {chipwright-0.2.6 → chipwright-0.2.7}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: chipwright
3
- Version: 0.2.6
3
+ Version: 0.2.7
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.6"
11
+ __version__ = "0.2.7"
@@ -234,7 +234,8 @@ def cmd_resolve(args):
234
234
  if args.model not in reg.models():
235
235
  return _unknown_model(args, reg)
236
236
  target = _get_target(args)
237
- d = resolve(target, reg.variants(args.model), args.quant, args.shape)
237
+ d = resolve(target, reg.variants(args.model), args.quant, args.shape,
238
+ want_runtime=getattr(args, "runtime", None), priority=getattr(args, "priority", None))
238
239
  if getattr(args, "json", False):
239
240
  _emit_json(jsonout.decision_to_dict(args.model, target, d))
240
241
  else:
@@ -307,6 +308,22 @@ def _render_run_report(rep, d):
307
308
  """Print a run's verification report (human door) and return its exit code. Shape-safe: the LLM
308
309
  generative gate reports a `tripwire` (token-match), CV/ASR a cosine — the human door must not diverge
309
310
  from the already-shape-safe `--json` projection (Principle IV, and never a KeyError traceback)."""
311
+ if rep.get("runtime") == "llamacpp-htp": # runtime path: a silicon record + the host recipe (FR-RT07)
312
+ rec = rep.get("record") or {}
313
+ d_ = rec.get("dispatch") or {}
314
+ q_ = rec.get("quality") or {}
315
+ print(f"\n{GREEN}✔ cached + hash-verified{RST} {rep.get('artifact')}")
316
+ print(f"{YELLOW}runtime = llamacpp-htp{RST} (on-device execution via the recipe below — not the Genie transport)")
317
+ if d_:
318
+ print(f" on-silicon record ({rec.get('board')}, {rec.get('htp_arch')}, {rec.get('date')}): "
319
+ f"Q8_0 dispatched to HTP0 — prefill {d_.get('htp0_prefill_tps')} vs CPU "
320
+ f"{d_.get('cpu_prefill_tps')} t/s = {d_.get('prefill_speedup')}×")
321
+ if q_:
322
+ print(f" quality: PPL(WikiText) = {q_.get('ppl_wikitext_q8_0')} (near-lossless)")
323
+ print(f" recipe: ADSP_LIBRARY_PATH=<lib> " + " ".join(rep.get("recipe") or []))
324
+ if rep.get("warn"):
325
+ print(f"{DIM} note: {rep['warn']}{RST}")
326
+ return jsonout.EXIT_USE_WITH_WARN
310
327
  mark = f"{GREEN}✔ VERIFIED{RST}" if rep["pass"] else f"{RED}✗ MISMATCH{RST}"
311
328
  if "tripwire" in rep: # LLM generative gate (token-match), not a cosine
312
329
  tw = rep["tripwire"]
@@ -332,7 +349,8 @@ def cmd_run(args):
332
349
  target = _get_target(args)
333
350
  if getattr(args, "json", False):
334
351
  return _run_json(args, reg, target)
335
- d = resolve(target, reg.variants(args.model), args.quant, args.shape)
352
+ d = resolve(target, reg.variants(args.model), args.quant, args.shape,
353
+ want_runtime=getattr(args, "runtime", None), priority=getattr(args, "priority", None))
336
354
  _print_decision(args.model, target, d)
337
355
  if not d.ok:
338
356
  if d.outcome == Outcome.BUILD:
@@ -340,7 +358,9 @@ def cmd_run(args):
340
358
  return jsonout.exit_code_for(d)
341
359
  # USE / USE_WITH_WARN: execute on the board and verify against a CPU reference.
342
360
  v = d.variant
343
- if not (v.run and v.run.get("modality")):
361
+ # the llama.cpp-HTP runtime has its own run path (fetch + silicon record + recipe, FR-RT07) and no
362
+ # cosine run block — route it through run_variant instead of the no-run-block short-circuit.
363
+ if not (v.run and v.run.get("modality")) and v.effective_runtime != "llamacpp-htp":
344
364
  print(f"{DIM}artifact resolves, but this variant has no run block (modality/meta/cpu_onnx) — "
345
365
  f"resolve-only.{RST}")
346
366
  if v.url:
@@ -363,13 +383,14 @@ def _run_json(args, reg, target) -> int:
363
383
  """
364
384
  # Everything human — the decision render and the runner's progress lines — to stderr.
365
385
  with contextlib.redirect_stdout(sys.stderr):
366
- d = resolve(target, reg.variants(args.model), args.quant, args.shape)
386
+ d = resolve(target, reg.variants(args.model), args.quant, args.shape,
387
+ want_runtime=getattr(args, "runtime", None), priority=getattr(args, "priority", None))
367
388
  _print_decision(args.model, target, d)
368
389
  if not d.ok:
369
390
  doc, code = jsonout.decision_to_dict(args.model, target, d), jsonout.exit_code_for(d)
370
391
  else:
371
392
  v = d.variant
372
- if not (v.run and v.run.get("modality")):
393
+ if not (v.run and v.run.get("modality")) and v.effective_runtime != "llamacpp-htp":
373
394
  print(f"artifact resolves, but this variant has no run block — resolve-only.")
374
395
  doc = jsonout.decision_to_dict(args.model, target, d)
375
396
  doc["verified"] = None
@@ -378,7 +399,10 @@ def _run_json(args, reg, target) -> int:
378
399
  from . import run as runner
379
400
  rep = runner.run_variant(v, args.input) # may raise → caught by the --json wrapper in main
380
401
  doc = jsonout.run_to_dict(args.model, target, d, rep)
381
- code = jsonout.exit_code_for(d) if rep.get("pass") else jsonout.EXIT_VERIFY_MISMATCH
402
+ if rep.get("runtime") == "llamacpp-htp":
403
+ code = jsonout.EXIT_USE_WITH_WARN # usable, with the documented recipe caveat (FR-RT07)
404
+ else:
405
+ code = jsonout.exit_code_for(d) if rep.get("pass") else jsonout.EXIT_VERIFY_MISMATCH
382
406
  _emit_json(doc)
383
407
  return code
384
408
 
@@ -437,7 +461,8 @@ def cmd_build(args):
437
461
  if args.model not in reg.models():
438
462
  return _unknown_model(args, reg)
439
463
  target = _get_target(args)
440
- d = resolve(target, reg.variants(args.model), args.quant, args.shape)
464
+ d = resolve(target, reg.variants(args.model), args.quant, args.shape,
465
+ want_runtime=getattr(args, "runtime", None), priority=getattr(args, "priority", None))
441
466
  _print_decision(args.model, target, d)
442
467
  if d.ok:
443
468
  print(f"{DIM}already resolves to USE — nothing to build.{RST}")
@@ -501,6 +526,9 @@ def main(argv=None):
501
526
  ap.add_argument("--target", help="offline target, e.g. 'htpv68,qnn2.37'")
502
527
  ap.add_argument("--quant", help="narrow to a quant, e.g. w8a16")
503
528
  ap.add_argument("--shape", help="narrow to a shape profile, e.g. win30s")
529
+ ap.add_argument("--runtime", help="pick an inference runtime, e.g. llamacpp-htp (default: the genie floor)")
530
+ ap.add_argument("--priority", choices=["quality", "decode-latency"],
531
+ help="when a model spans runtimes, prefer quality (llamacpp-htp) or decode-latency (genie)")
504
532
  ap.add_argument("--json", action="store_true",
505
533
  help="emit one JSON document to stdout (resolve/run/list); human text → stderr, "
506
534
  "always plain. Exit codes follow the table below.")
@@ -142,6 +142,50 @@ models:
142
142
  sdk_compatible: '>=2.34,<2.40'
143
143
  ext: qnnctx
144
144
  recipe: recipes/qwen2.5-1.5b/1.0.0.yaml
145
+ # The llama.cpp-HTP runtime variant — a >4-bit integer route (Q8_0) that classic Genie structurally
146
+ # lacks. Silicon-verified on v73 (spec 004 Track 2). On-device execution is via the documented
147
+ # llama.cpp-HTP recipe (staged glibc loader + ADSP_LIBRARY_PATH + libggml-htp-v73.so); chipwright-run
148
+ # CLI integration for this runtime is pending (T-RT11), so `run` here fetches + hash-verifies the GGUF
149
+ # and surfaces the recipe + this record (FR-RT07), rather than driving the board itself.
150
+ - arch: v73
151
+ quant: Q8_0
152
+ shape: ctx4096
153
+ runtime: llamacpp-htp
154
+ sdk_tested: b3278e921
155
+ sdk_compatible: llama.cpp-HTP (no QAIRT dependency)
156
+ ext: gguf
157
+ url: https://huggingface.co/nameissakthi/chipwright-llm/resolve/main/qwen2.5-1.5b-Q8_0.gguf
158
+ sha256: cedde4175edae2e9467d8b98d70843e57c8fb101afd54ca773bce022e80f7e4e
159
+ runtime_support:
160
+ llamacpp-htp:
161
+ v68: 'unsupported:needs-v73-or-newer-hexagon'
162
+ v73: supported
163
+ genie:
164
+ v68: supported
165
+ v73: supported
166
+ verified:
167
+ 'on': QCS9075 (Dragonwing IQ-9075, soc_id 676, Hexagon v73)
168
+ date: 2026-10-24
169
+ loads: true
170
+ runtime: llamacpp-htp
171
+ vs_reference:
172
+ backend: llama.cpp-cpu (same Q8_0 GGUF, GGML_HEXAGON_NDEV=0)
173
+ metric: dispatch+ppl
174
+ dispatch:
175
+ device: HTP0
176
+ htp0_prefill_tps: 592.82
177
+ cpu_prefill_tps: 217.55
178
+ prefill_speedup: 2.72
179
+ note: Q8_0 MUL_MAT dispatched to HTP0 on the compute-DSP (_dom=cdsp), not CPU fallback (SC-RT03)
180
+ quality:
181
+ metric: ppl_wikitext
182
+ chunks: 16
183
+ ppl_q8_0: 11.6631
184
+ ppl_stderr: 0.49427
185
+ ppl_f16_baseline: 11.6388
186
+ note: near-lossless vs the model's own f16
187
+ llamacpp_build: b3278e921
188
+ note: the >4-bit integer route Genie lacks; on-device exec via the llama.cpp-HTP recipe (CLI integration T-RT11)
145
189
  - model: qwen2.5-7b-instruct
146
190
  source: Qwen/Qwen2.5-7B-Instruct
147
191
  license: apache-2.0
@@ -100,6 +100,13 @@ def run_to_dict(model: str, target, d: Decision, report: dict) -> dict:
100
100
  rec["outputs"] = dict(report.get("per_output") or {})
101
101
  if "tripwire" in report: # LLM path: a generative tripwire, not a cosine
102
102
  rec["tripwire"] = report["tripwire"]
103
+ if report.get("runtime") == "llamacpp-htp": # runtime path: a silicon record + the host recipe
104
+ rec["runtime"] = report["runtime"]
105
+ rec["record"] = report.get("record")
106
+ rec["recipe"] = report.get("recipe")
107
+ rec["cli_execution"] = report.get("cli_execution")
108
+ if report.get("warn"):
109
+ rec.setdefault("warnings", []).append(report["warn"])
103
110
  return rec
104
111
 
105
112
 
@@ -285,6 +285,17 @@ def resolve(target: TargetKey, variants: List[Variant],
285
285
 
286
286
  built = [v for v in cand if v.has_artifact]
287
287
 
288
+ # The QAIRT SDK range is a genie/QNN concept — a GGUF on the llama.cpp-HTP runtime has no QAIRT
289
+ # dependency, its "sdk" is the llama.cpp build, which is not comparable to the board's QAIRT. So skip
290
+ # the QAIRT axis for it: a matching built artifact on this arch is USE (carry any runtime warning).
291
+ if built and built[0].effective_runtime == "llamacpp-htp":
292
+ v = built[0]
293
+ if runtime_warning:
294
+ return Decision(Outcome.USE_WITH_WARN, variant=v, warning=runtime_warning,
295
+ reason=f"artifact matches arch/quant/shape; runtime llamacpp-htp (llama.cpp {v.sdk_tested})")
296
+ return Decision(Outcome.USE, variant=v,
297
+ reason=f"artifact matches arch/quant/shape; runtime llamacpp-htp (llama.cpp {v.sdk_tested})")
298
+
288
299
  # Axis: SDK range. If we don't know the board's QAIRT (offline), we can't check the point —
289
300
  # prefer a built artifact and warn that the SDK match is unverified.
290
301
  if built:
@@ -46,11 +46,52 @@ def _emit_npu_record(variant, npu_out):
46
46
  print(f" # saved locally: {fn}", flush=True)
47
47
 
48
48
 
49
+ def run_llamacpp_htp_variant(variant):
50
+ """The llama.cpp-HTP runtime path (spec 004 FR-RT07) — surface the prereqs, don't fail opaquely.
51
+
52
+ This runtime does NOT go through the Genie/QNN device transport. v1 does the honest runnable-today
53
+ part: fetch + hash-verify the hosted GGUF (proving the artifact is real + intact), then surface the
54
+ on-device *recipe* (staged glibc loader + ADSP_LIBRARY_PATH + libggml-htp-<arch>.so) and the
55
+ on-silicon record (`SILICON_RECORD` — dispatch 2.72×, PPL 11.6631). Driving the loader-staging +
56
+ exec from the CLI is T-RT11; until then `run` names the prereq rather than pretending. A caller gets
57
+ USE_WITH_WARN: the variant is silicon-verified and fetchable, with the one documented caveat.
58
+ """
59
+ from . import cache
60
+ from .runtimes import llamacpp_htp as lh
61
+
62
+ if not variant.url and not variant.local:
63
+ raise RuntimeError(
64
+ f"{variant.model} [llamacpp-htp]: this variant carries a silicon record but no GGUF artifact "
65
+ f"in the active registry (no hosted url, no local path).")
66
+ if variant.url:
67
+ art = cache.fetch(variant.url, variant.sha256) # fetch + sha-verify (refuses on mismatch)
68
+ else:
69
+ art = os.path.expanduser(variant.local)
70
+ if not os.path.exists(art):
71
+ raise RuntimeError(f"local GGUF not found: {art}")
72
+ recipe = lh.bench_command(os.path.basename(art), "./pkg-snapdragon/lib", "./glibc238")
73
+ return {
74
+ "runtime": "llamacpp-htp",
75
+ "artifact": art,
76
+ "artifact_verified": True,
77
+ "record": lh.SILICON_RECORD,
78
+ "recipe": recipe,
79
+ "host_prereqs": list(lh.REQUIRED_LOADER_LIBS),
80
+ "cli_execution": "recipe", # not driven by `run` yet — spec 004 T-RT11
81
+ "pass": True, # verified on record + artifact integrity ok
82
+ "warn": ("on-device execution is via the documented llama.cpp-HTP recipe (shown); chipwright-run "
83
+ "CLI integration for this runtime is pending (spec 004 T-RT11)"),
84
+ }
85
+
86
+
49
87
  def run_variant(variant, input_path=None, emit_record=False):
50
88
  """Execute a resolved variant on the board and return the verification report.
51
89
 
52
90
  `emit_record=True` also captures the on-device output tensor(s) for a reproducible record (FR-RR05).
53
91
  """
92
+ # the llama.cpp-HTP runtime has its own path (no Genie/QNN transport) — handle before the modality logic.
93
+ if variant.effective_runtime == "llamacpp-htp":
94
+ return run_llamacpp_htp_variant(variant)
54
95
  r = variant.run or {}
55
96
  if not r.get("modality"):
56
97
  raise RuntimeError(f"variant for {variant.model} has no run block (modality/meta/cpu_onnx)")
@@ -0,0 +1,40 @@
1
+ """Runtime adapters — the seam the runtime axis resolves onto (spec 004, T-RT03).
2
+
3
+ The resolver (`resolver.py`) picks a *runtime* ({genie, geniex-qairt, llamacpp-htp}) as one axis of a
4
+ Decision; an adapter is the thing that then actually stages and runs a variant on that runtime and proves
5
+ it ran. The resolver stays pure and offline — adapters are where the on-device, runtime-specific work lives
6
+ (Principle IV: one implementation; the CLI/app call the adapter, they never fork its logic).
7
+
8
+ Only the adapters that are *silicon-verified* are registered here. `llamacpp-htp` is registered because its
9
+ Q8_0-on-HTP route is proven on v73 (see `llamacpp_htp.SILICON_RECORD`). `genie` / `geniex-qairt` are named
10
+ by the resolver but have no adapter yet — `get_adapter` refuses them by name rather than pretending.
11
+ """
12
+ from __future__ import annotations
13
+
14
+ from typing import Callable, Dict
15
+
16
+ from . import llamacpp_htp
17
+
18
+ # name -> the adapter module (each exposes dispatch_proof / host_preflight / bench_command / SILICON_RECORD)
19
+ _ADAPTERS: Dict[str, object] = {
20
+ "llamacpp-htp": llamacpp_htp,
21
+ }
22
+
23
+
24
+ def get_adapter(runtime: str):
25
+ """Return the adapter module for `runtime`, or raise with a named reason if none is wired yet.
26
+
27
+ Refuse-by-name, not a KeyError traceback — the same discipline the resolver uses (Principle II).
28
+ """
29
+ adapter = _ADAPTERS.get(runtime)
30
+ if adapter is None:
31
+ known = sorted(_ADAPTERS)
32
+ raise NotImplementedError(
33
+ f"no silicon-verified adapter for runtime {runtime!r}; wired runtimes: {known}. "
34
+ f"(the resolver may still *select* {runtime!r} offline, but running it needs an adapter)")
35
+ return adapter
36
+
37
+
38
+ def wired_runtimes():
39
+ """The runtimes that have a real, silicon-verified adapter."""
40
+ return sorted(_ADAPTERS)
@@ -0,0 +1,178 @@
1
+ """The llama.cpp-HTP runtime adapter (spec 004 T-RT07; FR-RT06 / SC-RT03).
2
+
3
+ This is the route Genie structurally lacks: a >4-bit integer quant (Q8_0, Q4_0) dispatched to the Hexagon
4
+ HTP through the llama.cpp Hexagon backend. Classic Genie is w4a16-only; llama.cpp-HTP runs Q8_0 MUL_MAT on
5
+ the NPU on v73+, which is the quality/prefill win.
6
+
7
+ Two pieces are the adapter's contract, and both are pure + offline-testable:
8
+
9
+ * `host_preflight(probe)` — refuse-by-name if the board lacks what the route needs (fastrpc / libcdsprpc,
10
+ the matching-arch HTP lib, the staged glibc loader). The llama.cpp-HTP binaries are built against a
11
+ newer glibc than the QDC board ships, so they run under an injected loader (`ld-linux-aarch64.so.1
12
+ --library-path <staged-glibc>:<pkg-lib>:...`). If the loader or the arch lib isn't staged, we say so by
13
+ name rather than letting the dynamic linker emit a raw error on-device.
14
+
15
+ * `dispatch_proof(stdout)` — THE gate (SC-RT03). "It ran" is not "it ran on the NPU": llama.cpp silently
16
+ falls back to CPU when the Hexagon registry is disabled (`GGML_HEXAGON_NDEV=0`) or a session can't open.
17
+ This parses the ggml-hex log + the llama-bench table and returns `dispatched=True` ONLY when an HTP0
18
+ session was opened on the compute-DSP (`_dom=cdsp`) AND a bench row actually executed on device `HTP0`.
19
+ The CPU-fallback output (ndev 0, no HTP0 session, no `dev` column) returns `dispatched=False` — the gate
20
+ distinguishes the two, the way the CV gate must FAIL random-noise.
21
+
22
+ SILICON_RECORD is the proof this adapter is wired on (Principle III: tested-is-a-record, not a badge).
23
+ """
24
+ from __future__ import annotations
25
+
26
+ import re
27
+ from dataclasses import dataclass, field
28
+ from typing import Dict, List, Optional, Tuple
29
+
30
+ # --- the proven on-silicon record (Principle I + III) ------------------------------------------------
31
+ # Re-verified on a fresh v73 board through this adapter's recipe on 2026-10-24 (Track 2). The dispatch
32
+ # proof and the PPL both reproduce the earlier sweep: PPL identical to 4 decimals, prefill speedup even
33
+ # larger on this part. This is what promotes the Q8_0-HTP variant from "proven@sweep" to silicon-backed.
34
+ SILICON_RECORD = {
35
+ "runtime": "llamacpp-htp",
36
+ "model": "qwen2.5-1.5b",
37
+ "precision": "Q8_0",
38
+ "board": "QCS9075 (Dragonwing IQ-9075)",
39
+ "soc_id": 676,
40
+ "htp_arch": "v73",
41
+ "date": "2026-10-24",
42
+ "gguf_sha256": "cedde4175edae2e9467d8b98d70843e57c8fb101afd54ca773bce022e80f7e4e",
43
+ "llamacpp_build": "3278e921b",
44
+ "dispatch": { # SC-RT03: Q8_0 MUL_MAT on HTP0, not CPU fallback
45
+ "htp0_prefill_tps": 592.82,
46
+ "cpu_prefill_tps": 217.55,
47
+ "prefill_speedup": 2.72,
48
+ "htp0_decode_tps": 23.51, # decode regresses vs CPU on a model this small (memory-bound) — recorded, not hidden
49
+ "cpu_decode_tps": 28.99,
50
+ },
51
+ "quality": { # near-lossless vs the model's own f16 (11.6388)
52
+ "ppl_wikitext_q8_0": 11.6631,
53
+ "ppl_stderr": 0.49427,
54
+ "chunks": 16,
55
+ },
56
+ # also proven on the earlier sweep: QCS8550 (soc_id 603, v73), PPL 11.6631, prefill 266.74 vs 152.93.
57
+ }
58
+
59
+ # The glibc the container-built binaries need, staged next to the package (loader injection).
60
+ LOADER = "ld-linux-aarch64.so.1"
61
+ REQUIRED_LOADER_LIBS = ("ld-linux-aarch64.so.1", "libc.so.6", "libstdc++.so.6", "libm.so.6")
62
+
63
+
64
+ @dataclass
65
+ class DispatchProof:
66
+ """The verdict of the on-device dispatch check (SC-RT03)."""
67
+ dispatched: bool
68
+ arch: Optional[str] = None # 'v73' etc., from the ggml-hex log
69
+ ndev: Optional[int] = None # Hexagon registry device count (0 => CPU-only)
70
+ on_cdsp: bool = False # an HTP0 session opened on the compute-DSP domain
71
+ device_rows: List[Dict] = field(default_factory=list) # bench rows that ran on dev=HTP0
72
+ reason: str = ""
73
+
74
+ def as_record(self) -> Dict:
75
+ return {"dispatched": self.dispatched, "arch": self.arch, "ndev": self.ndev,
76
+ "on_cdsp": self.on_cdsp, "device_rows": self.device_rows, "reason": self.reason}
77
+
78
+
79
+ _ARCH_RE = re.compile(r"Hexagon Arch version (v\d+)")
80
+ _NDEV_RE = re.compile(r"\bndev (\d+)\b")
81
+ _SESSION_RE = re.compile(r"HTP0 new session")
82
+ _CDSP_RE = re.compile(r"_dom=cdsp")
83
+
84
+
85
+ def _parse_bench_rows(stdout: str) -> List[Dict]:
86
+ """Parse the llama-bench markdown table; return one dict per data row with its columns.
87
+
88
+ The table has a header row, a separator row, then data rows, all `|`-delimited. The HTP build names a
89
+ `dev` column only when a Hexagon device is in play; CPU-fallback output omits it. We key rows by header.
90
+ """
91
+ header: Optional[List[str]] = None
92
+ rows: List[Dict] = []
93
+ for line in stdout.splitlines():
94
+ if "|" not in line:
95
+ continue
96
+ cells = [c.strip() for c in line.strip().strip("|").split("|")]
97
+ if set("".join(cells)) <= set("-: "): # the |---|---| separator
98
+ continue
99
+ if header is None:
100
+ header = cells
101
+ continue
102
+ if len(cells) == len(header):
103
+ rows.append(dict(zip(header, cells)))
104
+ return rows
105
+
106
+
107
+ def dispatch_proof(stdout: str) -> DispatchProof:
108
+ """THE gate (SC-RT03). Return a DispatchProof: did Q8_0 actually run on the HTP, or fall back to CPU?
109
+
110
+ `dispatched` is True only when ALL of: the Hexagon registry had a device (ndev >= 1), an HTP0 session
111
+ opened on the compute-DSP (`_dom=cdsp`), and at least one bench row executed on device `HTP0`. Any one
112
+ missing => CPU fallback (or no run), and we say which. This is pure: feed it captured stdout.
113
+ """
114
+ arch_m = _ARCH_RE.search(stdout)
115
+ ndev_m = _NDEV_RE.search(stdout)
116
+ arch = arch_m.group(1) if arch_m else None
117
+ ndev = int(ndev_m.group(1)) if ndev_m else None
118
+ on_cdsp = bool(_SESSION_RE.search(stdout) and _CDSP_RE.search(stdout))
119
+
120
+ rows = _parse_bench_rows(stdout)
121
+ # a device column may be named 'dev' or 'device'
122
+ def devcell(r: Dict) -> str:
123
+ return (r.get("dev") or r.get("device") or "").strip()
124
+ device_rows = [r for r in rows if devcell(r).upper() == "HTP0"]
125
+
126
+ if ndev == 0:
127
+ return DispatchProof(False, arch, ndev, on_cdsp, device_rows,
128
+ "Hexagon registry disabled (ndev 0) — ran on CPU, not the NPU")
129
+ if not on_cdsp:
130
+ return DispatchProof(False, arch, ndev, on_cdsp, device_rows,
131
+ "no HTP0 session opened on the compute-DSP (_dom=cdsp) — not dispatched to the NPU")
132
+ if not device_rows:
133
+ return DispatchProof(False, arch, ndev, on_cdsp, device_rows,
134
+ "HTP session opened but no bench row executed on device HTP0 — nothing dispatched")
135
+ tests = ", ".join(f"{r.get('test','?')}={r.get('t/s','?')}t/s" for r in device_rows)
136
+ return DispatchProof(True, arch, ndev, on_cdsp, device_rows,
137
+ f"Q8_0 dispatched to HTP0 on Hexagon {arch} ({tests})")
138
+
139
+
140
+ def host_preflight(probe: Dict) -> Tuple[bool, Optional[str]]:
141
+ """Refuse-by-name if the board can't host the llama.cpp-HTP route. Pure: `probe` is a board capability dict.
142
+
143
+ Expected keys: `has_fastrpc` (bool), `has_cdsprpc` (bool), `htp_arch` (str, e.g. 'v73'),
144
+ `staged_htp_libs` (iterable of arch tags present, e.g. {'v73'}), `staged_loader_libs` (iterable of
145
+ loader lib filenames staged). Returns (ok, reason-if-not).
146
+ """
147
+ if not probe.get("has_fastrpc") or not probe.get("has_cdsprpc"):
148
+ return False, ("runtime=llamacpp-htp needs fastrpc + libcdsprpc on the board to reach the Hexagon "
149
+ "DSP; this image has neither")
150
+ arch = probe.get("htp_arch")
151
+ staged = set(probe.get("staged_htp_libs") or ())
152
+ if arch not in staged:
153
+ return False, (f"runtime=llamacpp-htp needs libggml-htp-{arch}.so staged for this board's arch "
154
+ f"({arch}); staged: {sorted(staged) or 'none'}")
155
+ missing = [lib for lib in REQUIRED_LOADER_LIBS if lib not in set(probe.get("staged_loader_libs") or ())]
156
+ if missing:
157
+ return False, (f"runtime=llamacpp-htp binaries need an injected glibc loader; missing staged libs: "
158
+ f"{missing}")
159
+ return True, None
160
+
161
+
162
+ def bench_command(gguf: str, lib_dir: str, loader_dir: str, *, device: str = "HTP0",
163
+ ndev: Optional[int] = None, p: int = 64, n: int = 32, r: int = 2) -> List[str]:
164
+ """The exact loader-injected llama-bench argv proven on silicon. `ndev=0` forces the CPU reference.
165
+
166
+ Grounded in the verified run:
167
+ ADSP_LIBRARY_PATH=<lib_dir> <loader_dir>/ld-linux-aarch64.so.1 \\
168
+ --library-path <loader_dir>:<lib_dir>:/usr/lib:/lib \\
169
+ <bin>/llama-bench -m <gguf> [--device HTP0] -p <p> -n <n> -r <r>
170
+ The caller sets ADSP_LIBRARY_PATH and (for CPU) GGML_HEXAGON_NDEV in the environment.
171
+ """
172
+ bin_dir = lib_dir.rsplit("/lib", 1)[0] + "/bin"
173
+ argv = [f"{loader_dir}/{LOADER}", "--library-path",
174
+ f"{loader_dir}:{lib_dir}:/usr/lib:/lib", f"{bin_dir}/llama-bench", "-m", gguf]
175
+ if ndev != 0:
176
+ argv += ["--device", device]
177
+ argv += ["-p", str(p), "-n", str(n), "-r", str(r)]
178
+ return argv
@@ -0,0 +1,143 @@
1
+ """The llama.cpp-HTP adapter: the dispatch gate + host preflight (spec 004 T-RT07 / SC-RT03).
2
+
3
+ Layer 1 (these tests) proves the LOGIC: the dispatch gate must separate a real HTP0 dispatch from a silent
4
+ CPU fallback, and preflight must refuse-by-name. The fixtures below are the ACTUAL stdout captured on
5
+ silicon (QCS9075 / v73, 2026-10-24) — the HTP run and the `GGML_HEXAGON_NDEV=0` CPU run of the same Q8_0
6
+ model. Layer 2 (the record the fixtures come from) is `llamacpp_htp.SILICON_RECORD`. A gate that can't FAIL
7
+ the CPU-fallback case isn't a gate — so that negative is the central assertion, mirroring the CV gate on
8
+ random noise.
9
+ """
10
+ import os
11
+ import sys
12
+
13
+ sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
14
+
15
+ from chipwright import runtimes
16
+ from chipwright.runtimes import llamacpp_htp as lh
17
+
18
+ # --- real captured stdout (QCS9075 / v73, 2026-10-24) ------------------------------------------------
19
+ HTP_STDOUT = """\
20
+ ggml-hex: Loading driver libcdsprpc.so
21
+ ggml-hex: Hexagon backend (experimental) : allocating new registry : ndev 1
22
+ ggml-hex: Hexagon Arch version v73
23
+ ggml-hex: HTP0 allocating new session
24
+ ggml-hex: HTP0 hwinfo: threads 4, hvx 4, hmx 1, vtcm 8 MB
25
+ ggml-hex: HTP0 new session : session-id 0 domain-id 3 uri file:///libggml-htp-v73.so?htp_iface_skel_handle_invoke&_modver=1.0&_dom=cdsp&_session=0 handle 0x556115f200
26
+ ggml-hex: HTP0 op batching: n-bufs 16 n-tensors 7168 n-ops 1024 vmem 3145728000
27
+ | model | size | params | backend | ngl | dev | test | t/s |
28
+ | ------------------------------ | ---------: | ---------: | ---------- | --: | ------------ | --------------: | -------------------: |
29
+ | qwen2 1.5B Q8_0 | 1.53 GiB | 1.54 B | HTP | -1 | HTP0 | pp64 | 592.82 ± 20.43 |
30
+ | qwen2 1.5B Q8_0 | 1.53 GiB | 1.54 B | HTP | -1 | HTP0 | tg32 | 23.51 ± 0.04 |
31
+
32
+ build: 3278e921b (10049)
33
+ """
34
+
35
+ CPU_STDOUT = """\
36
+ ggml-hex: Loading driver libcdsprpc.so
37
+ ggml-hex: Hexagon backend (experimental) : allocating new registry : ndev 0
38
+ ggml-hex: Hexagon Arch version v73
39
+ | model | size | params | backend | ngl | test | t/s |
40
+ | ------------------------------ | ---------: | ---------: | ---------- | --: | --------------: | -------------------: |
41
+ | qwen2 1.5B Q8_0 | 1.53 GiB | 1.54 B | HTP | -1 | pp64 | 217.55 ± 0.02 |
42
+ | qwen2 1.5B Q8_0 | 1.53 GiB | 1.54 B | HTP | -1 | tg32 | 28.99 ± 0.01 |
43
+
44
+ build: 3278e921b (10049)
45
+ """
46
+
47
+
48
+ def test_dispatch_gate_confirms_real_htp_run():
49
+ proof = lh.dispatch_proof(HTP_STDOUT)
50
+ assert proof.dispatched is True
51
+ assert proof.arch == "v73"
52
+ assert proof.ndev == 1
53
+ assert proof.on_cdsp is True
54
+ # the pp/tg rows actually executed on device HTP0
55
+ tests = {r["test"] for r in proof.device_rows}
56
+ assert "pp64" in tests and "tg32" in tests
57
+
58
+
59
+ def test_dispatch_gate_FAILS_cpu_fallback():
60
+ """The whole point: NDEV=0 ran on CPU. The gate must NOT call that an NPU dispatch."""
61
+ proof = lh.dispatch_proof(CPU_STDOUT)
62
+ assert proof.dispatched is False
63
+ assert proof.ndev == 0
64
+ assert "CPU" in proof.reason or "ndev 0" in proof.reason
65
+ assert proof.device_rows == [] # no row ran on HTP0
66
+
67
+
68
+ def test_dispatch_gate_FAILS_session_without_device_row():
69
+ """A session can open yet nothing dispatch — still not a pass."""
70
+ stdout = ("ggml-hex: allocating new registry : ndev 1\n"
71
+ "ggml-hex: Hexagon Arch version v73\n"
72
+ "ggml-hex: HTP0 new session : uri file:///libggml-htp-v73.so?_dom=cdsp&_session=0\n"
73
+ "| model | backend | test | t/s |\n| --- | --- | --- | --- |\n")
74
+ proof = lh.dispatch_proof(stdout)
75
+ assert proof.dispatched is False
76
+ assert "no bench row" in proof.reason
77
+
78
+
79
+ def test_silicon_prefill_speedup_is_a_real_win():
80
+ d = lh.SILICON_RECORD["dispatch"]
81
+ assert d["htp0_prefill_tps"] > d["cpu_prefill_tps"] # the Q8_0 route Genie lacks, faster on-chip
82
+ assert round(d["htp0_prefill_tps"] / d["cpu_prefill_tps"], 2) == d["prefill_speedup"]
83
+
84
+
85
+ def test_quality_is_near_lossless():
86
+ assert abs(lh.SILICON_RECORD["quality"]["ppl_wikitext_q8_0"] - 11.6631) < 1e-4
87
+
88
+
89
+ # --- host preflight: refuse-by-name -----------------------------------------------------------------
90
+ _OK_PROBE = {"has_fastrpc": True, "has_cdsprpc": True, "htp_arch": "v73",
91
+ "staged_htp_libs": {"v73", "v75", "v79", "v81"},
92
+ "staged_loader_libs": set(lh.REQUIRED_LOADER_LIBS)}
93
+
94
+
95
+ def test_preflight_passes_on_the_verified_board():
96
+ ok, reason = lh.host_preflight(_OK_PROBE)
97
+ assert ok is True and reason is None
98
+
99
+
100
+ def test_preflight_refuses_missing_fastrpc_by_name():
101
+ probe = dict(_OK_PROBE, has_cdsprpc=False)
102
+ ok, reason = lh.host_preflight(probe)
103
+ assert ok is False and "libcdsprpc" in reason
104
+
105
+
106
+ def test_preflight_refuses_wrong_arch_lib_by_name():
107
+ probe = dict(_OK_PROBE, htp_arch="v68", staged_htp_libs={"v73"})
108
+ ok, reason = lh.host_preflight(probe)
109
+ assert ok is False and "v68" in reason and "libggml-htp-v68.so" in reason
110
+
111
+
112
+ def test_preflight_refuses_missing_loader_by_name():
113
+ probe = dict(_OK_PROBE, staged_loader_libs={"libc.so.6"})
114
+ ok, reason = lh.host_preflight(probe)
115
+ assert ok is False and "loader" in reason
116
+
117
+
118
+ # --- adapter registry seam (T-RT03) -----------------------------------------------------------------
119
+ def test_llamacpp_htp_adapter_is_wired():
120
+ assert "llamacpp-htp" in runtimes.wired_runtimes()
121
+ assert runtimes.get_adapter("llamacpp-htp") is lh
122
+
123
+
124
+ def test_unwired_runtime_refuses_by_name_not_keyerror():
125
+ import pytest
126
+ with pytest.raises(NotImplementedError) as ei:
127
+ runtimes.get_adapter("genie")
128
+ assert "genie" in str(ei.value)
129
+
130
+
131
+ def test_bench_command_matches_the_verified_recipe():
132
+ argv = lh.bench_command("qwen2.5-1.5b-Q8_0.gguf",
133
+ "./pkg-snapdragon/lib", "./glibc238", p=64, n=32, r=2)
134
+ joined = " ".join(argv)
135
+ assert argv[0] == "./glibc238/ld-linux-aarch64.so.1"
136
+ assert "--device HTP0" in joined
137
+ assert "./pkg-snapdragon/bin/llama-bench" in joined
138
+ assert "-m qwen2.5-1.5b-Q8_0.gguf" in joined
139
+
140
+
141
+ def test_bench_command_cpu_reference_drops_device_flag():
142
+ argv = lh.bench_command("m.gguf", "./pkg-snapdragon/lib", "./glibc238", ndev=0)
143
+ assert "--device" not in argv # the CPU reference run (GGML_HEXAGON_NDEV=0) has no HTP device
@@ -0,0 +1,103 @@
1
+ """The llama.cpp-HTP runtime's resolve + run path, end to end (spec 004 FR-RT07, public promotion).
2
+
3
+ Layer 1: the public index now carries a silicon-verified Q8_0 llamacpp-htp variant. These assert that
4
+ (a) it resolves USE on v73 when the runtime is chosen and FAILs by name on v68, and (b) `run_variant`
5
+ routes it to the runtime path — fetch/verify + surface the recipe + the record — instead of the Genie
6
+ transport, with no board and no network (a local artifact). The on-silicon proof is the record itself
7
+ (`llamacpp_htp.SILICON_RECORD`), re-verified 2026-10-24 on QCS9075/v73.
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, run as runner
15
+ from chipwright.env import TargetKey
16
+ from chipwright.registry import Registry
17
+ from chipwright.resolver import Outcome, Variant, resolve
18
+
19
+
20
+ def _htp_variant(local_path):
21
+ return Variant(
22
+ model="qwen2.5-1.5b-instruct", version="1.0.0", arch="v73", quant="Q8_0", shape="ctx4096",
23
+ sdk_tested="b3278e921", sdk_compatible="llama.cpp-HTP (no QAIRT dependency)", ext="gguf",
24
+ local=local_path, runtime="llamacpp-htp",
25
+ runtime_support={"llamacpp-htp": {"v68": "unsupported:needs-v73", "v73": "supported"},
26
+ "genie": {"v68": "supported", "v73": "supported"}})
27
+
28
+
29
+ # --- resolve (the public variant) -------------------------------------------------------------------
30
+ def test_public_index_has_silicon_verified_q8_0_llamacpp_htp_variant():
31
+ reg = Registry(None) # the bundled public index
32
+ vs = reg.variants("qwen2.5-1.5b-instruct")
33
+ htp = [v for v in vs if v.effective_runtime == "llamacpp-htp"]
34
+ assert htp, "public index must carry the llamacpp-htp variant"
35
+ v = htp[0]
36
+ assert v.quant == "Q8_0" and v.arch == "v73" and v.url and v.sha256
37
+ assert v.verified and v.verified.get("runtime") == "llamacpp-htp"
38
+
39
+
40
+ def test_resolve_use_on_v73_when_runtime_chosen():
41
+ reg = Registry(None)
42
+ d = resolve(TargetKey("v73", None), reg.variants("qwen2.5-1.5b-instruct"),
43
+ want_runtime="llamacpp-htp")
44
+ assert d.outcome == Outcome.USE
45
+ assert d.variant.effective_runtime == "llamacpp-htp" and d.variant.quant == "Q8_0"
46
+
47
+
48
+ def test_resolve_refuses_llamacpp_htp_on_v68_by_name():
49
+ reg = Registry(None)
50
+ d = resolve(TargetKey("v68", None), reg.variants("qwen2.5-1.5b-instruct"),
51
+ want_runtime="llamacpp-htp")
52
+ assert d.outcome == Outcome.FAIL and d.axis == "runtime"
53
+ assert "v68" in d.reason and "genie" in d.reason
54
+
55
+
56
+ def test_default_runtime_is_the_genie_floor_not_llamacpp():
57
+ """No --runtime, no priority: the conservative genie floor is chosen (and the user is told to choose)."""
58
+ reg = Registry(None)
59
+ d = resolve(TargetKey("v73", None), reg.variants("qwen2.5-1.5b-instruct"))
60
+ assert d.variant.effective_runtime == "genie" # Q8_0-HTP is opt-in, never the silent default
61
+
62
+
63
+ # --- run (the runtime path) -------------------------------------------------------------------------
64
+ def test_run_variant_routes_llamacpp_htp_to_record_and_recipe(tmp_path):
65
+ gguf = tmp_path / "qwen2.5-1.5b-Q8_0.gguf"
66
+ gguf.write_bytes(b"GGUF\x00dummy")
67
+ rep = runner.run_variant(_htp_variant(str(gguf)))
68
+ assert rep["runtime"] == "llamacpp-htp" and rep["pass"] is True
69
+ assert rep["record"]["quality"]["ppl_wikitext_q8_0"] == 11.6631
70
+ assert "--device" in " ".join(rep["recipe"]) and "HTP0" in " ".join(rep["recipe"])
71
+ assert rep["cli_execution"] == "recipe" and rep.get("warn")
72
+
73
+
74
+ def test_run_variant_refuses_when_no_artifact():
75
+ import pytest
76
+ v = _htp_variant(None)
77
+ v.local = None
78
+ with pytest.raises(RuntimeError) as ei:
79
+ runner.run_variant(v)
80
+ assert "no GGUF artifact" in str(ei.value)
81
+
82
+
83
+ def test_human_render_returns_use_with_warn(tmp_path, capsys):
84
+ from chipwright.resolver import Decision
85
+ gguf = tmp_path / "m.gguf"
86
+ gguf.write_bytes(b"GGUF\x00")
87
+ rep = runner.run_variant(_htp_variant(str(gguf)))
88
+ code = cli._render_run_report(rep, Decision(Outcome.USE))
89
+ out = capsys.readouterr().out
90
+ assert code == jsonout.EXIT_USE_WITH_WARN
91
+ assert "llamacpp-htp" in out and "2.72" in out and "11.6631" in out
92
+ assert "Traceback" not in out
93
+
94
+
95
+ def test_json_projection_carries_runtime_record(tmp_path):
96
+ from chipwright.resolver import Decision
97
+ gguf = tmp_path / "m.gguf"
98
+ gguf.write_bytes(b"GGUF\x00")
99
+ rep = runner.run_variant(_htp_variant(str(gguf)))
100
+ doc = jsonout.run_to_dict("qwen2.5-1.5b-instruct", "v73", Decision(Outcome.USE), rep)
101
+ assert doc["runtime"] == "llamacpp-htp"
102
+ assert doc["record"]["dispatch"]["prefill_speedup"] == 2.72
103
+ assert any("recipe" in w for w in doc.get("warnings", []))
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: chipwright
3
- Version: 0.2.6
3
+ Version: 0.2.7
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
@@ -35,6 +35,8 @@ chipwright/modalities/__init__.py
35
35
  chipwright/modalities/asr_encoder.py
36
36
  chipwright/modalities/cv.py
37
37
  chipwright/modalities/llm.py
38
+ chipwright/runtimes/__init__.py
39
+ chipwright/runtimes/llamacpp_htp.py
38
40
  chipwright/tests/conftest.py
39
41
  chipwright/tests/test_adapt.py
40
42
  chipwright/tests/test_build.py
@@ -46,6 +48,7 @@ chipwright/tests/test_emit_record.py
46
48
  chipwright/tests/test_env.py
47
49
  chipwright/tests/test_env_adb.py
48
50
  chipwright/tests/test_json_cli.py
51
+ chipwright/tests/test_llamacpp_htp.py
49
52
  chipwright/tests/test_llm_modality.py
50
53
  chipwright/tests/test_packaging.py
51
54
  chipwright/tests/test_precision_registry.py
@@ -55,6 +58,7 @@ chipwright/tests/test_replay.py
55
58
  chipwright/tests/test_resolver.py
56
59
  chipwright/tests/test_run_llm.py
57
60
  chipwright/tests/test_run_render.py
61
+ chipwright/tests/test_runtime_run.py
58
62
  chipwright/tests/test_runtime_support.py
59
63
  chipwright/tests/test_security.py
60
64
  chipwright/tests/test_tags.py
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "chipwright"
7
- version = "0.2.6"
7
+ version = "0.2.7"
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"
@@ -41,7 +41,7 @@ Specification = "https://github.com/nameissakthi25/chipwright-speckit"
41
41
  chipwright = "chipwright.cli:main"
42
42
 
43
43
  [tool.setuptools]
44
- packages = ["chipwright", "chipwright.modalities", "chipwright.tests"]
44
+ packages = ["chipwright", "chipwright.modalities", "chipwright.runtimes", "chipwright.tests"]
45
45
 
46
46
  [tool.setuptools.package-data]
47
47
  # The portable public registry index ships inside the package (see registry.py::DEFAULT_INDEX).
File without changes
File without changes
File without changes
File without changes
File without changes