chipwright 0.2.0__py3-none-any.whl
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/__init__.py +11 -0
- chipwright/adapt.py +57 -0
- chipwright/build.py +329 -0
- chipwright/cache.py +65 -0
- chipwright/cli.py +242 -0
- chipwright/config.py +106 -0
- chipwright/data/behavior_prompts.yaml +108 -0
- chipwright/data/index.yaml +327 -0
- chipwright/device.py +143 -0
- chipwright/device_adb.py +121 -0
- chipwright/env.py +229 -0
- chipwright/errors.py +41 -0
- chipwright/llm_reference.py +114 -0
- chipwright/modalities/__init__.py +3 -0
- chipwright/modalities/asr_encoder.py +90 -0
- chipwright/modalities/cv.py +34 -0
- chipwright/modalities/llm.py +300 -0
- chipwright/recipe.py +205 -0
- chipwright/registry.py +65 -0
- chipwright/resolver.py +197 -0
- chipwright/run.py +173 -0
- chipwright/sdk.py +140 -0
- chipwright/tags.py +56 -0
- chipwright/task_eval.py +190 -0
- chipwright/tests/conftest.py +12 -0
- chipwright/tests/test_adapt.py +61 -0
- chipwright/tests/test_build.py +279 -0
- chipwright/tests/test_cache.py +147 -0
- chipwright/tests/test_device_adb.py +140 -0
- chipwright/tests/test_env.py +73 -0
- chipwright/tests/test_env_adb.py +120 -0
- chipwright/tests/test_llm_modality.py +259 -0
- chipwright/tests/test_packaging.py +147 -0
- chipwright/tests/test_precision_support.py +128 -0
- chipwright/tests/test_registry.py +107 -0
- chipwright/tests/test_resolver.py +93 -0
- chipwright/tests/test_run_llm.py +47 -0
- chipwright/tests/test_tags.py +95 -0
- chipwright/tests/test_task_eval.py +164 -0
- chipwright/tests/test_verify.py +92 -0
- chipwright/tests/test_verify_llm.py +244 -0
- chipwright/verify.py +24 -0
- chipwright/verify_llm.py +203 -0
- chipwright-0.2.0.dist-info/METADATA +268 -0
- chipwright-0.2.0.dist-info/RECORD +49 -0
- chipwright-0.2.0.dist-info/WHEEL +5 -0
- chipwright-0.2.0.dist-info/entry_points.txt +2 -0
- chipwright-0.2.0.dist-info/licenses/LICENSE +21 -0
- chipwright-0.2.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
# Chipwright registry — the index is a single reviewable file; artifacts live by hash in object storage.
|
|
2
|
+
# The registry's unit is a BUILD VARIANT, not a model: one model version fans out to many artifacts,
|
|
3
|
+
# one per (arch × quant × shape × SDK-range). "verified:" is a record, not a badge — the evidence of
|
|
4
|
+
# the run that validated the variant, so a reader who distrusts the claim can check the inputs.
|
|
5
|
+
#
|
|
6
|
+
# Board context for every verified record below: QCS6490 / RB3 Gen2, Hexagon v68, QAIRT 2.38.0.
|
|
7
|
+
# The HTP is w8a16-only on v68 (no fp16); LLMs go all-integer w4a16. Our artifacts are built and
|
|
8
|
+
# tested at QAIRT 2.37.1 with a compatible range >=2.34,<2.40, so the board's in-field 2.38.0 is
|
|
9
|
+
# in-range (resolves USE_WITH_WARN, not a refusal).
|
|
10
|
+
registry_schema: 1
|
|
11
|
+
models:
|
|
12
|
+
- model: laya-decision
|
|
13
|
+
source: convaiinnovations/laya
|
|
14
|
+
license: apache-2.0
|
|
15
|
+
modality: typed-decision
|
|
16
|
+
version: 1.0.0
|
|
17
|
+
variants:
|
|
18
|
+
- arch: v68
|
|
19
|
+
quant: w8a16
|
|
20
|
+
shape: win256
|
|
21
|
+
sdk_tested: 2.37.1
|
|
22
|
+
sdk_compatible: '>=2.34,<2.40'
|
|
23
|
+
ext: qnnctx
|
|
24
|
+
url: https://huggingface.co/nameissakthi/laya-qcs6490-encoder/resolve/main/ctx/laya_encoder_256_sqnr_ctx.bin
|
|
25
|
+
sha256: b338a4067e9d9d3147aa8e016892450e51018de6db425ef2e3a4e533a4548a54
|
|
26
|
+
verified:
|
|
27
|
+
'on': QCS6490
|
|
28
|
+
loads: true
|
|
29
|
+
vs_reference:
|
|
30
|
+
backend: onnxruntime-cpu
|
|
31
|
+
metric: hidden_state_pearson
|
|
32
|
+
value: 0.88
|
|
33
|
+
latency_ms:
|
|
34
|
+
p50: 84
|
|
35
|
+
- model: laya-intent
|
|
36
|
+
source: convaiinnovations/laya
|
|
37
|
+
license: apache-2.0
|
|
38
|
+
modality: intent-slot
|
|
39
|
+
version: 1.0.0
|
|
40
|
+
variants:
|
|
41
|
+
- arch: v68
|
|
42
|
+
quant: w8a16
|
|
43
|
+
shape: win256
|
|
44
|
+
sdk_tested: 2.37.1
|
|
45
|
+
sdk_compatible: '>=2.34,<2.40'
|
|
46
|
+
ext: qnnctx
|
|
47
|
+
recipe: recipes/laya-intent/1.0.0.yaml
|
|
48
|
+
verified:
|
|
49
|
+
'on': QCS6490 (soc_id 498)
|
|
50
|
+
loads: true
|
|
51
|
+
vs_reference:
|
|
52
|
+
metric: top1_vs_fp32
|
|
53
|
+
value: 1.0
|
|
54
|
+
ptq_baseline: 0.425
|
|
55
|
+
task_metric:
|
|
56
|
+
name: intent_top1
|
|
57
|
+
value: 1.0
|
|
58
|
+
note: held-out synthetic test; fp32 also ~100% — proves QAT removed the on-device quantization loss, not real-world accuracy
|
|
59
|
+
latency_ms:
|
|
60
|
+
note: 120 inputs in ~3s on-device
|
|
61
|
+
- model: zipformer-enin
|
|
62
|
+
source: nameissakthi/zipformer-enin-qairt-aimet
|
|
63
|
+
license: apache-2.0
|
|
64
|
+
modality: asr
|
|
65
|
+
version: 7.0.0
|
|
66
|
+
variants:
|
|
67
|
+
- arch: v68
|
|
68
|
+
quant: w8a16
|
|
69
|
+
shape: win39
|
|
70
|
+
sdk_tested: 2.37.1
|
|
71
|
+
sdk_compatible: '>=2.34,<2.40'
|
|
72
|
+
ext: qnnctx
|
|
73
|
+
run:
|
|
74
|
+
modality: asr_encoder
|
|
75
|
+
board_name: zipformer_encoder_w8a16_ctx.bin
|
|
76
|
+
threshold: 0.95
|
|
77
|
+
recipe: recipes/zipformer-enin/7.0.0.yaml
|
|
78
|
+
verified:
|
|
79
|
+
'on': QCS6490 (soc_id 498)
|
|
80
|
+
date: 2026-10-08
|
|
81
|
+
loads: true
|
|
82
|
+
vs_reference:
|
|
83
|
+
backend: onnxruntime-cpu-float
|
|
84
|
+
metric: wer_delta
|
|
85
|
+
htp_wer: 0.27
|
|
86
|
+
cpu_float_wer: 0.287
|
|
87
|
+
note: w8a16 ~lossless vs float
|
|
88
|
+
latency_ms:
|
|
89
|
+
note: enc ~16.4 ms/chunk, RTF ~0.05 persistent-context
|
|
90
|
+
- model: supertonic-tts
|
|
91
|
+
source: supertonic/supertonic-tts
|
|
92
|
+
license: apache-2.0
|
|
93
|
+
modality: tts
|
|
94
|
+
version: 1.0.0
|
|
95
|
+
variants:
|
|
96
|
+
- arch: v68
|
|
97
|
+
quant: w8a16
|
|
98
|
+
shape: multigraph
|
|
99
|
+
sdk_tested: 2.37.1
|
|
100
|
+
sdk_compatible: '>=2.34,<2.40'
|
|
101
|
+
ext: qnnctx
|
|
102
|
+
recipe: recipes/supertonic-tts/1.0.0.yaml
|
|
103
|
+
verified:
|
|
104
|
+
'on': QCS6490 (soc_id 498)
|
|
105
|
+
loads: true
|
|
106
|
+
vs_reference:
|
|
107
|
+
metric: logmel_corr_dtw
|
|
108
|
+
value: 0.77
|
|
109
|
+
latency_ms:
|
|
110
|
+
note: ~3.8s wall / 3.07s audio, cold
|
|
111
|
+
note: small residual buzz; w8a16 flow-matching vocoder
|
|
112
|
+
- model: whisper-small-en
|
|
113
|
+
source: openai/whisper-small.en
|
|
114
|
+
license: mit
|
|
115
|
+
modality: asr
|
|
116
|
+
version: 1.2.0
|
|
117
|
+
variants:
|
|
118
|
+
- arch: v68
|
|
119
|
+
quant: w8a16
|
|
120
|
+
shape: win30s
|
|
121
|
+
sdk_tested: 2.37.1
|
|
122
|
+
sdk_compatible: '>=2.34,<2.40'
|
|
123
|
+
ext: qnnctx
|
|
124
|
+
recipe: recipes/whisper-small-en/1.2.0.yaml
|
|
125
|
+
- model: qwen2.5-1.5b-instruct
|
|
126
|
+
source: Qwen/Qwen2.5-1.5B-Instruct
|
|
127
|
+
license: apache-2.0
|
|
128
|
+
modality: llm
|
|
129
|
+
version: 1.0.0
|
|
130
|
+
variants:
|
|
131
|
+
- arch: v68
|
|
132
|
+
quant: w4a16
|
|
133
|
+
shape: ctx4096
|
|
134
|
+
sdk_tested: 2.37.1
|
|
135
|
+
sdk_compatible: '>=2.34,<2.40'
|
|
136
|
+
ext: qnnctx
|
|
137
|
+
recipe: recipes/qwen2.5-1.5b/1.0.0.yaml
|
|
138
|
+
- arch: v73
|
|
139
|
+
quant: w4a16
|
|
140
|
+
shape: ctx4096
|
|
141
|
+
sdk_tested: 2.37.1
|
|
142
|
+
sdk_compatible: '>=2.34,<2.40'
|
|
143
|
+
ext: qnnctx
|
|
144
|
+
recipe: recipes/qwen2.5-1.5b/1.0.0.yaml
|
|
145
|
+
- model: qwen2.5-7b-instruct
|
|
146
|
+
source: Qwen/Qwen2.5-7B-Instruct
|
|
147
|
+
license: apache-2.0
|
|
148
|
+
modality: llm
|
|
149
|
+
version: 1.0.0
|
|
150
|
+
variants:
|
|
151
|
+
- arch: v73
|
|
152
|
+
quant: w4a16
|
|
153
|
+
shape: ctx4096
|
|
154
|
+
sdk_tested: 2.37.1
|
|
155
|
+
sdk_compatible: '>=2.34,<2.40'
|
|
156
|
+
ext: qnnctx
|
|
157
|
+
recipe: recipes/qwen2.5-7b/1.0.0.yaml
|
|
158
|
+
- arch: v75
|
|
159
|
+
quant: w4a16
|
|
160
|
+
shape: ctx4096
|
|
161
|
+
sdk_tested: 2.37.1
|
|
162
|
+
sdk_compatible: '>=2.34,<2.40'
|
|
163
|
+
ext: qnnctx
|
|
164
|
+
recipe: recipes/qwen2.5-7b/1.0.0.yaml
|
|
165
|
+
- model: mobilenet-v2
|
|
166
|
+
source: qualcomm/MobileNet-v2
|
|
167
|
+
license: bsd-3-clause
|
|
168
|
+
modality: cv-classifier
|
|
169
|
+
version: 1.0.0
|
|
170
|
+
variants:
|
|
171
|
+
- arch: v68
|
|
172
|
+
quant: w8a16
|
|
173
|
+
shape: img224
|
|
174
|
+
sdk_tested: 2.37.1
|
|
175
|
+
sdk_compatible: '>=2.34,<2.40'
|
|
176
|
+
ext: dlc
|
|
177
|
+
run:
|
|
178
|
+
modality: cv
|
|
179
|
+
board_name: mobilenet
|
|
180
|
+
meta: ''
|
|
181
|
+
threshold: 0.95
|
|
182
|
+
verified:
|
|
183
|
+
'on': QCS6490 (soc_id 498)
|
|
184
|
+
date: 2026-10-09
|
|
185
|
+
loads: true
|
|
186
|
+
vs_reference:
|
|
187
|
+
backend: onnxruntime-cpu-float
|
|
188
|
+
metric: cosine_min
|
|
189
|
+
value: 0.956
|
|
190
|
+
note: real image (ImageNet 'Samoyed'); random-noise input gives 0.776 and the gate correctly FAILs it
|
|
191
|
+
latency_ms:
|
|
192
|
+
note: single 224x224 image; DLC compiled to context on-board
|
|
193
|
+
- model: quicksrnet-small
|
|
194
|
+
source: qualcomm/QuickSRNetSmall
|
|
195
|
+
license: bsd-3-clause
|
|
196
|
+
modality: cv-superres
|
|
197
|
+
version: 1.0.0
|
|
198
|
+
variants:
|
|
199
|
+
- arch: v68
|
|
200
|
+
quant: w8a8
|
|
201
|
+
shape: img128
|
|
202
|
+
sdk_tested: 2.37.1
|
|
203
|
+
sdk_compatible: '>=2.34,<2.40'
|
|
204
|
+
ext: dlc
|
|
205
|
+
run:
|
|
206
|
+
modality: cv
|
|
207
|
+
board_name: quicksrnet
|
|
208
|
+
meta: ''
|
|
209
|
+
threshold: 0.9
|
|
210
|
+
verified:
|
|
211
|
+
'on': QCS6490 (soc_id 498)
|
|
212
|
+
date: 2026-10-09
|
|
213
|
+
loads: true
|
|
214
|
+
vs_reference:
|
|
215
|
+
backend: onnxruntime-cpu-float
|
|
216
|
+
metric: cosine_min
|
|
217
|
+
value: 0.9999
|
|
218
|
+
note: 4x super-resolution; w8a8 upscaled image is near-lossless vs float
|
|
219
|
+
latency_ms:
|
|
220
|
+
note: 128x128 -> 512x512; DLC compiled to context on-board
|
|
221
|
+
- model: fcn-resnet50
|
|
222
|
+
source: qualcomm/FCN-ResNet50
|
|
223
|
+
license: bsd-3-clause
|
|
224
|
+
modality: cv-segmentation
|
|
225
|
+
version: 1.0.0
|
|
226
|
+
variants:
|
|
227
|
+
- arch: v68
|
|
228
|
+
quant: w8a8
|
|
229
|
+
shape: img520
|
|
230
|
+
sdk_tested: 2.37.1
|
|
231
|
+
sdk_compatible: '>=2.34,<2.40'
|
|
232
|
+
ext: dlc
|
|
233
|
+
run:
|
|
234
|
+
modality: cv
|
|
235
|
+
board_name: fcn
|
|
236
|
+
meta: ''
|
|
237
|
+
threshold: 0.9
|
|
238
|
+
verified:
|
|
239
|
+
'on': QCS6490 (soc_id 498)
|
|
240
|
+
date: 2026-10-09
|
|
241
|
+
loads: true
|
|
242
|
+
vs_reference:
|
|
243
|
+
backend: onnxruntime-cpu-float
|
|
244
|
+
metric: cosine_min
|
|
245
|
+
value: 0.998
|
|
246
|
+
note: per-pixel semantic mask; w8a8 near-lossless vs float
|
|
247
|
+
latency_ms:
|
|
248
|
+
note: 520x520; DLC compiled to context on-board (spills to DDR)
|
|
249
|
+
- model: mobilenet-cv-built
|
|
250
|
+
source: qualcomm/MobileNet-v2
|
|
251
|
+
license: bsd-3-clause
|
|
252
|
+
modality: cv-classification
|
|
253
|
+
version: 1.0.0
|
|
254
|
+
variants:
|
|
255
|
+
- arch: v68
|
|
256
|
+
quant: w8a16
|
|
257
|
+
shape: img224
|
|
258
|
+
sdk_tested: 2.37.1
|
|
259
|
+
sdk_compatible: '>=2.34,<2.40'
|
|
260
|
+
ext: dlc
|
|
261
|
+
run:
|
|
262
|
+
modality: cv
|
|
263
|
+
board_name: mobilenet_built
|
|
264
|
+
meta: ''
|
|
265
|
+
threshold: 0.95
|
|
266
|
+
verified:
|
|
267
|
+
'on': QCS6490 (soc_id 498)
|
|
268
|
+
date: 2026-10-09
|
|
269
|
+
loads: true
|
|
270
|
+
vs_reference:
|
|
271
|
+
backend: onnxruntime-cpu-float
|
|
272
|
+
metric: cosine_min
|
|
273
|
+
value: 0.956
|
|
274
|
+
note: the artifact chipwright BUILT (real qairt-converter on the A100) then verified on the board — full build->run->verify
|
|
275
|
+
loop
|
|
276
|
+
latency_ms:
|
|
277
|
+
note: built via dlc_route; context compiled on-board
|
|
278
|
+
- model: depth-anything-v2
|
|
279
|
+
source: qualcomm/Depth-Anything-V2
|
|
280
|
+
license: apache-2.0
|
|
281
|
+
modality: cv-depth
|
|
282
|
+
version: 1.0.0
|
|
283
|
+
variants:
|
|
284
|
+
- arch: v68
|
|
285
|
+
quant: w8a16
|
|
286
|
+
shape: img518
|
|
287
|
+
sdk_tested: 2.37.1
|
|
288
|
+
sdk_compatible: '>=2.34,<2.40'
|
|
289
|
+
ext: dlc
|
|
290
|
+
run:
|
|
291
|
+
modality: cv
|
|
292
|
+
board_name: depth
|
|
293
|
+
meta: ''
|
|
294
|
+
threshold: 0.9
|
|
295
|
+
verified:
|
|
296
|
+
"on": QCS6490 (soc_id 498)
|
|
297
|
+
date: 2026-10-09
|
|
298
|
+
loads: true
|
|
299
|
+
vs_reference:
|
|
300
|
+
backend: onnxruntime-cpu
|
|
301
|
+
metric: depth_cosine
|
|
302
|
+
value: 0.99996
|
|
303
|
+
- model: movenet
|
|
304
|
+
source: qualcomm/MoveNet
|
|
305
|
+
license: apache-2.0
|
|
306
|
+
modality: cv-pose
|
|
307
|
+
version: 1.0.0
|
|
308
|
+
variants:
|
|
309
|
+
- arch: v68
|
|
310
|
+
quant: w8a16
|
|
311
|
+
shape: img192
|
|
312
|
+
sdk_tested: 2.37.1
|
|
313
|
+
sdk_compatible: '>=2.34,<2.40'
|
|
314
|
+
ext: dlc
|
|
315
|
+
run:
|
|
316
|
+
modality: cv
|
|
317
|
+
board_name: pose
|
|
318
|
+
meta: ''
|
|
319
|
+
threshold: 0.9
|
|
320
|
+
verified:
|
|
321
|
+
"on": QCS6490 (soc_id 498)
|
|
322
|
+
date: 2026-10-09
|
|
323
|
+
loads: true
|
|
324
|
+
vs_reference:
|
|
325
|
+
backend: onnxruntime-cpu
|
|
326
|
+
metric: keypoint_cosine
|
|
327
|
+
value: 0.91607
|
chipwright/device.py
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"""Remote QNN runner — push a context binary to the board and execute it over SSH.
|
|
2
|
+
|
|
3
|
+
The device transport the `run` verb uses. It is deliberately quantization-agnostic: it moves raw
|
|
4
|
+
bytes in the graph's native dtype and runs `qnn-net-run --retrieve_context`. Quant/dequant belongs
|
|
5
|
+
to the modality runner, which owns the artifact's I/O scales. (Generalized from the proven
|
|
6
|
+
board-side driver; here it drives the board from the host.)
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import glob
|
|
11
|
+
import os
|
|
12
|
+
import shutil
|
|
13
|
+
import subprocess
|
|
14
|
+
import tempfile
|
|
15
|
+
import time
|
|
16
|
+
|
|
17
|
+
import numpy as np
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _require_sshpass():
|
|
21
|
+
"""`sshpass` is a system binary (not a pip package) the SSH transport shells out to.
|
|
22
|
+
|
|
23
|
+
Fail here, by name, with the install hint — not later with a cryptic FileNotFoundError from
|
|
24
|
+
subprocess — so a missing system dependency is reported the way the framework reports everything:
|
|
25
|
+
plainly, naming the fix.
|
|
26
|
+
"""
|
|
27
|
+
if shutil.which("sshpass") is None:
|
|
28
|
+
raise RuntimeError(
|
|
29
|
+
"chipwright run needs the `sshpass` system binary to reach the board over SSH, and it "
|
|
30
|
+
"is not on PATH. Install it: `brew install hudochenkov/sshpass/sshpass` (macOS) or "
|
|
31
|
+
"`apt-get install sshpass` (Debian/Ubuntu).")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _ssh(pw, port, dest, *cmd, timeout=600, capture=True):
|
|
35
|
+
base = ["sshpass", "-p", pw, "ssh", "-o", "StrictHostKeyChecking=no",
|
|
36
|
+
"-o", "UserKnownHostsFile=/dev/null", "-o", "ConnectTimeout=20",
|
|
37
|
+
"-o", "PreferredAuthentications=password", "-o", "PubkeyAuthentication=no",
|
|
38
|
+
"-p", str(port), dest, *cmd]
|
|
39
|
+
return subprocess.run(base, capture_output=capture, text=True, timeout=timeout)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _scp(pw, port, *args, timeout=1800):
|
|
43
|
+
base = ["sshpass", "-p", pw, "scp", "-P", str(port), "-o", "StrictHostKeyChecking=no",
|
|
44
|
+
"-o", "UserKnownHostsFile=/dev/null", "-o", "ConnectTimeout=20",
|
|
45
|
+
"-o", "PreferredAuthentications=password", "-o", "PubkeyAuthentication=no", *args]
|
|
46
|
+
return subprocess.run(base, capture_output=True, text=True, timeout=timeout)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class RemoteQNN:
|
|
50
|
+
def __init__(self, host, user, pw, port=22, backend="/usr/lib/libQnnHtp.so",
|
|
51
|
+
board_dir="/opt/chipwright"):
|
|
52
|
+
_require_sshpass()
|
|
53
|
+
self.host, self.user, self.pw, self.port = host, user, pw, int(port)
|
|
54
|
+
self.backend, self.board_dir = backend, board_dir
|
|
55
|
+
self.dest = f"{user}@{host}"
|
|
56
|
+
|
|
57
|
+
def _retry(self, fn, tries=4, wait=3):
|
|
58
|
+
last = None
|
|
59
|
+
for i in range(tries):
|
|
60
|
+
try:
|
|
61
|
+
r = fn()
|
|
62
|
+
if getattr(r, "returncode", 0) == 0:
|
|
63
|
+
return r
|
|
64
|
+
last = r
|
|
65
|
+
except Exception as e: # pragma: no cover
|
|
66
|
+
last = e
|
|
67
|
+
time.sleep(wait)
|
|
68
|
+
return last
|
|
69
|
+
|
|
70
|
+
def ensure_ctx(self, local_path, name):
|
|
71
|
+
"""Push an artifact to the board and return a runnable context-binary path (cached).
|
|
72
|
+
|
|
73
|
+
A DLC is the durable artifact; the HTP context binary is per-target, so a `.dlc` is compiled
|
|
74
|
+
into a context binary ON the board (2 MB VTCM) — regenerate, don't ship brittle. A prebuilt
|
|
75
|
+
context binary (`.bin`/`.qnnctx`) is pushed as-is.
|
|
76
|
+
"""
|
|
77
|
+
self._retry(lambda: _ssh(self.pw, self.port, self.dest, f"mkdir -p {self.board_dir}", timeout=40))
|
|
78
|
+
if local_path.endswith(".dlc"):
|
|
79
|
+
dlc_board = f"{self.board_dir}/{name}"
|
|
80
|
+
ctx_board = f"{self.board_dir}/ctx_{name}/{name}_ctx.bin"
|
|
81
|
+
present = _ssh(self.pw, self.port, self.dest, f"test -f {ctx_board} && echo OK", timeout=40)
|
|
82
|
+
if "OK" not in (present.stdout or ""):
|
|
83
|
+
r = self._retry(lambda: _scp(self.pw, self.port, local_path, f"{self.dest}:{dlc_board}"))
|
|
84
|
+
if getattr(r, "returncode", 1) != 0:
|
|
85
|
+
raise RuntimeError(f"failed to push DLC: {getattr(r,'stderr','')}")
|
|
86
|
+
env = "export ADSP_LIBRARY_PATH=/usr/lib/rfsa/adsp:/usr/lib LD_LIBRARY_PATH=/usr/lib"
|
|
87
|
+
gen = (f"cd {self.board_dir} && {env} && rm -rf ctx_{name} && "
|
|
88
|
+
f"qnn-context-binary-generator --backend {self.backend} "
|
|
89
|
+
f"--model /usr/lib/libQnnModelDlc.so --dlc_path {name} "
|
|
90
|
+
f"--binary_file {name}_ctx --output_dir ctx_{name}")
|
|
91
|
+
g = self._retry(lambda: _ssh(self.pw, self.port, self.dest, gen, timeout=1200))
|
|
92
|
+
check = _ssh(self.pw, self.port, self.dest, f"test -f {ctx_board} && echo OK", timeout=40)
|
|
93
|
+
if "OK" not in (check.stdout or ""):
|
|
94
|
+
raise RuntimeError("on-board context-gen failed:\n" + (getattr(g, "stdout", "") or "")[-500:])
|
|
95
|
+
return ctx_board
|
|
96
|
+
board_path = f"{self.board_dir}/{name}"
|
|
97
|
+
present = _ssh(self.pw, self.port, self.dest, f"test -f {board_path} && echo OK", timeout=40)
|
|
98
|
+
if "OK" not in (present.stdout or ""):
|
|
99
|
+
r = self._retry(lambda: _scp(self.pw, self.port, local_path, f"{self.dest}:{board_path}"))
|
|
100
|
+
if getattr(r, "returncode", 1) != 0:
|
|
101
|
+
raise RuntimeError(f"failed to push context binary: {getattr(r,'stderr','')}")
|
|
102
|
+
return board_path
|
|
103
|
+
|
|
104
|
+
def run(self, ctx_board_path, feeds_native, outputs, native_io=True):
|
|
105
|
+
"""feeds_native: {name: ndarray}. outputs: [{name,dtype,shape}]. Returns {name: ndarray[shape]}.
|
|
106
|
+
|
|
107
|
+
native_io=True: raws are the graph's native quantized dtype (the modality quantizes/dequantizes).
|
|
108
|
+
native_io=False: feed float32, let qnn-net-run quantize the input and dequantize the output —
|
|
109
|
+
the simple path for graphs with float I/O semantics (e.g. an image classifier).
|
|
110
|
+
"""
|
|
111
|
+
dt = {"uint16": np.uint16, "int32": np.int32, "float32": np.float32}
|
|
112
|
+
with tempfile.TemporaryDirectory() as td:
|
|
113
|
+
parts = []
|
|
114
|
+
for n, a in feeds_native.items():
|
|
115
|
+
np.ascontiguousarray(a).tofile(f"{td}/{n}.raw")
|
|
116
|
+
parts.append(f"{n}:={n}.raw")
|
|
117
|
+
open(f"{td}/il.txt", "w").write(" ".join(parts) + "\n")
|
|
118
|
+
rdir = f"{self.board_dir}/req_{int(time.time()*1000)}"
|
|
119
|
+
self._retry(lambda: _ssh(self.pw, self.port, self.dest, f"mkdir -p {rdir}", timeout=40))
|
|
120
|
+
files = [f"{td}/{f}" for f in os.listdir(td)]
|
|
121
|
+
r = self._retry(lambda: _scp(self.pw, self.port, *files, f"{self.dest}:{rdir}/"))
|
|
122
|
+
if getattr(r, "returncode", 1) != 0:
|
|
123
|
+
raise RuntimeError("failed to push inputs")
|
|
124
|
+
env = "export ADSP_LIBRARY_PATH=/usr/lib/rfsa/adsp:/usr/lib LD_LIBRARY_PATH=/usr/lib"
|
|
125
|
+
flags = " --use_native_input_files --use_native_output_files" if native_io else ""
|
|
126
|
+
cmd = (f"cd {rdir} && {env} && qnn-net-run --backend {self.backend} "
|
|
127
|
+
f"--retrieve_context {ctx_board_path} --input_list il.txt --output_dir out" + flags)
|
|
128
|
+
run = self._retry(lambda: _ssh(self.pw, self.port, self.dest, cmd, timeout=600))
|
|
129
|
+
if getattr(run, "returncode", 1) != 0:
|
|
130
|
+
_ssh(self.pw, self.port, self.dest, f"rm -rf {rdir}", timeout=30)
|
|
131
|
+
raise RuntimeError("qnn-net-run failed:\n" + (getattr(run, "stdout", "") or "")[-500:])
|
|
132
|
+
self._retry(lambda: _scp(self.pw, self.port, "-r", f"{self.dest}:{rdir}/out", td))
|
|
133
|
+
_ssh(self.pw, self.port, self.dest, f"rm -rf {rdir}", timeout=30)
|
|
134
|
+
res = {}
|
|
135
|
+
for spec in outputs:
|
|
136
|
+
nm, d, sh = spec["name"], dt[spec.get("dtype", "float32")], tuple(spec["shape"])
|
|
137
|
+
fs = (glob.glob(f"{td}/out/**/{nm}_native.raw", recursive=True)
|
|
138
|
+
or glob.glob(f"{td}/out/**/{nm}.raw", recursive=True)
|
|
139
|
+
or glob.glob(f"{td}/out/**/{nm}*.raw", recursive=True))
|
|
140
|
+
if not fs:
|
|
141
|
+
raise RuntimeError(f"missing output {nm}")
|
|
142
|
+
res[nm] = np.fromfile(fs[0], d).reshape(sh)
|
|
143
|
+
return res
|
chipwright/device_adb.py
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"""ADB transport — push an LLM bundle to an Android board and drive it over adb.
|
|
2
|
+
|
|
3
|
+
The device layer the `llm` modality uses. The existing SSH transport (`device.py::RemoteQNN`) reaches
|
|
4
|
+
a Linux board (QCS6490/RB3, the QDC fleet) over `ssh`/`scp`; a Snapdragon *phone* class target speaks
|
|
5
|
+
`adb` instead. This module is the adb parallel — same shape (`shell`/`push`/`pull`/`exists`,
|
|
6
|
+
retry-wrapped, a preflight that fails by name) so the modality can hold one interface and pick a
|
|
7
|
+
transport by target, exactly as the resolver holds one grammar across arches.
|
|
8
|
+
|
|
9
|
+
Two things are deliberate and load-bearing (both learned on real silicon, see spec 002 edge cases):
|
|
10
|
+
|
|
11
|
+
* **push one file at a time.** A single multi-GB `adb push` of a *directory* (an LLM Genie bundle is
|
|
12
|
+
N context binaries, often several GB) can wedge the device's adb daemon — the push stalls and every
|
|
13
|
+
later command hangs. `push_file` moves exactly one regular file and refuses a directory by name, so
|
|
14
|
+
the modality walks the bundle file-by-file rather than handing adb a tree that bricks the session.
|
|
15
|
+
* **`adb` is a system binary, not a pip dep.** Like `sshpass` for the SSH transport, it is shelled
|
|
16
|
+
out to; `_require_adb()` fails here, by name, with the install hint — not later with a cryptic
|
|
17
|
+
FileNotFoundError from subprocess.
|
|
18
|
+
"""
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import os
|
|
22
|
+
import shutil
|
|
23
|
+
import subprocess
|
|
24
|
+
import time
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _require_adb():
|
|
28
|
+
"""`adb` is a system binary (Android platform-tools), not a pip package, the transport shells to.
|
|
29
|
+
|
|
30
|
+
Fail here, by name, with the install hint — mirroring `device.py::_require_sshpass` — so a missing
|
|
31
|
+
system dependency is reported the way the framework reports everything: plainly, naming the fix.
|
|
32
|
+
"""
|
|
33
|
+
if shutil.which("adb") is None:
|
|
34
|
+
raise RuntimeError(
|
|
35
|
+
"chipwright llm needs the `adb` system binary to reach the board over Android Debug "
|
|
36
|
+
"Bridge, and it is not on PATH. Install the Android platform-tools: "
|
|
37
|
+
"`brew install --cask android-platform-tools` (macOS) or "
|
|
38
|
+
"`apt-get install android-tools-adb` (Debian/Ubuntu).")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class AdbDevice:
|
|
42
|
+
"""Drive one Android board over adb. `serial` disambiguates when several are attached.
|
|
43
|
+
|
|
44
|
+
The surface the LLM modality calls: `shell(cmd) -> CompletedProcess`, `push_file(local, remote)`
|
|
45
|
+
(one regular file), `pull(remote, local)`, `exists(remote) -> bool`. Every call is wrapped in the
|
|
46
|
+
same bounded retry as `RemoteQNN` because adb — like ssh to a thermally-throttling board — drops a
|
|
47
|
+
command now and then and a blind retry is the right, honest fix.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
def __init__(self, serial=None, board_dir="/data/local/tmp/chipwright", adb_bin="adb"):
|
|
51
|
+
_require_adb()
|
|
52
|
+
self.serial = serial
|
|
53
|
+
self.board_dir = board_dir
|
|
54
|
+
self.adb_bin = adb_bin
|
|
55
|
+
|
|
56
|
+
# --- plumbing -----------------------------------------------------------------------------
|
|
57
|
+
def _base(self):
|
|
58
|
+
"""The adb argv prefix, pinned to this device's serial when one was given."""
|
|
59
|
+
return [self.adb_bin] + (["-s", self.serial] if self.serial else [])
|
|
60
|
+
|
|
61
|
+
def _run(self, *args, timeout=600):
|
|
62
|
+
return subprocess.run(self._base() + list(args), capture_output=True, text=True, timeout=timeout)
|
|
63
|
+
|
|
64
|
+
def _retry(self, fn, tries=4, wait=3):
|
|
65
|
+
"""Run `fn` up to `tries` times; return the first returncode==0 result, else the last result/err.
|
|
66
|
+
|
|
67
|
+
Identical discipline to `RemoteQNN._retry` — the caller inspects `returncode` and decides; a
|
|
68
|
+
transport blip is retried, a real failure is returned so it can be reported by name.
|
|
69
|
+
"""
|
|
70
|
+
last = None
|
|
71
|
+
for _ in range(tries):
|
|
72
|
+
try:
|
|
73
|
+
r = fn()
|
|
74
|
+
if getattr(r, "returncode", 0) == 0:
|
|
75
|
+
return r
|
|
76
|
+
last = r
|
|
77
|
+
except Exception as e: # pragma: no cover
|
|
78
|
+
last = e
|
|
79
|
+
time.sleep(wait)
|
|
80
|
+
return last
|
|
81
|
+
|
|
82
|
+
# --- the interface the modality calls -----------------------------------------------------
|
|
83
|
+
def shell(self, cmd, timeout=600):
|
|
84
|
+
"""Run `cmd` on the board (`adb shell`) and return the CompletedProcess (retry-wrapped)."""
|
|
85
|
+
return self._retry(lambda: self._run("shell", cmd, timeout=timeout))
|
|
86
|
+
|
|
87
|
+
def push_file(self, local, remote, timeout=1800):
|
|
88
|
+
"""Push ONE regular file to `remote`. Refuses a directory by name.
|
|
89
|
+
|
|
90
|
+
A directory push is the exact thing that wedges the adb daemon on a multi-GB bundle, so we make
|
|
91
|
+
it an error here rather than let a caller do it by accident — the modality is expected to walk
|
|
92
|
+
the bundle and push each file. Ensures the remote parent dir exists first.
|
|
93
|
+
"""
|
|
94
|
+
if not os.path.isfile(local):
|
|
95
|
+
if os.path.isdir(local):
|
|
96
|
+
raise ValueError(
|
|
97
|
+
f"push_file is one file at a time by design (a multi-GB directory push wedges the "
|
|
98
|
+
f"adb daemon); {local!r} is a directory — walk it and push each file.")
|
|
99
|
+
raise FileNotFoundError(f"no such file to push: {local!r}")
|
|
100
|
+
parent = os.path.dirname(remote.rstrip("/"))
|
|
101
|
+
if parent:
|
|
102
|
+
self._retry(lambda: self._run("shell", f"mkdir -p {parent}", timeout=40))
|
|
103
|
+
r = self._retry(lambda: self._run("push", local, remote, timeout=timeout))
|
|
104
|
+
if getattr(r, "returncode", 1) != 0:
|
|
105
|
+
raise RuntimeError(f"adb push failed ({local} -> {remote}): {getattr(r, 'stderr', '')}")
|
|
106
|
+
return remote
|
|
107
|
+
|
|
108
|
+
def pull(self, remote, local, timeout=1800):
|
|
109
|
+
"""Pull `remote` off the board to the local path `local` (retry-wrapped)."""
|
|
110
|
+
parent = os.path.dirname(local.rstrip("/"))
|
|
111
|
+
if parent:
|
|
112
|
+
os.makedirs(parent, exist_ok=True)
|
|
113
|
+
r = self._retry(lambda: self._run("pull", remote, local, timeout=timeout))
|
|
114
|
+
if getattr(r, "returncode", 1) != 0:
|
|
115
|
+
raise RuntimeError(f"adb pull failed ({remote} -> {local}): {getattr(r, 'stderr', '')}")
|
|
116
|
+
return local
|
|
117
|
+
|
|
118
|
+
def exists(self, remote, timeout=40) -> bool:
|
|
119
|
+
"""True iff `remote` is present on the board (`test -e … && echo OK`)."""
|
|
120
|
+
r = self.shell(f"test -e {remote} && echo OK", timeout=timeout)
|
|
121
|
+
return "OK" in (getattr(r, "stdout", "") or "")
|