chipwright 0.2.1__tar.gz → 0.2.3__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 (58) hide show
  1. {chipwright-0.2.1 → chipwright-0.2.3}/PKG-INFO +49 -16
  2. {chipwright-0.2.1 → chipwright-0.2.3}/README.md +48 -15
  3. {chipwright-0.2.1 → chipwright-0.2.3}/chipwright/__init__.py +1 -1
  4. chipwright-0.2.3/chipwright/cache.py +111 -0
  5. {chipwright-0.2.1 → chipwright-0.2.3}/chipwright/cli.py +85 -15
  6. {chipwright-0.2.1 → chipwright-0.2.3}/chipwright/data/index.yaml +40 -0
  7. {chipwright-0.2.1 → chipwright-0.2.3}/chipwright/device.py +25 -2
  8. {chipwright-0.2.1 → chipwright-0.2.3}/chipwright/registry.py +6 -0
  9. {chipwright-0.2.1 → chipwright-0.2.3}/chipwright/run.py +1 -1
  10. {chipwright-0.2.1 → chipwright-0.2.3}/chipwright/tests/test_cache.py +6 -6
  11. chipwright-0.2.3/chipwright/tests/test_cli_ux.py +133 -0
  12. chipwright-0.2.3/chipwright/tests/test_device_ctx.py +81 -0
  13. {chipwright-0.2.1 → chipwright-0.2.3}/chipwright/tests/test_llm_modality.py +4 -0
  14. {chipwright-0.2.1 → chipwright-0.2.3}/chipwright/tests/test_packaging.py +1 -1
  15. chipwright-0.2.3/chipwright/tests/test_security.py +135 -0
  16. {chipwright-0.2.1 → chipwright-0.2.3}/chipwright.egg-info/PKG-INFO +49 -16
  17. {chipwright-0.2.1 → chipwright-0.2.3}/chipwright.egg-info/SOURCES.txt +3 -0
  18. {chipwright-0.2.1 → chipwright-0.2.3}/pyproject.toml +1 -1
  19. chipwright-0.2.1/chipwright/cache.py +0 -65
  20. {chipwright-0.2.1 → chipwright-0.2.3}/LICENSE +0 -0
  21. {chipwright-0.2.1 → chipwright-0.2.3}/chipwright/adapt.py +0 -0
  22. {chipwright-0.2.1 → chipwright-0.2.3}/chipwright/build.py +0 -0
  23. {chipwright-0.2.1 → chipwright-0.2.3}/chipwright/config.py +0 -0
  24. {chipwright-0.2.1 → chipwright-0.2.3}/chipwright/data/behavior_prompts.yaml +0 -0
  25. {chipwright-0.2.1 → chipwright-0.2.3}/chipwright/device_adb.py +0 -0
  26. {chipwright-0.2.1 → chipwright-0.2.3}/chipwright/env.py +0 -0
  27. {chipwright-0.2.1 → chipwright-0.2.3}/chipwright/errors.py +0 -0
  28. {chipwright-0.2.1 → chipwright-0.2.3}/chipwright/llm_reference.py +0 -0
  29. {chipwright-0.2.1 → chipwright-0.2.3}/chipwright/modalities/__init__.py +0 -0
  30. {chipwright-0.2.1 → chipwright-0.2.3}/chipwright/modalities/asr_encoder.py +0 -0
  31. {chipwright-0.2.1 → chipwright-0.2.3}/chipwright/modalities/cv.py +0 -0
  32. {chipwright-0.2.1 → chipwright-0.2.3}/chipwright/modalities/llm.py +0 -0
  33. {chipwright-0.2.1 → chipwright-0.2.3}/chipwright/recipe.py +0 -0
  34. {chipwright-0.2.1 → chipwright-0.2.3}/chipwright/resolver.py +0 -0
  35. {chipwright-0.2.1 → chipwright-0.2.3}/chipwright/sdk.py +0 -0
  36. {chipwright-0.2.1 → chipwright-0.2.3}/chipwright/tags.py +0 -0
  37. {chipwright-0.2.1 → chipwright-0.2.3}/chipwright/task_eval.py +0 -0
  38. {chipwright-0.2.1 → chipwright-0.2.3}/chipwright/tests/conftest.py +0 -0
  39. {chipwright-0.2.1 → chipwright-0.2.3}/chipwright/tests/test_adapt.py +0 -0
  40. {chipwright-0.2.1 → chipwright-0.2.3}/chipwright/tests/test_build.py +0 -0
  41. {chipwright-0.2.1 → chipwright-0.2.3}/chipwright/tests/test_device_adb.py +0 -0
  42. {chipwright-0.2.1 → chipwright-0.2.3}/chipwright/tests/test_env.py +0 -0
  43. {chipwright-0.2.1 → chipwright-0.2.3}/chipwright/tests/test_env_adb.py +0 -0
  44. {chipwright-0.2.1 → chipwright-0.2.3}/chipwright/tests/test_precision_support.py +0 -0
  45. {chipwright-0.2.1 → chipwright-0.2.3}/chipwright/tests/test_registry.py +0 -0
  46. {chipwright-0.2.1 → chipwright-0.2.3}/chipwright/tests/test_resolver.py +0 -0
  47. {chipwright-0.2.1 → chipwright-0.2.3}/chipwright/tests/test_run_llm.py +0 -0
  48. {chipwright-0.2.1 → chipwright-0.2.3}/chipwright/tests/test_tags.py +0 -0
  49. {chipwright-0.2.1 → chipwright-0.2.3}/chipwright/tests/test_task_eval.py +0 -0
  50. {chipwright-0.2.1 → chipwright-0.2.3}/chipwright/tests/test_verify.py +0 -0
  51. {chipwright-0.2.1 → chipwright-0.2.3}/chipwright/tests/test_verify_llm.py +0 -0
  52. {chipwright-0.2.1 → chipwright-0.2.3}/chipwright/verify.py +0 -0
  53. {chipwright-0.2.1 → chipwright-0.2.3}/chipwright/verify_llm.py +0 -0
  54. {chipwright-0.2.1 → chipwright-0.2.3}/chipwright.egg-info/dependency_links.txt +0 -0
  55. {chipwright-0.2.1 → chipwright-0.2.3}/chipwright.egg-info/entry_points.txt +0 -0
  56. {chipwright-0.2.1 → chipwright-0.2.3}/chipwright.egg-info/requires.txt +0 -0
  57. {chipwright-0.2.1 → chipwright-0.2.3}/chipwright.egg-info/top_level.txt +0 -0
  58. {chipwright-0.2.1 → chipwright-0.2.3}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: chipwright
3
- Version: 0.2.1
3
+ Version: 0.2.3
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
@@ -79,10 +79,35 @@ Chipwright closes that gap. Think of it as **PyPI wheel tags for NPUs**: the chi
79
79
 
80
80
  ## Quickstart
81
81
 
82
+ ### Offline — no board, in under a minute
83
+
84
+ ```bash
85
+ pip install chipwright
86
+ ```
87
+
88
+ Resolve a hosted model against a hand-named target. No board, no QAIRT SDK on the machine:
89
+
82
90
  ```bash
83
- pip install -e .
91
+ chipwright --target htpv68 resolve mobilenet-v2
84
92
  ```
85
93
 
94
+ ```text
95
+ model : mobilenet-v2
96
+ target : htpv68
97
+ ▲ USE (warn) mobilenet-v2-1.0.0 htpv68/w8a16/img224
98
+ warning: board QAIRT unknown (offline) — SDK compatibility unverified
99
+ ```
100
+
101
+ That offline-SDK warning is **expected**: with no board to probe, Chipwright confirms the arch/quant/shape match but can't check the exact SDK point, so it says so rather than pretend. Global flags — `--target`, `--registry`, `--quant`, `--shape` — go **before** the subcommand.
102
+
103
+ The `run` verb and its verification gate need a real board (set `CW_BOARD_HOST` / `CW_BOARD_PASS`, below), and the CV verify demo needs the CPU-reference runner:
104
+
105
+ ```bash
106
+ pip install 'chipwright[cv]' # onnxruntime + pillow for the ONNX reference
107
+ ```
108
+
109
+ ### On a board
110
+
86
111
  Point Chipwright at a board (over SSH) and probe it:
87
112
 
88
113
  ```bash
@@ -103,32 +128,38 @@ chipwright doctor
103
128
  Resolve a model against that exact board — no guessing:
104
129
 
105
130
  ```bash
106
- chipwright resolve whisper-small-en
131
+ chipwright resolve mobilenet-v2
107
132
  ```
108
133
 
109
134
  ```text
110
- model : whisper-small-en
135
+ model : mobilenet-v2
111
136
  target : htpv68,qnn2.38
112
- ▲ USE (warn) whisper-small-en-1.2.0 htpv68/w8a16/win30s
113
- warning: artifact tested at qnn2.37.1; board runs 2.38.0 — inside compatible range >=2.34,<2.40
137
+ ▲ USE (warn) mobilenet-v2-1.0.0 htpv68/w8a16/img224
138
+ warning: board QAIRT 2.38 is in range >=2.34,<2.40 but the artifact was tested at 2.37.1
114
139
  ```
115
140
 
116
- Then run it on the board and verify the NPU output against a CPU reference:
141
+ Then run it on the board and verify the NPU output against a CPU reference (the input ships with the variant, so no `--input` is needed):
117
142
 
118
143
  ```bash
119
- chipwright run zipformer-enin --input utterance.npy
144
+ chipwright run mobilenet-v2
120
145
  ```
121
146
 
122
147
  ```text
123
- model : zipformer-enin
148
+ model : mobilenet-v2
124
149
  target : htpv68,qnn2.38
125
- ✔ USE zipformer-enin-7.0.0 htpv68/w8a16/win39 qnn2.37.1 (exact match)
150
+ ▲ USE (warn) mobilenet-v2-1.0.0 htpv68/w8a16/img224
151
+ pushing artifact to board (mobilenet) …
152
+ running on NPU + CPU reference …
126
153
 
127
- ✔ VERIFIED NPU vs CPU-reference cosine_min = 0.98800 (threshold 0.95)
128
- encoder: 0.98800
154
+ ✔ VERIFIED NPU vs CPU-reference cosine_min = 0.95600 (threshold 0.95)
129
155
  ```
130
156
 
131
- That `0.98800` is not a claim in a README. It is the number Chipwright measured on a live QCS6490 against an ONNX-Runtime reference, on the input you gave it, this run.
157
+ That `0.95600` is not a claim in a README. It is the number Chipwright measured on a live QCS6490 against an ONNX-Runtime reference, on the input in the variant, this run.
158
+
159
+ ### Targets and registries
160
+
161
+ - `--target <arch[,sdk]>` — resolve **offline** against a hand-named target instead of a probed board, e.g. `--target htpv68` or `--target htpv68,qnn2.37`. The arch is required; the SDK is optional — omit it and the SDK axis is reported unverified rather than guessed. It is a global flag, so it goes before the subcommand (`chipwright --target htpv68 resolve …`).
162
+ - `CW_REGISTRY` — point the client at an alternate index (a local path or a URL) instead of the public index bundled in the wheel. Use it to resolve/run against a dev or private registry; the author's development overlay is `registry/index.dev.yaml`.
132
163
 
133
164
  ---
134
165
 
@@ -138,7 +169,7 @@ That `0.98800` is not a claim in a README. It is the number Chipwright measured
138
169
  | --- | --- |
139
170
  | **resolve** | Match a registry variant to your board's target key. If nothing matches, name the axis that failed — never silently downgrade to a near-miss. |
140
171
  | **run** | `resolve` → fetch (or locate) the artifact → push and execute on the board → compare the NPU output to a CPU reference and report the cosine. |
141
- | **build** | Produce the artifact from a recipe when no hosted variant matches your chip — op pre-check, host preflight, convert → quantize → on-board context, then gate on the verification before publishing. |
172
+ | **build** | Produce the artifact from a recipe when no hosted variant matches your chip — op pre-check, host preflight, convert → quantize → on-board context, then gate on the verification before publishing. *(Specified and partial in 0.2.x — see [Status](#status); not yet a working end-to-end verb.)* |
142
173
 
143
174
  ---
144
175
 
@@ -241,7 +272,9 @@ registry/
241
272
  | `movenet` | pose estimation (17 keypoints) | w8a16 | **0.916** |
242
273
 
243
274
  Two input modalities, five CV tasks, two quant profiles — the quant axis is real, and every run ends in a measured number. The gate earns its keep: a random-noise input to MobileNet correctly **FAILs** at 0.776, and a MediaPipe face detector surfaced a genuine w8a8 fidelity loss on its secondary score head (0.80 vs 0.98 elsewhere) — caught, not shipped. The cosine spread is itself the signal: a ViT depth head converts near-perfectly (0.99996), while a pose keypoint+confidence head shows real quant softness (0.916) yet still clears the bar — the number tells you which is which. Getting that Depth-Anything ViT to convert at all needed a recipe adaptation (an unsupported `Reshape allowzero` attribute, patched across 29 nodes) — the honest boundary, handled rather than papered over.
244
- - ✅ **build** — **verified end-to-end**: `chipwright build mobilenet-cv` on a real x86_64-linux QAIRT host produced a w8a16 DLC (`…+qnn2.34-htpv68-w8a16-img224.dlc`) via `qairt-converter`, and `chipwright run` then verified that *built* artifact on the board — **cosine 0.956**. The full `resolve → build → run → verify` loop. Op pre-check, host preflight, declared graph adaptations, and a fidelity gate before publish are all in; on a machine without QAIRT the CLI says so plainly instead of pretending.
275
+
276
+ **What runs on a clean `pip install`:** `mobilenet-v2` is hosted in the bundled public registry — artifact, CPU reference and input all fetched by hash — so `chipwright run mobilenet-v2` works end-to-end on any board out of the box. The other five rows ship their **verification records** in the public index; their artifacts live in the author's development registry (point at it with `CW_REGISTRY`). The records are what the resolver and `list` surface; the artifacts travel separately.
277
+ - ⚠️ **build** — **specified and partial, not yet end-to-end in 0.2.x.** The `build` verb resolves a model, reports host preflight, and loads the recipe, but on a machine without the QAIRT toolchain it stops plainly — e.g. `chipwright build laya-intent` prints `▲ build host not ready — missing: qairt-converter, qairt-quantizer, qnn-context-binary-generator` and exits on the `preflight` axis instead of pretending. The convert → quantize → on-board-context pipeline, op pre-check, declared graph adaptations and the pre-publish fidelity gate are designed and in the recipes, but building a fresh artifact still requires an x86_64-linux QAIRT host and is not wired as a turnkey command yet. `mobilenet-cv-built` in the registry is the record of one artifact produced this way and then verified on the board (cosine 0.956); reproducing that build from a clean checkout is not a one-liner today. What is solid in 0.2.x is **resolve + run + verify**.
245
278
 
246
279
  **SDK-range in practice:** that same board runs QAIRT **2.38.0** while the artifact was tested at **2.37.1**, both inside the compatible range `>=2.34,<2.40`. It ran correctly — and `resolve` says so plainly (USE-with-warning), rather than pretending the tested point and the board agree.
247
280
 
@@ -265,4 +298,4 @@ The specification lives in a separate repo, the **spec kit** (`chipwright-specki
265
298
 
266
299
  ## License
267
300
 
268
- MIT — see [LICENSE](LICENSE). Contributions welcome; start with [CONTRIBUTING.md](CONTRIBUTING.md).
301
+ The Chipwright **framework** is MIT — see [LICENSE](LICENSE). The **models** it resolves and runs carry their own upstream licenses, recorded per entry in the registry index (for example the Qualcomm AI Hub CV models are `bsd-3-clause`, Whisper is `mit`, and the LLMs are `apache-2.0`); the MIT license covers this tool, not the model weights. Contributions welcome; start with [CONTRIBUTING.md](CONTRIBUTING.md).
@@ -43,10 +43,35 @@ Chipwright closes that gap. Think of it as **PyPI wheel tags for NPUs**: the chi
43
43
 
44
44
  ## Quickstart
45
45
 
46
+ ### Offline — no board, in under a minute
47
+
48
+ ```bash
49
+ pip install chipwright
50
+ ```
51
+
52
+ Resolve a hosted model against a hand-named target. No board, no QAIRT SDK on the machine:
53
+
46
54
  ```bash
47
- pip install -e .
55
+ chipwright --target htpv68 resolve mobilenet-v2
48
56
  ```
49
57
 
58
+ ```text
59
+ model : mobilenet-v2
60
+ target : htpv68
61
+ ▲ USE (warn) mobilenet-v2-1.0.0 htpv68/w8a16/img224
62
+ warning: board QAIRT unknown (offline) — SDK compatibility unverified
63
+ ```
64
+
65
+ That offline-SDK warning is **expected**: with no board to probe, Chipwright confirms the arch/quant/shape match but can't check the exact SDK point, so it says so rather than pretend. Global flags — `--target`, `--registry`, `--quant`, `--shape` — go **before** the subcommand.
66
+
67
+ The `run` verb and its verification gate need a real board (set `CW_BOARD_HOST` / `CW_BOARD_PASS`, below), and the CV verify demo needs the CPU-reference runner:
68
+
69
+ ```bash
70
+ pip install 'chipwright[cv]' # onnxruntime + pillow for the ONNX reference
71
+ ```
72
+
73
+ ### On a board
74
+
50
75
  Point Chipwright at a board (over SSH) and probe it:
51
76
 
52
77
  ```bash
@@ -67,32 +92,38 @@ chipwright doctor
67
92
  Resolve a model against that exact board — no guessing:
68
93
 
69
94
  ```bash
70
- chipwright resolve whisper-small-en
95
+ chipwright resolve mobilenet-v2
71
96
  ```
72
97
 
73
98
  ```text
74
- model : whisper-small-en
99
+ model : mobilenet-v2
75
100
  target : htpv68,qnn2.38
76
- ▲ USE (warn) whisper-small-en-1.2.0 htpv68/w8a16/win30s
77
- warning: artifact tested at qnn2.37.1; board runs 2.38.0 — inside compatible range >=2.34,<2.40
101
+ ▲ USE (warn) mobilenet-v2-1.0.0 htpv68/w8a16/img224
102
+ warning: board QAIRT 2.38 is in range >=2.34,<2.40 but the artifact was tested at 2.37.1
78
103
  ```
79
104
 
80
- Then run it on the board and verify the NPU output against a CPU reference:
105
+ Then run it on the board and verify the NPU output against a CPU reference (the input ships with the variant, so no `--input` is needed):
81
106
 
82
107
  ```bash
83
- chipwright run zipformer-enin --input utterance.npy
108
+ chipwright run mobilenet-v2
84
109
  ```
85
110
 
86
111
  ```text
87
- model : zipformer-enin
112
+ model : mobilenet-v2
88
113
  target : htpv68,qnn2.38
89
- ✔ USE zipformer-enin-7.0.0 htpv68/w8a16/win39 qnn2.37.1 (exact match)
114
+ ▲ USE (warn) mobilenet-v2-1.0.0 htpv68/w8a16/img224
115
+ pushing artifact to board (mobilenet) …
116
+ running on NPU + CPU reference …
90
117
 
91
- ✔ VERIFIED NPU vs CPU-reference cosine_min = 0.98800 (threshold 0.95)
92
- encoder: 0.98800
118
+ ✔ VERIFIED NPU vs CPU-reference cosine_min = 0.95600 (threshold 0.95)
93
119
  ```
94
120
 
95
- That `0.98800` is not a claim in a README. It is the number Chipwright measured on a live QCS6490 against an ONNX-Runtime reference, on the input you gave it, this run.
121
+ That `0.95600` is not a claim in a README. It is the number Chipwright measured on a live QCS6490 against an ONNX-Runtime reference, on the input in the variant, this run.
122
+
123
+ ### Targets and registries
124
+
125
+ - `--target <arch[,sdk]>` — resolve **offline** against a hand-named target instead of a probed board, e.g. `--target htpv68` or `--target htpv68,qnn2.37`. The arch is required; the SDK is optional — omit it and the SDK axis is reported unverified rather than guessed. It is a global flag, so it goes before the subcommand (`chipwright --target htpv68 resolve …`).
126
+ - `CW_REGISTRY` — point the client at an alternate index (a local path or a URL) instead of the public index bundled in the wheel. Use it to resolve/run against a dev or private registry; the author's development overlay is `registry/index.dev.yaml`.
96
127
 
97
128
  ---
98
129
 
@@ -102,7 +133,7 @@ That `0.98800` is not a claim in a README. It is the number Chipwright measured
102
133
  | --- | --- |
103
134
  | **resolve** | Match a registry variant to your board's target key. If nothing matches, name the axis that failed — never silently downgrade to a near-miss. |
104
135
  | **run** | `resolve` → fetch (or locate) the artifact → push and execute on the board → compare the NPU output to a CPU reference and report the cosine. |
105
- | **build** | Produce the artifact from a recipe when no hosted variant matches your chip — op pre-check, host preflight, convert → quantize → on-board context, then gate on the verification before publishing. |
136
+ | **build** | Produce the artifact from a recipe when no hosted variant matches your chip — op pre-check, host preflight, convert → quantize → on-board context, then gate on the verification before publishing. *(Specified and partial in 0.2.x — see [Status](#status); not yet a working end-to-end verb.)* |
106
137
 
107
138
  ---
108
139
 
@@ -205,7 +236,9 @@ registry/
205
236
  | `movenet` | pose estimation (17 keypoints) | w8a16 | **0.916** |
206
237
 
207
238
  Two input modalities, five CV tasks, two quant profiles — the quant axis is real, and every run ends in a measured number. The gate earns its keep: a random-noise input to MobileNet correctly **FAILs** at 0.776, and a MediaPipe face detector surfaced a genuine w8a8 fidelity loss on its secondary score head (0.80 vs 0.98 elsewhere) — caught, not shipped. The cosine spread is itself the signal: a ViT depth head converts near-perfectly (0.99996), while a pose keypoint+confidence head shows real quant softness (0.916) yet still clears the bar — the number tells you which is which. Getting that Depth-Anything ViT to convert at all needed a recipe adaptation (an unsupported `Reshape allowzero` attribute, patched across 29 nodes) — the honest boundary, handled rather than papered over.
208
- - ✅ **build** — **verified end-to-end**: `chipwright build mobilenet-cv` on a real x86_64-linux QAIRT host produced a w8a16 DLC (`…+qnn2.34-htpv68-w8a16-img224.dlc`) via `qairt-converter`, and `chipwright run` then verified that *built* artifact on the board — **cosine 0.956**. The full `resolve → build → run → verify` loop. Op pre-check, host preflight, declared graph adaptations, and a fidelity gate before publish are all in; on a machine without QAIRT the CLI says so plainly instead of pretending.
239
+
240
+ **What runs on a clean `pip install`:** `mobilenet-v2` is hosted in the bundled public registry — artifact, CPU reference and input all fetched by hash — so `chipwright run mobilenet-v2` works end-to-end on any board out of the box. The other five rows ship their **verification records** in the public index; their artifacts live in the author's development registry (point at it with `CW_REGISTRY`). The records are what the resolver and `list` surface; the artifacts travel separately.
241
+ - ⚠️ **build** — **specified and partial, not yet end-to-end in 0.2.x.** The `build` verb resolves a model, reports host preflight, and loads the recipe, but on a machine without the QAIRT toolchain it stops plainly — e.g. `chipwright build laya-intent` prints `▲ build host not ready — missing: qairt-converter, qairt-quantizer, qnn-context-binary-generator` and exits on the `preflight` axis instead of pretending. The convert → quantize → on-board-context pipeline, op pre-check, declared graph adaptations and the pre-publish fidelity gate are designed and in the recipes, but building a fresh artifact still requires an x86_64-linux QAIRT host and is not wired as a turnkey command yet. `mobilenet-cv-built` in the registry is the record of one artifact produced this way and then verified on the board (cosine 0.956); reproducing that build from a clean checkout is not a one-liner today. What is solid in 0.2.x is **resolve + run + verify**.
209
242
 
210
243
  **SDK-range in practice:** that same board runs QAIRT **2.38.0** while the artifact was tested at **2.37.1**, both inside the compatible range `>=2.34,<2.40`. It ran correctly — and `resolve` says so plainly (USE-with-warning), rather than pretending the tested point and the board agree.
211
244
 
@@ -229,4 +262,4 @@ The specification lives in a separate repo, the **spec kit** (`chipwright-specki
229
262
 
230
263
  ## License
231
264
 
232
- MIT — see [LICENSE](LICENSE). Contributions welcome; start with [CONTRIBUTING.md](CONTRIBUTING.md).
265
+ The Chipwright **framework** is MIT — see [LICENSE](LICENSE). The **models** it resolves and runs carry their own upstream licenses, recorded per entry in the registry index (for example the Qualcomm AI Hub CV models are `bsd-3-clause`, Whisper is `mit`, and the LLMs are `apache-2.0`); the MIT license covers this tool, not the model weights. Contributions welcome; start with [CONTRIBUTING.md](CONTRIBUTING.md).
@@ -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.1"
11
+ __version__ = "0.2.3"
@@ -0,0 +1,111 @@
1
+ """Content-addressed artifact cache at ~/.cache/chipwright/.
2
+
3
+ Addressed by sha256 so two models that share a build never store it twice (brief §"The manifest and
4
+ the registry"). Verifies the hash on fetch and on read — a corrupted or truncated download is caught
5
+ here, not at load on the board where the error reads like file corruption.
6
+
7
+ Hardening (spec 001, "refuse rather than guess", at the fetch boundary): every sha is validated as
8
+ 64-hex before it is trusted (a url with no/!bad sha is refused by name, never fetched); a reference's
9
+ filename is confined to its cache dir (a registry-supplied `name` with a separator or `..` cannot write
10
+ outside the cache); and a remote url must be https unless CW_ALLOW_INSECURE=1 (so an http CW_REGISTRY /
11
+ artifact cannot be silently MITM'd — the sha only protects you over a trusted transport).
12
+ """
13
+ from __future__ import annotations
14
+
15
+ import hashlib
16
+ import os
17
+ import re
18
+ import urllib.request
19
+ from urllib.parse import urlparse
20
+
21
+ CACHE_DIR = os.environ.get("CW_CACHE", os.path.expanduser("~/.cache/chipwright"))
22
+
23
+ _HEX64 = re.compile(r"^[0-9a-f]{64}$")
24
+
25
+
26
+ def _require_sha(sha256, what: str = "artifact") -> str:
27
+ """A fetch target MUST carry a 64-hex sha256. Refuse by name rather than fetch unverified bytes."""
28
+ if not isinstance(sha256, str) or not _HEX64.match(sha256):
29
+ raise RuntimeError(
30
+ f"refusing to fetch an unverified {what}: expected a 64-hex sha256, got {sha256!r}")
31
+ return sha256
32
+
33
+
34
+ def _safe_name(name: str) -> str:
35
+ """A reference filename must be a plain basename inside its cache dir — no separator, no `..`, not
36
+ absolute. A registry-supplied name that escapes the cache is refused (it could otherwise land
37
+ sha-matching bytes at an arbitrary host path during a normal run)."""
38
+ if (not isinstance(name, str) or name in ("", ".", "..")
39
+ or os.path.isabs(name) or os.path.basename(name) != name):
40
+ raise RuntimeError(f"unsafe reference filename {name!r}: must be a plain name with no path separator or '..'")
41
+ return name
42
+
43
+
44
+ def _check_scheme(url: str) -> None:
45
+ """A remote fetch must be https (the sha pins integrity only over a transport you trust; over http a
46
+ MITM can serve a matching (url, sha) pair). Local paths pass; opt out with CW_ALLOW_INSECURE=1."""
47
+ scheme = urlparse(url).scheme.lower()
48
+ if scheme in ("", "file"):
49
+ return
50
+ if scheme != "https" and os.environ.get("CW_ALLOW_INSECURE") != "1":
51
+ raise RuntimeError(
52
+ f"refusing an insecure {scheme or 'no'}-scheme url ({url}): remote artifacts/references must be "
53
+ f"https (set CW_ALLOW_INSECURE=1 to override)")
54
+
55
+
56
+ def _sha256(path: str, buf: int = 1 << 20) -> str:
57
+ h = hashlib.sha256()
58
+ with open(path, "rb") as f:
59
+ for b in iter(lambda: f.read(buf), b""):
60
+ h.update(b)
61
+ return h.hexdigest()
62
+
63
+
64
+ def path_for(sha256: str) -> str:
65
+ return os.path.join(CACHE_DIR, "blobs", sha256[:2], sha256)
66
+
67
+
68
+ def have(sha256: str) -> bool:
69
+ p = path_for(sha256)
70
+ return os.path.exists(p) and _sha256(p) == sha256
71
+
72
+
73
+ def _ensure(url: str, sha256: str, dst: str) -> str:
74
+ """Download url to dst (verified by sha256), skipping if dst already matches. Returns dst."""
75
+ _require_sha(sha256, "artifact")
76
+ _check_scheme(url)
77
+ if os.path.exists(dst) and _sha256(dst) == sha256:
78
+ return dst
79
+ os.makedirs(os.path.dirname(dst), exist_ok=True)
80
+ tmp = dst + ".part"
81
+ urllib.request.urlretrieve(url, tmp)
82
+ got = _sha256(tmp)
83
+ if got != sha256:
84
+ os.remove(tmp)
85
+ raise RuntimeError(f"sha256 mismatch for {url}: got {got}, expected {sha256}")
86
+ os.replace(tmp, dst)
87
+ return dst
88
+
89
+
90
+ def fetch(url: str, sha256: str) -> str:
91
+ """Download url into the content-addressed cache, verify the hash, return the local path."""
92
+ _require_sha(sha256, "artifact")
93
+ return _ensure(url, sha256, path_for(sha256))
94
+
95
+
96
+ def fetch_named(url: str, sha256: str, name: str, sidecars=None) -> str:
97
+ """Fetch a reference file (verified) into a per-reference dir, KEEPING its original filename, so an
98
+ ONNX's external-data sidecar (`model.data` beside `model.onnx`) resolves. `sidecars` = an iterable of
99
+ (url, sha256, name) fetched into the same dir. Returns the local path to `name`.
100
+
101
+ The content-addressed `fetch` names a blob by its hash, which breaks a model that loads a neighbour
102
+ by filename — so a run reference (cpu_onnx / input / meta) uses this instead. Every `name` is confined
103
+ to the cache dir (`_safe_name`) so a registry entry cannot write outside it.
104
+ """
105
+ _require_sha(sha256, "reference")
106
+ d = os.path.join(CACHE_DIR, "refs", sha256[:12])
107
+ main = _ensure(url, sha256, os.path.join(d, _safe_name(name)))
108
+ for s_url, s_sha, s_name in (sidecars or []):
109
+ _require_sha(s_sha, "reference sidecar")
110
+ _ensure(s_url, s_sha, os.path.join(d, _safe_name(s_name)))
111
+ return main
@@ -11,7 +11,9 @@ plainly rather than guessing.
11
11
  from __future__ import annotations
12
12
 
13
13
  import argparse
14
+ import importlib.util
14
15
  import os
16
+ import re
15
17
  import sys
16
18
 
17
19
  from . import cache
@@ -21,6 +23,55 @@ from .resolver import Outcome, resolve
21
23
 
22
24
  GREEN, YELLOW, RED, DIM, RST = "\033[32m", "\033[33m", "\033[31m", "\033[2m", "\033[0m"
23
25
 
26
+ # A bare Hexagon arch (what `list` and FAIL messages print, e.g. "v68") looks like a target but is
27
+ # missing the mandatory `htp` prefix. Used to turn a malformed --target into a helpful suggestion.
28
+ _ARCH_LIKE = re.compile(r"^v\d+$", re.IGNORECASE)
29
+
30
+ # Modality extras → the key top-level imports that must resolve for that verify path to work. The
31
+ # package list behind each extra is in pyproject.toml ([project.optional-dependencies]); here we name
32
+ # the IMPORT each pip package provides (pillow → PIL). `doctor` reports these so a user learns an extra
33
+ # is missing up front, not when a run fails.
34
+ _MODALITY_EXTRAS = {
35
+ "cv": ["onnxruntime", "PIL"], # image classifiers (onnxruntime + pillow)
36
+ "asr": ["onnxruntime"], # streaming encoder/decoder/joiner
37
+ "tts": ["onnxruntime"], # text-to-speech reference
38
+ "llm": ["onnxruntime", "tokenizers"], # CPU-reference decoder + genie retokenise
39
+ }
40
+
41
+
42
+ def _can_import(module: str) -> bool:
43
+ """True if `module` is importable — a read-only probe (find_spec, never actually imports)."""
44
+ try:
45
+ return importlib.util.find_spec(module) is not None
46
+ except Exception:
47
+ return False
48
+
49
+
50
+ def _manual_target(spec: str) -> TargetKey:
51
+ """Turn --target into a TargetKey, refusing a malformed spec cleanly instead of a traceback.
52
+
53
+ The tool prints bare archs (`v68`) in `list` and in FAIL messages, so a user naturally copies
54
+ `v68` into --target. Rather than crash, we name the axis and — when the token looks like a known
55
+ arch missing its `htp` prefix — suggest the corrected form. Exits 2, matching `refuse rather than
56
+ guess`.
57
+ """
58
+ try:
59
+ return TargetKey.manual(spec)
60
+ except ValueError:
61
+ suggestion = None
62
+ for tok in spec.replace(" ", "").split(","):
63
+ if _ARCH_LIKE.match(tok):
64
+ suggestion = "htp" + tok.lower()
65
+ break
66
+ if suggestion:
67
+ msg = (f"target {spec!r} — did you mean {suggestion!r}? "
68
+ f"(an arch must start with 'htp', e.g. htpv68,qnn2.37)")
69
+ else:
70
+ msg = (f"target {spec!r} — expected an arch starting with 'htp', "
71
+ f"e.g. htpv68,qnn2.37")
72
+ print(f"{RED}✗ {msg}{RST}", file=sys.stderr)
73
+ sys.exit(2)
74
+
24
75
 
25
76
  def _board_from(args) -> dict:
26
77
  return {
@@ -40,28 +91,47 @@ def _get_target(args) -> TargetKey:
40
91
  return t
41
92
  print(f"{YELLOW}board {b['host']} not reachable{RST}", file=sys.stderr)
42
93
  if args.target:
43
- return TargetKey.manual(args.target)
94
+ return _manual_target(args.target)
44
95
  print(f"{RED}no board reachable and no --target given{RST}", file=sys.stderr)
45
96
  sys.exit(2)
46
97
 
47
98
 
99
+ def _print_extras_report():
100
+ """Report, per modality extra, whether its verify-path imports resolve. Read-only and safe with
101
+ nothing installed: base `pip install chipwright` ships no onnxruntime/pillow/tokenizers, so a user
102
+ told the product `proves it against a CPU reference` learns here — not mid-run — which extra to add.
103
+ """
104
+ print(f"\n{DIM}modality extras (the verify / CPU-reference path needs these){RST}")
105
+ for extra in ("cv", "asr", "tts", "llm"):
106
+ mods = _MODALITY_EXTRAS[extra]
107
+ missing = [m for m in mods if not _can_import(m)]
108
+ if missing:
109
+ print(f" {RED}✗{RST} {extra:3s} missing {', '.join(missing)} "
110
+ f"— pip install chipwright[{extra}]")
111
+ else:
112
+ print(f" {GREEN}✓{RST} {extra:3s} installed ({', '.join(mods)})")
113
+
114
+
48
115
  def cmd_doctor(args):
49
116
  b = _board_from(args)
117
+ rc = 0
50
118
  if not (b["host"] and b["pw"]):
51
119
  print(f"{YELLOW}no board configured{RST} — set CW_BOARD_HOST / CW_BOARD_PASS (or --host/--pw)")
52
- return 0
53
- t = probe(b["host"], b["user"], b["pw"], b["port"])
54
- if not t.reachable:
55
- print(f"{RED}● unreachable{RST} {b['user']}@{b['host']}:{b['port']}")
56
- return 1
57
- print(f"{GREEN}● board reachable{RST} {b['user']}@{b['host']}:{b['port']}")
58
- print(f" SoC : {t.soc_name or '?'}" + (f" (soc_id {t.soc_id})" if t.soc_id else ""))
59
- print(f" Hexagon arch: {t.htp_arch}")
60
- print(f" QNN libs : {', '.join(t.qnn_libs) or '(none found)'}")
61
- if t.thermal_c is not None:
62
- print(f" temperature : {t.thermal_c} °C")
63
- print(f" target key : {t}")
64
- return 0
120
+ else:
121
+ t = probe(b["host"], b["user"], b["pw"], b["port"])
122
+ if not t.reachable:
123
+ print(f"{RED}● unreachable{RST} {b['user']}@{b['host']}:{b['port']}")
124
+ rc = 1
125
+ else:
126
+ print(f"{GREEN}● board reachable{RST} {b['user']}@{b['host']}:{b['port']}")
127
+ print(f" SoC : {t.soc_name or '?'}" + (f" (soc_id {t.soc_id})" if t.soc_id else ""))
128
+ print(f" Hexagon arch: {t.htp_arch}")
129
+ print(f" QNN libs : {', '.join(t.qnn_libs) or '(none found)'}")
130
+ if t.thermal_c is not None:
131
+ print(f" temperature : {t.thermal_c} °C")
132
+ print(f" target key : {t}")
133
+ _print_extras_report()
134
+ return rc
65
135
 
66
136
 
67
137
  def _print_decision(model, target, d):
@@ -144,7 +214,7 @@ def cmd_list(args):
144
214
  if t.reachable:
145
215
  target = t
146
216
  if target is None and args.target:
147
- target = TargetKey.manual(args.target)
217
+ target = _manual_target(args.target)
148
218
 
149
219
  head = f"{target}" if target else "no target — pass --target or configure a board to resolve"
150
220
  print(f"{DIM}catalogue · {len(reg.models())} models · {head}{RST}")
@@ -212,11 +212,21 @@ models:
212
212
  sdk_tested: 2.37.1
213
213
  sdk_compatible: '>=2.34,<2.40'
214
214
  ext: dlc
215
+ url: https://huggingface.co/nameissakthi/chipwright-cv/resolve/main/quicksrnet-small/quicksrnet_w8a8.dlc
216
+ sha256: 9ca46d230bd79205f103144df526eeedc47c505b7558d97d157a6ec46f493115
215
217
  run:
216
218
  modality: cv
217
219
  board_name: quicksrnet
218
220
  meta: ''
219
221
  threshold: 0.9
222
+ cpu_onnx: https://huggingface.co/nameissakthi/chipwright-cv/resolve/main/quicksrnet-small/quicksrnetsmall.onnx
223
+ cpu_onnx_sha256: 8ef99aecf6ea37c3da18eef6bcac94327cc3ead7069e55743425eb912dc2d941
224
+ cpu_onnx_sidecars:
225
+ - url: https://huggingface.co/nameissakthi/chipwright-cv/resolve/main/quicksrnet-small/quicksrnetsmall.data
226
+ sha256: e377c1bfc040561b28b4931e7c4b27e6df39d8848addb25ebdeee3071b45dd64
227
+ name: quicksrnetsmall.data
228
+ input: https://huggingface.co/nameissakthi/chipwright-cv/resolve/main/quicksrnet-small/input.npy
229
+ input_sha256: 28eb37c45cfcc4c3b8f22e982af24d4df33f2f06ec0701fc535150973fdc5850
220
230
  verified:
221
231
  'on': QCS6490 (soc_id 498)
222
232
  date: 2026-10-09
@@ -240,11 +250,21 @@ models:
240
250
  sdk_tested: 2.37.1
241
251
  sdk_compatible: '>=2.34,<2.40'
242
252
  ext: dlc
253
+ url: https://huggingface.co/nameissakthi/chipwright-cv/resolve/main/fcn-resnet50/fcn_w8a8.dlc
254
+ sha256: 98c0a2700f48f800859972d580b931d058d58a4ae699e1b4612c994fc947c65a
243
255
  run:
244
256
  modality: cv
245
257
  board_name: fcn
246
258
  meta: ''
247
259
  threshold: 0.9
260
+ cpu_onnx: https://huggingface.co/nameissakthi/chipwright-cv/resolve/main/fcn-resnet50/fcn_resnet50.onnx
261
+ cpu_onnx_sha256: 05eaf5a4d40915f3681aac3ae7ae0119ba9e8c5e9e753f1f1d9369023762b00f
262
+ cpu_onnx_sidecars:
263
+ - url: https://huggingface.co/nameissakthi/chipwright-cv/resolve/main/fcn-resnet50/fcn_resnet50.data
264
+ sha256: 95e4a28fc598875a2cd836f96e808441954bea5df942b385e30304564ca8e8e1
265
+ name: fcn_resnet50.data
266
+ input: https://huggingface.co/nameissakthi/chipwright-cv/resolve/main/fcn-resnet50/input.npy
267
+ input_sha256: 08f15b114922848ef54c41f6ea240a99ca9a4f5c4708c256065c7f7c437c5b6a
248
268
  verified:
249
269
  'on': QCS6490 (soc_id 498)
250
270
  date: 2026-10-09
@@ -297,11 +317,21 @@ models:
297
317
  sdk_tested: 2.37.1
298
318
  sdk_compatible: '>=2.34,<2.40'
299
319
  ext: dlc
320
+ url: https://huggingface.co/nameissakthi/chipwright-cv/resolve/main/depth-anything-v2/depth_w8a16.dlc
321
+ sha256: 051e9ff31c35cc093fae4601275337d6d3f6dcc7f9d06d3e219e5042c92936e3
300
322
  run:
301
323
  modality: cv
302
324
  board_name: depth
303
325
  meta: ''
304
326
  threshold: 0.9
327
+ cpu_onnx: https://huggingface.co/nameissakthi/chipwright-cv/resolve/main/depth-anything-v2/depth_anything_v2.onnx
328
+ cpu_onnx_sha256: 70f3afea5df1a1115f066d75ff40f35c95ec710e6609fb06533dc928f6769696
329
+ cpu_onnx_sidecars:
330
+ - url: https://huggingface.co/nameissakthi/chipwright-cv/resolve/main/depth-anything-v2/depth_anything_v2.data
331
+ sha256: 1cd4366ce17bae5da226e58a3c0618d22ea128c12831cd6538aebe9fc5a3905d
332
+ name: depth_anything_v2.data
333
+ input: https://huggingface.co/nameissakthi/chipwright-cv/resolve/main/depth-anything-v2/input.npy
334
+ input_sha256: 8415c599c901a49cb5e1ad12d594b0d3c8e909af27ba0208183f2d161ff79418
305
335
  verified:
306
336
  "on": QCS6490 (soc_id 498)
307
337
  date: 2026-10-09
@@ -322,11 +352,21 @@ models:
322
352
  sdk_tested: 2.37.1
323
353
  sdk_compatible: '>=2.34,<2.40'
324
354
  ext: dlc
355
+ url: https://huggingface.co/nameissakthi/chipwright-cv/resolve/main/movenet/pose_w8a16.dlc
356
+ sha256: 061187349f6890b9af6445f3bae998570ccfcd6516e93e5c41346b20e62cf33b
325
357
  run:
326
358
  modality: cv
327
359
  board_name: pose
328
360
  meta: ''
329
361
  threshold: 0.9
362
+ cpu_onnx: https://huggingface.co/nameissakthi/chipwright-cv/resolve/main/movenet/movenet.onnx
363
+ cpu_onnx_sha256: 77668290aba4913b0ffa58b6b526df2dddb871d695889428295182ca03ebcb0d
364
+ cpu_onnx_sidecars:
365
+ - url: https://huggingface.co/nameissakthi/chipwright-cv/resolve/main/movenet/movenet.data
366
+ sha256: 0032a7960d9730794c02ea8c7e2ab3483db5f8f9474c0e5c3f5bb4cef6384272
367
+ name: movenet.data
368
+ input: https://huggingface.co/nameissakthi/chipwright-cv/resolve/main/movenet/input.npy
369
+ input_sha256: 6fd2bba24a2c5fe0662930ff0cef5472b2fb4bd8a7413853ff40d560bd1689b2
330
370
  verified:
331
371
  "on": QCS6490 (soc_id 498)
332
372
  date: 2026-10-09
@@ -9,6 +9,7 @@ from __future__ import annotations
9
9
 
10
10
  import glob
11
11
  import os
12
+ import re
12
13
  import shutil
13
14
  import subprocess
14
15
  import tempfile
@@ -16,6 +17,20 @@ import time
16
17
 
17
18
  import numpy as np
18
19
 
20
+ _SAFE_NAME = re.compile(r"^[A-Za-z0-9._-]+$")
21
+
22
+
23
+ def _safe_board_name(name):
24
+ """`board_name` comes from the registry and is interpolated into on-board shell commands (and used
25
+ as a board path), so it MUST be a plain identifier. A name with a shell metacharacter, space, slash
26
+ or `..` is refused — otherwise a crafted registry entry could run arbitrary commands as root on the
27
+ board (`board_name = 'x; curl … | sh'`)."""
28
+ if not isinstance(name, str) or name in (".", "..") or not _SAFE_NAME.match(name):
29
+ raise RuntimeError(
30
+ f"unsafe board_name {name!r}: must match [A-Za-z0-9._-]+ (it is interpolated into on-board "
31
+ f"shell commands — a separator or shell metacharacter is refused)")
32
+ return name
33
+
19
34
 
20
35
  def _require_sshpass():
21
36
  """`sshpass` is a system binary (not a pip package) the SSH transport shells out to.
@@ -67,15 +82,23 @@ class RemoteQNN:
67
82
  time.sleep(wait)
68
83
  return last
69
84
 
70
- def ensure_ctx(self, local_path, name):
85
+ def ensure_ctx(self, local_path, name, ext=None):
71
86
  """Push an artifact to the board and return a runnable context-binary path (cached).
72
87
 
73
88
  A DLC is the durable artifact; the HTP context binary is per-target, so a `.dlc` is compiled
74
89
  into a context binary ON the board (2 MB VTCM) — regenerate, don't ship brittle. A prebuilt
75
90
  context binary (`.bin`/`.qnnctx`) is pushed as-is.
91
+
92
+ `ext` is the registry-declared artifact kind ("dlc"/"bin"/…). It is authoritative: a hosted
93
+ artifact is fetched into the content-addressed cache under a HASHED filename with no extension,
94
+ so inferring "is this a DLC?" from `local_path` wrongly treats a fetched DLC as a prebuilt
95
+ context binary (skips context-gen → "Could not create context from binary" on-device). When
96
+ `ext` is given we trust it; only when it is absent do we fall back to the filename suffix.
76
97
  """
98
+ name = _safe_board_name(name) # registry-sourced; confine before it reaches any shell string
77
99
  self._retry(lambda: _ssh(self.pw, self.port, self.dest, f"mkdir -p {self.board_dir}", timeout=40))
78
- if local_path.endswith(".dlc"):
100
+ is_dlc = (ext == "dlc") if ext is not None else local_path.endswith(".dlc")
101
+ if is_dlc:
79
102
  dlc_board = f"{self.board_dir}/{name}"
80
103
  ctx_board = f"{self.board_dir}/ctx_{name}/{name}_ctx.bin"
81
104
  present = _ssh(self.pw, self.port, self.dest, f"test -f {ctx_board} && echo OK", timeout=40)
@@ -22,6 +22,12 @@ DEFAULT_INDEX = os.path.join(os.path.dirname(os.path.abspath(__file__)), "data",
22
22
 
23
23
  def _load_yaml(src: str) -> dict:
24
24
  if src.startswith(("http://", "https://")):
25
+ # The index is the root of trust — it carries every artifact's sha256 — so a plaintext fetch is
26
+ # MITM-able (swap the index, swap the shas). Require https unless explicitly opted out.
27
+ if src.startswith("http://") and os.environ.get("CW_ALLOW_INSECURE") != "1":
28
+ raise RuntimeError(
29
+ f"refusing to load the registry index over insecure http ({src}): it carries the shas "
30
+ f"that pin every artifact (set CW_ALLOW_INSECURE=1 to override)")
25
31
  with urllib.request.urlopen(src, timeout=30) as r:
26
32
  return yaml.safe_load(r.read())
27
33
  with open(src) as f: