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.
- chipwright-0.2.0/LICENSE +21 -0
- chipwright-0.2.0/PKG-INFO +268 -0
- chipwright-0.2.0/README.md +232 -0
- chipwright-0.2.0/chipwright/__init__.py +11 -0
- chipwright-0.2.0/chipwright/adapt.py +57 -0
- chipwright-0.2.0/chipwright/build.py +329 -0
- chipwright-0.2.0/chipwright/cache.py +65 -0
- chipwright-0.2.0/chipwright/cli.py +242 -0
- chipwright-0.2.0/chipwright/config.py +106 -0
- chipwright-0.2.0/chipwright/data/behavior_prompts.yaml +108 -0
- chipwright-0.2.0/chipwright/data/index.yaml +327 -0
- chipwright-0.2.0/chipwright/device.py +143 -0
- chipwright-0.2.0/chipwright/device_adb.py +121 -0
- chipwright-0.2.0/chipwright/env.py +229 -0
- chipwright-0.2.0/chipwright/errors.py +41 -0
- chipwright-0.2.0/chipwright/llm_reference.py +114 -0
- chipwright-0.2.0/chipwright/modalities/__init__.py +3 -0
- chipwright-0.2.0/chipwright/modalities/asr_encoder.py +90 -0
- chipwright-0.2.0/chipwright/modalities/cv.py +34 -0
- chipwright-0.2.0/chipwright/modalities/llm.py +300 -0
- chipwright-0.2.0/chipwright/recipe.py +205 -0
- chipwright-0.2.0/chipwright/registry.py +65 -0
- chipwright-0.2.0/chipwright/resolver.py +197 -0
- chipwright-0.2.0/chipwright/run.py +173 -0
- chipwright-0.2.0/chipwright/sdk.py +140 -0
- chipwright-0.2.0/chipwright/tags.py +56 -0
- chipwright-0.2.0/chipwright/task_eval.py +190 -0
- chipwright-0.2.0/chipwright/tests/conftest.py +12 -0
- chipwright-0.2.0/chipwright/tests/test_adapt.py +61 -0
- chipwright-0.2.0/chipwright/tests/test_build.py +279 -0
- chipwright-0.2.0/chipwright/tests/test_cache.py +147 -0
- chipwright-0.2.0/chipwright/tests/test_device_adb.py +140 -0
- chipwright-0.2.0/chipwright/tests/test_env.py +73 -0
- chipwright-0.2.0/chipwright/tests/test_env_adb.py +120 -0
- chipwright-0.2.0/chipwright/tests/test_llm_modality.py +259 -0
- chipwright-0.2.0/chipwright/tests/test_packaging.py +147 -0
- chipwright-0.2.0/chipwright/tests/test_precision_support.py +128 -0
- chipwright-0.2.0/chipwright/tests/test_registry.py +107 -0
- chipwright-0.2.0/chipwright/tests/test_resolver.py +93 -0
- chipwright-0.2.0/chipwright/tests/test_run_llm.py +47 -0
- chipwright-0.2.0/chipwright/tests/test_tags.py +95 -0
- chipwright-0.2.0/chipwright/tests/test_task_eval.py +164 -0
- chipwright-0.2.0/chipwright/tests/test_verify.py +92 -0
- chipwright-0.2.0/chipwright/tests/test_verify_llm.py +244 -0
- chipwright-0.2.0/chipwright/verify.py +24 -0
- chipwright-0.2.0/chipwright/verify_llm.py +203 -0
- chipwright-0.2.0/chipwright.egg-info/PKG-INFO +268 -0
- chipwright-0.2.0/chipwright.egg-info/SOURCES.txt +52 -0
- chipwright-0.2.0/chipwright.egg-info/dependency_links.txt +1 -0
- chipwright-0.2.0/chipwright.egg-info/entry_points.txt +2 -0
- chipwright-0.2.0/chipwright.egg-info/requires.txt +25 -0
- chipwright-0.2.0/chipwright.egg-info/top_level.txt +1 -0
- chipwright-0.2.0/pyproject.toml +48 -0
- chipwright-0.2.0/setup.cfg +4 -0
chipwright-0.2.0/LICENSE
ADDED
|
@@ -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
|
+
[](https://www.python.org/)
|
|
40
|
+
[](LICENSE)
|
|
41
|
+
[](#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
|
+
[](https://www.python.org/)
|
|
4
|
+
[](LICENSE)
|
|
5
|
+
[](#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
|