chipwright 0.2.5__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.5 → chipwright-0.2.7}/PKG-INFO +1 -1
- {chipwright-0.2.5 → chipwright-0.2.7}/chipwright/__init__.py +1 -1
- {chipwright-0.2.5 → chipwright-0.2.7}/chipwright/cli.py +56 -13
- {chipwright-0.2.5 → chipwright-0.2.7}/chipwright/data/index.yaml +44 -0
- {chipwright-0.2.5 → chipwright-0.2.7}/chipwright/jsonout.py +7 -0
- {chipwright-0.2.5 → chipwright-0.2.7}/chipwright/recipe.py +54 -1
- {chipwright-0.2.5 → chipwright-0.2.7}/chipwright/registry.py +2 -0
- chipwright-0.2.7/chipwright/resolver.py +336 -0
- {chipwright-0.2.5 → 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_precision_registry.py +68 -0
- chipwright-0.2.7/chipwright/tests/test_run_render.py +47 -0
- chipwright-0.2.7/chipwright/tests/test_runtime_run.py +103 -0
- chipwright-0.2.7/chipwright/tests/test_runtime_support.py +261 -0
- {chipwright-0.2.5 → chipwright-0.2.7}/chipwright.egg-info/PKG-INFO +1 -1
- {chipwright-0.2.5 → chipwright-0.2.7}/chipwright.egg-info/SOURCES.txt +7 -0
- {chipwright-0.2.5 → chipwright-0.2.7}/pyproject.toml +2 -2
- chipwright-0.2.5/chipwright/resolver.py +0 -197
- {chipwright-0.2.5 → chipwright-0.2.7}/LICENSE +0 -0
- {chipwright-0.2.5 → chipwright-0.2.7}/README.md +0 -0
- {chipwright-0.2.5 → chipwright-0.2.7}/chipwright/adapt.py +0 -0
- {chipwright-0.2.5 → chipwright-0.2.7}/chipwright/build.py +0 -0
- {chipwright-0.2.5 → chipwright-0.2.7}/chipwright/cache.py +0 -0
- {chipwright-0.2.5 → chipwright-0.2.7}/chipwright/config.py +0 -0
- {chipwright-0.2.5 → chipwright-0.2.7}/chipwright/data/behavior_prompts.yaml +0 -0
- {chipwright-0.2.5 → chipwright-0.2.7}/chipwright/device.py +0 -0
- {chipwright-0.2.5 → chipwright-0.2.7}/chipwright/device_adb.py +0 -0
- {chipwright-0.2.5 → chipwright-0.2.7}/chipwright/env.py +0 -0
- {chipwright-0.2.5 → chipwright-0.2.7}/chipwright/errors.py +0 -0
- {chipwright-0.2.5 → chipwright-0.2.7}/chipwright/llm_reference.py +0 -0
- {chipwright-0.2.5 → chipwright-0.2.7}/chipwright/modalities/__init__.py +0 -0
- {chipwright-0.2.5 → chipwright-0.2.7}/chipwright/modalities/asr_encoder.py +0 -0
- {chipwright-0.2.5 → chipwright-0.2.7}/chipwright/modalities/cv.py +0 -0
- {chipwright-0.2.5 → chipwright-0.2.7}/chipwright/modalities/llm.py +0 -0
- {chipwright-0.2.5 → chipwright-0.2.7}/chipwright/replay.py +0 -0
- {chipwright-0.2.5 → chipwright-0.2.7}/chipwright/sdk.py +0 -0
- {chipwright-0.2.5 → chipwright-0.2.7}/chipwright/tags.py +0 -0
- {chipwright-0.2.5 → chipwright-0.2.7}/chipwright/task_eval.py +0 -0
- {chipwright-0.2.5 → chipwright-0.2.7}/chipwright/tests/conftest.py +0 -0
- {chipwright-0.2.5 → chipwright-0.2.7}/chipwright/tests/test_adapt.py +0 -0
- {chipwright-0.2.5 → chipwright-0.2.7}/chipwright/tests/test_build.py +0 -0
- {chipwright-0.2.5 → chipwright-0.2.7}/chipwright/tests/test_cache.py +0 -0
- {chipwright-0.2.5 → chipwright-0.2.7}/chipwright/tests/test_cli_ux.py +0 -0
- {chipwright-0.2.5 → chipwright-0.2.7}/chipwright/tests/test_device_adb.py +0 -0
- {chipwright-0.2.5 → chipwright-0.2.7}/chipwright/tests/test_device_ctx.py +0 -0
- {chipwright-0.2.5 → chipwright-0.2.7}/chipwright/tests/test_emit_record.py +0 -0
- {chipwright-0.2.5 → chipwright-0.2.7}/chipwright/tests/test_env.py +0 -0
- {chipwright-0.2.5 → chipwright-0.2.7}/chipwright/tests/test_env_adb.py +0 -0
- {chipwright-0.2.5 → chipwright-0.2.7}/chipwright/tests/test_json_cli.py +0 -0
- {chipwright-0.2.5 → chipwright-0.2.7}/chipwright/tests/test_llm_modality.py +0 -0
- {chipwright-0.2.5 → chipwright-0.2.7}/chipwright/tests/test_packaging.py +0 -0
- {chipwright-0.2.5 → chipwright-0.2.7}/chipwright/tests/test_precision_support.py +0 -0
- {chipwright-0.2.5 → chipwright-0.2.7}/chipwright/tests/test_registry.py +0 -0
- {chipwright-0.2.5 → chipwright-0.2.7}/chipwright/tests/test_replay.py +0 -0
- {chipwright-0.2.5 → chipwright-0.2.7}/chipwright/tests/test_resolver.py +0 -0
- {chipwright-0.2.5 → chipwright-0.2.7}/chipwright/tests/test_run_llm.py +0 -0
- {chipwright-0.2.5 → chipwright-0.2.7}/chipwright/tests/test_security.py +0 -0
- {chipwright-0.2.5 → chipwright-0.2.7}/chipwright/tests/test_tags.py +0 -0
- {chipwright-0.2.5 → chipwright-0.2.7}/chipwright/tests/test_task_eval.py +0 -0
- {chipwright-0.2.5 → chipwright-0.2.7}/chipwright/tests/test_verify.py +0 -0
- {chipwright-0.2.5 → chipwright-0.2.7}/chipwright/tests/test_verify_llm.py +0 -0
- {chipwright-0.2.5 → chipwright-0.2.7}/chipwright/verify.py +0 -0
- {chipwright-0.2.5 → chipwright-0.2.7}/chipwright/verify_llm.py +0 -0
- {chipwright-0.2.5 → chipwright-0.2.7}/chipwright.egg-info/dependency_links.txt +0 -0
- {chipwright-0.2.5 → chipwright-0.2.7}/chipwright.egg-info/entry_points.txt +0 -0
- {chipwright-0.2.5 → chipwright-0.2.7}/chipwright.egg-info/requires.txt +0 -0
- {chipwright-0.2.5 → chipwright-0.2.7}/chipwright.egg-info/top_level.txt +0 -0
- {chipwright-0.2.5 → 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:
|
|
@@ -303,6 +304,42 @@ def cmd_verify(args):
|
|
|
303
304
|
return _replay(reg, args.model)
|
|
304
305
|
|
|
305
306
|
|
|
307
|
+
def _render_run_report(rep, d):
|
|
308
|
+
"""Print a run's verification report (human door) and return its exit code. Shape-safe: the LLM
|
|
309
|
+
generative gate reports a `tripwire` (token-match), CV/ASR a cosine — the human door must not diverge
|
|
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
|
|
327
|
+
mark = f"{GREEN}✔ VERIFIED{RST}" if rep["pass"] else f"{RED}✗ MISMATCH{RST}"
|
|
328
|
+
if "tripwire" in rep: # LLM generative gate (token-match), not a cosine
|
|
329
|
+
tw = rep["tripwire"]
|
|
330
|
+
thr = (tw.get("thresholds") or {}).get("token_match")
|
|
331
|
+
print(f"\n{mark} generative gate token_match = {float(tw.get('token_match', 0.0)):.5f}"
|
|
332
|
+
+ (f" (threshold {thr})" if thr is not None else ""))
|
|
333
|
+
if rep.get("behavior"):
|
|
334
|
+
print(f" behavioral probe: {rep['behavior']}")
|
|
335
|
+
else:
|
|
336
|
+
print(f"\n{mark} NPU vs CPU-reference cosine_min = {rep['cosine_min']:.5f} "
|
|
337
|
+
f"(threshold {rep['threshold']})")
|
|
338
|
+
for name, c in rep["per_output"].items():
|
|
339
|
+
print(f" {name}: {c:.5f}")
|
|
340
|
+
return jsonout.exit_code_for(d) if rep["pass"] else jsonout.EXIT_VERIFY_MISMATCH
|
|
341
|
+
|
|
342
|
+
|
|
306
343
|
def cmd_run(args):
|
|
307
344
|
reg = Registry(args.registry)
|
|
308
345
|
if args.model not in reg.models():
|
|
@@ -312,7 +349,8 @@ def cmd_run(args):
|
|
|
312
349
|
target = _get_target(args)
|
|
313
350
|
if getattr(args, "json", False):
|
|
314
351
|
return _run_json(args, reg, target)
|
|
315
|
-
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))
|
|
316
354
|
_print_decision(args.model, target, d)
|
|
317
355
|
if not d.ok:
|
|
318
356
|
if d.outcome == Outcome.BUILD:
|
|
@@ -320,7 +358,9 @@ def cmd_run(args):
|
|
|
320
358
|
return jsonout.exit_code_for(d)
|
|
321
359
|
# USE / USE_WITH_WARN: execute on the board and verify against a CPU reference.
|
|
322
360
|
v = d.variant
|
|
323
|
-
|
|
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":
|
|
324
364
|
print(f"{DIM}artifact resolves, but this variant has no run block (modality/meta/cpu_onnx) — "
|
|
325
365
|
f"resolve-only.{RST}")
|
|
326
366
|
if v.url:
|
|
@@ -333,12 +373,7 @@ def cmd_run(args):
|
|
|
333
373
|
except Exception as e:
|
|
334
374
|
print(f"{RED}run failed:{RST} {e}")
|
|
335
375
|
return jsonout.EXIT_UNEXPECTED
|
|
336
|
-
|
|
337
|
-
print(f"\n{mark} NPU vs CPU-reference cosine_min = {rep['cosine_min']:.5f} "
|
|
338
|
-
f"(threshold {rep['threshold']})")
|
|
339
|
-
for name, c in rep["per_output"].items():
|
|
340
|
-
print(f" {name}: {c:.5f}")
|
|
341
|
-
return jsonout.exit_code_for(d) if rep["pass"] else jsonout.EXIT_VERIFY_MISMATCH
|
|
376
|
+
return _render_run_report(rep, d)
|
|
342
377
|
|
|
343
378
|
|
|
344
379
|
def _run_json(args, reg, target) -> int:
|
|
@@ -348,13 +383,14 @@ def _run_json(args, reg, target) -> int:
|
|
|
348
383
|
"""
|
|
349
384
|
# Everything human — the decision render and the runner's progress lines — to stderr.
|
|
350
385
|
with contextlib.redirect_stdout(sys.stderr):
|
|
351
|
-
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))
|
|
352
388
|
_print_decision(args.model, target, d)
|
|
353
389
|
if not d.ok:
|
|
354
390
|
doc, code = jsonout.decision_to_dict(args.model, target, d), jsonout.exit_code_for(d)
|
|
355
391
|
else:
|
|
356
392
|
v = d.variant
|
|
357
|
-
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":
|
|
358
394
|
print(f"artifact resolves, but this variant has no run block — resolve-only.")
|
|
359
395
|
doc = jsonout.decision_to_dict(args.model, target, d)
|
|
360
396
|
doc["verified"] = None
|
|
@@ -363,7 +399,10 @@ def _run_json(args, reg, target) -> int:
|
|
|
363
399
|
from . import run as runner
|
|
364
400
|
rep = runner.run_variant(v, args.input) # may raise → caught by the --json wrapper in main
|
|
365
401
|
doc = jsonout.run_to_dict(args.model, target, d, rep)
|
|
366
|
-
|
|
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
|
|
367
406
|
_emit_json(doc)
|
|
368
407
|
return code
|
|
369
408
|
|
|
@@ -422,7 +461,8 @@ def cmd_build(args):
|
|
|
422
461
|
if args.model not in reg.models():
|
|
423
462
|
return _unknown_model(args, reg)
|
|
424
463
|
target = _get_target(args)
|
|
425
|
-
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))
|
|
426
466
|
_print_decision(args.model, target, d)
|
|
427
467
|
if d.ok:
|
|
428
468
|
print(f"{DIM}already resolves to USE — nothing to build.{RST}")
|
|
@@ -486,6 +526,9 @@ def main(argv=None):
|
|
|
486
526
|
ap.add_argument("--target", help="offline target, e.g. 'htpv68,qnn2.37'")
|
|
487
527
|
ap.add_argument("--quant", help="narrow to a quant, e.g. w8a16")
|
|
488
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)")
|
|
489
532
|
ap.add_argument("--json", action="store_true",
|
|
490
533
|
help="emit one JSON document to stdout (resolve/run/list); human text → stderr, "
|
|
491
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
|
|
|
@@ -23,6 +23,13 @@ RUNS_ON = ("npu", "host")
|
|
|
23
23
|
# through to the resolver's refusal verbatim.
|
|
24
24
|
_PRECISION_KINDS = ("supported", "blocked", "unsupported")
|
|
25
25
|
|
|
26
|
+
# Runtime-support statuses a recipe may declare (spec 004, FR-RT04). The runtime axis is the precision
|
|
27
|
+
# axis a second time — the same {key: "supported" | "<kind>:<reason>"} matrix the resolver already refuses
|
|
28
|
+
# on — only one level deeper, because a runtime's support is *per arch*: `{runtime: {arch: status}}`. A
|
|
29
|
+
# status is the bare word "supported", or "deprecated:<successor>" (runnable, but deprecating upstream —
|
|
30
|
+
# resolve warns), or "unsupported:<reason>" (the board's arch cannot host it — resolve refuses by name).
|
|
31
|
+
_RUNTIME_KINDS = ("supported", "deprecated", "unsupported")
|
|
32
|
+
|
|
26
33
|
|
|
27
34
|
@dataclass
|
|
28
35
|
class Source:
|
|
@@ -86,6 +93,7 @@ class Recipe:
|
|
|
86
93
|
host: Host = field(default_factory=Host)
|
|
87
94
|
graphs: List[Graph] = field(default_factory=list)
|
|
88
95
|
precision_support: Dict[str, str] = field(default_factory=dict) # {precision: status} — LLM, FR-L06
|
|
96
|
+
runtime_support: Dict[str, Dict[str, str]] = field(default_factory=dict) # {runtime: {arch: status}} — FR-RT04
|
|
89
97
|
|
|
90
98
|
@property
|
|
91
99
|
def quant_tag(self) -> str:
|
|
@@ -101,6 +109,15 @@ class Recipe:
|
|
|
101
109
|
"""The precisions this recipe declares runnable through the pipeline."""
|
|
102
110
|
return [p for p, s in self.precision_support.items() if s == "supported"]
|
|
103
111
|
|
|
112
|
+
def runtime_status(self, runtime: str, arch: str) -> Optional[str]:
|
|
113
|
+
"""The declared status for `runtime` on `arch` ('supported'/'deprecated:<s>'/'unsupported:<r>'), or None."""
|
|
114
|
+
return self.runtime_support.get(runtime, {}).get(arch)
|
|
115
|
+
|
|
116
|
+
def runnable_runtimes(self, arch: str) -> List[str]:
|
|
117
|
+
"""The runtimes this recipe declares runnable on `arch` — 'supported' OR 'deprecated' (not 'unsupported')."""
|
|
118
|
+
return sorted(rt for rt, am in self.runtime_support.items()
|
|
119
|
+
if str(am.get(arch, "")).split(":", 1)[0] in ("supported", "deprecated"))
|
|
120
|
+
|
|
104
121
|
def adaptation(self, kind: str) -> Optional[Adaptation]:
|
|
105
122
|
"""The declared adaptation of a given kind, or None (e.g. recipe.adaptation('erf_to_tanh'))."""
|
|
106
123
|
return next((a for a in self.adaptations if a.kind == kind), None)
|
|
@@ -153,6 +170,41 @@ def _parse_precision_support(items: Any) -> Dict[str, str]:
|
|
|
153
170
|
return out
|
|
154
171
|
|
|
155
172
|
|
|
173
|
+
def _parse_runtime_support(items: Any) -> Dict[str, Dict[str, str]]:
|
|
174
|
+
"""Parse + validate `runtime_support:` — a nested map {runtime: {arch: status}}. Raises ValueError if malformed.
|
|
175
|
+
|
|
176
|
+
A near-clone of `_parse_precision_support`, one level deeper (runtime -> arch -> status) because a
|
|
177
|
+
runtime's support is per-arch. A status must be "supported" or "<deprecated|unsupported>:<tail>" with a
|
|
178
|
+
non-empty tail (the successor for `deprecated`, the reason for `unsupported`) — so a recipe cannot smuggle
|
|
179
|
+
in a vague "no": a limitation is named or it is not a valid recipe (the same strictness as the tag grammar).
|
|
180
|
+
"""
|
|
181
|
+
if not items:
|
|
182
|
+
return {}
|
|
183
|
+
if not isinstance(items, dict):
|
|
184
|
+
raise ValueError(f"runtime_support must be a mapping {{runtime: {{arch: status}}}}, got {type(items).__name__}")
|
|
185
|
+
out: Dict[str, Dict[str, str]] = {}
|
|
186
|
+
for runtime, archmap in items.items():
|
|
187
|
+
if not isinstance(archmap, dict):
|
|
188
|
+
raise ValueError(
|
|
189
|
+
f"runtime_support[{runtime!r}] must be a mapping {{arch: status}}, got {type(archmap).__name__}")
|
|
190
|
+
per_arch: Dict[str, str] = {}
|
|
191
|
+
for arch, status in archmap.items():
|
|
192
|
+
status = str(status)
|
|
193
|
+
head = status.split(":", 1)[0]
|
|
194
|
+
if head not in _RUNTIME_KINDS:
|
|
195
|
+
raise ValueError(
|
|
196
|
+
f"runtime_support[{runtime!r}][{arch!r}] = {status!r}: status must start with one of "
|
|
197
|
+
f"{_RUNTIME_KINDS} (e.g. 'supported' or 'unsupported:htp-backend-excludes-v68')")
|
|
198
|
+
if head != "supported" and (":" not in status or not status.split(":", 1)[1].strip()):
|
|
199
|
+
raise ValueError(
|
|
200
|
+
f"runtime_support[{runtime!r}][{arch!r}] = {status!r}: a {head!r} status must name a "
|
|
201
|
+
f"reason/successor after the colon (e.g. 'deprecated:geniex-qairt' or "
|
|
202
|
+
f"'unsupported:htp-backend-excludes-v68'); only 'supported' stands alone")
|
|
203
|
+
per_arch[str(arch)] = status
|
|
204
|
+
out[str(runtime)] = per_arch
|
|
205
|
+
return out
|
|
206
|
+
|
|
207
|
+
|
|
156
208
|
def from_dict(doc: Dict[str, Any]) -> Recipe:
|
|
157
209
|
"""Build a Recipe from an already-parsed YAML mapping. Pure — no I/O. Raises ValueError if malformed."""
|
|
158
210
|
if not isinstance(doc, dict):
|
|
@@ -195,7 +247,8 @@ def from_dict(doc: Dict[str, Any]) -> Recipe:
|
|
|
195
247
|
return Recipe(name=str(doc["name"]), version=str(doc["version"]), route=route,
|
|
196
248
|
source=source, target=target, adaptations=_parse_adaptations(doc.get("adaptations")),
|
|
197
249
|
quant=quant, host=host, graphs=graphs,
|
|
198
|
-
precision_support=_parse_precision_support(doc.get("precision_support"))
|
|
250
|
+
precision_support=_parse_precision_support(doc.get("precision_support")),
|
|
251
|
+
runtime_support=_parse_runtime_support(doc.get("runtime_support")))
|
|
199
252
|
|
|
200
253
|
|
|
201
254
|
def load(path: str) -> Recipe:
|
|
@@ -57,6 +57,8 @@ class Registry:
|
|
|
57
57
|
url=v.get("url"), sha256=v.get("sha256"),
|
|
58
58
|
local=v.get("local"), run=v.get("run"),
|
|
59
59
|
recipe=v.get("recipe"), verified=v.get("verified"),
|
|
60
|
+
runtime=v.get("runtime"), runtime_support=v.get("runtime_support"),
|
|
61
|
+
precision_support=v.get("precision_support"),
|
|
60
62
|
))
|
|
61
63
|
self._models[model] = vs
|
|
62
64
|
|
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
"""Resolver — match registry variants against a board's TargetKey, with an explicit miss path.
|
|
2
|
+
|
|
3
|
+
The resolution rules, in order:
|
|
4
|
+
|
|
5
|
+
1. Exact match on arch, quant, shape; SDK inside the compatible range -> USE
|
|
6
|
+
2. Match, but SDK outside the tested point (still in range) -> USE_WITH_WARN
|
|
7
|
+
3. No artifact, but a recipe whose target can be built -> BUILD (offer, estimate)
|
|
8
|
+
4. Nothing -> FAIL, name the axis
|
|
9
|
+
|
|
10
|
+
Never silently downgrade to a near-miss artifact. The arch axis is hard (a mismatch fails at load);
|
|
11
|
+
the SDK axis is a range, not a point (warn outside the tested point, don't refuse).
|
|
12
|
+
"""
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from dataclasses import dataclass
|
|
16
|
+
from enum import Enum
|
|
17
|
+
from typing import List, Optional
|
|
18
|
+
|
|
19
|
+
from .env import TargetKey
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class Outcome(Enum):
|
|
23
|
+
USE = "use" # exact match — run it
|
|
24
|
+
USE_WITH_WARN = "use_warn" # in-range but outside the tested SDK point
|
|
25
|
+
BUILD = "build" # no artifact, but a recipe can produce one
|
|
26
|
+
FAIL = "fail" # nothing — the named axis is why
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass
|
|
30
|
+
class Variant:
|
|
31
|
+
"""One build variant from the registry (the registry's unit is a variant, not a model)."""
|
|
32
|
+
model: str
|
|
33
|
+
version: str
|
|
34
|
+
arch: str
|
|
35
|
+
quant: str
|
|
36
|
+
shape: str
|
|
37
|
+
sdk_tested: str # e.g. "2.37.1"
|
|
38
|
+
sdk_compatible: str # e.g. ">=2.34,<2.40"
|
|
39
|
+
ext: str = "qnnctx"
|
|
40
|
+
url: Optional[str] = None # present => a built artifact exists (remote)
|
|
41
|
+
sha256: Optional[str] = None
|
|
42
|
+
local: Optional[str] = None # present => an artifact file on disk (not yet hosted)
|
|
43
|
+
run: Optional[dict] = None # how `run` executes it: {modality, meta, cpu_onnx, board_name, input}
|
|
44
|
+
recipe: Optional[str] = None # present => can be built when no artifact matches
|
|
45
|
+
verified: Optional[dict] = None # the verification record (loads / cosine / task / latency)
|
|
46
|
+
precision_support: Optional[dict] = None # LLM: {precision: "supported"|"blocked:<r>"|"unsupported:<r>"}
|
|
47
|
+
runtime: Optional[str] = None # the inference runtime this variant binds to (None => classic genie)
|
|
48
|
+
runtime_support: Optional[dict] = None # {runtime: {arch: "supported"|"deprecated:<s>"|"unsupported:<r>"}} — FR-RT04
|
|
49
|
+
|
|
50
|
+
@property
|
|
51
|
+
def has_artifact(self) -> bool:
|
|
52
|
+
return bool(self.url or self.local)
|
|
53
|
+
|
|
54
|
+
@property
|
|
55
|
+
def effective_runtime(self) -> str:
|
|
56
|
+
"""The runtime this variant runs on. Historically every variant silently assumed classic Genie
|
|
57
|
+
(spec 004), so an unset runtime IS `genie` — the deprecating-but-default floor."""
|
|
58
|
+
return self.runtime or "genie"
|
|
59
|
+
|
|
60
|
+
@property
|
|
61
|
+
def has_recipe(self) -> bool:
|
|
62
|
+
return bool(self.recipe)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@dataclass
|
|
66
|
+
class Decision:
|
|
67
|
+
outcome: Outcome
|
|
68
|
+
variant: Optional[Variant] = None
|
|
69
|
+
reason: str = ""
|
|
70
|
+
axis: Optional[str] = None # on FAIL: which axis could not be satisfied
|
|
71
|
+
warning: Optional[str] = None # on USE_WITH_WARN
|
|
72
|
+
|
|
73
|
+
@property
|
|
74
|
+
def ok(self) -> bool:
|
|
75
|
+
return self.outcome in (Outcome.USE, Outcome.USE_WITH_WARN)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
# --- precision-support axis (LLM) -------------------------------------------------------------
|
|
79
|
+
# Not every precision survives the qai-hub -> Genie pipeline (spec 002, FR-L06): w4a16 is supported,
|
|
80
|
+
# w8a16 is blocked on a KV-cache-encoding divergence at convert, fp16 is unsupported because Genie's
|
|
81
|
+
# transformer path is integer-only. A recipe declares this per model; `resolve` refuses a non-supported
|
|
82
|
+
# precision and NAMES the reason — the same discipline as FAIL-names-the-axis, applied one axis earlier.
|
|
83
|
+
_SUPPORTED = "supported"
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def precision_refusal(precision: Optional[str], support: Optional[dict]) -> Optional[str]:
|
|
87
|
+
"""Return the named reason to refuse `precision`, or None if it is supported / not constrained.
|
|
88
|
+
|
|
89
|
+
Pure and tiny so it is trivial to test. A precision absent from the matrix is *not* refused here
|
|
90
|
+
(the matrix only constrains what it declares); the normal variant/quant matching handles the rest.
|
|
91
|
+
A declared non-"supported" status (e.g. "blocked:kv-cache-encoding") is returned verbatim so the
|
|
92
|
+
caller can quote the reason by name.
|
|
93
|
+
"""
|
|
94
|
+
if not precision or not support:
|
|
95
|
+
return None
|
|
96
|
+
status = support.get(precision)
|
|
97
|
+
if status is None or status == _SUPPORTED:
|
|
98
|
+
return None
|
|
99
|
+
return str(status)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
# --- runtime axis (spec 004) ------------------------------------------------------------------
|
|
103
|
+
# The same shape as precision, one axis over. A model+board can be served by different inference runtimes
|
|
104
|
+
# ({genie, geniex-qairt, llamacpp-htp}) with genuinely different arch + precision support (classic Genie is
|
|
105
|
+
# w4a16-only and deprecating; the llama.cpp HTP backend does Q4_0/Q8_0 on v73+ but NOT v68). So `resolve`
|
|
106
|
+
# must (a) refuse a runtime the board's arch cannot host, BY NAME (`axis=runtime`), and (b) among the
|
|
107
|
+
# runtimes the board CAN host, pick the one matching the caller's priority. The support table is per-arch:
|
|
108
|
+
# `{runtime: {arch: status}}`, validated in recipe.py (`_parse_runtime_support`).
|
|
109
|
+
#
|
|
110
|
+
# Priority -> the order runtimes are preferred in, among those runnable on the board's arch. Grounded in the
|
|
111
|
+
# v73 measurement: llamacpp-htp Q8_0 is the quality/prefill win (WikiText PPL 11.66, near-lossless, the
|
|
112
|
+
# >4-bit integer route Genie structurally lacks) but regresses decode (14.4 vs 22.5 tok/s); genie w4a16 is
|
|
113
|
+
# the decode win and the default floor. So quality-first prefers llamacpp-htp, decode-latency-first prefers
|
|
114
|
+
# genie, and the no-priority default is the conservative floor (genie) with a warn to choose explicitly.
|
|
115
|
+
_RUNTIME_PRIORITY = {
|
|
116
|
+
"quality": ("llamacpp-htp", "geniex-qairt", "genie"),
|
|
117
|
+
"decode-latency": ("genie", "geniex-qairt", "llamacpp-htp"),
|
|
118
|
+
}
|
|
119
|
+
_DEFAULT_RUNTIME_ORDER = ("genie", "geniex-qairt", "llamacpp-htp")
|
|
120
|
+
_RUNNABLE = ("supported", "deprecated") # 'unsupported' is the only hard refusal; 'deprecated' still runs
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def runtime_refusal(runtime: Optional[str], arch: str, support: Optional[dict]) -> Optional[str]:
|
|
124
|
+
"""Return the named reason to refuse `runtime` on `arch`, or None if it is runnable / not constrained.
|
|
125
|
+
|
|
126
|
+
Pure and tiny, mirroring `precision_refusal`. A runtime absent from the matrix (or an arch the matrix
|
|
127
|
+
doesn't declare for it) is *not* refused here — the matrix only constrains what it declares. Only an
|
|
128
|
+
explicit `unsupported:<reason>` is a refusal; a `deprecated:<successor>` status still runs (resolve warns).
|
|
129
|
+
"""
|
|
130
|
+
if not runtime or not support:
|
|
131
|
+
return None
|
|
132
|
+
archmap = support.get(runtime)
|
|
133
|
+
if not isinstance(archmap, dict):
|
|
134
|
+
return None
|
|
135
|
+
status = archmap.get(arch)
|
|
136
|
+
if status is None or str(status).split(":", 1)[0] != "unsupported":
|
|
137
|
+
return None
|
|
138
|
+
return str(status)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def runnable_runtimes(arch: str, support: Optional[dict]) -> List[str]:
|
|
142
|
+
"""The runtimes the matrix declares runnable on `arch` (status 'supported' or 'deprecated')."""
|
|
143
|
+
if not support:
|
|
144
|
+
return []
|
|
145
|
+
return sorted(rt for rt, am in support.items()
|
|
146
|
+
if isinstance(am, dict) and str(am.get(arch, "")).split(":", 1)[0] in _RUNNABLE)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def pick_runtime(runtimes: List[str], priority: Optional[str]):
|
|
150
|
+
"""Choose one runtime from `runtimes` (all runnable on the board's arch) by `priority`.
|
|
151
|
+
|
|
152
|
+
Returns (chosen, note). `note` records the trade-off (which runtime was chosen over which) so the
|
|
153
|
+
Decision can carry it — tested-is-a-record, now across runtimes.
|
|
154
|
+
"""
|
|
155
|
+
order = _RUNTIME_PRIORITY.get(priority)
|
|
156
|
+
if order:
|
|
157
|
+
for rt in order:
|
|
158
|
+
if rt in runtimes:
|
|
159
|
+
others = [r for r in runtimes if r != rt]
|
|
160
|
+
return rt, f"priority={priority}: chose runtime {rt} over {others}"
|
|
161
|
+
# no (or unknown) priority -> the conservative default floor, and say how to choose
|
|
162
|
+
for rt in _DEFAULT_RUNTIME_ORDER:
|
|
163
|
+
if rt in runtimes:
|
|
164
|
+
others = [r for r in runtimes if r != rt]
|
|
165
|
+
return rt, (f"multiple runtimes available {sorted(runtimes)}; defaulted to {rt} "
|
|
166
|
+
f"(pass priority=quality|decode-latency to choose)" + (f"; alternatives {others}" if others else ""))
|
|
167
|
+
rt = sorted(runtimes)[0] # only unrecognised runtimes present
|
|
168
|
+
return rt, f"multiple runtimes {sorted(runtimes)}; defaulted to {rt}"
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def _merge_warn(*parts: Optional[str]) -> Optional[str]:
|
|
172
|
+
"""Join the non-empty warning fragments, or None."""
|
|
173
|
+
return " | ".join(p for p in parts if p) or None
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
# --- tiny semver range check (enough for QAIRT x.y.z) -----------------------------------------
|
|
177
|
+
def _ver(v: str):
|
|
178
|
+
return tuple(int(x) for x in (v.split("-")[0].split("+")[0].split(".") + ["0", "0"])[:3])
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def _in_range(point: str, rng: str) -> bool:
|
|
182
|
+
"""point like '2.37.1' against a comma range like '>=2.34,<2.40'."""
|
|
183
|
+
p = _ver(point)
|
|
184
|
+
for clause in rng.split(","):
|
|
185
|
+
clause = clause.strip()
|
|
186
|
+
for op in (">=", "<=", "==", ">", "<"):
|
|
187
|
+
if clause.startswith(op):
|
|
188
|
+
bound = _ver(clause[len(op):])
|
|
189
|
+
ok = {">=": p >= bound, "<=": p <= bound, "==": p == bound,
|
|
190
|
+
">": p > bound, "<": p < bound}[op]
|
|
191
|
+
if not ok:
|
|
192
|
+
return False
|
|
193
|
+
break
|
|
194
|
+
return True
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def resolve(target: TargetKey, variants: List[Variant],
|
|
198
|
+
want_quant: Optional[str] = None, want_shape: Optional[str] = None,
|
|
199
|
+
precision_support: Optional[dict] = None,
|
|
200
|
+
want_runtime: Optional[str] = None, priority: Optional[str] = None,
|
|
201
|
+
runtime_support: Optional[dict] = None) -> Decision:
|
|
202
|
+
"""Return the single Decision for this board. Pure function — easy to test without a board."""
|
|
203
|
+
if not variants:
|
|
204
|
+
return Decision(Outcome.FAIL, reason="no variants registered for this model", axis="model")
|
|
205
|
+
|
|
206
|
+
# The runtime-support matrix may be passed explicitly (e.g. straight from the recipe) or carried on the
|
|
207
|
+
# variants (the registry populates it). Resolve it once so both the refusal and the selection use it.
|
|
208
|
+
rsupport = runtime_support
|
|
209
|
+
if rsupport is None:
|
|
210
|
+
rsupport = next((v.runtime_support for v in variants if v.runtime_support), None)
|
|
211
|
+
|
|
212
|
+
# Axis 0b (runtime refusal): mirror the precision refusal one axis over. If the caller names a runtime
|
|
213
|
+
# the board's arch cannot host, refuse up front and NAME it — listing the runtimes that ARE runnable on
|
|
214
|
+
# this arch — before arch/SDK matching, because the runtime is unavailable on this silicon regardless of
|
|
215
|
+
# which artifact is in hand (FAIL-names-the-axis, FR-RT03 / SC-RT02).
|
|
216
|
+
if want_runtime:
|
|
217
|
+
reason = runtime_refusal(want_runtime, target.htp_arch, rsupport)
|
|
218
|
+
if reason:
|
|
219
|
+
ok = runnable_runtimes(target.htp_arch, rsupport)
|
|
220
|
+
return Decision(Outcome.FAIL, axis="runtime",
|
|
221
|
+
reason=f"runtime {want_runtime} is not available on {target.htp_arch}: {reason} "
|
|
222
|
+
f"(supported on {target.htp_arch}: {ok})")
|
|
223
|
+
|
|
224
|
+
# Axis 0 (LLM precision-support): if the caller asked for a precision the model declares as
|
|
225
|
+
# blocked/unsupported, refuse up front and name the reason — before arch/SDK matching, because the
|
|
226
|
+
# precision is unavailable through the pipeline regardless of which board is in hand. The matrix may
|
|
227
|
+
# be passed explicitly or carried on the variants (the registry populates it per model).
|
|
228
|
+
if want_quant:
|
|
229
|
+
support = precision_support
|
|
230
|
+
if support is None:
|
|
231
|
+
support = next((v.precision_support for v in variants if v.precision_support), None)
|
|
232
|
+
reason = precision_refusal(want_quant, support)
|
|
233
|
+
if reason:
|
|
234
|
+
return Decision(Outcome.FAIL, axis="precision",
|
|
235
|
+
reason=f"precision {want_quant} is not available: {reason}")
|
|
236
|
+
|
|
237
|
+
# Axis 1 (hard): Hexagon architecture must match exactly, or the binary fails at load.
|
|
238
|
+
arch_ok = [v for v in variants if v.arch == target.htp_arch]
|
|
239
|
+
if not arch_ok:
|
|
240
|
+
have = sorted({v.arch for v in variants})
|
|
241
|
+
return Decision(Outcome.FAIL, axis="htp_arch",
|
|
242
|
+
reason=f"no variant for arch {target.htp_arch}; registry has {have}")
|
|
243
|
+
|
|
244
|
+
# Optional narrowing on quant / shape when the caller asked for a specific one.
|
|
245
|
+
cand = arch_ok
|
|
246
|
+
if want_quant:
|
|
247
|
+
c2 = [v for v in cand if v.quant == want_quant]
|
|
248
|
+
if not c2:
|
|
249
|
+
have = sorted({v.quant for v in cand})
|
|
250
|
+
return Decision(Outcome.FAIL, axis="quant",
|
|
251
|
+
reason=f"no {want_quant} for {target.htp_arch}; have {have}")
|
|
252
|
+
cand = c2
|
|
253
|
+
if want_shape:
|
|
254
|
+
c2 = [v for v in cand if v.shape == want_shape]
|
|
255
|
+
if not c2:
|
|
256
|
+
have = sorted({v.shape for v in cand})
|
|
257
|
+
return Decision(Outcome.FAIL, axis="shape",
|
|
258
|
+
reason=f"no shape {want_shape} for {target.htp_arch}; have {have}")
|
|
259
|
+
cand = c2
|
|
260
|
+
|
|
261
|
+
# Runtime axis (spec 004): pick WHICH runtime among the candidates runnable on this board's arch.
|
|
262
|
+
# If the caller named a runtime, narrow to it (its refusal was already handled above). Otherwise, only
|
|
263
|
+
# when the candidates span more than one runtime do we choose by priority — a single-runtime model
|
|
264
|
+
# resolves exactly as before, so nothing that pre-dates the runtime axis changes behaviour.
|
|
265
|
+
runtime_warning = None
|
|
266
|
+
if want_runtime:
|
|
267
|
+
c2 = [v for v in cand if v.effective_runtime == want_runtime]
|
|
268
|
+
if not c2:
|
|
269
|
+
have = sorted({v.effective_runtime for v in cand})
|
|
270
|
+
return Decision(Outcome.FAIL, axis="runtime",
|
|
271
|
+
reason=f"runtime {want_runtime} has no variant for {target.htp_arch}; have {have}")
|
|
272
|
+
cand = c2
|
|
273
|
+
else:
|
|
274
|
+
runtimes_here = sorted({v.effective_runtime for v in cand})
|
|
275
|
+
if len(runtimes_here) > 1:
|
|
276
|
+
chosen, runtime_warning = pick_runtime(runtimes_here, priority)
|
|
277
|
+
cand = [v for v in cand if v.effective_runtime == chosen]
|
|
278
|
+
# Warn if the chosen runtime is marked deprecated on this arch (its successor is named in the matrix).
|
|
279
|
+
if rsupport and cand:
|
|
280
|
+
dep = rsupport.get(cand[0].effective_runtime, {})
|
|
281
|
+
dep = dep.get(target.htp_arch, "") if isinstance(dep, dict) else ""
|
|
282
|
+
if str(dep).split(":", 1)[0] == "deprecated":
|
|
283
|
+
runtime_warning = _merge_warn(runtime_warning,
|
|
284
|
+
f"runtime {cand[0].effective_runtime} on {target.htp_arch} is {dep}")
|
|
285
|
+
|
|
286
|
+
built = [v for v in cand if v.has_artifact]
|
|
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
|
+
|
|
299
|
+
# Axis: SDK range. If we don't know the board's QAIRT (offline), we can't check the point —
|
|
300
|
+
# prefer a built artifact and warn that the SDK match is unverified.
|
|
301
|
+
if built:
|
|
302
|
+
if target.qairt is None:
|
|
303
|
+
v = built[0]
|
|
304
|
+
return Decision(Outcome.USE_WITH_WARN, variant=v,
|
|
305
|
+
warning=_merge_warn("board QAIRT unknown (offline) — SDK compatibility unverified",
|
|
306
|
+
runtime_warning),
|
|
307
|
+
reason="artifact matches arch/quant/shape")
|
|
308
|
+
in_range = [v for v in built if _in_range(target.qairt, v.sdk_compatible)]
|
|
309
|
+
if in_range:
|
|
310
|
+
exact = [v for v in in_range if _ver(v.sdk_tested) == _ver(target.qairt)]
|
|
311
|
+
if exact:
|
|
312
|
+
return Decision(Outcome.USE, variant=exact[0], reason="exact match", warning=runtime_warning)
|
|
313
|
+
v = in_range[0]
|
|
314
|
+
return Decision(Outcome.USE_WITH_WARN, variant=v,
|
|
315
|
+
warning=_merge_warn(f"board QAIRT {target.qairt} is in range {v.sdk_compatible} "
|
|
316
|
+
f"but the artifact was tested at {v.sdk_tested}", runtime_warning),
|
|
317
|
+
reason="in-range, outside tested point")
|
|
318
|
+
# artifact exists but SDK is out of every range -> fall through to recipe/fail on the SDK axis
|
|
319
|
+
sdk_axis = True
|
|
320
|
+
else:
|
|
321
|
+
sdk_axis = False
|
|
322
|
+
|
|
323
|
+
# Axis 3: no usable artifact, but a recipe can build one for this target.
|
|
324
|
+
buildable = [v for v in cand if v.has_recipe]
|
|
325
|
+
if buildable:
|
|
326
|
+
return Decision(Outcome.BUILD, variant=buildable[0], warning=runtime_warning,
|
|
327
|
+
reason=("no artifact matches the board SDK; a recipe can build one"
|
|
328
|
+
if sdk_axis else "no built artifact, but a recipe exists"))
|
|
329
|
+
|
|
330
|
+
# Axis 4: nothing.
|
|
331
|
+
if sdk_axis:
|
|
332
|
+
ranges = sorted({v.sdk_compatible for v in built})
|
|
333
|
+
return Decision(Outcome.FAIL, axis="qairt",
|
|
334
|
+
reason=f"board QAIRT {target.qairt} outside every artifact range {ranges}, and no recipe")
|
|
335
|
+
return Decision(Outcome.FAIL, axis="artifact",
|
|
336
|
+
reason="arch matches but no artifact and no recipe for this target")
|