chipwright 0.2.0__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 (54) hide show
  1. chipwright-0.2.0/LICENSE +21 -0
  2. chipwright-0.2.0/PKG-INFO +268 -0
  3. chipwright-0.2.0/README.md +232 -0
  4. chipwright-0.2.0/chipwright/__init__.py +11 -0
  5. chipwright-0.2.0/chipwright/adapt.py +57 -0
  6. chipwright-0.2.0/chipwright/build.py +329 -0
  7. chipwright-0.2.0/chipwright/cache.py +65 -0
  8. chipwright-0.2.0/chipwright/cli.py +242 -0
  9. chipwright-0.2.0/chipwright/config.py +106 -0
  10. chipwright-0.2.0/chipwright/data/behavior_prompts.yaml +108 -0
  11. chipwright-0.2.0/chipwright/data/index.yaml +327 -0
  12. chipwright-0.2.0/chipwright/device.py +143 -0
  13. chipwright-0.2.0/chipwright/device_adb.py +121 -0
  14. chipwright-0.2.0/chipwright/env.py +229 -0
  15. chipwright-0.2.0/chipwright/errors.py +41 -0
  16. chipwright-0.2.0/chipwright/llm_reference.py +114 -0
  17. chipwright-0.2.0/chipwright/modalities/__init__.py +3 -0
  18. chipwright-0.2.0/chipwright/modalities/asr_encoder.py +90 -0
  19. chipwright-0.2.0/chipwright/modalities/cv.py +34 -0
  20. chipwright-0.2.0/chipwright/modalities/llm.py +300 -0
  21. chipwright-0.2.0/chipwright/recipe.py +205 -0
  22. chipwright-0.2.0/chipwright/registry.py +65 -0
  23. chipwright-0.2.0/chipwright/resolver.py +197 -0
  24. chipwright-0.2.0/chipwright/run.py +173 -0
  25. chipwright-0.2.0/chipwright/sdk.py +140 -0
  26. chipwright-0.2.0/chipwright/tags.py +56 -0
  27. chipwright-0.2.0/chipwright/task_eval.py +190 -0
  28. chipwright-0.2.0/chipwright/tests/conftest.py +12 -0
  29. chipwright-0.2.0/chipwright/tests/test_adapt.py +61 -0
  30. chipwright-0.2.0/chipwright/tests/test_build.py +279 -0
  31. chipwright-0.2.0/chipwright/tests/test_cache.py +147 -0
  32. chipwright-0.2.0/chipwright/tests/test_device_adb.py +140 -0
  33. chipwright-0.2.0/chipwright/tests/test_env.py +73 -0
  34. chipwright-0.2.0/chipwright/tests/test_env_adb.py +120 -0
  35. chipwright-0.2.0/chipwright/tests/test_llm_modality.py +259 -0
  36. chipwright-0.2.0/chipwright/tests/test_packaging.py +147 -0
  37. chipwright-0.2.0/chipwright/tests/test_precision_support.py +128 -0
  38. chipwright-0.2.0/chipwright/tests/test_registry.py +107 -0
  39. chipwright-0.2.0/chipwright/tests/test_resolver.py +93 -0
  40. chipwright-0.2.0/chipwright/tests/test_run_llm.py +47 -0
  41. chipwright-0.2.0/chipwright/tests/test_tags.py +95 -0
  42. chipwright-0.2.0/chipwright/tests/test_task_eval.py +164 -0
  43. chipwright-0.2.0/chipwright/tests/test_verify.py +92 -0
  44. chipwright-0.2.0/chipwright/tests/test_verify_llm.py +244 -0
  45. chipwright-0.2.0/chipwright/verify.py +24 -0
  46. chipwright-0.2.0/chipwright/verify_llm.py +203 -0
  47. chipwright-0.2.0/chipwright.egg-info/PKG-INFO +268 -0
  48. chipwright-0.2.0/chipwright.egg-info/SOURCES.txt +52 -0
  49. chipwright-0.2.0/chipwright.egg-info/dependency_links.txt +1 -0
  50. chipwright-0.2.0/chipwright.egg-info/entry_points.txt +2 -0
  51. chipwright-0.2.0/chipwright.egg-info/requires.txt +25 -0
  52. chipwright-0.2.0/chipwright.egg-info/top_level.txt +1 -0
  53. chipwright-0.2.0/pyproject.toml +48 -0
  54. chipwright-0.2.0/setup.cfg +4 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Chipwright contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,268 @@
1
+ Metadata-Version: 2.4
2
+ Name: chipwright
3
+ Version: 0.2.0
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
+ License-Expression: MIT
6
+ Project-URL: Homepage, https://github.com/nameissakthi25/chipwright
7
+ Project-URL: Specification, https://github.com/nameissakthi25/chipwright-speckit
8
+ Keywords: npu,qualcomm,hexagon,qnn,edge-ai,quantization,inference,package-manager
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
13
+ Classifier: Topic :: Software Development :: Build Tools
14
+ Requires-Python: >=3.9
15
+ Description-Content-Type: text/markdown
16
+ License-File: LICENSE
17
+ Requires-Dist: pyyaml>=6
18
+ Requires-Dist: numpy>=1.24
19
+ Provides-Extra: asr
20
+ Requires-Dist: onnxruntime>=1.16; extra == "asr"
21
+ Provides-Extra: cv
22
+ Requires-Dist: onnxruntime>=1.16; extra == "cv"
23
+ Requires-Dist: pillow>=10; extra == "cv"
24
+ Provides-Extra: tts
25
+ Requires-Dist: onnxruntime>=1.16; extra == "tts"
26
+ Provides-Extra: llm
27
+ Requires-Dist: onnxruntime>=1.16; extra == "llm"
28
+ Requires-Dist: tokenizers>=0.19; extra == "llm"
29
+ Provides-Extra: eval
30
+ Requires-Dist: qai-hub-models>=4.5; extra == "eval"
31
+ Provides-Extra: build
32
+ Requires-Dist: onnx>=1.14; extra == "build"
33
+ Provides-Extra: dev
34
+ Requires-Dist: pytest>=7; extra == "dev"
35
+ Dynamic: license-file
36
+
37
+ # Chipwright
38
+
39
+ [![python](https://img.shields.io/badge/python-3.9%2B-blue.svg)](https://www.python.org/)
40
+ [![license](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE)
41
+ [![verified on QCS6490](https://img.shields.io/badge/verified%20on-QCS6490-brightgreen.svg)](#tested-is-a-record-not-a-badge)
42
+
43
+ **A verified package manager and model hub for NPUs.** Chipwright reads your board, hands you an artifact that will actually load on your exact chip, and proves it against a CPU reference before you trust the output.
44
+
45
+ `resolve · run · build` — Qualcomm first, cross-vendor by design.
46
+
47
+ ```mermaid
48
+ flowchart LR
49
+ M([model]):::io
50
+ B([your board<br/>arch · SDK · SoC]):::io
51
+ M --> R{{resolve}}:::core
52
+ B --> R
53
+ R -->|exact · in-range| RUN[run on the NPU]:::step
54
+ R -->|no artifact,<br/>recipe exists| BLD[build<br/>convert · quantize<br/>on-board context]:::step
55
+ R -->|nothing fits| FAIL([refuse —<br/>name the axis]):::bad
56
+ BLD --> RUN
57
+ RUN --> V{{verify<br/>NPU vs CPU cosine}}:::core
58
+ V -->|≥ threshold| OK([trusted artifact]):::good
59
+ V -->|below| NO([caught, not shipped]):::bad
60
+
61
+ classDef io fill:#1f2430,stroke:#8a919e,color:#e8eaee;
62
+ classDef core fill:#7c3f1d,stroke:#b4632f,color:#fde8d7;
63
+ classDef step fill:#222a36,stroke:#4b5563,color:#e2e8f0;
64
+ classDef good fill:#14321f,stroke:#2d7d54,color:#86efac;
65
+ classDef bad fill:#3b1a16,stroke:#b23a2c,color:#f5a3a3;
66
+ ```
67
+
68
+ ---
69
+
70
+ ## The problem
71
+
72
+ On an NPU, a model isn't a file. It's a context binary pinned to a precise tuple: **architecture × SDK × quantization × shape × backend**. Load it on the wrong chip, or the wrong SDK, and it fails at load — or worse, it loads and quietly returns a fast wrong answer.
73
+
74
+ A model hub tells you *which model*. It can't tell you *which chip*. The gap between "here is a checkpoint" and "here is something that runs correctly on the board in your hand" is where edge ML projects stall.
75
+
76
+ Chipwright closes that gap. Think of it as **PyPI wheel tags for NPUs**: the chip is the platform tag. You don't hand-pick a binary and hope — you ask the resolver, and it either gives you one that matches or tells you, by name, the axis that doesn't.
77
+
78
+ ---
79
+
80
+ ## Quickstart
81
+
82
+ ```bash
83
+ pip install -e .
84
+ ```
85
+
86
+ Point Chipwright at a board (over SSH) and probe it:
87
+
88
+ ```bash
89
+ export CW_BOARD_HOST=192.168.1.50
90
+ export CW_BOARD_PASS=…
91
+
92
+ chipwright doctor
93
+ ```
94
+
95
+ ```text
96
+ ● board reachable root@192.168.1.50:22
97
+ SoC : QCS6490 (soc_id 498)
98
+ Hexagon arch: v68
99
+ QNN libs : libQnnHtp.so (QAIRT 2.38.0)
100
+ target key : htpv68,qnn2.38
101
+ ```
102
+
103
+ Resolve a model against that exact board — no guessing:
104
+
105
+ ```bash
106
+ chipwright resolve whisper-small-en
107
+ ```
108
+
109
+ ```text
110
+ model : whisper-small-en
111
+ 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
114
+ ```
115
+
116
+ Then run it on the board and verify the NPU output against a CPU reference:
117
+
118
+ ```bash
119
+ chipwright run zipformer-enin --input utterance.npy
120
+ ```
121
+
122
+ ```text
123
+ model : zipformer-enin
124
+ target : htpv68,qnn2.38
125
+ ✔ USE zipformer-enin-7.0.0 htpv68/w8a16/win39 qnn2.37.1 (exact match)
126
+
127
+ ✔ VERIFIED NPU vs CPU-reference cosine_min = 0.98800 (threshold 0.95)
128
+ encoder: 0.98800
129
+ ```
130
+
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.
132
+
133
+ ---
134
+
135
+ ## The three verbs
136
+
137
+ | Verb | What it does |
138
+ | --- | --- |
139
+ | **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
+ | **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. |
142
+
143
+ ---
144
+
145
+ ## The four resolve outcomes
146
+
147
+ The resolver is deliberately honest. Every resolution lands in exactly one of four outcomes:
148
+
149
+ - **USE** — an exact match on arch, quant, and shape, with the board's SDK inside the compatible range. Run it.
150
+ - **USE-with-warning** — a match, but the board's SDK sits outside the *tested* point while staying inside the compatible range. You get the artifact and the caveat, both.
151
+ - **BUILD** — no hosted artifact for your chip, but a recipe exists that can produce one.
152
+ - **FAIL** — nothing fits, and the message names the axis:
153
+
154
+ ```text
155
+ ✗ FAIL axis=htp_arch: no variant for v68; registry has [v73]
156
+ ```
157
+
158
+ The arch axis is hard — a mismatch fails at load, so the resolver refuses. The SDK axis is a range, not a point — so outside the tested point the resolver warns rather than refuses. That distinction is the whole discipline: **never downgrade to a near-miss artifact silently.**
159
+
160
+ ---
161
+
162
+ ## Artifact tags
163
+
164
+ An artifact's identity is its name. Chipwright names the five axes a resolver matches on in the filename and expands the rest in the manifest — exactly how a PyPI wheel names `cp312-manylinux_x86_64` and leaves the rest to metadata:
165
+
166
+ ```text
167
+ whisper-small-en-1.2.0+qnn2.37-htpv68-w8a16-win30s.qnnctx
168
+ └──────┬───────┘ └─┬─┘ └──┬──┘ └──┬──┘ └─┬─┘ └─┬─┘ └─┬──┘
169
+ name version sdk arch quant shape ext
170
+ ```
171
+
172
+ Parsing is strict: a tag that doesn't parse is a bug, not a near-miss to paper over.
173
+
174
+ ---
175
+
176
+ ## Tested is a record, not a badge
177
+
178
+ Chipwright does not stamp a green checkmark and ask you to believe it. Every registry variant carries a **verification record** — which board it ran on, what it was compared to, and the numbers — so a reader who distrusts the claim can check the inputs.
179
+
180
+ The record for `zipformer-enin`, from a live run:
181
+
182
+ ```yaml
183
+ verified:
184
+ on: "QCS6490 (soc_id 498)"
185
+ date: 2026-10-08
186
+ loads: true
187
+ vs_reference:
188
+ backend: onnxruntime-cpu
189
+ metric: encoder_cosine
190
+ value: 0.988 # threshold 0.95 — VERIFIED
191
+ ```
192
+
193
+ A high cosine is necessary, not sufficient — a cosine of 0.99 can still cost task accuracy — so the record keeps the numerical check separate from the task metric. The point is not the badge. The point is that the evidence travels with the artifact.
194
+
195
+ ---
196
+
197
+ ## Architecture
198
+
199
+ One front door today, one core, three targets.
200
+
201
+ - **The front door** — the `chipwright` CLI (`doctor · resolve · run · build`), a thin surface over the core. The core is deliberately UI-agnostic so other surfaces (a desktop app, editor integrations) can call the same logic later without reimplementing it; today the CLI is what ships.
202
+ - **One core** — [`chipwright-core`](chipwright/) holds all the logic: tags, board probe, resolver, registry client, cache, device transport, verification, and the run loop.
203
+ - **Three targets** — the core reaches the **registry** (the index of verified variants), the **board** (probe and execute), and the **build-host** (produce an artifact from a recipe).
204
+
205
+ See [`docs/architecture.md`](docs/architecture.md) for the diagram and the module map.
206
+
207
+ ---
208
+
209
+ ## Repo layout
210
+
211
+ ```text
212
+ chipwright/
213
+ cli.py the `chipwright` CLI — doctor · resolve · run · build
214
+ tags.py wheel-tag grammar for NPU artifacts (parse / format)
215
+ env.py board probe → TargetKey (arch × SDK × SoC)
216
+ resolver.py match variants to a target; the four outcomes
217
+ registry.py read the index (a git-repo file), list a model's variants
218
+ cache.py content-addressed artifact cache (~/.cache/chipwright)
219
+ device.py remote QNN runner — push + execute over SSH
220
+ verify.py NPU-vs-CPU cosine; the check that catches a fast wrong answer
221
+ run.py the run verb — ties resolve · device · modality · verify
222
+ modalities/ per-task glue (frontend, decoders, CPU reference)
223
+ registry/
224
+ index.yaml the registry index — one reviewable file, variants by hash
225
+ ```
226
+
227
+ ---
228
+
229
+ ## Status
230
+
231
+ - ✅ **resolve** — done, with the four honest outcomes, offline targets, and axis-named failures.
232
+ - ✅ **run** — done and **verified on hardware**, six real models pulled from Hugging Face, on a live QCS6490 / RB3 Gen2:
233
+
234
+ | model | task | quant | cosine |
235
+ |---|---|---|---|
236
+ | `zipformer-enin` | ASR (streaming RNN-T) | w8a16 | **0.988** |
237
+ | `mobilenet-v2` | image classification | w8a16 | **0.956** |
238
+ | `quicksrnet-small` | super-resolution | w8a8 | **0.9999** |
239
+ | `fcn-resnet50` | semantic segmentation | w8a8 | **0.998** |
240
+ | `depth-anything-v2` | depth estimation (DINOv2 **ViT**) | w8a16 | **0.99996** |
241
+ | `movenet` | pose estimation (17 keypoints) | w8a16 | **0.916** |
242
+
243
+ 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.
245
+
246
+ **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
+
248
+ ### Modality roadmap
249
+
250
+ ```text
251
+ ASR → TTS → VLM → LLM → VLA
252
+ ▲
253
+ verified on hardware today
254
+ ```
255
+
256
+ ---
257
+
258
+ ## Vendor-neutrality
259
+
260
+ Chipwright is Qualcomm first because that is where the hardware and the proof are today. It is cross-vendor by design: the tag grammar, the resolver's axis model, the verification record, and the device transport are not tied to one silicon vendor. Adding a backend means adding a target adapter and its arch axis — not reworking the framework.
261
+
262
+ The specification lives in a separate repo, the **spec kit** (`chipwright-speckit`, in GitHub Spec Kit format). Changes are spec-first: the spec kit moves before the code.
263
+
264
+ ---
265
+
266
+ ## License
267
+
268
+ MIT — see [LICENSE](LICENSE). Contributions welcome; start with [CONTRIBUTING.md](CONTRIBUTING.md).
@@ -0,0 +1,232 @@
1
+ # Chipwright
2
+
3
+ [![python](https://img.shields.io/badge/python-3.9%2B-blue.svg)](https://www.python.org/)
4
+ [![license](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE)
5
+ [![verified on QCS6490](https://img.shields.io/badge/verified%20on-QCS6490-brightgreen.svg)](#tested-is-a-record-not-a-badge)
6
+
7
+ **A verified package manager and model hub for NPUs.** Chipwright reads your board, hands you an artifact that will actually load on your exact chip, and proves it against a CPU reference before you trust the output.
8
+
9
+ `resolve · run · build` — Qualcomm first, cross-vendor by design.
10
+
11
+ ```mermaid
12
+ flowchart LR
13
+ M([model]):::io
14
+ B([your board<br/>arch · SDK · SoC]):::io
15
+ M --> R{{resolve}}:::core
16
+ B --> R
17
+ R -->|exact · in-range| RUN[run on the NPU]:::step
18
+ R -->|no artifact,<br/>recipe exists| BLD[build<br/>convert · quantize<br/>on-board context]:::step
19
+ R -->|nothing fits| FAIL([refuse —<br/>name the axis]):::bad
20
+ BLD --> RUN
21
+ RUN --> V{{verify<br/>NPU vs CPU cosine}}:::core
22
+ V -->|≥ threshold| OK([trusted artifact]):::good
23
+ V -->|below| NO([caught, not shipped]):::bad
24
+
25
+ classDef io fill:#1f2430,stroke:#8a919e,color:#e8eaee;
26
+ classDef core fill:#7c3f1d,stroke:#b4632f,color:#fde8d7;
27
+ classDef step fill:#222a36,stroke:#4b5563,color:#e2e8f0;
28
+ classDef good fill:#14321f,stroke:#2d7d54,color:#86efac;
29
+ classDef bad fill:#3b1a16,stroke:#b23a2c,color:#f5a3a3;
30
+ ```
31
+
32
+ ---
33
+
34
+ ## The problem
35
+
36
+ On an NPU, a model isn't a file. It's a context binary pinned to a precise tuple: **architecture × SDK × quantization × shape × backend**. Load it on the wrong chip, or the wrong SDK, and it fails at load — or worse, it loads and quietly returns a fast wrong answer.
37
+
38
+ A model hub tells you *which model*. It can't tell you *which chip*. The gap between "here is a checkpoint" and "here is something that runs correctly on the board in your hand" is where edge ML projects stall.
39
+
40
+ Chipwright closes that gap. Think of it as **PyPI wheel tags for NPUs**: the chip is the platform tag. You don't hand-pick a binary and hope — you ask the resolver, and it either gives you one that matches or tells you, by name, the axis that doesn't.
41
+
42
+ ---
43
+
44
+ ## Quickstart
45
+
46
+ ```bash
47
+ pip install -e .
48
+ ```
49
+
50
+ Point Chipwright at a board (over SSH) and probe it:
51
+
52
+ ```bash
53
+ export CW_BOARD_HOST=192.168.1.50
54
+ export CW_BOARD_PASS=…
55
+
56
+ chipwright doctor
57
+ ```
58
+
59
+ ```text
60
+ ● board reachable root@192.168.1.50:22
61
+ SoC : QCS6490 (soc_id 498)
62
+ Hexagon arch: v68
63
+ QNN libs : libQnnHtp.so (QAIRT 2.38.0)
64
+ target key : htpv68,qnn2.38
65
+ ```
66
+
67
+ Resolve a model against that exact board — no guessing:
68
+
69
+ ```bash
70
+ chipwright resolve whisper-small-en
71
+ ```
72
+
73
+ ```text
74
+ model : whisper-small-en
75
+ 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
78
+ ```
79
+
80
+ Then run it on the board and verify the NPU output against a CPU reference:
81
+
82
+ ```bash
83
+ chipwright run zipformer-enin --input utterance.npy
84
+ ```
85
+
86
+ ```text
87
+ model : zipformer-enin
88
+ target : htpv68,qnn2.38
89
+ ✔ USE zipformer-enin-7.0.0 htpv68/w8a16/win39 qnn2.37.1 (exact match)
90
+
91
+ ✔ VERIFIED NPU vs CPU-reference cosine_min = 0.98800 (threshold 0.95)
92
+ encoder: 0.98800
93
+ ```
94
+
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.
96
+
97
+ ---
98
+
99
+ ## The three verbs
100
+
101
+ | Verb | What it does |
102
+ | --- | --- |
103
+ | **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
+ | **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. |
106
+
107
+ ---
108
+
109
+ ## The four resolve outcomes
110
+
111
+ The resolver is deliberately honest. Every resolution lands in exactly one of four outcomes:
112
+
113
+ - **USE** — an exact match on arch, quant, and shape, with the board's SDK inside the compatible range. Run it.
114
+ - **USE-with-warning** — a match, but the board's SDK sits outside the *tested* point while staying inside the compatible range. You get the artifact and the caveat, both.
115
+ - **BUILD** — no hosted artifact for your chip, but a recipe exists that can produce one.
116
+ - **FAIL** — nothing fits, and the message names the axis:
117
+
118
+ ```text
119
+ ✗ FAIL axis=htp_arch: no variant for v68; registry has [v73]
120
+ ```
121
+
122
+ The arch axis is hard — a mismatch fails at load, so the resolver refuses. The SDK axis is a range, not a point — so outside the tested point the resolver warns rather than refuses. That distinction is the whole discipline: **never downgrade to a near-miss artifact silently.**
123
+
124
+ ---
125
+
126
+ ## Artifact tags
127
+
128
+ An artifact's identity is its name. Chipwright names the five axes a resolver matches on in the filename and expands the rest in the manifest — exactly how a PyPI wheel names `cp312-manylinux_x86_64` and leaves the rest to metadata:
129
+
130
+ ```text
131
+ whisper-small-en-1.2.0+qnn2.37-htpv68-w8a16-win30s.qnnctx
132
+ └──────┬───────┘ └─┬─┘ └──┬──┘ └──┬──┘ └─┬─┘ └─┬─┘ └─┬──┘
133
+ name version sdk arch quant shape ext
134
+ ```
135
+
136
+ Parsing is strict: a tag that doesn't parse is a bug, not a near-miss to paper over.
137
+
138
+ ---
139
+
140
+ ## Tested is a record, not a badge
141
+
142
+ Chipwright does not stamp a green checkmark and ask you to believe it. Every registry variant carries a **verification record** — which board it ran on, what it was compared to, and the numbers — so a reader who distrusts the claim can check the inputs.
143
+
144
+ The record for `zipformer-enin`, from a live run:
145
+
146
+ ```yaml
147
+ verified:
148
+ on: "QCS6490 (soc_id 498)"
149
+ date: 2026-10-08
150
+ loads: true
151
+ vs_reference:
152
+ backend: onnxruntime-cpu
153
+ metric: encoder_cosine
154
+ value: 0.988 # threshold 0.95 — VERIFIED
155
+ ```
156
+
157
+ A high cosine is necessary, not sufficient — a cosine of 0.99 can still cost task accuracy — so the record keeps the numerical check separate from the task metric. The point is not the badge. The point is that the evidence travels with the artifact.
158
+
159
+ ---
160
+
161
+ ## Architecture
162
+
163
+ One front door today, one core, three targets.
164
+
165
+ - **The front door** — the `chipwright` CLI (`doctor · resolve · run · build`), a thin surface over the core. The core is deliberately UI-agnostic so other surfaces (a desktop app, editor integrations) can call the same logic later without reimplementing it; today the CLI is what ships.
166
+ - **One core** — [`chipwright-core`](chipwright/) holds all the logic: tags, board probe, resolver, registry client, cache, device transport, verification, and the run loop.
167
+ - **Three targets** — the core reaches the **registry** (the index of verified variants), the **board** (probe and execute), and the **build-host** (produce an artifact from a recipe).
168
+
169
+ See [`docs/architecture.md`](docs/architecture.md) for the diagram and the module map.
170
+
171
+ ---
172
+
173
+ ## Repo layout
174
+
175
+ ```text
176
+ chipwright/
177
+ cli.py the `chipwright` CLI — doctor · resolve · run · build
178
+ tags.py wheel-tag grammar for NPU artifacts (parse / format)
179
+ env.py board probe → TargetKey (arch × SDK × SoC)
180
+ resolver.py match variants to a target; the four outcomes
181
+ registry.py read the index (a git-repo file), list a model's variants
182
+ cache.py content-addressed artifact cache (~/.cache/chipwright)
183
+ device.py remote QNN runner — push + execute over SSH
184
+ verify.py NPU-vs-CPU cosine; the check that catches a fast wrong answer
185
+ run.py the run verb — ties resolve · device · modality · verify
186
+ modalities/ per-task glue (frontend, decoders, CPU reference)
187
+ registry/
188
+ index.yaml the registry index — one reviewable file, variants by hash
189
+ ```
190
+
191
+ ---
192
+
193
+ ## Status
194
+
195
+ - ✅ **resolve** — done, with the four honest outcomes, offline targets, and axis-named failures.
196
+ - ✅ **run** — done and **verified on hardware**, six real models pulled from Hugging Face, on a live QCS6490 / RB3 Gen2:
197
+
198
+ | model | task | quant | cosine |
199
+ |---|---|---|---|
200
+ | `zipformer-enin` | ASR (streaming RNN-T) | w8a16 | **0.988** |
201
+ | `mobilenet-v2` | image classification | w8a16 | **0.956** |
202
+ | `quicksrnet-small` | super-resolution | w8a8 | **0.9999** |
203
+ | `fcn-resnet50` | semantic segmentation | w8a8 | **0.998** |
204
+ | `depth-anything-v2` | depth estimation (DINOv2 **ViT**) | w8a16 | **0.99996** |
205
+ | `movenet` | pose estimation (17 keypoints) | w8a16 | **0.916** |
206
+
207
+ 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.
209
+
210
+ **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
+
212
+ ### Modality roadmap
213
+
214
+ ```text
215
+ ASR → TTS → VLM → LLM → VLA
216
+ ▲
217
+ verified on hardware today
218
+ ```
219
+
220
+ ---
221
+
222
+ ## Vendor-neutrality
223
+
224
+ Chipwright is Qualcomm first because that is where the hardware and the proof are today. It is cross-vendor by design: the tag grammar, the resolver's axis model, the verification record, and the device transport are not tied to one silicon vendor. Adding a backend means adding a target adapter and its arch axis — not reworking the framework.
225
+
226
+ The specification lives in a separate repo, the **spec kit** (`chipwright-speckit`, in GitHub Spec Kit format). Changes are spec-first: the spec kit moves before the code.
227
+
228
+ ---
229
+
230
+ ## License
231
+
232
+ MIT — see [LICENSE](LICENSE). Contributions welcome; start with [CONTRIBUTING.md](CONTRIBUTING.md).
@@ -0,0 +1,11 @@
1
+ """chipwright-core — the resolver, registry client, artifact cache, and device transport that the CLI,
2
+ the desktop app, and a Claude Code session all share, so they always agree about what will run on
3
+ your board.
4
+ """
5
+ from .env import TargetKey, probe
6
+ from .registry import Registry
7
+ from .resolver import Decision, Outcome, Variant, resolve
8
+ from .tags import ArtifactTag
9
+
10
+ __all__ = ["TargetKey", "probe", "Registry", "Decision", "Outcome", "Variant", "resolve", "ArtifactTag"]
11
+ __version__ = "0.2.0"
@@ -0,0 +1,57 @@
1
+ """Graph adaptations — the declared, auditable ONNX edits a recipe applies before conversion.
2
+
3
+ The converter and the HTP reject a handful of op/attribute patterns that are harmless to fix. Each
4
+ adaptation is named in `recipe.adaptations`, applied here, and logged, so a build never silently
5
+ mutates a graph — it does exactly what the recipe says, on the record.
6
+
7
+ Implemented so far:
8
+ - reshape_allowzero : set Reshape `allowzero` 1 → 0. Lossless when the target shape has no literal
9
+ 0 (the common case in ViT / detector exports); qairt-converter rejects allowzero=1 outright.
10
+ """
11
+ from __future__ import annotations
12
+
13
+ import os
14
+ from typing import List, Tuple
15
+
16
+
17
+ def _reshape_allowzero(model) -> int:
18
+ n = 0
19
+ for node in model.graph.node:
20
+ if node.op_type == "Reshape":
21
+ for a in node.attribute:
22
+ if a.name == "allowzero" and a.i == 1:
23
+ a.i = 0
24
+ n += 1
25
+ return n
26
+
27
+
28
+ _HANDLERS = {"reshape_allowzero": _reshape_allowzero}
29
+
30
+
31
+ def apply(onnx_path: str, adaptations, out_dir: str = None) -> Tuple[str, List[str]]:
32
+ """Apply the declared adaptations to `onnx_path`; return (path_to_use, logs).
33
+
34
+ Returns the original path unchanged when there is nothing to do (no supported adaptation declared,
35
+ onnx not installed, or the file isn't on disk) — the caller then converts the source as-is.
36
+ """
37
+ kinds = [a.kind for a in (adaptations or [])]
38
+ todo = [k for k in kinds if k in _HANDLERS]
39
+ if not todo:
40
+ return onnx_path, []
41
+ if not os.path.exists(onnx_path):
42
+ return onnx_path, [f"adapt: source onnx not local ({onnx_path}) — adaptations skipped"]
43
+ try:
44
+ import onnx
45
+ except ImportError:
46
+ return onnx_path, ["adapt: onnx not installed — adaptations skipped"]
47
+
48
+ model = onnx.load(onnx_path)
49
+ logs = []
50
+ for kind in todo:
51
+ count = _HANDLERS[kind](model)
52
+ logs.append(f"adapt: {kind} applied to {count} node(s)")
53
+ dest = out_dir or os.path.dirname(onnx_path) or "."
54
+ os.makedirs(dest, exist_ok=True)
55
+ out = os.path.join(dest, "adapted.onnx")
56
+ onnx.save(model, out)
57
+ return out, logs