@trillboards/edge-sdk 0.2.89 → 0.2.92
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.
- package/deploy/detector/README.md +76 -7
- package/deploy/detector/bench/box_units.py +71 -0
- package/deploy/detector/bench/run_candidate.py +76 -51
- package/deploy/detector/bench/verify_detector_decode.py +574 -0
- package/deploy/detector/bench/verify_yolo_decode.py +4 -1
- package/deploy/detector/tests/test_box_units.py +119 -0
- package/deploy/detector/tests/test_decode.py +559 -0
- package/deploy/detector/tests/test_loader.py +10 -1
- package/deploy/detector/tests/test_registry.py +42 -4
- package/deploy/detector/tests/test_server.py +9 -1
- package/deploy/detector/tests/test_session.py +402 -1
- package/deploy/detector/trillboards_detector/decode.py +373 -0
- package/deploy/detector/trillboards_detector/labels.py +139 -0
- package/deploy/detector/trillboards_detector/loader.py +15 -1
- package/deploy/detector/trillboards_detector/registry.py +9 -0
- package/deploy/detector/trillboards_detector/session.py +233 -26
- package/dist/EdgeAgent.js +1 -1
- package/dist/diagnose.d.ts +1 -1
- package/dist/diagnose.js +1 -1
- package/package.json +1 -1
|
@@ -51,12 +51,44 @@ what is resident on that box right now.
|
|
|
51
51
|
|
|
52
52
|
```
|
|
53
53
|
labels.py one committed ModelVocabulary per model (slots -> class names,
|
|
54
|
-
logit width, measured RAM
|
|
54
|
+
logit width, measured RAM, and the VariantContract naming what
|
|
55
|
+
its ONNX output MEANS). ClassIndex is the UNION over them.
|
|
56
|
+
decode.py the three output layouts and the arithmetic each needs, plus the
|
|
57
|
+
only IoU/NMS in the Python tree. NUMPY ONLY — no ORT, no Pillow.
|
|
55
58
|
registry.py name -> session map, the class index, lazy load, LRU eviction
|
|
56
59
|
under a RAM budget, and the merge. STDLIB ONLY.
|
|
57
60
|
loader.py the poll loop + the real ORT session factory and probe frame.
|
|
58
61
|
```
|
|
59
62
|
|
|
63
|
+
### Output layouts
|
|
64
|
+
|
|
65
|
+
Three, and **which one an artifact uses is DECLARED, never sniffed**. The
|
|
66
|
+
declaration is a `VariantContract` on the model's vocabulary — the box-side copy
|
|
67
|
+
of that artifact's `(model_id, format)` row in `model_platform_variants`
|
|
68
|
+
(`output_layout`, `box_units`, `box_scale_to_pixels`) — and `_read_graph_spec`
|
|
69
|
+
CHECKS the loaded graph against it and refuses on disagreement.
|
|
70
|
+
|
|
71
|
+
| layout | shape | needs NMS | preprocessing |
|
|
72
|
+
|---|---|---|---|
|
|
73
|
+
| `rfdetr_dets_labels` | `dets [1,Q,4]` + `labels [1,Q,C]` | no (set prediction) | ImageNet-normalised **stretch** |
|
|
74
|
+
| `anchor_4_plus_nc` | `[1, 4+nc, A]`, scores already sigmoid | **yes** | Ultralytics **letterbox**, 114-grey, /255 |
|
|
75
|
+
| `end2end_n_6` | `[1, N, 6]` = x1,y1,x2,y2,score,cls | no (already suppressed) | Ultralytics **letterbox**, 114-grey, /255 |
|
|
76
|
+
|
|
77
|
+
Sniffing is forbidden because `[1, 300, 6]` and `[1, 6, 8400]` are both rank-3
|
|
78
|
+
with a 6 in them: a reader that gets one backwards reads x1,y1,x2,y2,score,cls
|
|
79
|
+
as four box channels and two classes, with no exception and a plausible table.
|
|
80
|
+
|
|
81
|
+
**`box_units` is a property of the FORMAT, not of the model.** Ultralytics wraps
|
|
82
|
+
a `task=detect`, `end2end=False` TFLite export in `_NormalizeCoords`, dividing
|
|
83
|
+
the box channels by (W, H). Measured across all 17 committed LiteRT twins: 16 of
|
|
84
|
+
17 have ONNX boxes in pixels 0..640 and LiteRT boxes in 0..1, agreeing to
|
|
85
|
+
0.00037 px after multiplying by 640, with class scores agreeing to 5.5e-06.
|
|
86
|
+
`queue_supermarket`, the one `end2end_n_6` twin, is pixels on both sides. Every
|
|
87
|
+
other check passes on such a pair — same shape, same dtype, both load, both run
|
|
88
|
+
— so a decoder that assumes pixels draws every box 640x too small in silence.
|
|
89
|
+
The two preprocessors are likewise NOT interchangeable and `/health` reports
|
|
90
|
+
which one ran, next to `output_layout`.
|
|
91
|
+
|
|
60
92
|
* **A class routes to the FIRST catalogue model that names it.** Adding a
|
|
61
93
|
specialist can never move an existing class off the model it was scored on —
|
|
62
94
|
`person` stays on `rfdetr_medium` even after a PPE model that also emits it.
|
|
@@ -181,12 +213,13 @@ python3 -m unittest discover -s . -p "test_*.py" -v
|
|
|
181
213
|
|
|
182
214
|
Runs in `guards-fast`. `test_registry.py` covers the whole residency policy —
|
|
183
215
|
routing, lazy load, eviction, and every refusal — with FAKE sessions, so it runs
|
|
184
|
-
there too. **
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
216
|
+
there too. **The wire contract and the residency policy need nothing but the
|
|
217
|
+
standard library**; `test_decode.py` needs numpy and nothing else (`decode.py`
|
|
218
|
+
imports no ORT, deliberately, so the arithmetic is exercisable on any runner
|
|
219
|
+
that can `import numpy`), and `test_session.py` needs onnxruntime too because
|
|
220
|
+
`session.py` imports it. Each skips itself with a stated reason. That layering
|
|
221
|
+
is the durable claim; any count is a snapshot and goes stale the next time a
|
|
222
|
+
case is added, so derive it by running rather than reading it here.
|
|
190
223
|
|
|
191
224
|
```bash
|
|
192
225
|
# The real client against the real service, over loopback.
|
|
@@ -218,3 +251,39 @@ uv pip install --python /tmp/rfdetr-ref/bin/python rfdetr onnx onnxruntime pillo
|
|
|
218
251
|
Runs both pipelines on one image and fails on any disagreement. A documented
|
|
219
252
|
manual run, not a CI step — it needs a checkpoint download and ~3 GB of
|
|
220
253
|
dependencies.
|
|
254
|
+
|
|
255
|
+
`bench/verify_detector_decode.py` is the same control for the two YOLO layouts,
|
|
256
|
+
against `ultralytics` itself, in three arms that each hold one variable:
|
|
257
|
+
|
|
258
|
+
```bash
|
|
259
|
+
uv venv /tmp/yolo-ref --python 3.11
|
|
260
|
+
uv pip install --python /tmp/yolo-ref/bin/python ultralytics onnxruntime pillow ai-edge-litert
|
|
261
|
+
|
|
262
|
+
# decode: SAME raw tensor, our decode vs ultralytics NMS + scale_boxes.
|
|
263
|
+
/tmp/yolo-ref/bin/python bench/verify_detector_decode.py --arm decode \
|
|
264
|
+
--models-dir /path/to/onnx --frames-dir /path/to/frames --tolerance-px 0.0
|
|
265
|
+
|
|
266
|
+
# pipeline: the whole service path vs YOLO(...).predict, resize kernel held constant.
|
|
267
|
+
/tmp/yolo-ref/bin/python bench/verify_detector_decode.py --arm pipeline \
|
|
268
|
+
--models-dir /path/to/onnx --frames-dir /path/to/frames --tolerance-px 0.001
|
|
269
|
+
|
|
270
|
+
# litert: the same weights through both runtimes, in PIXELS, per the DECLARED scale.
|
|
271
|
+
/tmp/yolo-ref/bin/python bench/verify_detector_decode.py --arm litert \
|
|
272
|
+
--models-dir /path/to/onnx --frames-dir /path/to/frames \
|
|
273
|
+
--litert-manifest /path/to/litert/MANIFEST.json --tolerance-px 0.01
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
Two traps this bench cost, both of them the instrument rather than the decode,
|
|
277
|
+
and both now closed in the file:
|
|
278
|
+
|
|
279
|
+
* **The reference must be the ONNX, not the `.pt`** — Ultralytics letterboxes a
|
|
280
|
+
checkpoint to 640x480 while the graph is fixed 640x640, so the two arms
|
|
281
|
+
disagree by up to 0.25 confidence in both directions and it reads as a decode
|
|
282
|
+
bug.
|
|
283
|
+
* **Hold the resize kernel constant too.** Pillow's BILINEAR low-passes on
|
|
284
|
+
downscale, cv2's `INTER_LINEAR` does not, and on one 1200x900 frame that moved
|
|
285
|
+
a class from 0.18 to 0.51 — 20 of 378 presence decisions apart. The
|
|
286
|
+
`pipeline` arm therefore letterboxes ONCE and feeds both sides those bytes,
|
|
287
|
+
and it feeds the reference the pixels that came back OUT of the JPEG rather
|
|
288
|
+
than the canvas that went in, because JPEG at quality 100 is still lossy
|
|
289
|
+
enough to move a borderline box.
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""What a box coordinate MEANS, decided from a forward pass — STDLIB ONLY.
|
|
2
|
+
|
|
3
|
+
Split out of `run_candidate.py` so the `detector-service` CI lane can reach it.
|
|
4
|
+
That lane runs `python3 -m unittest discover` with NO pip install, and
|
|
5
|
+
`run_candidate.py` imports `trillboards_detector.session` and therefore
|
|
6
|
+
onnxruntime at module scope — so a test importing it does not skip, it errors
|
|
7
|
+
the whole lane during discovery. The decision lives here in stdlib; the forward
|
|
8
|
+
pass that produces `box_abs_max` stays there.
|
|
9
|
+
|
|
10
|
+
Same split, and the same reason, as `bench/twin_parity_contract.py`.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
#: LiteRT boxes in 0..1, agreeing to 0.00266 px after multiplying by 640, while
|
|
17
|
+
#: their CLASS channels agree to 1.2e-07. `queue_supermarket`, the one end2end
|
|
18
|
+
#: twin, agrees on both sides — end2end heads emit pixels everywhere.
|
|
19
|
+
#:
|
|
20
|
+
#: Every check a publish gate would otherwise run PASSES on that pair: same
|
|
21
|
+
#: output shape, same dtype, loads, runs, class scores identical. An Android
|
|
22
|
+
#: post-processor reusing the ONNX decode draws every box 640x too small and
|
|
23
|
+
#: nothing reports an error. So the units are MEASURED from a forward pass, per
|
|
24
|
+
#: artifact, and recorded — never carried across from the sibling format.
|
|
25
|
+
#:
|
|
26
|
+
#: The discriminator is a ratio, max|box channel| / imgsz, and it is decisive by
|
|
27
|
+
#: two and a half orders of magnitude. Measured over 119 ONNX artifacts the ratio
|
|
28
|
+
#: spans 0.988..1.167; a normalised head sits at ~1/640 = 0.0016. The dead zone
|
|
29
|
+
#: below is EMPTY across every artifact in the tree, and an artifact landing in it
|
|
30
|
+
#: is refused rather than guessed at — that is the whole point of the field.
|
|
31
|
+
BOX_UNITS_PIXEL_RATIO_MIN = 0.2
|
|
32
|
+
BOX_UNITS_NORMALIZED_RATIO_MAX = 0.02
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def classify_box_units(box_abs_max: float, imgsz: int) -> dict:
|
|
36
|
+
"""pixels, normalized, or REFUSE — never a guess.
|
|
37
|
+
|
|
38
|
+
Returns the verdict plus the evidence, because a bare enum in a score card is
|
|
39
|
+
a claim and the ratio is what makes it checkable.
|
|
40
|
+
"""
|
|
41
|
+
# `<= 0.0` MATTERS AS MUCH AS `is None`, and it is the case that actually
|
|
42
|
+
# happens. A model whose classification head is dead still emits box rows —
|
|
43
|
+
# `forklift_akdve_11n` has healthy boxes (2.93-636 px) and a max class score
|
|
44
|
+
# of 0.0063 — so nothing is admitted at threshold, `box_abs_max` stays at its
|
|
45
|
+
# 0.0 seed, and the ratio 0.0/imgsz lands under BOX_UNITS_NORMALIZED_RATIO_MAX.
|
|
46
|
+
# The old predicate then returned a CONFIDENT `normalized` with
|
|
47
|
+
# scale_to_pixels=640 from zero observations, and that declaration is exactly
|
|
48
|
+
# what `session.py` now trusts absolutely: the layout and units are DECLARED
|
|
49
|
+
# by the variant contract and never re-derived from the tensor. So a dead
|
|
50
|
+
# artifact published a coordinate-space claim no frame supports.
|
|
51
|
+
#
|
|
52
|
+
# The refusal string below already described this case. It just could not be
|
|
53
|
+
# reached, which is the whole shape of the defect this function exists to
|
|
54
|
+
# prevent — a check that runs, passes, and measures nothing.
|
|
55
|
+
if imgsz <= 0 or box_abs_max is None or box_abs_max <= 0.0:
|
|
56
|
+
return {"units": None, "ratio": None, "scale_to_pixels": None,
|
|
57
|
+
"refusal": "no box channels were observed, so the coordinate space is unmeasured"}
|
|
58
|
+
ratio = box_abs_max / imgsz
|
|
59
|
+
if ratio >= BOX_UNITS_PIXEL_RATIO_MIN:
|
|
60
|
+
return {"units": "pixels", "ratio": ratio, "scale_to_pixels": 1.0, "refusal": None}
|
|
61
|
+
if ratio <= BOX_UNITS_NORMALIZED_RATIO_MAX:
|
|
62
|
+
return {"units": "normalized", "ratio": ratio, "scale_to_pixels": float(imgsz), "refusal": None}
|
|
63
|
+
return {
|
|
64
|
+
"units": None, "ratio": ratio, "scale_to_pixels": None,
|
|
65
|
+
"refusal": (
|
|
66
|
+
f"max|box|/imgsz = {ratio:.4f} falls between {BOX_UNITS_NORMALIZED_RATIO_MAX} and "
|
|
67
|
+
f"{BOX_UNITS_PIXEL_RATIO_MIN}, where neither pixels nor normalised is credible. "
|
|
68
|
+
"Measured over 119 artifacts this band is empty; refusing rather than picking one, "
|
|
69
|
+
"because the wrong pick is silent and scales every box by 640."
|
|
70
|
+
),
|
|
71
|
+
}
|
|
@@ -82,11 +82,19 @@ import time
|
|
|
82
82
|
from pathlib import Path
|
|
83
83
|
|
|
84
84
|
sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) # noqa: E402
|
|
85
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent)) # noqa: E402 (bench siblings)
|
|
85
86
|
|
|
87
|
+
from box_units import ( # noqa: E402 (sibling module, stdlib-only on purpose)
|
|
88
|
+
BOX_UNITS_NORMALIZED_RATIO_MAX,
|
|
89
|
+
BOX_UNITS_PIXEL_RATIO_MIN,
|
|
90
|
+
classify_box_units,
|
|
91
|
+
)
|
|
86
92
|
from trillboards_detector.errors import ModelContractError # noqa: E402
|
|
87
93
|
from trillboards_detector.labels import ( # noqa: E402
|
|
94
|
+
BOX_UNITS_PIXELS,
|
|
88
95
|
BUILTIN_VOCABULARIES,
|
|
89
96
|
ModelVocabulary,
|
|
97
|
+
VariantContract,
|
|
90
98
|
normalize_class_name,
|
|
91
99
|
)
|
|
92
100
|
from trillboards_detector.registry import read_process_rss # noqa: E402
|
|
@@ -105,23 +113,7 @@ SCHEMA = "detector-candidate-run/2"
|
|
|
105
113
|
#: Ultralytics' TFLite export path wraps a `task=detect`, `end2end=False` model in
|
|
106
114
|
#: `_NormalizeCoords`, dividing the four box channels by (W, H). Measured here on
|
|
107
115
|
#: all 17 committed LiteRT twins: 16 of 17 have ONNX boxes in pixels 0..640 and
|
|
108
|
-
|
|
109
|
-
#: their CLASS channels agree to 1.2e-07. `queue_supermarket`, the one end2end
|
|
110
|
-
#: twin, agrees on both sides — end2end heads emit pixels everywhere.
|
|
111
|
-
#:
|
|
112
|
-
#: Every check a publish gate would otherwise run PASSES on that pair: same
|
|
113
|
-
#: output shape, same dtype, loads, runs, class scores identical. An Android
|
|
114
|
-
#: post-processor reusing the ONNX decode draws every box 640x too small and
|
|
115
|
-
#: nothing reports an error. So the units are MEASURED from a forward pass, per
|
|
116
|
-
#: artifact, and recorded — never carried across from the sibling format.
|
|
117
|
-
#:
|
|
118
|
-
#: The discriminator is a ratio, max|box channel| / imgsz, and it is decisive by
|
|
119
|
-
#: two and a half orders of magnitude. Measured over 119 ONNX artifacts the ratio
|
|
120
|
-
#: spans 0.988..1.167; a normalised head sits at ~1/640 = 0.0016. The dead zone
|
|
121
|
-
#: below is EMPTY across every artifact in the tree, and an artifact landing in it
|
|
122
|
-
#: is refused rather than guessed at — that is the whole point of the field.
|
|
123
|
-
BOX_UNITS_PIXEL_RATIO_MIN = 0.2
|
|
124
|
-
BOX_UNITS_NORMALIZED_RATIO_MAX = 0.02
|
|
116
|
+
|
|
125
117
|
|
|
126
118
|
#: Warm-up forward passes before any timing is kept. ORT's first run pays for
|
|
127
119
|
#: arena allocation and kernel selection; folding that into a p50 over 134 frames
|
|
@@ -269,31 +261,6 @@ def classify_output_layout(output_shapes: "list[list]") -> str:
|
|
|
269
261
|
return f"unknown_rank_{len(shape)}"
|
|
270
262
|
|
|
271
263
|
|
|
272
|
-
def classify_box_units(box_abs_max: float, imgsz: int) -> dict:
|
|
273
|
-
"""pixels, normalized, or REFUSE — never a guess.
|
|
274
|
-
|
|
275
|
-
Returns the verdict plus the evidence, because a bare enum in a score card is
|
|
276
|
-
a claim and the ratio is what makes it checkable.
|
|
277
|
-
"""
|
|
278
|
-
if imgsz <= 0 or box_abs_max is None:
|
|
279
|
-
return {"units": None, "ratio": None, "scale_to_pixels": None,
|
|
280
|
-
"refusal": "no box channels were observed, so the coordinate space is unmeasured"}
|
|
281
|
-
ratio = box_abs_max / imgsz
|
|
282
|
-
if ratio >= BOX_UNITS_PIXEL_RATIO_MIN:
|
|
283
|
-
return {"units": "pixels", "ratio": ratio, "scale_to_pixels": 1.0, "refusal": None}
|
|
284
|
-
if ratio <= BOX_UNITS_NORMALIZED_RATIO_MAX:
|
|
285
|
-
return {"units": "normalized", "ratio": ratio, "scale_to_pixels": float(imgsz), "refusal": None}
|
|
286
|
-
return {
|
|
287
|
-
"units": None, "ratio": ratio, "scale_to_pixels": None,
|
|
288
|
-
"refusal": (
|
|
289
|
-
f"max|box|/imgsz = {ratio:.4f} falls between {BOX_UNITS_NORMALIZED_RATIO_MAX} and "
|
|
290
|
-
f"{BOX_UNITS_PIXEL_RATIO_MIN}, where neither pixels nor normalised is credible. "
|
|
291
|
-
"Measured over 119 artifacts this band is empty; refusing rather than picking one, "
|
|
292
|
-
"because the wrong pick is silent and scales every box by 640."
|
|
293
|
-
),
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
|
|
297
264
|
def letterbox(image, width: int, height: int):
|
|
298
265
|
"""Ultralytics' resize: scale to fit, pad the remainder 114-grey, centred.
|
|
299
266
|
|
|
@@ -497,13 +464,24 @@ class ShippedRunner:
|
|
|
497
464
|
"""Inference through `OnnxDetector` — the class the connector actually runs."""
|
|
498
465
|
|
|
499
466
|
path_kind = "shipped"
|
|
500
|
-
preprocessing = "session.preprocess (imagenet-normalised stretch)"
|
|
501
467
|
variant_format = "onnx"
|
|
502
|
-
layout = "rfdetr_dets_labels"
|
|
503
468
|
|
|
504
469
|
def __init__(self, model_path: Path, vocabulary: ModelVocabulary):
|
|
505
470
|
self.detector = OnnxDetector(model_path, vocabulary)
|
|
506
471
|
self.vocabulary = vocabulary
|
|
472
|
+
# READ OFF THE LOADED GRAPH, never a constant. These two feed the run
|
|
473
|
+
# file's `variant.output_layout` / `variant.end2end` and its
|
|
474
|
+
# `preprocessing`, and this class stopped being RF-DETR-only the moment
|
|
475
|
+
# the service learned the two Ultralytics layouts — a hardcoded
|
|
476
|
+
# `rfdetr_dets_labels` would publish a score card whose declared layout
|
|
477
|
+
# is wrong for every YOLO artifact, which is the exact defect the
|
|
478
|
+
# declaration exists to prevent.
|
|
479
|
+
self.layout = self.detector.spec.output_layout
|
|
480
|
+
self.preprocessing = (
|
|
481
|
+
"session.preprocess (imagenet-normalised stretch)"
|
|
482
|
+
if self.layout == "rfdetr_dets_labels"
|
|
483
|
+
else "session.preprocess_letterbox (ultralytics letterbox, 114-grey, /255)"
|
|
484
|
+
)
|
|
507
485
|
self.width = self.detector.spec.width
|
|
508
486
|
self.height = self.detector.spec.height
|
|
509
487
|
self.providers = list(self.detector.spec.providers)
|
|
@@ -575,7 +553,9 @@ class Frame:
|
|
|
575
553
|
self._image = None
|
|
576
554
|
|
|
577
555
|
|
|
578
|
-
def synthesise_vocabulary(
|
|
556
|
+
def synthesise_vocabulary(
|
|
557
|
+
name: str, class_names: "dict[int, str]", output_layout: str
|
|
558
|
+
) -> ModelVocabulary:
|
|
579
559
|
"""A ModelVocabulary over a model's OWN names, canonicalised the service's way.
|
|
580
560
|
|
|
581
561
|
Synthesised rather than committed so the loadability check is a FAIR one: it
|
|
@@ -594,7 +574,35 @@ def synthesise_vocabulary(name: str, class_names: "dict[int, str]") -> ModelVoca
|
|
|
594
574
|
continue
|
|
595
575
|
slots[idx] = canonical
|
|
596
576
|
return ModelVocabulary(
|
|
597
|
-
model_name=name,
|
|
577
|
+
model_name=name,
|
|
578
|
+
slot_names=slots,
|
|
579
|
+
logit_slots=width,
|
|
580
|
+
approx_ram_bytes=1,
|
|
581
|
+
# THE LAYOUT IS DERIVED FROM THE GRAPH, and it is the only half
|
|
582
|
+
# `_read_graph_spec` reads — so the probe declares what it just measured
|
|
583
|
+
# rather than a guess, and an artifact whose layout this build cannot name
|
|
584
|
+
# fails `VariantContract` here and is recorded as not loadable, with the
|
|
585
|
+
# reason, which is the right answer to "would the service load this?".
|
|
586
|
+
#
|
|
587
|
+
# THE UNITS ARE THE IDENTITY, AND THAT IS THE MEASUREMENT, not a claim
|
|
588
|
+
# about the artifact. They cannot be known here — `classify_box_units`
|
|
589
|
+
# needs a forward pass, which happens later and lands in the run file's
|
|
590
|
+
# `variant` block — and one caller of this function (`ShippedRunner`)
|
|
591
|
+
# DOES decode with them. `pixels`/1.0 is the only pair that scales
|
|
592
|
+
# nothing, so the graph's own box magnitudes reach `box_abs_max`
|
|
593
|
+
# untouched and the classifier sees what the model actually emitted.
|
|
594
|
+
# Declaring the other pair would multiply by imgsz the very quantity
|
|
595
|
+
# being measured, and the verdict would then be a function of the guess.
|
|
596
|
+
#
|
|
597
|
+
# This is right for ONNX specifically: measured across all 17 committed
|
|
598
|
+
# twins, every ONNX side is pixels and only the LiteRT sides are
|
|
599
|
+
# normalised — and `.tflite` never reaches here, it routes to
|
|
600
|
+
# `LitertRunner`. A normalised ONNX artifact would still be REPORTED
|
|
601
|
+
# correctly by `classify_box_units`, because the identity leaves its
|
|
602
|
+
# 0..1 boxes as 0..1; what it would lose is per-class detections on
|
|
603
|
+
# padded frames, which is the signal to read the units verdict before
|
|
604
|
+
# trusting that candidate's score.
|
|
605
|
+
onnx_variant=VariantContract(output_layout, BOX_UNITS_PIXELS, 1.0),
|
|
598
606
|
)
|
|
599
607
|
|
|
600
608
|
|
|
@@ -613,6 +621,7 @@ def probe_loadability(model_path: Path, class_names: "dict[int, str]") -> dict:
|
|
|
613
621
|
f"the detector service builds an onnxruntime InferenceSession and this is a "
|
|
614
622
|
f"{model_path.suffix} artifact: {str(exc).splitlines()[0][:200]}"
|
|
615
623
|
),
|
|
624
|
+
"output_layout": None,
|
|
616
625
|
}
|
|
617
626
|
|
|
618
627
|
opts = ort.SessionOptions()
|
|
@@ -620,15 +629,28 @@ def probe_loadability(model_path: Path, class_names: "dict[int, str]") -> dict:
|
|
|
620
629
|
session = ort.InferenceSession(
|
|
621
630
|
str(model_path), sess_options=opts, providers=["CPUExecutionProvider"]
|
|
622
631
|
)
|
|
632
|
+
# DERIVED ONCE AND RETURNED, because the only other way for a caller to learn
|
|
633
|
+
# it is to open a second session — and the caller that needs it sits between
|
|
634
|
+
# the RSS baseline and `rss_after_session`, where a transient session is
|
|
635
|
+
# exactly the contamination `introspect_in_subprocess` exists to avoid.
|
|
636
|
+
layout = classify_output_layout([list(o.shape) for o in session.get_outputs()])
|
|
623
637
|
try:
|
|
624
|
-
vocabulary = synthesise_vocabulary("probe-only", class_names)
|
|
638
|
+
vocabulary = synthesise_vocabulary("probe-only", class_names, layout)
|
|
625
639
|
except ValueError as exc:
|
|
626
|
-
return {
|
|
640
|
+
return {
|
|
641
|
+
"shipped_service_loadable": False,
|
|
642
|
+
"refusal": f"vocabulary: {exc}",
|
|
643
|
+
"output_layout": layout,
|
|
644
|
+
}
|
|
627
645
|
try:
|
|
628
646
|
_read_graph_spec(session, vocabulary)
|
|
629
647
|
except ModelContractError as exc:
|
|
630
|
-
return {
|
|
631
|
-
|
|
648
|
+
return {
|
|
649
|
+
"shipped_service_loadable": False,
|
|
650
|
+
"refusal": str(exc),
|
|
651
|
+
"output_layout": layout,
|
|
652
|
+
}
|
|
653
|
+
return {"shipped_service_loadable": True, "refusal": None, "output_layout": layout}
|
|
632
654
|
|
|
633
655
|
|
|
634
656
|
def introspect_in_subprocess(model_path: Path) -> dict:
|
|
@@ -759,7 +781,10 @@ def main() -> int:
|
|
|
759
781
|
elif model_path.suffix.lower() == ".tflite":
|
|
760
782
|
runner = LitertRunner(model_path, declared_names)
|
|
761
783
|
elif loadability["shipped_service_loadable"]:
|
|
762
|
-
runner = ShippedRunner(
|
|
784
|
+
runner = ShippedRunner(
|
|
785
|
+
model_path,
|
|
786
|
+
synthesise_vocabulary(args.candidate, declared_names, loadability["output_layout"]),
|
|
787
|
+
)
|
|
763
788
|
else:
|
|
764
789
|
runner = HarnessYoloRunner(model_path, declared_names)
|
|
765
790
|
|