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.
- {chipwright-0.2.6 → chipwright-0.2.7}/PKG-INFO +1 -1
- {chipwright-0.2.6 → chipwright-0.2.7}/chipwright/__init__.py +1 -1
- {chipwright-0.2.6 → chipwright-0.2.7}/chipwright/cli.py +35 -7
- {chipwright-0.2.6 → chipwright-0.2.7}/chipwright/data/index.yaml +44 -0
- {chipwright-0.2.6 → chipwright-0.2.7}/chipwright/jsonout.py +7 -0
- {chipwright-0.2.6 → chipwright-0.2.7}/chipwright/resolver.py +11 -0
- {chipwright-0.2.6 → chipwright-0.2.7}/chipwright/run.py +41 -0
- chipwright-0.2.7/chipwright/runtimes/__init__.py +40 -0
- chipwright-0.2.7/chipwright/runtimes/llamacpp_htp.py +178 -0
- chipwright-0.2.7/chipwright/tests/test_llamacpp_htp.py +143 -0
- chipwright-0.2.7/chipwright/tests/test_runtime_run.py +103 -0
- {chipwright-0.2.6 → chipwright-0.2.7}/chipwright.egg-info/PKG-INFO +1 -1
- {chipwright-0.2.6 → chipwright-0.2.7}/chipwright.egg-info/SOURCES.txt +4 -0
- {chipwright-0.2.6 → chipwright-0.2.7}/pyproject.toml +2 -2
- {chipwright-0.2.6 → chipwright-0.2.7}/LICENSE +0 -0
- {chipwright-0.2.6 → chipwright-0.2.7}/README.md +0 -0
- {chipwright-0.2.6 → chipwright-0.2.7}/chipwright/adapt.py +0 -0
- {chipwright-0.2.6 → chipwright-0.2.7}/chipwright/build.py +0 -0
- {chipwright-0.2.6 → chipwright-0.2.7}/chipwright/cache.py +0 -0
- {chipwright-0.2.6 → chipwright-0.2.7}/chipwright/config.py +0 -0
- {chipwright-0.2.6 → chipwright-0.2.7}/chipwright/data/behavior_prompts.yaml +0 -0
- {chipwright-0.2.6 → chipwright-0.2.7}/chipwright/device.py +0 -0
- {chipwright-0.2.6 → chipwright-0.2.7}/chipwright/device_adb.py +0 -0
- {chipwright-0.2.6 → chipwright-0.2.7}/chipwright/env.py +0 -0
- {chipwright-0.2.6 → chipwright-0.2.7}/chipwright/errors.py +0 -0
- {chipwright-0.2.6 → chipwright-0.2.7}/chipwright/llm_reference.py +0 -0
- {chipwright-0.2.6 → chipwright-0.2.7}/chipwright/modalities/__init__.py +0 -0
- {chipwright-0.2.6 → chipwright-0.2.7}/chipwright/modalities/asr_encoder.py +0 -0
- {chipwright-0.2.6 → chipwright-0.2.7}/chipwright/modalities/cv.py +0 -0
- {chipwright-0.2.6 → chipwright-0.2.7}/chipwright/modalities/llm.py +0 -0
- {chipwright-0.2.6 → chipwright-0.2.7}/chipwright/recipe.py +0 -0
- {chipwright-0.2.6 → chipwright-0.2.7}/chipwright/registry.py +0 -0
- {chipwright-0.2.6 → chipwright-0.2.7}/chipwright/replay.py +0 -0
- {chipwright-0.2.6 → chipwright-0.2.7}/chipwright/sdk.py +0 -0
- {chipwright-0.2.6 → chipwright-0.2.7}/chipwright/tags.py +0 -0
- {chipwright-0.2.6 → chipwright-0.2.7}/chipwright/task_eval.py +0 -0
- {chipwright-0.2.6 → chipwright-0.2.7}/chipwright/tests/conftest.py +0 -0
- {chipwright-0.2.6 → chipwright-0.2.7}/chipwright/tests/test_adapt.py +0 -0
- {chipwright-0.2.6 → chipwright-0.2.7}/chipwright/tests/test_build.py +0 -0
- {chipwright-0.2.6 → chipwright-0.2.7}/chipwright/tests/test_cache.py +0 -0
- {chipwright-0.2.6 → chipwright-0.2.7}/chipwright/tests/test_cli_ux.py +0 -0
- {chipwright-0.2.6 → chipwright-0.2.7}/chipwright/tests/test_device_adb.py +0 -0
- {chipwright-0.2.6 → chipwright-0.2.7}/chipwright/tests/test_device_ctx.py +0 -0
- {chipwright-0.2.6 → chipwright-0.2.7}/chipwright/tests/test_emit_record.py +0 -0
- {chipwright-0.2.6 → chipwright-0.2.7}/chipwright/tests/test_env.py +0 -0
- {chipwright-0.2.6 → chipwright-0.2.7}/chipwright/tests/test_env_adb.py +0 -0
- {chipwright-0.2.6 → chipwright-0.2.7}/chipwright/tests/test_json_cli.py +0 -0
- {chipwright-0.2.6 → chipwright-0.2.7}/chipwright/tests/test_llm_modality.py +0 -0
- {chipwright-0.2.6 → chipwright-0.2.7}/chipwright/tests/test_packaging.py +0 -0
- {chipwright-0.2.6 → chipwright-0.2.7}/chipwright/tests/test_precision_registry.py +0 -0
- {chipwright-0.2.6 → chipwright-0.2.7}/chipwright/tests/test_precision_support.py +0 -0
- {chipwright-0.2.6 → chipwright-0.2.7}/chipwright/tests/test_registry.py +0 -0
- {chipwright-0.2.6 → chipwright-0.2.7}/chipwright/tests/test_replay.py +0 -0
- {chipwright-0.2.6 → chipwright-0.2.7}/chipwright/tests/test_resolver.py +0 -0
- {chipwright-0.2.6 → chipwright-0.2.7}/chipwright/tests/test_run_llm.py +0 -0
- {chipwright-0.2.6 → chipwright-0.2.7}/chipwright/tests/test_run_render.py +0 -0
- {chipwright-0.2.6 → chipwright-0.2.7}/chipwright/tests/test_runtime_support.py +0 -0
- {chipwright-0.2.6 → chipwright-0.2.7}/chipwright/tests/test_security.py +0 -0
- {chipwright-0.2.6 → chipwright-0.2.7}/chipwright/tests/test_tags.py +0 -0
- {chipwright-0.2.6 → chipwright-0.2.7}/chipwright/tests/test_task_eval.py +0 -0
- {chipwright-0.2.6 → chipwright-0.2.7}/chipwright/tests/test_verify.py +0 -0
- {chipwright-0.2.6 → chipwright-0.2.7}/chipwright/tests/test_verify_llm.py +0 -0
- {chipwright-0.2.6 → chipwright-0.2.7}/chipwright/verify.py +0 -0
- {chipwright-0.2.6 → chipwright-0.2.7}/chipwright/verify_llm.py +0 -0
- {chipwright-0.2.6 → chipwright-0.2.7}/chipwright.egg-info/dependency_links.txt +0 -0
- {chipwright-0.2.6 → chipwright-0.2.7}/chipwright.egg-info/entry_points.txt +0 -0
- {chipwright-0.2.6 → chipwright-0.2.7}/chipwright.egg-info/requires.txt +0 -0
- {chipwright-0.2.6 → chipwright-0.2.7}/chipwright.egg-info/top_level.txt +0 -0
- {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.
|
|
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
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
|
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
|