maglun commited on
Commit
c8d55fb
·
verified ·
1 Parent(s): 2aced58

Add SV0: Swedish one-pass form specialist (fp16 + int8 Core ML, eval receipts, MIT)

Browse files
.gitattributes CHANGED
@@ -1,35 +1,5 @@
1
- *.7z filter=lfs diff=lfs merge=lfs -text
2
- *.arrow filter=lfs diff=lfs merge=lfs -text
3
- *.bin filter=lfs diff=lfs merge=lfs -text
4
- *.bz2 filter=lfs diff=lfs merge=lfs -text
5
- *.ckpt filter=lfs diff=lfs merge=lfs -text
6
- *.ftz filter=lfs diff=lfs merge=lfs -text
7
- *.gz filter=lfs diff=lfs merge=lfs -text
8
- *.h5 filter=lfs diff=lfs merge=lfs -text
9
- *.joblib filter=lfs diff=lfs merge=lfs -text
10
- *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
- *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
- *.model filter=lfs diff=lfs merge=lfs -text
13
- *.msgpack filter=lfs diff=lfs merge=lfs -text
14
- *.npy filter=lfs diff=lfs merge=lfs -text
15
- *.npz filter=lfs diff=lfs merge=lfs -text
16
- *.onnx filter=lfs diff=lfs merge=lfs -text
17
- *.ot filter=lfs diff=lfs merge=lfs -text
18
- *.parquet filter=lfs diff=lfs merge=lfs -text
19
- *.pb filter=lfs diff=lfs merge=lfs -text
20
- *.pickle filter=lfs diff=lfs merge=lfs -text
21
- *.pkl filter=lfs diff=lfs merge=lfs -text
22
- *.pt filter=lfs diff=lfs merge=lfs -text
23
- *.pth filter=lfs diff=lfs merge=lfs -text
24
- *.rar filter=lfs diff=lfs merge=lfs -text
25
  *.safetensors filter=lfs diff=lfs merge=lfs -text
26
- saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
- *.tar.* filter=lfs diff=lfs merge=lfs -text
28
- *.tar filter=lfs diff=lfs merge=lfs -text
29
- *.tflite filter=lfs diff=lfs merge=lfs -text
30
- *.tgz filter=lfs diff=lfs merge=lfs -text
31
- *.wasm filter=lfs diff=lfs merge=lfs -text
32
- *.xz filter=lfs diff=lfs merge=lfs -text
33
- *.zip filter=lfs diff=lfs merge=lfs -text
34
- *.zst filter=lfs diff=lfs merge=lfs -text
35
- *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  *.safetensors filter=lfs diff=lfs merge=lfs -text
2
+ coreml/**/weight.bin filter=lfs diff=lfs merge=lfs -text
3
+ coreml/sv0_forms_fp16_options40.mlpackage/Data/com.apple.CoreML/model.mlmodel filter=lfs diff=lfs merge=lfs -text
4
+ coreml/sv0_forms_int4_options40.mlpackage/Data/com.apple.CoreML/model.mlmodel filter=lfs diff=lfs merge=lfs -text
5
+ coreml/sv0_forms_int8_options40.mlpackage/Data/com.apple.CoreML/model.mlmodel filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
 
LICENSE ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Precisit AB
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.
README.md ADDED
@@ -0,0 +1,115 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ license: mit
3
+ language:
4
+ - sv
5
+ library_name: coreml
6
+ pipeline_tag: text-classification
7
+ tags:
8
+ - jev
9
+ - system-one
10
+ - one-pass-scorer
11
+ - computer-use
12
+ - form-filling
13
+ - swedish
14
+ - coreml
15
+ - apple-silicon
16
+ base_model: []
17
+ ---
18
+
19
+ # One-Pass SV-Forms (SV0) — a Swedish one-pass form specialist (research checkpoint)
20
+
21
+ **This is a research checkpoint, not a product.** It is a 706 048-parameter model that scores a
22
+ supplied list of options in **one forward pass** — the "Jev"/System One shape — trained on
23
+ **synthetic** Swedish forms by a synthetic-data recipe we are publishing alongside it. It
24
+ generates no text, needs no tokenizer, and runs in ~1 ms per decision on an Apple laptop or
25
+ phone (Core ML package: 1.4 MB fp16, 787 KB int8).
26
+
27
+ | | |
28
+ | --- | --- |
29
+ | Task | given a UI element (role, label, state) plus the entities extracted from a document, pick one option: `fyll <entity>` / `kryssa` / `klicka` / `hoppa över` |
30
+ | Architecture | byte-level embeddings + 2-layer Transformer encoder (width 128, 4 heads) + option-attention head; `tinyx` |
31
+ | Parameters | 706 048 (2.83 MB fp16 checkpoint) |
32
+ | Context / option budget | 224 bytes of context, 96 bytes per option, **up to 40 options** |
33
+ | Training data | 900 synthetic Swedish form episodes (21 306 decisions), generated locally; **no real form, person or customer data** |
34
+ | Licence | MIT (weights and code) |
35
+ | Lineage | independent implementation of the same contract as [Cua's CUA-S1](https://huggingface.co/cua-ai/cua-s1-forms) and [jevlike](https://github.com/vinnylarouge/jevlike); trained from scratch, not a fine-tune of either |
36
+
37
+ ## Measured results (our harness, Apple M4)
38
+
39
+ | Run | decisions | top-1 | majority-class baseline | ECE | silently skipped a required fill |
40
+ | --- | ---: | ---: | ---: | ---: | ---: |
41
+ | Held-out synthetic test (form-signature disjoint) | 2 315 | **83.02 %** | 50.45 % | 0.017 | **76 (7.5 % of fills)** |
42
+ | Hand-written out-of-distribution Swedish demo | 50 | **86.00 %** | 64.00 % | 0.092 | 3 (of 32 fills) |
43
+ | Shuffled-context control (test) | 2 315 | 34.08 % | 50.45 % | 0.497 | 264 |
44
+
45
+ Per-action accuracy on the test split: `check` 98.3 %, `click` 93.7 %, `skip` 88.0 %,
46
+ `fill` 75.5 %. The shuffled-context control is the interesting one: rotating contexts between
47
+ rows drops the model below the majority baseline, i.e. it really reads the element label and the
48
+ document rather than exploiting option statistics.
49
+
50
+ For comparison, the released English checkpoint of the same family scores **21.4 %** on exactly
51
+ these Swedish rows — below the majority baseline, at its own shuffled-control floor. The
52
+ language was the barrier, not the contract.
53
+
54
+ Core ML export (same checkpoint, converted with coremltools 9.0):
55
+
56
+ | Variant | package | top-1 | argmax parity vs PyTorch | median latency | p95 |
57
+ | --- | ---: | ---: | ---: | ---: | ---: |
58
+ | fp16 (CPU + ANE) | 1.4 MB | 83.00 % | 0.99870 | 1.31 ms | 1.46 ms |
59
+ | **int8 (CPU + ANE)** | **787 KB** | **83.09 %** | 0.99611 | 1.32 ms | 1.40 ms |
60
+ | int4 (CPU + ANE) | 481 KB | 50.87 % | 0.48702 | 1.97 ms | 2.10 ms |
61
+ | PyTorch reference | 2.83 MB | 83.02 % | — | — | — |
62
+
63
+ We publish the int4 result because it is the most useful thing in this table: the same 4-bit
64
+ palettisation costs 0.06 pp on a converged model and destroys this one, and it also fails ANE
65
+ compilation. **Quantisation headroom is a property of the training run, not of the architecture.**
66
+ Use int8. `int4-weights/` ships the collapsed variant for reproducibility only.
67
+
68
+ ## What it is NOT
69
+
70
+ - **Not trained or tested on real Swedish forms.** The corpus is synthetic (`Label: value`
71
+ document entities, `.invalid` e-mail domains, fictional names, locally generated
72
+ personnummer-shaped strings with no link to any real person) and the only non-synthetic
73
+ evidence is a 50-decision set we wrote ourselves.
74
+ - **Not a general-purpose assistant or an autonomous agent.** It does not generate text, cannot
75
+ invent a value it was not given, and does not decide execution order.
76
+ - **Not finished.** Validation was still improving when the run stopped (48 % → 63 % → 79 % →
77
+ 83 % top-1 over four epochs), so treat 83 % as a floor for this recipe, not a ceiling.
78
+ - **Not safe to run unsupervised on real data.** 7.5 % of required fills are answered "skip" —
79
+ a required field that silently stays empty. Any real integration must verify outcomes outside
80
+ the model (fail-closed execution, dry run, one submit, human review before consequential
81
+ actions) — see the runtime contract in Cua's `planner.py` for a good pattern.
82
+
83
+ ## Intended use
84
+
85
+ Research on bounded, high-volume Swedish interface workflows where the option set is supplied by
86
+ deterministic code: filling forms from an extracted document, triaging/routing where the choices
87
+ are known in advance, or as a **criteria-decision** layer where a calibrated distribution per
88
+ question is more useful than generated prose. Also as a worked example of how to build and
89
+ measure a language-local one-pass specialist.
90
+
91
+ ## Usage
92
+
93
+ ```python
94
+ from pathlib import Path
95
+ from huggingface_hub import hf_hub_download
96
+
97
+ # the checkpoint format is <name>.safetensors + <name>.json (architecture + hashes)
98
+ weights = Path(hf_hub_download("precisit/one-pass-sv-forms", "sv0-forms.safetensors"))
99
+ hf_hub_download("precisit/one-pass-sv-forms", "sv0-forms.json", local_dir=weights.parent)
100
+ # then either use the Core ML packages (no Python runtime needed) or the PyTorch code in
101
+ # precisit/onepass — both are in this repository.
102
+ ```
103
+
104
+ The context string and options must be built exactly as in training (byte ids = UTF-8 byte + 1,
105
+ zero-padded; `UPPGIFT fyll i formuläret från dokumentet och skicka sedan in` / `FORM <title>` /
106
+ `ELEMENT <role> "<label>" value="…"`). A mismatch there is the most likely cause of poor output —
107
+ the recipe and the generator are in the toolkit repository.
108
+
109
+ ## Licence and attribution
110
+
111
+ MIT. The architecture, training loop and evaluation metrics come from Cua's MIT-licensed
112
+ `libs/cua-s1` (see `THIRD_PARTY_NOTICES.md`); the option-attention head design is credited there
113
+ to `jevlike` (MIT). The Swedish catalogue, synthetic generator, training run, measurements and
114
+ Core ML export are ours — **Precisit AB, 2026**. No TypeSafe AI code, weights or data was used;
115
+ "Jev" is their product and this is an independent implementation of a similar interface.
THIRD_PARTY_NOTICES.md ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Third-party notices
2
+
3
+ This release is an independent implementation of a public model interface. It uses code and
4
+ design from two MIT-licensed projects, and it is **not** affiliated with, derived from, or
5
+ endorsed by TypeSafe AI (the makers of "Jev").
6
+
7
+ ## trycua/cua — libs/cua-s1 (MIT)
8
+
9
+ - Source: https://github.com/trycua/cua/tree/main/libs/cua-s1
10
+ - Revision used: `9bbfa7d` (the commit that links the Hugging Face artifacts)
11
+ - What we use: the `tinyx` architecture (`cua_s1/model.py`), the checkpoint format
12
+ (`cua_s1/checkpoint.py`), the synthetic-data generator logic (`cua_s1/synth.py`), the
13
+ training loop (`training/train.py`) and the evaluation metrics (`evals/metrics.py`).
14
+ The files are vendored unmodified in the toolkit repository under `vendor/`, and the
15
+ generated corpus follows the same row/dataset format.
16
+ - Copyright and licence text: see the upstream repository's `LICENSE` (MIT).
17
+
18
+ Our model is **trained from scratch** on our own Swedish synthetic corpus: it shares the
19
+ architecture, the trainer and the data format, not any weights or data with Cua's release.
20
+
21
+ ## vinnylarouge/jevlike (MIT), via Cua's attribution
22
+
23
+ - Source: https://github.com/vinnylarouge/jevlike
24
+ - The option-attention head design (`AttentionHead`) and the byte-level collation shape used by
25
+ the `tinyx` model originate in `jevlike`; Cua's `model.py` credits commit `94f5fd1`
26
+ (MIT, Copyright 2026 Minimal Labs) and we keep that attribution intact in the vendored code.
27
+
28
+ ## What was NOT used
29
+
30
+ - No TypeSafe AI code, weights, data or API output. "Jev" and "System One" are TypeSafe AI's
31
+ terms; this release is an independent model that implements a similar input/output contract.
32
+ - No real personal data. The training corpus is synthetic: fictional names, `.invalid`
33
+ e-mail domains, locally generated personnummer-shaped identifiers that do not correspond to
34
+ any individual, and invented organisations.
coreml/conversion.json ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "checkpoint_sha256": "b9030d7a7b7abfaf70dd1cc2a390568eaa3a55849daa0757b60d01dbab162356",
3
+ "created_utc": "2026-09-21T08:00:57Z",
4
+ "limits": {
5
+ "context_bytes": 224,
6
+ "max_options": 40,
7
+ "option_bytes": 96
8
+ },
9
+ "model": "sv0-forms (spike jev-sv0)",
10
+ "model_config": {
11
+ "context_tokens": 224,
12
+ "encoder": "tinyx",
13
+ "heads": 4,
14
+ "layers": 2,
15
+ "option_tokens": 96,
16
+ "rank": 128,
17
+ "width": 128
18
+ },
19
+ "parameters": 706048,
20
+ "tooling": {
21
+ "coremltools": "9.0",
22
+ "python": "3.12.14",
23
+ "torch": "2.7.0"
24
+ },
25
+ "trace_check": null,
26
+ "variants": {
27
+ "fp16": {
28
+ "minimum_target": "iOS17/macOS14",
29
+ "path": "sv0_forms_fp16_options40.mlpackage",
30
+ "precision": "float16"
31
+ },
32
+ "int4": {
33
+ "granularity": "per_grouped_channel",
34
+ "group_size": 32,
35
+ "minimum_target": "iOS18/macOS15",
36
+ "mode": "KMEANS",
37
+ "path": "sv0_forms_int4_options40.mlpackage",
38
+ "precision": "int4_weights_float16_compute"
39
+ },
40
+ "int8": {
41
+ "granularity": "per_tensor",
42
+ "minimum_target": "iOS17/macOS14",
43
+ "mode": "UNIFORM",
44
+ "path": "sv0_forms_int8_options40.mlpackage",
45
+ "precision": "int8_weights_float16_compute"
46
+ }
47
+ }
48
+ }
coreml/sv0_forms_fp16_options40.mlpackage/Data/com.apple.CoreML/model.mlmodel ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:bfe30d2473f7c9a0d804f7b3306f252fcad4a307fa9202d7bf218049be41401a
3
+ size 58608
coreml/sv0_forms_fp16_options40.mlpackage/Data/com.apple.CoreML/weights/weight.bin ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:cf1b0db68a002fd003cf68278f5e8c049c9360b9b5b3754205692e20a63bb43c
3
+ size 1448256
coreml/sv0_forms_fp16_options40.mlpackage/Manifest.json ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "fileFormatVersion": "1.0.0",
3
+ "itemInfoEntries": {
4
+ "9B448D09-24D9-4E7E-B194-43D3D4AEE91F": {
5
+ "author": "com.apple.CoreML",
6
+ "description": "CoreML Model Specification",
7
+ "name": "model.mlmodel",
8
+ "path": "com.apple.CoreML/model.mlmodel"
9
+ },
10
+ "D3C625CB-29B6-456F-8FB3-B1111990E4E6": {
11
+ "author": "com.apple.CoreML",
12
+ "description": "CoreML Model Weights",
13
+ "name": "weights",
14
+ "path": "com.apple.CoreML/weights"
15
+ }
16
+ },
17
+ "rootModelIdentifier": "9B448D09-24D9-4E7E-B194-43D3D4AEE91F"
18
+ }
coreml/sv0_forms_int4_options40.mlpackage/Data/com.apple.CoreML/model.mlmodel ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:e145df71d2ee1a4566126487afd80cd6409aeda278d42494640f49dc17016168
3
+ size 58303
coreml/sv0_forms_int4_options40.mlpackage/Data/com.apple.CoreML/weights/weight.bin ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:53d889e570414557693f59309731d573aa6ddee95746dc9b9735a63afa2ad9ec
3
+ size 433664
coreml/sv0_forms_int4_options40.mlpackage/Manifest.json ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "fileFormatVersion": "1.0.0",
3
+ "itemInfoEntries": {
4
+ "495DA951-9B89-4CE4-8656-67B3D2E0038C": {
5
+ "author": "com.apple.CoreML",
6
+ "description": "CoreML Model Specification",
7
+ "name": "model.mlmodel",
8
+ "path": "com.apple.CoreML/model.mlmodel"
9
+ },
10
+ "7B688A87-1DA1-4178-9B4F-2F6A9FD6C274": {
11
+ "author": "com.apple.CoreML",
12
+ "description": "CoreML Model Weights",
13
+ "name": "weights",
14
+ "path": "com.apple.CoreML/weights"
15
+ }
16
+ },
17
+ "rootModelIdentifier": "495DA951-9B89-4CE4-8656-67B3D2E0038C"
18
+ }
coreml/sv0_forms_int8_options40.mlpackage/Data/com.apple.CoreML/model.mlmodel ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:4c63811d8b278b206c03cfc40c7d311a1e91997e9f710d524a1372477a28be74
3
+ size 62987
coreml/sv0_forms_int8_options40.mlpackage/Data/com.apple.CoreML/weights/weight.bin ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:36c31c5e396138a45eb191fec6f637d3831ffaca6005ebed12b305979be6b732
3
+ size 742528
coreml/sv0_forms_int8_options40.mlpackage/Manifest.json ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "fileFormatVersion": "1.0.0",
3
+ "itemInfoEntries": {
4
+ "BAE712EF-2551-49DE-BA14-3D8AB687D268": {
5
+ "author": "com.apple.CoreML",
6
+ "description": "CoreML Model Weights",
7
+ "name": "weights",
8
+ "path": "com.apple.CoreML/weights"
9
+ },
10
+ "E219151B-08B6-4AFD-BFEA-72D3F71825A6": {
11
+ "author": "com.apple.CoreML",
12
+ "description": "CoreML Model Specification",
13
+ "name": "model.mlmodel",
14
+ "path": "com.apple.CoreML/model.mlmodel"
15
+ }
16
+ },
17
+ "rootModelIdentifier": "E219151B-08B6-4AFD-BFEA-72D3F71825A6"
18
+ }
eval/RESULTS-SV0-COREML.md ADDED
@@ -0,0 +1,94 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # RESULTS-SV0-COREML — the Swedish SV0 specialist exported to Core ML
2
+
3
+ **Verdict: PARTIAL — fp16 and int8 export cleanly and run on the Neural Engine at the same
4
+ accuracy as PyTorch; int4 is _not_ usable for this checkpoint (83 % → 51 % top-1) and does not
5
+ compile for the ANE.** Recommended deployment artifact: **int8, 787 KB, ~1.3 ms per decision.**
6
+
7
+ ## What was exported
8
+
9
+ `export_sv_coreml.py` in this directory converts `runs/sv-tinyx/model` (the SV0 checkpoint,
10
+ 706 048 parameters) with coremltools 9.0, mirroring the fixed-shape contract of the
11
+ FluidInference CUA-S1 release: `context_ids (1, 224)` / `option_ids (1, 40, 96)` /
12
+ `option_mask (1, 40)`, all int32 byte-ids (byte + 1, 0 = pad), output `logits (1, 40)`.
13
+ `max_options` is **40** here rather than their 32 because our corpus tops out at 37 options —
14
+ the ceiling is baked into the export, so it must be sized from the widest real input.
15
+
16
+ Variants: fp16 (iOS17), int8 (UNIFORM, per-tensor, iOS17), int4 (KMEANS, per-grouped-channel,
17
+ group 32, iOS18). `conversion.json` records hashes, config and tooling versions.
18
+
19
+ ## Export-time checks (all passed before any measurement)
20
+
21
+ | Check | Result |
22
+ | --- | --- |
23
+ | Export forward vs vendored model, 256 real rows, max abs logit difference | **0.0** (bit-identical) |
24
+ | Traced graph vs model, 64 real rows | max abs Δ 7.2 × 10⁻⁶, argmax agreement **1.0** |
25
+
26
+ ## Measurements (Apple M4, macOS 26.6.2; 2 312 scored rows after 3 warm-up rows; same grader as the spike)
27
+
28
+ | Variant | package | test top-1 | demo top-1 | silent skips (of 1 008 fills) | argmax parity vs PyTorch | mismatches | median | p95 |
29
+ | --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: |
30
+ | PyTorch (MPS), reference | 2 828 784 B | **83.02 %** | 86 % | 76 | — | — | — | — |
31
+ | **fp16, CPU + ANE** | 1 507 481 B (1.44 MB) | 83.00 % | 85.11 % | 76 | 0.99870 | 3 / 2 312 | 1.307 ms | 1.460 ms |
32
+ | fp16, CPU only | 1 507 481 B | 83.04 % | — | 76 | 0.99913 | 2 / 2 312 | 1.734 ms | 1.917 ms |
33
+ | **int8, CPU + ANE** | **806 132 B (787 KB)** | **83.09 %** | 85.11 % | 76 | 0.99611 | 9 / 2 312 | 1.316 ms | 1.402 ms |
34
+ | int4, CPU + ANE | 492 584 B (481 KB) | **50.87 %** | 27.66 % | 954 | 0.48702 | 1 186 / 2 312 | 1.968 ms | 2.099 ms |
35
+
36
+ Fills per-action on fp16: `fill` 75.40 %, `skip` 88.26 %, `check` 96.55 %, `click` 93.67 % —
37
+ within a few rows of the PyTorch run, i.e. the export is behaviour-preserving.
38
+
39
+ ## What the numbers say
40
+
41
+ 1. **The export works, and the ANE helps here too**: fp16 median 1.307 ms with the ANE vs
42
+ 1.734 ms CPU-only, p95 1.460 ms vs 1.917 ms. Smaller absolute win than on the English model
43
+ (whose weights are better conditioned), but the same direction.
44
+ 2. **int8 is free and is the artifact to ship**: 787 KB (52 % of fp16), top-1 83.09 % — one
45
+ decision *better* than PyTorch on this split — 9 argmax deviations out of 2 312, identical
46
+ silent-skip count, and the same ANE behaviour.
47
+ 3. **int4 does not transfer to an undertrained model.** On Cua's English checkpoint the same
48
+ kind of palettisation cost 14 decisions in 24 367 (0.06 pp); here it costs 1 186 in 2 312
49
+ (51 pp) and raises silent skips from 76 to 954. Two mechanisms are plausible and both point
50
+ the same way: a model that has not converged stores information in fine weight differences
51
+ that a 4-bit palette cannot represent, and the per-grouped-channel kmeans graph also fails
52
+ to compile for the ANE (`ANECCompile() FAILED`, seen repeatedly during save and load), which
53
+ is why its latency is worst despite being the smallest artifact. **Quantisation headroom is
54
+ a property of the training run, not of the architecture** — that is the transferable finding.
55
+ 4. **fp16 parity is 99.87 %, not 100 %**, and that is expected rather than alarming: three
56
+ rows change their argmax under fp16 rounding on a model whose decision margins are thin
57
+ (83 % top-1). On the well-trained English model the same conversion showed zero deviations.
58
+ Parity must therefore be *measured* per checkpoint, not assumed — exactly what this harness
59
+ does.
60
+
61
+ ## Pitfalls found while exporting (all cost a failed run)
62
+
63
+ - **`torch.jit.trace` under `torch.no_grad()` takes PyTorch's fused sparsity fast path**
64
+ (`torch._transformer_encoder_layer_fwd`), which coremltools cannot convert. Trace with grad
65
+ *enabled*.
66
+ - **The coremltools torch frontend has no `__or__` for bool tensors** → use
67
+ `torch.logical_or` / `torch.logical_not`. (`&`, `~` alone, and in-place `mask[:, 0] = True`
68
+ all fail; probe the op support in a 5-line script before rewriting a model.)
69
+ - **`clamp_min(1)` with a Python int trips `assert x.dtype == y.dtype`** in the frontend; pass a
70
+ same-dtype tensor constant.
71
+ - **`per_grouped_channel` palettisation requires an iOS18 deployment target**, so the int4
72
+ variant needs its own fp16 base converted at iOS18 (the upstream release does the same).
73
+ - **k-means palettisation needs `scikit-learn`**, which is not a coremltools dependency; the
74
+ error arrives mid-run, after the fp16 conversion has already succeeded — hence the
75
+ `REUSE_FP16=1` cache flag in the export script.
76
+ - **Palettizer API in coremltools 9.0**: wrap the op config —
77
+ `OptimizationConfig(global_config=OpPalettizerConfig(...))` — and note the mode names are
78
+ upper-case (`UNIFORM`, `KMEANS`) while granularity is `per_tensor` / `per_grouped_channel`
79
+ only.
80
+ - **Check that the traced graph is the model** before converting: `torch.jit.trace(..., check_trace=False)`
81
+ suppresses a noisy replay check, so the export script compares logits itself and aborts on
82
+ mismatch.
83
+
84
+ ## Reproduction
85
+
86
+ ```bash
87
+ # (in the spike venv with coremltools 9.0, torch 2.7, safetensors, scikit-learn)
88
+ python check_export_forward.py # export forward is bit-identical to the model
89
+ REUSE_FP16=1 python export_sv_coreml.py # fp16 + int8 + int4 mlpackages, conversion.json
90
+ python eval_sv_coreml.py # accuracy, silent skips, parity, latency -> RESULTS-sv0-coreml.json
91
+ ```
92
+
93
+ Artifacts (not in Git): `sv-coreml/*.mlpackage`, `RESULTS-sv0-coreml.json`,
94
+ `export.log`, `eval-coreml.log`.
eval/RESULTS-SV0-pytorch.md ADDED
@@ -0,0 +1,94 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # RESULTS-SV0 — Swedish Jev-shaped form specialist
2
+
3
+ **Verdict: VALIDATED, with documented limits.**
4
+ The Cua-S1 contract trains on our own Swedish synthetic data with the upstream MIT code,
5
+ on one laptop, and reaches **83.02 %** top-1 on a held-out, form-disjoint synthetic test
6
+ (2 315 decisions) and **86 %** on a hand-written out-of-distribution Swedish demo (50
7
+ decisions) — against **21.4 %** / **20 %** for the released English `cua-s1-forms`
8
+ checkpoint on the very same rows. The result is undertrained (validation was still
9
+ improving at the last epoch), so 83 % is a floor, not a ceiling.
10
+
11
+ ## Protocol as run
12
+
13
+ | Item | Setting |
14
+ | --- | --- |
15
+ | Architecture | `tinyx` (byte-level 2-layer Transformer encoder, width 128, 4 heads) + jevlike `AttentionHead`, context 224 / option 96 bytes — identical to upstream |
16
+ | Code | `trycua/cua` → `libs/cua-s1` @ `9bbfa7d`, vendored unmodified (`vendor/`, MIT) |
17
+ | Corpus | `sv_synth.py` + `concepts_sv.py` (ours): 900 episodes, seed 2026, splits by form field signature |
18
+ | Rows | train 21 306 / validation 2 855 / test 2 315 decisions; 725 / 96 / 79 distinct form signatures, **zero overlap** between splits |
19
+ | Training | AdamW lr 2e-3, cosine + 5 % warmup, wd 1e-2, 4 epochs, batch 64, seed 7, MPS, 1 927 s |
20
+ | Checkpoint rule | best validation NLL (upstream's rule) — epoch 4 |
21
+ | Grader | `eval_sv.py`, unchanged for both checkpoints |
22
+
23
+ ## Numbers
24
+
25
+ | Run | decisions | top-1 | majority baseline | ECE | skipped a required fill | per action |
26
+ | --- | ---: | ---: | ---: | ---: | ---: | --- |
27
+ | Swedish specialist — synthetic test (form-disjoint) | 2 315 | **83.02 %** | 50.45 % | 0.01656 | 76 | `check` 98.28 % (n=58), `click` 93.67 % (n=79), `fill` 75.55 % (n=1010), `skip` 88.01 % (n=1168) |
28
+ | Swedish specialist — handwritten OOD demo (50) | 50 | **86.00 %** | 64.00 % | 0.09176 | 3 | `check` 100 % (n=3), `click` 100 % (n=3), `fill` 81.25 % (n=32), `skip` 91.67 % (n=12) |
29
+ | Swedish specialist — shuffled-context control (test) | 2 315 | 34.08 % | 50.45 % | 0.49735 | 264 | `check` 15.52 %, `click` 0 %, `fill` 1.29 %, `skip` 65.67 % |
30
+ | Swedish specialist — shuffled-context control (demo) | 50 | 6.00 % | 64.00 % | 0.78579 | 6 | all actions ≤ 25 % |
31
+ | English reference (`cua-s1-forms`) — synthetic test | 2 315 | 21.38 % | 50.45 % | 0.64072 | 371 | `check` 93.10 %, `click` 26.58 %, `fill` 5.54 %, `skip` 31.16 % |
32
+ | English reference — handwritten OOD demo | 50 | 20.00 % | 64.00 % | 0.66098 | 16 | `check` 66.67 %, `click` 33.33 %, `fill` 15.62 %, `skip` 16.67 % |
33
+ | English reference — shuffled-context control (test) | 2 315 | 17.50 % | 50.45 % | 0.67954 | 363 | `check` 60.35 %, `click` 6.33 %, `fill` 1.78 %, `skip` 29.71 % |
34
+
35
+ Per-epoch validation: 48.23 % → 62.98 % → 78.63 % → **83.01 %** top-1 (train NLL
36
+ 2.06 → 0.45; val NLL 1.75 → 0.52). Still improving when the run stopped.
37
+
38
+ ## Reading the numbers
39
+
40
+ 1. **The contract transfers; the language boundary was the real barrier.** The English
41
+ checkpoint on Swedish rows sits at **21.4 %**, barely above its own shuffled-context
42
+ control (17.5 %) and *below* the majority-action baseline (50.5 %), with ECE 0.64 — it
43
+ is not reading the content, it is guessing confidently. On the hand-written demo it
44
+ scores 20 % against a 64 % baseline, i.e. worse than always answering "fill". The
45
+ Swedish specialist, same architecture, same grader, same rows: 83 % / 86 %.
46
+ 2. **The shuffled control is the floor that matters.** Our specialist drops from 83.02 %
47
+ to 34.08 % when contexts are rotated between rows (below the 50.45 % majority
48
+ baseline), which is the evidence that it uses the element label and the document, not
49
+ option statistics. The English reference barely moves (21.4 % → 17.5 %) — its score was
50
+ near the floor to begin with.
51
+ 3. **`fill` is the weak action, and the silent-skip failure mode persists.** 75.5 % of
52
+ fills are right; 76 of 1 010 fill decisions (7.5 %) are answered `hoppa över` instead.
53
+ In a real workflow that is the dangerous class: a required field that looks done and is
54
+ not. It is much smaller than the English reference's (371/1 010), but it is not zero,
55
+ and it is the reason a production integration needs a per-item check outside the model.
56
+ 4. **Calibration is good in-distribution and degrades out-of-distribution** (ECE 0.017 →
57
+ 0.092), which is exactly the wrong direction for confidence routing if the threshold is
58
+ tuned on synthetic data.
59
+ 5. **It is undertrained.** Both the validation curve and the comparison with the reference
60
+ (trained on ~150 k rows vs our 21 k) say the remaining error is largely a data-scale
61
+ effect. The next honest step is more episodes, not a different architecture.
62
+
63
+ ## What this does not establish
64
+
65
+ - Nothing about real Swedish forms. The demo set is hand-written by the same person who
66
+ wrote the catalogue, so it shares the author's vocabulary; a genuine test needs real
67
+ blanketter (Skolverket/Försäkringskassan/kommunala) or our own live forms.
68
+ - Nothing about the runtime: this spike scores decisions, it does not drive a GUI.
69
+ Fill/click execution, retries and fail-closed behaviour were not exercised (upstream's
70
+ `planner.py` refuses to execute without a compatible driver contract).
71
+ - Nothing about English transfer in the other direction: we never trained a Swedish model
72
+ on English rows, and the corpus changes *language and label vocabulary at once*, so the
73
+ ablation "language vs vocabulary" is still open.
74
+ - Reproducibility is bounded by the environment: torch 2.14 on Apple Silicon MPS, seed 7,
75
+ `deterministic=False` (deterministic algorithms were too slow on MPS), so the exact
76
+ numbers are one draw, not a bit-reproducible receipt.
77
+
78
+ ## Follow-ups worth pre-registering
79
+
80
+ 1. **Scale test**: 4 000–10 000 episodes, 6 epochs, batch 32–64 on a machine with room —
81
+ does top-1 cross ~95 % and does the silent-skip rate fall below 2 % of fills?
82
+ 2. **Language ✕ vocabulary ablation**: Swedish documents + English labels, and the mirror,
83
+ to attribute the win.
84
+ 3. **Real-form OOD**: hand-build 3–5 authentic Swedish blanketter (never in the catalogue)
85
+ and score before touching the corpus again.
86
+ 4. **Abstention outside the model**: a spread/top-probability gate plus a "required field
87
+ still empty" check, measured on the silent-skip class specifically.
88
+
89
+ ## Reproduction
90
+
91
+ See `README.md` in this directory. Runtime: ~5 min generation, 32 min training, seconds of
92
+ scoring on one Apple Silicon laptop. Corpus and checkpoint are not in Git
93
+ (`~/agent-data/jevlike-sv-spike/`); `data/sv/manifest.json` carries the SHA-256 of each split
94
+ (test `92c0dfec…d272`).
eval/corpus-manifest.json ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "episodes": 900,
3
+ "format": "cua-s1-choice-jsonl",
4
+ "format_version": 1,
5
+ "generator": {
6
+ "config": {
7
+ "filled_field_probability": 0.6,
8
+ "hard_negative_probability": 0.35,
9
+ "max_distractor_entities": 3,
10
+ "max_extra_entities": 5,
11
+ "missing_entity_probability": 0.12,
12
+ "partial_form_probability": 0.3,
13
+ "stale_value_probability": 0.05
14
+ },
15
+ "format": "spike.sv_synth",
16
+ "task_line": "UPPGIFT fyll i formul\u00e4ret fr\u00e5n dokumentet och skicka sedan in"
17
+ },
18
+ "ratios": {
19
+ "test": 0.1,
20
+ "train": 0.8,
21
+ "validation": 0.1
22
+ },
23
+ "seed": 2026,
24
+ "splits": {
25
+ "test": {
26
+ "episodes": 79,
27
+ "file": "test.jsonl",
28
+ "rows": 2315,
29
+ "sha256": "92c0dfec00239b6e1d5cc6db4f098d12fc90bc5a8832428d9df0f6aea380d272"
30
+ },
31
+ "train": {
32
+ "episodes": 725,
33
+ "file": "train.jsonl",
34
+ "rows": 21306,
35
+ "sha256": "b3a7ef25e4edb47b18be2601d1060a3589c7ed8563397fab25e27088017ee3fa"
36
+ },
37
+ "validation": {
38
+ "episodes": 96,
39
+ "file": "validation.jsonl",
40
+ "rows": 2855,
41
+ "sha256": "09298b6319b8471379b8f9e87f983558f5fa194b38271eac195670a8eb2ef5f1"
42
+ }
43
+ }
44
+ }
eval/results-coreml.json ADDED
@@ -0,0 +1,246 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "checkpoint": "runs/sv-tinyx/model",
3
+ "limits": {
4
+ "context_bytes": 224,
5
+ "max_options": 40,
6
+ "option_bytes": 96
7
+ },
8
+ "torch_baseline": {
9
+ "per_action": {
10
+ "check": {
11
+ "acc": 0.9827586206896551,
12
+ "n": 58
13
+ },
14
+ "click": {
15
+ "acc": 0.9367088607594937,
16
+ "n": 79
17
+ },
18
+ "fill": {
19
+ "acc": 0.7554455445544555,
20
+ "n": 1010
21
+ },
22
+ "skip": {
23
+ "acc": 0.8801369863013698,
24
+ "n": 1168
25
+ }
26
+ },
27
+ "rows": 2315,
28
+ "silent_skip_rate_of_fills": 0.07524752475247524,
29
+ "silent_skips": 76,
30
+ "top1": 0.8302375809935205
31
+ },
32
+ "variants": {
33
+ "fp16": {
34
+ "handwritten_demo__ane": {
35
+ "compute_units": "ComputeUnit.CPU_AND_NE",
36
+ "latency_ms_median": 1.2992918491363525,
37
+ "latency_ms_p95": 1.4392081648111343,
38
+ "mismatches_vs_torch": 0,
39
+ "parity_with_torch": 1.0,
40
+ "per_action": {
41
+ "check": {
42
+ "acc": 1.0,
43
+ "n": 3
44
+ },
45
+ "click": {
46
+ "acc": 1.0,
47
+ "n": 3
48
+ },
49
+ "fill": {
50
+ "acc": 0.7931034482758621,
51
+ "n": 29
52
+ },
53
+ "skip": {
54
+ "acc": 0.9166666666666666,
55
+ "n": 12
56
+ }
57
+ },
58
+ "rows": 47,
59
+ "silent_skip_rate_of_fills": 0.10344827586206896,
60
+ "silent_skips": 3,
61
+ "top1": 0.851063829787234
62
+ },
63
+ "package_bytes": 1507481,
64
+ "synthetic_test__ane": {
65
+ "compute_units": "ComputeUnit.CPU_AND_NE",
66
+ "latency_ms_median": 1.3071454595774412,
67
+ "latency_ms_p95": 1.4601247385144234,
68
+ "mismatches_vs_torch": 3,
69
+ "parity_with_torch": 0.9987024221453287,
70
+ "per_action": {
71
+ "check": {
72
+ "acc": 0.9827586206896551,
73
+ "n": 58
74
+ },
75
+ "click": {
76
+ "acc": 0.9367088607594937,
77
+ "n": 79
78
+ },
79
+ "fill": {
80
+ "acc": 0.753968253968254,
81
+ "n": 1008
82
+ },
83
+ "skip": {
84
+ "acc": 0.8808911739502999,
85
+ "n": 1167
86
+ }
87
+ },
88
+ "rows": 2312,
89
+ "silent_skip_rate_of_fills": 0.07539682539682539,
90
+ "silent_skips": 76,
91
+ "top1": 0.8300173010380623
92
+ },
93
+ "synthetic_test__cpu": {
94
+ "compute_units": "ComputeUnit.CPU_ONLY",
95
+ "latency_ms_median": 1.733562909066677,
96
+ "latency_ms_p95": 1.9167079590260983,
97
+ "mismatches_vs_torch": 2,
98
+ "parity_with_torch": 0.9991349480968859,
99
+ "per_action": {
100
+ "check": {
101
+ "acc": 0.9827586206896551,
102
+ "n": 58
103
+ },
104
+ "click": {
105
+ "acc": 0.9367088607594937,
106
+ "n": 79
107
+ },
108
+ "fill": {
109
+ "acc": 0.7549603174603174,
110
+ "n": 1008
111
+ },
112
+ "skip": {
113
+ "acc": 0.8808911739502999,
114
+ "n": 1167
115
+ }
116
+ },
117
+ "rows": 2312,
118
+ "silent_skip_rate_of_fills": 0.07539682539682539,
119
+ "silent_skips": 76,
120
+ "top1": 0.8304498269896193
121
+ }
122
+ },
123
+ "int4": {
124
+ "handwritten_demo__ane": {
125
+ "compute_units": "ComputeUnit.CPU_AND_NE",
126
+ "latency_ms_median": 1.8591247498989105,
127
+ "latency_ms_p95": 1.993041019886732,
128
+ "mismatches_vs_torch": 32,
129
+ "parity_with_torch": 0.3191489361702128,
130
+ "per_action": {
131
+ "check": {
132
+ "acc": 0.0,
133
+ "n": 3
134
+ },
135
+ "click": {
136
+ "acc": 0.0,
137
+ "n": 3
138
+ },
139
+ "fill": {
140
+ "acc": 0.034482758620689655,
141
+ "n": 29
142
+ },
143
+ "skip": {
144
+ "acc": 1.0,
145
+ "n": 12
146
+ }
147
+ },
148
+ "rows": 47,
149
+ "silent_skip_rate_of_fills": 0.9655172413793104,
150
+ "silent_skips": 28,
151
+ "top1": 0.2765957446808511
152
+ },
153
+ "package_bytes": 492584,
154
+ "synthetic_test__ane": {
155
+ "compute_units": "ComputeUnit.CPU_AND_NE",
156
+ "latency_ms_median": 1.9683334976434708,
157
+ "latency_ms_p95": 2.099000383168459,
158
+ "mismatches_vs_torch": 1186,
159
+ "parity_with_torch": 0.4870242214532872,
160
+ "per_action": {
161
+ "check": {
162
+ "acc": 0.0,
163
+ "n": 58
164
+ },
165
+ "click": {
166
+ "acc": 0.0,
167
+ "n": 79
168
+ },
169
+ "fill": {
170
+ "acc": 0.018849206349206348,
171
+ "n": 1008
172
+ },
173
+ "skip": {
174
+ "acc": 0.9914310197086547,
175
+ "n": 1167
176
+ }
177
+ },
178
+ "rows": 2312,
179
+ "silent_skip_rate_of_fills": 0.9464285714285714,
180
+ "silent_skips": 954,
181
+ "top1": 0.5086505190311419
182
+ }
183
+ },
184
+ "int8": {
185
+ "handwritten_demo__ane": {
186
+ "compute_units": "ComputeUnit.CPU_AND_NE",
187
+ "latency_ms_median": 1.31650036200881,
188
+ "latency_ms_p95": 1.4507090672850609,
189
+ "mismatches_vs_torch": 0,
190
+ "parity_with_torch": 1.0,
191
+ "per_action": {
192
+ "check": {
193
+ "acc": 1.0,
194
+ "n": 3
195
+ },
196
+ "click": {
197
+ "acc": 1.0,
198
+ "n": 3
199
+ },
200
+ "fill": {
201
+ "acc": 0.7931034482758621,
202
+ "n": 29
203
+ },
204
+ "skip": {
205
+ "acc": 0.9166666666666666,
206
+ "n": 12
207
+ }
208
+ },
209
+ "rows": 47,
210
+ "silent_skip_rate_of_fills": 0.10344827586206896,
211
+ "silent_skips": 3,
212
+ "top1": 0.851063829787234
213
+ },
214
+ "package_bytes": 806132,
215
+ "synthetic_test__ane": {
216
+ "compute_units": "ComputeUnit.CPU_AND_NE",
217
+ "latency_ms_median": 1.315583474934101,
218
+ "latency_ms_p95": 1.401625107973814,
219
+ "mismatches_vs_torch": 9,
220
+ "parity_with_torch": 0.9961072664359861,
221
+ "per_action": {
222
+ "check": {
223
+ "acc": 0.9827586206896551,
224
+ "n": 58
225
+ },
226
+ "click": {
227
+ "acc": 0.9367088607594937,
228
+ "n": 79
229
+ },
230
+ "fill": {
231
+ "acc": 0.753968253968254,
232
+ "n": 1008
233
+ },
234
+ "skip": {
235
+ "acc": 0.882604970008569,
236
+ "n": 1167
237
+ }
238
+ },
239
+ "rows": 2312,
240
+ "silent_skip_rate_of_fills": 0.07539682539682539,
241
+ "silent_skips": 76,
242
+ "top1": 0.8308823529411765
243
+ }
244
+ }
245
+ }
246
+ }
eval/results-pytorch.json ADDED
@@ -0,0 +1,282 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "device": "mps",
3
+ "reference": {
4
+ "checkpoint": "/tmp/cua1ckpt/cua-s1-forms.safetensors",
5
+ "checkpoint_bytes": 2828784,
6
+ "config": {
7
+ "context_tokens": 224,
8
+ "encoder": "tinyx",
9
+ "heads": 4,
10
+ "hf_model": "Qwen/Qwen2.5-0.5B",
11
+ "layers": 2,
12
+ "option_tokens": 96,
13
+ "rank": 128,
14
+ "width": 128
15
+ },
16
+ "label": "cua-s1-forms-english",
17
+ "sv_handwritten_demo": {
18
+ "action_mix": {
19
+ "check": 3,
20
+ "click": 3,
21
+ "fill": 32,
22
+ "skip": 12
23
+ },
24
+ "ece": 0.660977,
25
+ "examples": 50,
26
+ "majority_baseline": 0.64,
27
+ "per_action": {
28
+ "check": {
29
+ "acc": 0.66667,
30
+ "n": 3
31
+ },
32
+ "click": {
33
+ "acc": 0.33333,
34
+ "n": 3
35
+ },
36
+ "fill": {
37
+ "acc": 0.15625,
38
+ "n": 32
39
+ },
40
+ "skip": {
41
+ "acc": 0.16667,
42
+ "n": 12
43
+ }
44
+ },
45
+ "skipped_when_fill_expected": 16,
46
+ "top1": 0.2
47
+ },
48
+ "sv_handwritten_demo__shuffled_context": {
49
+ "action_mix": {
50
+ "check": 3,
51
+ "click": 3,
52
+ "fill": 32,
53
+ "skip": 12
54
+ },
55
+ "ece": 0.760258,
56
+ "examples": 50,
57
+ "majority_baseline": 0.64,
58
+ "per_action": {
59
+ "check": {
60
+ "acc": 0.66667,
61
+ "n": 3
62
+ },
63
+ "click": {
64
+ "acc": 0.0,
65
+ "n": 3
66
+ },
67
+ "fill": {
68
+ "acc": 0.0,
69
+ "n": 32
70
+ },
71
+ "skip": {
72
+ "acc": 0.25,
73
+ "n": 12
74
+ }
75
+ },
76
+ "skipped_when_fill_expected": 13,
77
+ "top1": 0.1
78
+ },
79
+ "sv_synthetic_test": {
80
+ "action_mix": {
81
+ "check": 58,
82
+ "click": 79,
83
+ "fill": 1010,
84
+ "skip": 1168
85
+ },
86
+ "ece": 0.640725,
87
+ "examples": 2315,
88
+ "majority_baseline": 0.50454,
89
+ "per_action": {
90
+ "check": {
91
+ "acc": 0.93103,
92
+ "n": 58
93
+ },
94
+ "click": {
95
+ "acc": 0.26582,
96
+ "n": 79
97
+ },
98
+ "fill": {
99
+ "acc": 0.05545,
100
+ "n": 1010
101
+ },
102
+ "skip": {
103
+ "acc": 0.31164,
104
+ "n": 1168
105
+ }
106
+ },
107
+ "skipped_when_fill_expected": 371,
108
+ "top1": 0.21382
109
+ },
110
+ "sv_synthetic_test__shuffled_context": {
111
+ "action_mix": {
112
+ "check": 58,
113
+ "click": 79,
114
+ "fill": 1010,
115
+ "skip": 1168
116
+ },
117
+ "ece": 0.679541,
118
+ "examples": 2315,
119
+ "majority_baseline": 0.50454,
120
+ "per_action": {
121
+ "check": {
122
+ "acc": 0.60345,
123
+ "n": 58
124
+ },
125
+ "click": {
126
+ "acc": 0.06329,
127
+ "n": 79
128
+ },
129
+ "fill": {
130
+ "acc": 0.01782,
131
+ "n": 1010
132
+ },
133
+ "skip": {
134
+ "acc": 0.29709,
135
+ "n": 1168
136
+ }
137
+ },
138
+ "skipped_when_fill_expected": 363,
139
+ "top1": 0.17495
140
+ },
141
+ "trainable_params": 706048
142
+ },
143
+ "sv": {
144
+ "checkpoint": "runs/sv-tinyx/model/model.safetensors",
145
+ "checkpoint_bytes": 2828784,
146
+ "config": {
147
+ "context_tokens": 224,
148
+ "encoder": "tinyx",
149
+ "heads": 4,
150
+ "layers": 2,
151
+ "option_tokens": 96,
152
+ "rank": 128,
153
+ "width": 128
154
+ },
155
+ "label": "swedish-specialist",
156
+ "sv_handwritten_demo": {
157
+ "action_mix": {
158
+ "check": 3,
159
+ "click": 3,
160
+ "fill": 32,
161
+ "skip": 12
162
+ },
163
+ "ece": 0.09176,
164
+ "examples": 50,
165
+ "majority_baseline": 0.64,
166
+ "per_action": {
167
+ "check": {
168
+ "acc": 1.0,
169
+ "n": 3
170
+ },
171
+ "click": {
172
+ "acc": 1.0,
173
+ "n": 3
174
+ },
175
+ "fill": {
176
+ "acc": 0.8125,
177
+ "n": 32
178
+ },
179
+ "skip": {
180
+ "acc": 0.91667,
181
+ "n": 12
182
+ }
183
+ },
184
+ "skipped_when_fill_expected": 3,
185
+ "top1": 0.86
186
+ },
187
+ "sv_handwritten_demo__shuffled_context": {
188
+ "action_mix": {
189
+ "check": 3,
190
+ "click": 3,
191
+ "fill": 32,
192
+ "skip": 12
193
+ },
194
+ "ece": 0.785789,
195
+ "examples": 50,
196
+ "majority_baseline": 0.64,
197
+ "per_action": {
198
+ "check": {
199
+ "acc": 0.0,
200
+ "n": 3
201
+ },
202
+ "click": {
203
+ "acc": 0.0,
204
+ "n": 3
205
+ },
206
+ "fill": {
207
+ "acc": 0.0,
208
+ "n": 32
209
+ },
210
+ "skip": {
211
+ "acc": 0.25,
212
+ "n": 12
213
+ }
214
+ },
215
+ "skipped_when_fill_expected": 6,
216
+ "top1": 0.06
217
+ },
218
+ "sv_synthetic_test": {
219
+ "action_mix": {
220
+ "check": 58,
221
+ "click": 79,
222
+ "fill": 1010,
223
+ "skip": 1168
224
+ },
225
+ "ece": 0.016565,
226
+ "examples": 2315,
227
+ "majority_baseline": 0.50454,
228
+ "per_action": {
229
+ "check": {
230
+ "acc": 0.98276,
231
+ "n": 58
232
+ },
233
+ "click": {
234
+ "acc": 0.93671,
235
+ "n": 79
236
+ },
237
+ "fill": {
238
+ "acc": 0.75545,
239
+ "n": 1010
240
+ },
241
+ "skip": {
242
+ "acc": 0.88014,
243
+ "n": 1168
244
+ }
245
+ },
246
+ "skipped_when_fill_expected": 76,
247
+ "top1": 0.83024
248
+ },
249
+ "sv_synthetic_test__shuffled_context": {
250
+ "action_mix": {
251
+ "check": 58,
252
+ "click": 79,
253
+ "fill": 1010,
254
+ "skip": 1168
255
+ },
256
+ "ece": 0.497346,
257
+ "examples": 2315,
258
+ "majority_baseline": 0.50454,
259
+ "per_action": {
260
+ "check": {
261
+ "acc": 0.15517,
262
+ "n": 58
263
+ },
264
+ "click": {
265
+ "acc": 0.0,
266
+ "n": 79
267
+ },
268
+ "fill": {
269
+ "acc": 0.01287,
270
+ "n": 1010
271
+ },
272
+ "skip": {
273
+ "acc": 0.65668,
274
+ "n": 1168
275
+ }
276
+ },
277
+ "skipped_when_fill_expected": 264,
278
+ "top1": 0.34082
279
+ },
280
+ "trainable_params": 706048
281
+ }
282
+ }
example.py ADDED
@@ -0,0 +1,109 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Minimal inference example for precisit/sv0-forms.
2
+
3
+ Two routes: the Core ML package (no PyTorch needed) and the PyTorch checkpoint through the
4
+ vendored Cua-S1 loader. Both take the same byte-level inputs; the encoder is:
5
+
6
+ ids = utf-8 bytes, truncated to the limit, each byte + 1, zero-padded
7
+ context: 224 bytes | option: 96 bytes | up to 40 options (the export's ceiling)
8
+
9
+ Run: python example.py (Core ML, needs coremltools)
10
+ python example.py --torch (PyTorch, needs torch + safetensors + the vendored package)
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import argparse
16
+ import json
17
+ from pathlib import Path
18
+
19
+ HERE = Path(__file__).resolve().parent
20
+ CONTEXT_BYTES, OPTION_BYTES, MAX_OPTIONS = 224, 96, 40
21
+
22
+ TASK = "UPPGIFT fyll i formuläret från dokumentet och skicka sedan in"
23
+ FORM = "Exempelkliniken - Ny patientregistrering"
24
+ ELEMENT = ('Edit "E-post" value=""',)
25
+ OPTIONS = [
26
+ "fyll Förnamn: Anna",
27
+ "fyll Efternamn: Lindqvist",
28
+ "fyll E-post: anna.lindqvist@exempel.invalid",
29
+ "fyll Telefon: 070-341 22 87",
30
+ "kryssa",
31
+ "klicka",
32
+ "hoppa över",
33
+ ]
34
+
35
+
36
+ def context_string() -> str:
37
+ role, label, state = ELEMENT[0], "E-post", 'value=""'
38
+ return f"{TASK}\nFORM {FORM}\nELEMENT {role} \"{label}\" {state}"
39
+
40
+
41
+ def byte_ids(text: str, length: int) -> list[int]:
42
+ return [byte + 1 for byte in text.encode("utf-8", errors="replace")[:length]]
43
+
44
+
45
+ def pad(ids: list[int], length: int) -> list[int]:
46
+ return ids + [0] * (length - len(ids))
47
+
48
+
49
+ def via_coreml() -> None:
50
+ import coremltools as ct
51
+ import numpy as np
52
+
53
+ package = HERE / "coreml" / "sv0_forms_int8_options40.mlpackage"
54
+ model = ct.models.MLModel(str(package), compute_units=ct.ComputeUnit.CPU_AND_NE)
55
+ context = np.array([pad(byte_ids(context_string(), CONTEXT_BYTES), CONTEXT_BYTES)], dtype=np.int32)
56
+ options = np.zeros((1, MAX_OPTIONS, OPTION_BYTES), dtype=np.int32)
57
+ for index, option in enumerate(OPTIONS):
58
+ encoded = pad(byte_ids(option, OPTION_BYTES), OPTION_BYTES)
59
+ options[0, index] = np.array(encoded, dtype=np.int32)
60
+ mask = np.zeros((1, MAX_OPTIONS), dtype=np.int32)
61
+ mask[0, : len(OPTIONS)] = 1
62
+ output = model.predict({"context_ids": context, "option_ids": options, "option_mask": mask})
63
+ scores = output["logits"][0][: len(OPTIONS)] # raw logits; softmax them if you need probabilities
64
+ best = int(scores.argmax())
65
+ print(f"chosen: {OPTIONS[best]!r} (logit {float(scores[best]):.3f})")
66
+ print("logits:", {option: round(float(score), 3) for option, score in zip(OPTIONS, scores)})
67
+
68
+
69
+ def via_torch() -> None:
70
+ import torch
71
+ from huggingface_hub import hf_hub_download
72
+
73
+ try:
74
+ from cua_s1.model import ChoiceExample, load_checkpoint, select_device # type: ignore
75
+ except ImportError: # pragma: no cover
76
+ raise SystemExit(
77
+ "The PyTorch route needs the vendored Cua-S1 loader: clone the toolkit repository "
78
+ "(precisit/onepass) and put its `vendor/` directory on sys.path, or import this "
79
+ "repository's Core ML packages instead — they need no PyTorch."
80
+ )
81
+
82
+ weights = HERE / "sv0-forms.safetensors"
83
+ if not weights.exists(): # when the script is run outside the repository
84
+ weights = Path(hf_hub_download("precisit/one-pass-sv-forms", "sv0-forms.safetensors"))
85
+ hf_hub_download("precisit/one-pass-sv-forms", "sv0-forms.json", local_dir=weights.parent)
86
+ device = select_device("auto")
87
+ model, collator, _ = load_checkpoint(weights, device)
88
+ batch = collator([ChoiceExample(context=context_string(), options=tuple(OPTIONS), label=0)])
89
+ with torch.no_grad():
90
+ scores = model({key: value.to(device) for key, value in batch.items()})[0].softmax(-1)
91
+ best = int(scores.argmax())
92
+ print(f"chosen: {OPTIONS[best]!r} (probability {float(scores[best]):.3f})")
93
+
94
+
95
+ def metadata() -> None:
96
+ print(json.dumps(json.loads((HERE / "sv0-forms.json").read_text(encoding="utf-8"))["metadata"], indent=2))
97
+
98
+
99
+ if __name__ == "__main__":
100
+ parser = argparse.ArgumentParser(description=__doc__)
101
+ parser.add_argument("--torch", action="store_true", help="use the PyTorch checkpoint instead of Core ML")
102
+ parser.add_argument("--metadata", action="store_true", help="print the training metadata and exit")
103
+ arguments = parser.parse_args()
104
+ if arguments.metadata:
105
+ metadata()
106
+ elif arguments.torch:
107
+ via_torch()
108
+ else:
109
+ via_coreml()
sv0-forms.json ADDED
@@ -0,0 +1,162 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "config": {
3
+ "context_tokens": 224,
4
+ "encoder": "tinyx",
5
+ "heads": 4,
6
+ "layers": 2,
7
+ "option_tokens": 96,
8
+ "rank": 128,
9
+ "width": 128
10
+ },
11
+ "format": "cua-s1",
12
+ "format_version": 1,
13
+ "metadata": {
14
+ "batch_size": 64,
15
+ "best_validation": {
16
+ "ece": 0.018766840075841174,
17
+ "examples": 2855,
18
+ "nll": 0.51966637646463,
19
+ "per_action": {
20
+ "check": {
21
+ "acc": 1.0,
22
+ "n": 87
23
+ },
24
+ "click": {
25
+ "acc": 0.9479166666666666,
26
+ "n": 96
27
+ },
28
+ "fill": {
29
+ "acc": 0.7756680731364276,
30
+ "n": 1422
31
+ },
32
+ "skip": {
33
+ "acc": 0.8712,
34
+ "n": 1250
35
+ }
36
+ },
37
+ "top1": 0.8301225900650024
38
+ },
39
+ "deterministic": false,
40
+ "epochs": 4,
41
+ "history": [
42
+ {
43
+ "epoch": 1,
44
+ "train_nll": 2.060510016020325,
45
+ "validation": {
46
+ "ece": 0.05491860159418138,
47
+ "examples": 2855,
48
+ "nll": 1.748936438351713,
49
+ "per_action": {
50
+ "check": {
51
+ "acc": 0.4367816091954023,
52
+ "n": 87
53
+ },
54
+ "click": {
55
+ "acc": 0.3958333333333333,
56
+ "n": 96
57
+ },
58
+ "fill": {
59
+ "acc": 0.06118143459915612,
60
+ "n": 1422
61
+ },
62
+ "skip": {
63
+ "acc": 0.9712,
64
+ "n": 1250
65
+ }
66
+ },
67
+ "top1": 0.4823117256164551
68
+ }
69
+ },
70
+ {
71
+ "epoch": 2,
72
+ "train_nll": 1.3498624871227647,
73
+ "validation": {
74
+ "ece": 0.029052662350295577,
75
+ "examples": 2855,
76
+ "nll": 1.1316383522153945,
77
+ "per_action": {
78
+ "check": {
79
+ "acc": 0.9770114942528736,
80
+ "n": 87
81
+ },
82
+ "click": {
83
+ "acc": 0.8854166666666666,
84
+ "n": 96
85
+ },
86
+ "fill": {
87
+ "acc": 0.4212376933895921,
88
+ "n": 1422
89
+ },
90
+ "skip": {
91
+ "acc": 0.8232,
92
+ "n": 1250
93
+ }
94
+ },
95
+ "top1": 0.6297723054885864
96
+ }
97
+ },
98
+ {
99
+ "epoch": 3,
100
+ "train_nll": 0.7656327178699136,
101
+ "validation": {
102
+ "ece": 0.02394736194401048,
103
+ "examples": 2855,
104
+ "nll": 0.6415376866970296,
105
+ "per_action": {
106
+ "check": {
107
+ "acc": 0.9770114942528736,
108
+ "n": 87
109
+ },
110
+ "click": {
111
+ "acc": 0.9791666666666666,
112
+ "n": 96
113
+ },
114
+ "fill": {
115
+ "acc": 0.7088607594936709,
116
+ "n": 1422
117
+ },
118
+ "skip": {
119
+ "acc": 0.8464,
120
+ "n": 1250
121
+ }
122
+ },
123
+ "top1": 0.7863397598266602
124
+ }
125
+ },
126
+ {
127
+ "epoch": 4,
128
+ "train_nll": 0.4496109453594249,
129
+ "validation": {
130
+ "ece": 0.018766840075841174,
131
+ "examples": 2855,
132
+ "nll": 0.51966637646463,
133
+ "per_action": {
134
+ "check": {
135
+ "acc": 1.0,
136
+ "n": 87
137
+ },
138
+ "click": {
139
+ "acc": 0.9479166666666666,
140
+ "n": 96
141
+ },
142
+ "fill": {
143
+ "acc": 0.7756680731364276,
144
+ "n": 1422
145
+ },
146
+ "skip": {
147
+ "acc": 0.8712,
148
+ "n": 1250
149
+ }
150
+ },
151
+ "top1": 0.8301225900650024
152
+ }
153
+ }
154
+ ],
155
+ "learning_rate": 0.002,
156
+ "seed": 7,
157
+ "task": "form-specialist-choice-classification",
158
+ "warmup": 0.05,
159
+ "workers": 0
160
+ },
161
+ "state_signature": "86e836863df8a0b0caa34ac5eab18bcd4fb44e420f83e271c96379fac0b0aec4"
162
+ }
sv0-forms.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:b9030d7a7b7abfaf70dd1cc2a390568eaa3a55849daa0757b60d01dbab162356
3
+ size 2828784